215cae7393
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.
129 lines
4.7 KiB
JavaScript
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);
|
|
});
|
|
});
|