Files
nntrivi2001 198fbb7980 fix: event-loop freezes, plugin-install boot crash, tmux attach/scrollbar, Codex transcripts
- GET /api/workflows: one LEAD() window pass in a read worker (read-pool.js,
  workflow-stats.js); idx_events_session restored, idx_events_tool added
- compat-sqlite gains function/pluck/readonly so a plugin install without a
  better-sqlite3 build boots again; antigravity-usage uses db.js's Database
- install-hooks adds no checkout hooks while the agenthub plugin is enabled
  (events were doubled)
- terminal attach runs the live tmux server's own binary; the pane scrollbar
  gets a visible 2-column style, and claude panes start with
  CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 so tmux keeps history to scroll
- Codex sessions' conversation in GET /api/sessions/:id/transcript
- Codex/Antigravity sessions claimed by their workspace without hooks
- UI-audit follow-ups, docs and OpenAPI
2026-10-05 17:01:05 +07:00

157 lines
5.9 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);
});
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}`);
});
});