merge: fix/document-revision-post-restart-final-snippet

This commit is contained in:
weichen
2026-09-13 23:28:53 +00:00
10 changed files with 277 additions and 6 deletions
@@ -0,0 +1,48 @@
---
title: "post-restart 断言了已过期的 UI-06 正文 marker"
status: in_progress
created: 2026-09-13
updated: 2026-09-13
severity: high
---
# post-restart 断言了已过期的 UI-06 正文 marker
## 现象
第二次真实 post-restart(schema 31):login、项目、受控文档、`revision-state` 200、`V3`/`r2`、正文均已恢复。失败只因:
```
getByText('UI06-mu0eq020') → element(s) not found
```
`phase-state.json` 仍写 `bodySnippet=UI06-mu0eq020`,而 UI-13b A1 成功保存后最终持久正文是 `A1-win-*`。
## 根因分析
| 层 | 问题 |
|---|---|
| UI-06 | 硬刷新后把当时 marker 写入 `phase-state.bodySnippet` |
| UI-13b | A1 2xx 保存并断言 `markerA1` 可见;A2 409 冲突稿留在编辑器。未回写最终持久正文 |
| UI-15-pre | 更新了 `versionText`/`revisionText`,但 `...st` 原样带上过期 snippet |
| post-restart | 正确坚持断言 `st.bodySnippet` 可见,于是用旧 marker 对照新正文 FAIL |
产品恢复链已成立;这是 **测试状态链过期**,不是账本丢失。
## 严重度
`high`
## 关联整改
- Short-term: [../ops/short-term/2026-09-13-document-revision-post-restart-final-snippet.md](../ops/short-term/2026-09-13-document-revision-post-restart-final-snippet.md)
- 分支: `fix/document-revision-post-restart-final-snippet`
## 证据
- 失败归档(只读):`.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui/schema31-postrestart-stale-body-snippet-failure/`
- `phase-state.json`:`controlledDocId=doc_proj6157_intake`,`V3`/`r2`,`bodySnippet=UI06-mu0eq020`
## 开放问题
- 本轮不重跑真实 post-restart;不得标 UI-15 通过或任务 `done`
+6
View File
@@ -31,6 +31,12 @@
### 最新专项审计(2026-09-13)
**文件**: [2026-09-13-document-revision-post-restart-final-snippet.md](./2026-09-13-document-revision-post-restart-final-snippet.md)
**结论**: 第二次 post-restart 已恢复 V3/r2 与正文;失败只因 phase-state 仍断言 UI-06 marker,未写入 UI-13b A1 最终持久正文。
**整改**: [short-term](../ops/short-term/2026-09-13-document-revision-post-restart-final-snippet.md)
**文件**: [2026-09-13-document-revision-post-restart-selector.md](./2026-09-13-document-revision-post-restart-selector.md)
**结论**: 第18轮 main 18 PASS;post-restart 在项目已恢复时,页面级项目名同时命中顶栏与侧栏,strict violation,未到 revision-state。
+5 -4
View File
@@ -1,12 +1,12 @@
# 当前焦点
**更新**: 2026-09-13
**production_ready**: no(DOCREV:第18轮 main 18 PASS;post-restart 项目名 selector 未重跑;不执行 env-service 重建 / 不声称 E2E 通过)
**production_ready**: no(DOCREV:第二次 post-restart 恢复链已见 V3/r2,bodySnippet 过期未重跑;不执行 env-service 重建 / 不声称 E2E 通过)
## 焦点
1. **DOCREV-RT**: [post-restart 项目名 selector](short-term/2026-09-13-document-revision-post-restart-selector.md)(`fix/document-revision-post-restart-selector`;编码已提交,暂不合入;不声称 post-restart 通过)
2. **DOCREV-RT**: [项目 reviewer 同步到受控文档职责](short-term/2026-09-13-document-revision-reviewer-duty-sync.md)(已合入 `dev@b824948`;不声称 E2E 通过)
1. **DOCREV-RT**: [post-restart 最终持久正文](short-term/2026-09-13-document-revision-post-restart-final-snippet.md)(`fix/document-revision-post-restart-final-snippet`;编码已提交,暂不合入;不声称 post-restart 通过)
2. **DOCREV-RT**: [post-restart 项目名 selector](short-term/2026-09-13-document-revision-post-restart-selector.md)(已合入 `dev@eb85b8a`)
3. **DOCREV-RT**: [质量修复子工作区契约](short-term/2026-09-13-document-revision-quality-repair-workspace-contract.md)(已合入 `dev@339e871`;仍 `in_progress`,待占槽重跑 UI-03)
## 并行认领
@@ -34,7 +34,8 @@
| Cursor-Auto | document-revision-workflow-category-navigation | fix/document-revision-workflow-category-navigation | - | document-revision-workflow-category-navigation | in_progress |
| Cursor-Auto | document-revision-duty-cell-assertion | fix/document-revision-duty-cell-assertion | - | document-revision-duty-cell-assertion | in_progress |
| Cursor-Auto | document-revision-reviewer-duty-sync | fix/document-revision-reviewer-duty-sync | - | document-revision-reviewer-duty-sync | done |
| Cursor-Auto | document-revision-post-restart-selector | fix/document-revision-post-restart-selector | - | document-revision-post-restart-selector | in_progress |
| Cursor-Auto | document-revision-post-restart-selector | fix/document-revision-post-restart-selector | - | document-revision-post-restart-selector | done |
| Cursor-Auto | document-revision-post-restart-final-snippet | fix/document-revision-post-restart-final-snippet | - | document-revision-post-restart-final-snippet | in_progress |
> 新任务开始前登记。`branch` 必须为 `feature/<slug>` 或 `fix/<slug>`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。
@@ -0,0 +1,23 @@
---
title: "post-restart 最终持久正文 snippet"
status: in_progress
created: 2026-09-13
updated: 2026-09-13
related_task: "2026-09-13-document-revision-post-restart-final-snippet.md"
---
# post-restart 最终持久正文 snippet
## 本轮
- [x] `nextDurableBodySnippet`:后续成功修订覆盖旧 snippet
- [x] 冲突失败稿不成为期望正文
- [x] UI-13b 冲突断言完成后回写 `markerA1`;UI-15-pre 写出最终值
- [x] 不放宽/删除 post-restart body 断言
- [x] `npm test` / `npm run build`
- [ ] 真实 UI-15 post-restart(本轮不做)
## 完成定义
- [x] 上述实现项有单测或脚本证据
- [ ] 现场 post-restart 仍开放,不得标 done
@@ -0,0 +1,61 @@
---
title: "post-restart 最终正文 snippet:编码后交接"
created: 2026-09-13
from: Cursor-Auto
to: next-agent
status: open
---
# 交接:post-restart 最终正文 snippet(暂不合入)
## 目标(下一位 Agent 应完成什么)
- 不要改生产业务代码
- 合入前占 CURRENT 集成槽位;禁止 merge / 重建容器 / 声称 post-restart 通过
- 占槽后重跑 `UI_PHASE=post-restart`,断言应为 `A1-win-*` 而非 `UI06-*`
## 已完成
- `durableBodySnippet` 纯函数:成功修订覆盖;冲突稿不入账
- UI-13b 完成后与 UI-15-pre 同步 `phase-state.bodySnippet`
- post-restart 仍断言 `st.bodySnippet` 可见
## 未完成
- 真实 UI-15 post-restart
- 合入 `dev`
## 认领 / 槽位状态
| 项 | 值 |
|----|-----|
| mutex 已释放 | no(`document-revision-post-restart-final-snippet` 仍 `in_progress`) |
| 集成槽位 holder | 他人 e2e;本任务未占槽 |
| branch | `fix/document-revision-post-restart-final-snippet` |
| worktree | - |
> 若 mutex 未释放,下一位须先在 CURRENT 认领或写 blocker。
## 关键文件
| 路径 | 说明 |
|------|------|
| `web/src/lib/durableBodySnippet.ts` | 持久正文状态链 |
| `web/e2e/document-revision-runtime.spec.ts` | UI-06 / UI-13b / UI-15-pre |
## 如何验证
```bash
cd web && npm test && npm run build
```
## 风险 / Blocker
- 不得改删 schema31 stale-body-snippet 归档
- 不得删除 post-restart `bodySnippet` 可见断言
## 相关链接
- CURRENT: [../CURRENT.md](../CURRENT.md)
- Task: [../short-term/2026-09-13-document-revision-post-restart-final-snippet.md](../short-term/2026-09-13-document-revision-post-restart-final-snippet.md)
- Checklist: [../checklists/2026-09-13-document-revision-post-restart-final-snippet.md](../checklists/2026-09-13-document-revision-post-restart-final-snippet.md)
@@ -0,0 +1,56 @@
---
title: "post-restart 期望正文须为最终持久 marker"
status: in_progress
created: 2026-09-13
updated: 2026-09-13
owner: Cursor-Auto
branch: fix/document-revision-post-restart-final-snippet
worktree: "-"
mutex: document-revision-post-restart-final-snippet
---
# post-restart 期望正文须为最终持久 marker
## 目标
第二次 post-restart 已恢复 login/项目/文档/`revision-state` 200/`V3`/`r2`/正文,但 `phase-state.bodySnippet` 仍是 UI-06 marker,断言旧文失败。UI-13b A1 成功保存后须把最终持久正文写入 phase-state;不得放宽 post body 断言。
## 范围
- `web/src/lib/durableBodySnippet.ts` 纯函数契约
- UI-06 / UI-13b / UI-15-pre 同步 `latestDurableBodySnippet` → `phase-state.bodySnippet`
- audit / checklist / handoff / CURRENT
## 非目标
- 不改生产业务代码、不重跑真实 UI、不动容器、不 merge、不 push
- 不改删 assets
- 不声称 post-restart / UI-15 通过
## 步骤
1. [x] 从 `dev@eb85b8a` 认领分支并登记 mutex
2. [x] 成功修订覆盖旧 snippet;冲突失败稿不成为期望正文
3. [x] UI-13b 完成后与 UI-15-pre 写入最终 marker
4. [x] `npm test` / `npm run build`
5. [x] 原子提交
6. [ ] 占槽后重跑 post-restart(本轮不做)
## 验收标准
- [x] 契约测试:A1 覆盖 UI06;A2 conflict 不覆盖 A1
- [x] post-restart 仍断言 `st.bodySnippet` 可见
- [ ] 真实 UI-15 post-restart(未跑)
## 相关链接
- Audit: [../../audit/2026-09-13-document-revision-post-restart-final-snippet.md](../../audit/2026-09-13-document-revision-post-restart-final-snippet.md)
- Checklist: [../checklists/2026-09-13-document-revision-post-restart-final-snippet.md](../checklists/2026-09-13-document-revision-post-restart-final-snippet.md)
- Handoff: [../handoffs/2026-09-13-document-revision-post-restart-final-snippet.md](../handoffs/2026-09-13-document-revision-post-restart-final-snippet.md)
## 证据 / 备注
- 基线:`dev@eb85b8a`
- 失败归档(只读):`.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui/schema31-postrestart-stale-body-snippet-failure/`
- 现场 `phase-state`:`versionText=V3` `revisionText=r2` `bodySnippet=UI06-mu0eq020`
- **未跑真实 post-restart,不得标 done**
+1 -1
View File
@@ -25,7 +25,7 @@
| 前端工作台 | 三栏 + 块文档 + SSE | partial | [test workbench](../test-reports/2026-09-08-three-column-workbench-and-block-document-test-report.md);[现场专项审计](../audit/2026-09-13-conversation-intent-runtime-state.md) | 错误卡片/任务进度/文档 busy 状态刷新恢复与人工抽检待完成 | `web` |
| PI Runner / 配置链 | pi-agent + MiniMax 真实链路 | done | [test piagent](../test-reports/2026-09-13-codex-audit-p0-remediation-test-report.md);[container-env](../test-reports/2026-09-13-container-env-configuration-test-report.md) | — | `pi-agent` |
| 三环境运维 | dev/test/main + 共享 KB | done | [PROJECT.md](../../PROJECT.md) §4;四环境 healthy | — | `infra` |
| CI / E2E | PG 集成 + 真实 PI E2E | partial | [gap-matrix BUG-08](../remediation/gap-matrix.md);[test pg-ci](../test-reports/2026-09-12-pg-ci-soak-e2e-test-report.md);[UI-13a reviewer 职责同步](../audit/2026-09-13-document-revision-reviewer-duty-sync.md) | CI `TEST_DATABASE_URL` 隔离库;立项/改职责后 reviewer 必须成为受控文档 `reviewer_id`;post-restart 项目名须 banner exact;产物与结果 JSON 按 UI_PHASE 分文件 | `ci` |
| CI / E2E | PG 集成 + 真实 PI E2E | partial | [gap-matrix BUG-08](../remediation/gap-matrix.md);[test pg-ci](../test-reports/2026-09-12-pg-ci-soak-e2e-test-report.md);[UI-13a reviewer 职责同步](../audit/2026-09-13-document-revision-reviewer-duty-sync.md) | CI `TEST_DATABASE_URL` 隔离库;post-restart 须断言最终持久正文(A1 覆盖 UI06,冲突稿不入账);产物与结果 JSON 按 UI_PHASE 分文件 | `ci` |
---
+18 -1
View File
@@ -28,6 +28,7 @@ import {
SHARED_PHASE_STATE_FILE,
playwrightResultsFileName,
} from '../src/lib/playwrightPhaseArtifacts';
import { nextDurableBodySnippet } from '../src/lib/durableBodySnippet';
/**
* Document revision runtime — real UI acceptance (UI-01..UI-16).
@@ -670,6 +671,8 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
let secondDocTitle = '';
let secondDocId = '';
let uploadedDocId = '';
/** Latest server-durable body marker; UI-15-pre writes this as bodySnippet. */
let latestDurableBodySnippet = '';
test.beforeAll(async ({ browser }) => {
context = await browser.newContext();
@@ -1047,8 +1050,12 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
timeout: 30_000,
});
await shot(page, 'UI-06-after-refresh.png');
latestDurableBodySnippet = nextDurableBodySnippet(latestDurableBodySnippet, {
kind: 'persisted',
snippet: marker,
}) || marker;
const st = readPhaseState();
writePhaseState({ ...st, bodySnippet: marker, revisionText: r1 });
writePhaseState({ ...st, bodySnippet: latestDurableBodySnippet, revisionText: r1 });
recordPass({
id: 'UI-06',
name: 'save + hard refresh',
@@ -1386,6 +1393,15 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
await expect(page.getByText(markerA2)).toHaveCount(0);
await shot(pageA2, 'UI-13b-conflict-A2.png');
await ctxA2.close();
latestDurableBodySnippet = nextDurableBodySnippet(latestDurableBodySnippet, {
kind: 'persisted',
snippet: markerA1,
}) || markerA1;
latestDurableBodySnippet = nextDurableBodySnippet(latestDurableBodySnippet, {
kind: 'conflict_draft',
snippet: markerA2,
}) || latestDurableBodySnippet;
writePhaseState({ ...readPhaseState(), bodySnippet: latestDurableBodySnippet });
recordPass({
id: 'UI-13b',
name: 'author A/A head conflict',
@@ -1448,6 +1464,7 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
controlledDocId,
versionText: v,
revisionText: r,
bodySnippet: latestDurableBodySnippet || st.bodySnippet,
userA: USER_A,
userB: USER_B,
userC: USER_C,
+33
View File
@@ -0,0 +1,33 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { nextDurableBodySnippet, requireDurableSnippet } from './durableBodySnippet';
describe('durableBodySnippet', () => {
it('rejects empty persisted snippets', () => {
assert.throws(() => requireDurableSnippet(' '), /empty_durable_body_snippet/);
assert.throws(
() => nextDurableBodySnippet('UI06-old', { kind: 'persisted', snippet: ' ' }),
/empty_durable_body_snippet/,
);
});
it('lets a later successful revision replace the old snippet', () => {
const afterUi06 = nextDurableBodySnippet(undefined, { kind: 'persisted', snippet: ' UI06-mu0eq020 ' });
assert.equal(afterUi06, 'UI06-mu0eq020');
const afterA1 = nextDurableBodySnippet(afterUi06, { kind: 'persisted', snippet: 'A1-win-mu0eq020' });
assert.equal(afterA1, 'A1-win-mu0eq020');
assert.notEqual(afterA1, afterUi06);
});
it('does not promote a conflict-failed draft to the expected body', () => {
const afterUi06 = nextDurableBodySnippet(undefined, { kind: 'persisted', snippet: 'UI06-mu0eq020' });
const afterA1 = nextDurableBodySnippet(afterUi06, { kind: 'persisted', snippet: 'A1-win-mu0eq020' });
const afterA2Conflict = nextDurableBodySnippet(afterA1, {
kind: 'conflict_draft',
snippet: 'A2-conflict-mu0eq020',
});
assert.equal(afterA2Conflict, 'A1-win-mu0eq020');
assert.notEqual(afterA2Conflict, 'A2-conflict-mu0eq020');
assert.notEqual(afterA2Conflict, 'UI06-mu0eq020');
});
});
+26
View File
@@ -0,0 +1,26 @@
export type DurableBodyEvent =
| { kind: 'persisted'; snippet: string }
| { kind: 'conflict_draft'; snippet: string };
export function requireDurableSnippet(snippet: string): string {
const next = snippet.trim();
if (!next) {
throw new Error('empty_durable_body_snippet');
}
return next;
}
/**
* Latest server-durable body snippet for post-restart.
* A later successful save replaces the previous snippet.
* A conflict-failed editor draft never becomes the expected body.
*/
export function nextDurableBodySnippet(
current: string | undefined,
event: DurableBodyEvent,
): string | undefined {
if (event.kind === 'conflict_draft') {
return current;
}
return requireDurableSnippet(event.snippet);
}