// Task 512 — memory-lookup-by-name is a deterministic by-name read that
// bypasses the memory-search ranking stack. These tests assert the Cypher
// predicate (case-insensitive exact name, notTrashed, optional label gate),
// the reused scope/slice fragment, the agent-scope gate, account-isolation
// params, embedding/accountId stripping, and the empty-result-is-[] contract.
// Mocked session: same convention as memory-update-by-name.test.ts — the live
// Neo4j run is exercised on the Pi, the predicate correctness is locked here.
import { describe, it, expect, beforeEach, vi } from "vitest";
const mockSession = {
    run: vi.fn(),
    close: vi.fn().mockResolvedValue(undefined),
};
vi.mock("../../lib/neo4j.js", () => ({
    getSession: () => mockSession,
}));
vi.mock("../../../../../../lib/graph-trash/dist/index.js", () => ({
    notTrashed: (alias) => `(NOT ${alias}:Trashed)`,
}));
// Mirror graph-search's real branching so the test proves the tool threads
// the reused fragment, not a re-derived one.
vi.mock("../../../../../../lib/graph-search/dist/index.js", () => ({
    buildScopeAndSliceClause: (allowedScopes, sliceToken, alias) => {
        const hasSlice = typeof sliceToken === "string" && sliceToken.length > 0;
        const hasScopes = Array.isArray(allowedScopes) && allowedScopes.length > 0;
        if (!hasSlice && !hasScopes)
            return { clause: "", params: {} };
        if (hasSlice && hasScopes) {
            return {
                clause: `AND (${alias}.sliceToken = $sliceToken OR ${alias}.scope IS NULL OR ${alias}.scope IN $allowedScopes)`,
                params: { sliceToken, allowedScopes },
            };
        }
        if (hasSlice) {
            return {
                clause: `AND (${alias}.sliceToken = $sliceToken OR ${alias}.scope IS NULL)`,
                params: { sliceToken },
            };
        }
        return {
            clause: `AND (${alias}.scope IS NULL OR ${alias}.scope IN $allowedScopes)`,
            params: { allowedScopes },
        };
    },
}));
import { memoryLookupByName } from "../memory-lookup-by-name.js";
const ACCOUNT_ID = "20028fb5-db26-4de7-bff6-c5ec2d4fc766";
function record(fields) {
    return { get: (k) => fields[k] };
}
beforeEach(() => {
    mockSession.run.mockReset();
    mockSession.close.mockClear();
});
describe("memory-lookup-by-name — deterministic predicate", () => {
    it("matches name case-insensitively, excludes trashed, threads accountId", async () => {
        mockSession.run.mockResolvedValueOnce({
            records: [
                record({
                    nodeId: "4:org:27",
                    labels: ["Organization"],
                    properties: { name: "Rubytech LLC", url: "https://www.rubytech.llc/", accountId: ACCOUNT_ID, embedding: [0.1, 0.2] },
                }),
            ],
        });
        const hits = await memoryLookupByName({ name: "rubytech llc", accountId: ACCOUNT_ID });
        const [cypher, params] = mockSession.run.mock.calls[0];
        expect(cypher).toContain("toLower(n.name) = toLower($name)");
        expect(cypher).toContain("n.accountId = $accountId");
        expect(cypher).toContain("(NOT n:Trashed)");
        expect(params.accountId).toBe(ACCOUNT_ID);
        expect(params.name).toBe("rubytech llc");
        // One live hit; embedding + accountId stripped from the returned row.
        expect(hits).toHaveLength(1);
        expect(hits[0].nodeId).toBe("4:org:27");
        expect(hits[0].labels).toEqual(["Organization"]);
        expect(hits[0].properties).toEqual({ name: "Rubytech LLC", url: "https://www.rubytech.llc/" });
        expect(hits[0].properties).not.toHaveProperty("embedding");
        expect(hits[0].properties).not.toHaveProperty("accountId");
    });
    it("returns [] (not an error) when no node matches", async () => {
        mockSession.run.mockResolvedValueOnce({ records: [] });
        const hits = await memoryLookupByName({ name: "Nonexistent Co", accountId: ACCOUNT_ID });
        expect(hits).toEqual([]);
    });
    it("adds the label gate only when labels are passed", async () => {
        mockSession.run.mockResolvedValueOnce({ records: [] });
        await memoryLookupByName({ name: "Rubytech LLC", accountId: ACCOUNT_ID, labels: ["Organization"] });
        const [cypher, params] = mockSession.run.mock.calls[0];
        expect(cypher).toContain("any(l IN labels(n) WHERE l IN $labels)");
        expect(params.labels).toEqual(["Organization"]);
    });
    it("omits the label gate when labels are absent", async () => {
        mockSession.run.mockResolvedValueOnce({ records: [] });
        await memoryLookupByName({ name: "Rubytech LLC", accountId: ACCOUNT_ID });
        const [cypher, params] = mockSession.run.mock.calls[0];
        expect(cypher).not.toContain("labels(n) WHERE");
        expect(params).not.toHaveProperty("labels");
    });
    it("rejects an empty name before touching the session", async () => {
        await expect(memoryLookupByName({ name: "   ", accountId: ACCOUNT_ID })).rejects.toThrow(/name is required/);
        expect(mockSession.run).not.toHaveBeenCalled();
    });
});
describe("memory-lookup-by-name — scope isolation parity with memory-search", () => {
    it("composes the slice+scope fragment and threads both params", async () => {
        mockSession.run.mockResolvedValueOnce({ records: [] });
        await memoryLookupByName({
            name: "Rubytech LLC",
            accountId: ACCOUNT_ID,
            allowedScopes: ["public"],
            sliceToken: "slice-abc",
        });
        const [cypher, params] = mockSession.run.mock.calls[0];
        expect(cypher).toContain("n.sliceToken = $sliceToken OR n.scope IS NULL OR n.scope IN $allowedScopes");
        expect(params.sliceToken).toBe("slice-abc");
        expect(params.allowedScopes).toEqual(["public"]);
    });
    it("adds the agent-scope gate when agentSlug is set", async () => {
        mockSession.run.mockResolvedValueOnce({ records: [] });
        await memoryLookupByName({ name: "Rubytech LLC", accountId: ACCOUNT_ID, agentSlug: "researcher" });
        const [cypher, params] = mockSession.run.mock.calls[0];
        expect(cypher).toContain("n.agents IS NOT NULL AND $agentSlug IN n.agents");
        expect(params.agentSlug).toBe("researcher");
    });
    it("emits no scope/slice/agent clause for the unscoped admin caller", async () => {
        mockSession.run.mockResolvedValueOnce({ records: [] });
        await memoryLookupByName({ name: "Rubytech LLC", accountId: ACCOUNT_ID });
        const [cypher, params] = mockSession.run.mock.calls[0];
        expect(cypher).not.toContain("scope IN $allowedScopes");
        expect(cypher).not.toContain("$agentSlug");
        expect(params).not.toHaveProperty("sliceToken");
        expect(params).not.toHaveProperty("agentSlug");
    });
});
//# sourceMappingURL=memory-lookup-by-name.test.js.map