// Task 413 / Task 420 — memory-update operator-hint integration.
//
// Validates: admin operatorHint passes through to the rewriter; SET stamps
// source='operator' and the revision row receives source='operator'; the 7-day
// cooldown does not block hint passes; the public-twin auto-fire is skipped;
// hint without rewriteCompiledTruth throws.
//
// Public publicOperatorHint (Task 420): hint folds into the public rewriter
// call; public SET stamps source='operator'; public-cooldown bypassed; fires
// regardless of whether admin pass changed the truth; refuses on :Person;
// refuses without rewriteCompiledTruth; respects the length cap; admin and
// public hints are independent.
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/embeddings.js", () => ({
    embed: vi.fn().mockResolvedValue([0.1]),
}));
vi.mock("../../lib/graph-write-gate.js", () => ({
    checkGraphWriteGate: vi.fn().mockResolvedValue({ ok: true }),
    checkPropertyWriteGate: vi.fn().mockReturnValue({ ok: true }),
    checkLabelWriteGate: vi.fn().mockReturnValue({ ok: true }),
    COMPILED_TRUTH_MAX_CHARS: 2000,
}));
vi.mock("../../lib/label-origin-gate.js", () => ({
    checkLabelOriginGate: vi.fn().mockReturnValue({ ok: true }),
}));
const rewriterMock = vi.fn();
vi.mock("../../lib/compiled-truth-rewriter.js", () => ({
    rewriteCompiledTruth: (...args) => rewriterMock(...args),
    OPERATOR_HINT_MAX_CHARS: 1000,
}));
vi.mock("./memory-write.js", () => ({
    buildTextRepresentation: () => "stub",
}));
import { memoryUpdate } from "../memory-update.js";
function labelRecord(overrides = {}) {
    const base = {
        nodeLabels: ["Organization"],
        compiledTruth: "old summary",
        compiledTruthSource: "operator",
        compiledTruthUpdatedAt: { toString: () => new Date().toISOString() }, // recent
        compiledTruthPublic: "old public",
        compiledTruthPublicSource: "rewriter",
        compiledTruthPublicUpdatedAt: null,
        name: "Acme",
        ...overrides,
    };
    return { get: (k) => base[k] };
}
describe("memory-update — operator hint (Task 413)", () => {
    beforeEach(() => {
        mockSession.run.mockReset();
        rewriterMock.mockReset();
        rewriterMock.mockResolvedValue({ kind: "ok", compiledTruth: "Adam now works at Anthropic." });
    });
    it("passes operatorHint through to the rewriter and stamps source='operator' on the SET", async () => {
        // call sequence:
        //   1. label probe
        //   2. allProps probe (no safeProps so we skip the SET branch)
        //   3. timeline events query
        //   4. SET + revision compound statement
        mockSession.run
            .mockResolvedValueOnce({ records: [labelRecord()] })
            .mockResolvedValueOnce({ records: [{ get: () => ({ name: "Acme" }) }] })
            .mockResolvedValueOnce({ records: [] }) // empty events
            .mockResolvedValueOnce({ records: [] }); // SET
        await memoryUpdate({
            nodeId: "4:n:1",
            properties: {},
            accountId: "acct-1",
            createdBy: { agent: "test", session: "s1" },
            rewriteCompiledTruth: true,
            operatorHint: "Adam now works at Anthropic.",
        });
        // rewriter called with the hint
        const rewriterArgs = rewriterMock.mock.calls[0][0];
        expect(rewriterArgs.operatorHint).toBe("Adam now works at Anthropic.");
        // SET runs with source='operator' bound as $source
        const setCall = mockSession.run.mock.calls.find(([cypher]) => typeof cypher === "string" && cypher.includes("n.compiledTruthSource = $source"));
        expect(setCall).toBeDefined();
        expect(setCall[1]).toMatchObject({ source: "operator" });
    });
    it("bypasses the 7-day operator cooldown when operatorHint is set", async () => {
        // currentSource='operator' + recent timestamp would normally refuse.
        mockSession.run
            .mockResolvedValueOnce({ records: [labelRecord()] })
            .mockResolvedValueOnce({ records: [{ get: () => ({ name: "Acme" }) }] })
            .mockResolvedValueOnce({ records: [] })
            .mockResolvedValueOnce({ records: [] });
        const result = await memoryUpdate({
            nodeId: "4:n:1",
            properties: {},
            accountId: "acct-1",
            createdBy: { agent: "test", session: "s1" },
            rewriteCompiledTruth: true,
            operatorHint: "correction",
        });
        expect(result.rewriter?.kind).toBe("ok");
        // Without the hint, the same setup refuses.
        mockSession.run.mockReset();
        mockSession.run
            .mockResolvedValueOnce({ records: [labelRecord()] })
            .mockResolvedValueOnce({ records: [{ get: () => ({ name: "Acme" }) }] });
        const refused = await memoryUpdate({
            nodeId: "4:n:1",
            properties: {},
            accountId: "acct-1",
            createdBy: { agent: "test", session: "s1" },
            rewriteCompiledTruth: true,
        });
        expect(refused.rewriter).toMatchObject({ kind: "refused", reason: "operator-edit-cooldown" });
    });
    it("skips the public-twin auto-fire when operatorHint is set", async () => {
        // Fresh public state — would normally trigger public pass on :Organization.
        mockSession.run
            .mockResolvedValueOnce({
            records: [labelRecord({ compiledTruthSource: "rewriter", compiledTruthPublicSource: "rewriter" })],
        })
            .mockResolvedValueOnce({ records: [{ get: () => ({ name: "Acme" }) }] })
            .mockResolvedValueOnce({ records: [] })
            .mockResolvedValueOnce({ records: [] });
        const result = await memoryUpdate({
            nodeId: "4:n:1",
            properties: {},
            accountId: "acct-1",
            createdBy: { agent: "test", session: "s1" },
            rewriteCompiledTruth: true,
            operatorHint: "correction",
        });
        // Only one rewriter call — admin pass; no public-twin call.
        expect(rewriterMock).toHaveBeenCalledTimes(1);
        expect(result.publicRewriter).toBeUndefined();
    });
    it("rejects operatorHint without rewriteCompiledTruth", async () => {
        await expect(memoryUpdate({
            nodeId: "4:n:1",
            properties: { name: "Acme" },
            accountId: "acct-1",
            createdBy: { agent: "test", session: "s1" },
            operatorHint: "x",
        })).rejects.toThrow(/requires rewriteCompiledTruth/);
    });
    it("rejects operatorHint over the cap", async () => {
        await expect(memoryUpdate({
            nodeId: "4:n:1",
            properties: {},
            accountId: "acct-1",
            createdBy: { agent: "test", session: "s1" },
            rewriteCompiledTruth: true,
            operatorHint: "x".repeat(1001),
        })).rejects.toThrow(/exceeds cap/);
    });
});
describe("memory-update — public operator hint (Task 420)", () => {
    beforeEach(() => {
        mockSession.run.mockReset();
        rewriterMock.mockReset();
        // Admin pass returns the same string as the current truth — a no-op
        // admin rewrite. The public pass under publicOperatorHint must still fire.
        rewriterMock.mockImplementation((args) => {
            if (args.mode === "public") {
                return Promise.resolve({ kind: "ok", compiledTruth: "Acme is a company." });
            }
            return Promise.resolve({ kind: "ok", compiledTruth: args.currentTruth });
        });
    });
    function publicLabelRecord(overrides = {}) {
        const base = {
            nodeLabels: ["Organization"],
            compiledTruth: "admin truth",
            compiledTruthSource: "rewriter",
            compiledTruthUpdatedAt: null,
            compiledTruthPublic: "old public",
            compiledTruthPublicSource: "operator",
            compiledTruthPublicUpdatedAt: { toString: () => new Date().toISOString() }, // in cooldown
            name: "Acme",
            ...overrides,
        };
        return { get: (k) => base[k] };
    }
    it("folds publicOperatorHint into the public rewriter call and stamps public SET source='operator'", async () => {
        // call sequence:
        //   1. label probe
        //   2. allProps probe
        //   3. timeline events
        //   4. admin SET + revision
        //   5. public SET
        mockSession.run
            .mockResolvedValueOnce({ records: [publicLabelRecord()] })
            .mockResolvedValueOnce({ records: [{ get: () => ({ name: "Acme" }) }] })
            .mockResolvedValueOnce({ records: [] })
            .mockResolvedValueOnce({ records: [] })
            .mockResolvedValueOnce({ records: [] });
        const result = await memoryUpdate({
            nodeId: "4:n:1",
            properties: {},
            accountId: "acct-1",
            createdBy: { agent: "test", session: "s1" },
            rewriteCompiledTruth: true,
            publicOperatorHint: "Acme is publicly recognised for X.",
        });
        // Two rewriter calls — admin pass + public pass with the hint.
        expect(rewriterMock).toHaveBeenCalledTimes(2);
        const publicCall = rewriterMock.mock.calls.find(([args]) => args.mode === "public");
        expect(publicCall).toBeDefined();
        expect(publicCall[0].operatorHint).toBe("Acme is publicly recognised for X.");
        // Public SET runs with source='operator' bound as $source.
        const publicSet = mockSession.run.mock.calls.find(([cypher]) => typeof cypher === "string" && cypher.includes("n.compiledTruthPublic = $truth"));
        expect(publicSet).toBeDefined();
        expect(publicSet[1]).toMatchObject({ source: "operator" });
        expect(result.publicRewriter).toMatchObject({ kind: "ok", source: "operator" });
    });
    it("fires the public pass even when the admin rewrite is a no-op (string-identical)", async () => {
        // Admin pass returns currentTruth verbatim (no change). Without a public
        // hint, the public pass would be skipped; with a hint it must still fire.
        mockSession.run
            .mockResolvedValueOnce({ records: [publicLabelRecord({ compiledTruthPublicSource: "rewriter" })] })
            .mockResolvedValueOnce({ records: [{ get: () => ({ name: "Acme" }) }] })
            .mockResolvedValueOnce({ records: [] })
            .mockResolvedValueOnce({ records: [] })
            .mockResolvedValueOnce({ records: [] });
        const result = await memoryUpdate({
            nodeId: "4:n:1",
            properties: {},
            accountId: "acct-1",
            createdBy: { agent: "test", session: "s1" },
            rewriteCompiledTruth: true,
            publicOperatorHint: "Force-fire",
        });
        expect(result.publicRewriter?.kind).toBe("ok");
        expect(rewriterMock).toHaveBeenCalledTimes(2);
    });
    it("bypasses the public-cooldown when publicOperatorHint is set", async () => {
        // compiledTruthPublicSource='operator' + recent timestamp would normally
        // refuse the public pass.
        mockSession.run
            .mockResolvedValueOnce({ records: [publicLabelRecord()] })
            .mockResolvedValueOnce({ records: [{ get: () => ({ name: "Acme" }) }] })
            .mockResolvedValueOnce({ records: [] })
            .mockResolvedValueOnce({ records: [] })
            .mockResolvedValueOnce({ records: [] });
        const result = await memoryUpdate({
            nodeId: "4:n:1",
            properties: {},
            accountId: "acct-1",
            createdBy: { agent: "test", session: "s1" },
            rewriteCompiledTruth: true,
            publicOperatorHint: "Bypass",
        });
        expect(result.publicRewriter?.kind).toBe("ok");
    });
    it("refuses publicOperatorHint on a :Person label", async () => {
        mockSession.run
            .mockResolvedValueOnce({
            records: [publicLabelRecord({ nodeLabels: ["Person"], compiledTruthPublicSource: "rewriter" })],
        })
            .mockResolvedValueOnce({ records: [{ get: () => ({ name: "Adam" }) }] });
        await expect(memoryUpdate({
            nodeId: "4:n:1",
            properties: {},
            accountId: "acct-1",
            createdBy: { agent: "test", session: "s1" },
            rewriteCompiledTruth: true,
            publicOperatorHint: "no public twin",
        })).rejects.toThrow(/requires a public-twin label/);
    });
    it("rejects publicOperatorHint without rewriteCompiledTruth", async () => {
        await expect(memoryUpdate({
            nodeId: "4:n:1",
            properties: { name: "Acme" },
            accountId: "acct-1",
            createdBy: { agent: "test", session: "s1" },
            publicOperatorHint: "x",
        })).rejects.toThrow(/requires rewriteCompiledTruth/);
    });
    it("rejects publicOperatorHint over the cap", async () => {
        await expect(memoryUpdate({
            nodeId: "4:n:1",
            properties: {},
            accountId: "acct-1",
            createdBy: { agent: "test", session: "s1" },
            rewriteCompiledTruth: true,
            publicOperatorHint: "x".repeat(1001),
        })).rejects.toThrow(/exceeds cap/);
    });
    it("admin and public hints are independent — admin-only hint skips public, public-only hint runs both", async () => {
        // Case A: admin-only hint — public pass skipped (Task 413 behaviour).
        mockSession.run
            .mockResolvedValueOnce({ records: [publicLabelRecord({ compiledTruthPublicSource: "rewriter" })] })
            .mockResolvedValueOnce({ records: [{ get: () => ({ name: "Acme" }) }] })
            .mockResolvedValueOnce({ records: [] })
            .mockResolvedValueOnce({ records: [] });
        const adminOnly = await memoryUpdate({
            nodeId: "4:n:1",
            properties: {},
            accountId: "acct-1",
            createdBy: { agent: "test", session: "s1" },
            rewriteCompiledTruth: true,
            operatorHint: "admin correction",
        });
        expect(adminOnly.publicRewriter).toBeUndefined();
        // Case B: public-only hint — admin pass runs as no-op, public pass fires.
        rewriterMock.mockClear();
        mockSession.run.mockReset();
        mockSession.run
            .mockResolvedValueOnce({ records: [publicLabelRecord({ compiledTruthPublicSource: "rewriter" })] })
            .mockResolvedValueOnce({ records: [{ get: () => ({ name: "Acme" }) }] })
            .mockResolvedValueOnce({ records: [] })
            .mockResolvedValueOnce({ records: [] })
            .mockResolvedValueOnce({ records: [] });
        const publicOnly = await memoryUpdate({
            nodeId: "4:n:1",
            properties: {},
            accountId: "acct-1",
            createdBy: { agent: "test", session: "s1" },
            rewriteCompiledTruth: true,
            publicOperatorHint: "public correction",
        });
        expect(publicOnly.rewriter?.kind).toBe("ok");
        expect(publicOnly.publicRewriter?.kind).toBe("ok");
    });
});
//# sourceMappingURL=memory-update-operator-hint.test.js.map