// Task 237 — memory-empty-trash :AdminConversation defense-in-depth.
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { setupEmitCapture, makeMockSession } from "./_helpers/emit-capture.js";
const mockSession = makeMockSession();
vi.mock("../../lib/neo4j.js", () => ({
    getSession: () => mockSession,
    getClaudeSessionIdsByConversationElementId: vi.fn(),
}));
vi.mock("../../lib/attachments.js", () => ({
    removeAttachmentDir: vi.fn().mockResolvedValue(false),
}));
import { memoryEmptyTrash } from "../memory-empty-trash.js";
import { getClaudeSessionIdsByConversationElementId } from "../../lib/neo4j.js";
const lookupMock = vi.mocked(getClaudeSessionIdsByConversationElementId);
const ACCOUNT_ID = "acct-1";
const SESSION_ID = "conv-11111111-1111-1111-1111-111111111111";
const ELEMENT_ID = "4:000:42";
const CLAUDE_A = "css-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
const CLAUDE_B = "css-bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb";
function stageEmptyTrashCypherForOne(candidate, options = { expectDetachDelete: true }) {
    const state = { detachDeleteCalls: 0, runs: 0 };
    mockSession.run = vi.fn(async (cypher) => {
        state.runs++;
        // emptyTrash's candidate query
        if (cypher.includes("MATCH (n:Trashed)")) {
            const keysJson = candidate.sessionId
                ? JSON.stringify({ sessionId: candidate.sessionId })
                : JSON.stringify({});
            return {
                records: [
                    {
                        get: (k) => {
                            switch (k) {
                                case "eid": return ELEMENT_ID;
                                case "labels": return [...candidate.labels, "Trashed"];
                                case "trashedAt": return "2026-04-01T00:00:00.000Z";
                                case "trashedBy": return "test";
                                case "keysJson": return keysJson;
                                default: return undefined;
                            }
                        },
                    },
                ],
            };
        }
        if (cypher.includes("DETACH DELETE n")) {
            state.detachDeleteCalls++;
            if (!options.expectDetachDelete) {
                throw new Error(`unexpected DETACH DELETE — candidate should have been skipped`);
            }
            return { records: [] };
        }
        throw new Error(`unexpected cypher: ${cypher.slice(0, 80)}`);
    });
    return state;
}
beforeEach(() => {
    lookupMock.mockReset();
    delete process.env.CLAUDE_SESSION_MANAGER_PORT;
});
afterEach(() => {
    vi.restoreAllMocks();
    delete process.env.CLAUDE_SESSION_MANAGER_PORT;
});
describe("memory-empty-trash :AdminConversation onEmpty", () => {
    it("purges JSONL for a stale :Trashed :AdminConversation and DETACH DELETEs the node", async () => {
        process.env.CLAUDE_SESSION_MANAGER_PORT = "9999";
        const state = stageEmptyTrashCypherForOne({ labels: ["Conversation", "AdminConversation"], sessionId: SESSION_ID }, { expectDetachDelete: true });
        lookupMock.mockResolvedValue([CLAUDE_A, CLAUDE_B]);
        const fetchCalls = [];
        const fetchImpl = vi.fn(async (url, init) => {
            fetchCalls.push({ url: String(url), method: init?.method ?? "GET" });
            return new Response(null, { status: 204 });
        });
        const emit = setupEmitCapture();
        try {
            const result = await memoryEmptyTrash({ accountId: ACCOUNT_ID, fetchImpl });
            expect(result.emptied).toBe(1);
            expect(result.adminConversationsPurged).toBe(1);
            expect(state.detachDeleteCalls).toBe(1);
            expect(fetchCalls).toEqual([
                { url: `http://127.0.0.1:9999/${CLAUDE_A}`, method: "DELETE" },
                { url: `http://127.0.0.1:9999/${CLAUDE_B}`, method: "DELETE" },
            ]);
            expect(lookupMock).toHaveBeenCalledWith(ELEMENT_ID, ACCOUNT_ID);
        }
        finally {
            emit.restore();
        }
    });
    it("skips candidate (no DETACH DELETE) when manager returns 5xx for a JSONL", async () => {
        process.env.CLAUDE_SESSION_MANAGER_PORT = "9999";
        const state = stageEmptyTrashCypherForOne({ labels: ["Conversation", "AdminConversation"], sessionId: SESSION_ID }, { expectDetachDelete: false });
        lookupMock.mockResolvedValue([CLAUDE_A]);
        const fetchImpl = vi.fn(async () => new Response('{"error":"disk-fail"}', {
            status: 500,
            headers: { "content-type": "application/json" },
        }));
        const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
        const emit = setupEmitCapture();
        try {
            const result = await memoryEmptyTrash({ accountId: ACCOUNT_ID, fetchImpl });
            expect(result.emptied).toBe(0);
            expect(result.adminConversationsPurged).toBe(0);
            expect(state.detachDeleteCalls).toBe(0);
            const lines = stderrSpy.mock.calls.map((c) => String(c[0]));
            expect(lines.some((l) => l.includes("[trash:empty-run]") && l.includes("reason=jsonl-purge-failed"))).toBe(true);
        }
        finally {
            stderrSpy.mockRestore();
            emit.restore();
        }
    });
    it("skips candidate when CLAUDE_SESSION_MANAGER_PORT is unset (no DETACH DELETE)", async () => {
        // env var deliberately not set
        const state = stageEmptyTrashCypherForOne({ labels: ["Conversation", "AdminConversation"], sessionId: SESSION_ID }, { expectDetachDelete: false });
        lookupMock.mockResolvedValue([CLAUDE_A]);
        const fetchImpl = vi.fn();
        const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
        const emit = setupEmitCapture();
        try {
            const result = await memoryEmptyTrash({ accountId: ACCOUNT_ID, fetchImpl });
            expect(result.emptied).toBe(0);
            expect(result.adminConversationsPurged).toBe(0);
            expect(state.detachDeleteCalls).toBe(0);
            expect(fetchImpl.mock.calls).toHaveLength(0);
            const lines = stderrSpy.mock.calls.map((c) => String(c[0]));
            expect(lines.some((l) => l.includes("reason=jsonl-purge-failed") && l.includes("manager-port-missing"))).toBe(true);
        }
        finally {
            stderrSpy.mockRestore();
            emit.restore();
        }
    });
    it("skips candidate when sessionId is missing from _trashedKeys", async () => {
        process.env.CLAUDE_SESSION_MANAGER_PORT = "9999";
        const state = stageEmptyTrashCypherForOne({ labels: ["Conversation", "AdminConversation"] }, // no sessionId in snapshot
        { expectDetachDelete: false });
        const fetchImpl = vi.fn();
        const emit = setupEmitCapture();
        try {
            const result = await memoryEmptyTrash({ accountId: ACCOUNT_ID, fetchImpl });
            expect(result.emptied).toBe(0);
            expect(state.detachDeleteCalls).toBe(0);
            expect(lookupMock).not.toHaveBeenCalled();
        }
        finally {
            emit.restore();
        }
    });
    it("leaves :KnowledgeDocument path unchanged (no JSONL purge, no AdminConversation counter)", async () => {
        const state = stageEmptyTrashCypherForOne({ labels: ["KnowledgeDocument"] }, { expectDetachDelete: true });
        const fetchImpl = vi.fn();
        const emit = setupEmitCapture();
        try {
            const result = await memoryEmptyTrash({ accountId: ACCOUNT_ID, fetchImpl });
            expect(result.emptied).toBe(1);
            expect(result.adminConversationsPurged).toBe(0);
            expect(state.detachDeleteCalls).toBe(1);
            expect(lookupMock).not.toHaveBeenCalled();
            expect(fetchImpl.mock.calls).toHaveLength(0);
        }
        finally {
            emit.restore();
        }
    });
});
//# sourceMappingURL=memory-empty-trash-admin-conversation.test.js.map