57dc91585d
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.
209 lines
7.2 KiB
JavaScript
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");
|
|
});
|
|
});
|