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"); } }); });