Files
Claude-Code-Monitor/server/__tests__/first-user-descriptor.test.js
T
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

486 lines
17 KiB
JavaScript

/**
* @file Tests for the first-user-prompt fallback descriptor (issue #201).
* Covers:
* - TranscriptCache capturing the first real user message (tool-result,
* meta/caveat, slash-command plumbing, and interrupt entries skipped),
* normalized and length-capped, first value winning across incremental
* re-reads.
* - The hook ingestor filling placeholder session/main-agent names and the
* main agent task from the descriptor — without clobbering real titles,
* user-set names, or an in-flight current_tool — and staying idempotent.
* - A later ai-title replacing a descriptor-filled session name.
* - importSession using the descriptor for imported sessions (new rows and
* the re-import backfill path).
* 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 = `first-user-desc-${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 dbModule = require("../db");
const { db, stmts } = dbModule;
const TranscriptCache = require("../lib/transcript-cache");
const { parseSessionFile, importSession } = require("../../scripts/import-history");
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 transcriptPath(cwd, sessionId) {
return path.join(PROJECTS, enc(cwd), `${sessionId}.jsonl`);
}
function writeTranscript(cwd, sessionId, lines) {
const p = transcriptPath(cwd, sessionId);
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, jsonl(lines));
return p;
}
function appendTranscript(cwd, sessionId, lines) {
const p = transcriptPath(cwd, sessionId);
fs.appendFileSync(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 — first user message extraction", () => {
it("captures the first real prompt, skipping caveat/command/tool-result entries", () => {
const cwd = "/tmp/fud-cache-skip";
const sid = "cache-skip";
const p = writeTranscript(cwd, sid, [
{
type: "user",
isMeta: true,
message: {
role: "user",
content: "<local-command-caveat>Caveat: local commands</local-command-caveat>",
},
},
{
type: "user",
message: { role: "user", content: "<command-name>/model</command-name>" },
},
{
type: "user",
message: {
role: "user",
content: "<local-command-stdout>Set model</local-command-stdout>",
},
},
{
type: "user",
message: {
role: "user",
content: [{ type: "tool_result", tool_use_id: "t1", content: "result text" }],
},
},
{
type: "user",
message: { role: "user", content: "fix the login\nbug in auth.ts" },
},
{ type: "user", message: { role: "user", content: "a later prompt" } },
]);
const r = new TranscriptCache().extract(p);
assert.ok(r, "result should not be null");
// Whitespace runs / newlines collapse to single spaces; first prompt wins.
assert.equal(r.firstUserMessage, "fix the login bug in auth.ts");
});
it("skips user-interrupt entries and caps the captured text at 500 chars", () => {
const cwd = "/tmp/fud-cache-cap";
const sid = "cache-cap";
const long = "x".repeat(600);
const p = writeTranscript(cwd, sid, [
{
type: "user",
message: {
role: "user",
content: [{ type: "text", text: "[Request interrupted by user]" }],
},
},
{ type: "user", message: { role: "user", content: long } },
]);
const r = new TranscriptCache().extract(p);
assert.equal(r.firstUserMessage, "x".repeat(500));
});
it("returns a result for a transcript that has ONLY a user prompt", () => {
const cwd = "/tmp/fud-cache-only";
const sid = "cache-only";
const p = writeTranscript(cwd, sid, [
{ type: "user", message: { role: "user", content: "just a prompt" } },
]);
const r = new TranscriptCache().extract(p);
assert.ok(r, "result should not be null");
assert.equal(r.firstUserMessage, "just a prompt");
});
it("keeps the FIRST prompt across incremental appends (first wins)", () => {
const cwd = "/tmp/fud-cache-incr";
const sid = "cache-incr";
const cache = new TranscriptCache();
const p = writeTranscript(cwd, sid, [
{ type: "user", message: { role: "user", content: "original prompt" } },
]);
assert.equal(cache.extract(p).firstUserMessage, "original prompt");
appendTranscript(cwd, sid, [
{ type: "user", message: { role: "user", content: "second prompt" } },
]);
// Force a fresh stat (mtime granularity) by touching size — append above
// already grew the file, so the incremental path runs.
assert.equal(cache.extract(p).firstUserMessage, "original prompt");
});
});
describe("hook ingestor — descriptor fills placeholders", () => {
it("fills placeholder session name and main agent name/task on UserPromptSubmit", async () => {
const cwd = "/tmp/fud-hook-fill";
const sid = "10000000-0000-0000-0000-000000000001";
const tpath = writeTranscript(cwd, sid, [
{ type: "user", message: { role: "user", content: "add dark mode to settings" } },
]);
const res = await req("POST", "/api/hooks/event", {
hook_type: "UserPromptSubmit",
data: { session_id: sid, cwd, transcript_path: tpath },
});
assert.equal(res.status, 200);
const sess = stmts.getSession.get(sid);
assert.equal(sess.name, "add dark mode to settings");
const main = stmts.getAgent.get(`${sid}-main`);
assert.equal(main.name, "Main Agent - add dark mode to settings");
assert.equal(main.task, "add dark mode to settings");
});
it("truncates long prompts to 60 chars for names but keeps the task longer", async () => {
const cwd = "/tmp/fud-hook-trunc";
const sid = "10000000-0000-0000-0000-000000000002";
const prompt = "p".repeat(100);
const tpath = writeTranscript(cwd, sid, [
{ type: "user", message: { role: "user", content: prompt } },
]);
await req("POST", "/api/hooks/event", {
hook_type: "Stop",
data: { session_id: sid, cwd, transcript_path: tpath },
});
const sess = stmts.getSession.get(sid);
assert.equal(sess.name, "p".repeat(57) + "...");
const main = stmts.getAgent.get(`${sid}-main`);
assert.equal(main.name, `Main Agent - ${"p".repeat(57)}...`);
assert.equal(main.task, prompt);
});
it("title still wins: ai-title takes the session name, descriptor fills the agent", async () => {
const cwd = "/tmp/fud-hook-title";
const sid = "10000000-0000-0000-0000-000000000003";
const tpath = writeTranscript(cwd, sid, [
{ type: "user", message: { role: "user", content: "investigate the flaky test" } },
{ type: "ai-title", aiTitle: "Flaky test investigation", sessionId: sid },
]);
await req("POST", "/api/hooks/event", {
hook_type: "Stop",
data: { session_id: sid, cwd, transcript_path: tpath },
});
const sess = stmts.getSession.get(sid);
assert.equal(sess.name, "Flaky test investigation");
const main = stmts.getAgent.get(`${sid}-main`);
assert.equal(main.name, "Main Agent - investigate the flaky test");
assert.equal(main.task, "investigate the flaky test");
});
it("a later ai-title replaces a descriptor-filled session name", async () => {
const cwd = "/tmp/fud-hook-later-title";
const sid = "10000000-0000-0000-0000-000000000004";
const tpath = writeTranscript(cwd, sid, [
{ type: "user", message: { role: "user", content: "refactor the websocket layer" } },
]);
await req("POST", "/api/hooks/event", {
hook_type: "UserPromptSubmit",
data: { session_id: sid, cwd, transcript_path: tpath },
});
assert.equal(stmts.getSession.get(sid).name, "refactor the websocket layer");
appendTranscript(cwd, sid, [
{ type: "ai-title", aiTitle: "WebSocket refactor", 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, "WebSocket refactor");
});
it("never clobbers a user-set session name or a renamed main agent", async () => {
const cwd = "/tmp/fud-hook-user-set";
const sid = "10000000-0000-0000-0000-000000000005";
const tpath = writeTranscript(cwd, sid, [
{ type: "user", message: { role: "user", content: "descriptor text" } },
]);
// Seed the session, then simulate user-chosen names on both rows.
await req("POST", "/api/hooks/event", {
hook_type: "SessionStart",
data: { session_id: sid, cwd },
});
stmts.updateSessionName.run("my chosen name", sid, "my chosen name");
stmts.updateAgent.run("My Renamed Agent", null, "my task", null, null, null, `${sid}-main`);
await req("POST", "/api/hooks/event", {
hook_type: "Stop",
data: { session_id: sid, cwd, transcript_path: tpath },
});
assert.equal(stmts.getSession.get(sid).name, "my chosen name");
const main = stmts.getAgent.get(`${sid}-main`);
assert.equal(main.name, "My Renamed Agent");
assert.equal(main.task, "my task");
});
it("preserves an in-flight current_tool when filling the main agent", async () => {
const cwd = "/tmp/fud-hook-tool";
const sid = "10000000-0000-0000-0000-000000000006";
const tpath = writeTranscript(cwd, sid, [
{ type: "user", message: { role: "user", content: "run the test suite" } },
]);
// PreToolUse stamps current_tool = Bash in the same transaction that then
// applies the descriptor — the fill must not wipe the in-flight tool.
await req("POST", "/api/hooks/event", {
hook_type: "PreToolUse",
data: { session_id: sid, cwd, transcript_path: tpath, tool_name: "Bash" },
});
const main = stmts.getAgent.get(`${sid}-main`);
assert.equal(main.current_tool, "Bash");
assert.equal(main.name, "Main Agent - run the test suite");
assert.equal(main.task, "run the test suite");
});
it("is idempotent — a second event changes nothing", async () => {
const cwd = "/tmp/fud-hook-idem";
const sid = "10000000-0000-0000-0000-000000000007";
const tpath = writeTranscript(cwd, sid, [
{ type: "user", message: { role: "user", content: "one prompt only" } },
]);
await req("POST", "/api/hooks/event", {
hook_type: "UserPromptSubmit",
data: { session_id: sid, cwd, transcript_path: tpath },
});
const before = {
session: stmts.getSession.get(sid).name,
agent: stmts.getAgent.get(`${sid}-main`),
};
await req("POST", "/api/hooks/event", {
hook_type: "Stop",
data: { session_id: sid, cwd, transcript_path: tpath },
});
assert.equal(stmts.getSession.get(sid).name, before.session);
const main = stmts.getAgent.get(`${sid}-main`);
assert.equal(main.name, before.agent.name);
assert.equal(main.task, before.agent.task);
});
});
describe("import — descriptor for imported sessions", () => {
it("parseSessionFile falls back to the first user prompt below titles", async () => {
const cwd = "/tmp/fud-import-parse";
const sid = "20000000-0000-0000-0000-000000000001";
const p = writeTranscript(cwd, sid, [
{
type: "user",
cwd,
timestamp: "2026-01-01T00:00:00.000Z",
message: { role: "user", content: "build the CSV exporter" },
},
]);
const parsed = await parseSessionFile(p);
assert.equal(parsed.firstUserMessage, "build the CSV exporter");
assert.equal(parsed.name, "build the CSV exporter");
});
it("a real title still outranks the descriptor in parseSessionFile", async () => {
const cwd = "/tmp/fud-import-title";
const sid = "20000000-0000-0000-0000-000000000002";
const p = writeTranscript(cwd, sid, [
{
type: "user",
cwd,
timestamp: "2026-01-01T00:00:00.000Z",
message: { role: "user", content: "some prompt" },
},
{ type: "ai-title", aiTitle: "Real title", sessionId: sid },
]);
const parsed = await parseSessionFile(p);
assert.equal(parsed.name, "Real title");
assert.equal(parsed.firstUserMessage, "some prompt");
});
it("importSession names new rows and their main agent from the descriptor", async () => {
const cwd = "/tmp/fud-import-new";
const sid = "20000000-0000-0000-0000-000000000003";
const p = writeTranscript(cwd, sid, [
{
type: "user",
cwd,
timestamp: "2026-01-01T00:00:00.000Z",
message: { role: "user", content: "wire up the pricing table" },
},
]);
const parsed = await parseSessionFile(p);
importSession(dbModule, parsed);
const sess = stmts.getSession.get(sid);
assert.equal(sess.name, "wire up the pricing table");
const main = stmts.getAgent.get(`${sid}-main`);
assert.equal(main.name, "Main Agent - wire up the pricing table");
assert.equal(main.task, "wire up the pricing table");
});
it("re-import backfills placeholder-named rows created by earlier imports", async () => {
const cwd = "/tmp/fud-import-backfill";
const sid = "20000000-0000-0000-0000-000000000004";
const p = writeTranscript(cwd, sid, [
{
type: "user",
cwd,
timestamp: "2026-01-01T00:00:00.000Z",
message: { role: "user", content: "migrate the alerts schema" },
},
]);
// Simulate an old import: cwd-derived placeholder session + agent names.
// metadata.imported = true is what routes importSession to its backfill
// branch (hand-created sessions are never touched by re-imports).
const base = path.basename(cwd);
stmts.insertSession.run(
sid,
`${base} - ${sid.slice(0, 8)}`,
"completed",
cwd,
null,
JSON.stringify({ imported: true })
);
stmts.insertAgent.run(
`${sid}-main`,
sid,
`Main Agent - ${base} - ${sid.slice(0, 8)}`,
"main",
null,
"completed",
null,
null,
null
);
const parsed = await parseSessionFile(p);
const result = importSession(dbModule, parsed);
assert.equal(result.backfilled, true);
assert.equal(stmts.getSession.get(sid).name, "migrate the alerts schema");
const main = stmts.getAgent.get(`${sid}-main`);
assert.equal(main.name, "Main Agent - migrate the alerts schema");
assert.equal(main.task, "migrate the alerts schema");
});
it("re-import keeps user-picked names intact", async () => {
const cwd = "/tmp/fud-import-keep";
const sid = "20000000-0000-0000-0000-000000000005";
const p = writeTranscript(cwd, sid, [
{
type: "user",
cwd,
timestamp: "2026-01-01T00:00:00.000Z",
message: { role: "user", content: "descriptor that must not apply" },
},
]);
stmts.insertSession.run(
sid,
"picked by hand",
"completed",
cwd,
null,
JSON.stringify({ imported: true })
);
stmts.insertAgent.run(
`${sid}-main`,
sid,
"Main Agent - picked by hand",
"main",
null,
"completed",
"hand-written task",
null,
null
);
const parsed = await parseSessionFile(p);
importSession(dbModule, parsed);
assert.equal(stmts.getSession.get(sid).name, "picked by hand");
const main = stmts.getAgent.get(`${sid}-main`);
assert.equal(main.name, "Main Agent - picked by hand");
assert.equal(main.task, "hand-written task");
});
});