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.
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* @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"));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user