merge: fix/controlled-content-ui-acceptance
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: "UI-06 页面刷新后受控正文停留加载骨架竞争态审计"
|
||||
status: planned
|
||||
created: 2026-09-14
|
||||
updated: 2026-09-14
|
||||
severity: high
|
||||
---
|
||||
|
||||
# UI-06 页面刷新后受控正文停留加载骨架竞争态审计
|
||||
|
||||
## 现象
|
||||
|
||||
在真实 UI 验收主阶段 `e2e/document-revision-runtime.spec.ts` 的 `UI-06` 用例中:
|
||||
1. 用户 A 双击段落完成人工修改并保存,PATCH 200 成功,修订版本从 `r0` 递增至 `r1`;
|
||||
2. 执行 `await page.reload()` 硬刷新页面;
|
||||
3. 页面重新挂载后,顶栏与修订徽章显示 `r1`,但正文区域长时间停留在骨架占位文案:
|
||||
`用户初始需求收集表 提示说明 正在从服务端加载文档正文… 受控状态: 编制中 • 已被编制人认领`
|
||||
4. 导致 `getByText(marker)` 超过 30 秒未能在 DOM 中找到,测试套件判定 **FAIL**。
|
||||
|
||||
## 根因分析
|
||||
|
||||
从 Playwright Trace (`1-trace.network` / `1-trace.trace`) 的毫秒级时序分析:
|
||||
|
||||
1. **后端数据与接口均正常**:
|
||||
- `03:22:19.374Z (+6.1ms) GET 200 /api/v1/docgov/documents/doc_proj6172_intake/content`
|
||||
- 返回数据快照 `resources/039f1e925ea0bf047bca866dba4e35cff8c72a2b.json` 第一行即包含本次修改标记 `# 人工修订段落 UI06-mu0o5hbq 用于硬刷新验收。`;
|
||||
- 修订接口 `GET 200 /api/v1/documents/doc_proj6172_intake/revision-state` 正确返回 `r1`;
|
||||
- 浏览器 Console 无任何报错。
|
||||
2. **前端内部状态清理竞争(App.tsx)**:
|
||||
- 页面重新挂载时,`useEffect` 触发受控正文拉取,在 `03:22:19.380Z` 将解析好的 AST 存入 `docCache[docId]` 并置 `controlledContentReady[docId] = true`;
|
||||
- 页面重新挂载同时触发的 `syncWorkflowDocs`(`GET /api/v1/projects/6172/documents?category=workflow`)在 `03:22:19.384Z`(仅晚 4ms)返回;
|
||||
- 在 [`web/src/App.tsx#L1746-L1765`](file:///home/weichen/projects/Agent-System/.worktrees/fix-controlled-content-ui-acceptance/web/src/App.tsx#L1746-L1765):
|
||||
```typescript
|
||||
const serverBackedIds = merged
|
||||
.filter((d) => d.projectId === currentProjectId && shouldLoadControlledContent(d))
|
||||
.map((d) => d.docId);
|
||||
if (serverBackedIds.length > 0) {
|
||||
setDocCache((cache) => {
|
||||
const next = { ...cache };
|
||||
serverBackedIds.forEach((docId) => {
|
||||
delete next[docId]; // 无条件删除缓存
|
||||
});
|
||||
return next;
|
||||
});
|
||||
setControlledContentReady((ready) => {
|
||||
const next = { ...ready };
|
||||
serverBackedIds.forEach((docId) => {
|
||||
delete next[docId]; // 无条件清空 ready
|
||||
});
|
||||
return next;
|
||||
});
|
||||
}
|
||||
```
|
||||
`syncWorkflowDocs` 无条件删除了 `docCache[activeDocId]` 与 `controlledContentReady[activeDocId]`。
|
||||
3. **死锁成因**:
|
||||
- 此时 `docAST` 判定 `docCache` 缺失且 `controlledContentReady` 为 false,回退至 `buildControlledLoadingSkeleton`;
|
||||
- 在 [`web/src/App.tsx#L1258`](file:///home/weichen/projects/Agent-System/.worktrees/fix-controlled-content-ui-acceptance/web/src/App.tsx#L1258),拉取正文的 `useEffect` 依赖为 `[session, sessionValidated, activeDocId, activeHydrateFingerprint, applyHydratedDocument, hydrateResumeKey]`;
|
||||
- `activeHydrateFingerprint` 仅包含 `docId`、`stage`、`updatedAt` 等,在缓存被清空后并未发生变化,导致不会再次唤醒网络拉取;
|
||||
- 页面永久锁死在加载骨架屏。
|
||||
|
||||
## 严重度
|
||||
|
||||
`high`(阻断主阶段硬刷新、登出重登持久化及后续多文档验收)。
|
||||
|
||||
## 建议整改方案(供 Cursor 参考)
|
||||
|
||||
1. `syncWorkflowDocs` 不应对所有 `serverBackedIds` 进行盲目的全量缓存与 ready 标志清空;应比对服务端返回的 `updatedAt` / `sha256` / `stage`,仅在真正发生更新时定向失效;
|
||||
2. 或者,若主动清空了已加载文档的缓存,必须同步唤醒重新拉取(如递增 `hydrateResumeKey`),避免状态丢失后无重入机制。
|
||||
|
||||
## 证据路径
|
||||
|
||||
- 报告:`.doc/test-reports/2026-09-14-controlled-content-ui-acceptance.md`
|
||||
- Trace:`.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance/full-chain-valid/playwright-output-main/document-revision-runtime--d72a3-ard-refresh-persists-r-bump-chromium/trace.zip`
|
||||
- 截图:`.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance/full-chain-valid/playwright-output-main/document-revision-runtime--d72a3-ard-refresh-persists-r-bump-chromium/test-failed-1.png`
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
## 焦点
|
||||
|
||||
1. **DOC-CONTENT**: [受控正文 canonical hash 漂移](../audit/2026-09-14-project143-controlled-content-404.md)(`fix/controlled-content-canonical-hash` 实现完成;正在占槽合入 dev、迁移及验收)
|
||||
1. **DOC-CONTENT**: [受控正文 canonical hash 漂移](../audit/2026-09-14-project143-controlled-content-404.md)(真实 UI 验收已 PASS:项目 143 clean-run3 + 新项目 full-chain-final 主链及 restart 均全部通过)
|
||||
2. **CHAT-INTENT**: [对话意图运行时状态](../audit/2026-09-13-conversation-intent-runtime-state.md)(`fix/conversation-intent-state`;与文档账本运行时无关)
|
||||
3. **CHAT**: [对话历史刷新](../audit/2026-09-13-chat-history-refresh-disappears.md)(认领仍 `in_progress`;gap 行已 PASS,以代码/handoff 为准收敛)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
| owner | task | branch | worktree | mutex | status |
|
||||
|-------|------|--------|----------|-------|--------|
|
||||
| verifier_antigravity | controlled-content-ui-acceptance | fix/controlled-content-ui-acceptance | .worktrees/fix-controlled-content-ui-acceptance | document-content-ui-acceptance | done |
|
||||
| implementer_cursor | controlled-content-canonical-hash | fix/controlled-content-canonical-hash | .worktrees/fix-controlled-content-canonical-hash | docgov | done |
|
||||
| Codex | project143-controlled-content-404-diagnosis | fix/project143-controlled-content-404-diagnosis | - | docgov-content-diagnosis | done |
|
||||
| Codex | software-project-bootstrap-plan | feature/software-project-bootstrap-plan | - | workflow-design | done |
|
||||
@@ -52,7 +53,7 @@
|
||||
|
||||
| holder | phase | since | scope | queue |
|
||||
|--------|-------|-------|-------|-------|
|
||||
| orchestrator_codex | merging | 2026-09-14 | `fix/controlled-content-canonical-hash` 合 dev、schema 32、隔离 PG、项目 143 定向修复及 UI 验收 | — |
|
||||
| idle | idle | - | - | - |
|
||||
|
||||
**phase**: `idle` | `merging` | `smoke` | `e2e`
|
||||
|
||||
@@ -62,7 +63,7 @@
|
||||
|
||||
| 环境 | 端口 | schema | 状态 |
|
||||
|------|------|--------|------|
|
||||
| dev | 8080 | **31** | healthy(Round20 项目 6159:测试前 `env-service.sh dev rebuild` 部署 `b258ab8`;主阶段后仅 `restart shuimu-agent-app-dev`;文档收口不再动容器) |
|
||||
| dev | 8080 | **32** | healthy(产品代码部署为 `36e4bc3`;真实 UI 主链+restart PASS) |
|
||||
| test | 8081 | 25→27* | healthy |
|
||||
| main | 8000 | 25→27* | healthy(app + runner + DB) |
|
||||
|
||||
@@ -100,3 +101,5 @@
|
||||
| UI-13a 职责单元格误判审计 | [../audit/2026-09-13-document-revision-duty-cell-assertion.md](../audit/2026-09-13-document-revision-duty-cell-assertion.md) |
|
||||
| UI-13a reviewer 职责未同步审计 | [../audit/2026-09-13-document-revision-reviewer-duty-sync.md](../audit/2026-09-13-document-revision-reviewer-duty-sync.md) |
|
||||
| UI-03 首次生成 PG ID 误删 baseline | [../audit/2026-09-13-document-revision-pg-id-remap.md](../audit/2026-09-13-document-revision-pg-id-remap.md) |
|
||||
| UI-06 刷新后缓存竞争死锁审计 | [../audit/2026-09-14-document-revision-refresh-cache-race.md](../audit/2026-09-14-document-revision-refresh-cache-race.md) |
|
||||
| 受控正文真实 UI 验收报告 | [../test-reports/2026-09-14-controlled-content-ui-acceptance.md](../test-reports/2026-09-14-controlled-content-ui-acceptance.md) |
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# 受控文档正文修复真实 UI 验收 Checklist
|
||||
|
||||
## 执行身份与环境
|
||||
|
||||
- [x] 报告 Antigravity CLI / 实际模型。
|
||||
- [x] 阅读 `AGENTS.md`、`PROJECT.md`、`.doc/README.md`、`.doc/ops/CURRENT.md`、任务文、审计与整改方案。
|
||||
- [x] 使用 `mcr.microsoft.com/playwright:v1.63.0-noble`、`--network host`、`BASE_URL=http://127.0.0.1:3100`。
|
||||
- [x] acceptance worktree 挂载 `/work`,主仓 `web/node_modules` 只读挂载到 `/work/web/node_modules`。
|
||||
- [x] 管理员凭证只从 `shuimu-agent-app-dev` 容器环境安全传入,不输出、不截图、不提交。
|
||||
|
||||
## 强制边界
|
||||
|
||||
- [x] 用户、项目、成员、对话、文档、小队、编辑等数据只由 Playwright `page` 的真实 UI 操作产生。
|
||||
- [x] 不用 `page.request`、`context.request`、curl、API 直调、DB 写、route/mock/stub、localStorage 注入。
|
||||
- [x] 网络响应只用于监听和断言;PASS 必须同时有页面可见证据。
|
||||
- [x] 不复用主仓旧 phase state/旧 PASS;失败原样记录。
|
||||
- [x] 不从 worktree 重启共享 Docker;主阶段后输出 `NEEDS_HOST_RESTART`(若主阶段遇到 FAIL 阻塞,则先定位并等待修复,不提前重启)。
|
||||
|
||||
## A. 项目 143 回归(clean-run3 全部 PASS)
|
||||
|
||||
- [x] admin 从登录页登录,经 UI 打开项目标题“对话协同会话”(原项目143对应UI会话)。
|
||||
- [x] 经 UI 打开 `doc_proj143_intake`(用户初始需求收集表),正文非空、账本就绪。
|
||||
- [x] console/pageerror 无相关错误,content 与 revision-state 均无 404;初始 legacy doc-001 请求严格为 0。
|
||||
- [x] 初次稳定后空闲 30 秒,无新增重复正文请求、无闪烁。
|
||||
- [x] 双击真实块,追加唯一中文标记,保存后页面可见且修订号增加(r1 -> r2)。
|
||||
- [x] 纯净阅读 / 人工修改 / 全部变更均验证 active 状态与真实装饰差异。
|
||||
- [x] 刷新、退出、重新登录,再打开同一项目/文档,标记与修订号持久化。
|
||||
- [x] 截图、trace、结果写 `.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance/clean-run3/`。
|
||||
|
||||
## B. 新项目真实全链路(full-chain-final 全部 PASS)
|
||||
|
||||
- [x] 审读并运行 `web/e2e/document-revision-runtime.spec.ts` main phase;该测试只能通过 UI 建 A/B/C、新项目和全部数据。
|
||||
- [x] 真实产品 PI/小队完成初次文档生成和二次修改;页面显示本轮用户输入与 assistant 终态回复。
|
||||
- [x] 页面显示文档正文、版本账本、人工编辑、三模式(UI-06 刷新后通过,UI-07/08/09 三模式计算样式互斥通过)。
|
||||
- [x] 页面打开真实 change report,并显示项目影响、文档影响、风险、开放问题。
|
||||
- [x] 并发编辑、审核人、无关用户权限边界有 UI 证据(UI-12/13a/13b 均通过)。
|
||||
- [x] 产生本轮非敏感 phase state、截图、results 并提交 Git;本地运行产生的 trace/video/HTML 报告未提交仓库;本地 secrets 继续 ignored 且绝不披露。
|
||||
- [x] 主阶段写报告草稿并输出 `NEEDS_HOST_RESTART`,等待 orchestrator 重启。
|
||||
|
||||
## C. 重启后(由 orchestrator 重启并执行 post-restart 全部 PASS)
|
||||
|
||||
- [x] 仅用 UI 重新登录、打开本轮新项目与文档(UI-15 post-restart 2.8s 通过)。
|
||||
- [x] 正文唯一标记(A1-win-mu0oymfg)、版本/修订(V3/r2)页面可见。
|
||||
- [x] 目标受控文档 404 严格为 0。
|
||||
- [x] 完成报告;如实记录全部结果,主阶段与重启后产物完好归档。
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: "受控文档正文修复真实 UI 实战验收"
|
||||
status: done
|
||||
owner: verifier_antigravity
|
||||
branch: fix/controlled-content-ui-acceptance
|
||||
mutex: document-content-ui-acceptance
|
||||
created: 2026-09-14
|
||||
updated: 2026-09-14
|
||||
---
|
||||
|
||||
# 目标
|
||||
|
||||
仅通过真实浏览器页面操作,验证项目 143 历史文档恢复,并以真实产品 PI Agent Runner / 智能体小队完成新项目文档全链路。API、数据库和 stub 不能替代 UI 验收结果。
|
||||
|
||||
# 范围
|
||||
|
||||
- 项目 143:登录、打开正文、30 秒空闲、双击编辑保存、三种变更视图、刷新与重新登录持久化。(clean-run3 已全部 PASS)
|
||||
- 新项目:从 UI 创建,真实对话触发工作流/小队生成文档,再次对话触发修改;页面查看助手回复、文档正文、版本账本、影响、风险、开放问题。(full-chain-final UI-00..UI-16 全部 18 项 PASS)
|
||||
- 冲突与权限:双页面并发编辑、审核人/无关用户访问边界均通过;宿主容器重启后 post-restart 复验通过。
|
||||
- Ubuntu Server 浏览器:统一使用 Playwright 官方 `mcr.microsoft.com/playwright:v1.63.0-noble` 容器。
|
||||
|
||||
# 红线
|
||||
|
||||
- 不用 API 直建用户、项目、会话、文档或测试数据。
|
||||
- 不注入/伪造 assistant 消息、文档、账本、change report 或通过结果。
|
||||
- 所有通过项必须有页面可见证据;失败如实记录为 FAIL。
|
||||
- 测试账号凭证只读本机既有 secrets/phase 文件,不输出、不提交。
|
||||
|
||||
# 阶段判据与进展
|
||||
|
||||
- [x] 项目 143 页面正文可见且无 content/revision 404(UI-143-01..06 全 PASS,产物在 `clean-run3/`)。
|
||||
- [x] 人工编辑、视图切换、刷新/重登持久化通过(项目 143 回归)。
|
||||
- [x] 新项目真实 PI/小队链路及文档影响风险报告页面可见(UI-00..05b 通过,首轮 toolbar V2/r0,UI-05 pending 保持 V2,UI-05a 确认后晋升 V3,UI-05b 拒绝后保持 V3)。
|
||||
- [x] UI-06 硬刷新正文保留通过(合入 08bf615 修复后 1.1 秒内秒级通过)。
|
||||
- [x] 并发、权限、重启前凭据生成有真实 UI 判定(UI-12/13a/13b/15/16 全 PASS)。
|
||||
- [x] 容器重启后恢复核验通过(UI-15 post-restart 2.8 秒秒级通过,V3/r2 与标记完好呈现,目标 404=0)。
|
||||
- [x] 测试报告落盘至 `.doc/test-reports/2026-09-14-controlled-content-ui-acceptance.md`。
|
||||
@@ -21,7 +21,7 @@
|
||||
| 工作流治理 | 三正式工作流 + 阶段门禁 | done | [remediation/three-workflows](../remediation/2026-09-12-three-workflows-strict-governance-remediation.md);[test 2026-09-12](../test-reports/2026-09-12-three-workflows-governance-remediation-test-report.md) | Phase2 设计器 partial | `workflow` |
|
||||
| 纯软件项目自举 | 软件项目创建、Bootstrap 基线、Delivery 迭代与受控工程交接 | planned | [详细设计](../system-design/2026-09-14-software-project-bootstrap-workflow.md);[现状调研](../ops/research/2026-09-14-software-project-bootstrap-current-state.md);[验收计划](../ops/checklists/2026-09-14-software-project-bootstrap-acceptance.md) | 当前仅完成方案;目录/模板/执行器/创建向导/真实 PI UI 均未实施。受控代码执行单列后续 Epic | `workflow-design` |
|
||||
| IAM / 鉴权 | 项目级 API 鉴权 + RBAC | partial | [audit/authorization](../audit/authorization-matrix-v2.md);[刷新恢复专项审计](../audit/2026-09-13-chat-history-refresh-disappears.md) | 历史读取端点补统一会话 owner + 阶段授权;旧 API 410 标注(BUG-11 partial) | `authz` |
|
||||
| 文档治理 | 法定文档生命周期 + 租约 + 受控正文读取 | partial | [test docgov](../test-reports/2026-09-08-docgov-lifecycle-and-claiming-framework-test-report.md);[项目 143 正文 404 审计](../audit/2026-09-14-project143-controlled-content-404.md);[整改](../remediation/2026-09-14-controlled-content-canonical-hash-remediation.md);[handoff](../ops/handoffs/2026-09-14-controlled-content-canonical-hash.md) | 生命周期/租约保留。W1–W6 + 审查修补 + UI hydrate 资格 + 晚到 list sync 不再擦掉成功 hydrate。工程门禁已在 `fix/controlled-content-canonical-hash` 通过。隔离 PG apply、合 dev、143 apply、真实 UI 未做,不得标 done / PASS | `docgov` |
|
||||
| 文档治理 | 法定文档生命周期 + 租约 + 受控正文读取 | done | [test docgov](../test-reports/2026-09-08-docgov-lifecycle-and-claiming-framework-test-report.md);[项目 143 正文 404 审计](../audit/2026-09-14-project143-controlled-content-404.md);[受控正文真实 UI 验收报告](../test-reports/2026-09-14-controlled-content-ui-acceptance.md);[UI-06 缓存竞争审计](../audit/2026-09-14-document-revision-refresh-cache-race.md);[整改](../remediation/2026-09-14-controlled-content-canonical-hash-remediation.md) | 生命周期/租约保留。W1–W6 + 审查修补 + UI hydrate 资格 + 晚到 list sync 修复均已合入 dev。项目 143 纯 UI 回归(clean-run3)与新项目全链路(main + post-restart)已全量真实页面验证通过(PASS) | `docgov` |
|
||||
| 文档版本与修改认知 | 智能体版本边界、人工修订标记、任意版本比较、变更报告与负责人通知 | partial | [管理测试](../test-reports/2026-09-13-document-revision-management-test-report.md);[运行时 UI Round20](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md);[运行时审计](../audit/2026-09-13-document-revision-runtime-404.md);[ADR](../adr/2026-09-13-document-version-boundary.md) | **运行时 UI PASS**(6159 / V3/r2)。仍 `partial`:change report=`fallback`(真实 diff+lineage,非完整 PI 叙事)。系统级 `CHAT-INTENT` 见 gap-matrix | `docgov` |
|
||||
| 前端工作台 | 三栏 + 块文档 + 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` |
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
# 受控文档正文修复真实 UI 验收测试报告
|
||||
|
||||
- **日期**: 2026-09-14
|
||||
- **测试执行者**: Antigravity (Google DeepMind)
|
||||
- **判定汇总**:
|
||||
- **Part A (项目 143 纯 UI 回归)**: **PASS** (UI-143-01 .. UI-143-06 全通)
|
||||
- **Part B (新项目全链路主阶段)**: **PASS** (全量重跑 `full-chain-final`,UI-00 .. UI-16 全部 18 项 PASS)
|
||||
- **Part C (容器重启后恢复核验)**: **PASS** (宿主容器重启后 `UI-15` post-restart 2.8s 秒级通过)
|
||||
- **受控正文与版本账本运行时综合判定**: **PASS**
|
||||
- **整个系统 `production_ready`**: **no**(保留无关系统级 blocker,如 `CHAT-INTENT-STATE` 等;本报告不把模块 PASS 写成全仓可发布)
|
||||
|
||||
---
|
||||
|
||||
## 一、 测试背景与核心约束
|
||||
|
||||
受控文档正文拉取此前存在模板槽未就绪、假 `doc-upload` 占位、正文未进入不可变账本及初始 legacy `doc-001` 水合等问题。在开发分支依次合入 `d0e8f60`(含 `f693e1c` 停止水合本地 legacy 假文档)与 `36e4bc3`(含 `08bf615` 修复工作流同步导致的缓存清空死锁)后,本轮针对以下部分进行严格的真实 UI 验收:
|
||||
1. **项目 143 独立回归**:验证既有项目在纯页面交互下的文档加载、30 秒静置稳定性、双击块编辑保存与修订递增、三模式对比及登出重登持久化。
|
||||
2. **新项目全链路主阶段**:验证管理员建用户、用户建项目、真实小队生成受控文档、打开变更报告、高风险候选确认与拒绝、块编辑保存与硬刷新、三模式对比、版本对比、第二受控文档、页面上传、权限隔离、并发冲突以及重启前持久化等全流程。
|
||||
3. **重启后恢复核验(post-restart)**:宿主机执行 `./scripts/env-service.sh dev restart` 容器重启后,纯页面登录,核对同一项目、docId、最终正文标记、版本 V3/r2,验证目标 404 严格为 0。
|
||||
|
||||
### 强制测试边界
|
||||
- **零后端旁路**: 严禁 `psql`、`docker exec`、API 直调、`curl`、`page.route` mock/stub 或 `localStorage` 注入。全部用户、项目、会话、任务、正文、修订必须由真实的页面点击、输入与等待生成。
|
||||
- **环境隔离与有效性**:
|
||||
- `clean-run3/`:项目 143 纯页面回归最终有效验收产物(有效,全部 PASS)。
|
||||
- `full-chain-final/`:新项目全链路主阶段(UI-00 至 UI-16)与重启后恢复(UI-15 post-restart)最终有效产物(有效,全部 PASS)。
|
||||
- `full-chain-valid/`:早期未修复缓存死锁时的历史有效运行记录(UI-00 至 UI-05b PASS,UI-06 FAIL 留档,不冒充通过)。
|
||||
- `invalid-db-assisted-run1/`:包含 DB 辅助查询,归档标记为 **INVALID**,不计入。
|
||||
- `invalid-overlap-main/`:因已取消旧容器与新容器在输出目录清理竞争产生 ENOENT,归档标记为 **INVALID**,不计入。
|
||||
- `clean-run2-fail/`:早期历史失败产物,归档保留。
|
||||
|
||||
---
|
||||
|
||||
## 二、 测试方案与 Ubuntu Server 容器运行
|
||||
|
||||
### 2.1 运行环境
|
||||
- **操作系统**: Linux (Ubuntu Server,无图形桌面 / 无需宿主安装浏览器)
|
||||
- **容器镜像**: `mcr.microsoft.com/playwright:v1.63.0-noble`,参数 `--network host --ipc host`
|
||||
- **目标服务**: `BASE_URL=http://127.0.0.1:3100`(前端 Vite),后端 `shuimu-agent-app-dev`(端口 8080,schema 32,PI Agent Ready)
|
||||
- **挂载结构**:
|
||||
- 工作树根目录挂载至容器内 `/work`
|
||||
- 主仓 `web/node_modules` 只读挂载至 `/work/web/node_modules:ro`
|
||||
- **凭证安全处理**:
|
||||
- 管理员凭据仅通过本地运行容器环境变量 `ADMIN_USERNAME` 与 `ADMIN_PASSWORD` 读取,并在启动 `docker run` 时安全注入。
|
||||
- 测试脚本、断言、截图、Trace 与报告中**严禁包含或打印任何口令、Token 或密钥**。
|
||||
- 本地运行生成的 `phase-secrets.local.json` 包含测试账号临时凭证,已在 `.gitignore` 中严格忽略,绝不提交至仓库。
|
||||
|
||||
### 2.2 官方 Playwright 容器执行命令
|
||||
|
||||
```bash
|
||||
# 1. 项目 143 纯页面回归验收(Part A)
|
||||
docker run --rm --network host --ipc host \
|
||||
-e RUN_UI_ACCEPTANCE=1 \
|
||||
-e BASE_URL=http://127.0.0.1:3100 \
|
||||
-e UI_ACCEPTANCE_ASSETS_DIR=/work/.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance/clean-run3 \
|
||||
-e ADMIN_USERNAME="$(docker inspect shuimu-agent-app-dev --format '{{range .Config.Env}}{{println .}}{{end}}' | grep '^ADMIN_USERNAME=' | cut -d= -f2-)" \
|
||||
-e ADMIN_PASSWORD="$(docker inspect shuimu-agent-app-dev --format '{{range .Config.Env}}{{println .}}{{end}}' | grep '^ADMIN_PASSWORD=' | cut -d= -f2-)" \
|
||||
-v "/home/weichen/projects/Agent-System/.worktrees/fix-controlled-content-ui-acceptance:/work" \
|
||||
-v "/home/weichen/projects/Agent-System/web/node_modules:/work/web/node_modules:ro" \
|
||||
-w /work/web \
|
||||
mcr.microsoft.com/playwright:v1.63.0-noble \
|
||||
npx playwright test e2e/project143-regression.spec.ts --project=chromium
|
||||
|
||||
# 2. 新项目全链路主阶段验收(Part B - main)
|
||||
docker run --rm --network host --ipc host \
|
||||
-e RUN_UI_ACCEPTANCE=1 \
|
||||
-e UI_PHASE=main \
|
||||
-e BASE_URL=http://127.0.0.1:3100 \
|
||||
-e UI_ACCEPTANCE_ASSETS_DIR=/work/.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance/full-chain-final \
|
||||
-e ADMIN_USERNAME="$(docker inspect shuimu-agent-app-dev --format '{{range .Config.Env}}{{println .}}{{end}}' | grep '^ADMIN_USERNAME=' | cut -d= -f2-)" \
|
||||
-e ADMIN_PASSWORD="$(docker inspect shuimu-agent-app-dev --format '{{range .Config.Env}}{{println .}}{{end}}' | grep '^ADMIN_PASSWORD=' | cut -d= -f2-)" \
|
||||
-v "/home/weichen/projects/Agent-System/.worktrees/fix-controlled-content-ui-acceptance:/work" \
|
||||
-v "/home/weichen/projects/Agent-System/web/node_modules:/work/web/node_modules:ro" \
|
||||
-w /work/web \
|
||||
mcr.microsoft.com/playwright:v1.63.0-noble \
|
||||
npx playwright test e2e/document-revision-runtime.spec.ts --project=chromium
|
||||
|
||||
# 3. 新项目重启后恢复核验(Part C - post-restart)
|
||||
docker run --rm --network host --ipc host \
|
||||
-e RUN_UI_ACCEPTANCE=1 \
|
||||
-e UI_PHASE=post-restart \
|
||||
-e BASE_URL=http://127.0.0.1:3100 \
|
||||
-e UI_ACCEPTANCE_ASSETS_DIR=/work/.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance/full-chain-final \
|
||||
-e ADMIN_USERNAME="$(docker inspect shuimu-agent-app-dev --format '{{range .Config.Env}}{{println .}}{{end}}' | grep '^ADMIN_USERNAME=' | cut -d= -f2-)" \
|
||||
-e ADMIN_PASSWORD="$(docker inspect shuimu-agent-app-dev --format '{{range .Config.Env}}{{println .}}{{end}}' | grep '^ADMIN_PASSWORD=' | cut -d= -f2-)" \
|
||||
-v "/home/weichen/projects/Agent-System/.worktrees/fix-controlled-content-ui-acceptance:/work" \
|
||||
-v "/home/weichen/projects/Agent-System/web/node_modules:/work/web/node_modules:ro" \
|
||||
-w /work/web \
|
||||
mcr.microsoft.com/playwright:v1.63.0-noble \
|
||||
npx playwright test e2e/document-revision-runtime.spec.ts --project=chromium
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、 Part A: 项目 143 独立回归验收(`clean-run3` 全部 PASS)
|
||||
|
||||
- **测试套件**: `web/e2e/project143-regression.spec.ts`
|
||||
- **产物路径**: `.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance/clean-run3/`
|
||||
- **结果汇总**:
|
||||
|
||||
| 用例 ID | 步骤与验证点 | 耗时 | 判定 | 真实观测数据与证据 |
|
||||
|:---|:---|:---:|:---:|:---|
|
||||
| **UI-143-01** | 管理员登录并进入项目会话 | 1.74s | **PASS** | 明确校验从登录开始 **`doc-001` content/revision 请求数为 0**;准确在侧栏匹配并点击「对话协同会话」;目标受控文档 404 为 0 |
|
||||
| **UI-143-02** | 菜单确认打开目标正文与账本就绪 | 0.30s | **PASS** | `doc_proj143_intake` 正文成功渲染 146 块、文本 16,642 字符;版本账本就绪显示 `V1` / `r1`;无未就绪提示;目标 404 为 0 |
|
||||
| **UI-143-03** | 30 秒静置稳定性观测 | 30.11s | **PASS** | 静置期间**重复正文请求数为 0**;控制台零错误,页面无异常,目标 404 严格为 0 |
|
||||
| **UI-143-04** | 双击块编辑、追加纯中文标记与保存 | 0.70s | **PASS** | 首块追加 `(人工验收标记1789355089269)`;PATCH 200 成功;页面呈现可见文本;修订号由 **`r1` 递增至 `r2`** |
|
||||
| **UI-143-05** | 纯净阅读 / 人工修改 / 全部变更模式切换 | 2.25s | **PASS** | 纯净阅读无遮罩;人工修改与全部变更模式均带有计算样式遮罩修饰;计算样式互不相同,视觉区分明确 |
|
||||
| **UI-143-06** | 页面刷新、退出登录及重新登录持久化 | 2.43s | **PASS** | 页面硬刷新后标记与 `r2` 存在;UI 登出后重登打开文档,标记与 `r2` 完整持久化;目标文档所有接口全程 404 为 0 |
|
||||
|
||||
---
|
||||
|
||||
## 四、 Part B: 新项目全链路主阶段最终验收(`full-chain-final` 全部 PASS)
|
||||
|
||||
- **测试套件**: `web/e2e/document-revision-runtime.spec.ts` (`UI_PHASE=main`)
|
||||
- **产物路径**: `.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance/full-chain-final/`
|
||||
- **项目信息**: `UI验收-超声微创系统-mu0oymfg`(项目 ID: `6173`)
|
||||
- **账号体系**: 用户 A (`uia_mu0oymfg`),用户 B (`uib_mu0oymfg`),用户 C (`uic_mu0oymfg`)
|
||||
- **首份受控文档**: `doc_proj6173_intake`(用户初始需求收集表)
|
||||
- **终态 phase-state**: `V3` / `r2` / `bodySnippet=A1-win-mu0oymfg`
|
||||
- **套件总耗时**: 11.7 分钟(18 passed / 1 skipped)
|
||||
- **逐项结果清单**:
|
||||
|
||||
| 用例 ID | 步骤与验证点 | 耗时 | 判定 | 真实页面证据 |
|
||||
|:---|:---|:---:|:---:|:---|
|
||||
| **UI-00** | 管理员页面创建 A/B/C 并初始化密码 | 8.7s | **PASS** | 经管理界面创建三账号,分别重置首登凭据 |
|
||||
| **UI-01** | 用户 A 登录并新建项目,验证模板提示隔离 | 1.3s | **PASS** | 创建项目「UI验收-超声微创系统-mu0oymfg」,无 doc-p 伪正文泄漏 |
|
||||
| **UI-02** | 对话框向真实小队发送首轮摸底任务 | 310ms | **PASS** | 任务输入指令发送,PI Agent 接单运行 |
|
||||
| **UI-03** | 真实等待助手回复与受控文档生成 | 2.7m | **PASS** | 助手回复完成,首份受控文档 `doc_proj6173_intake` 生成,toolbar `V2/r0`,标题为「用户初始需求收集表」 |
|
||||
| **UI-04** | 打开变更报告并核验完整字段 | 246ms | **PASS** | 页面展开「变更报告」,风险评级、项目影响、文档影响、开放问题均完整可见 |
|
||||
| **UI-05** | 高风险候选 #1 挂起(待确认状态) | 2.6m | **PASS** | 二轮高风险任务触发,受控正文冻结在 head,pending 时保持 `V2`,版本不提前晋升 |
|
||||
| **UI-05a**| 页面点击「确认提交」高风险候选 | 322ms | **PASS** | POST `/decision`(`commit`, `confirmed: true`),确认后版本晋升至 `V3` |
|
||||
| **UI-05b**| 二轮高风险候选 #2 页面点击「拒绝」 | 2.7m | **PASS** | POST `/decision`(`reject`),二轮拒绝后版本保持 `V3`,未被非法合入 |
|
||||
| **UI-06** | 双击块编辑保存后,硬刷新验证标记与修订持久化 | 1.1s | **PASS** | **在合入 08bf615 修复后 1.1 秒内秒级通过!** 刷新后正文立即渲染,标记与 `r1` 完整可见 |
|
||||
| **UI-07..09** | 纯净阅读 / 人工修改 / 全部变更三模式对比 | 2.4s | **PASS** | 纯净阅读无遮罩;人工修改与全部变更模式带有琥珀/修饰样式,计算样式互斥生效 |
|
||||
| **UI-10** | 版本历史与两版本 A/B 比较 | 450ms | **PASS** | 打开版本历史,选择两版进行对比,比较窗口正确呈现 `+ / - / ~` 差异项 |
|
||||
| **UI-11** | 对话触发第二受控文档 + 页面上传本地材料 | 3.1m | **PASS** | 第二文档状态为 intake(用户初始需求收集表)`V3/r1`、prelim_design(系统初步设计方案)`V2/r0`;页面上传归档为“私密参考文档”,POST 201,激活 `doc-free-*`(`doc-free-1789357696535085171-198`)面板 |
|
||||
| **UI-12** | 越权隔离验证:非项目成员 C 无法看到项目 | 2.6s | **PASS** | 用户 C 登录工作台,侧栏与项目列表中项目均不可见(零泄漏) |
|
||||
| **UI-13a**| 审核人只读验证:协作者 B 无正文编辑入口 | 2.1s | **PASS** | 用户 B 登录打开受控文档,双击段落不唤起编辑对话框,操作权限收口为只读 |
|
||||
| **UI-13b**| 并发冲突验证:两作者上下文同修订提交报 409 | 2.7s | **PASS** | 上下文 A2 提交落后 revision 时页面报 409 冲突,保留冲突稿,A1 赢家正文持久生效 |
|
||||
| **UI-14** | 登出并重新登录恢复 | 1.3s | **PASS** | UI 登出用户 A,重新登录后项目与会话状态完好恢复 |
|
||||
| **UI-15** | 主阶段收尾:生成重启前持久化凭据与 phase-state | 114ms | **PASS** | 记录非敏感 `phase-state.json`,标记 `A1-win-mu0oymfg` |
|
||||
| **UI-16** | 注册上传交付物使用独立账本 | 366ms | **PASS** | 验证注册上传物不产生 doc-upload 孤儿槽,账本独立健康 |
|
||||
|
||||
---
|
||||
|
||||
## 五、 Part C: 容器重启后恢复核验(`post-restart` 全部 PASS)
|
||||
|
||||
- **测试套件**: `web/e2e/document-revision-runtime.spec.ts` (`UI_PHASE=post-restart`)
|
||||
- **执行命令**: 官方 Playwright 容器执行,`UI_PHASE=post-restart`
|
||||
- **执行耗时**: 2.8 秒 (1 passed / 18 skipped)
|
||||
- **结果汇总**:
|
||||
|
||||
| 用例 ID | 步骤与验证点 | 耗时 | 判定 | 真实页面证据 |
|
||||
|:---|:---|:---:|:---:|:---|
|
||||
| **UI-15 (post-restart)** | 容器重启后纯页面登录恢复项目与受控文档 | 2.8s | **PASS** | 用户 A 经页面登录,点击进入项目「UI验收-超声微创系统-mu0oymfg」;受控文档 `doc_proj6173_intake` 正确打开;版本 `V3` / 修订 `r2` 完全吻合;最终赢家正文标记 `A1-win-mu0oymfg` 页面完整可见;目标文档接口 404 全程严格为 0 |
|
||||
|
||||
### 5.1 重启后目标文档网络请求核查
|
||||
从 `ui-acceptance-results-post-restart.json` 提取的关于目标受控文档 `doc_proj6173_intake` 的网络日志如下:
|
||||
- `GET /api/v1/documents/doc_proj6173_intake/revision-state` -> **200 OK**
|
||||
- `GET /api/v1/docgov/documents/doc_proj6173_intake/content` -> **200 OK**
|
||||
- `GET /api/v1/documents/doc_proj6173_intake/versions` -> **200 OK**
|
||||
- `GET /api/v1/documents/doc_proj6173_intake/manual-overlay` -> **200 OK**
|
||||
- **目标文档 404 计数**: **严格为 0**
|
||||
|
||||
---
|
||||
|
||||
## 六、 产物与证据归档清单
|
||||
|
||||
本轮提交至仓库的证据与 `git ls-files` 严格一致(保存在 `.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance/`):
|
||||
|
||||
1. **项目 143 独立回归 (`clean-run3/`)**:
|
||||
- 结构化结果: `clean-run3/proj143-ui-acceptance-results.json`
|
||||
- 全套页面截图: 13 张(`UI-143-01` 至 `UI-143-08d`)
|
||||
- 会话视频: `clean-run3/videos/page@eedfc29121e052e4ca3355f0542261dd.webm`
|
||||
2. **新项目全链路最终产物 (`full-chain-final/`)**:
|
||||
- 结构化结果: `full-chain-final/ui-acceptance-results.json`(主阶段 18 passed)与 `full-chain-final/ui-acceptance-results-post-restart.json`(重启后 UI-15 passed)
|
||||
- 重启状态凭据: `full-chain-final/phase-state.json`
|
||||
- 页面截图证据: 21 张(含主阶段 20 张截图 + `UI-15-post-restart.png`)
|
||||
- 上传测试载荷: `full-chain-final/upload-mu0oymfg.txt`
|
||||
- *未提交产物与凭证说明*: Playwright Trace、录屏视频与 HTML 测试报告(`playwright-report-main/`, `playwright-report-post-restart/`)仅为本地测试运行产物且未提交 Git,不得声称仓库完整保留;`phase-secrets.local.json` 保持被 `.gitignore` 忽略,未提交且不得披露。
|
||||
|
||||
---
|
||||
|
||||
## 七、 结论
|
||||
|
||||
- 本次针对受控文档正文与版本账本的纯 UI 真实端到端验收(项目 143 恢复回归 + 新项目主阶段 + 容器重启后恢复)**全部通过(PASS)**。
|
||||
- 整个系统保留未关闭的系统级无关 blocker(`CHAT-INTENT-STATE` 等),系统级 `production_ready` 保持为 `no`。
|
||||
@@ -0,0 +1,7 @@
|
||||
# CLEAN RUN 2 (FAIL)
|
||||
|
||||
Status: FAIL (ARCHIVED)
|
||||
Details:
|
||||
- Target document `doc_proj143_intake` network responses (`/content`, `/revision-state`, `/versions`, `/manual-overlay`) all returned 200 OK.
|
||||
- Marker `PROJ143_CLEAN2_...` contained underscores which Markdown parsed into `<em>CLEAN2</em>` (italics), causing exact text node locator timeout; additionally, initial login legacy `doc-001` 404s were observed.
|
||||
- Not marked as PASS per instruction; archived as `clean-run2-fail`.
|
||||
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 170 KiB |
@@ -0,0 +1,571 @@
|
||||
{
|
||||
"UI-143-01": {
|
||||
"id": "UI-143-01",
|
||||
"name": "admin login & open project \"对话协同会话\"",
|
||||
"input": "login admin; assert doc-001 requests == 0; click exact title \"对话协同会话\" in complementary sidebar",
|
||||
"actual": "session active in banner; doc001Requests=0; target404s=0; observedOther404s=4",
|
||||
"networkLogs": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
}
|
||||
],
|
||||
"durationMs": 1742,
|
||||
"verdict": "PASS",
|
||||
"notes": "{\"observedOther404s\":[{\"method\":\"GET\",\"url\":\"/api/v1/chat/messages/window\",\"status\":404},{\"method\":\"GET\",\"url\":\"/api/v1/chat/unread\",\"status\":404},{\"method\":\"GET\",\"url\":\"/api/v1/chat/dates\",\"status\":404},{\"method\":\"GET\",\"url\":\"/api/v1/chat/categories\",\"status\":404}],\"doc001RequestsCount\":0}"
|
||||
},
|
||||
"UI-143-02": {
|
||||
"id": "UI-143-02",
|
||||
"name": "open doc_proj143_intake non-empty & ledger ready",
|
||||
"input": "open doc_proj143_intake via menu data-doc-id=doc_proj143_intake",
|
||||
"actual": "blocks=146, textLen=16642, version=V1, revision=r1, target404s=0",
|
||||
"networkLogs": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
}
|
||||
],
|
||||
"durationMs": 304,
|
||||
"verdict": "PASS",
|
||||
"notes": "{\"priorObservedConsoleCount\":7,\"observedOther404sCount\":4}"
|
||||
},
|
||||
"UI-143-03": {
|
||||
"id": "UI-143-03",
|
||||
"name": "idle 30s stability check",
|
||||
"input": "wait 30s on opened doc_proj143_intake",
|
||||
"actual": "newContentRequests=0, blocks intact, zero errors, zero target 404s",
|
||||
"networkLogs": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
}
|
||||
],
|
||||
"durationMs": 30110,
|
||||
"verdict": "PASS"
|
||||
},
|
||||
"UI-143-04": {
|
||||
"id": "UI-143-04",
|
||||
"name": "dblclick edit save & revision bump",
|
||||
"input": "append marker 人工验收标记1789355089269; saveOpenEditor",
|
||||
"actual": "marker visible; revision r1 -> r2",
|
||||
"networkLogs": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "PATCH",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
}
|
||||
],
|
||||
"durationMs": 703,
|
||||
"verdict": "PASS"
|
||||
},
|
||||
"UI-143-05": {
|
||||
"id": "UI-143-05",
|
||||
"name": "pure / manual / all overlay modes",
|
||||
"input": "switch overlay mode pure -> manual -> all",
|
||||
"actual": "pure unmarked; manual & all marked; computed styles visually distinct",
|
||||
"networkLogs": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "PATCH",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
}
|
||||
],
|
||||
"durationMs": 2249,
|
||||
"verdict": "PASS",
|
||||
"notes": "{\"pureCount\":146,\"manualCount\":146,\"allCount\":146}"
|
||||
},
|
||||
"UI-143-06": {
|
||||
"id": "UI-143-06",
|
||||
"name": "persistence check after refresh, logout, and re-login",
|
||||
"input": "page reload -> check marker/r -> logout -> re-login -> re-open doc",
|
||||
"actual": "marker \"人工验收标记1789355089269\" and revision \"r2\" persisted across reload and relogin",
|
||||
"networkLogs": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "PATCH",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/manual-overlay",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj143_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj143_intake/revision-state",
|
||||
"status": 200
|
||||
}
|
||||
],
|
||||
"durationMs": 2432,
|
||||
"verdict": "PASS",
|
||||
"notes": "{\"totalTarget404s\":0,\"totalObservedOther404s\":4}"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 271 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 260 KiB |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"projectName": "UI验收-超声微创系统-mu0oymfg",
|
||||
"userA": "uia_mu0oymfg",
|
||||
"userB": "uib_mu0oymfg",
|
||||
"userC": "uic_mu0oymfg",
|
||||
"controlledDocTitle": "用户初始需求收集表",
|
||||
"controlledDocId": "doc_proj6173_intake",
|
||||
"versionText": "V3",
|
||||
"revisionText": "r2",
|
||||
"bodySnippet": "A1-win-mu0oymfg"
|
||||
}
|
||||
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 272 KiB |
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 272 KiB |
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 199 KiB |
@@ -0,0 +1,302 @@
|
||||
{
|
||||
"UI-15": {
|
||||
"id": "UI-15",
|
||||
"name": "post-restart restore",
|
||||
"input": "UI login as A; open UI验收-超声微创系统-mu0oymfg",
|
||||
"actual": "version=V3 revision=r2",
|
||||
"networkLogs": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/freeformDoc.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/chatSessions.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/projectDocuments.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/projectWorkflow.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/projectMembers.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/projectSettings.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/taskStream.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/chatRetry.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/auth.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/chatStream.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/chatTurn.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/projectAssets.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/chatWindow.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/taskRuns.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/documentRevision.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/authHeaders.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/workflowAdmin.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/authUserMap.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/chatSearch.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/src/api/chatDigest.ts",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/auth/public-config",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/auth/login",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/auth/me",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/workflows/catalog",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/projects",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/projects/1",
|
||||
"status": 403
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/chat/sessions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/freeform-documents",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/freeform-documents",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/chat/messages/window",
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/projects/1/documents",
|
||||
"status": 403
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/chat/unread",
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/chat/categories",
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/chat/dates",
|
||||
"status": 404
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/projects/1/lineage",
|
||||
"status": 403
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/projects/1/settings",
|
||||
"status": 403
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/projects/6173/members",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/chat/sessions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/freeform-documents",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/freeform-documents",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/projects/6173",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/projects/6173/lineage",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/projects/6173/documents",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/projects/6173/settings",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/chat/unread",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/chat/dates",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/chat/messages/window",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/chat/categories",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/chat/sessions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj6173_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj6173_intake/content",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj6173_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj6173_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj6173_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj6173_intake/versions",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj6173_intake/manual-overlay",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/documents/doc_proj6173_intake/revision-state",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/api/v1/docgov/documents/doc_proj6173_intake/content",
|
||||
"status": 200
|
||||
}
|
||||
],
|
||||
"durationMs": 2631,
|
||||
"verdict": "PASS"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
attachment mu0oymfg
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"projectName": "UI验收-超声微创系统-mu0o5hbq",
|
||||
"userA": "uia_mu0o5hbq",
|
||||
"userB": "uib_mu0o5hbq",
|
||||
"userC": "uic_mu0o5hbq",
|
||||
"controlledDocTitle": "用户初始需求收集表",
|
||||
"controlledDocId": "doc_proj6172_intake",
|
||||
"versionText": "V2",
|
||||
"revisionText": "r0"
|
||||
}
|
||||
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 264 KiB |
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 252 KiB |
@@ -0,0 +1,5 @@
|
||||
# INVALID RUN 1
|
||||
|
||||
Status: INVALID
|
||||
Reason: Contains 6-fail and was contaminated by database check (psql).
|
||||
Preserved for audit integrity per project guidelines.
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
|
After Width: | Height: | Size: 279 KiB |
@@ -0,0 +1,6 @@
|
||||
# Run Result: INVALID (Archived)
|
||||
|
||||
- **Date**: 2026-09-14
|
||||
- **Phase**: main (task-421)
|
||||
- **Status**: INVALID
|
||||
- **Reason**: Playwright trace recorded ENOENT on trace/network copy due to concurrent output directory cleanup competition with previously cancelled container. Not counted as PASS. Re-executed cleanly in `full-chain-valid`.
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"projectName": "UI验收-超声微创系统-mu0nwyee",
|
||||
"userA": "uia_mu0nwyee",
|
||||
"userB": "uib_mu0nwyee",
|
||||
"userC": "uic_mu0nwyee",
|
||||
"controlledDocTitle": "用户初始需求收集表",
|
||||
"controlledDocId": "doc_proj6170_intake",
|
||||
"versionText": "V2",
|
||||
"revisionText": "r0"
|
||||
}
|
||||
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 253 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 232 KiB |
@@ -44,10 +44,12 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const runAcceptance = process.env.RUN_UI_ACCEPTANCE === '1';
|
||||
const uiPhase = (process.env.UI_PHASE || 'main').trim(); // main | post-restart
|
||||
|
||||
const ASSETS = path.resolve(
|
||||
__dirname,
|
||||
'../../.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui',
|
||||
);
|
||||
const ASSETS = process.env.UI_ACCEPTANCE_ASSETS_DIR
|
||||
? path.resolve(process.env.UI_ACCEPTANCE_ASSETS_DIR)
|
||||
: path.resolve(
|
||||
__dirname,
|
||||
'../../.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance',
|
||||
);
|
||||
const SCREENSHOTS = path.join(ASSETS, 'screenshots');
|
||||
const TRACES = path.join(ASSETS, 'traces');
|
||||
const VIDEOS = path.join(ASSETS, 'videos');
|
||||
|
||||
@@ -0,0 +1,615 @@
|
||||
import { test, expect, type Page, type Response } from '@playwright/test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import {
|
||||
BLOCK_EDITOR_DIALOG_LABEL,
|
||||
BLOCK_EDITOR_SAVE_NAME,
|
||||
} from '../src/lib/blockEditorDialog';
|
||||
import {
|
||||
CATEGORY_TRIGGER_LABEL,
|
||||
requiredCategoryForDocOpen,
|
||||
} from '../src/lib/workflowCategoryNavigation';
|
||||
|
||||
/**
|
||||
* Project 143 Regression UI Acceptance (Part A)
|
||||
*
|
||||
* Rules:
|
||||
* - Real browser UI only. No page.request / API / DB / mock / stub / localStorage injection.
|
||||
* - Admin credentials read from environment variables (ADMIN_USERNAME, ADMIN_PASSWORD).
|
||||
* - Verified against real running services on port 3100.
|
||||
* - Run as a single serial end-to-end test to eliminate cascading failure on worker restart.
|
||||
* - Target doc network 404 is strictly 0 throughout the entire session.
|
||||
* - Pre-login/initial unauthenticated 404s on legacy mock slots (e.g. doc-001) are recorded as observed,
|
||||
* and consoleErrors is reset upon successful verification of the target doc so post-open observation is clean.
|
||||
*/
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const runAcceptance = process.env.RUN_UI_ACCEPTANCE === '1';
|
||||
|
||||
const ASSETS = process.env.UI_ACCEPTANCE_ASSETS_DIR
|
||||
? path.resolve(process.env.UI_ACCEPTANCE_ASSETS_DIR)
|
||||
: path.resolve(
|
||||
__dirname,
|
||||
'../../.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance/clean-run3',
|
||||
);
|
||||
const SCREENSHOTS = path.join(ASSETS, 'screenshots');
|
||||
const TRACES = path.join(ASSETS, 'traces');
|
||||
const VIDEOS = path.join(ASSETS, 'videos');
|
||||
const RESULTS_FILE = path.join(ASSETS, 'proj143-ui-acceptance-results.json');
|
||||
|
||||
for (const d of [SCREENSHOTS, TRACES, VIDEOS]) {
|
||||
fs.mkdirSync(d, { recursive: true });
|
||||
}
|
||||
|
||||
type NetLog = { method: string; path: string; status: number };
|
||||
type CaseResult = {
|
||||
id: string;
|
||||
name: string;
|
||||
input: string;
|
||||
actual: string;
|
||||
networkLogs: NetLog[];
|
||||
durationMs: number;
|
||||
verdict: 'PASS' | 'FAIL';
|
||||
notes?: string;
|
||||
};
|
||||
|
||||
const recorded: Record<string, CaseResult> = {};
|
||||
|
||||
function fail(msg: string): never {
|
||||
throw new Error(msg);
|
||||
}
|
||||
|
||||
function sanitizeUrl(raw: string): string {
|
||||
try {
|
||||
const u = new URL(raw, 'http://127.0.0.1');
|
||||
return u.pathname;
|
||||
} catch {
|
||||
return raw.split('?')[0]?.replace(/https?:\/\/[^/]+/i, '') || raw;
|
||||
}
|
||||
}
|
||||
|
||||
function pushNet(logs: NetLog[], res: Response) {
|
||||
const pathOnly = sanitizeUrl(res.url());
|
||||
if (!pathOnly.includes('/api/')) return;
|
||||
// 精简网络日志:仅记录与目标受控文档相关的 API 请求
|
||||
if (!pathOnly.includes('doc_proj143_intake')) return;
|
||||
logs.push({ method: res.request().method(), path: pathOnly, status: res.status() });
|
||||
}
|
||||
|
||||
function saveResults() {
|
||||
const scrubbed = JSON.parse(JSON.stringify(recorded)) as Record<string, CaseResult>;
|
||||
const blob = JSON.stringify(scrubbed, null, 2);
|
||||
if (/admin-secret|Bearer\s|password["']?\s*[:=]|token=/i.test(blob)) {
|
||||
fail('results must not contain credentials or tokens');
|
||||
}
|
||||
fs.writeFileSync(RESULTS_FILE, blob, 'utf-8');
|
||||
}
|
||||
|
||||
function recordPass(r: CaseResult) {
|
||||
recorded[r.id] = { ...r, verdict: 'PASS' };
|
||||
saveResults();
|
||||
}
|
||||
|
||||
async function shot(page: Page, name: string) {
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS, name), fullPage: true });
|
||||
}
|
||||
|
||||
const ADMIN_USER = process.env.ADMIN_USERNAME?.trim() || '';
|
||||
const ADMIN_PASS = process.env.ADMIN_PASSWORD?.trim() || '';
|
||||
|
||||
function requireAdminCreds(): { username: string; password: string } {
|
||||
if (!ADMIN_USER || !ADMIN_PASS) {
|
||||
fail(
|
||||
'ADMIN_USERNAME and ADMIN_PASSWORD environment variables are required for UI acceptance',
|
||||
);
|
||||
}
|
||||
return { username: ADMIN_USER, password: ADMIN_PASS };
|
||||
}
|
||||
|
||||
async function login(page: Page, username: string, password: string) {
|
||||
await page.goto('/');
|
||||
await page.fill('input[placeholder*="系统用户名"]', username);
|
||||
await page.fill('input[placeholder*="登录密码"]', password);
|
||||
await page.click('button:has-text("进入医疗协同工作台")');
|
||||
await expect(page.getByText('新建立项会话')).toBeVisible({ timeout: 30_000 });
|
||||
}
|
||||
|
||||
async function logoutViaUI(page: Page) {
|
||||
await page.locator('button[title="个人资料与系统偏好设置"]').click({ timeout: 15_000 });
|
||||
const logout = page.getByRole('button', { name: '退出当前登录' });
|
||||
await expect(logout).toBeVisible({ timeout: 10_000 });
|
||||
await logout.click();
|
||||
await expect(page.getByPlaceholder(/系统用户名/)).toBeVisible({ timeout: 20_000 });
|
||||
}
|
||||
|
||||
async function selectDeliverableCategory(
|
||||
page: Page,
|
||||
category: 'workflow' | 'shared' | 'private',
|
||||
) {
|
||||
const trigger = page.getByTestId('doc-category-trigger');
|
||||
await expect(trigger).toBeVisible({ timeout: 15_000 });
|
||||
await trigger.click();
|
||||
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 confirmWorkflowDocById(page: Page, docId: string): Promise<void> {
|
||||
if (!docId) fail('confirmWorkflowDocById requires docId');
|
||||
const trigger = page.getByTestId('doc-selector-trigger');
|
||||
await expect(trigger).toBeVisible({ timeout: 15_000 });
|
||||
await trigger.click();
|
||||
|
||||
let item = page.locator(`.dropdown-menu button[data-doc-id="${docId}"]`).first();
|
||||
if ((await item.count()) === 0) {
|
||||
await selectDeliverableCategory(page, requiredCategoryForDocOpen({ kind: 'exact-id', docId }));
|
||||
await trigger.click();
|
||||
item = page.locator(`.dropdown-menu button[data-doc-id="${docId}"]`).first();
|
||||
}
|
||||
if ((await item.count()) === 0) fail(`doc-selector menu missing data-doc-id=${docId}`);
|
||||
await item.click();
|
||||
}
|
||||
|
||||
function blockEditorDialog(target: Page) {
|
||||
return target.getByRole('dialog', { name: BLOCK_EDITOR_DIALOG_LABEL });
|
||||
}
|
||||
|
||||
async function openFirstPaneEditorAndAppend(target: Page, appendText: string) {
|
||||
const pane = target.getByTestId('active-document-pane');
|
||||
const firstBlock = pane.getByTestId('document-block').first();
|
||||
await expect(firstBlock).toBeVisible({ timeout: 15_000 });
|
||||
await firstBlock.dblclick();
|
||||
const dialog = blockEditorDialog(target);
|
||||
await expect(dialog).toBeVisible({ timeout: 15_000 });
|
||||
const editor = dialog.locator('textarea, [contenteditable="true"]').first();
|
||||
await expect(editor).toBeVisible({ timeout: 10_000 });
|
||||
const currentVal = await editor.inputValue().catch(() => '');
|
||||
await editor.fill((currentVal ? currentVal + '\n\n' : '') + appendText);
|
||||
return editor;
|
||||
}
|
||||
|
||||
async function saveOpenEditor(target: Page) {
|
||||
const dialog = blockEditorDialog(target);
|
||||
await dialog.getByRole('button', { name: BLOCK_EDITOR_SAVE_NAME, exact: true }).click();
|
||||
}
|
||||
|
||||
async function readVersionBadge(page: Page): Promise<{ v: string; r: string }> {
|
||||
const v = (await page.locator('[data-testid="doc-rev-version"]').textContent())?.trim();
|
||||
const r = (await page.locator('[data-testid="doc-rev-revision"]').textContent())?.trim();
|
||||
if (!v || !r) fail('version/revision badge missing — document must be server-backed with ledger');
|
||||
return { v, r };
|
||||
}
|
||||
|
||||
type OverlaySample = {
|
||||
backgroundColor: string;
|
||||
boxShadow: string;
|
||||
className: string;
|
||||
overlayKind: string;
|
||||
blockId: string;
|
||||
};
|
||||
|
||||
function overlayPaneKey(samples: OverlaySample[]): string {
|
||||
return samples
|
||||
.map((s) => `${s.blockId}:${s.backgroundColor}|${s.boxShadow}|${s.overlayKind}`)
|
||||
.join(';');
|
||||
}
|
||||
|
||||
function overlaySampleIsTransparent(s: OverlaySample): boolean {
|
||||
const bg = s.backgroundColor.replace(/\s+/g, '');
|
||||
return (bg === 'transparent' || bg === 'rgba(0,0,0,0)') && (s.boxShadow === '' || s.boxShadow === 'none');
|
||||
}
|
||||
|
||||
function overlayPaneHasMark(samples: OverlaySample[]): boolean {
|
||||
return samples.some((s) => s.overlayKind !== '' || !overlaySampleIsTransparent(s));
|
||||
}
|
||||
|
||||
async function readOverlaySamples(page: Page): Promise<OverlaySample[]> {
|
||||
return page.getByTestId('active-document-pane').getByTestId('document-block').evaluateAll((els) =>
|
||||
els.map((node) => {
|
||||
const el = node as HTMLElement;
|
||||
const s = window.getComputedStyle(el);
|
||||
return {
|
||||
backgroundColor: s.backgroundColor,
|
||||
boxShadow: s.boxShadow,
|
||||
className: el.className || '',
|
||||
overlayKind: el.getAttribute('data-overlay-kind') || '',
|
||||
blockId: el.getAttribute('data-block-id') || '',
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForOverlayStylesSettled(page: Page, mode: 'pure' | 'manual' | 'all') {
|
||||
let previous = '';
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const samples = await readOverlaySamples(page);
|
||||
if (samples.length === 0) return 'empty';
|
||||
const key = overlayPaneKey(samples);
|
||||
const stable = previous !== '' && previous === key;
|
||||
previous = key;
|
||||
if (mode === 'pure') {
|
||||
return stable && !overlayPaneHasMark(samples) ? 'settled' : 'pending';
|
||||
}
|
||||
const marked = samples.some((s) => s.overlayKind) && overlayPaneHasMark(samples);
|
||||
return stable && marked ? 'settled' : 'pending';
|
||||
},
|
||||
{ timeout: 15_000 },
|
||||
)
|
||||
.toBe('settled');
|
||||
}
|
||||
|
||||
async function captureOverlayFingerprint(page: Page, mode: 'pure' | 'manual' | 'all') {
|
||||
const pane = page.getByTestId('active-document-pane');
|
||||
const modeSelect = pane.locator('select[aria-label="变更显示模式"]');
|
||||
await expect(modeSelect).toBeVisible({ timeout: 15_000 });
|
||||
await modeSelect.selectOption(mode);
|
||||
await expect(modeSelect).toHaveValue(mode);
|
||||
|
||||
const blocks = pane.getByTestId('document-block');
|
||||
await expect(blocks.first()).toBeVisible({ timeout: 15_000 });
|
||||
const marked = pane.locator('[data-testid="document-block"][data-overlay-kind]');
|
||||
if (mode === 'pure') {
|
||||
await expect(marked).toHaveCount(0, { timeout: 15_000 });
|
||||
} else {
|
||||
await expect(marked.first()).toBeVisible({ timeout: 15_000 });
|
||||
}
|
||||
await waitForOverlayStylesSettled(page, mode);
|
||||
|
||||
const samples = await readOverlaySamples(page);
|
||||
if (samples.length === 0) {
|
||||
fail(`UI overlay mode=${mode}: no document-block to sample computed style`);
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
test.describe('Project 143 Regression UI Acceptance', () => {
|
||||
test.skip(!runAcceptance, 'RUN_UI_ACCEPTANCE=1 required');
|
||||
|
||||
test('full sequential verification of Project 143 (A1-A7)', async ({ browser }) => {
|
||||
const context = await browser.newContext({
|
||||
baseURL: process.env.BASE_URL || 'http://127.0.0.1:3100',
|
||||
locale: 'zh-CN',
|
||||
recordVideo: { dir: VIDEOS },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
const logs: NetLog[] = [];
|
||||
const consoleErrors: string[] = [];
|
||||
const pageErrors: string[] = [];
|
||||
const target404s: { method: string; url: string; status: number }[] = [];
|
||||
const observedOther404s: { method: string; url: string; status: number }[] = [];
|
||||
const doc001Requests: { method: string; url: string; status: number }[] = [];
|
||||
let contentRequestsCount = 0;
|
||||
|
||||
page.on('response', (res) => {
|
||||
pushNet(logs, res);
|
||||
const u = sanitizeUrl(res.url());
|
||||
if (u.includes('/documents/doc_proj143_intake/content')) {
|
||||
contentRequestsCount++;
|
||||
}
|
||||
if (u.includes('doc-001/content') || u.includes('doc-001/revision-state')) {
|
||||
doc001Requests.push({ method: res.request().method(), url: u, status: res.status() });
|
||||
}
|
||||
if (u.includes('doc_proj143_intake') && res.status() === 404) {
|
||||
target404s.push({ method: res.request().method(), url: u, status: res.status() });
|
||||
} else if (res.status() === 404) {
|
||||
observedOther404s.push({ method: res.request().method(), url: u, status: res.status() });
|
||||
}
|
||||
});
|
||||
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
consoleErrors.push(msg.text());
|
||||
}
|
||||
});
|
||||
page.on('pageerror', (err) => {
|
||||
pageErrors.push(err.message);
|
||||
});
|
||||
|
||||
const creds = requireAdminCreds();
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Step 1: Admin login via login page and open session
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
let t0 = Date.now();
|
||||
await login(page, creds.username, creds.password);
|
||||
await shot(page, 'UI-143-01-admin-logged-in.png');
|
||||
|
||||
// 明确验证从登录开始 doc-001 content/revision 请求均为 0
|
||||
if (doc001Requests.length > 0) {
|
||||
fail(`Cursor fix assertion failed: doc-001 content/revision requests must be 0, got ${JSON.stringify(doc001Requests)}`);
|
||||
}
|
||||
const doc001InObserved404 = observedOther404s.filter(
|
||||
(l) => l.url.includes('doc-001/content') || l.url.includes('doc-001/revision-state'),
|
||||
);
|
||||
if (doc001InObserved404.length > 0) {
|
||||
fail(`Cursor fix assertion failed: observedOther404s contained doc-001 requests: ${JSON.stringify(doc001InObserved404)}`);
|
||||
}
|
||||
|
||||
// In LeftRail sidebar, find and click the session entry with exact title "对话协同会话"
|
||||
const sessionEntry = page
|
||||
.getByRole('complementary')
|
||||
.getByTitle('对话协同会话', { exact: true });
|
||||
await expect(sessionEntry).toBeVisible({ timeout: 15_000 });
|
||||
await sessionEntry.click();
|
||||
|
||||
// Verify banner shows project/session active
|
||||
await expect(page.getByRole('banner')).toContainText('对话协同会话', { timeout: 15_000 });
|
||||
await shot(page, 'UI-143-01-project-opened.png');
|
||||
|
||||
// Assert target doc has strictly 0 network 404s
|
||||
if (target404s.length > 0) {
|
||||
fail(`target doc encountered 404 in step 1: ${JSON.stringify(target404s)}`);
|
||||
}
|
||||
|
||||
recordPass({
|
||||
id: 'UI-143-01',
|
||||
name: 'admin login & open project "对话协同会话"',
|
||||
input: `login admin; assert doc-001 requests == 0; click exact title "对话协同会话" in complementary sidebar`,
|
||||
actual: `session active in banner; doc001Requests=0; target404s=0; observedOther404s=${observedOther404s.length}`,
|
||||
networkLogs: [...logs],
|
||||
durationMs: Date.now() - t0,
|
||||
verdict: 'PASS',
|
||||
notes: JSON.stringify({ observedOther404s, doc001RequestsCount: doc001Requests.length }),
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Step 2: Confirm doc_proj143_intake via dropdown menu
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
t0 = Date.now();
|
||||
await confirmWorkflowDocById(page, 'doc_proj143_intake');
|
||||
|
||||
// Wait for active pane document blocks
|
||||
const pane = page.getByTestId('active-document-pane');
|
||||
await expect(pane).toBeVisible({ timeout: 20_000 });
|
||||
const blocks = pane.getByTestId('document-block');
|
||||
await expect(blocks.first()).toBeVisible({ timeout: 20_000 });
|
||||
const blockCount = await blocks.count();
|
||||
if (blockCount === 0) fail('doc_proj143_intake has 0 document blocks');
|
||||
|
||||
// Check text is non-empty
|
||||
const fullText = (await pane.innerText()).trim();
|
||||
if (fullText.length < 50) {
|
||||
fail(`doc_proj143_intake content unexpectedly short: ${fullText.length} chars`);
|
||||
}
|
||||
|
||||
// Verify ledger ready
|
||||
const { v, r: rInitial } = await readVersionBadge(page);
|
||||
const unreadyHint = page.getByText(/版本账本未就绪|自动拉取受控文档正文失败/);
|
||||
if (await unreadyHint.isVisible().catch(() => false)) {
|
||||
fail('doc_proj143_intake shows unready hint or fetch failure');
|
||||
}
|
||||
|
||||
// Assert strictly zero 404s for target doc
|
||||
if (target404s.length > 0) {
|
||||
fail(`target doc 404 encountered in step 2: ${JSON.stringify(target404s)}`);
|
||||
}
|
||||
|
||||
// Verify target doc network requests were successful (status 200)
|
||||
const targetContentRequests = logs.filter(
|
||||
(l) => l.path.includes('/doc_proj143_intake/content') && l.status === 200,
|
||||
);
|
||||
if (targetContentRequests.length === 0) {
|
||||
fail('expected at least one successful 200 GET for doc_proj143_intake/content');
|
||||
}
|
||||
|
||||
await shot(page, 'UI-143-02-doc-opened.png');
|
||||
|
||||
// Record prior observed errors, then reset consoleErrors and pageErrors for clean post-open observation
|
||||
const priorObservedConsole = [...consoleErrors];
|
||||
consoleErrors.length = 0;
|
||||
pageErrors.length = 0;
|
||||
|
||||
recordPass({
|
||||
id: 'UI-143-02',
|
||||
name: 'open doc_proj143_intake non-empty & ledger ready',
|
||||
input: 'open doc_proj143_intake via menu data-doc-id=doc_proj143_intake',
|
||||
actual: `blocks=${blockCount}, textLen=${fullText.length}, version=${v}, revision=${rInitial}, target404s=0`,
|
||||
networkLogs: [...logs],
|
||||
durationMs: Date.now() - t0,
|
||||
verdict: 'PASS',
|
||||
notes: JSON.stringify({
|
||||
priorObservedConsoleCount: priorObservedConsole.length,
|
||||
observedOther404sCount: observedOther404s.length,
|
||||
}),
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Step 3: Idle 30 seconds stability check (clean observation)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
t0 = Date.now();
|
||||
const countBeforeIdle = contentRequestsCount;
|
||||
|
||||
await page.waitForTimeout(30_000);
|
||||
|
||||
const countAfterIdle = contentRequestsCount;
|
||||
const newContentRequests = countAfterIdle - countBeforeIdle;
|
||||
|
||||
if (newContentRequests > 0) {
|
||||
fail(`idle 30s triggered ${newContentRequests} unexpected duplicate content requests`);
|
||||
}
|
||||
|
||||
await expect(pane.getByTestId('document-block').first()).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
// Assert strictly no console errors or page errors occurred during the 30-second idle observation
|
||||
if (consoleErrors.length > 0) {
|
||||
fail(`console errors occurred during idle 30s: ${JSON.stringify(consoleErrors)}`);
|
||||
}
|
||||
if (pageErrors.length > 0) {
|
||||
fail(`page errors occurred during idle 30s: ${JSON.stringify(pageErrors)}`);
|
||||
}
|
||||
if (target404s.length > 0) {
|
||||
fail(`target doc 404 encountered during idle 30s: ${JSON.stringify(target404s)}`);
|
||||
}
|
||||
|
||||
await shot(page, 'UI-143-03-idle-30s.png');
|
||||
|
||||
recordPass({
|
||||
id: 'UI-143-03',
|
||||
name: 'idle 30s stability check',
|
||||
input: 'wait 30s on opened doc_proj143_intake',
|
||||
actual: `newContentRequests=${newContentRequests}, blocks intact, zero errors, zero target 404s`,
|
||||
networkLogs: [...logs],
|
||||
durationMs: Date.now() - t0,
|
||||
verdict: 'PASS',
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Step 4: Dblclick edit block, append unique timestamp marker, save
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
t0 = Date.now();
|
||||
const { r: rBeforeEdit } = await readVersionBadge(page);
|
||||
// 纯中文与数字,绝不含任何 Markdown 语法符号(无 _ * [ ] # - 等),使用实际可见文本验证
|
||||
const uniqueMarker = `人工验收标记${Date.now()}`;
|
||||
|
||||
await openFirstPaneEditorAndAppend(page, `(${uniqueMarker})`);
|
||||
await shot(page, 'UI-143-04a-editing-dialog.png');
|
||||
|
||||
await saveOpenEditor(page);
|
||||
await expect(blockEditorDialog(page)).toHaveCount(0, { timeout: 15_000 });
|
||||
|
||||
// Assert marker is visible on page via actual user-visible text
|
||||
await expect(page.getByText(uniqueMarker)).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// Assert revision number increased
|
||||
await expect(page.locator('[data-testid="doc-rev-revision"]')).not.toHaveText(rBeforeEdit, {
|
||||
timeout: 30_000,
|
||||
});
|
||||
const { r: rAfterSave } = await readVersionBadge(page);
|
||||
|
||||
if (consoleErrors.length > 0) {
|
||||
fail(`console errors occurred during edit/save: ${JSON.stringify(consoleErrors)}`);
|
||||
}
|
||||
if (target404s.length > 0) {
|
||||
fail(`target doc 404 encountered during edit/save: ${JSON.stringify(target404s)}`);
|
||||
}
|
||||
|
||||
await shot(page, 'UI-143-04b-edited-saved.png');
|
||||
|
||||
recordPass({
|
||||
id: 'UI-143-04',
|
||||
name: 'dblclick edit save & revision bump',
|
||||
input: `append marker ${uniqueMarker}; saveOpenEditor`,
|
||||
actual: `marker visible; revision ${rBeforeEdit} -> ${rAfterSave}`,
|
||||
networkLogs: [...logs],
|
||||
durationMs: Date.now() - t0,
|
||||
verdict: 'PASS',
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Step 5: Three overlay modes: pure / manual / all differ in styles
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
t0 = Date.now();
|
||||
const pure = await captureOverlayFingerprint(page, 'pure');
|
||||
await shot(page, 'UI-143-05-pure.png');
|
||||
|
||||
const manual = await captureOverlayFingerprint(page, 'manual');
|
||||
await shot(page, 'UI-143-06-manual.png');
|
||||
|
||||
const all = await captureOverlayFingerprint(page, 'all');
|
||||
await shot(page, 'UI-143-07-all.png');
|
||||
|
||||
const pureKey = overlayPaneKey(pure);
|
||||
const manualKey = overlayPaneKey(manual);
|
||||
const allKey = overlayPaneKey(all);
|
||||
|
||||
if (overlayPaneHasMark(pure)) {
|
||||
fail(`UI-143-05 纯净阅读 must have no overlay decoration: ${JSON.stringify(pure)}`);
|
||||
}
|
||||
if (!overlayPaneHasMark(manual)) {
|
||||
fail(`UI-143-05 人工修改 must decorate changed blocks: ${JSON.stringify(manual)}`);
|
||||
}
|
||||
if (!overlayPaneHasMark(all)) {
|
||||
fail(`UI-143-05 全部变更 must show version/human decorations: ${JSON.stringify(all)}`);
|
||||
}
|
||||
if (pureKey === manualKey || pureKey === allKey) {
|
||||
fail(`pure must be visually distinct from manual/all: pure=${pureKey} manual=${manualKey} all=${allKey}`);
|
||||
}
|
||||
|
||||
if (consoleErrors.length > 0) {
|
||||
fail(`console errors occurred during overlay mode checks: ${JSON.stringify(consoleErrors)}`);
|
||||
}
|
||||
if (target404s.length > 0) {
|
||||
fail(`target doc 404 encountered during overlay mode checks: ${JSON.stringify(target404s)}`);
|
||||
}
|
||||
|
||||
recordPass({
|
||||
id: 'UI-143-05',
|
||||
name: 'pure / manual / all overlay modes',
|
||||
input: 'switch overlay mode pure -> manual -> all',
|
||||
actual: 'pure unmarked; manual & all marked; computed styles visually distinct',
|
||||
networkLogs: [...logs],
|
||||
durationMs: Date.now() - t0,
|
||||
verdict: 'PASS',
|
||||
notes: JSON.stringify({ pureCount: pure.length, manualCount: manual.length, allCount: all.length }),
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Step 6: Reload, logout, re-login: marker and revision persisted
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
t0 = Date.now();
|
||||
|
||||
// 1. Hard reload
|
||||
await page.reload();
|
||||
await expect(page.getByRole('complementary').getByTitle('对话协同会话', { exact: true })).toBeVisible({
|
||||
timeout: 20_000,
|
||||
});
|
||||
// Assert marker persisted after reload via user-visible text
|
||||
await expect(page.getByText(uniqueMarker)).toBeVisible({ timeout: 30_000 });
|
||||
const { r: rAfterReload } = await readVersionBadge(page);
|
||||
if (rAfterReload !== rAfterSave) {
|
||||
fail(`revision mismatch after reload: expected ${rAfterSave}, got ${rAfterReload}`);
|
||||
}
|
||||
await shot(page, 'UI-143-08a-after-reload.png');
|
||||
|
||||
// 2. Logout via UI
|
||||
await logoutViaUI(page);
|
||||
await shot(page, 'UI-143-08b-logged-out.png');
|
||||
|
||||
// 3. Re-login as admin via UI
|
||||
await login(page, creds.username, creds.password);
|
||||
await shot(page, 'UI-143-08c-relogged-in.png');
|
||||
|
||||
// 4. Open project session and confirm doc
|
||||
const sessionEntryAfter = page
|
||||
.getByRole('complementary')
|
||||
.getByTitle('对话协同会话', { exact: true });
|
||||
await expect(sessionEntryAfter).toBeVisible({ timeout: 15_000 });
|
||||
await sessionEntryAfter.click();
|
||||
await confirmWorkflowDocById(page, 'doc_proj143_intake');
|
||||
|
||||
// Assert marker still present via user-visible text
|
||||
await expect(page.getByText(uniqueMarker)).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// Assert revision number is still persisted
|
||||
const { r: rFinal } = await readVersionBadge(page);
|
||||
if (rFinal !== rAfterSave) {
|
||||
fail(`revision mismatch after relogin: expected ${rAfterSave}, got ${rFinal}`);
|
||||
}
|
||||
await shot(page, 'UI-143-08d-persisted-after-relogin.png');
|
||||
|
||||
if (target404s.length > 0) {
|
||||
fail(`target doc 404 encountered across entire session: ${JSON.stringify(target404s)}`);
|
||||
}
|
||||
|
||||
recordPass({
|
||||
id: 'UI-143-06',
|
||||
name: 'persistence check after refresh, logout, and re-login',
|
||||
input: 'page reload -> check marker/r -> logout -> re-login -> re-open doc',
|
||||
actual: `marker "${uniqueMarker}" and revision "${rFinal}" persisted across reload and relogin`,
|
||||
networkLogs: [...logs],
|
||||
durationMs: Date.now() - t0,
|
||||
verdict: 'PASS',
|
||||
notes: JSON.stringify({
|
||||
totalTarget404s: target404s.length,
|
||||
totalObservedOther404s: observedOther404s.length,
|
||||
}),
|
||||
});
|
||||
|
||||
await context.close();
|
||||
});
|
||||
});
|
||||
@@ -11,10 +11,12 @@ import { playwrightArtifactDirNames } from './src/lib/playwrightPhaseArtifacts';
|
||||
*/
|
||||
const runAcceptance = process.env.RUN_UI_ACCEPTANCE === '1';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const evidenceRoot = path.resolve(
|
||||
__dirname,
|
||||
'../.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui',
|
||||
);
|
||||
const evidenceRoot = process.env.UI_ACCEPTANCE_ASSETS_DIR
|
||||
? path.resolve(process.env.UI_ACCEPTANCE_ASSETS_DIR)
|
||||
: path.resolve(
|
||||
__dirname,
|
||||
'../.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance',
|
||||
);
|
||||
const phaseDirs = playwrightArtifactDirNames(process.env.UI_PHASE);
|
||||
|
||||
export default defineConfig({
|
||||
|
||||