benchmark.md 12.7 KB

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,<h2>Org Graph</h2> 面板)

本次不修改 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="<planner|coder|validator>" 且计算样式包含 cursor:pointer(或对应 CSS class)。点击 coder 节点,window.location.hash 变为 #/tasks/<task_id>/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/<task_id> 页面内 <h2>Org Graph</h2> 面板下的 .org-node 同样携带 data-org-node,点击后在当前页面内滚动并高亮到该 org_node 对应的 Work Graph/日志/Artifact 区块(因为已经在 v_task_detail 里,不必再整页跳转),且 URL 同步更新为 #/tasks/<task_id>/org/<org_node>(history.replaceState/hash 更新均可,只要地址栏可复制分享)。
  • boundary:activeSet(当前高亮的 org_node 集合,见 renderDetail() 现有逻辑)与被点击的 org_node 不一致时(例如任务已进入 coder 阶段,但用户点击已完成的 planner),仍应正确定位到 planner 最新一次 run 的内容,不能只能查看"当前 active"的节点。
  • failure:直接用带 org_node 的 URL 深链打开(例如浏览器地址栏直接输入 #/tasks/<task_id>/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/<id>/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 全量数据,本次不需要改动)