import { afterEach, describe, expect, it, vi } from "vitest"; const generateLlmText = vi.fn(); vi.mock("@/lib/llm-client", () => ({ generateLlmText, })); afterEach(() => { generateLlmText.mockReset(); }); describe("/api/chat route", () => { it("returns resolved context and thinking summary for mock package asks", async () => { const { POST } = await import("./route"); const seed = (await import("@/agentic_pm_demo_codex_plans/data/work-packages.seed.json")) .default as Array>; const response = await POST( new Request("http://localhost/api/chat", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ messages: [ { role: "user", content: "@SRS ask What does verification method mean?" }, ], workPackages: seed, selectedWorkPackageId: "wp-srs", parsedCommand: { referencedPackageName: "System Requirements Specification", mode: "ask", instruction: "What does verification method mean?", }, }), }), ); const payload = (await response.json()) as { boardAction?: { type?: string }; resolvedContext?: { scope?: string; workPackageId?: string | null; mode?: string; provider?: string; boardMutationPolicy?: string; }; thinkingSummary?: string[]; }; expect(payload.boardAction?.type).toBe("none"); expect(payload.resolvedContext).toMatchObject({ scope: "package", workPackageId: "wp-srs", mode: "ask", provider: "mock", boardMutationPolicy: "none", }); expect(payload.thinkingSummary?.join("\n")).toContain("Resolved scope to System Requirements Specification."); expect(payload.thinkingSummary?.join("\n")).toContain("Using mock mode."); }); it("falls back to package-scoped mock ask guidance when the live model call throws", async () => { generateLlmText.mockRejectedValueOnce(new Error("Invalid JSON")); const { POST } = await import("./route"); const seed = (await import("@/agentic_pm_demo_codex_plans/data/work-packages.seed.json")) .default as Array>; const response = await POST( new Request("http://localhost/api/chat", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ messages: [ { role: "user", content: "@SRS ask What does verification method mean?" }, ], workPackages: seed, selectedWorkPackageId: "wp-srs", parsedCommand: { referencedPackageName: "System Requirements Specification", mode: "ask", instruction: "What does verification method mean?", }, llmConfig: { apiKey: "live-key", baseUrl: "https://api.example.com/v1", model: "gpt-test", }, }), }), ); const payload = (await response.json()) as { assistantMessage: string; boardAction?: { type?: string }; resolvedContext?: { scope?: string; workPackageId?: string | null; mode?: string; provider?: string; boardMutationPolicy?: string; }; thinkingSummary?: string[]; }; expect(payload.assistantMessage).toContain("For SRS, the objective here is:"); expect(payload.assistantMessage).toContain("verification"); expect(payload.assistantMessage).not.toContain("Network or parsing error calling LLM"); expect(payload.boardAction?.type).toBe("none"); expect(payload.resolvedContext).toMatchObject({ scope: "package", workPackageId: "wp-srs", mode: "ask", provider: "live", boardMutationPolicy: "none", }); expect(payload.thinkingSummary?.join("\n")).toContain( "Provider call failed; preserving the package-scoped fallback.", ); }); it("falls back to a scoped mock plan update when the live model call throws", async () => { generateLlmText.mockRejectedValueOnce(new Error("Invalid JSON")); const { POST } = await import("./route"); const seed = (await import("@/agentic_pm_demo_codex_plans/data/work-packages.seed.json")) .default as Array>; const response = await POST( new Request("http://localhost/api/chat", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ messages: [ { role: "user", content: "@SRS plan Break this into review steps." }, ], workPackages: seed, selectedWorkPackageId: "wp-srs", parsedCommand: { referencedPackageName: "System Requirements Specification", mode: "plan", instruction: "Break this into review steps.", }, llmConfig: { apiKey: "live-key", baseUrl: "https://api.example.com/v1", model: "gpt-test", }, }), }), ); const payload = (await response.json()) as { assistantMessage: string; boardAction?: { type?: string; workPackageId?: string | null; fields?: { tasks?: unknown[]; status?: string }; }; resolvedContext?: { scope?: string; workPackageId?: string | null; mode?: string; provider?: string; boardMutationPolicy?: string; }; thinkingSummary?: string[]; }; expect(payload.assistantMessage).toContain("Planned next steps for SRS"); expect(payload.boardAction?.type).toBe("update"); expect(payload.boardAction?.workPackageId).toBe("wp-srs"); expect(payload.boardAction?.fields?.tasks?.length).toBeGreaterThan(0); expect(payload.boardAction?.fields?.status).toBe("in_progress"); expect(payload.resolvedContext).toMatchObject({ scope: "package", workPackageId: "wp-srs", mode: "plan", provider: "live", boardMutationPolicy: "selected_package_only", }); expect(payload.thinkingSummary?.join("\n")).toContain( "Provider call failed; preserving the package-scoped fallback.", ); }); it("neutralizes live package updates that target a different work package", async () => { generateLlmText.mockResolvedValueOnce({ text: JSON.stringify({ assistantMessage: "Changed the wrong package.", boardAction: { type: "update", workPackageId: "wp-final-concept", fields: { objective: "This should not land on Final Concept.", }, }, }), endpoint: "https://api.example.com/v1/chat/completions", requestPreview: "request", responsePreview: "response", }); const { POST } = await import("./route"); const seed = (await import("@/agentic_pm_demo_codex_plans/data/work-packages.seed.json")) .default as Array>; const response = await POST( new Request("http://localhost/api/chat", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ messages: [ { role: "user", content: "@SRS change Add cybersecurity acceptance criteria." }, ], workPackages: seed, selectedWorkPackageId: "wp-srs", parsedCommand: { referencedPackageName: "System Requirements Specification", mode: "change", instruction: "Add cybersecurity acceptance criteria.", }, llmConfig: { apiKey: "live-key", baseUrl: "https://api.example.com/v1", model: "gpt-test", }, }), }), ); const payload = (await response.json()) as { boardAction?: { type?: string; workPackageId?: string | null }; thinkingSummary?: string[]; }; expect(payload.boardAction).toMatchObject({ type: "none", workPackageId: null, }); expect(payload.thinkingSummary?.join("\n")).toContain( "Blocked a board update that targeted a different work package.", ); }); });