## Benchmark:Org Graph 三节点条点击可达详情 ### 0. 文档定位与覆盖范围 本 Benchmark 对应 SDD「Org Graph 三节点条点击可达详情」,覆盖 `frontend/app.js` 中三处 Org Graph 渲染点里的**两处**(按 SDD 范围裁剪,第三处显式列为跟进项,见 §5): 1. `renderOrgStrip()`(`v_dashboard`,挂载于 `mountDashboard`,`id="org_strip"`) 2. `renderDetail()` 内联的 org-strip(`v_task_detail`,`

Org Graph

` 面板) 本次**不**修改 `renderOrgGraphTopology()`(`#/graph`,`v_graph_console`)——它是独立实现(`.graph-topo-node`,非 `.org-strip`/`.org-node`),保留其当前"三节点不可点、仅内部 Work Graph 子节点可点"的行为不算回归,只要不引入新的不一致即可。 本任务在 `docs/sdd/06-验收与测试锚点.md` 的 REQ-01~15 体系之外——它是一个纯前端可用性/接线修复,不改变任何 M/C/P 层行为,不新增后端接口(沿用 `GET /api/tasks/:task_id` 已返回的 `work_graph_runs[].{org_node,run_id,status,nodes}` 与 `artifacts[].{produced_by_run_id,...}`)。凡是新增后端路由、新增数据库字段、或复制/重写 DAG/日志/Artifact 三块已有渲染逻辑的实现,均视为**不满足验收**(违反 SDD"接线不重建"的硬约束)。 --- ### 1. 前置事实与实现约束核对(每条 PASS/FAIL 都必须先过,再谈场景测试) | # | 约束 | 核对方式 | 判据 | |---|---|---|---| | C1 | 未新增后端 HTTP 路由 | `git diff` 对比 `src/backend_server.py` 路由表 | diff 为空或仅有注释改动 | | C2 | 未新增/修改数据库 schema | `git diff src/schema.py` | diff 为空 | | C3 | DAG 展示复用 `renderGraphVisualizerPanel` | 读 `frontend/app.js` diff | 新代码调用 `renderGraphVisualizerPanel(run, {...})`,而非重写一份 DAG 渲染函数 | | C4 | 日志展示复用既有 `v_log_viewer` 机制 | 读 diff | 新代码调用 `renderLogViewerHtml`/`mountLogViewer`(`c_ws_log_tail` 实时 + `c_execution_log_get` 静态兜底),未新建日志拉取逻辑 | | C5 | Artifact 展示复用既有列表/`v_artifact_browser` | 读 diff | 新代码渲染/链接到既有 artifact 列表或 `#/artifacts/:artifact_id`,未重写 artifact 内容渲染 | | C6 | "最新一次 work_graph_run" 用纯前端过滤实现 | 读 diff | 逻辑为 `runs.filter(r => r.org_node === node).pop()`(或等价实现),不新增查询参数 | 任一条 FAIL,直接判定本次交付未达标,退回,不进入下面的行为场景测试。 --- ### 2. 路由与可点击性契约(Happy / Boundary / Failure) **契约**:新增哈希路由 `#/tasks/:task_id/org/:org_node`,`:org_node ∈ {planner, coder, validator}`。点击任一 org-strip 节点应导航到(或等效展开)该组合视图;`v_task_detail` 现有渲染逻辑被复用,而非另起一套页面骨架。 #### 2.1 v_dashboard 的 `renderOrgStrip()` - happy:任务处于 `coding` 态、`assigned_org_node="coder"`,dashboard 渲染后,`#org_strip` 内每个 `.org-node` 携带 `data-org-node=""` 且计算样式包含 `cursor:pointer`(或对应 CSS class)。点击 `coder` 节点,`window.location.hash` 变为 `#/tasks//org/coder`。 - boundary:任务刚 `queued`,`work_graph_runs` 为空(三个节点都还没跑过)。三个节点仍各自可点,点击后组合视图能正常渲染"暂无 Work Graph / 暂无日志 / 暂无 Artifact"空态,不抛异常、不出现空白页。 - failure:dashboard 尚未完成首次 `refreshTasksAndOrgGraph()`(`orgStrip.innerHTML` 还是初始占位)时快速点击——点击处理器必须挂在渲染出的节点元素上(事件委托或渲染后绑定均可),不得因为绑定时机早于渲染而导致点击无响应;用"渲染完成后立即模拟 click 事件"验证监听器已生效。 #### 2.2 v_task_detail 内联 org-strip - happy:`#/tasks/` 页面内 `

Org Graph

