Files

107 lines
4.3 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { CatalogRetriever } from "../src/ai/retrieval/catalog.js";
describe("CatalogRetriever", () => {
it("resolves an exact SKU token regardless of case", async () => {
const r = new CatalogRetriever();
const hits = await r.retrieve("How much is CDD01?", 3);
expect(hits.length).toBeGreaterThan(0);
expect(hits[0].text).toContain("得到 全平台");
expect(hits[0].source).toBe("https://www.digikedai.com/products/cdd01/");
});
it("surfaces the FREE twin when a paid SKU is queried", async () => {
// CZH01 has a generated trial twin FREECZH01; asking about the paid SKU
// must expose the twin as a fact so the LLM doesn't guess from rules.
const r = new CatalogRetriever();
const hits = await r.retrieve("CZH01 有免费版吗", 5);
const texts = hits.map((h) => h.text);
expect(texts.some((t) => t.includes("SKU CZH01"))).toBe(true);
expect(texts.some((t) => t.includes("SKU FREECZH01"))).toBe(true);
// Paid product ranks first (1.0), twin second (0.95).
expect(hits[0].text).toContain("SKU CZH01");
});
it("returns a FREE SKU's own entry when queried directly", async () => {
const r = new CatalogRetriever();
const hits = await r.retrieve("FREECZH01", 3);
expect(hits.length).toBeGreaterThan(0);
expect(hits[0].text).toContain("FREECZH01");
});
it("does not fabricate a twin when the paid SKU has none", async () => {
// Only CZP01/CZP02 lack twins (254 paid / 252 free, verified 2026-08-31).
const r = new CatalogRetriever();
const hits = await r.retrieve("CZP01", 3);
const texts = hits.map((h) => h.text);
expect(texts.some((t) => t.includes("CZP01"))).toBe(true);
expect(texts.some((t) => t.includes("FREECZP01"))).toBe(false);
});
it("matches a lowercased SKU token", async () => {
const r = new CatalogRetriever();
const hits = await r.retrieve("tell me about cdd01", 3);
expect(hits.length).toBeGreaterThan(0);
expect(hits[0].text).toContain("得到 全平台");
});
it("matches by product-name substring", async () => {
const r = new CatalogRetriever();
const hits = await r.retrieve("喜马拉雅", 5);
expect(hits.length).toBeGreaterThan(1);
expect(hits[0].text).toContain("喜马拉雅");
});
it("strips chinese question particles", async () => {
const r = new CatalogRetriever();
const hits = await r.retrieve("有没有喜马拉雅?", 5);
expect(hits.length).toBeGreaterThan(0);
expect(hits[0].text).toContain("喜马拉雅");
});
it("finds both when query mixes SKU and name", async () => {
const r = new CatalogRetriever();
const hits = await r.retrieve("CDD01 喜马拉雅", 5);
const texts = hits.map((h) => h.text);
expect(texts.some((t) => t.includes("CDD01"))).toBe(true);
expect(texts.some((t) => t.includes("喜马拉雅"))).toBe(true);
});
it("matches partial english product names", async () => {
const r = new CatalogRetriever();
const hits = await r.retrieve("amazon ses", 3);
expect(hits.length).toBeGreaterThan(0);
expect(hits[0].text).toContain("Amazon SES");
});
it("caps results at topK", async () => {
const r = new CatalogRetriever();
const hits = await r.retrieve("喜马拉雅", 2);
expect(hits.length).toBeLessThanOrEqual(2);
});
it("returns trial flag and lowercase product URL for FREE-prefixed SKUs", async () => {
// Free items use SKU = FREE + original paid SKU (e.g. FREECXM04 from CXM04,
// 2026-08-31 plan); the catalogue may legitimately lack a matching hit until content exists.
const r = new CatalogRetriever();
const hits = await r.retrieve("FREECXM04 免费 课堂", 5);
// When no free SKU exists yet, we must not crash and return nothing.
expect(Array.isArray(hits)).toBe(true);
});
it("returns empty for non-catalogue queries", async () => {
const r = new CatalogRetriever();
const hits = await r.retrieve("what is your refund policy?", 3);
expect(hits).toEqual([]);
});
it("does not leak prices or storage paths in results", async () => {
const r = new CatalogRetriever();
const hits = await r.retrieve("CDD01 喜马拉雅", 20);
for (const h of hits) {
expect(h.text).not.toMatch(/RM\d/);
expect(h.text).not.toContain("/Class/");
expect(h.text).not.toContain("/volume1");
}
});
});