diff --git a/src/index.mjs b/src/index.mjs index b4a1129..ecb07e4 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -87,6 +87,12 @@ export function compact(payload, opts = {}) { requireNonNegativeNumber("maxToolResultTokens", maxToolResultTokens); if (maxTokens != null) requireNonNegativeNumber("maxTokens", maxTokens); requireNonNegativeNumber("keepLastTurns", keepLastTurns, { integer: true }); + if (payload == null) { + return { + payload: { messages: [] }, + report: { beforeTokens: 0, afterTokens: 0, savedTokens: 0, savedPct: 0, actions: [] }, + }; + } const before = analyzePayload(payload, { counter }).totalTokens; const out = clone(payload); const actions = []; diff --git a/test/basic.test.mjs b/test/basic.test.mjs index b637be4..5c4158f 100644 --- a/test/basic.test.mjs +++ b/test/basic.test.mjs @@ -93,6 +93,61 @@ test("compact rejects invalid budgets before transforming the payload", () => { } }); + + +test("analyzePayload null / undefined yields zero tokens", () => { + assert.equal(analyzePayload(null).totalTokens, 0); + assert.equal(analyzePayload(undefined).totalTokens, 0); +}); + +test("analyzePayload system-only object counts system text", () => { + const a = analyzePayload({ system: "You are a careful assistant." }); + assert.ok(a.totalTokens > 0); + assert.ok(a.byRole.system > 0); + assert.equal(a.byRole.user ?? 0, 0); +}); + +test("compact preserves system-only payload under a tight budget", () => { + const payload = { system: "sys", messages: [] }; + const { payload: out, report } = compact(payload, { maxTokens: 1, keepLastTurns: 0 }); + assert.ok(out.system === "sys" || (out.messages && out.messages[0]?.role === "system")); + assert.ok(report.afterTokens >= 0); +}); + +test("compact null payload returns empty messages without throwing", () => { + const { payload: out, report } = compact(null); + assert.deepEqual(out, { messages: [] }); + assert.equal(report.beforeTokens, 0); + assert.equal(report.afterTokens, 0); +}); +test("analyzePayload handles empty messages and whitespace-only content", () => { + const empty = analyzePayload({ messages: [] }); + assert.equal(empty.totalTokens, 0); + + const whitespace = analyzePayload({ + messages: [{ role: "user", content: " \n\t " }], + }); + assert.ok(whitespace.totalTokens >= 0); + + const mixed = analyzePayload({ + system: "", + messages: [ + { role: "user", content: [] }, + { role: "assistant", content: "" }, + ], + }); + assert.ok(mixed.totalTokens >= 0); +}); + +test("compact is a no-op on already-small payloads", () => { + const payload = { + messages: [{ role: "user", content: "hi" }], + }; + const { payload: out, report } = compact(payload, { maxTokens: 10_000 }); + assert.equal(out.messages.length, 1); + assert.ok(report.afterTokens <= report.beforeTokens); +}); + test("analyzePayload empty messages yields zero tokens", () => { const a = analyzePayload({ messages: [] }); assert.equal(a.totalTokens, 0);