import { describe, test, expect, beforeAll, afterAll, mock } from "bun:test"; import { db } from "@/db"; import { annotators, paragraphs, assignments, humanLabels, adjudications, } from "@/db/schema"; import { eq, and, inArray } from "drizzle-orm"; const cookieJar = new Map(); const mockCookieStore = { get(name: string) { const value = cookieJar.get(name); return value ? { value } : undefined; }, set(name: string, value: string, _opts?: unknown) { cookieJar.set(name, value); }, delete(name: string) { cookieJar.delete(name); }, }; mock.module("next/headers", () => ({ cookies: async () => mockCookieStore, })); const { createSession } = await import("@/lib/auth"); const { GET, POST } = await import("../route"); const ADMIN_USER = { id: "admin", displayName: "Admin", password: "adminpass", }; const TEST_ANNOTATORS = [ { id: "adj-ann-1", displayName: "Annotator 1", password: "pass1" }, { id: "adj-ann-2", displayName: "Annotator 2", password: "pass2" }, { id: "adj-ann-3", displayName: "Annotator 3", password: "pass3" }, ]; const TEST_PARAGRAPHS = [ { id: "adj-para-agree", text: "The board of directors oversees the company cybersecurity risk management.", wordCount: 11, paragraphIndex: 0, companyName: "Test Corp", cik: "0009999999", ticker: "ADJT", filingType: "10-K", filingDate: "2024-03-15", fiscalYear: 2024, accessionNumber: "0009999999-24-000001", secItem: "Item 1C", }, { id: "adj-para-disagree", text: "We employ third-party vendors and manage internal risk processes simultaneously.", wordCount: 10, paragraphIndex: 1, companyName: "Test Corp", cik: "0009999999", ticker: "ADJT", filingType: "10-K", filingDate: "2024-03-15", fiscalYear: 2024, accessionNumber: "0009999999-24-000001", secItem: "Item 1C", }, { id: "adj-para-majority", text: "Our CISO reports quarterly to the board on cybersecurity matters.", wordCount: 10, paragraphIndex: 2, companyName: "Test Corp", cik: "0009999999", ticker: "ADJT", filingType: "10-K", filingDate: "2024-03-15", fiscalYear: 2024, accessionNumber: "0009999999-24-000001", secItem: "Item 1C", }, ]; const PARAGRAPH_IDS = TEST_PARAGRAPHS.map((p) => p.id); const ALL_ANNOTATOR_IDS = [ ADMIN_USER.id, ...TEST_ANNOTATORS.map((a) => a.id), ]; async function cleanup() { // Clean in dependency order await db .delete(adjudications) .where(inArray(adjudications.paragraphId, PARAGRAPH_IDS)); await db .delete(humanLabels) .where(inArray(humanLabels.paragraphId, PARAGRAPH_IDS)); await db .delete(assignments) .where(inArray(assignments.paragraphId, PARAGRAPH_IDS)); await db.delete(paragraphs).where(inArray(paragraphs.id, PARAGRAPH_IDS)); for (const a of TEST_ANNOTATORS) { await db.delete(annotators).where(eq(annotators.id, a.id)); } // Only delete admin if it exists (don't fail if not present) await db.delete(annotators).where(eq(annotators.id, ADMIN_USER.id)); } beforeAll(async () => { await cleanup(); // Create test data await db .insert(annotators) .values([ADMIN_USER, ...TEST_ANNOTATORS]) .onConflictDoNothing(); await db.insert(paragraphs).values(TEST_PARAGRAPHS); // Assign all paragraphs to all 3 annotators const assignmentRows = TEST_PARAGRAPHS.flatMap((p) => TEST_ANNOTATORS.map((a) => ({ paragraphId: p.id, annotatorId: a.id, })), ); await db.insert(assignments).values(assignmentRows); // Set up admin session cookieJar.clear(); await createSession(ADMIN_USER.id); }); afterAll(async () => { await cleanup(); }); describe("GET /api/adjudicate", () => { test("returns empty queue when no labels exist", async () => { const res = await GET(); const data = await res.json(); expect(res.status).toBe(200); expect(data.queue).toEqual([]); }); test("auto-resolves consensus (3/3 agree) and returns empty queue", async () => { // Insert 3 agreeing labels for adj-para-agree const labels = TEST_ANNOTATORS.map((a) => ({ paragraphId: "adj-para-agree", annotatorId: a.id, contentCategory: "Board Governance", specificityLevel: 3, sessionId: "test-adj-session", })); await db.insert(humanLabels).values(labels); const res = await GET(); const data = await res.json(); expect(res.status).toBe(200); expect(data.queue).toEqual([]); // Verify adjudication was created const [adj] = await db .select() .from(adjudications) .where(eq(adjudications.paragraphId, "adj-para-agree")); expect(adj).toBeDefined(); expect(adj.method).toBe("consensus"); expect(adj.finalCategory).toBe("Board Governance"); expect(adj.finalSpecificity).toBe(3); }); test("auto-resolves majority (2/3 agree) and returns empty queue", async () => { // Insert 2 agreeing + 1 disagreeing for adj-para-majority await db.insert(humanLabels).values([ { paragraphId: "adj-para-majority", annotatorId: TEST_ANNOTATORS[0].id, contentCategory: "Management Role", specificityLevel: 2, sessionId: "test-adj-session", }, { paragraphId: "adj-para-majority", annotatorId: TEST_ANNOTATORS[1].id, contentCategory: "Management Role", specificityLevel: 2, sessionId: "test-adj-session", }, { paragraphId: "adj-para-majority", annotatorId: TEST_ANNOTATORS[2].id, contentCategory: "Board Governance", specificityLevel: 3, sessionId: "test-adj-session", }, ]); const res = await GET(); const data = await res.json(); expect(res.status).toBe(200); expect(data.queue).toEqual([]); // Verify adjudication was created const [adj] = await db .select() .from(adjudications) .where(eq(adjudications.paragraphId, "adj-para-majority")); expect(adj).toBeDefined(); expect(adj.method).toBe("majority"); expect(adj.finalCategory).toBe("Management Role"); expect(adj.finalSpecificity).toBe(2); }); test("returns 3-way disagreement in queue", async () => { // Insert 3 fully disagreeing labels for adj-para-disagree await db.insert(humanLabels).values([ { paragraphId: "adj-para-disagree", annotatorId: TEST_ANNOTATORS[0].id, contentCategory: "Third-Party Risk", specificityLevel: 1, sessionId: "test-adj-session", }, { paragraphId: "adj-para-disagree", annotatorId: TEST_ANNOTATORS[1].id, contentCategory: "Risk Management Process", specificityLevel: 2, sessionId: "test-adj-session", }, { paragraphId: "adj-para-disagree", annotatorId: TEST_ANNOTATORS[2].id, contentCategory: "Management Role", specificityLevel: 3, sessionId: "test-adj-session", }, ]); const res = await GET(); const data = await res.json(); expect(res.status).toBe(200); expect(data.queue).toHaveLength(1); expect(data.queue[0].paragraphId).toBe("adj-para-disagree"); expect(data.queue[0].labels).toHaveLength(3); expect(data.queue[0].splitSeverity).toBe(3); }); }); describe("POST /api/adjudicate", () => { test("resolves an adjudication", async () => { const req = new Request("http://localhost:3000/api/adjudicate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ paragraphId: "adj-para-disagree", finalCategory: "Third-Party Risk", finalSpecificity: 2, notes: "Resolved via discussion", }), }); const res = await POST(req); const data = await res.json(); expect(res.status).toBe(200); expect(data.ok).toBe(true); // Verify adjudication row const [adj] = await db .select() .from(adjudications) .where(eq(adjudications.paragraphId, "adj-para-disagree")); expect(adj).toBeDefined(); expect(adj.method).toBe("discussion"); expect(adj.finalCategory).toBe("Third-Party Risk"); expect(adj.finalSpecificity).toBe(2); expect(adj.adjudicatorId).toBe("admin"); expect(adj.notes).toBe("Resolved via discussion"); }); test("GET returns empty queue after resolution", async () => { const res = await GET(); const data = await res.json(); expect(res.status).toBe(200); expect(data.queue).toEqual([]); }); test("rejects invalid category", async () => { const req = new Request("http://localhost:3000/api/adjudicate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ paragraphId: "adj-para-disagree", finalCategory: "Invalid", finalSpecificity: 2, }), }); const res = await POST(req); expect(res.status).toBe(400); }); test("rejects invalid specificity", async () => { const req = new Request("http://localhost:3000/api/adjudicate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ paragraphId: "adj-para-disagree", finalCategory: "Board Governance", finalSpecificity: 5, }), }); const res = await POST(req); expect(res.status).toBe(400); }); }); describe("GET /api/adjudicate - unauthorized", () => { test("returns 401 without session", async () => { cookieJar.clear(); const res = await GET(); expect(res.status).toBe(401); }); test("returns 403 for non-admin user", async () => { cookieJar.clear(); await createSession(TEST_ANNOTATORS[0].id); const res = await GET(); expect(res.status).toBe(403); // Restore admin session for other tests cookieJar.clear(); await createSession(ADMIN_USER.id); }); });