Files
diegosouzapw--omniroute/tests/unit/memory-cache-safe-injection.test.ts
2026-07-13 13:39:12 +08:00

106 lines
4.8 KiB
TypeScript

/**
* Tests for #3890: prompt-cache misses when memory injection is enabled.
*
* When the client uses prompt caching (cache_control breakpoints), the previous behavior
* prepended the (per-query, varying) memory message at index 0 — shifting the entire
* cacheable prefix and forcing a cache miss on every turn. With `cacheSafe`, memory is
* inserted just before the last user message so the cacheable prefix stays byte-stable.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { injectMemory } from "../../src/lib/memory/injection.ts";
import type { ChatRequest } from "../../src/lib/memory/injection.ts";
import type { Memory } from "../../src/lib/memory/types.ts";
function mem(content: string): Memory {
return {
id: `mem-${content}`,
content,
type: "factual" as any,
apiKeyId: "k",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
importance: 0.5,
};
}
function multiTurn(): ChatRequest {
return {
model: "anthropic/claude-sonnet-4-6",
messages: [
{ role: "system", content: "SYSTEM PROMPT", cache_control: { type: "ephemeral" } } as any,
{ role: "user", content: "turn 1 question" },
{ role: "assistant", content: "turn 1 answer" },
{ role: "user", content: "turn 2 question" },
],
};
}
describe("injectMemory cache-safe positioning (#3890)", () => {
it("default (cacheSafe off) prepends memory at index 0 — unchanged legacy behavior", () => {
const out = injectMemory(multiTurn(), [mem("dark mode")], "anthropic");
assert.equal(out.messages[0].role, "system");
assert.ok(out.messages[0].content.includes("Memory context"));
assert.equal(out.messages[1].content, "SYSTEM PROMPT");
});
it("cacheSafe inserts memory just before the last user message, preserving the prefix", () => {
const req = multiTurn();
const prefixBefore = JSON.stringify(req.messages.slice(0, 3)); // sys, u1, a1
const out = injectMemory(req, [mem("dark mode")], "anthropic", { cacheSafe: true });
// The cacheable prefix (system + prior turns up to the last assistant) is byte-identical.
assert.equal(JSON.stringify(out.messages.slice(0, 3)), prefixBefore);
// Memory is injected right before the last user message...
assert.equal(out.messages[3].role, "system");
assert.ok(out.messages[3].content.includes("Memory context"));
// ...and the last user message is preserved at the tail.
assert.equal(out.messages[4].content, "turn 2 question");
assert.equal(out.messages.length, 5);
// Memory must NOT be at index 0 (that is what broke caching).
assert.notEqual(out.messages[0].content, out.messages[3].content);
assert.equal(out.messages[0].content, "SYSTEM PROMPT");
});
it("cacheSafe keeps the cacheable prefix identical across two turns despite different memories", () => {
// Turn 1: [sys, u1]; Turn 2: [sys, u1, a1, u2]. Different memories retrieved per query.
// Same conversation, observed on two consecutive turns.
const turn1: ChatRequest = {
model: "anthropic/claude-sonnet-4-6",
messages: [
{ role: "system", content: "SYSTEM PROMPT", cache_control: { type: "ephemeral" } } as any,
{ role: "user", content: "turn 1 question" },
],
};
const turn2 = multiTurn();
const out1 = injectMemory(turn1, [mem("A")], "anthropic", { cacheSafe: true });
const out2 = injectMemory(turn2, [mem("B")], "anthropic", { cacheSafe: true });
// The cache-breakpoint-bearing system message stays at the head, byte-identical, in
// both turns (and is NOT displaced by the per-query memory) — so the prompt cache
// created on turn 1 still matches on turn 2. (Memory is inserted before the last user
// turn: [SYS, MEM_A, u1] and [SYS, u1, a1, MEM_B, u2] respectively.)
assert.deepEqual(out1.messages[0], out2.messages[0]);
assert.equal(out1.messages[0].content, "SYSTEM PROMPT");
assert.equal((out1.messages[0] as any).cache_control?.type, "ephemeral");
// In turn 2 the earlier turns up to the last assistant are preserved before memory.
assert.equal(out2.messages[1].content, "turn 1 question");
assert.equal(out2.messages[2].content, "turn 1 answer");
assert.ok(out2.messages[3].content.includes("Memory context"));
assert.equal(out2.messages[4].content, "turn 2 question");
});
it("cacheSafe falls back to leading injection when there is no user message", () => {
const req: ChatRequest = {
model: "anthropic/claude-sonnet-4-6",
messages: [{ role: "system", content: "SYS" }],
};
const out = injectMemory(req, [mem("x")], "anthropic", { cacheSafe: true });
assert.equal(out.messages[0].role, "system");
assert.ok(out.messages[0].content.includes("Memory context"));
assert.equal(out.messages[1].content, "SYS");
});
});