test_run_detail_smoke.js
5.34 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
// 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-<node_exec_id> DOM-id contract mountLogViewer relies on
// (`#todo-detail-<id> .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, ">").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-<id> 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> 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: <br>"), "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 <li> 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 = '<li data-run-id="' + r.run_id + '" style="cursor:pointer;"><span>' + escapeHtml(r.org_node) + " · run "
+ r.run_id.slice(0, 8) + "</span><span>" + escapeHtml(r.status) + " · " + r.node_count + " 节点</span></li>";
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");