test_org_click_through_smoke.js 6.89 KB
// Org Graph 三节点条点击可达详情 (2026-08-06 SDD) smoke test. Same
// no-framework/no-fixture convention as frontend/test_graph_console_smoke.js
// -- loads app.js in a bare vm.Context and asserts on generated HTML
// strings / regex matches / plain data for the new top-level functions:
//   - renderDashboardOrgStripHtml / renderTaskDetailOrgStripHtml: both
//     org-strip render locations now carry data-org-node on every node
//     (root cause from the BRD: renderOrgStrip() had zero data-* attrs).
//   - ORG_NODE_DETAIL_RE: the new #/tasks/:task_id/org/:org_node route --
//     legal three-way org_node enum, illegal node name, missing segment.
//   - getLatestRunForOrgNode: "取最新 run" filter -- must return the
//     started_at-latest (array-last) entry for a given org_node, not the
//     first, and must not merge two runs' nodes together.
//   - renderOrgComboPanelContent: empty-state (no run yet) and populated
//     (with/without artifacts) cases, always via the existing simple-list
//     markup pattern -- no separate artifact-content renderer invented here.
//
// Run: node frontend/test_org_click_through_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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
    },
  };
}

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 test_graph_console_smoke.js
// already documents for ARTIFACT_DETAIL_RE) -- re-export what this test needs.
const augmentedSrc = src + "\nglobalThis.ORG_NODE_DETAIL_RE = ORG_NODE_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. data-org-node on both org-strip render locations ----
{
  const dashboardHtml = sandbox.renderDashboardOrgStripHtml("coder", { planner: "done", coder: "running" });
  for (const node of ["planner", "coder", "validator"]) {
    check(dashboardHtml.includes('data-org-node="' + node + '"'), "dashboard org-strip must carry data-org-node for " + node);
  }
  check(dashboardHtml.includes('class="org-node active" data-org-node="coder"'), "active node keeps its existing .active class alongside the new attribute");

  const activeSet = new Set(["planner"]);
  const taskDetailHtml = sandbox.renderTaskDetailOrgStripHtml(activeSet);
  for (const node of ["planner", "coder", "validator"]) {
    check(taskDetailHtml.includes('data-org-node="' + node + '"'), "v_task_detail inline org-strip must carry data-org-node for " + node);
  }
}

// ---- 2. ORG_NODE_DETAIL_RE: happy / boundary / failure ----
{
  const legal = "#/tasks/abc-123/org/coder".match(sandbox.ORG_NODE_DETAIL_RE);
  check(legal, "legal task_id + coder must match");
  check(legal[1] === "abc-123" && legal[2] === "coder", "captured groups must be task_id and org_node");

  for (const node of ["planner", "coder", "validator"]) {
    check(("#/tasks/t1/org/" + node).match(sandbox.ORG_NODE_DETAIL_RE), "all three org_node enum values must match: " + node);
  }

  check(!"#/tasks/t1/org/reviewer".match(sandbox.ORG_NODE_DETAIL_RE), "illegal org_node (not in the 3-way enum) must not match");
  check(!"#/tasks/t1/org/".match(sandbox.ORG_NODE_DETAIL_RE), "missing org_node segment must not match");
  check(!"#/tasks/t1".match(sandbox.ORG_NODE_DETAIL_RE), "plain #/tasks/:id (no /org/ suffix) must not match this regex");
}

// ---- 3. getLatestRunForOrgNode: latest-wins, no cross-run merging ----
{
  const task = {
    work_graph_runs: [
      { run_id: "run-old", org_nodes_involved: ["planner"], status: "done", started_at: "2026-08-01 00:00:00" },
      { run_id: "run-new", org_nodes_involved: ["planner"], status: "done", started_at: "2026-08-02 00:00:00" },
      { run_id: "run-coder", org_nodes_involved: ["coder"], status: "running", started_at: "2026-08-02 01:00:00" },
    ],
  };
  const latestPlanner = sandbox.getLatestRunForOrgNode(task, "planner");
  check(latestPlanner.run_id === "run-new", "must return the array-last (time-latest) run for a repeated org_node, not the first");

  const latestCoder = sandbox.getLatestRunForOrgNode(task, "coder");
  check(latestCoder.run_id === "run-coder", "single-run org_node resolves to that one run");

  const noValidator = sandbox.getLatestRunForOrgNode(task, "validator");
  check(noValidator === null, "org_node with zero runs must resolve to null, not throw or return undefined-shaped data");
}

// ---- 4. renderOrgComboPanelContent: empty state + artifact filtering ----
{
  const emptyTask = { work_graph_runs: [], artifacts: [] };
  const emptyHtml = sandbox.renderOrgComboPanelContent(emptyTask, "validator");
  check(emptyHtml.includes("尚未开始"), "no-run org_node must render an explicit empty state, not blank/undefined");

  const taskWithRun = {
    work_graph_runs: [
      { run_id: "run-1", org_nodes_involved: ["coder"], status: "done", graph_manifest: { nodes: [{ node_id: "n1", depends_on: [] }] } },
    ],
    artifacts: [
      { artifact_id: "art-1", produced_by_run_id: "run-1", file_path: "src/x.py", artifact_type: "code_diff", validation_status: "passed" },
      { artifact_id: "art-2", produced_by_run_id: "run-other", file_path: "src/y.py", artifact_type: "code_diff", validation_status: "passed" },
    ],
  };
  const populatedHtml = sandbox.renderOrgComboPanelContent(taskWithRun, "coder");
  check(populatedHtml.includes("run-1"), "combo content must name the resolved run_id");
  check(populatedHtml.includes('data-artifact-id="art-1"'), "artifact produced by the resolved run must be listed");
  check(!populatedHtml.includes('data-artifact-id="art-2"'), "artifact from a different run_id must be filtered out");

  const taskNoArtifacts = {
    work_graph_runs: [{ run_id: "run-2", org_nodes_involved: ["coder"], status: "failed", graph_manifest: null }],
    artifacts: [],
  };
  const noArtifactsHtml = sandbox.renderOrgComboPanelContent(taskNoArtifacts, "coder");
  check(noArtifactsHtml.includes("暂无 Artifact"), "run with zero artifacts must show the existing empty-state text, not blank/error");
}

console.log(
  "frontend/test_org_click_through_smoke.js: all " + assertionCount + " assertions passed"
);