Files
Claude-Code-Monitor/server/__tests__/workflow-api.test.js
T
nntrivi2001 57dc91585d feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
2026-07-30 14:39:03 +07:00

197 lines
6.2 KiB
JavaScript

/**
* @file Integration test for the Workflow-tool run feature (issue #167): a Stop
* hook with a transcript_path triggers on-disk journal ingestion off the
* response path, and the run then surfaces via GET /api/workflows/runs,
* GET /api/workflows/runs/:runId, and the session-detail `workflows[]` field.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, before, after } = require("node:test");
const assert = require("node:assert/strict");
const fs = require("fs");
const os = require("os");
const path = require("path");
const http = require("http");
const TEST_DB = path.join(os.tmpdir(), `dashboard-wfapi-test-${Date.now()}-${process.pid}.db`);
process.env.DASHBOARD_DB_PATH = TEST_DB;
const { createApp, startServer } = require("../index");
const { db } = require("../db");
let server;
let BASE;
let ROOT;
const SESSION_ID = "wfapi-sess-1";
function fetchJson(urlPath, options = {}) {
return new Promise((resolve, reject) => {
const url = new URL(urlPath, BASE);
const req = http.request(
{
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
method: options.method || "GET",
headers: { "Content-Type": "application/json", ...options.headers },
},
(res) => {
let body = "";
res.on("data", (c) => (body += c));
res.on("end", () => {
try {
resolve({ status: res.statusCode, body: body ? JSON.parse(body) : null });
} catch {
resolve({ status: res.statusCode, body });
}
});
}
);
req.on("error", reject);
if (options.body) req.write(JSON.stringify(options.body));
req.end();
});
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
before(async () => {
ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "wfapi-fixture-"));
const transcriptPath = path.join(ROOT, `${SESSION_ID}.jsonl`);
fs.writeFileSync(transcriptPath, "");
// On-disk run journal next to the transcript.
const wfDir = path.join(ROOT, SESSION_ID, "workflows");
fs.mkdirSync(wfDir, { recursive: true });
fs.writeFileSync(
path.join(wfDir, "wf_apitest1.json"),
JSON.stringify({
runId: "wf_apitest1",
workflowName: "api-review",
status: "completed",
startTime: 1700001000000,
durationMs: 4000,
defaultModel: "claude-opus-4-8",
agentCount: 1,
totalTokens: 999,
totalToolCalls: 2,
phases: [{ title: "Scan" }],
workflowProgress: [
{ type: "workflow_phase", index: 1, title: "Scan" },
{
type: "workflow_agent",
index: 1,
agentId: "x1",
state: "done",
phaseTitle: "Scan",
label: "scan:repo",
tokens: 999,
toolCalls: 2,
},
],
})
);
const app = createApp();
server = await startServer(app, 0);
BASE = `http://127.0.0.1:${server.address().port}`;
// Drive a Stop hook with the transcript_path — this is what triggers the
// post-response workflow ingest in the hooks router.
await fetchJson("/api/hooks/event", {
method: "POST",
body: {
hook_type: "Stop",
data: {
session_id: SESSION_ID,
cwd: "/tmp/proj",
transcript_path: transcriptPath,
},
},
});
// Ingest is fire-and-forget after res.json; poll until it lands.
for (let i = 0; i < 40; i++) {
const r = await fetchJson(`/api/workflows/runs?session_id=${SESSION_ID}`);
if (r.body && r.body.runs && r.body.runs.length > 0) break;
await sleep(50);
}
});
after(() => {
server?.close();
try {
db.close();
} catch {
/* ignore */
}
try {
fs.rmSync(ROOT, { recursive: true, force: true });
} catch {
/* ignore */
}
try {
fs.rmSync(TEST_DB, { force: true });
} catch {
/* ignore */
}
});
describe("GET /api/workflows/runs", () => {
it("lists the ingested run with hydrated phases/progress arrays", async () => {
const r = await fetchJson(`/api/workflows/runs?session_id=${SESSION_ID}`);
assert.equal(r.status, 200);
assert.ok(Array.isArray(r.body.runs));
const run = r.body.runs.find((x) => x.run_id === "wf_apitest1");
assert.ok(run, "run present");
assert.equal(run.name, "api-review");
assert.equal(run.status, "completed");
assert.equal(run.total_tokens, 999);
assert.ok(Array.isArray(run.phases) && run.phases.length === 1);
assert.ok(Array.isArray(run.progress) && run.progress.length === 2);
assert.equal(run.progress.filter((p) => p.type === "workflow_agent").length, 1);
assert.equal(typeof r.body.total, "number");
assert.ok(r.body.counts && typeof r.body.counts === "object");
assert.ok(r.body.counts.completed >= 1, "status counts include completed");
});
it("filters by status", async () => {
const completed = await fetchJson("/api/workflows/runs?status=completed");
assert.equal(completed.status, 200);
assert.ok(completed.body.runs.some((x) => x.run_id === "wf_apitest1"));
const running = await fetchJson("/api/workflows/runs?status=running");
assert.equal(running.status, 200);
assert.ok(
!running.body.runs.some((x) => x.run_id === "wf_apitest1"),
"completed run excluded from the running filter"
);
});
});
describe("GET /api/workflows/runs/:runId", () => {
it("returns the run with its linked inner agents", async () => {
const r = await fetchJson("/api/workflows/runs/wf_apitest1");
assert.equal(r.status, 200);
assert.equal(r.body.workflow.run_id, "wf_apitest1");
assert.ok(Array.isArray(r.body.agents));
const linked = r.body.agents.find((a) => a.id === `${SESSION_ID}-jsonl-x1`);
assert.ok(linked, "inner agent linked");
assert.equal(linked.workflow_run_id, "wf_apitest1");
assert.equal(linked.workflow_phase, "Scan");
});
it("404s an unknown run id", async () => {
const r = await fetchJson("/api/workflows/runs/wf_nope");
assert.equal(r.status, 404);
});
});
describe("GET /api/sessions/:id includes workflows[]", () => {
it("surfaces the run on the parent session detail", async () => {
const r = await fetchJson(`/api/sessions/${SESSION_ID}`);
assert.equal(r.status, 200);
assert.ok(Array.isArray(r.body.workflows));
assert.ok(r.body.workflows.some((w) => w.run_id === "wf_apitest1"));
});
});