test_graph_console_smoke.js
6.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// 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, ">").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('<div class="log-viewer-slot">'), "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");