import { beforeEach, describe, expect, it, vi } from "vitest"; const mockAnalyseScenario = vi.fn(); vi.mock("@/lib/analysis.js", () => ({ analyseScenario: (...args) => mockAnalyseScenario(...args), })); function makeAnalysisResult(overrides = {}) { return { success: true, validationStatus: "valid", modelName: "llama3", responseDurationMs: 321, rawResponse: "{}", promptVersion: "v0.3", reconstruction: { summary: "Revenue and complaints diverge", actors: [], systemsOrObjects: [], expectedStates: [], observedStates: [ { id: "obs-1", label: "Revenue up", description: "Revenue up 15%", confidence: "high", }, ], differences: [], knownTransitions: [], unexplainedTransitions: [], contradictions: [], importantUnknowns: [ { id: "unk-1", label: "Complaint rate denominator", description: "Need the denominator for complaint rate", confidence: "high", }, ], plausibleInterpretations: [], }, evidence: [], nextQuestion: { id: "q-1", question: "What denominator is being used for the complaint rate?", }, compatibilityApplied: false, compatibilityChanges: [], compatibilityWarnings: [], ...overrides, }; } describe("lib/graph/orchestrator startCase", () => { beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); }); it("passes a valid request through to analyseScenario", async () => { mockAnalyseScenario.mockResolvedValue(makeAnalysisResult()); const { startCase } = await import("@/lib/graph/orchestrator.js"); const result = await startCase({ scenario: "Revenue increased while complaint counts rose faster.", promptVersion: "v0.3", }); expect(result.success).toBe(true); expect(mockAnalyseScenario).toHaveBeenCalledWith( "Revenue increased while complaint counts rose faster.", { promptVersion: "v0.3" }, ); }); it("rejects invalid request input without throwing", async () => { const { startCase } = await import("@/lib/graph/orchestrator.js"); const result = await startCase({ scenario: "" }); expect(result).toMatchObject({ success: false, error: "Invalid start-case request", statusCode: 400, }); expect(result.validationErrors).toBeInstanceOf(Array); expect(mockAnalyseScenario).not.toHaveBeenCalled(); }); it("builds a valid graph on successful analysis", async () => { mockAnalyseScenario.mockResolvedValue(makeAnalysisResult()); const { startCase } = await import("@/lib/graph/orchestrator.js"); const result = await startCase({ scenario: "Scenario text" }); expect(result.success).toBe(true); expect(result.situationGraph.centralStatement).toBe("Scenario text"); expect(result.situationGraph.currentSummary).toContain("Nodes:"); expect(result.diagnostics).toMatchObject({ validationStatus: "valid", modelName: "llama3", graphReferenceValidation: { valid: true, errors: [] }, }); }); it("applies active unknown selection to the graph", async () => { mockAnalyseScenario.mockResolvedValue(makeAnalysisResult()); const { startCase } = await import("@/lib/graph/orchestrator.js"); const result = await startCase({ scenario: "Scenario text" }); expect(result.success).toBe(true); expect(result.situationGraph.activeUnknownNodeId).toBeTruthy(); }); it("returns structured failure when graph reference validation fails", async () => { mockAnalyseScenario.mockResolvedValue(makeAnalysisResult()); const utils = await import("@/lib/graph/utils.js"); const validateSpy = vi .spyOn(utils, "validateGraphReferences") .mockReturnValue({ valid: false, errors: ['Edge references non-existent toNodeId "missing"'], }); const { startCase } = await import("@/lib/graph/orchestrator.js"); const result = await startCase({ scenario: "Scenario text" }); expect(result).toMatchObject({ success: false, error: "Situation graph reference validation failed", validationErrors: ['Edge references non-existent toNodeId "missing"'], statusCode: 500, }); expect(result.diagnostics.graphReferenceValidation.valid).toBe(false); validateSpy.mockRestore(); }); it("preserves analysis/provider failure details", async () => { mockAnalyseScenario.mockResolvedValue({ success: false, error: "Provider unavailable", errors: ["socket hang up"], rawResponse: null, modelName: "llama3", responseDurationMs: 99, promptVersion: "v0.3", validationStatus: "invalid", statusCode: 502, }); const { startCase } = await import("@/lib/graph/orchestrator.js"); const result = await startCase({ scenario: "Scenario text" }); expect(result).toMatchObject({ success: false, error: "Provider unavailable", analysisErrors: ["socket hang up"], statusCode: 502, }); }); it("returns null selectedQuestion when analysis has no nextQuestion", async () => { mockAnalyseScenario.mockResolvedValue( makeAnalysisResult({ nextQuestion: undefined }), ); const { startCase } = await import("@/lib/graph/orchestrator.js"); const result = await startCase({ scenario: "Scenario text" }); expect(result.success).toBe(true); expect(result.selectedQuestion).toBeNull(); }); it("includes compatibility diagnostics when provided by analysis", async () => { mockAnalyseScenario.mockResolvedValue( makeAnalysisResult({ compatibilityApplied: true, compatibilityChanges: [ { path: ["evidence", 0, "source"], change: "Converted null source to undefined", }, ], compatibilityWarnings: [ "Applied deterministic reconstruction compatibility normalisation", ], }), ); const { startCase } = await import("@/lib/graph/orchestrator.js"); const result = await startCase({ scenario: "Scenario text" }); expect(result.diagnostics.compatibilityApplied).toBe(true); expect(result.diagnostics.compatibilityChanges).toHaveLength(1); }); it("exports placeholder updateCase", async () => { const { updateCase } = await import("@/lib/graph/orchestrator.js"); await expect(updateCase()).rejects.toThrow( "updateCase is not implemented yet", ); }); });