chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user