Files
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

755 lines
26 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file Tests for subagent tool-event attribution.
*
* Subagent tool calls (Read, Bash, Edit, etc.) never fire hooks on the
* parent session — they only show up in the subagent's own JSONL file.
* Without dedicated extraction, every subagent ends up with at most a
* single spawn event, leaving 561/561 historical subagents with 05
* events instead of the dozens-to-hundreds they actually performed.
*
* This suite verifies that:
* 1. parseSubagentFile pairs tool_use blocks with their tool_result
* counterparts and surfaces them as `toolEvents`.
* 2. importSubagentFromJsonl emits PreToolUse + PostToolUse events
* under the subagent's own `agent_id`, so the UI attributes them
* to the subagent rather than the main agent.
* 3. Re-running the import is idempotent — no duplicate event rows.
* 4. When a live subagent (created via PreToolUse "Agent" hook) matches
* the JSONL by type + start time, events attach to the live row
* instead of creating a duplicate JSONL-keyed row.
*
* @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 path = require("path");
const fs = require("fs");
const os = require("os");
const TEST_DB = path.join(os.tmpdir(), `dashboard-subagent-test-${Date.now()}-${process.pid}.db`);
process.env.DASHBOARD_DB_PATH = TEST_DB;
const dbModule = require("../db");
const { db, stmts } = dbModule;
const importHistory = require("../../scripts/import-history");
const { calculateCost } = require("../routes/pricing");
after(() => {
if (db) db.close();
for (const suffix of ["", "-wal", "-shm"]) {
try {
fs.unlinkSync(TEST_DB + suffix);
} catch {
/* ignore */
}
}
});
// ── Fixture helpers ──────────────────────────────────────────────────
function writeSubagentJsonl(filePath, lines) {
fs.writeFileSync(filePath, lines.map((o) => JSON.stringify(o)).join("\n"));
}
/**
* Builds a minimal subagent JSONL with two tool calls — one Read with a
* paired tool_result, and one Bash with a paired error tool_result.
*/
function buildSubagentLines(agentType = "coder") {
return [
{
type: "user",
timestamp: "2026-04-28T10:00:00.000Z",
message: { content: [{ type: "text", text: "Investigate the bug" }] },
},
{
type: "assistant",
timestamp: "2026-04-28T10:00:01.000Z",
message: {
model: "claude-opus-4-7",
content: [
{
type: "tool_use",
id: "toolu_read_001",
name: "Read",
input: { file_path: "/tmp/foo.py" },
},
],
usage: {
input_tokens: 50,
output_tokens: 20,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
},
{
type: "user",
timestamp: "2026-04-28T10:00:02.000Z",
message: {
content: [
{
type: "tool_result",
tool_use_id: "toolu_read_001",
content: "def main():\n pass\n",
},
],
},
},
{
type: "assistant",
timestamp: "2026-04-28T10:00:03.000Z",
message: {
model: "claude-opus-4-7",
content: [
{
type: "tool_use",
id: "toolu_bash_002",
name: "Bash",
input: { command: "ls /tmp" },
},
],
},
},
{
type: "user",
timestamp: "2026-04-28T10:00:04.000Z",
message: {
content: [
{
type: "tool_result",
tool_use_id: "toolu_bash_002",
content: "ls: cannot access /tmp: not allowed",
is_error: true,
},
],
},
},
{
// Meta: keeps file timestamps coherent
type: "user",
timestamp: "2026-04-28T10:00:05.000Z",
message: { content: [{ type: "text", text: "done" }] },
},
].map((line, i) => {
// Inject agentType into one entry as a hint, mirroring real CC output
if (i === 0) line.agentType = agentType;
return line;
});
}
function writeMetaJson(filePath, agentType) {
fs.writeFileSync(filePath, JSON.stringify({ agentType }));
}
/**
* Minimal subagent JSONL on a specific model carrying one usage record, so
* parseSubagentFile yields a single token bucket keyed by that model.
*/
function buildModelSubLines(agentType, model, usage, startedAt = "2026-05-01T10:00:00.000Z") {
const t0 = startedAt;
return [
{
type: "user",
timestamp: t0,
agentType,
message: { content: [{ type: "text", text: "go" }] },
},
{
type: "assistant",
timestamp: "2026-05-01T10:00:01.000Z",
message: {
model,
content: [{ type: "tool_use", id: `toolu_${agentType}_1`, name: "Read", input: {} }],
usage: {
input_tokens: usage.input || 0,
output_tokens: usage.output || 0,
cache_read_input_tokens: usage.cacheRead || 0,
cache_creation_input_tokens: usage.cacheWrite || 0,
},
},
},
{
type: "user",
timestamp: "2026-05-01T10:00:02.000Z",
message: {
content: [{ type: "tool_result", tool_use_id: `toolu_${agentType}_1`, content: "ok" }],
},
},
];
}
// Lay out <base>/<sessionId>/subagents/agent-*.jsonl and return the matching
// transcriptPath (<base>/<sessionId>.jsonl) that scanAndImportSubagents expects.
function buildSubagentDir(sessionId, files) {
const base = fs.mkdtempSync(path.join(os.tmpdir(), `sa-scan-${process.pid}-`));
const subDir = path.join(base, sessionId, "subagents");
fs.mkdirSync(subDir, { recursive: true });
for (const f of files) {
writeSubagentJsonl(path.join(subDir, `agent-${f.id}.jsonl`), f.lines);
// Companion meta.json so parseSubagentFile resolves agentType (the live-match key).
if (f.agentType) {
fs.writeFileSync(
path.join(subDir, `agent-${f.id}.meta.json`),
JSON.stringify({ agentType: f.agentType })
);
}
}
return { transcriptPath: path.join(base, `${sessionId}.jsonl`), base };
}
// ── Tests ────────────────────────────────────────────────────────────
describe("parseSubagentFile — tool event extraction", () => {
it("pairs tool_use with tool_result and returns ordered toolEvents", async () => {
const tmpFile = path.join(os.tmpdir(), `agent-${Date.now()}-${process.pid}.jsonl`);
writeSubagentJsonl(tmpFile, buildSubagentLines("coder"));
writeMetaJson(tmpFile.replace(/\.jsonl$/, ".meta.json"), "coder");
try {
const data = await importHistory.parseSubagentFile(tmpFile);
assert.ok(data, "subagent data should parse");
assert.equal(data.agentType, "coder");
assert.ok(Array.isArray(data.toolEvents));
assert.equal(data.toolEvents.length, 2);
const [readEv, bashEv] = data.toolEvents;
assert.equal(readEv.tool_use_id, "toolu_read_001");
assert.equal(readEv.tool_name, "Read");
assert.deepEqual(readEv.tool_input, { file_path: "/tmp/foo.py" });
assert.equal(readEv.is_error, false);
assert.equal(typeof readEv.pre_timestamp, "string");
assert.equal(typeof readEv.post_timestamp, "string");
assert.ok(readEv.tool_response);
assert.equal(bashEv.tool_use_id, "toolu_bash_002");
assert.equal(bashEv.is_error, true);
} finally {
fs.unlinkSync(tmpFile);
try {
fs.unlinkSync(tmpFile.replace(/\.jsonl$/, ".meta.json"));
} catch {
/* ignore */
}
}
});
it("emits a tool_use even when no matching tool_result exists yet (live tail)", async () => {
const tmpFile = path.join(os.tmpdir(), `agent-tail-${Date.now()}-${process.pid}.jsonl`);
writeSubagentJsonl(tmpFile, [
{
type: "assistant",
timestamp: "2026-04-28T10:00:01.000Z",
message: {
model: "claude-opus-4-7",
content: [{ type: "tool_use", id: "toolu_pending", name: "Read", input: {} }],
},
},
]);
try {
const data = await importHistory.parseSubagentFile(tmpFile);
assert.equal(data.toolEvents.length, 1);
const ev = data.toolEvents[0];
assert.equal(ev.tool_use_id, "toolu_pending");
assert.equal(ev.post_timestamp, null);
assert.equal(ev.tool_response, null);
} finally {
fs.unlinkSync(tmpFile);
}
});
});
describe("importSubagentFromJsonl — event attribution", () => {
const sessionId = "test-sess-attribution";
const mainAgentId = `${sessionId}-main`;
before(() => {
// Seed session + main agent so importSubagentFromJsonl has parents to point at.
stmts.insertSession.run(sessionId, "Test Session", "active", "/tmp", null, null);
stmts.insertAgent.run(
mainAgentId,
sessionId,
"Main Agent",
"main",
null,
"waiting",
null,
null,
null
);
});
it("creates one subagent row and per-call PreToolUse + PostToolUse events", async () => {
const tmpFile = path.join(os.tmpdir(), `agent-attr-${Date.now()}-${process.pid}.jsonl`);
writeSubagentJsonl(tmpFile, buildSubagentLines("coder"));
writeMetaJson(tmpFile.replace(/\.jsonl$/, ".meta.json"), "coder");
try {
const data = await importHistory.parseSubagentFile(tmpFile);
const created = importHistory.importSubagentFromJsonl(dbModule, sessionId, mainAgentId, data);
assert.ok(created > 0, "should create at least the agent + spawn + 4 events");
const subId = `${sessionId}-jsonl-${data.agentId}`;
const subAgent = stmts.getAgent.get(subId);
assert.ok(subAgent, "JSONL-keyed subagent row should exist");
assert.equal(subAgent.parent_agent_id, mainAgentId);
const toolEvents = db
.prepare(
"SELECT event_type, tool_name FROM events WHERE agent_id = ? AND event_type IN ('PreToolUse', 'PostToolUse') ORDER BY id ASC"
)
.all(subId);
assert.equal(toolEvents.length, 4, "expected 2 Pre + 2 Post events under subagent's id");
assert.deepEqual(
toolEvents.map((e) => `${e.event_type}:${e.tool_name}`),
["PreToolUse:Read", "PostToolUse:Read", "PreToolUse:Bash", "PostToolUse:Bash"]
);
// Spawn marker lives under the main agent so the parent chain shows
// "Subagent spawned: coder" alongside main's other actions.
const spawnEvents = db
.prepare(
"SELECT 1 FROM events WHERE agent_id = ? AND event_type = 'PreToolUse' AND tool_name = 'Agent'"
)
.all(mainAgentId);
assert.equal(spawnEvents.length, 1);
} finally {
fs.unlinkSync(tmpFile);
try {
fs.unlinkSync(tmpFile.replace(/\.jsonl$/, ".meta.json"));
} catch {
/* ignore */
}
}
});
it("is idempotent — re-running does not duplicate events", async () => {
const tmpFile = path.join(os.tmpdir(), `agent-idem-${Date.now()}-${process.pid}.jsonl`);
writeSubagentJsonl(tmpFile, buildSubagentLines("reviewer"));
writeMetaJson(tmpFile.replace(/\.jsonl$/, ".meta.json"), "reviewer");
try {
const data = await importHistory.parseSubagentFile(tmpFile);
importHistory.importSubagentFromJsonl(dbModule, sessionId, mainAgentId, data);
const subId = `${sessionId}-jsonl-${data.agentId}`;
const before = db.prepare("SELECT COUNT(*) AS c FROM events WHERE agent_id = ?").get(subId).c;
// Second run — should be a no-op.
importHistory.importSubagentFromJsonl(dbModule, sessionId, mainAgentId, data);
const after = db.prepare("SELECT COUNT(*) AS c FROM events WHERE agent_id = ?").get(subId).c;
assert.equal(after, before, "idempotent re-import — no new rows");
} finally {
fs.unlinkSync(tmpFile);
try {
fs.unlinkSync(tmpFile.replace(/\.jsonl$/, ".meta.json"));
} catch {
/* ignore */
}
}
});
it("merges into a live subagent when one matches — no JSONL-keyed duplicate row", async () => {
// Simulate a live PreToolUse Agent hook having pre-created a subagent row.
const liveSubId = "live-uuid-xyz";
const startedAt = "2026-04-28T10:00:00.000Z";
stmts.insertAgent.run(
liveSubId,
sessionId,
"Live Coder",
"subagent",
"live-coder",
"completed",
"task",
mainAgentId,
null
);
db.prepare("UPDATE agents SET started_at = ?, ended_at = ?, updated_at = ? WHERE id = ?").run(
startedAt,
startedAt,
startedAt,
liveSubId
);
const tmpFile = path.join(os.tmpdir(), `agent-live-${Date.now()}-${process.pid}.jsonl`);
const lines = buildSubagentLines("live-coder");
writeSubagentJsonl(tmpFile, lines);
writeMetaJson(tmpFile.replace(/\.jsonl$/, ".meta.json"), "live-coder");
try {
const data = await importHistory.parseSubagentFile(tmpFile);
importHistory.importSubagentFromJsonl(dbModule, sessionId, mainAgentId, data);
const jsonlSubId = `${sessionId}-jsonl-${data.agentId}`;
assert.equal(
stmts.getAgent.get(jsonlSubId),
undefined,
"no JSONL-keyed row when a live subagent absorbed the events"
);
const eventsUnderLive = db
.prepare(
"SELECT 1 FROM events WHERE agent_id = ? AND event_type IN ('PreToolUse', 'PostToolUse')"
)
.all(liveSubId);
assert.ok(eventsUnderLive.length >= 4, "events should attach to the live subagent's id");
} finally {
fs.unlinkSync(tmpFile);
try {
fs.unlinkSync(tmpFile.replace(/\.jsonl$/, ".meta.json"));
} catch {
/* ignore */
}
}
});
});
// ── Per-subagent model token attribution (issue #185) ──────────────────
describe("scanAndImportSubagents — per-subagent model token attribution", () => {
it("buckets each subagent's tokens under its OWN model, skipping the parent model", async () => {
const sessionId = "sess-185-tiered";
// Orchestrator on Opus; subagents tiered to Sonnet + Haiku, plus one on the
// SAME model as the parent (Opus) to verify that bucket is intentionally skipped.
stmts.insertSession.run(sessionId, "Tiered", "active", "/tmp", "claude-opus-4-8", null);
stmts.insertAgent.run(
`${sessionId}-main`,
sessionId,
"Main",
"main",
null,
"working",
null,
null,
null
);
const { transcriptPath, base } = buildSubagentDir(sessionId, [
{
id: "hq",
lines: buildModelSubLines("qa", "claude-haiku-4-5-20251001", { input: 1000, output: 500 }),
},
{
id: "se",
lines: buildModelSubLines("engineer", "claude-sonnet-4-6", { input: 2000, output: 800 }),
},
{
id: "op",
lines: buildModelSubLines("planner", "claude-opus-4-8", { input: 9999, output: 9999 }),
},
]);
try {
await importHistory.scanAndImportSubagents(dbModule, sessionId, transcriptPath);
const rows = stmts.getTokensBySession.all(sessionId);
const byModel = Object.fromEntries(rows.map((r) => [r.model, r]));
const models = Object.keys(byModel).sort();
// Haiku + Sonnet buckets are written under their own models.
assert.deepEqual(models, ["claude-haiku-4-5-20251001", "claude-sonnet-4-6"]);
assert.equal(byModel["claude-haiku-4-5-20251001"].input_tokens, 1000);
assert.equal(byModel["claude-haiku-4-5-20251001"].output_tokens, 500);
assert.equal(byModel["claude-sonnet-4-6"].input_tokens, 2000);
assert.equal(byModel["claude-sonnet-4-6"].output_tokens, 800);
// The Opus subagent's tokens are NOT written here — that bucket belongs to
// the main-transcript writer; double-writing it would inflate via baseline.
assert.equal(byModel["claude-opus-4-8"], undefined, "parent-model bucket must be skipped");
// Cost is priced at the real (cheaper) per-subagent models, never Opus.
const cost = calculateCost(rows, stmts.listPricing.all());
const costModels = cost.breakdown.map((b) => b.model).sort();
assert.deepEqual(costModels, ["claude-haiku-4-5-20251001", "claude-sonnet-4-6"]);
assert.ok(cost.total_cost > 0);
// Each subagent row records its real model in metadata.
const hq = stmts.getAgent.get(`${sessionId}-jsonl-hq`);
assert.equal(JSON.parse(hq.metadata).model, "claude-haiku-4-5-20251001");
const se = stmts.getAgent.get(`${sessionId}-jsonl-se`);
assert.equal(JSON.parse(se.metadata).model, "claude-sonnet-4-6");
} finally {
fs.rmSync(base, { recursive: true, force: true });
}
});
it("re-running does not inflate token buckets (idempotent per-model write)", async () => {
const sessionId = "sess-185-idem";
stmts.insertSession.run(sessionId, "Idem", "active", "/tmp", "claude-opus-4-8", null);
stmts.insertAgent.run(
`${sessionId}-main`,
sessionId,
"Main",
"main",
null,
"working",
null,
null,
null
);
const { transcriptPath, base } = buildSubagentDir(sessionId, [
{
id: "hq",
lines: buildModelSubLines("qa", "claude-haiku-4-5-20251001", { input: 1000, output: 500 }),
},
]);
try {
await importHistory.scanAndImportSubagents(dbModule, sessionId, transcriptPath);
await importHistory.scanAndImportSubagents(dbModule, sessionId, transcriptPath);
const rows = stmts.getTokensBySession.all(sessionId);
const haiku = rows.find((r) => r.model === "claude-haiku-4-5-20251001");
// getTokensBySession already returns effective totals (current + baseline).
// Re-running must not double them — append-only subagent JSONLs never drop.
assert.equal(haiku.input_tokens, 1000);
assert.equal(haiku.output_tokens, 500);
} finally {
fs.rmSync(base, { recursive: true, force: true });
}
});
it("skips every model the MAIN transcript used, not just the latest (mid-session /model switch)", async () => {
const sessionId = "sess-185-switch";
// Orchestrator switched Opus → Sonnet mid-session; session.model holds the
// latest (Sonnet), but the main transcript wrote BOTH. A subagent on the
// earlier Opus must still be skipped to avoid colliding with the main writer.
stmts.insertSession.run(sessionId, "Switch", "active", "/tmp", "claude-sonnet-4-6", null);
stmts.insertAgent.run(
`${sessionId}-main`,
sessionId,
"Main",
"main",
null,
"working",
null,
null,
null
);
const { transcriptPath, base } = buildSubagentDir(sessionId, [
{
id: "op",
lines: buildModelSubLines("planner", "claude-opus-4-8", { input: 5000, output: 5000 }),
},
{
id: "hq",
lines: buildModelSubLines("qa", "claude-haiku-4-5-20251001", { input: 100, output: 50 }),
},
]);
try {
// parentModels carries BOTH orchestrator models (as hooks.js would pass).
await importHistory.scanAndImportSubagents(dbModule, sessionId, transcriptPath, {
parentModels: ["claude-sonnet-4-6", "claude-opus-4-8"],
});
const models = stmts.getTokensBySession
.all(sessionId)
.map((r) => r.model)
.sort();
// Only Haiku is written; both orchestrator models (Sonnet + the earlier
// Opus) are skipped even though Opus != session.model.
assert.deepEqual(models, ["claude-haiku-4-5-20251001"]);
} finally {
fs.rmSync(base, { recursive: true, force: true });
}
});
it("backfills a live subagent row's model from its own transcript", async () => {
const sessionId = "sess-185-live";
const startedAt = "2026-05-01T10:00:00.000Z";
stmts.insertSession.run(sessionId, "Live", "active", "/tmp", "claude-opus-4-8", null);
stmts.insertAgent.run(
`${sessionId}-main`,
sessionId,
"Main",
"main",
null,
"working",
null,
null,
null
);
// Live subagent created by the PreToolUse "Agent" hook — no model recorded.
const liveId = "live-185-qa";
stmts.insertAgent.run(
liveId,
sessionId,
"QA",
"subagent",
"qa",
"working",
"task",
`${sessionId}-main`,
null
);
db.prepare("UPDATE agents SET started_at = ?, ended_at = ?, updated_at = ? WHERE id = ?").run(
startedAt,
startedAt,
startedAt,
liveId
);
const { transcriptPath, base } = buildSubagentDir(sessionId, [
{
id: "qa1",
agentType: "qa",
lines: buildModelSubLines(
"qa",
"claude-haiku-4-5-20251001",
{ input: 10, output: 5 },
startedAt
),
},
]);
try {
assert.equal(stmts.getAgent.get(liveId).metadata, null, "live row starts with no model");
await importHistory.scanAndImportSubagents(dbModule, sessionId, transcriptPath);
const meta = JSON.parse(stmts.getAgent.get(liveId).metadata || "{}");
assert.equal(meta.model, "claude-haiku-4-5-20251001", "live row backfilled with real model");
// No JSONL-keyed duplicate row was created (events merged into the live row).
assert.equal(stmts.getAgent.get(`${sessionId}-jsonl-qa1`), undefined);
} finally {
fs.rmSync(base, { recursive: true, force: true });
}
});
});
describe("scanAndImportSubagents — nested subagent hierarchy", () => {
// A subagent's own transcript records each child it spawned via the Task tool
// as `toolUseResult.agentId`. Build a spawner file that claims `childIds`.
function buildSpawnerLines(childIds, startedAt = "2026-06-01T10:00:00.000Z") {
const lines = [
{ type: "user", timestamp: startedAt, message: { content: [{ type: "text", text: "go" }] } },
];
childIds.forEach((cid, i) => {
const ts = `2026-06-01T10:00:0${i + 1}.000Z`;
lines.push({
type: "assistant",
timestamp: ts,
message: {
model: "claude-opus-4-8",
content: [{ type: "tool_use", id: `toolu_task_${cid}`, name: "Task", input: {} }],
},
});
lines.push({
type: "user",
timestamp: ts,
toolUseResult: { agentId: cid, status: "completed" },
message: {
content: [{ type: "tool_result", tool_use_id: `toolu_task_${cid}`, content: "spawned" }],
},
});
});
return lines;
}
function buildLeafLines(startedAt = "2026-06-01T10:01:00.000Z") {
return [
{
type: "user",
timestamp: startedAt,
message: { content: [{ type: "text", text: "work" }] },
},
{
type: "assistant",
timestamp: startedAt,
message: {
model: "claude-haiku-4-5-20251001",
content: [{ type: "tool_use", id: "toolu_leaf", name: "Read", input: {} }],
},
},
];
}
it("nests subagents under their true spawner instead of flattening to main", async () => {
const sessionId = "sess-nested-tree";
const mainAgentId = `${sessionId}-main`;
stmts.insertSession.run(sessionId, "Nested", "active", "/tmp", "claude-opus-4-8", null);
stmts.insertAgent.run(
mainAgentId,
sessionId,
"Main",
"main",
null,
"working",
null,
null,
null
);
// main → orch; orch → leafA, leafB; main → solo
const { transcriptPath, base } = buildSubagentDir(sessionId, [
{ id: "orch", lines: buildSpawnerLines(["leafA", "leafB"]) },
{ id: "leafA", lines: buildLeafLines() },
{ id: "leafB", lines: buildLeafLines() },
{ id: "solo", lines: buildLeafLines() },
]);
try {
const res = await importHistory.scanAndImportSubagents(dbModule, sessionId, transcriptPath);
assert.equal(res.reparented, 2, "leafA + leafB repointed under orch");
const parentOf = (id) => stmts.getAgent.get(`${sessionId}-jsonl-${id}`).parent_agent_id;
assert.equal(parentOf("orch"), mainAgentId, "orch is a direct child of main");
assert.equal(parentOf("solo"), mainAgentId, "solo stays under main");
assert.equal(parentOf("leafA"), `${sessionId}-jsonl-orch`, "leafA nests under orch");
assert.equal(parentOf("leafB"), `${sessionId}-jsonl-orch`, "leafB nests under orch");
} finally {
fs.rmSync(base, { recursive: true, force: true });
}
});
it("is idempotent — a second scan repoints nothing", async () => {
const sessionId = "sess-nested-idem";
const mainAgentId = `${sessionId}-main`;
stmts.insertSession.run(sessionId, "NestedIdem", "active", "/tmp", "claude-opus-4-8", null);
stmts.insertAgent.run(
mainAgentId,
sessionId,
"Main",
"main",
null,
"working",
null,
null,
null
);
const { transcriptPath, base } = buildSubagentDir(sessionId, [
{ id: "orch2", lines: buildSpawnerLines(["leafC"]) },
{ id: "leafC", lines: buildLeafLines() },
]);
try {
const first = await importHistory.scanAndImportSubagents(dbModule, sessionId, transcriptPath);
assert.equal(first.reparented, 1);
const second = await importHistory.scanAndImportSubagents(
dbModule,
sessionId,
transcriptPath
);
assert.equal(second.reparented, 0, "no re-parenting on the second pass");
assert.equal(
stmts.getAgent.get(`${sessionId}-jsonl-leafC`).parent_agent_id,
`${sessionId}-jsonl-orch2`
);
} finally {
fs.rmSync(base, { recursive: true, force: true });
}
});
});
describe("events dedup index", () => {
it("has a (agent_id, event_type) index so per-tool-event dedup is not a full scan", () => {
// importSubagentFromJsonl dedups every tool event with
// "WHERE agent_id = ? AND event_type = ? AND data LIKE '%tool_use_id%'".
// Without this index each dedup full-scans the events table; on a large DB a
// single re-import (e.g. the startup sweep touching a subagent-heavy session)
// takes tens of seconds and blocks the event loop. Guard it from regressing.
const row = db
.prepare(
"SELECT name FROM sqlite_master WHERE type = 'index' AND name = 'idx_events_agent_type'"
)
.get();
assert.ok(row, "idx_events_agent_type must exist to keep subagent dedup indexed");
});
});