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

211 lines
7.0 KiB
JavaScript

/**
* @file Tests that TranscriptCache caps the size of each per-entry array
* (turnDurations / errors / compaction.entries / usageExtras.*) so a long
* session cannot grow a single cache entry without bound.
* @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 fs = require("fs");
const path = require("path");
const os = require("os");
const TranscriptCache = require("../lib/transcript-cache");
let tmpDir;
before(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tc-bounded-"));
});
after(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
function writeJsonl(name, lines) {
const p = path.join(tmpDir, name);
fs.writeFileSync(p, lines.map((l) => JSON.stringify(l)).join("\n") + "\n");
return p;
}
describe("TranscriptCache._trimArray", () => {
it("exists and trims arrays to the given max length, keeping the tail", () => {
const cache = new TranscriptCache();
assert.equal(typeof cache._trimArray, "function");
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
cache._trimArray(arr, 3);
assert.deepEqual(arr, [8, 9, 10]);
});
it("is a no-op when array is within the cap", () => {
const cache = new TranscriptCache();
const arr = [1, 2, 3];
cache._trimArray(arr, 5);
assert.deepEqual(arr, [1, 2, 3]);
});
it("handles null/undefined safely", () => {
const cache = new TranscriptCache();
assert.doesNotThrow(() => cache._trimArray(null, 5));
assert.doesNotThrow(() => cache._trimArray(undefined, 5));
});
});
describe("TranscriptCache.extract — array caps", () => {
it("caps turnDurations at MAX_ARRAY_LEN on full read, keeping the tail", () => {
// 1500 turn_duration entries, ascending timestamps
const lines = [];
for (let i = 0; i < 1500; i++) {
lines.push({
type: "system",
subtype: "turn_duration",
durationMs: i + 1,
timestamp: new Date(2026, 0, 1, 0, 0, i).toISOString(),
});
}
const p = writeJsonl("turns.jsonl", lines);
process.env.TRANSCRIPT_CACHE_MAX_ARRAY_LEN = "100";
// Re-require fresh to pick up the env override
delete require.cache[require.resolve("../lib/transcript-cache")];
const Fresh = require("../lib/transcript-cache");
const cache = new Fresh();
const result = cache.extract(p);
assert.ok(result, "expected non-null result");
assert.equal(result.turnDurations.length, 100);
// Tail-kept: durationMs should be 1401..1500
assert.equal(result.turnDurations[0].durationMs, 1401);
assert.equal(result.turnDurations[99].durationMs, 1500);
delete process.env.TRANSCRIPT_CACHE_MAX_ARRAY_LEN;
delete require.cache[require.resolve("../lib/transcript-cache")];
});
it("caps errors and compaction.entries on full read", () => {
const lines = [];
for (let i = 0; i < 300; i++) {
lines.push({
isApiErrorMessage: true,
error: "rate_limit",
message: { content: [{ text: `err-${i}` }] },
timestamp: new Date(2026, 0, 1, 0, 0, i).toISOString(),
});
lines.push({
isCompactSummary: true,
uuid: `c-${i}`,
timestamp: new Date(2026, 0, 1, 0, 0, i).toISOString(),
});
}
const p = writeJsonl("err-compact.jsonl", lines);
process.env.TRANSCRIPT_CACHE_MAX_ARRAY_LEN = "50";
delete require.cache[require.resolve("../lib/transcript-cache")];
const Fresh = require("../lib/transcript-cache");
const cache = new Fresh();
const result = cache.extract(p);
assert.equal(result.errors.length, 50);
assert.equal(result.compaction.entries.length, 50);
assert.equal(
result.compaction.count,
300,
"count must reflect ALL parsed entries, not just retained"
);
delete process.env.TRANSCRIPT_CACHE_MAX_ARRAY_LEN;
delete require.cache[require.resolve("../lib/transcript-cache")];
});
it("incremental merge respects cap (append to existing capped entry)", () => {
process.env.TRANSCRIPT_CACHE_MAX_ARRAY_LEN = "100";
delete require.cache[require.resolve("../lib/transcript-cache")];
const Fresh = require("../lib/transcript-cache");
const cache = new Fresh();
// First batch: 80 turns
const linesA = [];
for (let i = 0; i < 80; i++) {
linesA.push({
type: "system",
subtype: "turn_duration",
durationMs: i + 1,
timestamp: new Date(2026, 0, 1, 0, 0, i).toISOString(),
});
}
const p = writeJsonl("incr.jsonl", linesA);
let result = cache.extract(p);
assert.equal(result.turnDurations.length, 80);
// Append 50 more — total 130, cache should retain only last 100
const fd = fs.openSync(p, "a");
for (let i = 80; i < 130; i++) {
const line =
JSON.stringify({
type: "system",
subtype: "turn_duration",
durationMs: i + 1,
timestamp: new Date(2026, 0, 1, 0, 0, i).toISOString(),
}) + "\n";
fs.writeSync(fd, line);
}
fs.closeSync(fd);
result = cache.extract(p);
assert.equal(result.turnDurations.length, 100);
// Tail check: last entry should be durationMs=130
assert.equal(result.turnDurations[99].durationMs, 130);
// Head should be durationMs=31 (130 - 100 + 1)
assert.equal(result.turnDurations[0].durationMs, 31);
delete process.env.TRANSCRIPT_CACHE_MAX_ARRAY_LEN;
delete require.cache[require.resolve("../lib/transcript-cache")];
});
});
describe("TranscriptCache._set — single storage", () => {
it("cache entry contains ONLY {mtimeMs, size, bytesRead, result}", () => {
const p = writeJsonl("single.jsonl", [
{
type: "system",
subtype: "turn_duration",
durationMs: 100,
timestamp: "2026-01-01T00:00:00Z",
},
]);
const cache = new TranscriptCache();
cache.extract(p);
const entry = cache._cache.get(p);
assert.ok(entry, "entry should be cached");
const keys = Object.keys(entry).sort();
assert.deepEqual(keys, ["bytesRead", "mtimeMs", "result", "size"]);
});
it("does not store duplicate top-level errors/turnDurations/compaction", () => {
const p = writeJsonl("dup.jsonl", [
{
type: "system",
subtype: "turn_duration",
durationMs: 1,
timestamp: "2026-01-01T00:00:00Z",
},
{
isApiErrorMessage: true,
error: "x",
message: { content: [{ text: "y" }] },
timestamp: "2026-01-01T00:00:01Z",
},
{ isCompactSummary: true, uuid: "u1", timestamp: "2026-01-01T00:00:02Z" },
]);
const cache = new TranscriptCache();
cache.extract(p);
const entry = cache._cache.get(p);
assert.equal(entry.errors, undefined);
assert.equal(entry.turnDurations, undefined);
assert.equal(entry.compaction, undefined);
assert.equal(entry.tokensByModel, undefined);
assert.equal(entry.usageExtras, undefined);
assert.equal(entry.thinkingBlockCount, undefined);
assert.equal(entry.latestModel, undefined);
});
});