chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,413 @@
/**
* 单元测试: autoUpdater - 安装类型检测
*
* 测试 Windows 安装类型检测逻辑NSIS vs MSI
* 通过 mock electron 和 fs 模块验证 getInstallerType() / canAutoUpdate() 的行为
*
* 注意: detectInstallerType 是私有函数,但 getInstallerType / canAutoUpdate 已导出
* 每个测试重置模块缓存以清除 cachedInstallerType
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
// ── Mocks ──
const mockExistsSync = vi.fn((...args: unknown[]) => false);
const mockReaddirSync = vi.fn((...args: unknown[]) => [] as string[]);
vi.mock("fs", () => ({
existsSync: (...args: unknown[]) => mockExistsSync(...args),
readdirSync: (...args: unknown[]) => mockReaddirSync(...args),
}));
vi.mock("electron-log", () => ({
default: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
vi.mock("electron", () => ({
app: {
isPackaged: true,
getName: () => "QimingClaw",
getPath: (name: string) => {
if (name === "exe")
return "C:\\Users\\user\\AppData\\Local\\Programs\\QimingClaw\\QimingClaw.exe";
return "";
},
getAppPath: () => "/app",
getVersion: () => "0.9.4",
},
BrowserWindow: class {},
shell: {},
dialog: {},
net: {},
}));
vi.mock("electron-updater", () => ({
autoUpdater: {
on: vi.fn(),
setFeedURL: vi.fn(),
checkForUpdates: vi.fn(),
downloadUpdate: vi.fn(),
quitAndInstall: vi.fn(),
autoDownload: false,
autoInstallOnAppQuit: true,
},
}));
vi.mock("@shared/constants", () => ({
APP_DATA_DIR_NAME: ".qimingclaw",
}));
vi.mock("../db", () => ({
readSetting: vi.fn(),
}));
vi.mock("./i18n", () => ({
t: (key: string) => key,
}));
vi.mock("./updatePlatformUtils", () => ({
getWindowsDownloadUrl: vi.fn(),
getMacosDownloadUrl: vi.fn(),
getLinuxDownloadUrl: vi.fn(),
}));
// ── Helper ──
/** 重置模块缓存并重新导入,确保 cachedInstallerType 被清除 */
async function importFresh() {
vi.resetModules();
return import("./autoUpdater");
}
/** 让 process.platform 模拟为 win32 */
function mockWin32() {
Object.defineProperty(process, "platform", {
value: "win32",
configurable: true,
});
}
/** 让 process.platform 模拟为 darwin */
function mockDarwin() {
Object.defineProperty(process, "platform", {
value: "darwin",
configurable: true,
});
}
/** 让 process.platform 模拟为 linux */
function mockLinux() {
Object.defineProperty(process, "platform", {
value: "linux",
configurable: true,
});
}
/** 恢复 platform */
function restorePlatform() {
// vitest 在 Node 中 process.platform 原值已丢失,记录初始值
// 这里不做恢复,每个测试自己设 platform
}
// ── Tests ──
describe("autoUpdater - getInstallerType & canAutoUpdate", () => {
beforeEach(() => {
mockExistsSync.mockReset();
mockReaddirSync.mockReset();
mockExistsSync.mockReturnValue(false);
mockReaddirSync.mockReturnValue([]);
});
describe("macOS / Linux 平台", () => {
it("macOS 应返回 'mac'", async () => {
mockDarwin();
const { getInstallerType } = await importFresh();
expect(getInstallerType()).toBe("mac");
});
it("Linux 应返回 'linux'", async () => {
mockLinux();
const { getInstallerType } = await importFresh();
expect(getInstallerType()).toBe("linux");
});
});
describe("Windows NSIS 检测", () => {
beforeEach(() => {
mockWin32();
});
it("标准 NSIS: 存在 'Uninstall QimingClaw.exe' 应返回 'nsis'", async () => {
mockExistsSync.mockImplementation((...args: unknown[]) =>
String(args[0]).includes("Uninstall QimingClaw.exe"),
);
const { getInstallerType } = await importFresh();
expect(getInstallerType()).toBe("nsis");
});
it("通用 NSIS: 存在 unins000.exe 应返回 'nsis'", async () => {
mockReaddirSync.mockReturnValue(["app.exe", "resources", "unins000.exe"]);
const { getInstallerType } = await importFresh();
expect(getInstallerType()).toBe("nsis");
});
it("通用 NSIS: unins001.exe 也应返回 'nsis'", async () => {
mockReaddirSync.mockReturnValue(["app.exe", "unins001.exe"]);
const { getInstallerType } = await importFresh();
expect(getInstallerType()).toBe("nsis");
});
it("方式3: Uninstall.exe (无产品名) 应返回 'nsis'", async () => {
mockReaddirSync.mockReturnValue(["app.exe", "Uninstall.exe"]);
const { getInstallerType } = await importFresh();
expect(getInstallerType()).toBe("nsis");
});
it("方式3: unins.exe 应返回 'nsis'", async () => {
mockReaddirSync.mockReturnValue(["app.exe", "unins.exe"]);
const { getInstallerType } = await importFresh();
expect(getInstallerType()).toBe("nsis");
});
it("优先级: 标准 NSIS 优先于通用 NSIS", async () => {
mockExistsSync.mockImplementation((...args: unknown[]) =>
String(args[0]).includes("Uninstall QimingClaw.exe"),
);
// 即使目录中也有 unins000.exe应该先走 existsSync 的标准检测
mockReaddirSync.mockReturnValue(["app.exe", "unins000.exe"]);
const { getInstallerType } = await importFresh();
expect(getInstallerType()).toBe("nsis");
// readdirSync 不应被调用(标准检测先命中)
expect(mockReaddirSync).not.toHaveBeenCalled();
});
});
describe("Windows MSI 检测 (fallback)", () => {
beforeEach(() => {
mockWin32();
});
it("目录中无卸载程序文件应返回 'msi'", async () => {
mockReaddirSync.mockReturnValue(["app.exe", "resources", "locales"]);
const { getInstallerType } = await importFresh();
expect(getInstallerType()).toBe("msi");
});
it("readdirSync 抛出异常应 fallback 为 'msi'", async () => {
mockReaddirSync.mockImplementation(() => {
throw new Error("ENOENT");
});
const { getInstallerType } = await importFresh();
expect(getInstallerType()).toBe("msi");
});
it("目录为空应返回 'msi'", async () => {
mockReaddirSync.mockReturnValue([]);
const { getInstallerType } = await importFresh();
expect(getInstallerType()).toBe("msi");
});
it("不应将无关文件误判为 NSIS", async () => {
mockReaddirSync.mockReturnValue([
"app.exe",
"qimingclaw.exe",
"setup.exe",
"uninstall.dat",
"uninstall", // 无扩展名
"helper.dll",
]);
const { getInstallerType } = await importFresh();
expect(getInstallerType()).toBe("msi");
});
});
describe("canAutoUpdate", () => {
it("NSIS 应支持自动更新", async () => {
mockWin32();
mockExistsSync.mockImplementation((...args: unknown[]) =>
String(args[0]).includes("Uninstall QimingClaw.exe"),
);
const { canAutoUpdate } = await importFresh();
expect(canAutoUpdate()).toBe(true);
});
it("MSI 不支持自动更新", async () => {
mockWin32();
mockReaddirSync.mockReturnValue(["app.exe"]);
const { canAutoUpdate } = await importFresh();
expect(canAutoUpdate()).toBe(false);
});
it("macOS 应支持自动更新", async () => {
mockDarwin();
const { canAutoUpdate } = await importFresh();
expect(canAutoUpdate()).toBe(true);
});
});
});
// ── yml URL 处理逻辑测试 ──
// 测试 electron-updater generic provider 的 URL 构造行为:
// setFeedURL({ provider: "generic", url: "https://.../dir/" }) 会自动拼接 {channel}.yml
// 因此传入的 URL 必须是目录路径(以 / 结尾),而不是文件 URL
describe("autoUpdater - yml URL 处理", () => {
// 纯函数测试:模拟 doCheckViaLatestJson 中的 URL 推导逻辑
function deriveFeedUrl(params: {
ymlUrl: string | null;
updateChannel: "stable" | "beta";
version: string;
}): string {
const OSS_BASE =
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron";
const { ymlUrl, updateChannel, version } = params;
// 从 yml 文件 URL 提取目录路径electron-updater generic provider 期望目录 URL
const ymlDir = ymlUrl ? ymlUrl.replace(/\/[^/]+\.yml$/, "/") : null;
return ymlDir
? ymlDir
: updateChannel === "beta"
? `${OSS_BASE}/beta-build/prerelease-v${version}`
: `${OSS_BASE}/electron-v${version}`;
}
describe("yml 文件 URL → 目录 URL 转换(供 electron-updater generic provider 使用)", () => {
it("Windows: yml 文件 URL 应提取为目录 URL", () => {
const ymlUrl =
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron/beta-build/prerelease-v0.10.7/latest.yml";
const result = deriveFeedUrl({
ymlUrl,
updateChannel: "beta",
version: "0.10.7",
});
expect(result).toBe(
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron/beta-build/prerelease-v0.10.7/",
);
});
it("macOS: yml 文件 URL 应提取为目录 URL", () => {
const ymlUrl =
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron/beta-build/prerelease-v0.10.7/latest-mac.yml";
const result = deriveFeedUrl({
ymlUrl,
updateChannel: "beta",
version: "0.10.7",
});
expect(result).toBe(
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron/beta-build/prerelease-v0.10.7/",
);
});
it("Linux: yml 文件 URL 应提取为目录 URL", () => {
const ymlUrl =
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron/beta-build/prerelease-v0.10.7/latest-linux.yml";
const result = deriveFeedUrl({
ymlUrl,
updateChannel: "beta",
version: "0.10.7",
});
expect(result).toBe(
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron/beta-build/prerelease-v0.10.7/",
);
});
it("stable 通道: yml 文件 URL 应提取为目录 URL", () => {
const ymlUrl =
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron/electron-v0.10.7/latest.yml";
const result = deriveFeedUrl({
ymlUrl,
updateChannel: "stable",
version: "0.10.7",
});
expect(result).toBe(
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron/electron-v0.10.7/",
);
});
});
describe("降级逻辑yml 字段缺失时的 fallback", () => {
it("yml 字段缺失时beta 通道应使用 beta-build/prerelease-v{version} 路径", () => {
const result = deriveFeedUrl({
ymlUrl: null,
updateChannel: "beta",
version: "0.10.7",
});
expect(result).toBe(
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron/beta-build/prerelease-v0.10.7",
);
});
it("yml 字段缺失时stable 通道应使用 electron-v{version} 路径", () => {
const result = deriveFeedUrl({
ymlUrl: null,
updateChannel: "stable",
version: "0.10.7",
});
expect(result).toBe(
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron/electron-v0.10.7",
);
});
});
describe("electron-updater generic provider URL 拼接验证", () => {
// 模拟 GenericProvider 的 URL 构造行为:
// new URL(channelFile, newBaseUrl(directoryUrl))
// newBaseUrl 会确保目录 URL 以 / 结尾
function simulateGenericProvider(
channelFile: string,
feedUrl: string,
): string {
const url = new URL(feedUrl);
if (!url.pathname.endsWith("/")) {
url.pathname += "/";
}
return new URL(channelFile, url).toString();
}
it("目录 URL 传给 electron-updater 应拼接出正确的 yml 文件路径", () => {
const feedUrl =
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron/beta-build/prerelease-v0.10.7/";
const windowsResult = simulateGenericProvider("latest.yml", feedUrl);
const macResult = simulateGenericProvider("latest-mac.yml", feedUrl);
const linuxResult = simulateGenericProvider("latest-linux.yml", feedUrl);
expect(windowsResult).toBe(
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron/beta-build/prerelease-v0.10.7/latest.yml",
);
expect(macResult).toBe(
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron/beta-build/prerelease-v0.10.7/latest-mac.yml",
);
expect(linuxResult).toBe(
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron/beta-build/prerelease-v0.10.7/latest-linux.yml",
);
});
it("文件 URL 传给 electron-updater 会导致路径重复Bug 演示)", () => {
// 这是之前错误的做法:直接传文件 URL
const fileUrl =
"https://qiming-packages.oss-rg-china-mainland.aliyuncs.com/qimingclaw-electron/beta-build/prerelease-v0.10.7/latest.yml";
const result = simulateGenericProvider("latest.yml", fileUrl);
// URL 构造器会将 /latest.yml 当作文件名,拼接后变成 /beta-build/prerelease-v0.10.7//latest.yml/latest.yml
// 最终标准化为 .../beta-build/prerelease-v0.10.7/latest.yml/latest.yml路径重复
expect(result).toContain("latest.yml/latest.yml");
});
});
});