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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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` 规避页面分类控件
|
||||
@@ -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,把成功终态误判为失败。
|
||||
|
||||
+4
-2
@@ -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/<slug>` 或 `fix/<slug>`,**禁止** `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) |
|
||||
|
||||
@@ -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**
|
||||
@@ -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/`
|
||||
@@ -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,保存静默丢稿 |
|
||||
|
||||
@@ -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` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
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();
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
export type DeliverableCategory = 'workflow' | 'shared' | 'private';
|
||||
|
||||
export const CATEGORY_TRIGGER_LABEL: Record<DeliverableCategory, string> = {
|
||||
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 }];
|
||||
}
|
||||
Reference in New Issue
Block a user