Files
Genarrative/apps/ai-game-creator-shell/tests/appUpdate.test.ts
T
kdletters e3b4127440
Project CI / AI game creator shell Rust crates (push) Successful in 1m52s
Project CI / AI game creator shell Rust smoke (push) Successful in 2m47s
Project CI / Backend tests (push) Failing after 4m0s
Project CI / Frontend tests (push) Successful in 1m59s
Project CI / Native shell tests (push) Successful in 6m19s
Project CI / Repository checks (push) Successful in 2m43s
Project CI / AI game creator shell web tests (push) Successful in 1m47s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 11m5s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 11m19s
统一 AGC 失败兜底口径:兜底只用专用错误码与专用文案
新增 homeCreationFailure 统一创建失败出口:宿主机器码 / IPC 失联 / 专用未分类兜底三种文案,宿主原始文本改为 appendWebviewLog 落 application.log 而不上屏
首页创建失败不再无条件覆盖成「创建未完成,请重试」,也不再写死「首条需求投递失败」指认某一步;工作区已建好时保留该事实并提供「已创建的工作区」恢复说法
宿主预检按真实原因分流浏览器验证错误码(browser-missing / launch-timeout / launch-failed / cancelled / browser-cleanup-failed / browser-environment-failed / evidence-failed / input-invalid / page-check-failed),未识别落回专用 web-preflight-unclassified
依赖准备收据保留宿主返回的真实失败码,不再一律写成 web-scaffold-preparation-failed
直连回合失败分类器 else 改为专用 runtime-unclassified,并补 sidecar-turn-timeout / sidecar-launch-failed / app-server-launch-failed / claude-code-failed 四个有证据的具体码
Codex CLI 与 Claude sidecar 的进程启动失败保留真实 OS 错误,不再报成「不支持 app-server 子命令」或「Node runtime 不可用」
更新检查新增结果型入口 checkForAppUpdateResult,「关于」页检查失败显示真实原因而不是「当前已是最新版本」
首页无码预检兜底文案不再写「请检查 Node/npm 或浏览器」这类未证实的候选原因
同步 pitfalls 六条与团队约定:兜底码/兜底文案规则、五项修复的现象根因处理验证、测试夹具编译修复记录
验证:vitest homeCreationFailure/homeWebPreflight/appUpdate/appSurface 共 215 passed;cargo test environment_check:: 19 passed、agent::runtime_error:: 5 passed、direct_failure_diagnostic 4 passed、tests::configuration:: 45 passed;cargo fmt --check、tsc --noEmit、npm run typecheck、check:encoding、git diff --check 通过
2026-10-02 13:07:00 +08:00

206 lines
7.6 KiB
TypeScript

