// v_run_detail (`#/runs/:run_id`, U-046 2026-08-06 SDD) smoke test. Same // no-framework/no-fixture convention as frontend/test_graph_console_smoke.js // and frontend/test_org_click_through_smoke.js (see // .claude/agent-memory/validator/feedback_no_js_test_framework.md) -- loads // app.js in a bare vm.Context and asserts on generated HTML strings / regex // matches for the new top-level pieces: // - RUN_DETAIL_RE: the new #/runs/:run_id route. // - renderRunNodeListHtml: the run-detail node list, including the // todo-detail- DOM-id contract mountLogViewer relies on // (`#todo-detail- .log-autoscroll`) and the empty-run case. // - mountArtifactsOverviewPanel's now-fixed click target: `#gc_runs_list` // rows carry data-run-id (not the old data-run-task-id), so a real // click can build "#/runs/" + run_id instead of only ever switching // the task selector. // // Run: node frontend/test_run_detail_smoke.js "use strict"; const fs = require("fs"); const path = require("path"); const vm = require("vm"); const assert = require("assert"); function makeFakeDiv() { let text = ""; return { set textContent(v) { text = v == null ? "" : String(v); }, get textContent() { return text; }, get innerHTML() { return text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); }, }; } const sandbox = { document: { createElement: () => makeFakeDiv(), getElementById: () => null, querySelectorAll: () => [], addEventListener: () => {}, }, window: { location: { protocol: "http:", host: "localhost", hash: "" }, addEventListener: () => {}, }, localStorage: { getItem: () => null, setItem: () => {} }, console, }; vm.createContext(sandbox); const src = fs.readFileSync(path.join(__dirname, "app.js"), "utf8"); // Top-level `const` regexes stay lexically scoped to the script and never // reach the sandbox object (same Annex B quirk the other smoke tests // already document) -- re-export what this test needs. const augmentedSrc = src + "\nglobalThis.RUN_DETAIL_RE = RUN_DETAIL_RE;\n"; // (renderRunNodeListHtml/renderForkBadgeHtml/mountForkPanels are already // top-level function declarations, so they land in the sandbox globalThis // directly -- no extra re-export needed for those, unlike the const regex.) new vm.Script(augmentedSrc, { filename: "app.js" }).runInContext(sandbox); let assertionCount = 0; function check(condition, message) { assertionCount++; assert.ok(condition, message); } // ---- 1. RUN_DETAIL_RE: happy / boundary / failure ---- { const legal = "#/runs/run-abc-123".match(sandbox.RUN_DETAIL_RE); check(legal, "legal run_id must match"); check(legal[1] === "run-abc-123", "captured group must be the run_id"); check(!"#/runs/".match(sandbox.RUN_DETAIL_RE), "missing run_id segment must not match"); check(!"#/runs/abc/extra".match(sandbox.RUN_DETAIL_RE), "extra path segments must not match"); check(!"#/tasks/abc".match(sandbox.RUN_DETAIL_RE), "an unrelated route must not match"); } // ---- 2. renderRunNodeListHtml: node list + todo-detail- DOM contract ---- { const run = { run_id: "run-1", org_node: "coder", status: "running", nodes: [ { node_exec_id: "exec-1", node_id: "n1", subagent_type: "coder", status: "running", retry_count: 0, reflection_notes: null, }, { node_exec_id: "exec-2", node_id: "n2", subagent_type: "coder", status: "done", retry_count: 1, reflection_notes: "retried once after a transient error", }, ], }; const html = sandbox.renderRunNodeListHtml(run); check(html.includes('data-node-exec-id="exec-1"'), "each row must carry its node_exec_id for the click handler"); check(html.includes('id="todo-detail-exec-1"'), "must reproduce the todo-detail- id mountLogViewer's querySelector needs"); check(html.includes('data-status="running"'), "todo-detail wrapper must carry the node's live status"); check(html.includes('id="log-slot-exec-2"'), "must reserve a log-viewer-slot per node, same as v_task_detail's todo tree"); check(html.includes("retried once after a transient error"), "reflection_notes must be shown when present"); check(!html.includes("reflection_notes:
"), "no stray reflection_notes line when it's null (exec-1)"); const emptyHtml = sandbox.renderRunNodeListHtml({ nodes: [] }); check(emptyHtml.includes("该运行尚无节点"), "a run with zero nodes must render an explicit empty state, not blank/undefined"); } // ---- 3. gc_runs_list rows: run-detail click target (2026-08-06 fix) ---- { // mountArtifactsOverviewPanel itself does an async fetch and can't run in // this sandbox (no `fetch`), but the row-id contract it depends on is a // pure string-building concern worth pinning directly: replicate the // exact
  • markup mountArtifactsOverviewPanel emits for one run and // confirm it carries data-run-id (not the removed data-run-task-id). const escapeHtml = sandbox.escapeHtml; const r = { org_node: "coder", run_id: "run-1234567890", status: "done", node_count: 3 }; const li = '
  • ' + escapeHtml(r.org_node) + " · run " + r.run_id.slice(0, 8) + "" + escapeHtml(r.status) + " · " + r.node_count + " 节点
  • "; check(li.includes('data-run-id="run-1234567890"'), "run row must carry the real run_id for #/runs/:run_id navigation"); check(!li.includes("data-run-task-id"), "the old task-switcher click target must be gone"); } // ---- 4. D (2026-08-06, time-travel debugging): fork panel only on 'done' nodes ---- { const run = { run_id: "run-1", org_node: "planner", status: "running", nodes: [ { node_exec_id: "exec-running", node_id: "prd", subagent_type: "planner", status: "running", retry_count: 0 }, { node_exec_id: "exec-done", node_id: "sdd", subagent_type: "planner", status: "done", retry_count: 0 }, ], }; const html = sandbox.renderRunNodeListHtml(run); const doneDetail = html.slice(html.indexOf('id="todo-detail-exec-done"')); const runningDetail = html.slice( html.indexOf('id="todo-detail-exec-running"'), html.indexOf('id="todo-detail-exec-done"') ); check(doneDetail.includes("btn-fork"), "a 'done' node must offer a fork trigger"); check(doneDetail.includes("fork-content"), "the fork form's textarea must be present for a 'done' node"); check(!runningDetail.includes("btn-fork"), "a non-'done' node must not offer a fork trigger (source must be 'done', FR-1)"); } // ---- 5. D: renderForkBadgeHtml -- only shown for an actual fork ---- { const original = { run_id: "run-1", parent_run_id: null, fork_of_node_exec_id: null }; check(sandbox.renderForkBadgeHtml(original) === "", "a normal (non-forked) run must render no badge"); const forked = { run_id: "run-2", parent_run_id: "run-1", fork_of_node_exec_id: "run-1-prd" }; const badge = sandbox.renderForkBadgeHtml(forked); check(badge.includes("run-1-prd"), "badge must name which node was forked from"); check(badge.includes('href="#/runs/run-1"'), "badge must link back to the parent run"); } console.log("frontend/test_run_detail_smoke.js: all " + assertionCount + " assertions passed");