208 lines
6.4 KiB
JavaScript
208 lines
6.4 KiB
JavaScript
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",
|
|
);
|
|
});
|
|
});
|