import { check } from '@tauri-apps/plugin-updater';
import { afterEach, describe, expect, it, vi } from 'vitest';
vi.mock('@tauri-apps/plugin-updater', () => ({ check: vi.fn() }));
const checkMock = vi.mocked(check);
type FakeDownloadEvent =
| { event: 'Started'; data: { contentLength?: number } }
| { event: 'Progress'; data: { chunkLength: number } }
| { event: 'Finished' };
function fakeUpdate() {
return {
version: '99.0.0',
currentVersion: '0.1.47',
body: '修复与改进',
downloadAndInstall: vi.fn(
async (onEvent: (event: FakeDownloadEvent) => void) => {
onEvent({ event: 'Started', data: { contentLength: 100 } });
onEvent({ event: 'Progress', data: { chunkLength: 40 } });
onEvent({ event: 'Progress', data: { chunkLength: 60 } });
onEvent({ event: 'Finished' });
},
),
};
}
function stubTauriWindow() {
const invoke = vi.fn(async () => undefined);
vi.stubGlobal('window', { __TAURI__: { core: { invoke } } });
return invoke;
}
afterEach(() => {
vi.unstubAllEnvs();
vi.unstubAllGlobals();
vi.resetModules();
checkMock.mockReset();
});
describe('AGC 客户端更新', () => {
it('开发态开关关闭时不请求清单', async () => {
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '0');
vi.resetModules();
const { checkForAppUpdate, resetAppUpdateCheckForTests } =
await import('../src/services/appUpdate');
await expect(checkForAppUpdate()).resolves.toBeNull();
expect(checkMock).not.toHaveBeenCalled();
resetAppUpdateCheckForTests();
});
it('开关打开时把插件返回的更新映射给界面,且同一生命周期只查一次', async () => {
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
vi.resetModules();
checkMock.mockResolvedValue(fakeUpdate() as never);
const { checkForAppUpdate, resetAppUpdateCheckForTests } =
await import('../src/services/appUpdate');
await expect(checkForAppUpdate()).resolves.toEqual({
version: '99.0.0',
currentVersion: '0.1.47',
releaseNotes: '修复与改进',
});
await checkForAppUpdate();
expect(checkMock).toHaveBeenCalledTimes(1);
resetAppUpdateCheckForTests();
});
it('清单缺失或网络失败时启动期静默,但结果型入口如实报失败', async () => {
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
vi.resetModules();
checkMock.mockRejectedValue(new Error('updater: manifest 404'));
const {
checkForAppUpdate,
checkForAppUpdateResult,
resetAppUpdateCheckForTests,
} = await import('../src/services/appUpdate');
// 启动期提示用旧投影:失败仍然静默,不阻塞启动、不弹更新。
await expect(checkForAppUpdate()).resolves.toBeNull();
// 手动检查必须拿到失败原因,不能显示成「当前已是最新版本」。
await expect(checkForAppUpdateResult({ force: true })).resolves.toEqual({
status: 'failed',
message: 'updater: manifest 404',
});
resetAppUpdateCheckForTests();
});
it('结果型入口区分「有更新」与「已是最新」', async () => {
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
vi.resetModules();
checkMock.mockResolvedValueOnce(null as never);
const { checkForAppUpdateResult, resetAppUpdateCheckForTests } =
await import('../src/services/appUpdate');
await expect(checkForAppUpdateResult()).resolves.toEqual({
status: 'current',
});
checkMock.mockResolvedValueOnce(fakeUpdate() as never);
await expect(checkForAppUpdateResult({ force: true })).resolves.toEqual({
status: 'available',
update: {
version: '99.0.0',
currentVersion: '0.1.47',
releaseNotes: '修复与改进',
},
});
resetAppUpdateCheckForTests();
});
it('开关关闭时结果型入口报 disabled,不冒充已是最新', async () => {
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '0');
vi.resetModules();
const { checkForAppUpdateResult, resetAppUpdateCheckForTests } =
await import('../src/services/appUpdate');
await expect(checkForAppUpdateResult()).resolves.toEqual({
status: 'disabled',
});
expect(checkMock).not.toHaveBeenCalled();
resetAppUpdateCheckForTests();
});
it('安装时按下载事件上报进度并在完成后重启进程', async () => {
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
vi.resetModules();
const invoke = stubTauriWindow();
const update = fakeUpdate();
checkMock.mockResolvedValue(update as never);
const { checkForAppUpdate, installAppUpdate, resetAppUpdateCheckForTests } =
await import('../src/services/appUpdate');
await checkForAppUpdate();
const progress: Array<{ downloadedBytes: number; totalBytes?: number }> =
[];
await installAppUpdate((value) => progress.push(value));
expect(update.downloadAndInstall).toHaveBeenCalledTimes(1);
expect(progress).toEqual([
{ downloadedBytes: 0, totalBytes: 100 },
{ downloadedBytes: 40, totalBytes: 100 },
{ downloadedBytes: 100, totalBytes: 100 },
{ downloadedBytes: 100, totalBytes: 100 },
]);
expect(invoke).toHaveBeenCalledWith('restart_agc_app');
resetAppUpdateCheckForTests();
});
it('没有待安装更新时安装请求失败关闭', async () => {
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
vi.resetModules();
const { installAppUpdate, resetAppUpdateCheckForTests } =
await import('../src/services/appUpdate');
await expect(installAppUpdate()).rejects.toThrow('没有可安装的更新');
resetAppUpdateCheckForTests();
});
it('签名校验失败时拒绝安装、不重启进程,并保留待装更新供重试', async () => {
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
vi.resetModules();
const invoke = stubTauriWindow();
const update = fakeUpdate();
// 官方插件在原生侧校验签名;校验不过时它拒绝安装并抛错。客户端不得把这次失败
// 当成安装成功(更不能重启进程把坏包"坐实"),同时要保留待装更新让重试仍走同一条链路。
update.downloadAndInstall.mockRejectedValueOnce(
new Error('signature verification failed: invalid minisign signature'),
);
checkMock.mockResolvedValue(update as never);
const { checkForAppUpdate, installAppUpdate, resetAppUpdateCheckForTests } =
await import('../src/services/appUpdate');
await checkForAppUpdate();
await expect(installAppUpdate()).rejects.toThrow(
'signature verification failed',
);
expect(invoke).not.toHaveBeenCalledWith('restart_agc_app');
// 待装更新仍在:重试会再次走安装链路,成功后才会重启。
await installAppUpdate();
expect(update.downloadAndInstall).toHaveBeenCalledTimes(2);
expect(invoke).toHaveBeenCalledWith('restart_agc_app');
resetAppUpdateCheckForTests();
});
it('下载失败后仍可重试安装', async () => {
vi.stubEnv('VITE_AGC_ENABLE_APP_UPDATE_CHECK', '1');
vi.resetModules();
const update = fakeUpdate();
update.downloadAndInstall.mockRejectedValue(new Error('下载更新失败'));
checkMock.mockResolvedValue(update as never);
const { checkForAppUpdate, installAppUpdate, resetAppUpdateCheckForTests } =
await import('../src/services/appUpdate');
await checkForAppUpdate();
await expect(installAppUpdate()).rejects.toThrow('下载更新失败');
// 重试仍能拿到待装更新,而不是报“没有可安装的更新”。
await expect(installAppUpdate()).rejects.toThrow('下载更新失败');
expect(update.downloadAndInstall).toHaveBeenCalledTimes(2);
resetAppUpdateCheckForTests();
});
});