mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-25 08:32:11 +03:00
Validated on a 4-PR combined board: memory-cache-safe-injection + memory-glm-injection + chatcore-memory-skills-injection 39/39 (node:test), injection.test.ts 23/23 (vitest), typecheck:core clean, check:cycles clean, gates within baseline. Narrows the #3890 mid-array system-splice skip to the exact shape that 400s on Claude Opus 5 (splice point not preceded by a server tool result) instead of disabling the cache-safe optimization for every Claude request — preserves the fast path everywhere it's actually safe. Closes #11290.
233 lines
8.6 KiB
TypeScript
233 lines
8.6 KiB
TypeScript
import { describe, test, expect } from "vitest";
|
|
import {
|
|
injectMemory,
|
|
shouldInjectMemory,
|
|
formatMemoryContext,
|
|
providerSupportsSystemMessage,
|
|
ChatRequest,
|
|
} from "../injection";
|
|
import { Memory, MemoryType } from "../types";
|
|
|
|
function makeMemory(content: string, overrides: Partial<Memory> = {}): Memory {
|
|
return {
|
|
id: "mem-1",
|
|
apiKeyId: "key-1",
|
|
sessionId: "sess-1",
|
|
type: MemoryType.FACTUAL,
|
|
key: "test-key",
|
|
content,
|
|
metadata: {},
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
expiresAt: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeRequest(overrides: Partial<ChatRequest> = {}): ChatRequest {
|
|
return {
|
|
model: "gpt-4",
|
|
messages: [{ role: "user", content: "Hello" }],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("formatMemoryContext", () => {
|
|
test("returns empty string for empty array", () => {
|
|
expect(formatMemoryContext([])).toBe("");
|
|
});
|
|
|
|
test("single memory is formatted with 'Memory context:' prefix", () => {
|
|
const result = formatMemoryContext([makeMemory("User prefers dark mode")]);
|
|
expect(result).toBe("Memory context: User prefers dark mode");
|
|
});
|
|
|
|
test("multiple memories are joined with newline", () => {
|
|
const memories = [makeMemory("fact one"), makeMemory("fact two")];
|
|
const result = formatMemoryContext(memories);
|
|
expect(result).toBe("Memory context: fact one\nfact two");
|
|
});
|
|
|
|
test("trims whitespace from individual memory content", () => {
|
|
const result = formatMemoryContext([makeMemory(" padded content ")]);
|
|
expect(result).toBe("Memory context: padded content");
|
|
});
|
|
|
|
test("filters out blank memories", () => {
|
|
const memories = [makeMemory("real content"), makeMemory(" ")];
|
|
const result = formatMemoryContext(memories);
|
|
expect(result).toBe("Memory context: real content");
|
|
});
|
|
});
|
|
|
|
describe("providerSupportsSystemMessage", () => {
|
|
test("returns true for null/undefined provider", () => {
|
|
expect(providerSupportsSystemMessage(null)).toBe(true);
|
|
expect(providerSupportsSystemMessage(undefined)).toBe(true);
|
|
});
|
|
|
|
test("returns true for standard providers", () => {
|
|
expect(providerSupportsSystemMessage("openai")).toBe(true);
|
|
expect(providerSupportsSystemMessage("anthropic")).toBe(true);
|
|
expect(providerSupportsSystemMessage("deepseek")).toBe(true);
|
|
expect(providerSupportsSystemMessage("google")).toBe(true);
|
|
});
|
|
|
|
test("returns false for o1 family providers", () => {
|
|
expect(providerSupportsSystemMessage("o1")).toBe(false);
|
|
expect(providerSupportsSystemMessage("o1-mini")).toBe(false);
|
|
expect(providerSupportsSystemMessage("o1-preview")).toBe(false);
|
|
});
|
|
|
|
test("comparison is case-insensitive", () => {
|
|
expect(providerSupportsSystemMessage("O1")).toBe(false);
|
|
expect(providerSupportsSystemMessage("O1-MINI")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("injectMemory — system message injection", () => {
|
|
test("injects memory as system message when provider supports it", () => {
|
|
const request = makeRequest();
|
|
const memories = [makeMemory("User prefers concise answers")];
|
|
const result = injectMemory(request, memories, "openai");
|
|
|
|
expect(result.messages[0].role).toBe("system");
|
|
expect(result.messages[0].content).toBe("Memory context: User prefers concise answers");
|
|
expect(result.messages[1]).toEqual({ role: "user", content: "Hello" });
|
|
});
|
|
|
|
test("preserves existing messages after injected system message", () => {
|
|
const request = makeRequest({
|
|
messages: [
|
|
{ role: "system", content: "You are helpful" },
|
|
{ role: "user", content: "Hello" },
|
|
],
|
|
});
|
|
const memories = [makeMemory("User is an expert developer")];
|
|
const result = injectMemory(request, memories, "anthropic");
|
|
|
|
expect(result.messages).toHaveLength(3);
|
|
expect(result.messages[0].role).toBe("system");
|
|
expect(result.messages[0].content).toContain("Memory context:");
|
|
expect(result.messages[1]).toEqual({ role: "system", content: "You are helpful" });
|
|
expect(result.messages[2]).toEqual({ role: "user", content: "Hello" });
|
|
});
|
|
|
|
test("does not mutate the original request", () => {
|
|
const request = makeRequest();
|
|
const originalMessages = [...request.messages];
|
|
const memories = [makeMemory("Some fact")];
|
|
injectMemory(request, memories, "openai");
|
|
|
|
expect(request.messages).toEqual(originalMessages);
|
|
});
|
|
|
|
test("preserves all other request fields", () => {
|
|
const request = makeRequest({ temperature: 0.7, max_tokens: 256, stream: true });
|
|
const memories = [makeMemory("fact")];
|
|
const result = injectMemory(request, memories, "openai");
|
|
|
|
expect(result.temperature).toBe(0.7);
|
|
expect(result.max_tokens).toBe(256);
|
|
expect(result.stream).toBe(true);
|
|
expect(result.model).toBe("gpt-4");
|
|
});
|
|
});
|
|
|
|
describe("injectMemory — message prefix fallback", () => {
|
|
test("injects memory as first user message for o1 provider", () => {
|
|
const request = makeRequest();
|
|
const memories = [makeMemory("User context detail")];
|
|
const result = injectMemory(request, memories, "o1");
|
|
|
|
expect(result.messages[0].role).toBe("user");
|
|
expect(result.messages[0].content).toBe("Memory context: User context detail");
|
|
expect(result.messages[1]).toEqual({ role: "user", content: "Hello" });
|
|
});
|
|
|
|
test("injects memory as first user message for o1-mini", () => {
|
|
const request = makeRequest();
|
|
const memories = [makeMemory("Preference")];
|
|
const result = injectMemory(request, memories, "o1-mini");
|
|
|
|
expect(result.messages[0].role).toBe("user");
|
|
expect(result.messages[0].content).toContain("Memory context:");
|
|
});
|
|
});
|
|
|
|
describe("injectMemory — edge cases", () => {
|
|
test("returns original request when memories array is empty", () => {
|
|
const request = makeRequest();
|
|
const result = injectMemory(request, [], "openai");
|
|
expect(result).toBe(request);
|
|
});
|
|
|
|
test("returns original request when memories is null-ish", () => {
|
|
const request = makeRequest();
|
|
const result = injectMemory(request, null as unknown as Memory[], "openai");
|
|
expect(result).toBe(request);
|
|
});
|
|
|
|
test("handles request with empty messages array", () => {
|
|
const request = makeRequest({ messages: [] });
|
|
const memories = [makeMemory("fact")];
|
|
const result = injectMemory(request, memories, "openai");
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
expect(result.messages[0].role).toBe("system");
|
|
});
|
|
|
|
test("handles multiple memories combined into single injection", () => {
|
|
const request = makeRequest();
|
|
const memories = [makeMemory("fact A"), makeMemory("fact B"), makeMemory("fact C")];
|
|
const result = injectMemory(request, memories, "openai");
|
|
|
|
expect(result.messages[0].role).toBe("system");
|
|
expect(result.messages[0].content).toBe("Memory context: fact A\nfact B\nfact C");
|
|
expect(result.messages).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe("injectMemory — Claude-family cache-safe splice gate (#11290)", () => {
|
|
test("does not splice mid-array on anthropic when the last turn before the splice point is plain assistant text", () => {
|
|
const request = makeRequest({
|
|
messages: [
|
|
{ role: "system", content: "SYSTEM PROMPT" },
|
|
{ role: "user", content: "turn 1 question" },
|
|
{ role: "assistant", content: "turn 1 answer" },
|
|
{ role: "user", content: "turn 2 question" },
|
|
],
|
|
});
|
|
const memories = [makeMemory("dark mode")];
|
|
|
|
const result = injectMemory(request, memories, "anthropic", { cacheSafe: true });
|
|
|
|
// The plain-text assistant turn must stay immediately followed by the final user
|
|
// turn — no system message spliced between them (that shape is what Opus 5 rejects
|
|
// with HTTP 400, #11290). Memory is merged into the leading system message instead.
|
|
expect(result.messages).toHaveLength(4);
|
|
expect(result.messages[0].role).toBe("system");
|
|
expect(result.messages[0].content).toContain("Memory context: dark mode");
|
|
expect(result.messages[0].content).toContain("SYSTEM PROMPT");
|
|
expect(result.messages[2]).toEqual({ role: "assistant", content: "turn 1 answer" });
|
|
expect(result.messages[3]).toEqual({ role: "user", content: "turn 2 question" });
|
|
});
|
|
});
|
|
|
|
describe("shouldInjectMemory", () => {
|
|
test("returns true when messages are present and enabled not set", () => {
|
|
const request = makeRequest();
|
|
expect(shouldInjectMemory(request)).toBe(true);
|
|
});
|
|
|
|
test("returns false when config.enabled is false", () => {
|
|
const request = makeRequest();
|
|
expect(shouldInjectMemory(request, { enabled: false })).toBe(false);
|
|
});
|
|
|
|
test("returns false when messages array is empty", () => {
|
|
const request = makeRequest({ messages: [] });
|
|
expect(shouldInjectMemory(request)).toBe(false);
|
|
});
|
|
});
|