diff --git a/apps/ai-game-creator-shell/tests/projectResourceLiveUpdateModel.test.ts b/apps/ai-game-creator-shell/tests/projectResourceLiveUpdateModel.test.ts index 4459dc912..af4231096 100644 --- a/apps/ai-game-creator-shell/tests/projectResourceLiveUpdateModel.test.ts +++ b/apps/ai-game-creator-shell/tests/projectResourceLiveUpdateModel.test.ts @@ -340,6 +340,14 @@ describe('清单快照被拒收后的可见性与恢复', () => { }); }); +/** + * ⚠️ **覆盖边界**:下面这组断言打在生产函数 `uploadProjectAssetFilesAndReadSnapshot` 上 —— + * 它同时拥有「上传」与「配对读」,所以整条配对语义是真测的;但 AGC 侧**没有**"资源面板上传 → + * 拒收提示条"的端到端 UI 用例(现有 harness 只有聊天入口的 `/asset.upload` 通路,资源面板的 + * file input 没有用例,补一条要渲染整个 `ProjectDevelopmentView`)。因此「界面上不再出现拒收 + * 提示」这一层是**推理结论**(配对正确 ⇒ merge 判 accepted ⇒ 不产生拒收决策 ⇒ 不渲染提示条), + * 不是端到端断言。别把它当成端到端覆盖引用。 + */ describe('素材上传后的清单快照必须配对', () => { /** * 假后端:上传推进 revision,其余读命令返回当前磁盘状态。 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 13d6975c1..5632394f2 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -5308,4 +5308,14 @@ - **处理**:**每新增或改名一个源文件后,必须重跑一次完整 typecheck**;门禁顺序固定为「改完 → 全量 typecheck → 定向测试 → 其余门禁」。**不要**因为 pre-commit 绿或定向测试绿就认为类型没问题。 - **验证**:修复(`useRef`)后重跑 `npm run ai-game-creator-shell:typecheck` → `exit 0`;修复前同命令 `exit 2`。定向测试修复前后都是 `22 passed`——**说明测试通过完全推不出类型正确**。 - **推广**:同类漏检还包括未使用的 import、只在类型层违约的 props 不匹配、以及只有 `tsc` 才发现的 `.tsx` 泛型推断失败;这几类都不会让 eslint 或 vitest 变红。 -- **关联**:`apps/ai-game-creator-shell/src/view/project-development/ResourceFilterPanel.tsx`、`package.json` 的 `ai-game-creator-shell:typecheck` 与 `format:staged`(lint-staged)、提交 `5f03f050a`。 \ No newline at end of file +- **关联**:`apps/ai-game-creator-shell/src/view/project-development/ResourceFilterPanel.tsx`、`package.json` 的 `ai-game-creator-shell:typecheck` 与 `format:staged`(lint-staged)、提交 `5f03f050a`。 + +## 2026-09-11 素材上传不要把「上传前读到的 revision」贴到「上传后读到的清单」上 + +- **现象**:用户每上传一次素材,就先看到一条「资源清单更新被拒收(同一版本号上的清单内容不一致),正在重新读取磁盘清单…」。资源最终能出来,但每次上传都要多一轮读盘 + 一次误报。 +- **原因**:上传路径读盘与写盘的**配对错位**。代码在上传**前**读 `get_local_game_project_revision` 拿到 revision,上传完再把那个**旧 revision** 交给重载函数;重载函数自己重新读盘拿的是**新清单**,于是"旧版本号 + 新内容"被 `mergeProjectManifestSnapshot` 判成 `revision-conflict`(同 revision 不同指纹)。上传本身会推进 revision(Rust `advance_agent_runtime_project_revision_locked`),所以这个错配是**必然发生**而不是竞态。同一个提交里其实已经有正解 `rereadAuthoritativeProjectManifestSnapshot`(同一次读里配对 revision + 清单,并再读一次 revision 确认没被写盘插队),只是上传路径没用它。 +- **处理**:把「上传动作 + 配对读」收进**同一个函数** `uploadProjectAssetFilesAndReadSnapshot`,调用方拿不到中间那个 revision,也就没有机会贴错。凡是"命令会推进 revision、之后要重载清单"的路径都按这个形状办:**要么命令自己返回提交后的 revision,要么用配对读,绝不用写盘前读到的 revision。** +- **同类文案坑**:拒收后的恢复阶段 `unresolved` 有三条进入路径,其中两条**重读是成功的**(读到的一对比手上旧 / 读到的是撕裂的一对)。文案不能写成"重新读取磁盘清单失败"——那是误报;写"未能按磁盘清单重新对齐"才对所有路径都成立。 +- **验证**:`projectResourceLiveUpdateModel.test.ts` 用假后端(上传时推进 revision)断言 merge 判 `accepted` 且 `projectManifestMergeRejectionDecision === null`,并附一条对照钉子显式写出旧形状、断言它必判 `revision-conflict`。变异验证:把修法还原成旧形状 → 配对用例变红。 +- **覆盖边界(重要,别误读)**:上述断言打**在生产函数 `uploadProjectAssetFilesAndReadSnapshot` 上**,它同时拥有上传与配对读,所以整条配对语义被真测;但 **AGC 侧没有"资源面板上传 → 拒收提示条"的端到端 UI 用例**(现有 harness 只有聊天入口的 `/asset.upload` 通路,资源面板的 file input 没有用例,补一条要渲染整个 `ProjectDevelopmentView`)。因此「用户界面上不再出现拒收提示」这一层**未被端到端覆盖**,它是"配对正确 ⇒ merge 判 accepted ⇒ 不产生拒收决策 ⇒ 不渲染提示条"的推理结论。**不要把它当成端到端断言引用。** +- **关联**:`apps/ai-game-creator-shell/src/view/project-development/projectResourceLiveUpdateModel.ts`、`index.tsx` 的 `uploadResourcePanelFiles`、`src/features/app-shell/WorkspaceLauncher.tsx`、提交 `006e9cc2f`。 \ No newline at end of file