150 lines
4.3 KiB
TypeScript
150 lines
4.3 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { MemoSummarizer } from "../src/ai/memory/summarizer.js";
|
|
import {
|
|
shouldSummarize,
|
|
parseQueryList,
|
|
MEMORY_KEY_SUMMARY,
|
|
} from "../src/ai/agent/agent.js";
|
|
import type { LlmProvider } from "../src/ai/providers/llm.js";
|
|
import { createLogger } from "../src/utils/logger.js";
|
|
|
|
class FakeLlm implements LlmProvider {
|
|
smallCalls: { system: string; user: string }[] = [];
|
|
result: string;
|
|
fail = false;
|
|
constructor(result = "Customer speaks Chinese and is interested in CDD01.") {
|
|
this.result = result;
|
|
}
|
|
async chat(): Promise<string> {
|
|
return "main reply";
|
|
}
|
|
async chatSmall(args: {
|
|
system: string;
|
|
messages: { role: "user" | "assistant"; content: string }[];
|
|
}): Promise<string> {
|
|
this.smallCalls.push({
|
|
system: args.system,
|
|
user: args.messages.map((m) => m.content).join("\n"),
|
|
});
|
|
if (this.fail) throw new Error("small model down");
|
|
return this.result;
|
|
}
|
|
}
|
|
|
|
const logger = createLogger("fatal");
|
|
|
|
describe("MemoSummarizer.extract", () => {
|
|
it("returns the plain-text summary from the small model", async () => {
|
|
const llm = new FakeLlm("Customer speaks Chinese, wants CDD01.");
|
|
const s = new MemoSummarizer(llm, logger);
|
|
|
|
const out = await s.extract({
|
|
priorSummary: "old",
|
|
priorQueries: ["what is CDD01?", "price of CDD01"],
|
|
currentTurn: "I want to buy CDD01",
|
|
});
|
|
|
|
expect(out).toBe("Customer speaks Chinese, wants CDD01.");
|
|
expect(llm.smallCalls).toHaveLength(1);
|
|
expect(llm.smallCalls[0].system).toContain("plain-text");
|
|
expect(llm.smallCalls[0].user).toContain("Prior summary: old");
|
|
expect(llm.smallCalls[0].user).toContain("Latest customer message: I want to buy CDD01");
|
|
});
|
|
|
|
it("returns undefined when the small model throws (caller keeps old value)", async () => {
|
|
const llm = new FakeLlm();
|
|
llm.fail = true;
|
|
const s = new MemoSummarizer(llm, logger);
|
|
|
|
const out = await s.extract({
|
|
priorSummary: "old",
|
|
priorQueries: [],
|
|
currentTurn: "hi",
|
|
});
|
|
|
|
expect(out).toBeUndefined();
|
|
});
|
|
|
|
it("returns undefined on empty model output", async () => {
|
|
const llm = new FakeLlm(" ");
|
|
const s = new MemoSummarizer(llm, logger);
|
|
|
|
const out = await s.extract({
|
|
priorQueries: [],
|
|
currentTurn: "hi",
|
|
});
|
|
|
|
expect(out).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("shouldSummarize", () => {
|
|
const base = {
|
|
langChanged: false,
|
|
userText: "anything",
|
|
mem: {},
|
|
priorQueries: [] as string[],
|
|
turn: 3,
|
|
lastSummarizedAt: 0,
|
|
};
|
|
|
|
it("true when the language preference changed", () => {
|
|
expect(shouldSummarize({ ...base, langChanged: true })).toBe(true);
|
|
});
|
|
|
|
it("true on a brand-new SKU token", () => {
|
|
expect(shouldSummarize({ ...base, userText: "how about CDD01?" })).toBe(true);
|
|
});
|
|
|
|
it("false when the SKU was already seen in prior queries", () => {
|
|
expect(
|
|
shouldSummarize({
|
|
...base,
|
|
userText: "how about CDD01 again?",
|
|
priorQueries: ["is CDD01 good?"],
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("false when the SKU is already in the summary", () => {
|
|
expect(
|
|
shouldSummarize({
|
|
...base,
|
|
userText: "CDD01 price?",
|
|
mem: { [MEMORY_KEY_SUMMARY]: "likes CDD01" },
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("true on identity keywords", () => {
|
|
expect(shouldSummarize({ ...base, userText: "我是 Hoelee,想买" })).toBe(true);
|
|
expect(shouldSummarize({ ...base, userText: "I need a refund" })).toBe(true);
|
|
});
|
|
|
|
it("true when >=10 turns since last summary (fallback)", () => {
|
|
expect(
|
|
shouldSummarize({ ...base, turn: 10, lastSummarizedAt: 0 }),
|
|
).toBe(true);
|
|
expect(
|
|
shouldSummarize({ ...base, turn: 9, lastSummarizedAt: 0 }),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("false when nothing matches", () => {
|
|
expect(shouldSummarize({ ...base, userText: "thanks" })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("parseQueryList", () => {
|
|
it("parses a valid JSON array", () => {
|
|
expect(parseQueryList('["a","b"]')).toEqual(["a", "b"]);
|
|
});
|
|
it("returns [] for undefined, empty, or malformed", () => {
|
|
expect(parseQueryList(undefined)).toEqual([]);
|
|
expect(parseQueryList("")).toEqual([]);
|
|
expect(parseQueryList("{not json")).toEqual([]);
|
|
});
|
|
it("drops non-string entries", () => {
|
|
expect(parseQueryList('[1,"a",null]')).toEqual(["a"]);
|
|
});
|
|
}); |