diff --git a/apps/ai-game-creator-shell/tests/appUpdate.test.ts b/apps/ai-game-creator-shell/tests/appUpdate.test.ts index bd89c3a3a..f17c8406c 100644 --- a/apps/ai-game-creator-shell/tests/appUpdate.test.ts +++ b/apps/ai-game-creator-shell/tests/appUpdate.test.ts @@ -118,6 +118,33 @@ describe('AGC 客户端更新', () => { 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(); diff --git a/docs/project-memory/plans/【里程碑】AGC客户端更新切换到官方更新插件-2026-09-17.md b/docs/project-memory/plans/【里程碑】AGC客户端更新切换到官方更新插件-2026-09-17.md index 025cae983..081b976d4 100644 --- a/docs/project-memory/plans/【里程碑】AGC客户端更新切换到官方更新插件-2026-09-17.md +++ b/docs/project-memory/plans/【里程碑】AGC客户端更新切换到官方更新插件-2026-09-17.md @@ -32,7 +32,7 @@ ## 验收标准 -- [ ] 正式包走官方更新插件的检查与安装路径;更新包校验失败时必须拒绝安装并清理临时文件。 +- [ ] 正式包走官方更新插件的检查与安装路径;更新包校验失败时必须拒绝安装并清理临时文件。(2026-09-29 补:客户端侧「校验失败不得当成安装成功」已有回归用例——插件拒装时 `installAppUpdate` 原样抛错、**不重启进程**、并保留待装更新让重试仍走同一链路(`apps/ai-game-creator-shell/tests/appUpdate.test.ts` 7 passed,含新增「签名校验失败时拒绝安装、不重启进程,并保留待装更新供重试」;变异验证:把 `restartAppAfterUpdate()` 挪到 `await downloadAndInstall` 之前,该用例立刻以 `expected "spy" to not be called with arguments: [ 'restart_agc_app' ]` 变红)。**仍未验收**:真机上的完整安装闭环(下载→插件验签→安装→重启后运行新版本)与插件侧临时文件清理——两者都在官方插件原生实现里,需要真实机器与已发布包。) - [x] 客户端只请求本渠道清单,且不因清单缺失、格式错误或网络失败阻塞启动。 - [x] 开发态启动不产生任何更新清单请求,也不显示更新入口。 - [x] 更新能力只授予客户端主窗口,其它窗口调用被拒绝。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 5741aa2c5..b68146b52 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -6124,4 +6124,11 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/` 复验(真 AGC CLI → 真 app-server → loopback Responses):`node apps/ai-game-creator-shell/scripts/direct-execution-production-fixture.mjs --agc-exe apps/ai-game-creator-shell/src-tauri/target/debug/genarrative-ai-game-creator-shell.exe --cases completed,passes,mcp,mcp-write,native,deadline` → **6/6 passed**;`completed` 用例的账本从 `phase=interrupted` 变成 `phase=completed`、`executorStopped=true`,`terminalReport` 变成宿主真实交付报告(「本轮已完成宿主验收…已通过:产物 marker.txt」),不再是「执行通道已断开」。定向单测:`cargo test … codex_app_server::execution` 13 passed、`cargo test … direct_execution` 26 passed。 - **保留的诊断开关**:关闭原因日志留在 `GENARRATIVE_AGC_DIRECT_DEBUG=1` 下(与既有的 `agent.direct_codex.stderr bytes=` 同一开关),以后排查「谁关掉的」不用再插桩。 - **顺带发现(2026-09-28)**:同一个夹具的 `restart` / `running` 用例证明——CLI / 无前端宿主**既不会把用户消息、也不会把最终助手回复写进 `.agent/conversations/project.jsonl`**(两者都由前端调 `append_direct_project_conversation_message` 写入,GUI 因此总有至少一条用户条目)。CLI 路径只落宿主侧条目(中间文本、工具卡片、工具回执),回复文本走 CLI 标准输出。写依赖"历史里一定有用户/助手消息"的断言前先确认是走 GUI 还是 CLI 路径,否则会得到假失败。宿主侧「正在跑」的可观测事实(`phase=working` + `active` 里的 `execute` 许可)两条路径一致,可以放心断言。 + +## 2026-09-29 更新安装的"重启"必须排在安装成功之后(否则会把坏包坐实) + +- **现象/风险**:客户端更新走官方插件 `downloadAndInstall()`;插件在原生侧验签,不过就拒装并抛错。如果服务端/前端把 `restart_agc_app` 放在 `await` 之前或 `finally` 里,签名不匹配、下载中断这类失败也会触发重启,用户看到的是一次"莫名其妙重启",而坏包状态被坐实。 +- **现状(正确)**:`src/services/appUpdate.ts` 的 `installAppUpdate` 先 `await update.downloadAndInstall(...)`、成功后才清空待装更新并 `restartAppAfterUpdate()`;失败时保留待装更新,重试走同一条链路。 +- **判据**:`apps/ai-game-creator-shell/tests/appUpdate.test.ts` 新增「签名校验失败时拒绝安装、不重启进程,并保留待装更新供重试」——插件抛 `signature verification failed` 时断言 ①错误原样上抛 ②`restart_agc_app` 未被调用 ③再次安装仍会走插件调用并在成功后重启。变异验证:把 `restartAppAfterUpdate()` 挪到 `await` 之前,该用例立即以 `expected "spy" to not be called with arguments: [ 'restart_agc_app' ]` 变红。 +- **边界**:真正的验签与临时文件清理都在官方插件原生实现里,本地只能证明"客户端不把失败当成功",真机安装闭环仍需已发布包与真实设备。 - **顺带记一条环境陷阱(2026-09-28 已修)**:`apps/ai-game-creator-shell/src-tauri/resources/codex/win-x64/` 下曾有两个 codex 二进制——`bin/codex.exe` 是**真正被解析**的那份(0.155.1),而包根目录那份 `codex.exe` 是 0.147.0 的旧残留(tauri 的 Windows 资源映射只引用 `bin/` 等路径),检查都查不出来,却会让本地核对误判「应用跑的是 0.147.0」。根因是 `src-tauri/build.rs` 的 `stage_codex_target()` 只按布局拷贝、从不清理目录,旧布局的组件会永久留在随包资源目录里。现在加了 `prune_stale_codex_components()`:拷贝前删掉不在本轮布局、也不在 `manifest.json`/`NOTICE.md` 白名单里的文件并收掉空目录;实测重建后根目录 `codex.exe` 被清掉、六个声明组件与清单/声明保留。