From ba4bb41e1c61e7f746b2c4147ff97e2a4b203042 Mon Sep 17 00:00:00 2001 From: weichen Date: Sun, 13 Sep 2026 21:03:23 +0000 Subject: [PATCH] fix(e2e): switch to workflow category before opening controlled docs UI-13a stayed on private after upload, so the doc menu had no doc_proj* entries. Always use the category trigger first, then exact id. Co-authored-by: Cursor --- ...t-revision-workflow-category-navigation.md | 48 ++++++++++++++++ .doc/audit/INDEX.md | 8 +++ .doc/ops/CURRENT.md | 6 +- ...t-revision-workflow-category-navigation.md | 52 +++++++++++++++++ ...t-revision-workflow-category-navigation.md | 46 +++++++++++++++ .doc/remediation/INDEX.md | 1 + .doc/status/FEATURE-MATRIX.md | 2 +- web/e2e/document-revision-runtime.spec.ts | 11 ++++ .../lib/workflowCategoryNavigation.test.ts | 57 +++++++++++++++++++ web/src/lib/workflowCategoryNavigation.ts | 57 +++++++++++++++++++ 10 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 .doc/audit/2026-09-13-document-revision-workflow-category-navigation.md create mode 100644 .doc/ops/short-term/2026-09-13-document-revision-workflow-category-navigation.md create mode 100644 .doc/remediation/2026-09-13-document-revision-workflow-category-navigation.md create mode 100644 web/src/lib/workflowCategoryNavigation.test.ts create mode 100644 web/src/lib/workflowCategoryNavigation.ts diff --git a/.doc/audit/2026-09-13-document-revision-workflow-category-navigation.md b/.doc/audit/2026-09-13-document-revision-workflow-category-navigation.md new file mode 100644 index 00000000..08efdfc5 --- /dev/null +++ b/.doc/audit/2026-09-13-document-revision-workflow-category-navigation.md @@ -0,0 +1,48 @@ +--- +title: "UI-13a 在私密分类下打不开受控工作流文档" +status: in_progress +created: 2026-09-13 +updated: 2026-09-13 +severity: high +--- + +# UI-13a 在私密分类下打不开受控工作流文档 + +## 现象 + +第15轮真实 UI(schema 30,`dev@48a4dcb`):UI-00 至 UI-12 全部 PASS,UI-11 快速完成修复已实证 PASS。**UI-13a 在真正权限断言前失败**。 + +`openWorkflowDocById(page, doc_proj6143_intake)` 报:`doc-selector menu missing data-doc-id=doc_proj6143_intake`。 + +现场快照:分类触发器为「我的私密 1」,文档下拉只有私密清单。intake 仍存在于对话引用中,只是当前分类菜单不可见。 + +## 根因分析 + +| 层 | 问题 | 后果 | +|---|---|---| +| UI-11 后状态 | 上传归档把 active category 留在 `private` | 后续步骤默认停在「我的私密」 | +| 验收导航 | `openWorkflowDocById` / `ByLabel` / `ByControlledTaskKey` 直接展开当前分类的文档菜单 | 私密菜单里没有 `doc_proj*` | +| 分类控件 | 真实页面必须先点 `doc-category-trigger` 再选 `data-category=workflow`(文案「流程受控」) | 脚本跳过该步即 fail-open 于错误清单 | + +不是权限矩阵错,也不是 intake 丢失。 + +## 严重度 + +`high` + +阻断 UI-13a 及之后所有「从任意分类回到受控文档」的步骤。 + +## 关联整改 + +- Remediation: [../remediation/2026-09-13-document-revision-workflow-category-navigation.md](../remediation/2026-09-13-document-revision-workflow-category-navigation.md) +- Short-term: [../ops/short-term/2026-09-13-document-revision-workflow-category-navigation.md](../ops/short-term/2026-09-13-document-revision-workflow-category-navigation.md) +- 分支: `fix/document-revision-workflow-category-navigation` + +## 证据 + +- 失败证据(只读,不得改/移/删/提交):`.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui13a-category-navigation-failure/` + +## 开放问题 + +- 本轮不重跑真实 UI-13a;不得标 E2E 通过或任务 `done` +- 不得用直接 API / 改内存 `activeCategory` 规避页面分类控件 diff --git a/.doc/audit/INDEX.md b/.doc/audit/INDEX.md index a4e941b0..c5f41057 100644 --- a/.doc/audit/INDEX.md +++ b/.doc/audit/INDEX.md @@ -31,6 +31,14 @@ ### 最新专项审计(2026-09-13) +**文件**: [2026-09-13-document-revision-workflow-category-navigation.md](./2026-09-13-document-revision-workflow-category-navigation.md) + +**结论**: 第15轮 UI-00–12 PASS;UI-13a 在私密分类下直接展开文档菜单,找不到 `doc_proj*_intake`。须先用页面控件切到流程受控。 + +**整改**: [受控文档分类导航整改](../remediation/2026-09-13-document-revision-workflow-category-navigation.md) + +### UI-11 快速完成观测(2026-09-13) + **文件**: [2026-09-13-document-revision-fast-completion-observation.md](./2026-09-13-document-revision-fast-completion-observation.md) **结论**: 第14轮 UI-11 上传归档后助手已 `completed`;验收默认硬等 inflight 60s,把成功终态误判为失败。 diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index 4af4dd49..380108c0 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -1,11 +1,11 @@ # 当前焦点 **更新**: 2026-09-13 -**production_ready**: no(DOCREV:schema **30**;第14轮 UI-11 快速完成观测契约;不执行 env-service 重建 / 不声称 E2E 通过) +**production_ready**: no(DOCREV:schema **30**;第15轮 UI-13a 分类导航契约;不执行 env-service 重建 / 不声称 E2E 通过) ## 焦点 -1. **DOCREV-RT**: [UI-11 快速完成观测契约](short-term/2026-09-13-document-revision-fast-completion-observation.md)(`fix/document-revision-fast-completion-observation`;第14轮成功终态被硬等 inflight 误杀;不声称 E2E 通过) +1. **DOCREV-RT**: [受控文档分类导航契约](short-term/2026-09-13-document-revision-workflow-category-navigation.md)(`fix/document-revision-workflow-category-navigation`;第15轮 UI-13a 停在私密分类找不到 intake;不声称 E2E 通过) 2. **DOCREV-RT**: [质量修复子工作区契约](short-term/2026-09-13-document-revision-quality-repair-workspace-contract.md)(已合入 `dev@339e871`;仍 `in_progress`,待占槽重跑 UI-03) 3. **DOCREV-RT**: [编辑弹窗 hydration 竞态](short-term/2026-09-13-document-revision-editor-hydration-race.md)(已合入 `dev@8b1a531`;仍 `in_progress`,待占槽重跑 UI-06) @@ -31,6 +31,7 @@ | Cursor-Auto | document-revision-editor-hydration-race | fix/document-revision-editor-hydration-race | - | document-revision-editor-hydration-race | in_progress | | Cursor-Auto | document-revision-quality-repair-workspace-contract | fix/document-revision-quality-repair-workspace-contract | - | document-revision-quality-repair-workspace-contract | in_progress | | Cursor-Auto | document-revision-fast-completion-observation | fix/document-revision-fast-completion-observation | - | document-revision-fast-completion-observation | in_progress | +| Cursor-Auto | document-revision-workflow-category-navigation | fix/document-revision-workflow-category-navigation | - | document-revision-workflow-category-navigation | in_progress | > 新任务开始前登记。`branch` 必须为 `feature/` 或 `fix/`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。 @@ -79,3 +80,4 @@ | UI-06 编辑弹窗 hydration 竞态审计 | [../audit/2026-09-13-document-revision-editor-hydration-race.md](../audit/2026-09-13-document-revision-editor-hydration-race.md) | | UI-03 质量修复子工作区契约审计 | [../audit/2026-09-13-document-revision-quality-repair-workspace-contract.md](../audit/2026-09-13-document-revision-quality-repair-workspace-contract.md) | | UI-11 快速完成观测误判审计 | [../audit/2026-09-13-document-revision-fast-completion-observation.md](../audit/2026-09-13-document-revision-fast-completion-observation.md) | +| UI-13a 私密分类导航失败审计 | [../audit/2026-09-13-document-revision-workflow-category-navigation.md](../audit/2026-09-13-document-revision-workflow-category-navigation.md) | diff --git a/.doc/ops/short-term/2026-09-13-document-revision-workflow-category-navigation.md b/.doc/ops/short-term/2026-09-13-document-revision-workflow-category-navigation.md new file mode 100644 index 00000000..68d6919f --- /dev/null +++ b/.doc/ops/short-term/2026-09-13-document-revision-workflow-category-navigation.md @@ -0,0 +1,52 @@ +--- +title: "受控文档分类导航契约" +status: in_progress +created: 2026-09-13 +updated: 2026-09-13 +owner: Cursor-Auto +branch: fix/document-revision-workflow-category-navigation +worktree: "-" +mutex: document-revision-workflow-category-navigation +--- + +# 受控文档分类导航契约 + +## 目标 + +堵住第15轮 UI-13a:UI-11 后停在「我的私密」,未切「流程受控」就展开文档菜单,找不到 `doc_proj*_intake`。只改验收导航,不降真实性。 + +## 范围 + +- `web/src/lib/workflowCategoryNavigation.ts` 与单测 +- `web/e2e/document-revision-runtime.spec.ts` 三个 opener +- audit / remediation / CURRENT + +## 非目标 + +- 不改产品分类状态机;不用 API/内存规避 UI +- 不碰证据;不合 dest;不启停容器;不 E2E;不 push + +## 步骤 + +1. [x] 从 `dev@48a4dcb` 开分支并登记 mutex +2. [x] 导航谓词 + private→workflow / 缺失 fail-closed 测试 +3. [x] 三个 opener 先真实切分类 +4. [x] `npm test` + `npm run build` 并原子提交 + +## 验收标准 + +- [x] 从私密分类可恢复到精确受控文档 +- [x] 目标缺失 fail-closed +- [ ] 现场 UI-13a(未跑) + +## 相关链接 + +- Audit: [../../audit/2026-09-13-document-revision-workflow-category-navigation.md](../../audit/2026-09-13-document-revision-workflow-category-navigation.md) +- Remediation: [../../remediation/2026-09-13-document-revision-workflow-category-navigation.md](../../remediation/2026-09-13-document-revision-workflow-category-navigation.md) + +## 证据 / 备注 + +- mutex:`document-revision-workflow-category-navigation`(新 mutex) +- 基线:`dev@48a4dcb` +- 验证:`npx tsx --test src/lib/workflowCategoryNavigation.test.ts` PASS;`cd web && npm test` PASS(309 + 29);`npm run build` PASS +- **未跑真实 UI-13a,不得标 done**;**不得 merge / 启停容器 / E2E / push** diff --git a/.doc/remediation/2026-09-13-document-revision-workflow-category-navigation.md b/.doc/remediation/2026-09-13-document-revision-workflow-category-navigation.md new file mode 100644 index 00000000..181caff4 --- /dev/null +++ b/.doc/remediation/2026-09-13-document-revision-workflow-category-navigation.md @@ -0,0 +1,46 @@ +--- +title: "受控文档打开前必须先切工作流分类" +status: in_progress +created: 2026-09-13 +updated: 2026-09-13 +--- + +# 受控文档打开前必须先切工作流分类 + +## 背景 + +链到 [../audit/2026-09-13-document-revision-workflow-category-navigation.md](../audit/2026-09-13-document-revision-workflow-category-navigation.md)。 + +只改验收导航契约,不改产品运行时,不用 API/内存状态绕过 UI。 + +## 目标 + +- 打开受控工作流文档前,必须通过真实页面控件切到 `data-category=workflow`(「流程受控」) +- 再按精确 `data-doc-id`(或既有 taskKey / label)选择 +- 从「我的私密」等任意当前分类可恢复 +- 目标不在正确分类菜单中 → fail-closed + +## 分波次计划 + +| 波次 | 内容 | 验收 | +|------|------|------| +| Wave 0 | 抽取导航谓词;三个 opener 先 `selectDeliverableCategory` | private→workflow→exact;缺失 fail-closed;`npm test` + `build` | +| Wave 1 | 占槽后重跑真实 UI-13a(本轮不做) | 权限断言前能打开 intake | + +## 设计 + +1. `requiredCategoryForDocOpen`:`doc_proj*` / label / taskKey → `workflow`;`doc-free-*` → `private`(UI-16 仍走页面私密分类)。 +2. `planExactDocOpen`:始终先 `ensure_category`;仅当当前分类已是目标分类时才在可见菜单里 `select_exact`;否则不得因私密菜单缺 `doc_proj*` 立即失败。 +3. 三个 `openWorkflowDoc*` 都先点 `doc-category-trigger` + `data-category=...`,再打开文档菜单。禁止 `page.evaluate` / 路由 mock / 直调 `setActiveCategory`。 + +## 验收标准 + +- [x] private 菜单看不到 `doc_proj*` 时先切 workflow,不立刻 FAIL +- [x] workflow 菜单有精确 id 则 select +- [x] 正确分类下目标缺失 fail-closed +- [ ] 真实 UI-13a(本轮不做) + +## 相关链接 + +- Audit / Short-term:同 slug `2026-09-13-document-revision-workflow-category-navigation` +- 证据(只读):`.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui13a-category-navigation-failure/` diff --git a/.doc/remediation/INDEX.md b/.doc/remediation/INDEX.md index 0d06433d..f4322dfe 100644 --- a/.doc/remediation/INDEX.md +++ b/.doc/remediation/INDEX.md @@ -3,6 +3,7 @@ | 文档 | 说明 | |------|------| | [gap-matrix.md](./gap-matrix.md) | 发布缺口 SSOT | +| [2026-09-13-document-revision-workflow-category-navigation.md](./2026-09-13-document-revision-workflow-category-navigation.md) | UI-13a 停在私密分类,打开受控文档前必须先切流程受控 | | [2026-09-13-document-revision-fast-completion-observation.md](./2026-09-13-document-revision-fast-completion-observation.md) | UI-11 快完成被硬等 inflight 误杀,只改验收观测契约 | | [2026-09-13-document-revision-quality-repair-workspace-contract.md](./2026-09-13-document-revision-quality-repair-workspace-contract.md) | 质量修复子工作区越界改根 output.md,本地目标缺失 | | [2026-09-13-document-revision-editor-hydration-race.md](./2026-09-13-document-revision-editor-hydration-race.md) | 编辑弹窗打开期间 hydration 换块 ID,保存静默丢稿 | diff --git a/.doc/status/FEATURE-MATRIX.md b/.doc/status/FEATURE-MATRIX.md index 3efe04c6..a242c9db 100644 --- a/.doc/status/FEATURE-MATRIX.md +++ b/.doc/status/FEATURE-MATRIX.md @@ -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-11 快完成观测](../audit/2026-09-13-document-revision-fast-completion-observation.md) | CI `TEST_DATABASE_URL` + 真实 PI E2E 实录;UI-11 验收不得把已 completed 误判为缺 inflight | `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 分类导航](../audit/2026-09-13-document-revision-workflow-category-navigation.md) | CI `TEST_DATABASE_URL` + 真实 PI E2E 实录;打开受控文档前须先切流程受控分类 | `ci` | --- diff --git a/web/e2e/document-revision-runtime.spec.ts b/web/e2e/document-revision-runtime.spec.ts index c68035df..fb3b41e5 100644 --- a/web/e2e/document-revision-runtime.spec.ts +++ b/web/e2e/document-revision-runtime.spec.ts @@ -10,6 +10,10 @@ import { ANY_TERMINAL_STATUS_RE, evaluateSquadObservation, } from '../src/lib/squadCompletionObservation'; +import { + CATEGORY_TRIGGER_LABEL, + requiredCategoryForDocOpen, +} from '../src/lib/workflowCategoryNavigation'; /** * Document revision runtime — real UI acceptance (UI-01..UI-16). @@ -308,12 +312,14 @@ async function selectDeliverableCategory( const item = page.locator(`.dropdown-menu button[data-category="${category}"]`).first(); if ((await item.count()) === 0) fail(`category menu missing data-category=${category}`); await item.click(); + await expect(trigger).toContainText(CATEGORY_TRIGGER_LABEL[category], { timeout: 10_000 }); } async function openWorkflowDocByLabel( page: Page, label: RegExp, ): Promise<{ title: string; docId: string }> { + await selectDeliverableCategory(page, requiredCategoryForDocOpen({ kind: 'workflow-label' })); const trigger = page.getByTestId('doc-selector-trigger'); await expect(trigger).toBeVisible({ timeout: 15_000 }); await trigger.click(); @@ -334,6 +340,10 @@ async function openWorkflowDocByControlledTaskKey( taskKey: string, ): Promise<{ title: string; docId: string }> { if (!/^[a-z][a-z0-9_]*$/.test(taskKey)) fail(`invalid controlled taskKey: ${taskKey}`); + await selectDeliverableCategory( + page, + requiredCategoryForDocOpen({ kind: 'controlled-task-key', taskKey }), + ); const trigger = page.getByTestId('doc-selector-trigger'); await expect(trigger).toBeVisible({ timeout: 15_000 }); await trigger.click(); @@ -354,6 +364,7 @@ async function openWorkflowDocByControlledTaskKey( async function openWorkflowDocById(page: Page, docId: string): Promise { if (!docId) fail('openWorkflowDocById requires docId'); + await selectDeliverableCategory(page, requiredCategoryForDocOpen({ kind: 'exact-id', docId })); const trigger = page.getByTestId('doc-selector-trigger'); await expect(trigger).toBeVisible({ timeout: 15_000 }); await trigger.click(); diff --git a/web/src/lib/workflowCategoryNavigation.test.ts b/web/src/lib/workflowCategoryNavigation.test.ts new file mode 100644 index 00000000..658269f0 --- /dev/null +++ b/web/src/lib/workflowCategoryNavigation.test.ts @@ -0,0 +1,57 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { + planExactDocOpen, + requiredCategoryForDocOpen, +} from './workflowCategoryNavigation'; + +describe('workflowCategoryNavigation', () => { + it('private menu cannot see doc_proj* so it must switch to workflow before exact select', () => { + const fromPrivate = planExactDocOpen({ + currentCategory: 'private', + visibleMenuDocIds: ['doc-free-1789-1'], + targetDocId: 'doc_proj6143_intake', + }); + assert.deepEqual(fromPrivate, [{ action: 'ensure_category', category: 'workflow' }]); + + const afterSwitch = planExactDocOpen({ + currentCategory: 'workflow', + visibleMenuDocIds: ['doc_proj6143_intake', 'doc_proj6143_prelim_design'], + targetDocId: 'doc_proj6143_intake', + }); + assert.deepEqual(afterSwitch, [ + { action: 'ensure_category', category: 'workflow' }, + { action: 'select_exact', docId: 'doc_proj6143_intake' }, + ]); + }); + + it('fail-closes when the workflow menu is showing but the exact doc is missing', () => { + const verdict = planExactDocOpen({ + currentCategory: 'workflow', + visibleMenuDocIds: ['doc_proj6143_prelim_design'], + targetDocId: 'doc_proj6143_intake', + }); + const last = verdict[verdict.length - 1]; + assert.equal(last?.action, 'fail'); + if (last?.action !== 'fail') { + throw new Error('expected fail-closed last step'); + } + assert.match(last.reason, /doc-selector menu missing data-doc-id=doc_proj6143_intake/); + }); + + it('label and taskKey opens always require the workflow category', () => { + assert.equal(requiredCategoryForDocOpen({ kind: 'workflow-label' }), 'workflow'); + assert.equal( + requiredCategoryForDocOpen({ kind: 'controlled-task-key', taskKey: 'intake' }), + 'workflow', + ); + assert.equal( + requiredCategoryForDocOpen({ kind: 'exact-id', docId: 'doc_proj12_intake' }), + 'workflow', + ); + assert.equal( + requiredCategoryForDocOpen({ kind: 'exact-id', docId: 'doc-free-1' }), + 'private', + ); + }); +}); diff --git a/web/src/lib/workflowCategoryNavigation.ts b/web/src/lib/workflowCategoryNavigation.ts new file mode 100644 index 00000000..88e46593 --- /dev/null +++ b/web/src/lib/workflowCategoryNavigation.ts @@ -0,0 +1,57 @@ +export type DeliverableCategory = 'workflow' | 'shared' | 'private'; + +export const CATEGORY_TRIGGER_LABEL: Record = { + workflow: '流程受控', + shared: '项目共享', + private: '我的私密', +}; + +export type DocOpenIntent = + | { kind: 'exact-id'; docId: string } + | { kind: 'controlled-task-key'; taskKey: string } + | { kind: 'workflow-label' }; + +export type DocSelectorStep = + | { action: 'ensure_category'; category: DeliverableCategory } + | { action: 'select_exact'; docId: string } + | { action: 'fail'; reason: string }; + +const CONTROLLED_DOC_ID = /^doc_proj\d+_[a-z][a-z0-9_]*$/; + +export function requiredCategoryForDocOpen(intent: DocOpenIntent): DeliverableCategory { + if (intent.kind === 'workflow-label' || intent.kind === 'controlled-task-key') { + return 'workflow'; + } + const docId = intent.docId.trim(); + if (CONTROLLED_DOC_ID.test(docId)) { + return 'workflow'; + } + if (docId.startsWith('doc-free-')) { + return 'private'; + } + throw new Error(`cannot infer deliverable category for ${JSON.stringify(docId)}`); +} + +/** + * Plan the real UI sequence: always ensure the target category via the category + * trigger, then select by exact data-doc-id from that category's menu. + * A private (or other) menu missing doc_proj* is not a failure until after switch. + */ +export function planExactDocOpen(input: { + currentCategory: DeliverableCategory; + visibleMenuDocIds: string[]; + targetDocId: string; +}): DocSelectorStep[] { + const category = requiredCategoryForDocOpen({ kind: 'exact-id', docId: input.targetDocId }); + const ensure: DocSelectorStep = { action: 'ensure_category', category }; + if (input.currentCategory !== category) { + return [ensure]; + } + if (!input.visibleMenuDocIds.includes(input.targetDocId)) { + return [ + ensure, + { action: 'fail', reason: `doc-selector menu missing data-doc-id=${input.targetDocId}` }, + ]; + } + return [ensure, { action: 'select_exact', docId: input.targetDocId }]; +}