import { describe, it, expect } from "vitest"; import { bm25Only } from "../index.js"; /** * Stub Session records every `run()` call — we assert on the Cypher text and * params to pin down the clause-composition invariants from the task brief. * No real Neo4j is contacted; this is pure unit coverage of the SQL-building * decision layer. */ function makeStubSession(records: Array> = []) { const calls: Array<{ query: string; params: Record }> = []; const session = { run(query: string, params: Record) { calls.push({ query, params }); return Promise.resolve({ records: records.map((r) => ({ get: (k: string) => r[k], })), }); }, } as unknown as import("neo4j-driver").Session; return { session, calls }; } describe("bm25Only Cypher composition", () => { it("omits scope clause when allowedScopes is not set", async () => { const { session, calls } = makeStubSession(); await bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10 }); expect(calls).toHaveLength(1); expect(calls[0].query).not.toContain("node.scope"); expect(calls[0].params).not.toHaveProperty("allowedScopes"); }); it("adds scope clause only when allowedScopes is set", async () => { const { session, calls } = makeStubSession(); await bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10, allowedScopes: ["public", "account"], }); expect(calls[0].query).toContain("node.scope IS NULL OR node.scope IN $allowedScopes"); expect(calls[0].params.allowedScopes).toEqual(["public", "account"]); }); it("omits agent clause when agentSlug is not set", async () => { const { session, calls } = makeStubSession(); await bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10 }); expect(calls[0].query).not.toContain("node.agents"); expect(calls[0].params).not.toHaveProperty("agentSlug"); }); it("adds agent clause when agentSlug is set", async () => { const { session, calls } = makeStubSession(); await bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10, agentSlug: "support", }); expect(calls[0].query).toContain("$agentSlug IN node.agents"); expect(calls[0].params.agentSlug).toBe("support"); }); it("omits keyword clause when keywords is not set", async () => { const { session, calls } = makeStubSession(); await bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10 }); expect(calls[0].query).not.toContain("node.keywords IS NULL"); expect(calls[0].params).not.toHaveProperty("keywords"); }); it("adds keyword clause with ANY (default) when keywords is set", async () => { const { session, calls } = makeStubSession(); await bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10, keywords: ["Alpha", "Beta"], }); expect(calls[0].query).toContain("ANY(kw IN $keywords WHERE kw IN node.keywords)"); expect(calls[0].params.keywords).toEqual(["alpha", "beta"]); }); it("uses ALL when keywordMatch='all'", async () => { const { session, calls } = makeStubSession(); await bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10, keywords: ["a"], keywordMatch: "all", }); expect(calls[0].query).toContain("ALL(kw IN $keywords WHERE kw IN node.keywords)"); }); it("always applies notTrashed filter", async () => { const { session, calls } = makeStubSession(); await bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10 }); expect(calls[0].query).toContain("NOT `node`:Trashed"); expect(calls[0].query).toContain("`node`.deletedAt IS NULL"); }); it("escapes Lucene special chars in query param", async () => { const { session, calls } = makeStubSession(); await bm25Only(session, { query: "foo:bar (x)", accountId: "acc-1", limit: 10 }); expect(calls[0].params.query).toBe("foo\\:bar \\(x\\)"); }); it("returns [] when the fulltext index does not exist", async () => { const session = { run() { return Promise.reject(new Error("There is no such fulltext index")); }, } as unknown as import("neo4j-driver").Session; const hits = await bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10 }); expect(hits).toEqual([]); }); it("rethrows non-index errors", async () => { const session = { run() { return Promise.reject(new Error("ServiceUnavailable: database offline")); }, } as unknown as import("neo4j-driver").Session; await expect( bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10 }), ).rejects.toThrow("ServiceUnavailable"); }); });