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.
294 lines
11 KiB
JavaScript
294 lines
11 KiB
JavaScript
/**
|
|
* @file Tests for dual-layout (flat + nested Workflow-tool) sub-agent transcript
|
|
* resolution. Covers the resolver helpers in server/lib/claude-home.js, the
|
|
* durable snapshot writer in scripts/import-history.js, and the end-to-end
|
|
* GET /:id/transcript?run_id= route that surfaces a workflow inner agent's full
|
|
* (un-truncated) prompt/result text in the Workflows UI. Uses Node's built-in
|
|
* test runner with temp CLAUDE_HOME / DASHBOARD_DATA_DIR roots.
|
|
* @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 http = require("http");
|
|
|
|
// Isolate the dashboard DB and Claude Code home BEFORE requiring server modules.
|
|
const STAMP = `wf-transcript-${Date.now()}-${process.pid}`;
|
|
const TMP = path.join(os.tmpdir(), STAMP);
|
|
const CLAUDE_HOME = path.join(TMP, "home");
|
|
const DATA_DIR = path.join(TMP, "data");
|
|
const TEST_DB = path.join(TMP, "dashboard.db");
|
|
process.env.DASHBOARD_DB_PATH = TEST_DB;
|
|
process.env.CLAUDE_HOME = CLAUDE_HOME;
|
|
process.env.DASHBOARD_DATA_DIR = DATA_DIR;
|
|
|
|
const {
|
|
resolveAgentTranscriptInDir,
|
|
getSubagentTranscriptPath,
|
|
findSubagentTranscriptPath,
|
|
getSnapshotSubagentTranscriptPath,
|
|
} = require("../lib/claude-home");
|
|
const {
|
|
snapshotTranscript,
|
|
findSessionWorkflowSubagents,
|
|
} = require("../../scripts/import-history");
|
|
const { createApp, startServer } = require("../index");
|
|
const { db } = require("../db");
|
|
|
|
// encodeCwd is not exported; mirror its rule for building project paths.
|
|
const enc = (cwd) => cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
|
|
const CWD = "/tmp/cam-wf-project";
|
|
const SESSION = "sess-wf-resolve";
|
|
const SESSION_SNAP = "sess-wf-snapshot";
|
|
const RUN1 = "wf_run1aaaaaa";
|
|
const RUN2 = "wf_run2bbbbbb";
|
|
const WF_ID = "ad18a79192af10ed1"; // workflow inner agent — nested only
|
|
const FLAT_ID = "bd29b80203bf21fe2"; // regular sub-agent — flat
|
|
const DUP_ID = "cc1122334455667788"; // present in BOTH runs — ambiguous
|
|
|
|
const PROJECTS = path.join(CLAUDE_HOME, "projects");
|
|
const ENC_DIR = path.join(PROJECTS, enc(CWD));
|
|
const SUBAGENTS = path.join(ENC_DIR, SESSION, "subagents");
|
|
|
|
// A result longer than the journal's truncated preview would ever carry, so the
|
|
// route test can prove the full text (not a "…"-suffixed teaser) comes back.
|
|
const LONG_RESULT =
|
|
"CONFIRMED: " +
|
|
"the orbital telemetry reconciles across all three independent ground stations. ".repeat(8);
|
|
const PROMPT_TEXT = "Adversarially verify the starship claim against primary sources.";
|
|
|
|
function writeFile(p, contents) {
|
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
fs.writeFileSync(p, contents);
|
|
}
|
|
|
|
function jsonl(lines) {
|
|
return lines.map((o) => JSON.stringify(o)).join("\n") + "\n";
|
|
}
|
|
|
|
function transcriptJsonl(prompt, result) {
|
|
return jsonl([
|
|
{ type: "user", message: { role: "user", content: [{ type: "text", text: prompt }] } },
|
|
{
|
|
type: "assistant",
|
|
message: { role: "assistant", content: [{ type: "text", text: result }] },
|
|
},
|
|
]);
|
|
}
|
|
|
|
function fetch(urlPath) {
|
|
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: "GET" },
|
|
(res) => {
|
|
let body = "";
|
|
res.on("data", (c) => (body += c));
|
|
res.on("end", () => {
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(body);
|
|
} catch {
|
|
parsed = body;
|
|
}
|
|
resolve({ status: res.statusCode, body: parsed });
|
|
});
|
|
}
|
|
);
|
|
req.on("error", reject);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
function post(urlPath, body) {
|
|
return new Promise((resolve, reject) => {
|
|
const url = new URL(urlPath, BASE);
|
|
const payload = JSON.stringify(body);
|
|
const req = http.request(
|
|
{
|
|
hostname: url.hostname,
|
|
port: url.port,
|
|
path: url.pathname,
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Content-Length": Buffer.byteLength(payload),
|
|
},
|
|
},
|
|
(res) => {
|
|
let b = "";
|
|
res.on("data", (c) => (b += c));
|
|
res.on("end", () => resolve({ status: res.statusCode, body: JSON.parse(b || "{}") }));
|
|
}
|
|
);
|
|
req.on("error", reject);
|
|
req.write(payload);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
let server;
|
|
let BASE;
|
|
|
|
before(async () => {
|
|
// Live project tree: one flat sub-agent + nested workflow agents, with DUP_ID
|
|
// deliberately present in two runs to exercise glob ambiguity.
|
|
writeFile(path.join(SUBAGENTS, `agent-${FLAT_ID}.jsonl`), transcriptJsonl("flat", "flat-result"));
|
|
writeFile(
|
|
path.join(SUBAGENTS, "workflows", RUN1, `agent-${WF_ID}.jsonl`),
|
|
transcriptJsonl(PROMPT_TEXT, LONG_RESULT)
|
|
);
|
|
writeFile(
|
|
path.join(SUBAGENTS, "workflows", RUN1, `agent-${DUP_ID}.jsonl`),
|
|
transcriptJsonl("dup-r1", "dup-r1-result")
|
|
);
|
|
writeFile(
|
|
path.join(SUBAGENTS, "workflows", RUN2, `agent-${DUP_ID}.jsonl`),
|
|
transcriptJsonl("dup-r2", "dup-r2-result")
|
|
);
|
|
|
|
const app = createApp();
|
|
server = await startServer(app, 0);
|
|
BASE = `http://127.0.0.1:${server.address().port}`;
|
|
});
|
|
|
|
after(() => {
|
|
if (server) server.close();
|
|
if (db) db.close();
|
|
try {
|
|
fs.rmSync(TMP, { recursive: true, force: true });
|
|
} catch {
|
|
/* ignore cleanup errors */
|
|
}
|
|
});
|
|
|
|
describe("resolveAgentTranscriptInDir", () => {
|
|
it("prefers the flat layout (regular sub-agents resolve unchanged)", () => {
|
|
const hit = resolveAgentTranscriptInDir(SUBAGENTS, FLAT_ID);
|
|
assert.equal(hit, path.join(SUBAGENTS, `agent-${FLAT_ID}.jsonl`));
|
|
});
|
|
|
|
it("resolves a nested Workflow agent directly when the runId is known", () => {
|
|
const hit = resolveAgentTranscriptInDir(SUBAGENTS, WF_ID, RUN1);
|
|
assert.equal(hit, path.join(SUBAGENTS, "workflows", RUN1, `agent-${WF_ID}.jsonl`));
|
|
});
|
|
|
|
it("resolves a nested agent by scanning when it appears in exactly one run", () => {
|
|
const hit = resolveAgentTranscriptInDir(SUBAGENTS, WF_ID);
|
|
assert.equal(hit, path.join(SUBAGENTS, "workflows", RUN1, `agent-${WF_ID}.jsonl`));
|
|
});
|
|
|
|
it("returns null for an agentId ambiguous across runs (no guessing)", () => {
|
|
assert.equal(resolveAgentTranscriptInDir(SUBAGENTS, DUP_ID), null);
|
|
});
|
|
|
|
it("disambiguates an across-run agentId once the runId is supplied", () => {
|
|
assert.equal(
|
|
resolveAgentTranscriptInDir(SUBAGENTS, DUP_ID, RUN2),
|
|
path.join(SUBAGENTS, "workflows", RUN2, `agent-${DUP_ID}.jsonl`)
|
|
);
|
|
});
|
|
|
|
it("never throws and returns null for missing inputs", () => {
|
|
assert.equal(resolveAgentTranscriptInDir(null, WF_ID), null);
|
|
assert.equal(resolveAgentTranscriptInDir(SUBAGENTS, "does-not-exist"), null);
|
|
assert.equal(resolveAgentTranscriptInDir(SUBAGENTS, WF_ID, "wf_nope"), null);
|
|
});
|
|
});
|
|
|
|
describe("getSubagentTranscriptPath / findSubagentTranscriptPath", () => {
|
|
it("resolves a nested Workflow agent via cwd + runId", () => {
|
|
const hit = getSubagentTranscriptPath(SESSION, CWD, WF_ID, RUN1);
|
|
assert.equal(hit, path.join(SUBAGENTS, "workflows", RUN1, `agent-${WF_ID}.jsonl`));
|
|
});
|
|
|
|
it("still resolves a flat regular sub-agent (no runId — no regression)", () => {
|
|
const hit = getSubagentTranscriptPath(SESSION, CWD, FLAT_ID);
|
|
assert.equal(hit, path.join(SUBAGENTS, `agent-${FLAT_ID}.jsonl`));
|
|
});
|
|
|
|
it("falls back to a project scan (cwd unknown) for the nested layout", () => {
|
|
const hit = findSubagentTranscriptPath(SESSION, WF_ID, RUN1);
|
|
assert.equal(hit, path.join(SUBAGENTS, "workflows", RUN1, `agent-${WF_ID}.jsonl`));
|
|
});
|
|
|
|
it("returns null when nothing resolves", () => {
|
|
assert.equal(getSubagentTranscriptPath(SESSION, CWD, "missing", RUN1), null);
|
|
assert.equal(getSubagentTranscriptPath(SESSION, null, WF_ID, RUN1), null);
|
|
});
|
|
});
|
|
|
|
describe("snapshotTranscript (durable nested-layout fallback)", () => {
|
|
it("discovers nested Workflow sub-agents separately from flat ones", () => {
|
|
const mainPath = path.join(ENC_DIR, `${SESSION}.jsonl`);
|
|
writeFile(mainPath, jsonl([{ type: "summary", summary: "x" }]));
|
|
const found = findSessionWorkflowSubagents(mainPath);
|
|
const rels = found.map((f) => f.rel).sort();
|
|
assert.deepEqual(rels, [
|
|
path.join("workflows", RUN1, `agent-${WF_ID}.jsonl`),
|
|
path.join("workflows", RUN1, `agent-${DUP_ID}.jsonl`),
|
|
path.join("workflows", RUN2, `agent-${DUP_ID}.jsonl`),
|
|
]);
|
|
});
|
|
|
|
it("preserves the nested layout into the snapshot so the read route resolves it", () => {
|
|
// Build an independent source session, snapshot it, then resolve from the
|
|
// snapshot dir exactly as the route's third fallback does.
|
|
const srcMain = path.join(ENC_DIR, `${SESSION_SNAP}.jsonl`);
|
|
const srcNested = path.join(
|
|
ENC_DIR,
|
|
SESSION_SNAP,
|
|
"subagents",
|
|
"workflows",
|
|
RUN1,
|
|
`agent-${WF_ID}.jsonl`
|
|
);
|
|
writeFile(srcMain, jsonl([{ type: "summary", summary: "snap" }]));
|
|
writeFile(srcNested, transcriptJsonl(PROMPT_TEXT, LONG_RESULT));
|
|
|
|
snapshotTranscript(srcMain, SESSION_SNAP);
|
|
|
|
const snapHit = getSnapshotSubagentTranscriptPath(SESSION_SNAP, WF_ID, RUN1);
|
|
assert.ok(snapHit, "snapshot resolver should find the preserved nested transcript");
|
|
assert.equal(fs.readFileSync(snapHit, "utf8"), fs.readFileSync(srcNested, "utf8"));
|
|
});
|
|
|
|
it("does not throw for a session with no transcripts", () => {
|
|
assert.doesNotThrow(() => snapshotTranscript(path.join(ENC_DIR, "nope.jsonl"), "nope"));
|
|
});
|
|
});
|
|
|
|
describe("GET /:id/transcript?run_id= (full workflow agent text)", () => {
|
|
it("returns the full, un-truncated prompt + result for a nested agent", async () => {
|
|
const created = await post("/api/sessions", { id: SESSION, name: "wf", cwd: CWD });
|
|
assert.equal(created.status, 201);
|
|
|
|
const res = await fetch(
|
|
`/api/sessions/${SESSION}/transcript?agent_id=${WF_ID}&run_id=${RUN1}&limit=200`
|
|
);
|
|
assert.equal(res.status, 200);
|
|
assert.ok(Array.isArray(res.body.messages));
|
|
|
|
const texts = res.body.messages
|
|
.flatMap((m) => m.content || [])
|
|
.filter((b) => b.type === "text")
|
|
.map((b) => b.text);
|
|
assert.ok(texts.includes(PROMPT_TEXT), "user prompt should be present in full");
|
|
assert.ok(texts.includes(LONG_RESULT), "assistant result should be present in full");
|
|
// The whole point of the fix: full text, not a "…"-suffixed teaser.
|
|
assert.ok(!texts.some((t) => t.endsWith("…")));
|
|
});
|
|
|
|
it("returns an empty message list (no throw) for an unknown run/agent", async () => {
|
|
const res = await fetch(
|
|
`/api/sessions/${SESSION}/transcript?agent_id=${WF_ID}&run_id=wf_missing&limit=200`
|
|
);
|
|
assert.equal(res.status, 200);
|
|
assert.deepEqual(res.body.messages, []);
|
|
});
|
|
});
|