// v_graph_console (`#/graph`, 2026-08-03 SDD) smoke test. Not a framework, // not a fixture -- app.js has no automated JS test precedent (SDD's own // scope note: this repo relies on manual browser verification for the // console's DOM-rendering functions), but the "leave one runnable check // behind" rule still applies to non-trivial new logic. This loads app.js // in a bare vm.Context with just enough document/window/localStorage // stubbing for escapeHtml() and the render* functions to run, then asserts // on the generated HTML strings for the new/changed functions: // - renderOrgGraphTopology (FR-1: node status classes + retry-loop edges) // - renderGraphVisualizerPanel (FR-4: enableNodeClick opt-in, and the // backward-compat case that actually matters -- v_task_detail still // calls this via `.map(renderGraphVisualizerPanel)`, which passes the // array index as a second argument; that must NOT be misread as an // options object with enableNodeClick=true) // - renderArtifactSummaryList (FR-5: type grouping/truncation/empty state, // plus 2026-08-04's click-through-to-v_artifact_browser fix: each item // now carries data-artifact-id) // - ARTIFACT_DETAIL_RE (2026-08-04: #/artifacts/:artifact_id route that // v_artifact_browser's own mountArtifactBrowser(initialArtifactId) uses // to pre-select the clicked artifact -- see render()'s route dispatch) // // Run: node frontend/test_graph_console_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"); // `function ...` declarations at top level become sandbox globals // automatically (Annex B global-object semantics), but top-level `const` // (e.g. ARTIFACT_DETAIL_RE) stays lexically scoped to the script and never // reaches the sandbox object -- explicitly re-export the one const this // test needs, in the same script/lexical scope, rather than changing app.js's // declaration style just for testability. const augmentedSrc = src + "\nglobalThis.ARTIFACT_DETAIL_RE = ARTIFACT_DETAIL_RE;\n"; new vm.Script(augmentedSrc, { filename: "app.js" }).runInContext(sandbox); // ---- computeGraphLayers: still the shared layering logic v_graph_console // leans on for FR-2/3, unchanged by this SDD but worth pinning down since // nothing else in this repo exercises it automatically ---- { const nodes = [ { node_id: "a", depends_on: [] }, { node_id: "b", depends_on: ["a"] }, { node_id: "c", depends_on: ["a"] }, { node_id: "d", depends_on: ["b", "c"] }, ]; // cross-realm arrays (sandbox's Array != this file's Array) don't // deepStrictEqual cleanly -- compare plain joined strings instead. const layers = sandbox.computeGraphLayers(nodes); assert.strictEqual(layers.length, 3, "expected 3 layers"); assert.strictEqual(Array.from(layers[0]).map((n) => n.node_id).join(","), "a"); assert.strictEqual(Array.from(layers[1]).map((n) => n.node_id).sort().join(","), "b,c"); assert.strictEqual(Array.from(layers[2]).map((n) => n.node_id).join(","), "d"); } // ---- renderOrgGraphTopology (FR-1) ---- { const html = sandbox.renderOrgGraphTopology("coder", { planner: "done", coder: "running" }, null); assert.ok(html.includes("graph-topo-node status-done"), "planner should render its run status (done)"); assert.ok(html.includes("graph-topo-node status-running active"), "coder is both active and running"); assert.ok(html.includes("graph-topo-node status-not_started"), "validator has no run yet -> not_started"); assert.ok(!html.includes("retry-loop"), "no retry edge drawn when task.route is null"); const fastLane = sandbox.renderOrgGraphTopology("coder", {}, "fast_lane"); assert.ok(fastLane.includes("validator → coder(fast_lane)"), "fast_lane draws validator->coder"); const fullReplan = sandbox.renderOrgGraphTopology("planner", {}, "full_replan"); assert.ok(fullReplan.includes("validator → planner(full_replan)"), "full_replan draws validator->planner"); } // ---- renderGraphVisualizerPanel (FR-4): options is new+optional ---- { const run = { org_node: "coder", run_id: "run-1", graph_manifest: { nodes: [{ node_id: "n1", depends_on: [] }] }, nodes: [{ node_id: "n1", node_exec_id: "exec-1", subagent_type: "coder", status: "running" }], }; // v_task_detail's real, unchanged call site: task.work_graph_runs.map(renderGraphVisualizerPanel) const legacyHtml = [run].map(sandbox.renderGraphVisualizerPanel)[0]; assert.ok(!legacyHtml.includes("clickable"), "legacy .map() call must not add the clickable class"); assert.ok(!legacyHtml.includes("data-clickable-node-exec-id"), "legacy .map() call must not add the new data attribute"); assert.ok(!legacyHtml.includes("log-viewer-slot"), "legacy .map() call must not add a log-viewer-slot"); // a hypothetical second run at .map index=1 must not have its index // misread as a truthy options object with enableNodeClick set const secondIndexHtml = sandbox.renderGraphVisualizerPanel(run, 1); assert.ok(!secondIndexHtml.includes("clickable"), "Array.map's index=1 second arg must not enable click-to-expand"); const graphConsoleHtml = sandbox.renderGraphVisualizerPanel(run, { enableNodeClick: true }); assert.ok(graphConsoleHtml.includes("work-node status-running clickable"), "opted-in call adds the clickable class"); assert.ok(graphConsoleHtml.includes('data-clickable-node-exec-id="exec-1"'), "opted-in call adds the click-target id"); assert.ok(graphConsoleHtml.includes('
'), "opted-in call reserves a log-viewer-slot"); assert.ok(graphConsoleHtml.includes('data-node-id="n1"'), "every node carries data-node-id for edge-drawing lookup"); } // ---- renderGraphVisualizerPanel: Work Graph self-planning + loop-back // (2026-08-08) -- retry_self badge and on_failure edge data. Real line // positions require getBoundingClientRect (mountGraphVisualizerEdges), // which this bare vm sandbox can't provide -- that part is manual-browser- // verified only, same convention this file's own header note already // documents for DOM-rendering functions in general. What CAN be // mechanically checked here is the HTML-string generation itself: the // badge text/count, and the data-on-failure-edges payload the real mount // function would read. { const runWithSelfLoop = { org_node: "planner", run_id: "run-2", graph_manifest: { nodes: [{ node_id: "benchmark", depends_on: [] }] }, nodes: [{ node_id: "benchmark", node_exec_id: "exec-2", subagent_type: "planner", status: "reflecting" }], loop_back_events: [ { source_node_id: "benchmark", target_node_id: "benchmark", attempt_number: 1, feedback: ["missing scenario"] }, { source_node_id: "benchmark", target_node_id: "benchmark", attempt_number: 2, feedback: ["still missing"] }, ], }; const html = sandbox.renderGraphVisualizerPanel(runWithSelfLoop); assert.ok(html.includes('class="loop-badge"'), "a node with self-loop events gets a loop-badge"); assert.ok(html.includes(">↺ 2<"), "the badge shows the real count of self-loop attempts, not just a presence flag"); // the attribute is always present (simpler than conditionally omitting // it), just empty when there's no on_failure edge to draw. assert.ok(html.includes('data-on-failure-edges="[]"'), "retry_self alone has no on_failure edges -- payload is an empty array"); const runWithoutLoop = { org_node: "planner", run_id: "run-3", graph_manifest: { nodes: [{ node_id: "brd", depends_on: [] }] }, nodes: [{ node_id: "brd", node_exec_id: "exec-3", subagent_type: "planner", status: "done" }], }; const plainHtml = sandbox.renderGraphVisualizerPanel(runWithoutLoop); assert.ok(!plainHtml.includes("loop-badge"), "a run with no loop_back_events at all (older tasks) renders no badge"); const runWithLoopBackEdge = { org_node: "planner", run_id: "run-4", graph_manifest: { nodes: [{ node_id: "sdd", depends_on: [] }, { node_id: "benchmark", depends_on: ["sdd"] }], edges: [{ from: "benchmark", to: "sdd", edge_type: "on_failure" }], }, nodes: [ { node_id: "sdd", node_exec_id: "exec-sdd", subagent_type: "planner", status: "done" }, { node_id: "benchmark", node_exec_id: "exec-bm", subagent_type: "planner", status: "running" }, ], loop_back_events: [ { source_node_id: "benchmark", target_node_id: "sdd", attempt_number: 1, feedback: ["x"] }, ], }; const loopBackHtml = sandbox.renderGraphVisualizerPanel(runWithLoopBackEdge); assert.ok(loopBackHtml.includes("data-on-failure-edges"), "a real on_failure edge emits the edges payload for mountGraphVisualizerEdges to read"); assert.ok(loopBackHtml.includes(""from":"benchmark""), "the payload's escaped JSON names the real source node"); assert.ok(loopBackHtml.includes(""triggerCount":1"), "the payload counts real triggers from loop_back_events, not just declares the edge"); assert.ok(loopBackHtml.includes("loop_back(失败回路)"), "the legend explains the on_failure line when one is present"); const noEdgeLegendHtml = sandbox.renderGraphVisualizerPanel(runWithSelfLoop); assert.ok(!noEdgeLegendHtml.includes("loop_back(失败回路)"), "no on_failure edge -- legend stays unchanged, no dangling explanation"); } // ---- countSelfLoopAttempts ---- { const run = { loop_back_events: [ { source_node_id: "a", target_node_id: "a", attempt_number: 1 }, { source_node_id: "a", target_node_id: "b", attempt_number: 1 }, // loop_back, not a self-loop -- must not count { source_node_id: "a", target_node_id: "a", attempt_number: 2 }, ], }; assert.strictEqual(sandbox.countSelfLoopAttempts(run, "a"), 2, "counts only true self-loops (source===target===nodeId)"); assert.strictEqual(sandbox.countSelfLoopAttempts(run, "b"), 0, "a node that was only ever a loop_back TARGET (not source) has zero self-loops"); assert.strictEqual(sandbox.countSelfLoopAttempts({}, "a"), 0, "missing loop_back_events (older task) is treated as zero, not a crash"); } // ---- renderArtifactSummaryList (FR-5) ---- { const artifacts = [ { artifact_id: "art-1", artifact_type: "brd", file_path: "docs/brd.md", validation_status: "passed" }, { artifact_id: "art-2", artifact_type: "code_diff", file_path: "src/x.py", validation_status: "pending" }, { artifact_id: "art-3", artifact_type: "sdd", file_path: "docs/sdd.md", validation_status: "passed" }, ]; const docsHtml = sandbox.renderArtifactSummaryList(artifacts, ["brd", "prd", "sdd", "benchmark"], "空"); assert.ok(docsHtml.includes("brd") && docsHtml.includes("sdd"), "docs list groups brd+sdd together"); assert.ok(!docsHtml.includes("code_diff"), "docs list excludes code-type artifacts"); const emptyHtml = sandbox.renderArtifactSummaryList([], ["brd"], "暂无文档类 Artifact"); assert.ok(emptyHtml.includes("暂无文档类 Artifact"), "empty group shows the given empty-state text"); // 2026-08-04 fix: each item must carry a real artifact_id so // mountArtifactsOverviewPanel can wire a click -> #/artifacts/:id // handler (see render()'s ARTIFACT_DETAIL_RE dispatch) instead of the // old bare, unclickable
  • . assert.ok(docsHtml.includes('data-artifact-id="art-1"'), "brd item carries its real artifact_id"); assert.ok(docsHtml.includes('data-artifact-id="art-3"'), "sdd item carries its real artifact_id"); } // ---- ARTIFACT_DETAIL_RE (2026-08-04 click-through fix) ---- { const match = "#/artifacts/abc-123".match(sandbox.ARTIFACT_DETAIL_RE); assert.ok(match, "#/artifacts/:id must match ARTIFACT_DETAIL_RE"); assert.strictEqual(match[1], "abc-123", "captured group must be the artifact_id"); assert.ok(!"#/artifacts".match(sandbox.ARTIFACT_DETAIL_RE), "bare #/artifacts (unfiltered browser) must not match -- stays a static route"); assert.ok(!"#/artifacts/abc/extra".match(sandbox.ARTIFACT_DETAIL_RE), "extra path segments must not match"); } console.log("frontend/test_graph_console_smoke.js: all assertions passed");