import { DEFAULT_OPENAI_MODEL } from "@capakraken/shared"; import { TRPCError } from "@trpc/server"; import { beforeEach, describe, expect, it, vi } from "vitest"; const aiCompletionCreate = vi.fn(); vi.mock("../ai-client.js", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, createAiClient: vi.fn(() => ({ chat: { completions: { create: aiCompletionCreate, }, }, })), isAiConfigured: vi.fn((settings: unknown) => settings != null), loggedAiCall: vi.fn(async (_provider, _model, _promptLength, fn) => fn()), parseAiError: vi.fn((error: unknown) => error instanceof Error ? error.message : String(error)), }; }); import { createAiClient, isAiConfigured, loggedAiCall } from "../ai-client.js"; import { countBusinessDays, generateProjectNarrative, getCachedNarrative, } from "../router/insights-procedure-support.js"; function createContext(db: Record) { return { db: db as never }; } describe("insights procedure support", () => { beforeEach(() => { vi.clearAllMocks(); aiCompletionCreate.mockResolvedValue({ choices: [{ message: { content: "Project is stable but needs one more staffed role." } }], }); }); it("counts only weekdays between two dates", () => { expect( countBusinessDays( new Date("2026-03-27T00:00:00.000Z"), new Date("2026-03-31T00:00:00.000Z"), ), ).toBe(3); }); it("generates and stores a project narrative", async () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-03-31T10:15:00.000Z")); const projectFindUnique = vi.fn().mockResolvedValue({ id: "project_1", name: "Apollo", shortCode: "APO", status: "ACTIVE", startDate: new Date("2026-03-03T00:00:00.000Z"), endDate: new Date("2026-04-30T00:00:00.000Z"), budgetCents: 200_000_00, dynamicFields: { existingFlag: true }, demandRequirements: [ { id: "dem_1", role: "Developer", headcount: 2, hoursPerDay: 8, startDate: new Date("2026-03-10T00:00:00.000Z"), endDate: new Date("2026-04-10T00:00:00.000Z"), status: "OPEN", _count: { assignments: 1 }, }, ], assignments: [ { id: "asn_1", role: "Developer", hoursPerDay: 8, startDate: new Date("2026-03-10T00:00:00.000Z"), endDate: new Date("2026-04-15T00:00:00.000Z"), status: "ACTIVE", dailyCostCents: 50_000, resource: { displayName: "Carol Danvers" }, }, ], }); const projectUpdate = vi.fn().mockResolvedValue({ id: "project_1" }); try { const result = await generateProjectNarrative( createContext({ project: { findUnique: projectFindUnique, update: projectUpdate, }, systemSettings: { findUnique: vi.fn().mockResolvedValue({ id: "singleton", aiProvider: "openai", azureOpenAiDeployment: DEFAULT_OPENAI_MODEL, aiMaxCompletionTokens: 220, aiTemperature: 0.4, }), }, }), { projectId: "project_1" }, ); expect(isAiConfigured).toHaveBeenCalledWith( expect.objectContaining({ azureOpenAiDeployment: DEFAULT_OPENAI_MODEL }), ); expect(createAiClient).toHaveBeenCalledWith( expect.objectContaining({ azureOpenAiDeployment: DEFAULT_OPENAI_MODEL }), ); expect(loggedAiCall).toHaveBeenCalledOnce(); expect(aiCompletionCreate).toHaveBeenCalledWith( expect.objectContaining({ model: DEFAULT_OPENAI_MODEL, max_completion_tokens: 220, temperature: 0.4, messages: expect.arrayContaining([ expect.objectContaining({ role: "system" }), expect.objectContaining({ role: "user", content: expect.stringContaining("Staffing: 1/2 positions filled (50%)"), }), ]), }), ); expect(projectUpdate).toHaveBeenCalledWith({ where: { id: "project_1" }, data: { dynamicFields: { existingFlag: true, aiNarrative: "Project is stable but needs one more staffed role.", aiNarrativeGeneratedAt: "2026-03-31T10:15:00.000Z", }, }, }); expect(result).toEqual({ narrative: "Project is stable but needs one more staffed role.", generatedAt: "2026-03-31T10:15:00.000Z", }); } finally { vi.useRealTimers(); } }); it("fails when AI settings are missing", async () => { await expect( generateProjectNarrative( createContext({ project: { findUnique: vi.fn().mockResolvedValue({ id: "project_1", name: "Apollo", shortCode: "APO", status: "ACTIVE", startDate: new Date("2026-03-03T00:00:00.000Z"), endDate: new Date("2026-04-30T00:00:00.000Z"), budgetCents: 200_000_00, dynamicFields: null, demandRequirements: [], assignments: [], }), update: vi.fn(), }, systemSettings: { findUnique: vi.fn().mockResolvedValue(null), }, }), { projectId: "project_1" }, ), ).rejects.toEqual( expect.objectContaining>({ code: "PRECONDITION_FAILED", message: "AI is not configured. Please set credentials in Admin → Settings.", }), ); expect(createAiClient).not.toHaveBeenCalled(); expect(loggedAiCall).not.toHaveBeenCalled(); }); it("fails when AI configuration is incomplete", async () => { vi.mocked(isAiConfigured).mockReturnValue(false); await expect( generateProjectNarrative( createContext({ project: { findUnique: vi.fn().mockResolvedValue({ id: "project_1", name: "Apollo", shortCode: "APO", status: "ACTIVE", startDate: new Date("2026-03-03T00:00:00.000Z"), endDate: new Date("2026-04-30T00:00:00.000Z"), budgetCents: 200_000_00, dynamicFields: null, demandRequirements: [], assignments: [], }), update: vi.fn(), }, systemSettings: { findUnique: vi.fn().mockResolvedValue({ id: "singleton", aiProvider: "openai", azureOpenAiDeployment: null, }), }, }), { projectId: "project_1" }, ), ).rejects.toEqual( expect.objectContaining>({ code: "PRECONDITION_FAILED", message: "AI is not configured. Please set credentials in Admin → Settings.", }), ); expect(createAiClient).not.toHaveBeenCalled(); expect(aiCompletionCreate).not.toHaveBeenCalled(); }); it("returns the cached narrative for a project", async () => { const result = await getCachedNarrative( createContext({ project: { findUnique: vi.fn().mockResolvedValue({ dynamicFields: { aiNarrative: "Cached summary", aiNarrativeGeneratedAt: "2026-03-30T08:00:00.000Z", }, }), }, }), { projectId: "project_1" }, ); expect(result).toEqual({ narrative: "Cached summary", generatedAt: "2026-03-30T08:00:00.000Z", }); }); it("fails when the project does not exist for the cached narrative lookup", async () => { await expect( getCachedNarrative( createContext({ project: { findUnique: vi.fn().mockResolvedValue(null), }, }), { projectId: "missing" }, ), ).rejects.toEqual( expect.objectContaining>({ code: "NOT_FOUND", message: "Project not found", }), ); }); });