import { describe, it, expect, beforeEach } from "vitest"; import { hybrid, clearIndexCache } from "../index.js"; /** * brochure-class calibration fixture. * * Pre-Task-978 the route's `DEFAULT_VECTOR_THRESHOLD` was 0.72, calibrated * against the `savage` query (every score < 0.70, only the relevant row * passes the BM25 carve-out). `brochure` is a different shape: keepers * cluster in [0.8473, 0.8623], noise in [0.7159, 0.8143]. At 0.72 the * floor caught only the lone outlier below 0.72; all 11 in-cluster rows * rendered. raised the default to 0.82, the midpoint of the * 0.033-wide gap between brochure-keepers and brochure-noise. * * This fixture pins both ends of that calibration so a future drop back * toward 0.72 (or a push past 0.85 that would clip the lowest keeper) * fails the test instead of silently regressing /graph search quality. */ interface StubRun { match: (query: string) => boolean; records: Array>; } function record(fields: Record) { return { get: (k: string) => fields[k] }; } function makeStubSession(scripted: StubRun[]) { return { run(query: string) { const hit = scripted.find((s) => s.match(query)); if (!hit) return Promise.resolve({ records: [] }); return Promise.resolve({ records: hit.records.map(record) }); }, } as unknown as import("neo4j-driver").Session; } // Real Agent laptop 2026-05-12 snapshot, `brochure` against // project_embedding for account b3b638d9. BM25 = 0 on every row. const BROCHURE_VECTOR_ROWS = [ { nodeId: "k1", name: "Brochure Production — Alex Pelosi Buchanan", score: 0.8623 }, { nodeId: "k2", name: "Brochure Production — Ross Margetts", score: 0.8523 }, { nodeId: "k3", name: "Brochure Production — Donna Vincent", score: 0.8521 }, { nodeId: "k4", name: "Brochure Production — Paul Leslie", score: 0.8509 }, { nodeId: "k5", name: "Brochure Production — John Savage", score: 0.8474 }, { nodeId: "k6", name: "Brochure Production — Muvin", score: 0.8473 }, { nodeId: "n1", name: "Real Agent Product Development", score: 0.8143 }, { nodeId: "n2", name: "Dan McLeod Partnership", score: 0.7498 }, { nodeId: "n3", name: "Real Agent Lettings — New Vertical", score: 0.7498 }, { nodeId: "n4", name: "Real Agent Lettings", score: 0.7487 }, { nodeId: "n5", name: "Fundraising & Finance", score: 0.7323 }, { nodeId: "n6", name: "Muvin Pilot", score: 0.7240 }, { nodeId: "n7", name: "Go-to-Market & Events", score: 0.7159 }, ]; function makeBrochureSession() { return makeStubSession([ { match: (q) => q.includes("SHOW INDEXES"), records: [{ name: "project_embedding", labelsOrTypes: ["Project"] }], }, { match: (q) => q.includes("db.index.vector.queryNodes"), records: BROCHURE_VECTOR_ROWS.map((r) => ({ nodeId: r.nodeId, nodeLabels: ["Project"], node: { properties: { name: r.name } }, score: r.score, })), }, // BM25 returns nothing — no `brochure` token literal in any name. { match: (q) => q.includes("db.index.fulltext.queryNodes"), records: [] }, ]); } beforeEach(() => { clearIndexCache(); }); describe("brochure-class threshold calibration", () => { it("at threshold 0.72 (pre-fix) renders 12 rows — the bug shape", async () => { const res = await hybrid(makeBrochureSession(), async () => [0.1], { query: "brochure", accountId: "acc-1", limit: 100, labels: ["Project"], expandHops: 0, vectorThreshold: 0.72, }); // 13 rows merged, lowest (0.7159) below 0.72, 12 rendered. expect(res.rawMerged).toBe(13); expect(res.results).toHaveLength(12); expect(res.suppressed).toBe(1); expect(res.bm25Bypass).toBe(0); }); it("at threshold 0.82 (post-fix) renders the 6 keepers exactly", async () => { const res = await hybrid(makeBrochureSession(), async () => [0.1], { query: "brochure", accountId: "acc-1", limit: 100, labels: ["Project"], expandHops: 0, vectorThreshold: 0.82, }); expect(res.rawMerged).toBe(13); expect(res.results).toHaveLength(6); expect(res.suppressed).toBe(7); expect(res.bm25Bypass).toBe(0); expect(res.results.map((r) => r.properties.name as string).sort()).toEqual([ "Brochure Production — Alex Pelosi Buchanan", "Brochure Production — Donna Vincent", "Brochure Production — John Savage", "Brochure Production — Muvin", "Brochure Production — Paul Leslie", "Brochure Production — Ross Margetts", ]); }); it("at threshold 0.85 (too tight) starts clipping keepers — regression guard", async () => { const res = await hybrid(makeBrochureSession(), async () => [0.1], { query: "brochure", accountId: "acc-1", limit: 100, labels: ["Project"], expandHops: 0, vectorThreshold: 0.85, }); // Two keepers (0.8473, 0.8474) fall below 0.85 — proves 0.82 sits in // the gap on purpose, not by accident. Don't move the default this // high without rechecking. expect(res.results).toHaveLength(4); expect(res.suppressed).toBe(9); }); });