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.
This commit is contained in:
@@ -0,0 +1,416 @@
|
||||
/**
|
||||
* @file Tests for human-readable session names and the transcript rename
|
||||
* marker. Covers:
|
||||
* - TranscriptCache surfacing the latest custom-title / ai-title.
|
||||
* - The hook ingestor syncing sessions.name from the transcript title, with
|
||||
* custom-title winning and ai-title only filling placeholder names.
|
||||
* - GET /:id/transcript surfacing custom-title (/rename) as a synthetic
|
||||
* `session_event` marker, deduped, with ai-title excluded.
|
||||
* - GET /:id/transcript surfacing mid-turn queued user messages
|
||||
* (attachment/queued_command) as user rows, with queue-operation
|
||||
* bookkeeping and other attachment subtypes dropped.
|
||||
* Uses Node's built-in test runner with temp CLAUDE_HOME / DASHBOARD_DATA_DIR.
|
||||
* @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");
|
||||
|
||||
const STAMP = `sess-name-${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 { createApp, startServer } = require("../index");
|
||||
const { db, stmts } = require("../db");
|
||||
const TranscriptCache = require("../lib/transcript-cache");
|
||||
|
||||
const enc = (cwd) => cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
||||
const PROJECTS = path.join(CLAUDE_HOME, "projects");
|
||||
|
||||
function jsonl(lines) {
|
||||
return lines.map((o) => JSON.stringify(o)).join("\n") + "\n";
|
||||
}
|
||||
|
||||
function writeTranscript(cwd, sessionId, lines) {
|
||||
const p = path.join(PROJECTS, enc(cwd), `${sessionId}.jsonl`);
|
||||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||
fs.writeFileSync(p, jsonl(lines));
|
||||
return p;
|
||||
}
|
||||
|
||||
function req(method, urlPath, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(urlPath, BASE);
|
||||
const payload = body ? JSON.stringify(body) : null;
|
||||
const r = http.request(
|
||||
{
|
||||
hostname: url.hostname,
|
||||
port: url.port,
|
||||
path: url.pathname + url.search,
|
||||
method,
|
||||
headers: payload
|
||||
? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) }
|
||||
: {},
|
||||
},
|
||||
(res) => {
|
||||
let b = "";
|
||||
res.on("data", (c) => (b += c));
|
||||
res.on("end", () => {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(b || "{}");
|
||||
} catch {
|
||||
parsed = b;
|
||||
}
|
||||
resolve({ status: res.statusCode, body: parsed });
|
||||
});
|
||||
}
|
||||
);
|
||||
r.on("error", reject);
|
||||
if (payload) r.write(payload);
|
||||
r.end();
|
||||
});
|
||||
}
|
||||
|
||||
let server;
|
||||
let BASE;
|
||||
|
||||
before(async () => {
|
||||
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 */
|
||||
}
|
||||
});
|
||||
|
||||
describe("TranscriptCache — title extraction", () => {
|
||||
it("returns the latest custom-title and ai-title (last value wins)", () => {
|
||||
const cwd = "/tmp/cam-name-cache";
|
||||
const sid = "cache-titles";
|
||||
const p = writeTranscript(cwd, sid, [
|
||||
{ type: "ai-title", aiTitle: "Auto one", sessionId: sid },
|
||||
{ type: "user", message: { role: "user", content: "hi" } },
|
||||
{ type: "ai-title", aiTitle: "Auto two", sessionId: sid },
|
||||
{ type: "custom-title", customTitle: "my-feature", sessionId: sid },
|
||||
]);
|
||||
const r = new TranscriptCache().extract(p);
|
||||
assert.equal(r.customTitle, "my-feature");
|
||||
assert.equal(r.aiTitle, "Auto two");
|
||||
});
|
||||
|
||||
it("returns a result for a transcript that has ONLY a title line", () => {
|
||||
const cwd = "/tmp/cam-name-only";
|
||||
const sid = "only-title";
|
||||
const p = writeTranscript(cwd, sid, [
|
||||
{ type: "custom-title", customTitle: "title-only", sessionId: sid },
|
||||
]);
|
||||
const r = new TranscriptCache().extract(p);
|
||||
assert.ok(r, "result should not be null");
|
||||
assert.equal(r.customTitle, "title-only");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hook ingestor — sessions.name sync from transcript", () => {
|
||||
it("sets the name to the custom-title on the next hook event", async () => {
|
||||
const cwd = "/tmp/cam-name-custom";
|
||||
const sid = "11111111-2222-3333-4444-555555555555";
|
||||
const tpath = writeTranscript(cwd, sid, [
|
||||
{ type: "user", message: { role: "user", content: "hello" } },
|
||||
{ type: "custom-title", customTitle: "auth-refactor", sessionId: sid },
|
||||
]);
|
||||
const res = await req("POST", "/api/hooks/event", {
|
||||
hook_type: "Stop",
|
||||
data: { session_id: sid, cwd, transcript_path: tpath },
|
||||
});
|
||||
assert.equal(res.status, 200);
|
||||
const row = stmts.getSession.get(sid);
|
||||
assert.equal(row.name, "auth-refactor");
|
||||
});
|
||||
|
||||
it("fills a placeholder name with the ai-title when there is no custom-title", async () => {
|
||||
const cwd = "/tmp/cam-name-ai";
|
||||
const sid = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
|
||||
const tpath = writeTranscript(cwd, sid, [
|
||||
{ type: "user", message: { role: "user", content: "hello" } },
|
||||
{ type: "ai-title", aiTitle: "Investigate flaky test", sessionId: sid },
|
||||
]);
|
||||
await req("POST", "/api/hooks/event", {
|
||||
hook_type: "Stop",
|
||||
data: { session_id: sid, cwd, transcript_path: tpath },
|
||||
});
|
||||
const row = stmts.getSession.get(sid);
|
||||
assert.equal(row.name, "Investigate flaky test");
|
||||
});
|
||||
|
||||
it("does not let an ai-title clobber a user-chosen name, but a custom-title does", async () => {
|
||||
const cwd = "/tmp/cam-name-precedence";
|
||||
const sid = "99999999-8888-7777-6666-555555555555";
|
||||
// Seed a real, user-chosen name via the sessions API.
|
||||
await req("POST", "/api/sessions", { id: sid, name: "keep-me", cwd });
|
||||
|
||||
// An ai-title must NOT overwrite the user-chosen name.
|
||||
let tpath = writeTranscript(cwd, sid, [
|
||||
{ type: "user", message: { role: "user", content: "x" } },
|
||||
{ type: "ai-title", aiTitle: "Auto generated", sessionId: sid },
|
||||
]);
|
||||
await req("POST", "/api/hooks/event", {
|
||||
hook_type: "Stop",
|
||||
data: { session_id: sid, cwd, transcript_path: tpath },
|
||||
});
|
||||
assert.equal(stmts.getSession.get(sid).name, "keep-me");
|
||||
|
||||
// But an explicit /rename (custom-title) always wins.
|
||||
tpath = writeTranscript(cwd, sid, [
|
||||
{ type: "user", message: { role: "user", content: "x" } },
|
||||
{ type: "ai-title", aiTitle: "Auto generated", sessionId: sid },
|
||||
{ type: "custom-title", customTitle: "renamed-explicitly", sessionId: sid },
|
||||
]);
|
||||
await req("POST", "/api/hooks/event", {
|
||||
hook_type: "Stop",
|
||||
data: { session_id: sid, cwd, transcript_path: tpath },
|
||||
});
|
||||
assert.equal(stmts.getSession.get(sid).name, "renamed-explicitly");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /:id/transcript — rename markers", () => {
|
||||
it("surfaces custom-title as a deduped session_event and excludes ai-title", async () => {
|
||||
const cwd = "/tmp/cam-rename-marker";
|
||||
const sid = "deadbeef-0000-1111-2222-333333333333";
|
||||
writeTranscript(cwd, sid, [
|
||||
{ type: "user", message: { role: "user", content: "first" } },
|
||||
{ type: "ai-title", aiTitle: "noise 1", sessionId: sid },
|
||||
{ type: "custom-title", customTitle: "feature-x", sessionId: sid },
|
||||
{ type: "ai-title", aiTitle: "noise 2", sessionId: sid },
|
||||
// Duplicate custom-title with the SAME value — must be deduped away.
|
||||
{ type: "custom-title", customTitle: "feature-x", sessionId: sid },
|
||||
{
|
||||
type: "assistant",
|
||||
message: { role: "assistant", content: [{ type: "text", text: "ok" }] },
|
||||
},
|
||||
{ type: "custom-title", customTitle: "feature-y", sessionId: sid },
|
||||
]);
|
||||
// Register the session so the endpoint doesn't 404.
|
||||
await req("POST", "/api/sessions", { id: sid, cwd });
|
||||
|
||||
const res = await req("GET", `/api/sessions/${sid}/transcript?limit=200`);
|
||||
assert.equal(res.status, 200);
|
||||
const events = res.body.messages.filter((m) => m.type === "session_event");
|
||||
const titles = events.map((e) => e.title);
|
||||
// feature-x once (dupe collapsed), then feature-y. No ai-title leaks in.
|
||||
assert.deepEqual(titles, ["feature-x", "feature-y"]);
|
||||
assert.ok(
|
||||
events.every((e) => e.event_kind === "rename"),
|
||||
"every marker is a rename"
|
||||
);
|
||||
assert.ok(
|
||||
!res.body.messages.some((m) => m.type === "session_event" && /noise/.test(m.title || "")),
|
||||
"ai-title values are never surfaced in the transcript stream"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /:id/transcript — local slash-command output (system/local_command)", () => {
|
||||
it("surfaces /color command + its stdout, skips empty + noise system lines", async () => {
|
||||
const cwd = "/tmp/cam-local-cmd";
|
||||
const sid = "c010rrrr-1111-2222-3333-444444444444";
|
||||
writeTranscript(cwd, sid, [
|
||||
{ type: "user", message: { role: "user", content: "hi" } },
|
||||
// /color, current Claude Code shape: command + output as system/local_command
|
||||
{
|
||||
type: "system",
|
||||
subtype: "local_command",
|
||||
content:
|
||||
"<command-name>/color</command-name>\n <command-message>color</command-message>\n <command-args></command-args>",
|
||||
sessionId: sid,
|
||||
},
|
||||
{ type: "agent-color", agentColor: "cyan", sessionId: sid },
|
||||
{
|
||||
type: "system",
|
||||
subtype: "local_command",
|
||||
content: "<local-command-stdout>Session color set to: cyan</local-command-stdout>",
|
||||
sessionId: sid,
|
||||
},
|
||||
// /clear writes a content-less local_command line — must NOT become a row
|
||||
{ type: "system", subtype: "local_command", content: "", sessionId: sid },
|
||||
// unrelated system subtype — pure noise, must be dropped
|
||||
{ type: "system", subtype: "turn_duration", durationMs: 1200, sessionId: sid },
|
||||
]);
|
||||
await req("POST", "/api/sessions", { id: sid, cwd });
|
||||
|
||||
const res = await req("GET", `/api/sessions/${sid}/transcript?limit=200`);
|
||||
assert.equal(res.status, 200);
|
||||
const texts = res.body.messages.flatMap((m) =>
|
||||
m.content.filter((c) => c.type === "text").map((c) => c.text)
|
||||
);
|
||||
assert.ok(
|
||||
texts.some((tx) => tx.includes("<command-name>/color</command-name>")),
|
||||
"the /color command invocation is surfaced"
|
||||
);
|
||||
assert.ok(
|
||||
texts.some((tx) => tx.includes("Session color set to: cyan")),
|
||||
"the /color stdout is surfaced"
|
||||
);
|
||||
// Empty local_command (/clear) and turn_duration noise produce no message.
|
||||
assert.ok(
|
||||
!texts.some((tx) => /turn_duration|durationMs/.test(tx)),
|
||||
"non-local_command system subtypes are not surfaced"
|
||||
);
|
||||
// Exactly two surfaced rows from the system lines (command + stdout), plus
|
||||
// the one real user message — the empty + noise lines add nothing.
|
||||
assert.equal(res.body.messages.length, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /:id/transcript — mid-turn queued user messages (attachment/queued_command)", () => {
|
||||
it("surfaces a queued_command attachment as a user message; drops queue-operation and other attachments", async () => {
|
||||
const cwd = "/tmp/cam-queued-msg";
|
||||
const sid = "aaaa1111-2222-3333-4444-555566667777";
|
||||
writeTranscript(cwd, sid, [
|
||||
{ type: "user", message: { role: "user", content: "start the docs sweep" } },
|
||||
{
|
||||
type: "assistant",
|
||||
message: { role: "assistant", content: [{ type: "text", text: "working on it" }] },
|
||||
},
|
||||
// A message typed while Claude was mid-turn is journaled as queue-operation
|
||||
// bookkeeping plus a queued_command attachment — there is NO user line.
|
||||
{
|
||||
type: "queue-operation",
|
||||
operation: "enqueue",
|
||||
content: "doc sweep too pls",
|
||||
sessionId: sid,
|
||||
},
|
||||
{
|
||||
type: "queue-operation",
|
||||
operation: "remove",
|
||||
content: "doc sweep too pls",
|
||||
sessionId: sid,
|
||||
},
|
||||
{
|
||||
type: "attachment",
|
||||
attachment: {
|
||||
type: "queued_command",
|
||||
prompt: "doc sweep too pls",
|
||||
commandMode: "prompt",
|
||||
origin: { kind: "human" },
|
||||
timestamp: "2026-07-16T02:40:45.596Z",
|
||||
},
|
||||
timestamp: "2026-07-16T02:40:45.596Z",
|
||||
sessionId: sid,
|
||||
},
|
||||
// Non-queued_command attachments are harness noise — must NOT surface.
|
||||
{
|
||||
type: "attachment",
|
||||
attachment: { type: "task_reminder", content: "reminder noise" },
|
||||
sessionId: sid,
|
||||
},
|
||||
// A content-less queued_command must not become an empty row either.
|
||||
{
|
||||
type: "attachment",
|
||||
attachment: { type: "queued_command", prompt: " ", origin: { kind: "human" } },
|
||||
sessionId: sid,
|
||||
},
|
||||
]);
|
||||
await req("POST", "/api/sessions", { id: sid, cwd });
|
||||
|
||||
const res = await req("GET", `/api/sessions/${sid}/transcript?limit=200`);
|
||||
assert.equal(res.status, 200);
|
||||
const queued = res.body.messages.filter((m) =>
|
||||
m.content.some((c) => c.type === "text" && c.text === "doc sweep too pls")
|
||||
);
|
||||
assert.equal(queued.length, 1, "the mid-turn message surfaces exactly once");
|
||||
assert.equal(queued[0].type, "user");
|
||||
assert.equal(
|
||||
queued[0].sender,
|
||||
"user",
|
||||
"a human-typed queued message is attributed to the user"
|
||||
);
|
||||
assert.equal(queued[0].timestamp, "2026-07-16T02:40:45.596Z");
|
||||
assert.ok(
|
||||
!res.body.messages.some((m) =>
|
||||
m.content.some((c) => c.type === "text" && /reminder noise/.test(c.text || ""))
|
||||
),
|
||||
"non-queued_command attachments stay hidden"
|
||||
);
|
||||
// start + working + the queued message; queue-operation lines, the noise
|
||||
// attachment, and the blank prompt add nothing.
|
||||
assert.equal(res.body.messages.length, 3);
|
||||
});
|
||||
|
||||
it("attributes harness-injected queued_command lines (task-notifications) to system, not the user", async () => {
|
||||
const cwd = "/tmp/cam-queued-sys";
|
||||
const sid = "bbbb1111-2222-3333-4444-555566667777";
|
||||
writeTranscript(cwd, sid, [
|
||||
{ type: "user", message: { role: "user", content: "kick off the agents" } },
|
||||
// Background-agent task-notification delivered through the SAME queue as
|
||||
// typed messages — real shape: attachment has NO origin field at all.
|
||||
{
|
||||
type: "attachment",
|
||||
attachment: {
|
||||
type: "queued_command",
|
||||
prompt:
|
||||
"<task-notification>\n<task-id>a30201bfc90e18271</task-id>\n<status>completed</status>\n</task-notification>",
|
||||
commandMode: "prompt",
|
||||
timestamp: "2026-07-16T02:47:36.000Z",
|
||||
},
|
||||
timestamp: "2026-07-16T02:47:36.000Z",
|
||||
sessionId: sid,
|
||||
},
|
||||
// Banner-prefixed variant must also be system.
|
||||
{
|
||||
type: "attachment",
|
||||
attachment: {
|
||||
type: "queued_command",
|
||||
prompt: "[SYSTEM NOTIFICATION - NOT USER INPUT]\nautomated background-task event",
|
||||
commandMode: "prompt",
|
||||
},
|
||||
sessionId: sid,
|
||||
},
|
||||
// Explicit non-human origin → system too.
|
||||
{
|
||||
type: "attachment",
|
||||
attachment: {
|
||||
type: "queued_command",
|
||||
prompt: "sdk enqueued follow-up",
|
||||
origin: { kind: "sdk" },
|
||||
},
|
||||
sessionId: sid,
|
||||
},
|
||||
// Missing origin but plain human-looking text stays user (older builds).
|
||||
{
|
||||
type: "attachment",
|
||||
attachment: { type: "queued_command", prompt: "and update the docs" },
|
||||
sessionId: sid,
|
||||
},
|
||||
]);
|
||||
await req("POST", "/api/sessions", { id: sid, cwd });
|
||||
|
||||
const res = await req("GET", `/api/sessions/${sid}/transcript?limit=200`);
|
||||
assert.equal(res.status, 200);
|
||||
const bySnippet = (s) =>
|
||||
res.body.messages.find((m) =>
|
||||
m.content.some((c) => c.type === "text" && (c.text || "").includes(s))
|
||||
);
|
||||
assert.equal(bySnippet("<task-notification>").sender, "system");
|
||||
assert.equal(bySnippet("[SYSTEM NOTIFICATION").sender, "system");
|
||||
assert.equal(bySnippet("sdk enqueued follow-up").sender, "system");
|
||||
assert.equal(bySnippet("and update the docs").sender, "user");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user