/**
 * Task 433 — the per-chunk derive step accepts agent-produced claims
 * and emits cypher proposals; it no longer calls Haiku, no longer paginates.
 * `conversation-archive-list-chunks` covers the page-walk side.
 */
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync, } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const mockSession = {
    run: vi.fn(),
    close: vi.fn().mockResolvedValue(undefined),
};
vi.mock("../../lib/neo4j.js", () => ({
    getSession: () => mockSession,
}));
vi.mock("../../lib/log-ingest.js", () => ({
    withWriteEvent: async (_info, body) => body(),
}));
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PACKAGE_ROOT = resolve(__dirname, "..", "..", "..");
const INDEX_PATH = resolve(PACKAGE_ROOT, "src/index.ts");
const PLUGIN_PATH = resolve(PACKAGE_ROOT, "..", "PLUGIN.md");
const TOOL_PATH = resolve(PACKAGE_ROOT, "src/tools/conversation-archive-derive-insights.ts");
const LIST_TOOL_PATH = resolve(PACKAGE_ROOT, "src/tools/conversation-archive-list-chunks.ts");
describe("conversation-archive-derive-insights — registration", () => {
    it("registers both tools with boot log lines", () => {
        const idx = readFileSync(INDEX_PATH, "utf-8");
        expect(idx).toContain('[memory-mcp] registered tool=conversation-archive-list-chunks');
        expect(idx).toContain('[memory-mcp] registered tool=conversation-archive-derive-insights');
    });
    it("lists both tools in PLUGIN.md tools array", () => {
        const plugin = readFileSync(PLUGIN_PATH, "utf-8");
        expect(plugin).toMatch(/-\s+name:\s+conversation-archive-list-chunks\b/);
        expect(plugin).toMatch(/-\s+name:\s+conversation-archive-derive-insights\b/);
    });
    it("derive-insights no longer references Haiku or OAuth", () => {
        const src = readFileSync(TOOL_PATH, "utf-8");
        expect(src).not.toMatch(/callOauthLlm/);
        expect(src).not.toMatch(/deriveClaimsForChunk/);
        expect(src).not.toMatch(/HAIKU_TIMEOUT_MS/);
    });
    it("derive-insights accepts agent-produced claims as its core input", () => {
        const src = readFileSync(TOOL_PATH, "utf-8");
        expect(src).toMatch(/claims:\s*AgentClaim\[\]/);
        expect(src).toMatch(/chunkElementId:\s*string/);
    });
    it("list-chunks is a separate read-only tool returning chunk bodies", () => {
        const src = readFileSync(LIST_TOOL_PATH, "utf-8");
        expect(src).toMatch(/conversationArchiveListChunks/);
        expect(src).toMatch(/chunks:\s*ChunkPageRow\[\]/);
        expect(src).not.toMatch(/proposalsRemaining/);
    });
});
describe("conversation-archive-derive-insights — runtime", () => {
    let scratchDir;
    let origPlatformRoot;
    let conversationArchiveDeriveInsights;
    beforeEach(async () => {
        scratchDir = mkdtempSync(resolve(tmpdir(), "derive-test-"));
        origPlatformRoot = process.env.PLATFORM_ROOT;
        process.env.PLATFORM_ROOT = resolve(scratchDir, "platform");
        mockSession.run.mockReset();
        mockSession.close.mockClear();
        vi.resetModules();
        const mod = await import("../conversation-archive-derive-insights.js");
        conversationArchiveDeriveInsights = mod.conversationArchiveDeriveInsights;
    });
    afterEach(() => {
        if (origPlatformRoot === undefined)
            delete process.env.PLATFORM_ROOT;
        else
            process.env.PLATFORM_ROOT = origPlatformRoot;
        rmSync(scratchDir, { recursive: true, force: true });
    });
    it("LOUD-FAILs when chunkElementId is not a :Section of the named archive", async () => {
        mockSession.run.mockResolvedValueOnce({ records: [] });
        await expect(conversationArchiveDeriveInsights({
            accountId: "acct",
            archiveElementId: "4:ARCH:1",
            chunkElementId: "4:CHUNK:99",
            claims: [],
        })).rejects.toThrow(/chunk-not-in-archive/);
    });
    it("returns proposals for a valid mention claim", async () => {
        mockSession.run.mockResolvedValueOnce({
            records: [{ get: (k) => (k === "eid" ? "4:CHUNK:1" : null) }],
        });
        const result = await conversationArchiveDeriveInsights({
            accountId: "acct",
            archiveElementId: "4:ARCH:1",
            chunkElementId: "4:CHUNK:1",
            claims: [
                {
                    kind: "mention",
                    evidenceSnippet: "spoke to Adam yesterday",
                    subject: "Adam Mackay",
                    object: null,
                    payload: {},
                },
            ],
        });
        expect(result.proposals).toHaveLength(1);
        expect(result.proposals[0].kind).toBe("mention");
        expect(result.proposals[0].chunkElementId).toBe("4:CHUNK:1");
        expect(result.proposals[0].disambiguation?.needsResolution[0]?.displayName).toBe("Adam Mackay");
        expect(result.proposals[0].mergeCypher).toMatch(/MERGE \(c\)-\[m:MENTIONS/);
        expect(result.invalidClaims).toBe(0);
    });
    it("drops claims that lack required per-kind fields", async () => {
        mockSession.run.mockResolvedValueOnce({
            records: [{ get: (k) => (k === "eid" ? "4:CHUNK:1" : null) }],
        });
        const result = await conversationArchiveDeriveInsights({
            accountId: "acct",
            archiveElementId: "4:ARCH:1",
            chunkElementId: "4:CHUNK:1",
            claims: [
                // missing subject — invalid mention
                { kind: "mention", evidenceSnippet: "abc", subject: "", object: null, payload: {} },
                // missing taskTitle — invalid task
                { kind: "task", evidenceSnippet: "abc", subject: null, object: null, payload: {} },
                // valid preference
                {
                    kind: "preference",
                    evidenceSnippet: "only meets on Tuesdays",
                    subject: null,
                    object: null,
                    payload: {
                        preferenceCategory: "meeting",
                        preferenceKey: "day-of-week",
                        preferenceValue: "Tuesday",
                    },
                },
            ],
        });
        expect(result.proposals).toHaveLength(1);
        expect(result.proposals[0].kind).toBe("preference");
        expect(result.invalidClaims).toBe(2);
    });
    it("filters proposals matching a recorded rejection (chunkElementId, kind, contentHash)", async () => {
        // Archive membership check passes.
        mockSession.run.mockResolvedValueOnce({
            records: [{ get: (k) => (k === "eid" ? "4:CHUNK:1" : null) }],
        });
        // Pre-write a rejection sidecar matching what the next mention claim will produce.
        // The contentHash is deterministic on (kind, subject, evidenceSnippet).
        const { rejectionsJsonlPath } = await import("../conversation-archive-derive-insights.js");
        const jsonlPath = rejectionsJsonlPath("acct");
        mkdirSync(dirname(jsonlPath), { recursive: true });
        // Compute contentHash the same way the tool does.
        const { createHash } = await import("node:crypto");
        const h = createHash("sha256");
        h.update("mention");
        h.update(" ");
        h.update("Adam Mackay");
        h.update(" ");
        h.update("spoke to Adam yesterday");
        const contentHash = h.digest("hex");
        writeFileSync(jsonlPath, `${JSON.stringify({ chunkElementId: "4:CHUNK:1", kind: "mention", contentHash })}\n`);
        const result = await conversationArchiveDeriveInsights({
            accountId: "acct",
            archiveElementId: "4:ARCH:1",
            chunkElementId: "4:CHUNK:1",
            claims: [
                {
                    kind: "mention",
                    evidenceSnippet: "spoke to Adam yesterday",
                    subject: "Adam Mackay",
                    object: null,
                    payload: {},
                },
            ],
        });
        expect(result.proposals).toHaveLength(0);
        expect(result.rejectionsFiltered).toBe(1);
    });
});
// ---------------------------------------------------------------------------
// Task 595 — inline-rewrite signals
// ---------------------------------------------------------------------------
describe("conversation-archive-derive-insights — inline-rewrite signals (Task 595)", () => {
    it("DeriveInsightsParams declares inlineRewrite and inlineRewriteThreshold", () => {
        const src = readFileSync(TOOL_PATH, "utf-8");
        expect(src).toContain("inlineRewrite?: boolean");
        expect(src).toContain("inlineRewriteThreshold?: number");
    });
    it("DeriveInsightsResult declares inlineRewriteSignals optional field with displayName and mentionCount", () => {
        const src = readFileSync(TOOL_PATH, "utf-8");
        expect(src).toMatch(/inlineRewriteSignals\?:\s*\{/);
        expect(src).toContain("displayName: string");
        expect(src).toContain("mentionCount: number");
    });
    it("inlineRewriteSignals is undefined when inlineRewrite param is absent", async () => {
        mockSession.run.mockResolvedValue({ records: [{ get: () => "archive-1" }] });
        const { conversationArchiveDeriveInsights } = await import("../../tools/conversation-archive-derive-insights.js");
        const result = await conversationArchiveDeriveInsights({
            accountId: "acc1",
            archiveElementId: "arc1",
            chunkElementId: "chunk1",
            claims: [
                { kind: "mention", evidenceSnippet: "Joel joined.", subject: "Joel Smith", payload: {} },
                { kind: "mention", evidenceSnippet: "Joel again.", subject: "Joel Smith", payload: {} },
            ],
        });
        expect(result.inlineRewriteSignals).toBeUndefined();
    });
    it("returns signal when a subject appears >= threshold times with inlineRewrite=true", async () => {
        mockSession.run.mockResolvedValue({ records: [{ get: () => "archive-1" }] });
        const { conversationArchiveDeriveInsights } = await import("../../tools/conversation-archive-derive-insights.js");
        const result = await conversationArchiveDeriveInsights({
            accountId: "acc1",
            archiveElementId: "arc1",
            chunkElementId: "chunk1",
            claims: [
                { kind: "mention", evidenceSnippet: "Joel joined.", subject: "Joel Smith", payload: {} },
                { kind: "mention", evidenceSnippet: "Joel again.", subject: "Joel Smith", payload: {} },
            ],
            inlineRewrite: true,
            inlineRewriteThreshold: 2,
        });
        expect(result.inlineRewriteSignals).toBeDefined();
        expect(result.inlineRewriteSignals).toHaveLength(1);
        expect(result.inlineRewriteSignals[0].displayName).toBe("Joel Smith");
        expect(result.inlineRewriteSignals[0].mentionCount).toBe(2);
    });
    it("does not return a signal when mention count is below threshold", async () => {
        mockSession.run.mockResolvedValue({ records: [{ get: () => "archive-1" }] });
        const { conversationArchiveDeriveInsights } = await import("../../tools/conversation-archive-derive-insights.js");
        const result = await conversationArchiveDeriveInsights({
            accountId: "acc1",
            archiveElementId: "arc1",
            chunkElementId: "chunk1",
            claims: [
                { kind: "mention", evidenceSnippet: "Joel joined.", subject: "Joel Smith", payload: {} },
            ],
            inlineRewrite: true,
            inlineRewriteThreshold: 2,
        });
        expect(result.inlineRewriteSignals).toEqual([]);
    });
    it("uses default threshold of 2 when inlineRewriteThreshold is omitted", async () => {
        mockSession.run.mockResolvedValue({ records: [{ get: () => "archive-1" }] });
        const { conversationArchiveDeriveInsights } = await import("../../tools/conversation-archive-derive-insights.js");
        // Two mentions of same entity with no explicit threshold → default 2 fires
        const result = await conversationArchiveDeriveInsights({
            accountId: "acc1",
            archiveElementId: "arc1",
            chunkElementId: "chunk1",
            claims: [
                { kind: "mention", evidenceSnippet: "Joel joined.", subject: "Joel Smith", payload: {} },
                { kind: "mention", evidenceSnippet: "Joel again.", subject: "Joel Smith", payload: {} },
            ],
            inlineRewrite: true,
            // inlineRewriteThreshold intentionally omitted — default 2 should apply
        });
        expect(result.inlineRewriteSignals).toHaveLength(1);
        expect(result.inlineRewriteSignals[0].displayName).toBe("Joel Smith");
        expect(result.inlineRewriteSignals[0].mentionCount).toBe(2);
    });
    it("counts rejected mention claims in signals (density is rejection-independent)", async () => {
        mockSession.run.mockResolvedValue({ records: [{ get: () => "archive-1" }] });
        const { conversationArchiveDeriveInsights, rejectionsJsonlPath } = await import("../../tools/conversation-archive-derive-insights.js");
        // Write a rejection for the first Joel mention so it is filtered from proposals
        const { createHash } = await import("node:crypto");
        const { writeFileSync, mkdirSync } = await import("node:fs");
        const { dirname } = await import("node:path");
        const rPath = rejectionsJsonlPath("acc1");
        mkdirSync(dirname(rPath), { recursive: true });
        const h = createHash("sha256");
        h.update("mention");
        h.update(" ");
        h.update("Joel Smith");
        h.update(" ");
        h.update("Joel joined.");
        const contentHash = h.digest("hex");
        writeFileSync(rPath, JSON.stringify({ chunkElementId: "chunk1", kind: "mention", contentHash }) + "\n");
        const result = await conversationArchiveDeriveInsights({
            accountId: "acc1",
            archiveElementId: "arc1",
            chunkElementId: "chunk1",
            claims: [
                { kind: "mention", evidenceSnippet: "Joel joined.", subject: "Joel Smith", payload: {} },
                { kind: "mention", evidenceSnippet: "Joel again.", subject: "Joel Smith", payload: {} },
            ],
            inlineRewrite: true,
            inlineRewriteThreshold: 2,
        });
        // First mention is rejected → only 1 proposal, but signals should still count both
        expect(result.proposals).toHaveLength(1);
        expect(result.rejectionsFiltered).toBe(1);
        expect(result.inlineRewriteSignals).toHaveLength(1);
        expect(result.inlineRewriteSignals[0].mentionCount).toBe(2); // not 1
    });
});
//# sourceMappingURL=conversation-archive-derive-insights.test.js.map