// 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"; 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"); } console.log("frontend/test_run_detail_smoke.js: all " + assertionCount + " assertions passed");