/**
 * Unit tests for the Active Learning system.
 *
 * These tests use a mock API client to verify extraction rules
 * and confidence-based routing - no real API calls.
 *
 * Run with: bun test packages/mcp-server/src/__tests__/active-learning.test.ts
 */
import { describe, expect, mock, test } from "bun:test";
// Mock the config module before importing active-learning
let mockWorkspaceId = "ws-test-123";
const mockProjectId = "proj-test-456";
mock.module("../config.js", () => ({
    getActiveWorkspaceId: () => mockWorkspaceId,
    getActiveProjectId: () => mockProjectId,
    isConfigured: () => true,
    loadConfig: () => { },
}));
// Import after mocking
const { extractLearnings } = await import("../active-learning.js");
// Extended mock client that satisfies all methods called by fire-and-forget paths
// (autoExpandGraph, detectAndCreatePatterns). Unused by most tests but prevents crashes.
function makeFullMockClient() {
    const createdEntities = [];
    let nextId = 1;
    return {
        createdEntities,
        createMemoryEntity: mock(async (data) => {
            const entity = { id: `entity-${nextId++}`, ...data };
            createdEntities.push(entity);
            return { entity };
        }),
        updateMemoryEntity: mock(async (entityId, updates) => ({
            entity: { id: entityId, ...updates },
        })),
        deleteMemoryEntity: mock(async () => ({ success: true })),
        listMemoryEntities: mock(async () => ({
            entities: [],
            count: 0,
        })),
        getMemoryEntity: mock(async (id) => ({
            entity: { id, type: "lesson" },
        })),
        searchMemoryEntities: mock(async () => ({
            entities: [],
            count: 0,
        })),
        createMemoryRelation: mock(async () => ({ relation: {} })),
    };
}
// ---- Mock helpers ----
function makeMockClient() {
    const createdEntities = [];
    let nextId = 1;
    return {
        createdEntities,
        createMemoryEntity: mock(async (data) => {
            const entity = { id: `entity-${nextId++}`, ...data };
            createdEntities.push(entity);
            return { entity };
        }),
        updateMemoryEntity: mock(async (entityId, updates) => {
            return { entity: { id: entityId, ...updates } };
        }),
        deleteMemoryEntity: mock(async () => ({ success: true })),
        listMemoryEntities: mock(async () => ({
            entities: [],
            count: 0,
        })),
    };
}
function makeSession(overrides = {}) {
    return {
        cardId: "card-1",
        cardTitle: "Fix login bug",
        cardLabels: ["feature"],
        agentIdentifier: "claude-code",
        agentName: "Claude Code",
        status: "completed",
        progressPercent: 100,
        ...overrides,
    };
}
// ---- Tests ----
describe("extractLearnings", () => {
    describe("Rule 1: Blockers create error entities", () => {
        test("session with blockers creates error entities", async () => {
            const client = makeMockClient();
            const session = makeSession({
                blockers: ["API rate limit exceeded", "Database timeout"],
                status: "completed",
            });
            const result = await extractLearnings(client, session);
            // 2 blockers + 1 session lesson = 3 entities
            expect(result.count).toBe(3);
            expect(result.entityIds).toHaveLength(3);
            // Check error entities were created for each blocker
            const errorEntities = client.createdEntities.filter((e) => e.type === "error");
            expect(errorEntities).toHaveLength(2);
            expect(errorEntities[0].title).toContain("Blocker:");
            expect(errorEntities[0].title).toContain("API rate limit");
            expect(errorEntities[0].memory_tier).toBe("reference");
            expect(errorEntities[0].confidence).toBe(0.7);
            expect(errorEntities[0].tags ?? []).toContain("auto-extracted");
            expect(errorEntities[0].tags ?? []).toContain("blocker");
        });
        test("blocker title is truncated to 100 chars", async () => {
            const client = makeMockClient();
            const longBlocker = "A".repeat(200);
            const session = makeSession({
                blockers: [longBlocker],
                status: "paused",
            });
            await extractLearnings(client, session);
            const errorEntities = client.createdEntities.filter((e) => e.type === "error");
            expect(errorEntities).toHaveLength(1);
            // Title: "Blocker: " + 100 chars
            expect(errorEntities[0].title.length).toBeLessThanOrEqual(110);
        });
    });
    describe("Rule 2: Completed sessions create lesson entities", () => {
        test("clean completed session does NOT create lesson entity", async () => {
            const client = makeMockClient();
            const session = makeSession({
                status: "completed",
                currentTask: "Deployed to production",
                progressPercent: 100,
                sessionDurationMs: 300000, // 5 minutes
            });
            const result = await extractLearnings(client, session);
            expect(result.count).toBe(0);
            const lessons = client.createdEntities.filter((e) => e.type === "lesson");
            expect(lessons).toHaveLength(0);
        });
        test("completed session with blockers creates lesson entity", async () => {
            const client = makeMockClient();
            const session = makeSession({
                status: "completed",
                currentTask: "Deployed to production",
                progressPercent: 100,
                sessionDurationMs: 300000, // 5 minutes
                blockers: ["API rate limit hit"],
            });
            const _result = await extractLearnings(client, session);
            // 1 error entity (Rule 1) + 1 lesson entity (Rule 2)
            const lesson = client.createdEntities.find((e) => e.type === "lesson");
            expect(lesson).toBeTruthy();
            expect(lesson.title).toContain("Session:");
            expect(lesson.memory_tier).toBe("episode");
            expect(lesson.confidence).toBe(0.7);
            expect(lesson.content).toContain("Deployed to production");
            expect(lesson.content).toContain("5 minutes");
        });
        test("completed session with incomplete subtasks creates lesson entity", async () => {
            const client = makeMockClient();
            const session = makeSession({
                status: "completed",
                currentTask: "Finished main work",
                progressPercent: 100,
                cardSubtasks: [
                    { title: "Step 1", done: true },
                    { title: "Step 2", done: false },
                ],
            });
            const _result = await extractLearnings(client, session);
            const lesson = client.createdEntities.find((e) => e.type === "lesson");
            expect(lesson).toBeTruthy();
            expect(lesson.title).toContain("Session:");
        });
        test("paused session does NOT create lesson entity", async () => {
            const client = makeMockClient();
            const session = makeSession({ status: "paused", blockers: undefined });
            const result = await extractLearnings(client, session);
            expect(result.count).toBe(0);
            const lessons = client.createdEntities.filter((e) => e.type === "lesson");
            expect(lessons).toHaveLength(0);
        });
    });
    describe("Rule 3: Bug label + completed creates solution entity", () => {
        test('card with "bug" label creates solution entity', async () => {
            const client = makeMockClient();
            const session = makeSession({
                cardLabels: ["bug", "priority-high"],
                status: "completed",
            });
            const _result = await extractLearnings(client, session);
            const solutions = client.createdEntities.filter((e) => e.type === "solution");
            expect(solutions).toHaveLength(1);
            expect(solutions[0].title).toContain("Solution:");
            expect(solutions[0].memory_tier).toBe("reference");
            expect(solutions[0].confidence).toBe(0.8);
            expect(solutions[0].tags ?? []).toContain("bug-fix");
        });
        test('case-insensitive "Bug" label works', async () => {
            const client = makeMockClient();
            const session = makeSession({
                cardLabels: ["Bug"],
                status: "completed",
            });
            await extractLearnings(client, session);
            const solutions = client.createdEntities.filter((e) => e.type === "solution");
            // The check is .toLowerCase(), so "Bug" matches
            expect(solutions).toHaveLength(1);
        });
        test("bug label + paused does NOT create solution", async () => {
            const client = makeMockClient();
            const session = makeSession({
                cardLabels: ["bug"],
                status: "paused",
            });
            await extractLearnings(client, session);
            const solutions = client.createdEntities.filter((e) => e.type === "solution");
            expect(solutions).toHaveLength(0);
        });
        for (const label of ["fix", "hotfix", "defect", "error"]) {
            test(`"${label}" label also triggers solution creation`, async () => {
                const client = makeMockClient();
                const session = makeSession({
                    cardLabels: [label],
                    status: "completed",
                });
                await extractLearnings(client, session);
                const solutions = client.createdEntities.filter((e) => e.type === "solution");
                expect(solutions).toHaveLength(1);
            });
        }
    });
    describe("Card labels are included in tags", () => {
        test("first 3 card labels are appended to tags", async () => {
            const client = makeMockClient();
            const session = makeSession({
                cardLabels: ["feature", "frontend", "urgent", "extra"],
                status: "completed",
                blockers: ["test blocker"],
            });
            await extractLearnings(client, session);
            const lesson = client.createdEntities.find((e) => e.type === "lesson");
            const tags = lesson.tags;
            expect(tags).toContain("feature");
            expect(tags).toContain("frontend");
            expect(tags).toContain("urgent");
            expect(tags).not.toContain("extra"); // Only first 3
        });
    });
    describe("Error handling", () => {
        test("individual entity creation failure does not break others", async () => {
            let callCount = 0;
            const client = {
                createMemoryEntity: mock(async (_data) => {
                    callCount++;
                    if (callCount === 1)
                        throw new Error("Network error");
                    return { entity: { id: `entity-${callCount}` } };
                }),
            };
            const session = makeSession({
                blockers: ["blocker-1"],
                status: "completed",
            });
            // blocker creates error (fails), completed creates lesson (succeeds)
            const result = await extractLearnings(client, session);
            // Only 1 succeeded out of 2
            expect(result.entityIds).toHaveLength(1);
            expect(result.count).toBe(1);
        });
    });
    describe("No workspace returns empty", () => {
        test("returns empty when no workspace configured", async () => {
            // Temporarily override
            const origWs = mockWorkspaceId;
            mockWorkspaceId = null;
            const client = makeMockClient();
            const session = makeSession();
            const result = await extractLearnings(client, session);
            expect(result.count).toBe(0);
            expect(result.entityIds).toHaveLength(0);
            // Restore
            mockWorkspaceId = origWs;
        });
    });
});
describe("extractLearnings - entity metadata", () => {
    test("stores card_id and source in metadata", async () => {
        const client = makeMockClient();
        const session = makeSession({
            cardId: "card-abc",
            status: "completed",
            blockers: ["test blocker"],
        });
        await extractLearnings(client, session);
        const lesson = client.createdEntities.find((e) => e.type === "lesson");
        const meta = lesson.metadata;
        expect(meta.source).toBe("active_learning");
        expect(meta.card_id).toBe("card-abc");
    });
    test("lesson entity carries agent_identifier", async () => {
        const client = makeMockClient();
        const session = makeSession({
            agentIdentifier: "my-agent-v2",
            status: "completed",
            blockers: ["test blocker"],
        });
        await extractLearnings(client, session);
        const lesson = client.createdEntities.find((e) => e.type === "lesson");
        expect(lesson.agent_identifier).toBe("my-agent-v2");
    });
    test("session lesson content includes agent name", async () => {
        const client = makeMockClient();
        const session = makeSession({
            agentName: "TestBot",
            status: "completed",
            blockers: ["test blocker"],
        });
        await extractLearnings(client, session);
        const lesson = client.createdEntities.find((e) => e.type === "lesson");
        expect(lesson.content).toContain("TestBot");
    });
    test("clean completed session without meaningful content skips lesson", async () => {
        const client = makeMockClient();
        const session = makeSession({
            status: "completed",
            currentTask: undefined,
            progressPercent: undefined,
            sessionDurationMs: undefined,
            blockers: undefined,
        });
        const result = await extractLearnings(client, session);
        expect(result.count).toBe(0);
        const lessons = client.createdEntities.filter((e) => e.type === "lesson");
        expect(lessons).toHaveLength(0);
    });
});
describe("extractLearnings - returns correct entityIds", () => {
    test("entityIds array matches returned count", async () => {
        const client = makeMockClient();
        const session = makeSession({
            cardLabels: ["bug"],
            blockers: ["error occurred"],
            status: "completed",
        });
        const result = await extractLearnings(client, session);
        // error + lesson + solution = 3
        expect(result.count).toBe(result.entityIds.length);
        expect(result.entityIds).toHaveLength(3);
        // All IDs should be truthy strings
        for (const id of result.entityIds) {
            expect(typeof id).toBe("string");
            expect(id.length).toBeGreaterThan(0);
        }
    });
    test("entityIds are unique", async () => {
        const client = makeMockClient();
        const session = makeSession({
            cardLabels: ["bug"],
            blockers: ["err-1", "err-2"],
            status: "completed",
        });
        const result = await extractLearnings(client, session);
        const unique = new Set(result.entityIds);
        expect(unique.size).toBe(result.entityIds.length);
    });
});
describe("extractLearnings - fire-and-forget calls don't crash", () => {
    test("works with full client (autoExpandGraph and detectAndCreatePatterns paths)", async () => {
        const client = makeFullMockClient();
        const session = makeSession({ status: "completed", blockers: ["test"] });
        // Should not throw even when graph expansion / pattern detection run
        const result = await extractLearnings(client, session);
        expect(result.count).toBeGreaterThan(0);
        // Flush microtasks so fire-and-forget paths complete
        await new Promise((r) => setTimeout(r, 10));
        // searchMemoryEntities was called by autoExpandGraph
        expect(client.searchMemoryEntities).toHaveBeenCalled();
    });
    test("client errors in fire-and-forget paths do not surface to caller", async () => {
        const client = makeFullMockClient();
        // Make the search throw - should be swallowed
        client.searchMemoryEntities = mock(async () => {
            throw new Error("Search service unavailable");
        });
        const session = makeSession({ status: "completed", blockers: ["test"] });
        const result = await extractLearnings(client, session);
        // Main extraction still succeeded (error + lesson)
        expect(result.count).toBe(2);
    });
});
