test(e2e): open pending change report before confirm or reject

UI-04 must close via the dialog 关闭 button; UI-05a/05b must click 待确认报告 and assert panel actions so leftover first-round reports cannot hide the decision path.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-13 16:25:39 +00:00
co-authored by Cursor
parent e6cd7bef9e
commit ca01a1cf9d
4 changed files with 139 additions and 13 deletions
@@ -0,0 +1,51 @@
---
title: "UI-05a 未打开待确认报告导致确认按钮缺失"
status: in_progress
created: 2026-09-13
updated: 2026-09-13
severity: high
---
# UI-05a 未打开待确认报告导致确认按钮缺失
## 现象
第四轮真实 UI(schema 30)**UI-05a 首个失败**:
- UI-00~05 PASS;**UI-05a** 抛 `confirm button missing for pending high-risk candidate`
- 工具条已显示「待确认 high」与「待确认报告」
- 助手气泡写明候选 #91、变更报告 91、风险 high、状态 `pending_author_confirmation`
- 页面仍挂着 **首轮** `dialog「变更报告」`:风险 **low** / `fallback`,无 `change-report-pending-actions`,无「确认提交」
- 脚本用全局 `getByRole('button', { name: /确认提交/ }).count()` 判定,未先点「待确认报告」
项目 `UI验收-超声微创系统-mu00igjt`,文档 `doc_proj6119_intake`,首轮报告 `change-reports/90`,pending 候选/报告 `#91`。
## 根因分析
| 层 | 问题 |
|---|---|
| UI-04 关闭 | `ChangeReportPanel` 只有弹窗内「关闭」;**没有 Escape 处理**。`page.keyboard.press('Escape')` 被 `.catch` 吞掉,首轮 low 报告 `reportId` 仍打开 |
| 产品条件 | `pendingDecision`(确认/拒绝)仅当 `reportId === pending_change_report_id` 才注入。首轮报告 90 ≠ pending 91,弹窗里不会出现「确认提交」 |
| UI-05a | 未模拟真实用户点击「待确认报告」并等待 `change-report-panel`;只用全局 count,错过必须打开 pending 报告的路径 |
产品确认/拒绝逻辑本身未坏;失败来自验收脚本未走真实面板路径。本轮**禁止改产品业务逻辑**。
## 严重度
`high`
## 关联整改
- Short-term: [../ops/short-term/2026-09-13-document-revision-e2e-pending-flow.md](../ops/short-term/2026-09-13-document-revision-e2e-pending-flow.md)
- 分支: `fix/document-revision-e2e-pending-flow`
## 证据
- Evidence root(勿删、勿提交本轮):`.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui05a-pending-report-selection-failure`
- Playwright:`playwright-output/document-revision-runtime--91599-andidate-→-version-advances-chromium/error-context.md`
- 结果:`ui-acceptance-results.json`(UI-04 PASS 131ms,打开 `/change-reports/90`;UI-05 PASS;UI-05a 未写入 PASS)
## 开放问题
- 占槽后真实 UI-05a/05b 重跑;本轮不操作容器、不重跑 E2E
- 脚本收紧后状态仍为 `in_progress`,真实重跑前不得 `done`
+4 -2
View File
@@ -1,11 +1,11 @@
# 当前焦点
**更新**: 2026-09-13
**production_ready**: no(DOCREV:schema **30**;行动表状态「待续」假阳性在 `fix/document-revision-quality-context`;不操作容器)
**production_ready**: no(DOCREV:schema **30**;第四轮 UI-05a 待确认报告未打开;不操作容器)
## 焦点
1. **DOCREV-RT**: [行动计划表状态「待续」窄豁免](short-term/2026-09-13-document-revision-quality-context.md)(`fix/document-revision-quality-context`:不删禁词、不放宽正文;不 push / 不操作容器)
1. **DOCREV-RT**: [E2E 待确认报告真实打开再确认/拒绝](short-term/2026-09-13-document-revision-e2e-pending-flow.md)(`fix/document-revision-e2e-pending-flow`:不改产品逻辑;不重跑 E2E / 不操作容器)
2. **CHAT**: [刷新恢复整改已实施](short-term/2026-09-13-chat-history-refresh-remediation.md)(待合 dev + PI E2E 补录)
3. **CI**: `TEST_DATABASE_URL` + 真实 PI E2E 实录;INTENT 发布 blocker 见 conversation-intent short-term
@@ -20,6 +20,7 @@
| Cursor-Auto | document-revision-sse-idempotency | fix/document-revision-sse-idempotency | - | docgov | done |
| Cursor-Auto | document-revision-active-doc-routing | fix/document-revision-active-doc-routing | - | docgov | done |
| Cursor-Auto | document-revision-quality-context | fix/document-revision-quality-context | - | docgov | in_progress |
| Cursor-Auto | document-revision-e2e-pending-flow | fix/document-revision-e2e-pending-flow | - | document-revision-runtime-ui | in_progress |
> 新任务开始前登记。`branch` 必须为 `feature/<slug>` 或 `fix/<slug>`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。
@@ -56,3 +57,4 @@
| 质量门禁自污染审计 | [../audit/2026-09-13-document-revision-quality-gate-self-pollution.md](../audit/2026-09-13-document-revision-quality-gate-self-pollution.md) |
| 当前文档路由审计 | [../audit/2026-09-13-document-revision-active-doc-routing.md](../audit/2026-09-13-document-revision-active-doc-routing.md) |
| 行动表状态假阳性审计 | [../audit/2026-09-13-document-revision-quality-status-false-positive.md](../audit/2026-09-13-document-revision-quality-status-false-positive.md) |
| 待确认报告选择失败审计 | [../audit/2026-09-13-document-revision-pending-report-selection.md](../audit/2026-09-13-document-revision-pending-report-selection.md) |
@@ -0,0 +1,49 @@
---
title: "E2E:待确认报告必须真实打开再确认/拒绝"
status: in_progress
created: 2026-09-13
updated: 2026-09-13
owner: Cursor-Auto
branch: fix/document-revision-e2e-pending-flow
worktree: "-"
mutex: document-revision-runtime-ui
---
# E2E:待确认报告必须真实打开再确认/拒绝
## 目标
第四轮 UI-05a 首错修复:验收脚本按真实用户路径关闭首轮报告、打开「待确认报告」,在面板内断言 high(critical 允许)与 pending 动作后再确认/拒绝。不改产品业务逻辑。
## 范围
- `web/e2e/document-revision-runtime.spec.ts` UI-04 / UI-05a / UI-05b
- 文档:audit / CURRENT 认领(mutex `document-revision-runtime-ui`)
## 非目标
- 不改 `ChangeReportPanel` / Toolbar / decision API
- 不降低已有业务断言或超时;不伪造、不直接打 API
- 不触碰/提交 `.doc/test-reports/assets`;不操作容器;本轮不重跑 E2E
- 不改 FEATURE-MATRIX
## 步骤
1. [x] 认领 CURRENT;写 audit/short-term
2. [x] UI-04 用弹窗内「关闭」并断言 dialog 消失
3. [x] UI-05a 点击「待确认报告」,断言 risk/pending-actions/确认提交后确认
4. [x] UI-05b 新 pending 后同样打开面板,断言 pending-actions/拒绝后再拒绝
5. [ ] 占槽后真实 UI 重跑(本轮不做)
## 验收标准
- [x] UI-04 不再使用无效 Escape
- [x] UI-05a/05b 不靠全局 count 代替打开待确认报告
- [x] web tests + `tsc`/build + `git diff --check`
- [ ] 真实 UI-05a/05b PASS 后才可将本任务标 `done`
## 相关链接
- Audit: [../../audit/2026-09-13-document-revision-pending-report-selection.md](../../audit/2026-09-13-document-revision-pending-report-selection.md)
- Evidence: [../../test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui05a-pending-report-selection-failure](../../test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui05a-pending-report-selection-failure)
- Checklist: [../checklists/2026-09-13-document-revision-runtime-ui-acceptance.md](../checklists/2026-09-13-document-revision-runtime-ui-acceptance.md)
+35 -11
View File
@@ -649,19 +649,23 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
const reportBtn = page.getByRole('button', { name: /变更报告|待确认报告/ });
await expect(reportBtn).toBeEnabled({ timeout: 15_000 });
await reportBtn.click();
const dialog = page.getByRole('dialog', { name: '变更报告' });
await expect(dialog).toBeVisible({ timeout: 20_000 });
await expect(page.getByTestId('change-report-panel')).toBeVisible({ timeout: 20_000 });
const panel = page.locator('text=风险').or(page.locator('text=开放问题')).or(page.locator('text=项目影响'));
await expect(panel.first()).toBeVisible({ timeout: 20_000 });
// Report list / body must be non-empty
const bodyText = await page.locator('body').innerText();
if (!/风险|开放问题|摘要|影响/.test(bodyText)) fail('change report panel missing required fields');
await shot(page, '04-first-report.png');
// Close if possible
await page.keyboard.press('Escape').catch(() => undefined);
await dialog.getByRole('button', { name: '关闭' }).click();
await expect(dialog).toHaveCount(0, { timeout: 10_000 });
await expect(page.getByTestId('change-report-panel')).toHaveCount(0);
recordPass({
id: 'UI-04',
name: 'change report fields',
input: 'click 变更报告',
actual: 'risk/impact/open questions visible',
input: 'click 变更报告; close via 关闭',
actual: 'risk/impact/open questions visible; dialog closed',
networkLogs: logs,
durationMs: Date.now() - t0,
verdict: 'PASS',
@@ -696,8 +700,21 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
test('UI-05a confirm high-risk candidate → version advances', async () => {
const t0 = Date.now();
logs = [];
const confirm = page.getByRole('button', { name: /确认提交/ });
if ((await confirm.count()) === 0) fail('confirm button missing for pending high-risk candidate');
const pendingBtn = page.getByRole('button', { name: '待确认报告' });
await expect(pendingBtn).toBeVisible({ timeout: 15_000 });
await pendingBtn.click();
const reportPanel = page.getByTestId('change-report-panel');
await expect(reportPanel).toBeVisible({ timeout: 20_000 });
const riskEl = reportPanel.getByTestId('change-report-risk');
await expect(riskEl).toBeVisible({ timeout: 20_000 });
const risk = (await riskEl.innerText()).trim().toLowerCase();
if (risk !== 'high' && risk !== 'critical') {
fail(`UI-05a pending report risk must be high (critical allowed); got ${JSON.stringify(risk)}`);
}
const actions = reportPanel.getByTestId('change-report-pending-actions');
await expect(actions).toBeVisible({ timeout: 15_000 });
const confirm = actions.getByRole('button', { name: '确认提交' });
await expect(confirm).toBeVisible({ timeout: 15_000 });
const decisionWait = page.waitForResponse(
(r) => r.url().includes('/decision') && r.request().method() === 'POST',
{ timeout: 60_000 },
@@ -716,8 +733,8 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
recordPass({
id: 'UI-05a',
name: 'confirm pending',
input: 'click 确认提交',
actual: `decision ok; version ${v}`,
input: 'click 待确认报告; assert pending actions; click 确认提交',
actual: `risk=${risk}; decision ok; version ${v}`,
networkLogs: logs,
durationMs: Date.now() - t0,
verdict: 'PASS',
@@ -735,8 +752,15 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
await expect(page.getByTestId('doc-rev-pending-badge')).toBeVisible({ timeout: 60_000 });
const { v: vPend } = await readVersionBadge(page);
if (vPend !== vBefore) fail(`second pending must not advance version (${vBefore}→${vPend})`);
const reject = page.getByRole('button', { name: /拒绝/ });
if ((await reject.count()) === 0) fail('reject button missing');
const pendingBtn = page.getByRole('button', { name: '待确认报告' });
await expect(pendingBtn).toBeVisible({ timeout: 15_000 });
await pendingBtn.click();
const reportPanel = page.getByTestId('change-report-panel');
await expect(reportPanel).toBeVisible({ timeout: 20_000 });
const actions = reportPanel.getByTestId('change-report-pending-actions');
await expect(actions).toBeVisible({ timeout: 15_000 });
const reject = actions.getByRole('button', { name: '拒绝' });
await expect(reject).toBeVisible({ timeout: 15_000 });
const decisionWait = page.waitForResponse(
(r) => r.url().includes('/decision') && r.request().method() === 'POST',
{ timeout: 60_000 },
@@ -753,7 +777,7 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
recordPass({
id: 'UI-05b',
name: 'reject pending',
input: prompt,
input: `${prompt}; click 待确认报告; click 拒绝`,
actual: `rejected; version stayed ${vAfter}`,
networkLogs: logs,
durationMs: Date.now() - t0,