107 lines
4.3 KiB
TypeScript
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");
|
|
}
|
|
});
|
|
}); |