Drop test-only ETag helpers. App now calls saveControlledBlockEdit, which PATCHes once on the bound editor docId or fail-closes. Resume hydrate after a blocked first load, and refuse silent doc switches. Co-authored-by: Cursor <cursoragent@cursor.com>
3.7 KiB
3.7 KiB
title, status, created, updated
| title | status | created | updated |
|---|---|---|---|
| 编辑弹窗 hydration 竞态整改 | in_progress | 2026-09-13 | 2026-09-13 |
编辑弹窗 hydration 竞态整改
背景
链到 ../audit/2026-09-13-document-revision-editor-hydration-race.md。
第十二轮 UI-06:fill 瞬间后台 GET 完成 hydration,块 ID 被重建;保存按旧 ID 找不到目标后静默关窗,PATCH 次数为 0,修订停在 r0。
目标
- 编辑期间后台 hydration(含 fill 与 save 之间完成的响应)不得替换块身份、不得覆盖编辑基线
- PATCH 成功后迟到 hydration 不得回滚新正文 / 新 ETag
- 找不到目标块时 fail-closed:保留弹窗与用户输入,明确提示,绝不关窗、不按正文改另一个块
- 正常保存仍发 PATCH,采用打开时/hydrate 基线 ETag;409 语义不削弱
- 不声称真实 UI/E2E 已通过
分波次计划
| 波次 | 内容 | 验收 |
|---|---|---|
| Wave 0 | hydrate 对齐 head_block_map;抽出 editorHydrationGate + blockSavePolicy;App 三条 hydrate 路径与保存接线 |
单测:同 map 两次 hydrate ID 稳定;缺 map 仍随机;deferred hydrate / stale id / 迟到 hydrate / PATCH+409;npm test + npm run build |
| Wave 1 | 占槽后真实 UI-06(本轮不做) | 现场 PATCH 次数 ≥ 1 且 r0→r1 |
设计
两层,缺一不可;不靠 Playwright sleep:
- 身份对齐:
hydrateControlledDocumentContent保留完整RevisionState(含head_block_map),buildControlledDocumentAST后applyHeadBlockMap。同一 head 的重复 hydrate 不再因Math.random换 ID。 - generation / 编辑门闩:双击立刻
setEditorOpen(true);编辑中beginHydration返回null;已在途 token 经decideHydrationApply丢弃。PATCH 成功先markSaved()再关窗,迟到 hydrate 即使 ID 已对齐也不得回滚新正文。 - stale-id fail-closed:
resolveSaveTargetBlock只按 id;找不到则提示并留窗,绝不按正文改另一块、绝不静默关窗。 - 真实 PATCH 编排:
saveControlledBlockEdit被App.tsx调用;依赖注入patchDocumentContent。成功路径恰好一次 PATCH(绑定editorDocId、hydrate ETag、client_block_map),采用响应 ETag。409/网络错误不关窗。 - 编辑会话绑 docId:
editorSession = { docId, block }。activeDocId被 canonical/session 改写后保存 fail-closed,无 PATCH。 - 关窗续跑:
editor_open跳过/丢弃的 hydrate 置blockedNeedsResume;安全关闭后hydrateResumeKey重跑 effect,避免永久 loading skeleton。 - 切文档:modal
fixed inset:0 z-index 100挡住 SlimTabBar,但 chat/drawer 仍可调handleSelectDoc。编辑中切换 fail-closed 并提示,不静默关窗。
验收标准
- 纯函数门闩覆盖 UI-06 时序(hydrate 在 fill 后 resolve 不覆盖)
- stale block id 保存不静默关闭、不假成功、不改同内容另一块
- PATCH 成功后迟到 hydration 不回滚;409 保留草稿
saveControlledBlockEdit成功路径断言恰好一次 PATCH(docId/ETag/body/map);doc 漂移/409/网络不关窗- 关窗后续跑被阻塞的 hydrate;编辑中切文档 fail-closed
- 真实 UI-06 E2E(本轮不做,不得勾选)
相关链接
- Audit: ../audit/2026-09-13-document-revision-editor-hydration-race.md
- Short-term: ../ops/short-term/2026-09-13-document-revision-editor-hydration-race.md
- 证据(只读):
.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui06-r-bump-noop-failure/