// 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, """); }, }; } 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");