Files
Claude-Code-Monitor/server/__tests__/import-workflow-link.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

209 lines
7.2 KiB
JavaScript

/**
* @file Regression: the offline CLI import path must link Workflow-tool inner
* agents to their run.
*
* A Claude Code Workflow-tool run (dynamic workflow / fleet of sub-agents)
* emits NO hooks, so in a headless `claude -p` run, a CI job, or an HPC/cluster
* compute node its per-run journal is never ingested live. Before this fix the
* CLI import path (`ccam import rescan` → importAllSessions, `ccam import path`
* → importFromDirectory) never called the workflow-journal ingest, so the
* nested inner-agent transcripts under
* `<sid>/subagents/workflows/<runId>/agent-*.jsonl` were imported with
* `workflow_run_id = NULL` — orphaned from their run — leaving the workflow
* stuck showing 1 agent instead of N.
*
* These tests build a fixture session tree with a streaming
* `subagents/workflows/<runId>/journal.jsonl` plus a few `agent-*.jsonl`, run
* the CLI import path (never the server), and assert every inner agent ends up
* with `workflow_run_id = <runId>` and the workflows row reports the full fleet.
*
* @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");
// Scope the importer at a throwaway CLAUDE_HOME and DB before any server module
// loads — import-history captures PROJECTS_DIR from CLAUDE_HOME at require time.
const TMP_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), `ccam-wf-link-${process.pid}-`));
process.env.CLAUDE_HOME = TMP_ROOT;
process.env.DASHBOARD_DATA_DIR = path.join(TMP_ROOT, "data");
process.env.DASHBOARD_DB_PATH = path.join(TMP_ROOT, "dashboard.db");
const dbModule = require("../db");
const { importAllSessions, importFromDirectory } = require("../../scripts/import-history");
const PROJECTS_DIR = path.join(TMP_ROOT, "projects");
/** A minimal session transcript with one timestamped assistant turn. */
function sessionJsonl(cwd) {
return [
{
type: "user",
timestamp: "2026-05-01T00:00:00.000Z",
cwd,
message: { content: "run the workflow" },
},
{
type: "assistant",
timestamp: "2026-05-01T00:00:01.000Z",
cwd,
message: {
model: "claude-opus-4-8",
content: [{ type: "text", text: "starting" }],
usage: {
input_tokens: 10,
output_tokens: 5,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
},
]
.map((l) => JSON.stringify(l))
.join("\n");
}
/** A parseable inner-agent transcript with token usage + one tool call. */
function agentJsonl(agentId) {
return [
{
type: "user",
timestamp: "2026-05-01T00:00:02.000Z",
message: { content: `task for ${agentId}` },
},
{
type: "assistant",
timestamp: "2026-05-01T00:00:03.000Z",
message: {
model: "claude-opus-4-8",
content: [{ type: "tool_use", id: `${agentId}-t1`, name: "Read", input: {} }],
usage: {
input_tokens: 100,
output_tokens: 20,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
},
]
.map((l) => JSON.stringify(l))
.join("\n");
}
/**
* Write a session tree with a Workflow-tool run to disk under PROJECTS_DIR:
* <projName>/<sid>.jsonl
* <projName>/<sid>/subagents/workflows/<runId>/journal.jsonl (streaming)
* <projName>/<sid>/subagents/workflows/<runId>/agent-<id>.jsonl (one per agent)
* Returns the project dir path.
*/
function writeFixture(projName, sid, runId, agentIds) {
const projDir = path.join(PROJECTS_DIR, projName);
const cwd = `/tmp/${projName}`;
fs.mkdirSync(projDir, { recursive: true });
fs.writeFileSync(path.join(projDir, `${sid}.jsonl`), sessionJsonl(cwd));
const runDir = path.join(projDir, sid, "subagents", "workflows", runId);
fs.mkdirSync(runDir, { recursive: true });
const journalLines = [];
for (const id of agentIds) {
fs.writeFileSync(path.join(runDir, `agent-${id}.jsonl`), agentJsonl(id));
journalLines.push(JSON.stringify({ type: "started", agentId: id }));
journalLines.push(JSON.stringify({ type: "result", agentId: id, result: "ok" }));
}
fs.writeFileSync(path.join(runDir, "journal.jsonl"), journalLines.join("\n"));
return projDir;
}
const linkedCount = (runId) =>
dbModule.db.prepare("SELECT COUNT(*) AS c FROM agents WHERE workflow_run_id = ?").get(runId).c;
const workflowRow = (runId) =>
dbModule.db.prepare("SELECT agent_count FROM workflows WHERE run_id = ?").get(runId);
const linkedIds = (runId) =>
dbModule.db
.prepare("SELECT id FROM agents WHERE workflow_run_id = ? ORDER BY id")
.all(runId)
.map((r) => r.id);
before(() => {
fs.mkdirSync(PROJECTS_DIR, { recursive: true });
});
after(() => {
try {
dbModule.db.close();
} catch {
/* ignore */
}
try {
fs.rmSync(TMP_ROOT, { recursive: true, force: true });
} catch {
/* ignore */
}
});
describe("offline CLI import links Workflow-tool inner agents", () => {
it("importFromDirectory (ccam import path) links every inner agent to its run", async () => {
const SID = "aaaa1111-0000-4000-8000-000000000001";
const RUN = "wf_dirimport01";
const AGENTS = ["d1", "d2", "d3"];
const projDir = writeFixture("-tmp-projA", SID, RUN, AGENTS);
assert.equal(linkedCount(RUN), 0, "no inner agents linked before import");
await importFromDirectory(dbModule, projDir);
assert.equal(linkedCount(RUN), AGENTS.length, "all inner agents linked to the run");
assert.equal(
workflowRow(RUN)?.agent_count,
AGENTS.length,
"workflows.agent_count reflects fleet"
);
assert.deepEqual(
linkedIds(RUN),
AGENTS.map((a) => `${SID}-jsonl-${a}`).sort(),
"each inner agent linked under its <sid>-jsonl-<agentId> id"
);
});
it("importAllSessions (ccam import rescan) links every inner agent to its run", async () => {
const SID = "bbbb2222-0000-4000-8000-000000000002";
const RUN = "wf_rescan02";
const AGENTS = ["r1", "r2", "r3", "r4"];
writeFixture("-tmp-projB", SID, RUN, AGENTS);
assert.equal(linkedCount(RUN), 0, "no inner agents linked before rescan");
await importAllSessions(dbModule);
assert.equal(linkedCount(RUN), AGENTS.length, "all inner agents linked to the run");
assert.equal(
workflowRow(RUN)?.agent_count,
AGENTS.length,
"workflows.agent_count reflects fleet"
);
});
it("re-running the import is idempotent — no duplicate links or rows", async () => {
const SID = "cccc3333-0000-4000-8000-000000000003";
const RUN = "wf_idem03";
const AGENTS = ["i1", "i2"];
const projDir = writeFixture("-tmp-projC", SID, RUN, AGENTS);
await importFromDirectory(dbModule, projDir);
await importFromDirectory(dbModule, projDir);
assert.equal(linkedCount(RUN), AGENTS.length, "still exactly N links after a second import");
// N inner agents + the one main agent for the session, nothing duplicated.
const total = dbModule.db
.prepare("SELECT COUNT(*) AS c FROM agents WHERE session_id = ?")
.get(SID).c;
assert.equal(total, AGENTS.length + 1, "no duplicate agent rows on re-import");
});
});