test_org_meta_graph_smoke.js
9.72 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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
// Org Graph unification Phase 3 (FR-5a + FR-5b) smoke test. Same no-
// framework/no-fixture convention as the other frontend/test_*_smoke.js
// files -- loads app.js in a bare vm.Context and asserts on generated HTML
// strings for the new pieces:
// - FR-5a: ORG_META_STAGE_ORDER / orgMetaStageStatus / renderOrgMetaGraph
// (the four-stage top-level meta-graph, Planner -> 复核 -> Coder ->
// 终审, separate from the existing 3-node renderOrgGraphTopology) and
// renderOrgMetaStageDetailHtml's planner_reviewer special-case (a
// verdict list, not a faked Work Graph panel).
// - FR-5b: renderLoopBackTimelineHtml (the attempt_number-ordered
// loop_back replay list, reusing run.loop_back_events which
// task_query.py already exposed but nothing rendered before this).
// Mount-time DOM wiring (mountOrgMetaGraph/mountLoopBackTimeline's click
// handlers) needs real querySelectorAll traversal this bare sandbox doesn't
// provide -- same "manual browser verification only" boundary the other
// smoke tests already draw around mount*/edge-drawing functions; this file
// only pins the pure render* HTML-string output.
//
// Run: node frontend/test_org_meta_graph_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");
// Top-level `const` stays lexically scoped to the script and never reaches
// the sandbox object (same Annex B quirk the other smoke tests already
// document for e.g. ARTIFACT_DETAIL_RE) -- re-export what this test needs.
const augmentedSrc = src + "\nglobalThis.ORG_META_STAGE_ORDER = ORG_META_STAGE_ORDER;\n";
new vm.Script(augmentedSrc, { filename: "app.js" }).runInContext(sandbox);
let assertionCount = 0;
function check(condition, message) {
assertionCount++;
assert.ok(condition, message);
}
// ---- FR-5a: ORG_META_STAGE_ORDER has all four stages, in order ----
{
check(
sandbox.ORG_META_STAGE_ORDER.join(",") === "planner,planner_reviewer,coder,validator",
"the meta-graph must cover all four stages in Planner -> 复核 -> Coder -> 终审 order"
);
}
// ---- FR-5a: orgMetaStageStatus ----
{
const emptyTask = { work_graph_runs: [], planner_review_events: [] };
check(sandbox.orgMetaStageStatus(emptyTask, "planner") === "not_started", "no run yet -> not_started");
check(sandbox.orgMetaStageStatus(emptyTask, "planner_reviewer") === "not_started", "no review events yet -> not_started");
const taskWithRuns = {
work_graph_runs: [
{ run_id: "run-1", org_nodes_involved: ["coder"], status: "running", started_at: "2026-08-01 00:00:00" },
],
planner_review_events: [],
};
check(sandbox.orgMetaStageStatus(taskWithRuns, "coder") === "running", "status mirrors the latest run's own status");
const passedReview = { work_graph_runs: [], planner_review_events: [
{ attempt: 1, verdict: "REJECTED", created_at: "t1" },
{ attempt: 2, verdict: "PASS", created_at: "t2" },
] };
check(sandbox.orgMetaStageStatus(passedReview, "planner_reviewer") === "done", "latest verdict PASS -> done");
const rejectedReview = { work_graph_runs: [], planner_review_events: [
{ attempt: 1, verdict: "REJECTED", created_at: "t1" },
] };
check(sandbox.orgMetaStageStatus(rejectedReview, "planner_reviewer") === "reflecting", "latest verdict not PASS -> reflecting, not done");
}
// ---- FR-5a: renderOrgMetaGraph -- four boxes, three arrows, real statuses ----
{
const task = {
work_graph_runs: [
{ run_id: "run-1", org_nodes_involved: ["planner"], status: "done", started_at: "t1" },
{ run_id: "run-2", org_nodes_involved: ["coder"], status: "running", started_at: "t2" },
],
planner_review_events: [{ attempt: 1, verdict: "PASS", created_at: "t1.5" }],
};
const html = sandbox.renderOrgMetaGraph(task);
for (const stage of ["planner", "planner_reviewer", "coder", "validator"]) {
check(html.includes('data-meta-stage="' + stage + '"'), "every stage box must carry data-meta-stage=" + stage);
}
check(html.includes("graph-topo-node status-done"), "planner shows its real run status (done)");
check(html.includes("graph-topo-node status-running"), "coder shows its real run status (running)");
check(html.includes("graph-topo-node status-not_started"), "validator has no run yet -> not_started");
check((html.match(/graph-edge/g) || []).length === 3, "four stages are joined by exactly three arrows");
check(html.includes('id="org-meta-detail-planner_reviewer"'), "every stage gets its own (initially hidden) expand target");
check(html.includes('style="display:none;"'), "expand targets start collapsed");
}
// ---- FR-5a: planner_reviewer special-case detail (verdict list, not a
// faked Work Graph panel) ----
{
const emptyEventsHtml = sandbox.renderOrgMetaStageDetailHtml({ planner_review_events: [] }, "planner_reviewer");
check(emptyEventsHtml.includes("尚无复核记录"), "zero review events -> explicit empty state");
check(!emptyEventsHtml.includes("work-graph"), "planner_reviewer must never render a fake DAG panel");
const task = {
planner_review_events: [
{ attempt: 1, verdict: "REJECTED", used_existing_artifacts: false, created_at: "2026-08-12 10:00:00" },
{ attempt: 2, verdict: "PASS", used_existing_artifacts: true, created_at: "2026-08-12 10:05:00" },
],
};
const html = sandbox.renderOrgMetaStageDetailHtml(task, "planner_reviewer");
check(html.includes("attempt 1"), "each attempt is listed");
check(html.includes("REJECTED") && html.includes("PASS"), "each attempt's real verdict is shown");
check(html.includes("2026-08-12 10:00:00") && html.includes("2026-08-12 10:05:00"), "each attempt's real timestamp is shown");
check(html.includes("复用已有产出"), "used_existing_artifacts=true is surfaced, not silently dropped");
}
// ---- FR-5a: planner/coder/validator detail reuses renderGraphVisualizerPanel
// unmodified, with the existing empty-run / empty-DAG fallbacks ----
{
const noRunTask = { work_graph_runs: [] };
const noRunHtml = sandbox.renderOrgMetaStageDetailHtml(noRunTask, "coder");
check(noRunHtml.includes("尚未开始"), "no run yet for this stage -> explicit empty state");
const emptyDagTask = {
work_graph_runs: [{ run_id: "run-1", org_nodes_involved: ["coder"], status: "failed", graph_manifest: null }],
};
const emptyDagHtml = sandbox.renderOrgMetaStageDetailHtml(emptyDagTask, "coder");
check(emptyDagHtml.includes("run-1") && emptyDagHtml.includes("尚未生成 Work Graph"), "run exists but has no DAG yet -> explicit fallback naming the run");
const realRunTask = {
work_graph_runs: [{
run_id: "run-2", org_nodes_involved: ["coder"], status: "running",
graph_manifest: { nodes: [{ node_id: "n1", depends_on: [] }] },
nodes: [{ node_id: "n1", node_exec_id: "exec-1", subagent_type: "coder", status: "running" }],
}],
};
const realHtml = sandbox.renderOrgMetaStageDetailHtml(realRunTask, "coder");
check(realHtml.includes("run_id=run-2"), "a real run renders the actual renderGraphVisualizerPanel DAG output");
check(!realHtml.includes("clickable"), "meta-graph expand opts out of click-to-expand-logs (enableNodeClick: false)");
}
// ---- FR-5b: renderLoopBackTimelineHtml ----
{
const noEventsHtml = sandbox.renderLoopBackTimelineHtml({ loop_back_events: [] });
check(noEventsHtml === "", "a run with no loop_back_events renders nothing, not an empty panel");
check(sandbox.renderLoopBackTimelineHtml({}) === "", "missing loop_back_events entirely is treated the same as empty");
const run = {
run_id: "run-1",
loop_back_events: [
{ source_node_id: "benchmark", target_node_id: "sdd", attempt_number: 1, feedback: ["missing scenario"], created_at: "t1" },
{ source_node_id: "benchmark", target_node_id: "sdd", attempt_number: 2, feedback: ["still missing", "and X"], created_at: "t2" },
],
};
const html = sandbox.renderLoopBackTimelineHtml(run);
check(html.includes("attempt 1") && html.includes("attempt 2"), "every attempt gets its own timeline entry, in order");
check(html.includes("benchmark") && html.includes("sdd"), "each entry names its real source/target node");
check(html.includes('data-attempt-index="0"') && html.includes('data-attempt-index="1"'), "entries are addressable by index for the click-to-reveal wiring");
check(html.includes("missing scenario"), "attempt 1's real feedback text is rendered");
check(html.includes("still missing") && html.includes("and X"), "attempt 2's real (multi-item) feedback text is rendered");
check(html.includes('style="display:none;"'), "feedback panels start collapsed -- a click reveals one at a time");
const noFeedbackRun = {
loop_back_events: [{ source_node_id: "a", target_node_id: "a", attempt_number: 1, feedback: [], created_at: "t1" }],
};
const noFeedbackHtml = sandbox.renderLoopBackTimelineHtml(noFeedbackRun);
check(noFeedbackHtml.includes("未记录 feedback"), "an attempt with empty feedback[] still renders an explicit state, not blank");
}
console.log("frontend/test_org_meta_graph_smoke.js: all " + assertionCount + " assertions passed");