Files
AgentHub/server/__tests__/event-payload.test.js
T
nntrivi2001 215cae7393 feat(db): trim stored hook payloads; price the session list like a session
Analytics parity, phase 1 (data hygiene), from the analytics-parity session.

events.data is now stored through the SQL function trim_event_data(?)
(server/lib/event-payload.js, registered in server/db.js) on every event
INSERT and in the duplicate guard: Edit/MultiEdit/NotebookEdit/Write drop
the whole-file mirrors (originalFile, oldString/newString, Write content),
Read drops file.content/base64, and any string in tool_input/tool_response
over 8 KB keeps a 6 KB head + 2 KB tail. structuredPatch and every field
outside those two stay whole; _trimmed names what was dropped. The full
text stays in the transcript and its snapshot. Rows stored before this are
trimmed once in the background (sweepEventPayloads, 200 rows per write
transaction, user_version 4). No VACUUM, no row pruned.

The session list now prices every token column /api/pricing/cost/:id does
(speed, geo, tier, the 1h cache write, server-tool fees) through one
tokensForSessions helper, so a row's cost equals the session's own.
2026-10-05 13:13:02 +07:00

129 lines
4.7 KiB
JavaScript

/**
* @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ĩ <vinnt@smartgift.vn>
*/
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);
});
});
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);
});
});