// 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) // // 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"); new vm.Script(src, { 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"); } // ---- renderArtifactSummaryList (FR-5) ---- { const artifacts = [ { artifact_type: "brd", file_path: "docs/brd.md", validation_status: "passed" }, { artifact_type: "code_diff", file_path: "src/x.py", validation_status: "pending" }, { 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"); } console.log("frontend/test_graph_console_smoke.js: all assertions passed");