Files
Claude-Code-Monitor/server/__tests__/import.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

554 lines
22 KiB
JavaScript

/**
* @file Tests for the Import History feature — the generalized directory
* importer and the /api/import routes. Verifies that token counts and cost
* computations come out identical between auto-import and manual import
* for the same JSONL fixtures, that re-imports are idempotent, and that
* archive extraction rejects path-traversal entries.
*
* @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 zlib = require("zlib");
const TEST_DB = path.join(os.tmpdir(), `dashboard-import-test-${Date.now()}-${process.pid}.db`);
process.env.DASHBOARD_DB_PATH = TEST_DB;
const { createApp, startServer } = require("../index");
const { db, stmts } = require("../db");
const importHistory = require("../../scripts/import-history");
const archive = require("../lib/archive");
let server;
let BASE;
function fetch(urlPath, options = {}) {
return new Promise((resolve, reject) => {
const url = new URL(urlPath, BASE);
const opts = {
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
method: options.method || "GET",
headers: { "Content-Type": "application/json", ...options.headers },
};
const req = http.request(opts, (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, headers: res.headers });
});
});
req.on("error", reject);
if (options.body) req.write(JSON.stringify(options.body));
req.end();
});
}
function post(urlPath, body) {
return fetch(urlPath, { method: "POST", body });
}
// ────────────────────────────────────────────────────────────────────────────
// Fixtures — deterministic JSONL sessions with known token counts so we can
// assert imported values match byte-for-byte.
// ────────────────────────────────────────────────────────────────────────────
const SESSION_A = "aaaaaaaa-1111-4111-8111-aaaaaaaaaaaa";
const SESSION_B = "bbbbbbbb-2222-4222-8222-bbbbbbbbbbbb";
function fixtureLines(sessionId, cwd, model, inputTok, outputTok) {
const base = "2026-04-18T12:00:00.000Z";
return [
{ type: "user", cwd, sessionId, timestamp: base, message: { content: "hi" } },
{
type: "assistant",
cwd,
sessionId,
timestamp: base,
message: {
model,
content: [{ type: "text", text: "ok" }],
usage: {
input_tokens: inputTok,
output_tokens: outputTok,
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
},
{
type: "assistant",
cwd,
sessionId,
timestamp: "2026-04-18T12:00:01.000Z",
message: {
model,
content: [{ type: "tool_use", name: "Read", input: { file_path: "/tmp/foo" } }],
usage: {
input_tokens: inputTok,
output_tokens: outputTok,
cache_read_input_tokens: 10,
cache_creation_input_tokens: 20,
},
},
},
];
}
function writeFixtureDir() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-fixture-"));
const projDir = path.join(root, "-Users-demo-project");
fs.mkdirSync(projDir, { recursive: true });
fs.writeFileSync(
path.join(projDir, `${SESSION_A}.jsonl`),
fixtureLines(SESSION_A, "/Users/demo/project", "claude-opus-4-7", 100, 50)
.map((o) => JSON.stringify(o))
.join("\n")
);
fs.writeFileSync(
path.join(projDir, `${SESSION_B}.jsonl`),
fixtureLines(SESSION_B, "/Users/demo/project", "claude-sonnet-4-6", 200, 100)
.map((o) => JSON.stringify(o))
.join("\n")
);
return root;
}
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();
for (const suffix of ["", "-wal", "-shm"]) {
try {
fs.unlinkSync(TEST_DB + suffix);
} catch {
/* ignore */
}
}
});
// ────────────────────────────────────────────────────────────────────────────
describe("GET /api/import/guide", () => {
it("returns OS-aware instructions and supported extensions", async () => {
const res = await fetch("/api/import/guide");
assert.equal(res.status, 200);
assert.equal(typeof res.body.default_projects_dir, "string");
assert.ok(res.body.supported_extensions.includes(".jsonl"));
assert.ok(res.body.supported_extensions.includes(".tar.gz"));
assert.ok(res.body.supported_extensions.includes(".zip"));
assert.ok(Array.isArray(res.body.steps));
assert.ok(res.body.steps.length >= 4);
assert.ok(res.body.archive_command.includes("tar"));
});
});
describe("POST /api/import/scan-path validation", () => {
it("rejects missing path", async () => {
const res = await post("/api/import/scan-path", {});
assert.equal(res.status, 400);
assert.equal(res.body.error.code, "INVALID_INPUT");
});
it("rejects relative paths", async () => {
const res = await post("/api/import/scan-path", { path: "./somewhere" });
assert.equal(res.status, 400);
assert.equal(res.body.error.code, "INVALID_INPUT");
});
it("rejects non-existent paths", async () => {
const res = await post("/api/import/scan-path", {
path: "/definitely/does/not/exist/ccam-" + Date.now(),
});
assert.equal(res.status, 400);
assert.equal(res.body.error.code, "PATH_NOT_FOUND");
});
it("rejects files (not directories)", async () => {
const tmp = path.join(os.tmpdir(), `ccam-not-dir-${Date.now()}.txt`);
fs.writeFileSync(tmp, "hello");
try {
const res = await post("/api/import/scan-path", { path: tmp });
assert.equal(res.status, 400);
assert.equal(res.body.error.code, "NOT_A_DIRECTORY");
} finally {
fs.unlinkSync(tmp);
}
});
});
describe("POST /api/import/scan-path happy path", () => {
it("imports sessions from a custom folder and records token usage", async () => {
const root = writeFixtureDir();
try {
const res = await post("/api/import/scan-path", { path: root });
assert.equal(res.status, 200);
assert.ok(res.body.ok);
// Both sessions should import the first time.
assert.ok(res.body.imported >= 2);
assert.equal(res.body.errors, 0);
const sessA = stmts.getSession.get(SESSION_A);
const sessB = stmts.getSession.get(SESSION_B);
assert.ok(sessA, "session A should exist in DB");
assert.ok(sessB, "session B should exist in DB");
// Tokens: each fixture has 2 assistant messages with usage.
const tokA = stmts.getTokensBySession.all(SESSION_A);
const opus = tokA.find((t) => /opus/.test(t.model));
assert.ok(opus, "expected opus tokens");
assert.equal(opus.input_tokens, 200);
assert.equal(opus.output_tokens, 100);
assert.equal(opus.cache_read_tokens, 10);
assert.equal(opus.cache_write_tokens, 20);
// Cost endpoint should produce a non-zero result after we add a pricing rule.
const ruleRes = await fetch("/api/pricing", {
method: "PUT",
body: {
model_pattern: "claude-opus-4-7",
display_name: "Opus 4.7",
input_per_mtok: 15,
output_per_mtok: 75,
cache_read_per_mtok: 1.5,
cache_write_per_mtok: 18.75,
},
});
assert.equal(ruleRes.status, 200);
const costRes = await fetch(`/api/pricing/cost/${SESSION_A}`);
assert.equal(costRes.status, 200);
assert.ok(costRes.body.total_cost > 0);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
it("is idempotent: a second scan does not duplicate tokens", async () => {
const before = stmts.getTokensBySession.all(SESSION_A);
const root = writeFixtureDir();
try {
const res = await post("/api/import/scan-path", { path: root });
assert.equal(res.status, 200);
const after = stmts.getTokensBySession.all(SESSION_A);
const beforeOpus = before.find((t) => /opus/.test(t.model));
const afterOpus = after.find((t) => /opus/.test(t.model));
assert.equal(afterOpus.input_tokens, beforeOpus.input_tokens);
assert.equal(afterOpus.output_tokens, beforeOpus.output_tokens);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
});
describe("archive helpers", () => {
it("isPathInside rejects traversal", () => {
const root = path.resolve("/tmp/ccam-root");
assert.equal(archive.isPathInside(root, "/tmp/ccam-root/ok.jsonl"), true);
assert.equal(archive.isPathInside(root, "/tmp/other/bad.jsonl"), false);
assert.equal(archive.isPathInside(root, "/tmp/ccam-root/../escape"), false);
});
it("safeJoin rejects absolute and traversal entries", () => {
const root = path.resolve("/tmp/ccam-root");
assert.equal(archive.safeJoin(root, "/etc/passwd"), path.join(root, "etc/passwd"));
assert.equal(archive.safeJoin(root, "../escape.txt"), null);
assert.equal(archive.safeJoin(root, "deep/../../escape"), null);
assert.ok(archive.safeJoin(root, "good/file.jsonl").startsWith(root));
});
it("detectKind handles common extensions", () => {
assert.equal(archive.detectKind("a.jsonl"), "jsonl");
assert.equal(archive.detectKind("a.meta.json"), "meta");
assert.equal(archive.detectKind("a.zip"), "zip");
assert.equal(archive.detectKind("a.tar"), "tar");
assert.equal(archive.detectKind("a.tar.gz"), "tgz");
assert.equal(archive.detectKind("a.tgz"), "tgz");
assert.equal(archive.detectKind("a.gz"), "gz");
assert.equal(archive.detectKind("random.bin"), "unknown");
});
it("extractGzSingle decompresses plain gz", async () => {
const dest = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-gz-"));
const src = path.join(dest, "sample.jsonl.gz");
fs.writeFileSync(src, zlib.gzipSync(Buffer.from('{"ok":true}\n')));
try {
const result = await archive.extractGzSingle(src, dest);
assert.equal(result.extracted, 1);
assert.ok(fs.existsSync(path.join(dest, "sample.jsonl")));
} finally {
fs.rmSync(dest, { recursive: true, force: true });
}
});
});
describe("importFromDirectory directly", () => {
it("reports progress and never throws on empty dirs", async () => {
const empty = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-empty-"));
try {
const events = [];
const counters = await importHistory.importFromDirectory({ db, stmts }, empty, {
onProgress: (p) => events.push(p.phase),
});
assert.equal(counters.filesScanned, 0);
assert.ok(events.includes("complete"));
} finally {
fs.rmSync(empty, { recursive: true, force: true });
}
});
it("matches the legacy importer's token totals on the same fixtures", async () => {
// Clean any tokens from prior tests for a fresh comparison.
const freshSession = "cccccccc-3333-4333-8333-cccccccccccc";
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-fresh-"));
const projDir = path.join(root, "-Users-demo-fresh");
fs.mkdirSync(projDir, { recursive: true });
fs.writeFileSync(
path.join(projDir, `${freshSession}.jsonl`),
fixtureLines(freshSession, "/Users/demo/fresh", "claude-haiku-4-5", 7, 3)
.map((o) => JSON.stringify(o))
.join("\n")
);
try {
await importHistory.importFromDirectory({ db, stmts }, root);
const tok = stmts.getTokensBySession.all(freshSession);
const haiku = tok.find((t) => /haiku/.test(t.model));
assert.ok(haiku);
assert.equal(haiku.input_tokens, 14); // 7 * 2 messages
assert.equal(haiku.output_tokens, 6); // 3 * 2 messages
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
it("snapshots the transcript into the data dir so it survives Claude Code pruning", async () => {
// Regression: the Conversation tab reads JSONL live from ~/.claude/projects,
// but Claude Code deletes session files older than cleanupPeriodDays
// (default 30 days). Import must snapshot the transcript into the
// dashboard's own data dir so the conversation survives that deletion.
const prevDataDir = process.env.DASHBOARD_DATA_DIR;
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-data-"));
process.env.DASHBOARD_DATA_DIR = dataDir;
const src = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-src-"));
const sessionId = "dddddddd-4444-4444-8444-dddddddddddd";
fs.writeFileSync(
path.join(src, `${sessionId}.jsonl`),
fixtureLines(sessionId, "/Users/demo/snap", "claude-opus-4-8", 5, 3)
.map((o) => JSON.stringify(o))
.join("\n")
);
try {
await importHistory.importFromDirectory({ db, stmts }, src);
const snapshot = path.join(dataDir, "transcripts", `${sessionId}.jsonl`);
assert.ok(fs.existsSync(snapshot), "transcript should be snapshotted into the data dir");
assert.ok(
fs.readFileSync(snapshot, "utf8").includes('"text":"ok"'),
"snapshot should contain the original conversation"
);
// And the read route should resolve it via the snapshot helper.
const { getSnapshotTranscriptPath } = require("../lib/claude-home");
assert.equal(getSnapshotTranscriptPath(sessionId), snapshot);
} finally {
if (prevDataDir === undefined) delete process.env.DASHBOARD_DATA_DIR;
else process.env.DASHBOARD_DATA_DIR = prevDataDir;
fs.rmSync(src, { recursive: true, force: true });
fs.rmSync(dataDir, { recursive: true, force: true });
}
});
});
describe("POST /api/import/rescan", () => {
it("runs without crashing even when default projects dir is missing", async () => {
// We can't mutate the real projects dir, but we can assert the endpoint
// always returns a JSON envelope regardless of whether it found anything.
const res = await post("/api/import/rescan");
assert.equal(res.status, 200);
assert.equal(res.body.ok, true);
assert.equal(typeof res.body.imported, "number");
assert.equal(typeof res.body.skipped, "number");
});
});
// ────────────────────────────────────────────────────────────────────────────
// Hardening tests
// ────────────────────────────────────────────────────────────────────────────
describe("tar path-traversal hardening", () => {
it("extractTar rejects entries with ../ segments", async () => {
const tar = require("tar");
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-tar-bad-"));
const stageDir = path.join(tmp, "stage");
const targetDir = path.join(tmp, "target");
fs.mkdirSync(stageDir, { recursive: true });
fs.mkdirSync(targetDir, { recursive: true });
// Build a tar that tries to write "../escape.jsonl".
const inner = path.join(stageDir, "legit.jsonl");
fs.writeFileSync(inner, '{"ok":true}\n');
const tarPath = path.join(tmp, "evil.tar");
await tar.c({ file: tarPath, cwd: stageDir, prefix: "../" }, ["legit.jsonl"]);
try {
await archive.extractTar(tarPath, targetDir);
// Anything extracted must remain inside targetDir.
const walked = [];
(function walk(d) {
for (const ent of fs.readdirSync(d, { withFileTypes: true })) {
const p = path.join(d, ent.name);
if (ent.isDirectory()) walk(p);
else walked.push(p);
}
})(targetDir);
for (const p of walked) {
assert.ok(
path.resolve(p).startsWith(path.resolve(targetDir) + path.sep) ||
path.resolve(p) === path.resolve(targetDir),
`traversal escape detected: ${p}`
);
}
// Nothing should exist one level above targetDir with name escape.jsonl
assert.equal(fs.existsSync(path.join(tmp, "escape.jsonl")), false);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
});
describe("extraction size cap", () => {
it("extractGzSingle aborts past MAX_EXTRACT_BYTES", async () => {
const prev = process.env.CCAM_IMPORT_MAX_EXTRACT_BYTES;
process.env.CCAM_IMPORT_MAX_EXTRACT_BYTES = "128";
// Re-require to pick up the lowered limit for this one check.
delete require.cache[require.resolve("../lib/archive")];
const localArchive = require("../lib/archive");
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-bomb-"));
const gzPath = path.join(tmp, "bomb.jsonl.gz");
// 2 KB of zeros compresses to a few bytes — decompressing blows past 128 B.
fs.writeFileSync(gzPath, zlib.gzipSync(Buffer.alloc(2048, 0)));
try {
await assert.rejects(
() => localArchive.extractGzSingle(gzPath, tmp),
(err) => err.code === "EXTRACTION_LIMIT_EXCEEDED"
);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
if (prev === undefined) delete process.env.CCAM_IMPORT_MAX_EXTRACT_BYTES;
else process.env.CCAM_IMPORT_MAX_EXTRACT_BYTES = prev;
// Restore the module with production limits for subsequent tests.
delete require.cache[require.resolve("../lib/archive")];
require("../lib/archive");
}
});
});
describe("orphan subagent inference", () => {
it("attaches subagent via Layout 2: <proj>/subagents/<sessionId>/agent.jsonl", async () => {
const orphanSession = "dddddddd-4444-4444-8444-dddddddddddd";
// Seed a parent session first.
const seedRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-orphan-seed-"));
const seedProj = path.join(seedRoot, "project");
fs.mkdirSync(seedProj, { recursive: true });
fs.writeFileSync(
path.join(seedProj, `${orphanSession}.jsonl`),
fixtureLines(orphanSession, "/Users/demo/orphan", "claude-opus-4-7", 5, 5)
.map((o) => JSON.stringify(o))
.join("\n")
);
await importHistory.importFromDirectory({ db, stmts }, seedRoot);
assert.ok(stmts.getSession.get(orphanSession), "parent session must exist before orphan pass");
// Now create an "orphan" subagent tree in the non-standard layout.
const orphanRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-orphan-"));
const orphanLayoutDir = path.join(orphanRoot, "project", "subagents", orphanSession);
fs.mkdirSync(orphanLayoutDir, { recursive: true });
const subAgentId = "agent-xyz";
fs.writeFileSync(
path.join(orphanLayoutDir, `${subAgentId}.jsonl`),
[
{ type: "user", timestamp: "2026-04-18T12:00:00.000Z", message: { content: "hi" } },
{
type: "assistant",
timestamp: "2026-04-18T12:00:00.000Z",
message: {
model: "claude-opus-4-7",
content: [{ type: "text", text: "ok" }],
usage: { input_tokens: 1, output_tokens: 1 },
},
},
]
.map((o) => JSON.stringify(o))
.join("\n")
);
try {
const before = db
.prepare("SELECT COUNT(*) as c FROM agents WHERE session_id = ?")
.get(orphanSession).c;
await importHistory.importFromDirectory({ db, stmts }, orphanRoot);
const after = db
.prepare("SELECT COUNT(*) as c FROM agents WHERE session_id = ?")
.get(orphanSession).c;
assert.ok(after > before, "orphan subagent should attach under known session");
} finally {
fs.rmSync(seedRoot, { recursive: true, force: true });
fs.rmSync(orphanRoot, { recursive: true, force: true });
}
});
});
describe("concurrent scan-path requests", () => {
it("two concurrent imports of different folders both succeed without clobbering", async () => {
const sessA = "eeeeeeee-5555-4555-8555-eeeeeeeeeeee";
const sessB = "ffffffff-6666-4666-8666-ffffffffffff";
const rootA = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-concurrent-a-"));
const rootB = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-concurrent-b-"));
fs.mkdirSync(path.join(rootA, "-Users-demo-a"), { recursive: true });
fs.mkdirSync(path.join(rootB, "-Users-demo-b"), { recursive: true });
fs.writeFileSync(
path.join(rootA, "-Users-demo-a", `${sessA}.jsonl`),
fixtureLines(sessA, "/Users/demo/a", "claude-opus-4-7", 3, 2)
.map((o) => JSON.stringify(o))
.join("\n")
);
fs.writeFileSync(
path.join(rootB, "-Users-demo-b", `${sessB}.jsonl`),
fixtureLines(sessB, "/Users/demo/b", "claude-sonnet-4-6", 4, 1)
.map((o) => JSON.stringify(o))
.join("\n")
);
try {
const [rA, rB] = await Promise.all([
post("/api/import/scan-path", { path: rootA }),
post("/api/import/scan-path", { path: rootB }),
]);
assert.equal(rA.status, 200);
assert.equal(rB.status, 200);
assert.ok(stmts.getSession.get(sessA), "session A should be imported");
assert.ok(stmts.getSession.get(sessB), "session B should be imported");
} finally {
fs.rmSync(rootA, { recursive: true, force: true });
fs.rmSync(rootB, { recursive: true, force: true });
}
});
});