/** * @file Tests for Workflow-tool run ingestion (issue #167): parsing the on-disk * run journal, upserting a workflows row, linking inner agents by the shared * `${sessionId}-jsonl-` id scheme, idempotency, running→completed * detection with launch-time preservation, and folding inner-agent token usage * into the session cost under a namespaced `workflow` service_tier. * @author Nguyễn Ngọc Trí Vĩ */ 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"); // Isolated test DB before requiring any server module. const TEST_DB = path.join(os.tmpdir(), `dashboard-wf-test-${Date.now()}-${process.pid}.db`); process.env.DASHBOARD_DB_PATH = TEST_DB; const dbModule = require("../db"); const { stmts } = dbModule; const { ingestWorkflowsForSession, ingestAllWorkflows, workflowsMaxMtime, extractRunId, nameFromScript, mapState, } = require("../lib/workflow-ingest"); const SESSION_ID = "sess-wf-1"; let ROOT; // temp transcript root let transcriptPath; function writeJson(p, obj) { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(obj)); } // A minimal subagent transcript with token usage + one tool call. function agentJsonl(model, input, output) { return [ { type: "user", timestamp: "2026-02-01T00:00:00.000Z", message: { content: "go" } }, { type: "assistant", timestamp: "2026-02-01T00:00:02.000Z", message: { model, content: [{ type: "tool_use", id: "t1", name: "WebSearch", input: {} }], usage: { input_tokens: input, output_tokens: output, cache_read_input_tokens: 0, cache_creation_input_tokens: 0, }, }, }, ] .map((l) => JSON.stringify(l)) .join("\n"); } function subagentDir() { return path.join(ROOT, SESSION_ID, "subagents"); } function workflowsDir() { return path.join(ROOT, SESSION_ID, "workflows"); } before(() => { ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "wf-fixture-")); transcriptPath = path.join(ROOT, `${SESSION_ID}.jsonl`); fs.writeFileSync(transcriptPath, ""); // only dirname + basename are used // Parent session + main agent (FK targets). stmts.insertSession.run( SESSION_ID, "WF test session", "active", "/tmp/proj", "claude-opus-4-8", null ); stmts.insertAgent.run( `${SESSION_ID}-main`, SESSION_ID, "Main", "main", null, "completed", null, null, null ); // A completed run journal with two inner agents in two phases. writeJson(path.join(workflowsDir(), "wf_test123.json"), { runId: "wf_test123", taskId: "task-1", workflowName: "review-changes", status: "completed", startTime: 1700000000000, durationMs: 5000, defaultModel: "claude-opus-4-8", agentCount: 2, totalTokens: 12345, totalToolCalls: 7, phases: [ { title: "Review", detail: "review the diff" }, { title: "Verify", detail: "verify findings" }, ], workflowProgress: [ { type: "workflow_phase", index: 1, title: "Review" }, { type: "workflow_phase", index: 2, title: "Verify" }, { type: "workflow_agent", index: 1, agentId: "a1", model: "claude-opus-4-8", state: "done", label: "review:bugs", phaseTitle: "Review", startedAt: 1700000000000, tokens: 5000, toolCalls: 3, durationMs: 2000, lastToolName: "Read", }, { type: "workflow_agent", index: 2, agentId: "a2", model: "claude-haiku-4-5", state: "error", label: "verify:x", phaseTitle: "Verify", startedAt: 1700000002000, tokens: 7345, toolCalls: 4, durationMs: 3000, lastToolName: "Bash", }, ], }); // Inner-agent transcripts in the per-run nested dir, each with token usage so // ingest can fold their spend into the session cost. const runAgentDir = path.join(workflowsDir(), "..", "subagents", "workflows", "wf_test123"); const agentLines = (model, input, output) => [ { type: "user", timestamp: "2026-01-01T00:00:00.000Z", message: { content: "go" } }, { type: "assistant", timestamp: "2026-01-01T00:00:01.000Z", message: { model, content: [{ type: "text", text: "done" }], usage: { input_tokens: input, output_tokens: output, cache_read_input_tokens: 0, cache_creation_input_tokens: 0, }, }, }, ] .map((l) => JSON.stringify(l)) .join("\n"); fs.mkdirSync(runAgentDir, { recursive: true }); fs.writeFileSync( path.join(runAgentDir, "agent-a1.jsonl"), agentLines("claude-opus-4-8", 4000, 1000) ); fs.writeFileSync( path.join(runAgentDir, "agent-a2.jsonl"), agentLines("claude-opus-4-8", 6000, 1345) ); }); after(() => { try { fs.rmSync(ROOT, { recursive: true, force: true }); } catch { /* ignore */ } try { dbModule.db.close(); } catch { /* ignore */ } try { fs.rmSync(TEST_DB, { force: true }); } catch { /* ignore */ } }); describe("extractRunId / nameFromScript", () => { it("derives the same run id from a journal and its launch script", () => { assert.equal(extractRunId("wf_run999.json"), "wf_run999"); assert.equal(extractRunId("/x/y/myflow-wf_run999.js"), "wf_run999"); }); it("strips the -wf_ tail to recover the workflow name", () => { assert.equal(nameFromScript("review-changes-wf_abc123.js"), "review-changes"); }); }); describe("mapState", () => { it("maps journal states to agent statuses", () => { assert.equal(mapState("done"), "completed"); assert.equal(mapState("completed"), "completed"); assert.equal(mapState("success"), "completed"); assert.equal(mapState("error"), "error"); assert.equal(mapState("failed"), "error"); assert.equal(mapState("running"), "working"); assert.equal(mapState("queued"), "working"); assert.equal(mapState("in_progress"), "working"); assert.equal(mapState("anything-unknown"), "completed"); assert.equal(mapState(null), "completed"); }); }); describe("ingestWorkflowsForSession — completed journal", () => { it("ingests the journal as a workflow row with parsed phases/progress", async () => { const changed = await ingestWorkflowsForSession(dbModule, { id: SESSION_ID, transcript_path: transcriptPath, }); assert.ok(changed.length >= 1); const wf = stmts.getWorkflow.get("wf_test123"); assert.ok(wf, "workflow row exists"); assert.equal(wf.session_id, SESSION_ID); assert.equal(wf.name, "review-changes"); assert.equal(wf.status, "completed"); assert.equal(wf.agent_count, 2); assert.equal(wf.total_tokens, 12345); assert.equal(wf.total_tool_calls, 7); assert.equal(wf.source, "journal"); assert.ok(wf.started_at, "started_at populated"); assert.equal(wf.ended_at, new Date(1700000000000 + 5000).toISOString()); assert.equal(JSON.parse(wf.phases).length, 2); // progress keeps all entries (2 phase markers + 2 agents) assert.equal(JSON.parse(wf.progress).length, 4); assert.equal(JSON.parse(wf.progress).filter((p) => p.type === "workflow_agent").length, 2); }); it("links each inner agent by the shared jsonl id scheme, with phase + status", () => { const a1 = stmts.getAgent.get(`${SESSION_ID}-jsonl-a1`); const a2 = stmts.getAgent.get(`${SESSION_ID}-jsonl-a2`); assert.ok(a1 && a2, "both inner-agent rows exist"); assert.equal(a1.workflow_run_id, "wf_test123"); assert.equal(a1.workflow_phase, "Review"); assert.equal(a1.status, "completed"); assert.equal(a2.workflow_run_id, "wf_test123"); assert.equal(a2.workflow_phase, "Verify"); assert.equal(a2.status, "error"); const linked = stmts.listAgentsByWorkflow.all("wf_test123"); assert.equal(linked.length, 2); }); it("folds inner-agent tokens into the session under a 'workflow' service_tier", () => { const rows = dbModule.db .prepare("SELECT * FROM token_usage WHERE session_id = ?") .all(SESSION_ID); assert.ok(rows.length > 0, "workflow token rows written"); assert.ok( rows.every((r) => r.service_tier === "workflow"), "isolated under the workflow tier (no collision with main buckets)" ); // a1(4000)+a2(6000)=10000 input, 1000+1345=2345 output (same model → one row) const totalInput = rows.reduce((s, r) => s + r.input_tokens + r.baseline_input, 0); const totalOutput = rows.reduce((s, r) => s + r.output_tokens + r.baseline_output, 0); assert.equal(totalInput, 10000); assert.equal(totalOutput, 2345); }); it("is idempotent — re-ingest creates no duplicate rows and stable token totals", async () => { await ingestWorkflowsForSession(dbModule, { id: SESSION_ID, transcript_path: transcriptPath }); const wfCount = dbModule.db .prepare("SELECT COUNT(*) AS n FROM workflows WHERE session_id = ?") .get(SESSION_ID); assert.equal(wfCount.n, 1); const subCount = dbModule.db .prepare("SELECT COUNT(*) AS n FROM agents WHERE session_id = ? AND type = 'subagent'") .get(SESSION_ID); assert.equal(subCount.n, 2); // tokens not double-counted on re-ingest (replace semantics) const tot = dbModule.db .prepare( "SELECT SUM(input_tokens + baseline_input) AS i FROM token_usage WHERE session_id = ?" ) .get(SESSION_ID); assert.equal(tot.i, 10000); }); }); describe("running detection → completed transition", () => { it("shows a launch-script-only run as running, then completes it preserving started_at", async () => { // 1) Launch script, no journal yet. fs.mkdirSync(path.join(workflowsDir(), "scripts"), { recursive: true }); fs.writeFileSync(path.join(workflowsDir(), "scripts", "deep-audit-wf_run999.js"), "// script"); await ingestWorkflowsForSession(dbModule, { id: SESSION_ID, transcript_path: transcriptPath }); const running = stmts.getWorkflow.get("wf_run999"); assert.ok(running, "running row created from launch script"); assert.equal(running.status, "running"); assert.equal(running.source, "live"); assert.equal(running.name, "deep-audit"); assert.ok(running.started_at, "running row has a launch time"); const launchTime = running.started_at; // 2) Journal lands → same run_id → becomes completed, launch time preserved. writeJson(path.join(workflowsDir(), "wf_run999.json"), { runId: "wf_run999", workflowName: "deep-audit", status: "completed", startTime: 1700000500000, durationMs: 1000, agentCount: 0, totalTokens: 0, totalToolCalls: 0, phases: [], workflowProgress: [], }); await ingestWorkflowsForSession(dbModule, { id: SESSION_ID, transcript_path: transcriptPath }); const done = stmts.getWorkflow.get("wf_run999"); assert.equal(done.status, "completed"); assert.equal(done.started_at, launchTime, "launch time preserved across transition"); }); }); describe("workflowsMaxMtime", () => { it("returns the newest artifact mtime for a session with workflows, 0 otherwise", () => { assert.ok(workflowsMaxMtime(transcriptPath) > 0, "fingerprint > 0 when journals exist"); assert.equal( workflowsMaxMtime(path.join(ROOT, "no-such-session.jsonl")), 0, "0 when there are no workflow artifacts" ); }); // Regression guard for the maintenance sweep (server/index.js step 3) and // startWorkflowPoll: both skip a session whose workflow artifacts are // unchanged and re-ingest only once the fingerprint advances. Before the // gate, the 5-min sweep full-re-parsed every workflow journal and every // inner agent-*.jsonl for every active session every cycle; on a large // corpus each sweep outran the interval, sweeps overlapped, and the event // loop pegged (dashboard stopped responding). This asserts the exact // skip/re-ingest decision that gate relies on. Isolated root — no shared // fixture state so later suites are unaffected. it("gates skip vs re-ingest: stable fingerprint when unchanged, higher after a new artifact", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "wf-gate-")); try { const sid = "sess-gate"; const tp = path.join(root, `${sid}.jsonl`); fs.writeFileSync(tp, ""); const wdir = path.join(root, sid, "workflows"); writeJson(path.join(wdir, "wf_a.json"), { runId: "wf_a", status: "completed", startTime: 1700000000000, workflowProgress: [], }); const seen = new Map(); // mirrors sweepWorkflowSeen / lastSeen const m1 = workflowsMaxMtime(tp); assert.ok(m1 > 0, "fingerprint > 0 with a journal"); assert.equal(m1 === 0 || seen.get(sid) === m1, false, "first sight ingests"); seen.set(sid, m1); const m2 = workflowsMaxMtime(tp); assert.equal(m2, m1, "fingerprint stable when nothing changes"); assert.equal(m2 === 0 || seen.get(sid) === m2, true, "unchanged session is skipped"); const later = path.join(wdir, "wf_b.json"); writeJson(later, { runId: "wf_b", status: "completed", startTime: 1700000001000, workflowProgress: [], }); const bump = m1 / 1000 + 5; // seconds, safely newer than m1 fs.utimesSync(later, bump, bump); const m3 = workflowsMaxMtime(tp); assert.ok(m3 > m1, "fingerprint advances when a new artifact appears"); assert.equal(m3 === 0 || seen.get(sid) === m3, false, "changed session is re-ingested"); } finally { fs.rmSync(root, { recursive: true, force: true }); } }); }); describe("live running workflow (no terminal journal)", () => { it("builds real-time progress + tokens from the streaming run dir", async () => { const runId = "wf_live77"; const runDir = path.join(ROOT, SESSION_ID, "subagents", "workflows", runId); fs.mkdirSync(runDir, { recursive: true }); // a1 finished (has a result event), a2 still running (started only) fs.writeFileSync(path.join(runDir, "agent-a1.jsonl"), agentJsonl("claude-opus-4-8", 3000, 800)); fs.writeFileSync(path.join(runDir, "agent-a2.jsonl"), agentJsonl("claude-opus-4-8", 1500, 200)); fs.writeFileSync( path.join(runDir, "journal.jsonl"), [ JSON.stringify({ type: "started", agentId: "a1" }), JSON.stringify({ type: "started", agentId: "a2" }), // a3 started but has no transcript yet (queued) → minimal live entry JSON.stringify({ type: "started", agentId: "a3" }), JSON.stringify({ type: "result", agentId: "a1", result: { ok: true, note: "done" } }), ].join("\n") ); // a launch script (no terminal journal) → name resolves from it fs.mkdirSync(path.join(workflowsDir(), "scripts"), { recursive: true }); fs.writeFileSync(path.join(workflowsDir(), "scripts", `ds-pipeline-${runId}.js`), "// s"); await ingestWorkflowsForSession(dbModule, { id: SESSION_ID, transcript_path: transcriptPath }); const wf = stmts.getWorkflow.get(runId); assert.ok(wf, "live run row created"); assert.equal(wf.status, "running", "shown as running before terminal journal"); assert.equal(wf.source, "live"); assert.equal(wf.name, "ds-pipeline"); assert.equal(wf.agent_count, 3, "two transcripts + one queued agent"); assert.ok(wf.total_tokens > 0, "live tokens accumulated"); assert.ok(wf.total_tool_calls >= 2, "live tool calls counted"); const prog = JSON.parse(wf.progress); assert.equal(prog.length, 3); const a1 = prog.find((p) => p.agentId === "a1"); const a2 = prog.find((p) => p.agentId === "a2"); const a3 = prog.find((p) => p.agentId === "a3"); assert.equal(a1.state, "done", "a1 has a result → done"); assert.equal(a2.state, "running", "a2 only started → running"); assert.equal(a3.state, "running", "a3 queued (started, no transcript) → running"); assert.equal(a3.tokens, 0, "queued agent has no tokens yet"); assert.ok(a1.tokens > 0 && a1.toolCalls >= 1); assert.ok(a1.resultPreview, "finished agent carries its result"); // inner agents linked + live workflow tokens folded under the workflow tier assert.equal(stmts.listAgentsByWorkflow.all(runId).length, 2); const liveTier = dbModule.db .prepare("SELECT COUNT(*) AS n FROM token_usage WHERE session_id = ? AND service_tier = ?") .get(SESSION_ID, "workflow"); assert.ok(liveTier.n >= 1, "workflow-tier cost row written for the live run"); }); }); describe("ingestAllWorkflows backfill", () => { it("ingests on-disk workflows for sessions whose transcript_path is in the DB", async () => { // Backfill resolves the transcript from the session row, so persist it. dbModule.db .prepare("UPDATE sessions SET transcript_path = ? WHERE id = ?") .run(transcriptPath, SESSION_ID); const res = await ingestAllWorkflows(dbModule); assert.ok(res.sessions >= 1, "at least one session backfilled"); assert.ok(res.workflows >= 1, "at least one workflow ingested"); // The completed fixture run is present after backfill. assert.ok(stmts.getWorkflow.get("wf_test123"), "fixture run present"); }); });