diff --git a/.doc/ops/short-term/2026-09-13-conversation-intent-runtime-state.md b/.doc/archive/ops/2026-09-13-conversation-intent-runtime-state.md similarity index 70% rename from .doc/ops/short-term/2026-09-13-conversation-intent-runtime-state.md rename to .doc/archive/ops/2026-09-13-conversation-intent-runtime-state.md index 6dce5a07..d24d3a17 100644 --- a/.doc/ops/short-term/2026-09-13-conversation-intent-runtime-state.md +++ b/.doc/archive/ops/2026-09-13-conversation-intent-runtime-state.md @@ -1,11 +1,11 @@ --- title: "修复查询误触发写任务与任务状态刷新退化" -status: in_progress +status: done created: 2026-09-13 -updated: 2026-09-13 -owner: Codex -branch: fix/conversation-intent-state -worktree: ".worktrees/fix-conversation-intent-state" +updated: 2026-09-15 +owner: Cursor +branch: docs/conversation-intent-dual-tab +worktree: "-" mutex: intent --- @@ -34,24 +34,26 @@ mutex: intent 3. [x] 收紧写任务动作识别并修复 busy 错误分型 4. [x] 实现任务/租约状态刷新恢复与统一提示 5. [x] 实现命名文档的服务端鉴权只读总结(“总结”不止是不写入) -6. [ ] 执行前端、Go、PG 与真实 PI 双标签页验收 +6. [x] 执行前端、Go、PG 与真实 PI 双标签页验收 - [x] Go、前端、构建、dev 重建与真实 PI 只读总结 - [x] 服务端 busy 预检不创建 run,并持久化无重试状态卡 - - [ ] 浏览器刷新恢复与双标签页 busy 可见性 + - [x] 浏览器刷新恢复与双标签页 busy 可见性(项目 6275;禁止项目 143) ## 验收标准 -- [ ] [整改方案](../../remediation/2026-09-13-conversation-intent-runtime-state-remediation.md) 全部验收项有证据 -- [ ] `CHAT-INTENT-STATE` 由 FAIL 更新为 PASS -- [ ] test-report 归档并回链 +- [x] [整改方案](../../remediation/2026-09-13-conversation-intent-runtime-state-remediation.md) 全部验收项有证据 +- [x] `CHAT-INTENT-STATE` 由 FAIL 更新为 PASS +- [x] test-report 归档并回链 ## 相关链接 - Checklist: 本任务验收标准 -- Handoff: 与 `fix/chat-history-refresh` 合并前复核 `App.tsx/chatWindow.ts` +- Handoff: [../../ops/handoffs/2026-09-15-conversation-intent-dual-tab.md](../../ops/handoffs/2026-09-15-conversation-intent-dual-tab.md) - Test report: [../../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md](../../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md) ## 证据 / 备注 - Audit: [../../audit/2026-09-13-conversation-intent-runtime-state.md](../../audit/2026-09-13-conversation-intent-runtime-state.md) - Remediation: [../../remediation/2026-09-13-conversation-intent-runtime-state-remediation.md](../../remediation/2026-09-13-conversation-intent-runtime-state-remediation.md) +- 双标签活验:项目 **6275** / `sess-p6275-u10` / `run_1789452602295343365`;产物 `.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/` +- 未改写项目 143,未删除 `sess-p143-u9` 探针 diff --git a/.doc/ops/CURRENT.md b/.doc/ops/CURRENT.md index c0651338..aa5d45f5 100644 --- a/.doc/ops/CURRENT.md +++ b/.doc/ops/CURRENT.md @@ -1,11 +1,11 @@ # 当前焦点 **更新**: 2026-09-15 -**production_ready**: no(`CHAT-INTENT-STATE` 等无关 blocker 仍开放。文档账本运行时 UI Round20 **PASS**,不把 fallback change report 写成完整 PI,也不把本轮写成全仓可发布) +**production_ready**: no(CI 真实 PI 三工作流 E2E / 前端人工抽检仍开放。`CHAT-INTENT-STATE` 双标签 busy **PASS**。文档账本运行时 UI Round20 **PASS**,不把 fallback change report 写成完整 PI,也不把本轮写成全仓可发布) ## 焦点 -1. **CHAT-INTENT**: [对话意图运行时状态](../audit/2026-09-13-conversation-intent-runtime-state.md)(`fix/conversation-intent-state` 已是 `dev` 祖先;浏览器双标签 busy 待 Cursor 写 handoff 后接管) +1. **CHAT-INTENT**: [对话意图运行时状态](../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md)(双标签 busy 已签:项目 **6275** / `DOC_BUSY` 单 run;不删 `sess-p143-u9` 探针) 2. **DOC-RELIABILITY**: [U11–U12 组合根与传输门禁](../archive/ops/2026-09-15-app-composition-u11-u12.md)(已合入 `dev@bf59cce`;rebuild digest `a8e3e940…`;schema 38) 3. **CHAT**: [对话历史刷新活验已补](../test-reports/2026-09-13-chat-history-refresh-test-report.md)(项目 6252 闲聊刷新同集 + guest 403;编制任务链仍开放) @@ -24,7 +24,8 @@ | 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 | -| Codex | conversation-intent-runtime-state | fix/conversation-intent-state | .worktrees/fix-conversation-intent-state | intent | in_progress | +| Codex | conversation-intent-runtime-state | fix/conversation-intent-state | .worktrees/fix-conversation-intent-state | intent | done | +| Cursor | conversation-intent-runtime-state | docs/conversation-intent-dual-tab | - | intent | done | | Cursor | document-revision-wave1-6-implementation | feature/document-revision-management | - | docgov | done | | Cursor-Auto | document-revision-runtime-ui03 | fix/document-revision-runtime-ui03 | - | docgov | done | | Cursor-Auto | document-revision-sse-idempotency | fix/document-revision-sse-idempotency | - | docgov | done | @@ -58,7 +59,7 @@ > 新任务开始前登记。`branch` 必须为 `feature/` 或 `fix/`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。 > workflow-plan 409 与治理鉴权 G1–G5 已收口(见 [test-report](../test-reports/2026-09-14-governance-action-level-authz-test-report.md))。禁止终止或改写项目 143。 -> `document-reliability-pilot` U11–U12 已合入 `dev@bf59cce` 并 rebuild。禁止 worktree 内 `env-service`。项目 143 只读,禁止终止。intent 浏览器门下一步由 Cursor 写 handoff 接管,不覆盖 Codex worktree。 +> `document-reliability-pilot` U11–U12 已合入 `dev@bf59cce` 并 rebuild。`CHAT-INTENT-STATE` 双标签 busy 已 PASS(项目 6275)。禁止 worktree 内 `env-service`。项目 143 只读,禁止终止。 ## 集成槽位 @@ -87,6 +88,7 @@ | 发布缺口 | [gap-matrix](../remediation/gap-matrix.md) | | 测试报告 | [test-reports/](../test-reports/) | | Agent 手册 | [AGENTS.md](../../AGENTS.md) | +| 意图双标签 busy 交接 | [./handoffs/2026-09-15-conversation-intent-dual-tab.md](./handoffs/2026-09-15-conversation-intent-dual-tab.md) | | 受控正文 hash 漂移 W1–W6 | [../ops/handoffs/2026-09-14-controlled-content-canonical-hash.md](../ops/handoffs/2026-09-14-controlled-content-canonical-hash.md) | | DOCREV ADR | [../adr/2026-09-13-document-version-boundary.md](../adr/2026-09-13-document-version-boundary.md) | | DOCREV 管理测试 | [../test-reports/2026-09-13-document-revision-management-test-report.md](../test-reports/2026-09-13-document-revision-management-test-report.md) | diff --git a/.doc/ops/handoffs/2026-09-15-conversation-intent-dual-tab.md b/.doc/ops/handoffs/2026-09-15-conversation-intent-dual-tab.md new file mode 100644 index 00000000..fb55f941 --- /dev/null +++ b/.doc/ops/handoffs/2026-09-15-conversation-intent-dual-tab.md @@ -0,0 +1,67 @@ +--- +title: "意图 mutex 从 Codex 转 Cursor:仅剩浏览器双标签门" +created: 2026-09-15 +from: Codex +to: Cursor +status: closed +--- + +# 交接:对话意图运行时状态 — 浏览器双标签门 + +## 目标(下一位 Agent 应完成什么) + +在 U11–U12 已合入并 rebuild 的 `:8080` 上,用**新建项目**补齐真实 PI + 双标签 busy / 刷新恢复证据。通过则关 `CHAT-INTENT-STATE` 并 archive;失败则从最新 `dev` 新开 `fix/conversation-intent-dual-tab`,**不要**在已合入的 `fix/conversation-intent-state` 上追加。 + +## 已完成 + +- Wave 0–4 服务端/单测已在 `dev`(`fix/conversation-intent-state` 是祖先)。 +- 前端 5s 租约轮询:`web/src/App.tsx`。 +- `DOC_BUSY` 预检:`internal/server/task_run_handlers.go`(不创建第二 run,`error_info.actions=[]`)。 +- UI busy 文案:`BlockRenderer` / `DeckDrawer`「编辑中」。 +- 只读总结真实 PI(用例 H)已签;项目 143 诊断污染未删。 + +## 未完成 + +- 浏览器刷新恢复与双标签租约可见性(报告用例 E)。 +- 整改 Wave 3–4 未勾项:运行中刷新、busy 自动解除、并发写不新增 run 的**活验证据**。 + +## 认领 / 槽位状态 + +| 项 | 值 | +|----|-----| +| mutex 已释放 | no(`intent` 转 Cursor `in_progress`) | +| 集成槽位 holder | idle(U11–U12 已释放;本门禁不占槽,除非要合入 UI 补丁) | +| branch | 编码已在 `dev`;活验文档用 `docs/conversation-intent-dual-tab` | +| worktree | `.worktrees/fix-conversation-intent-state` **禁止** `env-service` | + +> 用户已要求关掉 CURRENT 两行 `in_progress`,授权 Cursor 接管剩余验收。不覆盖 Codex worktree 未合并代码。 + +## 关键文件 + +| 路径 | 说明 | +|------|------| +| `.doc/ops/short-term/2026-09-13-conversation-intent-runtime-state.md` | 任务文 | +| `.doc/test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md` | 用例 E 待补 | +| `.doc/remediation/2026-09-13-conversation-intent-runtime-state-remediation.md` | Wave 3–4 | +| `web/src/App.tsx` | 5s lease 轮询 | +| `internal/server/task_run_handlers.go` | DOC_BUSY 预检 | + +## 如何验证 + +```bash +# 主仓已 rebuild 的 dev,禁止 worktree 内 env-service +curl -sf http://127.0.0.1:8080/ready +# 新建项目 + 隔离 session;禁止项目 143;禁止 sess-p143-u9 探针 +``` + +## 风险 / Blocker + +- 项目 143 只读:不终止、不改写、不打探针。 +- 客户端在 lease 已投影后会本地拦截写请求(不发 POST);HTTP 409 须在轮询窗口内或走 API 第二通道取证。 +- 诊断污染消息仍不擅自删除。 + +## 相关链接 + +- CURRENT: [CURRENT.md](../CURRENT.md) +- Task: [../short-term/2026-09-13-conversation-intent-runtime-state.md](../short-term/2026-09-13-conversation-intent-runtime-state.md) +- Test report: [../../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md](../../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md) diff --git a/.doc/remediation/2026-09-13-conversation-intent-runtime-state-remediation.md b/.doc/remediation/2026-09-13-conversation-intent-runtime-state-remediation.md index 20390a1d..d017bbbb 100644 --- a/.doc/remediation/2026-09-13-conversation-intent-runtime-state-remediation.md +++ b/.doc/remediation/2026-09-13-conversation-intent-runtime-state-remediation.md @@ -1,8 +1,8 @@ --- title: "对话意图、可恢复任务状态与文档占用可见性整改" -status: in_progress +status: done created: 2026-09-13 -updated: 2026-09-13 +updated: 2026-09-15 --- # 对话意图、可恢复任务状态与文档占用可见性整改 @@ -30,11 +30,10 @@ updated: 2026-09-13 ## 验收标准 -- [x] “总结一下用户初始需求表”返回已有文档摘要,不创建 `task_runs`。 -- [ ] “开始编写用户初始需求表”只创建一个 run;运行中再次写请求不创建失败 run。 -- [ ] 任务失败后刷新,错误码、诊断摘要及允许动作与刷新前一致。 -- [ ] 运行中刷新,聊天占位、阶段进度、文档 busy 标识和输入保护均恢复。 -- [ ] 租约释放/任务终态后 UI 自动解除 busy,无需用户手动刷新。 +- [x] “开始编写用户初始需求表”只创建一个 run;运行中再次写请求不创建失败 run。 +- [x] 任务失败后刷新,错误码、诊断摘要及允许动作与刷新前一致。 +- [x] 运行中刷新,聊天占位、阶段进度、文档 busy 标识和输入保护均恢复。 +- [x] 租约释放/任务终态后 UI 自动解除 busy,无需用户手动刷新。 - [x] 测试使用专用项目/会话并在 teardown 清理;禁止 `debug/test` 探针进入非 fixture 会话。 ## 相关链接 diff --git a/.doc/remediation/gap-matrix.md b/.doc/remediation/gap-matrix.md index 39f35b4e..7944467c 100644 --- a/.doc/remediation/gap-matrix.md +++ b/.doc/remediation/gap-matrix.md @@ -2,7 +2,7 @@ **更新**: 2026-09-14 **分支**: dev -**production_ready**: no(`CHAT-INTENT-STATE` 等无关 blocker、CI 真实 PI 三工作流 E2E / 前端人工抽检未完成;文档账本运行时 UI Round20 已 PASS,不把 fallback 报告当成完整 PI) +**production_ready**: no(CI 真实 PI 三工作流 E2E / 前端人工抽检未完成。`CHAT-INTENT-STATE` 双标签 busy 已 PASS。文档账本运行时 UI Round20 已 PASS,不把 fallback 报告当成完整 PI) **协作焦点**: [../ops/CURRENT.md](../ops/CURRENT.md) **Agent 手册**: [AGENTS.md](../AGENTS.md) · [PROJECT.md](../PROJECT.md) @@ -60,7 +60,7 @@ | WORKFLOW-PLAN-FROZEN-OPTIONAL-DUTIES | active 项目仍展示可编辑职责表单,409 提示在长表单视口外;现有规划又不支持非必需文档“三权全空”,且后端缺少职责人在任成员复核 | **PASS(设置页)** | [audit](../audit/2026-09-14-workflow-plan-409-frozen-and-optional-duties.md);[test-report](../test-reports/2026-09-14-workflow-plan-409-frozen-and-optional-duties-test-report.md)。项目 143 只读 frozen、A full+none 激活、B partial 不发 PUT、C 零治理拒绝、双标签转 frozen、成员无配置入口、刷新后 143 仍 frozen | 真实 PI 生成 full/none 并走审批/阶段不阻塞未纳入本轮页面矩阵,不作为本 gap 关闭的替代证据,另立项 | | DOC-CONTENT-CANONICAL-HASH-DRIFT | 旧受控文档 bootstrap 裁剪 Markdown 双空格硬换行并将 canonical SHA 写入账本/DocGov,但未同步文件;正文读取把完整性冲突误报 404,lease/list sync 又放大并制造刷新后加载死锁 | **PASS(真实 UI)** | [audit](../audit/2026-09-14-project143-controlled-content-404.md);[UI report](../test-reports/2026-09-14-controlled-content-ui-acceptance.md)。代码已合 dev、schema 32、项目 143 定向 `migration_repair`;项目 143 `clean-run3` 与新项目 `full-chain-final` 主链及 restart 全部纯页面 PASS,目标 404=0 | [remediation](./2026-09-14-controlled-content-canonical-hash-remediation.md)。隔离 `TestReconcile_LegacyRepair_PG` 未执行,按用户“本轮只允许页面测试”的边界保留为工程测试债,不作为本轮 UI PASS 的替代证据 | | CHAT-HISTORY-REFRESH | 工作流激活后刷新,对话正文被阶段过滤器清空,但分类数量与锚点仍按全量返回 | **PASS** | [test-report](../test-reports/2026-09-13-chat-history-refresh-test-report.md);活验 [live.json](../test-reports/assets/2026-09-15-chat-history-refresh-live/live.json)(项目 6252,window=categories=2,guest 403);代码:`message_scope.go`、`chat_history_auth.go`、goose 00026/00027 | 闲聊刷新已补。发布门禁「激活后文档任务 + 重登/重启」仍开放,不回退本 gap | -| CHAT-INTENT-STATE | 真实会话被诊断探针污染;只读查询误启动文档写任务;错误动作与编辑状态刷新后丢失 | **FAIL** | [audit](../audit/2026-09-13-conversation-intent-runtime-state.md);dev `sess-p143-u9` / task run / lease 现场 | [remediation](./2026-09-13-conversation-intent-runtime-state-remediation.md) | +| CHAT-INTENT-STATE | 真实会话被诊断探针污染;只读查询误启动文档写任务;错误动作与编辑状态刷新后丢失 | **PASS** | [audit](../audit/2026-09-13-conversation-intent-runtime-state.md);[test-report](../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md);双标签活验项目 **6275** / `sess-p6275-u10`(`live.json` + Docker 截图)。**不**删除 `sess-p143-u9` 历史探针 | [remediation](./2026-09-13-conversation-intent-runtime-state-remediation.md)。Wave 3–4 浏览器门已签;诊断污染消息仍不擅自删除 | | ENV-RESTART-SOURCE | `docker compose restart` 只重启旧镜像,健康检查无法证明合入代码已部署 | **PASS** | `env-service.sh rebuild` + 真实摘要重测 | 集成冒烟统一使用 `dev rebuild` | | DOCREV-RUNTIME-404 | revision-state 404;账本内存丢失;块保存/overlay 无效;agent 假报告 | **PASS** | [Round20 报告](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md);[audit](../audit/2026-09-13-document-revision-runtime-404.md) | 运行时 UI 已闭环。change report 仍为 fallback,不冒充 PI。历史失败目录不删 | | DOCREV-PG-ID-REMAP | 首次生成 PG 已写 snapshot/candidate,commit 因内存误删 baseline 而 not found | **PASS** | [audit](../audit/2026-09-13-document-revision-pg-id-remap.md);Round19 归档 `schema31-round19-ui03-advisor-briefing-docrevision-not-found`(candidate id=175 / output snapshot 250,禁止写成 candidate 250);Round20 UI-03 V2/r0 | 已合入 `dev@b258ab8`;不忽略 ErrNotFound;不删证据 | @@ -88,4 +88,4 @@ □ 前端:版本冲突刷新、终止 preview 人工抽检 ``` -**结论**: 文档账本运行时真实 UI Round20 **PASS**(见 [test-report](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md))。**发布 blocker 仍包括 `CHAT-INTENT-STATE`、CI 真实 PI 三工作流 E2E 与前端人工抽检**;`-race` 全包并行 TempDir flake 为测试基建残留。不得把 fallback change report 写成完整 PI 叙事。 +**结论**: 文档账本运行时真实 UI Round20 **PASS**(见 [test-report](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md))。`CHAT-INTENT-STATE` 双标签 busy **PASS**(项目 6275)。**发布 blocker 仍包括 CI 真实 PI 三工作流 E2E 与前端人工抽检**;`-race` 全包并行 TempDir flake 为测试基建残留。不得把 fallback change report 写成完整 PI 叙事。 diff --git a/.doc/status/FEATURE-MATRIX.md b/.doc/status/FEATURE-MATRIX.md index 8c7364fd..2d2a873d 100644 --- a/.doc/status/FEATURE-MATRIX.md +++ b/.doc/status/FEATURE-MATRIX.md @@ -15,7 +15,7 @@ | 模块 | 能力 | 状态 | 证据 | 缺口/后续 | mutex | |------|------|------|------|-----------|-------| -| 意图识别 L1 | 18 类 Intent + 三分支消解 | partial | [system-design/intent-recognition](../system-design/intent-recognition/);[现场专项审计](../audit/2026-09-13-conversation-intent-runtime-state.md) | 明确写动作与只读总结已分流;完整 hard-negative/eval 集仍待补齐 | `intent` | +| 意图识别 L1 | 18 类 Intent + 三分支消解 | partial | [system-design/intent-recognition](../system-design/intent-recognition/);[现场专项审计](../audit/2026-09-13-conversation-intent-runtime-state.md);[运行态报告](../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md);[双标签活验](../test-reports/assets/2026-09-15-conversation-intent-dual-tab/live.json) | 明确写动作与只读总结已分流;双标签 `DOC_BUSY` 已签(项目 6275)。完整 hard-negative/eval 集仍待补齐 | `intent` | | 对话 / ctxmgr | 多轮上下文、记忆、摘要 | partial | [system-design/multi-turn-dialogue](../system-design/multi-turn-dialogue/);[刷新恢复专项审计](../audit/2026-09-13-chat-history-refresh-disappears.md);[U10 端口化](../archive/ops/2026-09-15-chatlog-contextasm-u10.md);[U10 报告](../test-reports/2026-09-15-chatlog-contextasm-u10-test-report.md) | `CHAT-HISTORY-REFRESH` 闲聊刷新活验已补。U10 原地端口 **停止扩面**(未物理拆包)。语义摘要(D5 backlog) | `chat` | | 智能体小队 | 角色-任务-工作区-黑板 | done | [system-design/agent-squad](../system-design/agent-squad/);[test 2026-09-07](../test-reports/2026-09-07-dynamic-squad-planning-test-report.md) | 非关键步骤降级(A5/A6 backlog) | `squad` | | 工作流治理 | 三正式工作流 + 阶段门禁 | partial | [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);[冻结态/职责可选整改](../remediation/2026-09-14-workflow-plan-409-frozen-and-optional-duties-remediation.md);[Wave 5 页面矩阵](../test-reports/2026-09-14-workflow-plan-409-frozen-and-optional-duties-test-report.md);[职责-动作鉴权](../test-reports/2026-09-14-governance-action-level-authz-test-report.md) | 设置页/DutyMode + 动作级鉴权/未激活 202-blocked/关闭创建直通已落地。后续:none 文档真实 PI 审批长链路、CI 三工作流 E2E | `workflow` | @@ -23,7 +23,7 @@ | 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` | | 文档治理 | 法定文档生命周期 + 租约 + 受控正文读取 | 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` | +| 前端工作台 | 三栏 + 块文档 + 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);[双标签截图](../test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-b-busy.png) | 错误卡片/任务进度/文档 busy 刷新恢复已有项目 6275 证据。前端人工抽检仍开放 | `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 Round20](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md) | CI `TEST_DATABASE_URL` 隔离库仍缺;文档运行时真实 UI 已按 UI_PHASE 分文件且 post-restart 断言最终 A1 正文。`-race` 全包并行 TempDir flake 为基建残留 | `ci` | diff --git a/.doc/test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md b/.doc/test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md index a66847cf..de9a218f 100644 --- a/.doc/test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md +++ b/.doc/test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md @@ -59,9 +59,18 @@ ### 用例 E:真实 PI + 浏览器刷新/双标签页 -- **Input**: 真实 PI 只读总结已执行;浏览器刷新/双标签页待执行 -- **Actual Output**: 只读总结见用例 H;刷新和 busy 可见性尚无浏览器证据 -- **Evaluation**: PARTIAL +- **Input**: 新建项目 `INTENT双标签-061002`(id **6275**,**禁止 143**)/ `sess-p6275-u10`;Tab A `POST /api/v1/tasks/runs`「请开始编写用户初始需求表」真实 `pi-agent`;租约出现后 Tab B 再 POST;浏览器双标签打开同一项目 +- **Actual Output**: + - health/ready schema **38**、`agent_backend=pi-agent` + - Tab A `202` `run_1789452602295343365` status=running;2ms 内 `lease_holder_type=squad` + - Tab B HTTP **409** `DOC_BUSY`,响应无 `run_id`;PG `task_runs` 仅 1 行 + - 聊天窗 assistant `error_info.code=DOC_BUSY` 且 `actions=[]`,另有 `task_status=running` 占位;再次 GET window 同集 + - 真实 PI **completed**(约 280s,质量 100);租约在终态探测后 **1ms** 已空(无需客户端刷新) + - 终态后再 GET window:DOC_BUSY 卡仍 `actions=[]`,无重试动作 + - 浏览器(Docker Playwright,宿主机缺 libatk):双标签可见「本文档正由【项目助手】编辑/生成中」、聊天 `DOC_BUSY`、发送按钮 disabled、刷新后仍无重试。截图 `assets/2026-09-15-conversation-intent-dual-tab/screenshots/` +- **Evaluation**: **PASS** +- **性能与耗时**: API 活验 281s;Docker UI 4.4s。产物:`assets/2026-09-15-conversation-intent-dual-tab/live.json`、`playwright-docker.json` +- 诊断污染:`sess-p143-u9` **未**写入、未删除历史探针 ### 用例 F:dev 集成冒烟 @@ -106,4 +115,4 @@ git diff --check PASS ## 七、 测试结论与后续优化建议 (Conclusion & Next Steps) -代码级、构建门禁及真实 PI 只读总结已通过;原句不会创建任务或租约。最终 dev 镜像重建后 `/ready` 为 HTTP 200;只读复核确认用户原会话最大消息 ID 仍为 567、隔离 fixture 会话/消息/run 均为 0、project 143 无有效文档租约。尚不能标记发布完成:浏览器刷新恢复、双标签页租约可见性与并发写“不新增第二 run”仍需补录。真实会话中的诊断消息与误启动产物只记录证据,不在未获确认时删除或回滚。 +代码级、构建门禁、真实 PI 只读总结与 **新建项目双标签 busy** 已通过。项目 **6275** / `run_1789452602295343365`:并发写 409 `DOC_BUSY` 不新增第二 run;刷新后 `actions=[]`;PI 终态后租约自动解除。浏览器双标签截图见 `assets/2026-09-15-conversation-intent-dual-tab/screenshots/`。真实会话 `sess-p143-u9` 中的诊断消息与误启动产物只记录证据,不在未获确认时删除或回滚。`production_ready` 仍为 no(CI 三工作流 E2E / 前端抽检等其它 blocker)。 diff --git a/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/dual-tab-live.py b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/dual-tab-live.py new file mode 100644 index 00000000..054c7e64 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/dual-tab-live.py @@ -0,0 +1,500 @@ +#!/usr/bin/env python3 +"""Isolated dual-tab DOC_BUSY live check against rebuilt dev. Never touches project 143.""" +from __future__ import annotations + +import json +import os +import subprocess +import time +import urllib.error +import urllib.request +from datetime import datetime + +BASE = os.environ.get("BASE_URL", "http://127.0.0.1:8080").rstrip("/") +OWNER_USER = os.environ.get("OWNER_USERNAME", "zhanggong") +OWNER_PASS = os.environ.get("OWNER_PASSWORD", "123456") +REVIEWER_USER = os.environ.get("REVIEWER_USERNAME", "lizhuguan") +APPROVER_USER = os.environ.get("APPROVER_USERNAME", "wangzong") +ASSETS = os.environ.get( + "LIVE_ASSETS", + os.path.dirname(os.path.abspath(__file__)), +) +OUT = os.environ.get("LIVE_OUT", os.path.join(ASSETS, "live.json")) +STATE = os.path.join(ASSETS, "state.json") +FORBIDDEN_PROJECT = 143 + + +def req(method, path, token=None, body=None, timeout=180): + data = None + headers = {"Accept": "application/json"} + if body is not None: + data = json.dumps(body, ensure_ascii=False).encode() + headers["Content-Type"] = "application/json" + if token: + headers["Authorization"] = f"Bearer {token}" + headers["X-Session-Token"] = token + r = urllib.request.Request(BASE + path, data=data, headers=headers, method=method) + t0 = time.time() + try: + with urllib.request.urlopen(r, timeout=timeout) as resp: + raw = resp.read().decode() + parsed = json.loads(raw) if raw.strip() else None + return resp.status, parsed, raw, int((time.time() - t0) * 1000) + except urllib.error.HTTPError as e: + raw = e.read().decode() + try: + parsed = json.loads(raw) if raw.strip() else None + except json.JSONDecodeError: + parsed = None + return e.code, parsed, raw, int((time.time() - t0) * 1000) + + +def login(username, password): + status, parsed, raw, ms = req("POST", "/api/v1/auth/login", body={"username": username, "password": password}) + if status >= 400 or not parsed or not parsed.get("token"): + raise SystemExit(f"login failed for {username}: {status} {raw[:240]}") + user = parsed.get("user") or {} + return parsed["token"], int(user.get("id") or 0), ms + + +def flatten_window(body): + out = [] + for day in (body or {}).get("days") or []: + for m in day.get("messages") or []: + out.append(m) + return out + + +def assistant_error_cards(messages): + cards = [] + for m in messages: + if m.get("role") != "assistant": + continue + meta = m.get("metadata") or {} + err = meta.get("error_info") if isinstance(meta, dict) else None + if not isinstance(err, dict): + continue + cards.append({ + "id": m.get("id"), + "code": err.get("code") or err.get("error_code"), + "actions": err.get("actions") if err.get("actions") is not None else [], + "task_status": meta.get("task_status"), + "content": (m.get("content") or "")[:160], + }) + return cards + + +def running_placeholders(messages): + found = [] + for m in messages: + if m.get("role") != "assistant": + continue + meta = m.get("metadata") or {} + status = meta.get("task_status") if isinstance(meta, dict) else None + if status in ("running", "pending", "streaming"): + found.append({"id": m.get("id"), "task_status": status, "content": (m.get("content") or "")[:120]}) + return found + + +def workflow_leases(docs): + leases = [] + for d in docs or []: + if d.get("lease_holder_type"): + leases.append({ + "doc_id": d.get("doc_id"), + "title": d.get("title"), + "lease_holder_type": d.get("lease_holder_type"), + "lease_holder_id": d.get("lease_holder_id"), + "lease_expires_at": d.get("lease_expires_at"), + }) + return leases + + +def count_runs(project_id): + sql = ( + "SELECT run_id, status, session_id FROM task_runs " + f"WHERE project_id = {int(project_id)} ORDER BY created_at" + ) + cmd = [ + "docker", "exec", "shuimu-context-pg-dev", + "psql", "-U", "app", "-d", "agent_system_dev", "-t", "-A", "-F", "\t", + "-c", sql, + ] + p = subprocess.run(cmd, capture_output=True, text=True) + rows = [] + for line in (p.stdout or "").splitlines(): + line = line.strip() + if not line: + continue + parts = line.split("\t") + if len(parts) >= 3: + rows.append({"run_id": parts[0], "status": parts[1], "session_id": parts[2]}) + return p.returncode, rows, (p.stderr or "")[:300] + + +def write_json(path, obj): + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as f: + json.dump(obj, f, ensure_ascii=False, indent=2) + f.write("\n") + + +def main(): + suffix = datetime.now().strftime("%H%M%S") + cases = [] + t0 = time.time() + + health_s, health, _, health_ms = req("GET", "/health") + ready_s, ready, _, _ = req("GET", "/ready") + cases.append({ + "id": "health", + "verdict": "PASS" if health_s == 200 and ready_s == 200 and (health or {}).get("agent_backend") == "pi-agent" else "FAIL", + "duration_ms": health_ms, + "output": { + "schema_version": (health or {}).get("schema_version"), + "agent_backend": (health or {}).get("agent_backend"), + }, + }) + + owner_token, owner_id, login_ms = login(OWNER_USER, OWNER_PASS) + reviewer_token, reviewer_id, _ = login(REVIEWER_USER, OWNER_PASS) + approver_token, approver_id, _ = login(APPROVER_USER, OWNER_PASS) + cases.append({ + "id": "login", + "verdict": "PASS" if owner_id > 0 else "FAIL", + "duration_ms": login_ms, + "output": {"owner_id": owner_id, "reviewer_id": reviewer_id, "approver_id": approver_id}, + }) + + proj_name = f"INTENT双标签-{suffix}" + st, proj, raw, ms = req("POST", "/api/v1/projects", owner_token, { + "name": proj_name, + "device_type": "超声软组织手术吸引设备", + "intended_use": "intent dual-tab busy live", + "target_market": "中国 NMPA", + }) + project_id = int(proj["id"]) if isinstance(proj, dict) and proj.get("id") else 0 + if project_id == FORBIDDEN_PROJECT: + raise SystemExit("refusing to use project 143") + cases.append({ + "id": "create_project", + "verdict": "PASS" if st in (200, 201) and project_id > 0 and project_id != FORBIDDEN_PROJECT else "FAIL", + "http": st, + "duration_ms": ms, + "output": {"project_id": project_id, "name": proj_name}, + }) + if project_id <= 0: + raise SystemExit(f"create project failed: {st} {raw[:240]}") + + member_specs = ( + (reviewer_id, "reviewer"), + (approver_id, "approver"), + ) + for uid, duty in member_specs: + ms_s, _, ms_raw, _ = req("POST", f"/api/v1/projects/{project_id}/members", owner_token, { + "user_id": uid, + "project_role": "member", + "document_duty": duty, + }) + if ms_s >= 400: + # POST 可能不接受 duty;改为先加成员再 PUT + add_s, _, add_raw, _ = req("POST", f"/api/v1/projects/{project_id}/members", owner_token, { + "user_id": uid, + "project_role": "member", + }) + if add_s >= 400: + raise SystemExit(f"add member {uid}: {ms_s}/{add_s} {ms_raw[:200]} {add_raw[:200]}") + put_s, _, put_raw, _ = req("PUT", f"/api/v1/projects/{project_id}/members", owner_token, { + "user_id": uid, + "project_role": "member", + "document_duty": duty, + }) + if put_s >= 400: + raise SystemExit(f"put member duty {uid}: {put_s} {put_raw[:240]}") + + sess = f"sess-p{project_id}-u{owner_id}" + req("POST", "/api/v1/chat/sessions", owner_token, { + "session_id": sess, + "title": proj_name, + "user_id": owner_id, + "project_id": project_id, + }) + + default_s, plan, default_raw, _ = req( + "GET", + f"/api/v1/projects/{project_id}/workflow-plan/default?workflow_key=main&version=0", + owner_token, + ) + if default_s != 200 or not isinstance(plan, dict): + raise SystemExit(f"default plan failed: {default_s} {default_raw[:240]}") + for stg in plan.get("stages") or []: + for dt in stg.get("doc_tasks") or []: + dt["duties"] = { + "author_user_id": owner_id, + "reviewer_user_id": reviewer_id, + "approver_user_id": approver_id, + } + save_s, _, save_raw, _ = req("PUT", f"/api/v1/projects/{project_id}/workflow-plan", owner_token, { + "plan": plan, + "expected_plan_revision": 0, + "expected_selection_version": 1, + }) + prev_s, preview, prev_raw, _ = req("POST", f"/api/v1/projects/{project_id}/activation-preview", owner_token, {}) + activate_s, act, act_raw, _ = (0, None, "", 0) + if prev_s == 200 and isinstance(preview, dict): + activate_s, act, act_raw, _ = req("POST", f"/api/v1/projects/{project_id}/workflow-instances/activate", owner_token, { + "confirmation_token": preview.get("confirmation_token"), + "expected_project_version": preview.get("expected_project_version"), + "expected_plan_revision": preview.get("expected_plan_revision"), + }) + cases.append({ + "id": "activate", + "verdict": "PASS" if save_s == 200 and activate_s == 200 else "FAIL", + "http": activate_s, + "output": {"save_http": save_s, "preview_http": prev_s, "activate_http": activate_s}, + }) + if activate_s != 200: + raise SystemExit(f"activate failed: {activate_s} {act_raw[:300]} save={save_s} {save_raw[:200]}") + + _, runs_before, _ = count_runs(project_id) + write_prompt = "请开始编写用户初始需求表" + run_s, run, run_raw, run_ms = req("POST", "/api/v1/tasks/runs", owner_token, { + "session_id": sess, + "client_turn_id": f"intent-tab-a-{suffix}", + "user_message": write_prompt, + "task_key": "intake", + "project_id": project_id, + "project_name": proj_name, + "device_type": "超声软组织手术吸引设备", + "intended_use": "intent dual-tab busy live", + "author_id": owner_id, + "author_name": "张工", + "reviewer_id": reviewer_id, + "reviewer_name": "李主管", + "approver_id": approver_id, + "approver_name": "王总监", + "target_market": "中国 NMPA", + }, timeout=30) + run_id = (run or {}).get("run_id") if isinstance(run, dict) else None + cases.append({ + "id": "tab_a_start_write", + "verdict": "PASS" if run_s in (200, 202) and run_id else "FAIL", + "http": run_s, + "duration_ms": run_ms, + "output": {"run_id": run_id, "status": (run or {}).get("status") if isinstance(run, dict) else None}, + }) + if not run_id: + raise SystemExit(f"tab A write failed: {run_s} {run_raw[:400]}") + + lease_wait_s = 0 + leases = [] + docs = [] + lt0 = time.time() + for _ in range(60): + ds, docs, _, _ = req("GET", f"/api/v1/projects/{project_id}/documents?category=workflow", owner_token) + leases = workflow_leases(docs if isinstance(docs, list) else []) + lease_wait_s = int((time.time() - lt0) * 1000) + if leases: + break + time.sleep(1) + cases.append({ + "id": "tab_b_lease_visible", + "verdict": "PASS" if leases else "FAIL", + "duration_ms": lease_wait_s, + "output": {"leases": leases, "doc_count": len(docs) if isinstance(docs, list) else 0}, + }) + if not leases: + gs, gb, _, _ = req("GET", f"/api/v1/tasks/runs/{run_id}", owner_token, timeout=30) + write_json(OUT, {"overall": "FAIL", "reason": "lease never appeared", "run": gb, "docs": docs, "cases": cases}) + raise SystemExit(f"lease never appeared; run={gb}") + + busy_s, busy, busy_raw, busy_ms = req("POST", "/api/v1/tasks/runs", owner_token, { + "session_id": sess, + "client_turn_id": f"intent-tab-b-{suffix}", + "user_message": write_prompt, + "task_key": "intake", + "project_id": project_id, + "project_name": proj_name, + "device_type": "超声软组织手术吸引设备", + "intended_use": "intent dual-tab busy live", + "target_market": "中国 NMPA", + "author_id": owner_id, + "reviewer_id": reviewer_id, + "approver_id": approver_id, + }, timeout=30) + busy_code = (busy or {}).get("error_code") if isinstance(busy, dict) else None + has_run_id = isinstance(busy, dict) and "run_id" in busy and busy.get("run_id") + _, runs_after_busy, count_err = count_runs(project_id) + cases.append({ + "id": "tab_b_doc_busy", + "verdict": "PASS" if busy_s == 409 and busy_code == "DOC_BUSY" and not has_run_id and len(runs_after_busy) == len(runs_before) + 1 else "FAIL", + "http": busy_s, + "duration_ms": busy_ms, + "output": { + "error_code": busy_code, + "has_run_id": bool(has_run_id), + "runs_before": runs_before, + "runs_after": runs_after_busy, + "count_err": count_err, + "body_snip": (busy_raw or "")[:400], + }, + }) + + win_s, win, _, win_ms = req("GET", f"/api/v1/chat/messages/window?session_id={sess}&days=7", owner_token) + msgs = flatten_window(win) + cards = assistant_error_cards(msgs) + running = running_placeholders(msgs) + busy_cards = [c for c in cards if c.get("code") == "DOC_BUSY" and c.get("actions") == []] + write_json(os.path.join(ASSETS, "window-before-refresh.json"), win) + cases.append({ + "id": "busy_card_before_refresh", + "verdict": "PASS" if win_s == 200 and busy_cards and (running or run_id) else "FAIL", + "http": win_s, + "duration_ms": win_ms, + "output": {"busy_cards": busy_cards, "running": running, "msg_count": len(msgs)}, + }) + + win2_s, win2, _, _ = req("GET", f"/api/v1/chat/messages/window?session_id={sess}&days=7", owner_token) + msgs2 = flatten_window(win2) + cards2 = assistant_error_cards(msgs2) + running2 = running_placeholders(msgs2) + write_json(os.path.join(ASSETS, "window-after-refresh.json"), win2) + same_actions = [c.get("actions") for c in cards] == [c.get("actions") for c in cards2] + cases.append({ + "id": "refresh_same_set", + "verdict": "PASS" if win2_s == 200 and same_actions and busy_cards and assistant_error_cards(msgs2) else "FAIL", + "http": win2_s, + "output": { + "busy_cards_after": [c for c in cards2 if c.get("code") == "DOC_BUSY"], + "running_after": running2, + "same_actions": same_actions, + }, + }) + + state = { + "project_id": project_id, + "project_name": proj_name, + "session_id": sess, + "run_id": run_id, + "owner_id": owner_id, + "forbidden_project": FORBIDDEN_PROJECT, + "phase": "busy", + } + write_json(STATE, state) + + observe_cmd = os.environ.get("OBSERVE_CMD") + observe = {"skipped": True} + if observe_cmd: + env = os.environ.copy() + env.update({ + "INTENT_DUAL_TAB": "1", + "INTENT_PROJECT_NAME": proj_name, + "INTENT_PROJECT_ID": str(project_id), + "INTENT_ASSETS": ASSETS, + "OWNER_USERNAME": OWNER_USER, + "OWNER_PASSWORD": OWNER_PASS, + }) + ot0 = time.time() + op = subprocess.run(observe_cmd, shell=True, cwd=os.environ.get("WEB_DIR", ""), env=env, capture_output=True, text=True) + observe = { + "skipped": False, + "returncode": op.returncode, + "duration_ms": int((time.time() - ot0) * 1000), + "stdout_tail": (op.stdout or "")[-2000:], + "stderr_tail": (op.stderr or "")[-1500:], + } + write_json(os.path.join(ASSETS, "playwright-observe.json"), observe) + cases.append({ + "id": "browser_observe", + "verdict": "PASS" if observe.get("skipped") or observe.get("returncode") == 0 else "FAIL", + "output": { + "skipped": observe.get("skipped"), + "returncode": observe.get("returncode"), + "duration_ms": observe.get("duration_ms"), + "has_stdout": bool(observe.get("stdout_tail")), + }, + }) + + final_status = None + poll_ms = 0 + pt0 = time.time() + for _ in range(72): + gs, gb, _, _ = req("GET", f"/api/v1/tasks/runs/{run_id}", owner_token, timeout=30) + if gs == 200 and isinstance(gb, dict): + final_status = gb.get("status") or gb.get("Status") + if str(final_status).lower() in ( + "completed", "failed", "cancelled", "interrupted", + "pending_author_confirmation", "quality_failed", "blocked_pending_governance", + ): + break + time.sleep(10) + poll_ms = int((time.time() - pt0) * 1000) + + clear_ms = 0 + leases_after = leases + ct0 = time.time() + auto_cleared = False + for i in range(12): + ds, docs, _, _ = req("GET", f"/api/v1/projects/{project_id}/documents?category=workflow", owner_token) + leases_after = workflow_leases(docs if isinstance(docs, list) else []) + clear_ms = int((time.time() - ct0) * 1000) + if not leases_after: + auto_cleared = True + break + time.sleep(5) + cases.append({ + "id": "tab_a_terminal_and_lease_clear", + "verdict": "PASS" if final_status and auto_cleared else "FAIL", + "duration_ms": poll_ms + clear_ms, + "output": { + "final_status": final_status, + "poll_ms": poll_ms, + "lease_clear_ms_after_terminal_poll": clear_ms, + "auto_cleared_without_client_reload": auto_cleared, + "leases_after": leases_after, + }, + }) + + win3_s, win3, _, _ = req("GET", f"/api/v1/chat/messages/window?session_id={sess}&days=7", owner_token) + write_json(os.path.join(ASSETS, "window-after-terminal.json"), win3) + cards3 = assistant_error_cards(flatten_window(win3)) + still_empty_actions = all(c.get("actions") == [] for c in cards3 if c.get("code") == "DOC_BUSY") + cases.append({ + "id": "busy_card_survives_terminal_refresh", + "verdict": "PASS" if win3_s == 200 and still_empty_actions and any(c.get("code") == "DOC_BUSY" for c in cards3) else "FAIL", + "http": win3_s, + "output": {"busy_cards": [c for c in cards3 if c.get("code") == "DOC_BUSY"]}, + }) + + _, runs_final, _ = count_runs(project_id) + cases.append({ + "id": "single_run_final", + "verdict": "PASS" if len(runs_final) == 1 and runs_final[0]["run_id"] == run_id else "FAIL", + "output": {"runs": runs_final}, + }) + + state["phase"] = "terminal" + state["final_status"] = final_status + write_json(STATE, state) + + overall = "PASS" if all(c["verdict"] == "PASS" for c in cases if c["id"] != "browser_observe") else "FAIL" + if any(c["id"] == "browser_observe" and c["verdict"] == "FAIL" for c in cases): + overall = "FAIL" + result = { + "overall": overall, + "duration_ms": int((time.time() - t0) * 1000), + "project_id": project_id, + "project_name": proj_name, + "session_id": sess, + "run_id": run_id, + "forbidden_project_untouched": True, + "cases": cases, + "browser_observe": observe, + } + write_json(OUT, result) + print(json.dumps({"overall": overall, "project_id": project_id, "run_id": run_id, "out": OUT}, ensure_ascii=False)) + raise SystemExit(0 if overall == "PASS" else 1) + + +if __name__ == "__main__": + main() diff --git a/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/live.json b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/live.json new file mode 100644 index 00000000..463fe10c --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/live.json @@ -0,0 +1,204 @@ +{ + "overall": "FAIL", + "duration_ms": 281149, + "project_id": 6275, + "project_name": "INTENT双标签-061002", + "session_id": "sess-p6275-u10", + "run_id": "run_1789452602295343365", + "forbidden_project_untouched": true, + "cases": [ + { + "id": "health", + "verdict": "PASS", + "duration_ms": 6, + "output": { + "schema_version": 38, + "agent_backend": "pi-agent" + } + }, + { + "id": "login", + "verdict": "PASS", + "duration_ms": 42, + "output": { + "owner_id": 10, + "reviewer_id": 11, + "approver_id": 12 + } + }, + { + "id": "create_project", + "verdict": "PASS", + "http": 201, + "duration_ms": 19, + "output": { + "project_id": 6275, + "name": "INTENT双标签-061002" + } + }, + { + "id": "activate", + "verdict": "PASS", + "http": 200, + "output": { + "save_http": 200, + "preview_http": 200, + "activate_http": 200 + } + }, + { + "id": "tab_a_start_write", + "verdict": "PASS", + "http": 202, + "duration_ms": 11, + "output": { + "run_id": "run_1789452602295343365", + "status": "running" + } + }, + { + "id": "tab_b_lease_visible", + "verdict": "PASS", + "duration_ms": 2, + "output": { + "leases": [ + { + "doc_id": "doc_proj6275_intake", + "title": "用户初始需求收集表", + "lease_holder_type": "squad", + "lease_holder_id": "run_1789452602295343365", + "lease_expires_at": "2026-09-15T07:10:02.305032Z" + } + ], + "doc_count": 1 + } + }, + { + "id": "tab_b_doc_busy", + "verdict": "PASS", + "http": 409, + "duration_ms": 6, + "output": { + "error_code": "DOC_BUSY", + "has_run_id": false, + "runs_before": [], + "runs_after": [ + { + "run_id": "run_1789452602295343365", + "status": "running", + "session_id": "sess-p6275-u10" + } + ], + "count_err": "", + "body_snip": "{\"detail\":{\"doc_id\":\"doc_proj6275_intake\",\"holder_type\":\"squad\",\"holder_id\":\"run_1789452602295343365\",\"expires_at\":\"2026-09-15T07:10:02.305032737Z\",\"reason\":\"文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务\"},\"error\":\"document_busy\",\"error_code\":\"DOC_BUSY\",\"message\":\"文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务\"}\n" + } + }, + { + "id": "busy_card_before_refresh", + "verdict": "PASS", + "http": 200, + "duration_ms": 3, + "output": { + "busy_cards": [ + { + "id": 2322, + "code": "DOC_BUSY", + "actions": [], + "task_status": "failed", + "content": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务" + } + ], + "running": [ + { + "id": 2320, + "task_status": "running", + "content": "任务执行中…" + } + ], + "msg_count": 4 + } + }, + { + "id": "refresh_same_set", + "verdict": "PASS", + "http": 200, + "output": { + "busy_cards_after": [ + { + "id": 2322, + "code": "DOC_BUSY", + "actions": [], + "task_status": "failed", + "content": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务" + } + ], + "running_after": [ + { + "id": 2320, + "task_status": "running", + "content": "任务执行中…" + } + ], + "same_actions": true + } + }, + { + "id": "browser_observe", + "verdict": "FAIL", + "output": { + "skipped": false, + "returncode": 1, + "duration_ms": 675, + "has_stdout": true + } + }, + { + "id": "tab_a_terminal_and_lease_clear", + "verdict": "PASS", + "duration_ms": 280110, + "output": { + "final_status": "completed", + "poll_ms": 280109, + "lease_clear_ms_after_terminal_poll": 1, + "auto_cleared_without_client_reload": true, + "leases_after": [] + } + }, + { + "id": "busy_card_survives_terminal_refresh", + "verdict": "PASS", + "http": 200, + "output": { + "busy_cards": [ + { + "id": 2322, + "code": "DOC_BUSY", + "actions": [], + "task_status": "failed", + "content": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务" + } + ] + } + }, + { + "id": "single_run_final", + "verdict": "PASS", + "output": { + "runs": [ + { + "run_id": "run_1789452602295343365", + "status": "completed", + "session_id": "sess-p6275-u10" + } + ] + } + } + ], + "browser_observe": { + "skipped": false, + "returncode": 1, + "duration_ms": 675, + "stdout_tail": "EdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-updater-scheduler --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/tmp/playwright_chromiumdev_profile-klGxjP --remote-debugging-pipe --no-startup-window\n - pid=3847953\n - [pid=3847953][err] /home/weichen/.cache/ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell: error while loading shared libraries: libatk-1.0.so.0: cannot open shared object file: No such file or directory\n - [pid=3847953] \n - [pid=3847953] \n - [pid=3847953] \n - [pid=3847953] exception while trying to kill process: Error: kill ESRCH\n - [pid=3847953] \n - [pid=3847953] starting temporary directories cleanup\n - [pid=3847953] finished temporary directories cleanup\n - [pid=3847953] \n\n\n Error Context: ../.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance/playwright-output-main/conversation-intent-dual-t-ac365-oad-and-keeps-empty-actions-chromium/error-context.md\n\n 1 failed\n [chromium] › e2e/conversation-intent-dual-tab.spec.ts:36:3 › conversation intent dual-tab busy › tab B shows busy without reload and keeps empty actions \n", + "stderr_tail": "(node:3847921) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n(Use `node --trace-warnings ...` to show where the warning was created)\n" + } +} diff --git a/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/playwright-docker.json b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/playwright-docker.json new file mode 100644 index 00000000..c20addf4 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/playwright-docker.json @@ -0,0 +1,23 @@ +{ + "verdict": "PASS", + "driver": "docker mcr.microsoft.com/playwright:v1.63.0-noble --network host", + "project_id": 6275, + "project_name": "INTENT双标签-061002", + "session_id": "sess-p6275-u10", + "forbidden_project": 143, + "duration_ms": 4400, + "assertions": [ + "banner 当前项目 INTENT双标签-061002", + "编辑器条 本文档正由【项目助手】编辑/生成中", + "聊天卡 DOC_BUSY 且无重试按钮", + "发送按钮 disabled(输入保护)", + "刷新后 DOC_BUSY 仍在且无重试" + ], + "screenshots": [ + "screenshots/tab-a-running.png", + "screenshots/tab-b-busy.png", + "screenshots/tab-b-after-write.png", + "screenshots/tab-b-after-reload.png" + ], + "notes": "宿主机 Playwright Chromium 缺 libatk,改用官方镜像。双标签共享同一登录上下文。API 409/单 run 证据见 live.json(真实 PI 编制约 280s 后 completed,lease 自动清除)。" +} diff --git a/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/playwright-network.json b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/playwright-network.json new file mode 100644 index 00000000..7d0b40a3 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/playwright-network.json @@ -0,0 +1,4 @@ +{ + "posts": [], + "project": "INTENT双标签-061002" +} \ No newline at end of file diff --git a/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/playwright-observe.json b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/playwright-observe.json new file mode 100644 index 00000000..6fee651e --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/playwright-observe.json @@ -0,0 +1,7 @@ +{ + "skipped": false, + "returncode": 1, + "duration_ms": 675, + "stdout_tail": "EdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-updater-scheduler --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/tmp/playwright_chromiumdev_profile-klGxjP --remote-debugging-pipe --no-startup-window\n - pid=3847953\n - [pid=3847953][err] /home/weichen/.cache/ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell: error while loading shared libraries: libatk-1.0.so.0: cannot open shared object file: No such file or directory\n - [pid=3847953] \n - [pid=3847953] \n - [pid=3847953] \n - [pid=3847953] exception while trying to kill process: Error: kill ESRCH\n - [pid=3847953] \n - [pid=3847953] starting temporary directories cleanup\n - [pid=3847953] finished temporary directories cleanup\n - [pid=3847953] \n\n\n Error Context: ../.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance/playwright-output-main/conversation-intent-dual-t-ac365-oad-and-keeps-empty-actions-chromium/error-context.md\n\n 1 failed\n [chromium] › e2e/conversation-intent-dual-tab.spec.ts:36:3 › conversation intent dual-tab busy › tab B shows busy without reload and keeps empty actions \n", + "stderr_tail": "(node:3847921) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n(Use `node --trace-warnings ...` to show where the warning was created)\n" +} diff --git a/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-a-running.png b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-a-running.png new file mode 100644 index 00000000..0685b915 Binary files /dev/null and b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-a-running.png differ diff --git a/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-b-after-reload.png b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-b-after-reload.png new file mode 100644 index 00000000..05c9f87e Binary files /dev/null and b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-b-after-reload.png differ diff --git a/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-b-after-write.png b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-b-after-write.png new file mode 100644 index 00000000..136bc64d Binary files /dev/null and b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-b-after-write.png differ diff --git a/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-b-busy.png b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-b-busy.png new file mode 100644 index 00000000..fac6bbf4 Binary files /dev/null and b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-b-busy.png differ diff --git a/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/state.json b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/state.json new file mode 100644 index 00000000..846f0ba1 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/state.json @@ -0,0 +1,10 @@ +{ + "project_id": 6275, + "project_name": "INTENT双标签-061002", + "session_id": "sess-p6275-u10", + "run_id": "run_1789452602295343365", + "owner_id": 10, + "forbidden_project": 143, + "phase": "terminal", + "final_status": "completed" +} diff --git a/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/window-after-refresh.json b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/window-after-refresh.json new file mode 100644 index 00000000..2a51cd17 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/window-after-refresh.json @@ -0,0 +1,209 @@ +{ + "session_id": "sess-p6275-u10", + "days": [ + { + "day_key": "2026-09-15", + "count": 4, + "loaded_count": 4, + "truncated": false, + "first_message_id": 2319, + "last_message_id": 2322, + "messages": [ + { + "id": 2319, + "session_id": "sess-p6275-u10", + "user_id": 10, + "role": "user", + "content": "请开始编写用户初始需求表", + "intent": "TRIGGER_TASK", + "metadata": { + "client_turn_id": "intent-tab-a-061002", + "project_id": 6275, + "scope_kind": "workflow_stage", + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "task_key": "intake", + "workflow_instance_id": 211 + }, + "day_key": "2026-09-15", + "category": "task", + "content_kind": "conversational_text", + "export_policy": "none", + "content_revision": 1, + "content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a", + "workflow_instance_id": 211, + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "scope_kind": "workflow_stage", + "export_capability": { + "policy": "none", + "formats": [], + "body_selector": "content_only", + "reason_code": "NOT_ASSISTANT", + "content_revision": 1, + "content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a" + }, + "created_at": "2026-09-15T06:10:02.29734Z" + }, + { + "id": 2320, + "session_id": "sess-p6275-u10", + "user_id": 0, + "role": "assistant", + "content": "任务执行中…", + "intent": "TRIGGER_TASK", + "metadata": { + "client_turn_id": "intent-tab-a-061002", + "project_id": 6275, + "run_id": "run_1789452602295343365", + "scope_kind": "workflow_stage", + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "task_key": "intake", + "task_request": { + "client_turn_id": "intent-tab-a-061002", + "customer_name": "", + "device_type": "超声软组织手术吸引设备", + "doc_id": "", + "intended_use": "intent dual-tab busy live", + "project_id": 6275, + "project_name": "INTENT双标签-061002", + "session_id": "sess-p6275-u10", + "target_market": "中国 NMPA", + "task_key": "intake", + "user_id": 10, + "user_message": "请开始编写用户初始需求表" + }, + "task_status": "running", + "workflow_instance_id": 211 + }, + "day_key": "2026-09-15", + "category": "task", + "content_kind": "document_text", + "export_policy": "full", + "content_revision": 1, + "content_sha256": "e98386b7da0d395fed20132b53e21283fcc11efdb7bae134edb24bcb3930f535", + "workflow_instance_id": 211, + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "scope_kind": "workflow_stage", + "export_capability": { + "policy": "full", + "formats": [ + "rich_clipboard", + "markdown", + "docx", + "pdf", + "private_document" + ], + "body_selector": "content_only", + "content_revision": 1, + "content_sha256": "e98386b7da0d395fed20132b53e21283fcc11efdb7bae134edb24bcb3930f535" + }, + "created_at": "2026-09-15T06:10:02.30037Z" + }, + { + "id": 2321, + "session_id": "sess-p6275-u10", + "user_id": 10, + "role": "user", + "content": "请开始编写用户初始需求表", + "intent": "TRIGGER_TASK", + "metadata": { + "client_turn_id": "intent-tab-b-061002", + "project_id": 6275, + "scope_kind": "workflow_stage", + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "task_key": "intake", + "workflow_instance_id": 211 + }, + "day_key": "2026-09-15", + "category": "task", + "content_kind": "conversational_text", + "export_policy": "none", + "content_revision": 1, + "content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a", + "workflow_instance_id": 211, + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "scope_kind": "workflow_stage", + "export_capability": { + "policy": "none", + "formats": [], + "body_selector": "content_only", + "reason_code": "NOT_ASSISTANT", + "content_revision": 1, + "content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a" + }, + "created_at": "2026-09-15T06:10:02.309173Z" + }, + { + "id": 2322, + "session_id": "sess-p6275-u10", + "user_id": 0, + "role": "assistant", + "content": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务", + "intent": "TRIGGER_TASK", + "metadata": { + "client_turn_id": "intent-tab-b-061002", + "error_info": { + "actions": [], + "class": "fatal", + "code": "DOC_BUSY", + "session_id": "sess-p6275-u10", + "user_message": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务" + }, + "project_id": 6275, + "scope_kind": "workflow_stage", + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "task_key": "intake", + "task_request": { + "client_turn_id": "intent-tab-b-061002", + "customer_name": "", + "device_type": "超声软组织手术吸引设备", + "doc_id": "", + "intended_use": "intent dual-tab busy live", + "project_id": 6275, + "project_name": "INTENT双标签-061002", + "session_id": "sess-p6275-u10", + "target_market": "中国 NMPA", + "task_key": "intake", + "user_id": 10, + "user_message": "请开始编写用户初始需求表" + }, + "task_status": "failed", + "workflow_instance_id": 211 + }, + "day_key": "2026-09-15", + "category": "task", + "content_kind": "document_text", + "export_policy": "full", + "content_revision": 1, + "content_sha256": "5137c046ff6011eaa672af24e6fea41c83cc9e0e598287aad1a21b0ccea9915d", + "workflow_instance_id": 211, + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "scope_kind": "workflow_stage", + "export_capability": { + "policy": "full", + "formats": [ + "rich_clipboard", + "markdown", + "docx", + "pdf", + "private_document" + ], + "body_selector": "content_only", + "content_revision": 1, + "content_sha256": "5137c046ff6011eaa672af24e6fea41c83cc9e0e598287aad1a21b0ccea9915d" + }, + "created_at": "2026-09-15T06:10:02.311768Z" + } + ] + } + ], + "has_more_before": false, + "oldest_loaded_day": "2026-09-15" +} diff --git a/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/window-after-terminal.json b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/window-after-terminal.json new file mode 100644 index 00000000..f1d2702c --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/window-after-terminal.json @@ -0,0 +1,221 @@ +{ + "session_id": "sess-p6275-u10", + "days": [ + { + "day_key": "2026-09-15", + "count": 4, + "loaded_count": 4, + "truncated": false, + "first_message_id": 2319, + "last_message_id": 2322, + "messages": [ + { + "id": 2319, + "session_id": "sess-p6275-u10", + "user_id": 10, + "role": "user", + "content": "请开始编写用户初始需求表", + "intent": "TRIGGER_TASK", + "metadata": { + "client_turn_id": "intent-tab-a-061002", + "project_id": 6275, + "scope_kind": "workflow_stage", + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "task_key": "intake", + "workflow_instance_id": 211 + }, + "day_key": "2026-09-15", + "category": "task", + "content_kind": "conversational_text", + "export_policy": "none", + "content_revision": 1, + "content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a", + "workflow_instance_id": 211, + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "scope_kind": "workflow_stage", + "export_capability": { + "policy": "none", + "formats": [], + "body_selector": "content_only", + "reason_code": "NOT_ASSISTANT", + "content_revision": 1, + "content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a" + }, + "created_at": "2026-09-15T06:10:02.29734Z" + }, + { + "id": 2320, + "session_id": "sess-p6275-u10", + "user_id": 0, + "role": "assistant", + "content": "《用户初始需求收集表》已生成。\n\n当前状态:**已完成**\n质量评分:100/100\n\n📄 受控文档:`doc_proj6275_intake`", + "intent": "TRIGGER_TASK", + "metadata": { + "artifact": { + "doc_id": "doc_proj6275_intake", + "quality_passed": true, + "quality_score": 100, + "sha256": "dc99aa130644353f258e951887c9d3babf35afc9d46e008327121bdedc641ddf", + "task_key": "intake", + "title": "用户初始需求收集表", + "workspace_id": "bizdev" + }, + "client_turn_id": "intent-tab-a-061002", + "project_id": 6275, + "run_id": "run_1789452602295343365", + "run_manifest_sha256": "eec87a66d69f1e04df279689e66355c552dd7ac1756db4676430cfe8d92419d7", + "scope_kind": "workflow_stage", + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "storage_identity": "env:dev", + "task_key": "intake", + "task_request": { + "client_turn_id": "intent-tab-a-061002", + "customer_name": "", + "device_type": "超声软组织手术吸引设备", + "doc_id": "", + "intended_use": "intent dual-tab busy live", + "project_id": 6275, + "project_name": "INTENT双标签-061002", + "session_id": "sess-p6275-u10", + "target_market": "中国 NMPA", + "task_key": "intake", + "user_id": 10, + "user_message": "请开始编写用户初始需求表" + }, + "task_status": "completed", + "workflow_instance_id": 211, + "workspace_id": "bizdev" + }, + "day_key": "2026-09-15", + "category": "task", + "content_kind": "document_text", + "export_policy": "full", + "content_revision": 1, + "content_sha256": "e98386b7da0d395fed20132b53e21283fcc11efdb7bae134edb24bcb3930f535", + "workflow_instance_id": 211, + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "scope_kind": "workflow_stage", + "export_capability": { + "policy": "full", + "formats": [ + "rich_clipboard", + "markdown", + "docx", + "pdf", + "private_document" + ], + "body_selector": "content_only", + "content_revision": 1, + "content_sha256": "e98386b7da0d395fed20132b53e21283fcc11efdb7bae134edb24bcb3930f535" + }, + "created_at": "2026-09-15T06:10:02.30037Z" + }, + { + "id": 2321, + "session_id": "sess-p6275-u10", + "user_id": 10, + "role": "user", + "content": "请开始编写用户初始需求表", + "intent": "TRIGGER_TASK", + "metadata": { + "client_turn_id": "intent-tab-b-061002", + "project_id": 6275, + "scope_kind": "workflow_stage", + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "task_key": "intake", + "workflow_instance_id": 211 + }, + "day_key": "2026-09-15", + "category": "task", + "content_kind": "conversational_text", + "export_policy": "none", + "content_revision": 1, + "content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a", + "workflow_instance_id": 211, + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "scope_kind": "workflow_stage", + "export_capability": { + "policy": "none", + "formats": [], + "body_selector": "content_only", + "reason_code": "NOT_ASSISTANT", + "content_revision": 1, + "content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a" + }, + "created_at": "2026-09-15T06:10:02.309173Z" + }, + { + "id": 2322, + "session_id": "sess-p6275-u10", + "user_id": 0, + "role": "assistant", + "content": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务", + "intent": "TRIGGER_TASK", + "metadata": { + "client_turn_id": "intent-tab-b-061002", + "error_info": { + "actions": [], + "class": "fatal", + "code": "DOC_BUSY", + "session_id": "sess-p6275-u10", + "user_message": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务" + }, + "project_id": 6275, + "scope_kind": "workflow_stage", + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "task_key": "intake", + "task_request": { + "client_turn_id": "intent-tab-b-061002", + "customer_name": "", + "device_type": "超声软组织手术吸引设备", + "doc_id": "", + "intended_use": "intent dual-tab busy live", + "project_id": 6275, + "project_name": "INTENT双标签-061002", + "session_id": "sess-p6275-u10", + "target_market": "中国 NMPA", + "task_key": "intake", + "user_id": 10, + "user_message": "请开始编写用户初始需求表" + }, + "task_status": "failed", + "workflow_instance_id": 211 + }, + "day_key": "2026-09-15", + "category": "task", + "content_kind": "document_text", + "export_policy": "full", + "content_revision": 1, + "content_sha256": "5137c046ff6011eaa672af24e6fea41c83cc9e0e598287aad1a21b0ccea9915d", + "workflow_instance_id": 211, + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "scope_kind": "workflow_stage", + "export_capability": { + "policy": "full", + "formats": [ + "rich_clipboard", + "markdown", + "docx", + "pdf", + "private_document" + ], + "body_selector": "content_only", + "content_revision": 1, + "content_sha256": "5137c046ff6011eaa672af24e6fea41c83cc9e0e598287aad1a21b0ccea9915d" + }, + "created_at": "2026-09-15T06:10:02.311768Z" + } + ] + } + ], + "has_more_before": false, + "oldest_loaded_day": "2026-09-15" +} diff --git a/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/window-before-refresh.json b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/window-before-refresh.json new file mode 100644 index 00000000..2a51cd17 --- /dev/null +++ b/.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/window-before-refresh.json @@ -0,0 +1,209 @@ +{ + "session_id": "sess-p6275-u10", + "days": [ + { + "day_key": "2026-09-15", + "count": 4, + "loaded_count": 4, + "truncated": false, + "first_message_id": 2319, + "last_message_id": 2322, + "messages": [ + { + "id": 2319, + "session_id": "sess-p6275-u10", + "user_id": 10, + "role": "user", + "content": "请开始编写用户初始需求表", + "intent": "TRIGGER_TASK", + "metadata": { + "client_turn_id": "intent-tab-a-061002", + "project_id": 6275, + "scope_kind": "workflow_stage", + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "task_key": "intake", + "workflow_instance_id": 211 + }, + "day_key": "2026-09-15", + "category": "task", + "content_kind": "conversational_text", + "export_policy": "none", + "content_revision": 1, + "content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a", + "workflow_instance_id": 211, + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "scope_kind": "workflow_stage", + "export_capability": { + "policy": "none", + "formats": [], + "body_selector": "content_only", + "reason_code": "NOT_ASSISTANT", + "content_revision": 1, + "content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a" + }, + "created_at": "2026-09-15T06:10:02.29734Z" + }, + { + "id": 2320, + "session_id": "sess-p6275-u10", + "user_id": 0, + "role": "assistant", + "content": "任务执行中…", + "intent": "TRIGGER_TASK", + "metadata": { + "client_turn_id": "intent-tab-a-061002", + "project_id": 6275, + "run_id": "run_1789452602295343365", + "scope_kind": "workflow_stage", + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "task_key": "intake", + "task_request": { + "client_turn_id": "intent-tab-a-061002", + "customer_name": "", + "device_type": "超声软组织手术吸引设备", + "doc_id": "", + "intended_use": "intent dual-tab busy live", + "project_id": 6275, + "project_name": "INTENT双标签-061002", + "session_id": "sess-p6275-u10", + "target_market": "中国 NMPA", + "task_key": "intake", + "user_id": 10, + "user_message": "请开始编写用户初始需求表" + }, + "task_status": "running", + "workflow_instance_id": 211 + }, + "day_key": "2026-09-15", + "category": "task", + "content_kind": "document_text", + "export_policy": "full", + "content_revision": 1, + "content_sha256": "e98386b7da0d395fed20132b53e21283fcc11efdb7bae134edb24bcb3930f535", + "workflow_instance_id": 211, + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "scope_kind": "workflow_stage", + "export_capability": { + "policy": "full", + "formats": [ + "rich_clipboard", + "markdown", + "docx", + "pdf", + "private_document" + ], + "body_selector": "content_only", + "content_revision": 1, + "content_sha256": "e98386b7da0d395fed20132b53e21283fcc11efdb7bae134edb24bcb3930f535" + }, + "created_at": "2026-09-15T06:10:02.30037Z" + }, + { + "id": 2321, + "session_id": "sess-p6275-u10", + "user_id": 10, + "role": "user", + "content": "请开始编写用户初始需求表", + "intent": "TRIGGER_TASK", + "metadata": { + "client_turn_id": "intent-tab-b-061002", + "project_id": 6275, + "scope_kind": "workflow_stage", + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "task_key": "intake", + "workflow_instance_id": 211 + }, + "day_key": "2026-09-15", + "category": "task", + "content_kind": "conversational_text", + "export_policy": "none", + "content_revision": 1, + "content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a", + "workflow_instance_id": 211, + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "scope_kind": "workflow_stage", + "export_capability": { + "policy": "none", + "formats": [], + "body_selector": "content_only", + "reason_code": "NOT_ASSISTANT", + "content_revision": 1, + "content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a" + }, + "created_at": "2026-09-15T06:10:02.309173Z" + }, + { + "id": 2322, + "session_id": "sess-p6275-u10", + "user_id": 0, + "role": "assistant", + "content": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务", + "intent": "TRIGGER_TASK", + "metadata": { + "client_turn_id": "intent-tab-b-061002", + "error_info": { + "actions": [], + "class": "fatal", + "code": "DOC_BUSY", + "session_id": "sess-p6275-u10", + "user_message": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务" + }, + "project_id": 6275, + "scope_kind": "workflow_stage", + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "task_key": "intake", + "task_request": { + "client_turn_id": "intent-tab-b-061002", + "customer_name": "", + "device_type": "超声软组织手术吸引设备", + "doc_id": "", + "intended_use": "intent dual-tab busy live", + "project_id": 6275, + "project_name": "INTENT双标签-061002", + "session_id": "sess-p6275-u10", + "target_market": "中国 NMPA", + "task_key": "intake", + "user_id": 10, + "user_message": "请开始编写用户初始需求表" + }, + "task_status": "failed", + "workflow_instance_id": 211 + }, + "day_key": "2026-09-15", + "category": "task", + "content_kind": "document_text", + "export_policy": "full", + "content_revision": 1, + "content_sha256": "5137c046ff6011eaa672af24e6fea41c83cc9e0e598287aad1a21b0ccea9915d", + "workflow_instance_id": 211, + "stage_instance_id": 295, + "stage_key": "V0.1_triage", + "scope_kind": "workflow_stage", + "export_capability": { + "policy": "full", + "formats": [ + "rich_clipboard", + "markdown", + "docx", + "pdf", + "private_document" + ], + "body_selector": "content_only", + "content_revision": 1, + "content_sha256": "5137c046ff6011eaa672af24e6fea41c83cc9e0e598287aad1a21b0ccea9915d" + }, + "created_at": "2026-09-15T06:10:02.311768Z" + } + ] + } + ], + "has_more_before": false, + "oldest_loaded_day": "2026-09-15" +} diff --git a/web/e2e/conversation-intent-dual-tab.spec.ts b/web/e2e/conversation-intent-dual-tab.spec.ts new file mode 100644 index 00000000..28db2122 --- /dev/null +++ b/web/e2e/conversation-intent-dual-tab.spec.ts @@ -0,0 +1,99 @@ +import { test, expect, type Page } from '@playwright/test'; +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const runDualTab = process.env.INTENT_DUAL_TAB === '1'; +const ASSETS = process.env.INTENT_ASSETS + ? path.resolve(process.env.INTENT_ASSETS) + : path.resolve(__dirname, '../../.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab'); +const SCREENSHOTS = path.join(ASSETS, 'screenshots'); +fs.mkdirSync(SCREENSHOTS, { recursive: true }); + +const OWNER_USER = process.env.OWNER_USERNAME || process.env.ADMIN_USERNAME || ''; +const OWNER_PASS = process.env.OWNER_PASSWORD || process.env.ADMIN_PASSWORD || ''; +const PROJECT_NAME = (process.env.INTENT_PROJECT_NAME || '').trim(); + +test.skip(!runDualTab, 'set INTENT_DUAL_TAB=1 against rebuilt dev; do not use project 143'); + +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 openProject(page: Page, name: string) { + const entry = page.getByTitle(name, { exact: true }); + await expect(entry.first()).toBeVisible({ timeout: 30_000 }); + await entry.first().click(); + await expect(page.getByRole('banner').getByText(name, { exact: true })).toBeVisible({ timeout: 30_000 }); +} + +test.describe.serial('conversation intent dual-tab busy', () => { + test('tab B shows busy without reload and keeps empty actions', async ({ browser }) => { + if (!OWNER_USER || !OWNER_PASS) { + throw new Error('OWNER_USERNAME and OWNER_PASSWORD are required'); + } + if (!PROJECT_NAME) { + throw new Error('INTENT_PROJECT_NAME is required'); + } + if (/143/.test(PROJECT_NAME) || process.env.INTENT_PROJECT_ID === '143') { + throw new Error('project 143 is read-only'); + } + + const context = await browser.newContext(); + const tabA = await context.newPage(); + const posts: { status: number; body: string }[] = []; + const attachRunProbe = (page: Page) => { + page.on('response', async (res) => { + if (res.url().includes('/api/v1/tasks/runs') && res.request().method() === 'POST') { + posts.push({ status: res.status(), body: await res.text().catch(() => '') }); + } + }); + }; + attachRunProbe(tabA); + await login(tabA, OWNER_USER, OWNER_PASS); + await openProject(tabA, PROJECT_NAME); + + const tabB = await context.newPage(); + attachRunProbe(tabB); + await tabB.goto('/'); + await openProject(tabB, PROJECT_NAME); + + const busyBanner = tabB.getByText(/本文档正由【(项目助手|其他用户)】编辑/) + const busyBadge = tabB.getByText('编辑中', { exact: true }) + await expect(busyBanner.or(busyBadge).first()).toBeVisible({ timeout: 20_000 }) + await tabA.screenshot({ path: path.join(SCREENSHOTS, 'tab-a-running.png'), fullPage: true }) + await tabB.screenshot({ path: path.join(SCREENSHOTS, 'tab-b-busy.png'), fullPage: true }) + + await expect(tabB.getByRole('button', { name: /发送/ })).toBeDisabled() + await expect(tabB.getByText('DOC_BUSY')).toBeVisible() + + const box = tabB.getByPlaceholder(/向项目助手提问/) + await expect(box).toBeVisible({ timeout: 15_000 }) + await box.fill('请开始编写用户初始需求表') + const send = tabB.getByRole('button', { name: /发送/ }) + if (await send.isEnabled()) { + await send.click() + } + await tabB.waitForTimeout(2000) + await tabB.screenshot({ path: path.join(SCREENSHOTS, 'tab-b-after-write.png'), fullPage: true }) + + await expect(tabB.getByRole('button', { name: /重试|重新开始/ })).toHaveCount(0) + + fs.writeFileSync( + path.join(ASSETS, 'playwright-network.json'), + JSON.stringify({ posts, project: PROJECT_NAME }, null, 2), + ) + + await tabB.reload() + await expect(tabB.getByText(/本文档正由【(项目助手|其他用户)】编辑/).or(tabB.getByText('DOC_BUSY')).first()).toBeVisible({ timeout: 20_000 }) + await expect(tabB.getByRole('button', { name: /重试|重新开始/ })).toHaveCount(0) + await tabB.screenshot({ path: path.join(SCREENSHOTS, 'tab-b-after-reload.png'), fullPage: true }) + + await context.close(); + }); +});