` 面板下的 `.org-node` 同样携带 `data-org-node`,点击后**在当前页面内**滚动并高亮到该 org_node 对应的 Work Graph/日志/Artifact 区块(因为已经在 `v_task_detail` 里,不必再整页跳转),且 URL 同步更新为 `#/tasks//org/`(`history.replaceState`/hash 更新均可,只要地址栏可复制分享)。 - boundary:`activeSet`(当前高亮的 org_node 集合,见 `renderDetail()` 现有逻辑)与被点击的 org_node 不一致时(例如任务已进入 `coder` 阶段,但用户点击已完成的 `planner`),仍应正确定位到 `planner` 最新一次 run 的内容,不能只能查看"当前 active"的节点。 - failure:直接用带 org_node 的 URL 深链打开(例如浏览器地址栏直接输入 `#/tasks//org/validator` 后回车,未经过点击),页面必须能独立完成初始渲染并定位到 validator 区块——即高亮/滚动逻辑不能只挂在 click handler 里,必须在 `mount` 阶段读取路由参数后统一触发一次("共享 scroll/highlight 逻辑"的核心要求)。 #### 2.3 路由正则/派发(对齐现有 `TASK_DETAIL_RE`/`ARTIFACT_DETAIL_RE` 风格) - happy:新增正则(例如 `ORG_NODE_DETAIL_RE = /^#\/tasks\/([^/]+)\/org\/(planner|coder|validator)$/`)能正确捕获 `task_id` 与 `org_node` 两个分组。 - boundary:`org_node` 段不在白名单内(例如 `#/tasks/abc/org/reviewer`)不应匹配该正则,应落回 `render()` 现有的"Not found"兜底或原样按 `#/tasks/:task_id` 处理,不得让非法值穿透到后续的 DAG/日志/Artifact 过滤逻辑导致 `undefined` 报错。 - failure:`task_id` 段包含需要转义的字符(如 URL 编码过的 UUID 本身不含特殊字符,可用一个刻意构造的非法 `task_id` 如 `"' OR 1=1"` 编码后传入)时,前端不应据此拼出未转义的字符串插入 DOM(沿用现有 `escapeHtml`),且 `fetch("/api/tasks/" + encodeURIComponent(taskId))` 对不存在的 task_id 应走 404 分支而不是让页面白屏。 --- ### 3. 组合视图内容正确性(DAG + 日志 + Artifact 三块拼装) **契约**:"最新一次相关 work_graph_run" = `task.work_graph_runs.filter(r => r.org_node === orgNode)` 取最后一条(数组按 `started_at` ASC 排序,因此 `.pop()`/取末项即最新)。 - happy(单次 run,已完成):`org_node="planner"` 只跑过一次且 `status="done"`。组合视图应包含:(a) 该 run 的 DAG 面板(`renderGraphVisualizerPanel` 产出的 HTML,包含 `run.run_id`),(b) 该 run 下每个 `work_node_execution` 对应的日志入口,点开走**静态**拉取(`c_execution_log_get`,因为节点已终态),(c) `produced_by_run_id === run.run_id` 的 artifact 列表项,可点击链接到 `#/artifacts/:artifact_id`。 - happy(单次 run,进行中):`org_node="coder"` 当前 `status="running"`。日志入口应走**实时 WS tail**模式(`c_ws_log_tail`),而不是静态一次性拉取——用 mock WebSocket 断言建立了连接且 `onmessage` 会追加内容到日志容器。 - boundary(多次 run / 重试循环):`org_node="planner"` 因 `full_replan` 被重新触发过,`work_graph_runs` 中存在两条 `org_node="planner"` 的记录(不同 `run_id`)。组合视图必须只展示**最后一条**(时间上最新)的 DAG/日志/Artifact,不能把两次 run 的节点混在同一个 DAG 里,也不能默认展示第一条。 - boundary(该 org_node 尚无 run):`org_node="validator"` 在 `work_graph_runs` 里完全没有记录(任务还在 `coding` 阶段)。组合视图应展示明确的空态提示(例如"validator 尚未开始"),DAG/日志/Artifact 三块均不渲染成"undefined"或抛异常。 - failure(该 org_node 有 run 但零 artifact 产出):`org_node="coder"` 有一条 `status="failed"` 的 run,且尚未产出任何 artifact(例如首次尝试就失败)。Artifact 区块应显示"暂无 Artifact"而不是空白或报错,DAG/日志区块仍应正常展示失败前的执行状态(`work_node_execution.status` 里可能存在 `failed`/`reflecting` 节点,沿用 `renderGraphVisualizerPanel` 现有的 status class 展示,无需新逻辑)。 --- ### 4. 生命周期/清理(复用现有 `registerRouteCleanup`/`closeAllLogViewers` 约定) - happy:从组合视图(`#/tasks//org/coder`,日志走实时 WS)导航离开到 `#/tasks`,`registerRouteCleanup` 注册的清理函数应被调用,对应的 `c_ws_log_tail` WebSocket 连接被关闭(mock socket 的 `close()` 被调用一次)。 - boundary:同一页面内先点开 `planner` 的日志(假设也是运行中态),再点 `coder`(不离开 `v_task_detail` 页面)——旧的 WS 连接应关闭后再开新的,不允许同时保留两个未关闭的实时日志连接(沿用 `v_log_viewer` 现有的"懒加载 + 单例"约定,不新造一套)。 - failure:WS 连接建立后立即触发 `hashchange`(用户点击过快,两次路由切换之间日志面板还没渲染完)——不应抛出"Cannot read property of undefined"一类异常;用"渲染函数返回前先 mock 一次路由切换"来复现这个竞态并断言无未捕获异常。 --- ### 5. 显式跟进项(本次不验收,但必须在交付说明中写明) - `#/graph`(`v_graph_console`)里 `renderOrgGraphTopology()` 渲染的三个拓扑节点(`.graph-topo-node`)仍然不可点——这是已知、有意保留的范围外项,Benchmark 不对其设置 PASS 条件,但要求交付说明里显式列出"未处理",不能被误读为"顺带修好了"或被静默忽略。若实现顺手把它也改了,允许,但不能因为改了这一处而让 §1 的 C3/C4/C5 复用契约破功(例如给拓扑节点造了第 4 套渲染逻辑)。 --- ### 6. 自动化回归("留一个可运行检查") 沿用 `frontend/test_graph_console_smoke.js` 的既有约定(Node `vm` 沙箱跑 `app.js`,对生成的 HTML 字符串做断言,无框架无夹具),新增/扩展同风格的 smoke test(可以是新文件 `frontend/test_org_click_through_smoke.js`,或扩展现有文件——由实现者按现有文件职责边界判断),至少覆盖: 1. `renderOrgStrip`/`renderDetail` 内联 org-strip 生成的 HTML 包含 `data-org-node` 属性(每个节点一个,值 ∈ `{planner, coder, validator}`)。 2. 新增的路由正则(`ORG_NODE_DETAIL_RE` 或等价物)能正确匹配/拒绝 §2.3 列出的 happy/boundary 用例(合法三态、非法节点名、缺段)。 3. "取最新 run"的过滤函数:给定包含两条同 `org_node` 记录的 `work_graph_runs` 数组,断言返回的是 `started_at` 更晚(或数组末位)的那条,而不是第一条。 判据:`node frontend/test_org_click_through_smoke.js`(或扩展后的既有文件)以 0 退出码结束,且新增断言数 ≥ 6(对应上面 3 类 × 至少 2 个用例)。 --- ### 7. Definition of Done(本 Benchmark 的整体判据) 全部满足才算通过: 1. §1 六条实现约束全部 PASS(未重建已有机制、未新增后端面)。 2. §2 路由与可点击性的 happy/boundary/failure 用例全部可在浏览器手工验证(本仓库前端无构建步骤、依赖手工浏览器验证是既有约定)中复现预期行为。 3. §3 组合视图内容正确性的 5 个场景(2 happy + 2 boundary + 1 failure)全部符合"最新 run 过滤 + 三块内容拼装正确"的判据。 4. §4 生命周期清理的 3 个场景不出现 WS 连接泄漏或未捕获异常。 5. §5 的范围外项在交付说明中被显式提及,未被静默默认为"已修复"。 6. §6 自动化 smoke test 新增并以 0 退出码通过。 --- ### 附:关键代码定位(供 Coder 节点核对,不代表本次要修改的完整清单) - `frontend/app.js:589-598` `renderOrgStrip()`(v_dashboard,待加 `data-org-node`/点击) - `frontend/app.js:1085-1088` `renderDetail()` 内联 org-strip(v_task_detail,待加同样接线) - `frontend/app.js:855-` `renderGraphVisualizerPanel(run, options)`(DAG 面板,`{enableNodeClick:true}` 可选项已存在,直接复用) - `frontend/app.js:942-1016` `v_log_viewer`(`renderLogViewerHtml`/`mountLogViewer`,WS 实时 + 静态兜底已验证生效) - `frontend/app.js:1571-` `v_artifact_browser`(`c_artifact_get`,单 artifact 完整内容展示) - `frontend/app.js:97-99` 现有 `TASK_DETAIL_RE`/`SKILL_AUDIT_RE`/`ARTIFACT_DETAIL_RE` 路由正则风格,新增正则应沿用同一模式并加入 `render()` 的分派链(app.js:106-140) - `frontend/app.js:1322-1347` `renderOrgGraphTopology()`(`#/graph`,本次范围外,仅供对照,不要复制其实现风格到 org-strip 的两处) - `src/task_query.py:115-171` `get_task()`(已返回 `work_graph_runs`/`artifacts` 全量数据,本次不需要改动)