/** * @file Tests for hook-payload trimming (`server/lib/event-payload.js`): what is * dropped and kept, that every event insert goes through `trim_event_data`, that * the duplicate guard still matches a trimmed payload, and the one-time sweep. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it } = require("node:test"); const assert = require("node:assert/strict"); const path = require("path"); const os = require("os"); const TMP = path.join(os.tmpdir(), `event-payload-${Date.now()}-${process.pid}`); process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db"); process.env.CLAUDE_HOME = path.join(TMP, "home"); process.env.DASHBOARD_DATA_DIR = path.join(TMP, "data"); const { trimHookPayload, trimEventData, sweepEventPayloads, STR_CAP, } = require("../lib/event-payload"); const { db, stmts } = require("../db"); const big = (n, ch = "x") => ch.repeat(n); describe("trimHookPayload", () => { it("drops an Edit's whole-file mirror and keeps the patch", () => { const patch = [{ oldStart: 1, lines: ["-a", "+b"] }]; const out = trimHookPayload({ tool_name: "Edit", tool_input: { file_path: "/a.js", old_string: "a", new_string: "b" }, tool_response: { originalFile: big(50), oldString: "a", newString: "b", structuredPatch: patch, }, }); assert.equal(out.tool_response.originalFile, undefined); assert.equal(out.tool_response.oldString, undefined); assert.deepEqual(out.tool_response.structuredPatch, patch); assert.equal(out.tool_input.old_string, "a"); assert.deepEqual(out._trimmed.sort(), [ "tool_response.newString", "tool_response.oldString", "tool_response.originalFile", ]); }); it("drops a Read's file body but keeps its counts", () => { const out = trimHookPayload({ tool_name: "Read", tool_response: { type: "text", file: { filePath: "/a", content: "hi", numLines: 1, totalLines: 9 }, }, }); assert.deepEqual(out.tool_response.file, { filePath: "/a", numLines: 1, totalLines: 9 }); assert.deepEqual(out._trimmed, ["tool_response.file.content"]); }); it("keeps another tool's `content` (Grep matches), only capping it", () => { const out = trimHookPayload({ tool_name: "Grep", tool_response: { content: "a:1:x" } }); assert.equal(out.tool_response.content, "a:1:x"); assert.equal(out._trimmed, undefined); }); it("caps long strings to head + tail, idempotently, and leaves other fields", () => { const stdout = `HEAD${big(STR_CAP * 3)}TAIL`; const data = { tool_name: "Bash", prompt_id: "p", tool_response: { stdout, stderr: "", interrupted: false }, tool_input: { command: "ls" }, }; const once = trimHookPayload(data); assert.ok(once.tool_response.stdout.length <= STR_CAP); assert.ok(once.tool_response.stdout.startsWith("HEAD")); assert.ok(once.tool_response.stdout.endsWith("TAIL")); assert.equal(once.tool_response.interrupted, false); assert.equal(once.prompt_id, "p"); assert.deepEqual(trimHookPayload(once), once); assert.equal(data.tool_response.stdout, stdout, "input not mutated"); }); it("caps a string-typed tool_response", () => { const out = trimHookPayload({ tool_name: "Read", tool_response: big(STR_CAP * 2) }); assert.ok(out.tool_response.length <= STR_CAP); }); }); describe("trimEventData", () => { it("passes short, null and non-JSON input through", () => { assert.equal(trimEventData(null), null); assert.equal(trimEventData('{"a":1}'), '{"a":1}'); const junk = big(STR_CAP + 10, "{"); assert.equal(trimEventData(junk), junk); }); it("returns a long payload it has nothing to trim byte for byte (no number rounding)", () => { // 12345678901234567890 does not survive a JSON.parse/stringify round trip. const json = `{"tool_name":"Bash","id":12345678901234567890,"tool_input":{"a":"${big( STR_CAP - 10 )}","b":"${big(STR_CAP - 10)}"}}`; assert.equal(trimEventData(json), json); }); }); describe("event inserts", () => { const payload = JSON.stringify({ session_id: "s-trim", tool_name: "Edit", tool_response: { originalFile: big(STR_CAP * 4) }, }); it("store the trimmed payload and the duplicate guard still matches it", () => { db.prepare("INSERT OR IGNORE INTO sessions (id) VALUES ('s-trim')").run(); stmts.insertEvent.run("s-trim", null, "PostToolUse", "Edit", "x", payload); const row = db.prepare("SELECT data FROM events WHERE session_id = 's-trim'").get(); assert.ok(row.data.length < 500); assert.ok(stmts.findRecentDuplicateEvent.get("s-trim", "PostToolUse", payload)); }); it("the sweep trims rows stored untrimmed", async () => { db.prepare( "INSERT INTO events (session_id, event_type, data, created_at) VALUES ('s-trim', 'PostToolUse', ?, '2026-01-01T00:00:00Z')" ).run(payload); const r = await sweepEventPayloads(db, { chunk: 1 }); assert.equal(r.rows, 1); assert.ok(r.bytesSaved > STR_CAP * 3); const again = await sweepEventPayloads(db); assert.equal(again.rows, 0); }); it("the sweep commits once a chunk's bytes pass the budget, and still trims every row", async () => { db.prepare("INSERT OR IGNORE INTO sessions (id) VALUES ('s-sweep')").run(); const ins = db.prepare( "INSERT INTO events (session_id, event_type, data, created_at) VALUES ('s-sweep', 'PostToolUse', ?, '2026-01-01T00:00:00Z')" ); for (let i = 0; i < 5; i++) ins.run(payload); let txns = 0; const counting = { prepare: (sql) => db.prepare(sql), transaction: (fn) => { const t = db.transaction(fn); return (...a) => (txns++, t(...a)); }, }; // Each payload is ~32 KB; a 1-byte budget closes the transaction after every row. const r = await sweepEventPayloads(counting, { chunk: 200, maxBytes: 1 }); assert.equal(r.rows, 5); assert.ok(txns >= 5, `expected a transaction per row, got ${txns}`); }); });