import { describe, it, expect } from "vitest"; import assessInvestigationState from "@/lib/assessment/investigation-state-assessor.js"; /* ── Helper: build scenario fixture data inline ─────────── */ function mkN(id, label, opts = {}) { const kind = opts.kind || "unknown"; const status = opts.status || (kind === "unknown" ? "unknown" : "known"); const confidence = opts.confidence || (kind === "unknown" ? "low" : "high"); return { id, label, description: label, kind, status, confidence, evidenceIds: [], dependsOn: [], affects: [], childIds: [] }; } function makeInput(graphOpts = {}, scenarioName) { const scenarios = getScenarios(); const turn = scenarios[scenarioName]; if (!turn) return null; const nodes = (graphOpts.nodes ?? turn.nodes); const resolved = graphOpts.resolved ?? turn.resolvedNodeIds; const activeId = graphOpts.activeUnknownNodeId ?? turn.activeUnknownNodeId; const edges = graphOpts.edges ?? turn.edges; const summary = graphOpts.summary ?? turn.currentSummary; return { situationGraph: { centralStatement: turn.centralStatement, currentSummary: summary, nodes: Array.isArray(nodes) ? nodes : nodes, edges: Array.isArray(edges) ? edges : [], activeUnknownNodeId: activeId, resolvedNodeIds: resolved }, selectedQuestion: turn.selectedQuestion, noQuestionReason: turn.noQuestionReason, diagnostics: { promptVersion: "v0.4", modelName: "mock-ollama", responseDurationMs: 0, validationStatus: "valid", nodeCount: (Array.isArray(nodes) ? nodes.length : 0), edgeCount: (Array.isArray(edges) ? edges.length : 0), reasoningPattern: turn.diagnosticReasoningPattern || null } }; } /* ── Mock scenarios for test data ────────────────────────── */ function getScenarios() { return { "comparison-turn-0": { centralStatement: "Product A has a 4.2 star average rating while Product B averages 4.6 stars across 10,000+ reviews each.", nodes: [ mkN("obs-1", "Product A average rating: 4.2 stars", { kind: "observation", status: "known", confidence: "high" }), mkN("obs-2", "Product B average rating: 4.6 stars", { kind: "observation", status: "known", confidence: "high" }), mkN("obs-3", "Both products have 10,000+ reviews", { kind: "observation", status: "known", confidence: "high" }), mkN("state-1", "Comparing two products before purchase decision", { kind: "state", status: "provisional", confidence: "medium" }), mkN("u-1", "Whether the rating systems are comparable") ], edges: [], resolvedNodeIds: [], activeUnknownNodeId: "u-1", selectedQuestion: { nodeId: "u-1", question: "Are both products rated on the same validated scale?", reason: "comparability_check" }, noQuestionReason: null, currentSummary: "Two products have been rated highly, but we do not yet know whether their ratings are measured the same way.", diagnosticReasoningPattern: "comparability_check" }, "comparison-turn-1": { centralStatement: "Product A has a 4.2 star average rating while Product B averages 4.6 stars across 10,000+ reviews each.", nodes: [ mkN("obs-1", "Product A average rating: 4.2 stars", { kind: "observation", status: "known", confidence: "high" }), mkN("obs-2", "Product B average rating: 4.6 stars", { kind: "observation", status: "known", confidence: "high" }), mkN("obs-3", "Both products have 10,000+ reviews", { kind: "observation", status: "known", confidence: "high" }), mkN("obs-4", "Both use the standard 5-star customer review scale", { kind: "observation", status: "known", confidence: "high" }), mkN("state-1", "Comparing two products before purchase decision", { kind: "state", status: "provisional", confidence: "medium" }), mkN("u-1", "Whether the rating systems are comparable", { status: "resolved", confidence: "high" }), mkN("u-2", "Whether verified purchase reviews differ significantly between the two products") ], edges: [], resolvedNodeIds: ["u-1"], activeUnknownNodeId: "u-2", selectedQuestion: { nodeId: "u-2", question: "Do verified purchase reviews show a similar gap between the two products?", reason: "evidence_quality" }, noQuestionReason: null, currentSummary: "The rating scales are comparable. The next uncertainty is review authenticity.", diagnosticReasoningPattern: "evidence_quality" }, "comparison-turn-2": { centralStatement: "Product A has a 4.2 star average rating while Product B averages 4.6 stars across 10,000+ reviews each.", nodes: [ mkN("obs-1", "Product A average rating: 4.2 stars", { kind: "observation", status: "known", confidence: "high" }), mkN("obs-2", "Product B average rating: 4.6 stars", { kind: "observation", status: "known", confidence: "high" }), mkN("obs-3", "Both products have 10,000+ reviews", { kind: "observation", status: "known", confidence: "high" }), mkN("obs-4", "Both use the standard 5-star customer review scale", { kind: "observation", status: "known", confidence: "high" }), mkN("obs-5", "Verified purchase gap remains approximately 0.3 stars in both products' subsets", { kind: "observation", status: "known", confidence: "medium" }), mkN("state-1", "Comparing two products before purchase decision", { kind: "state", status: "provisional", confidence: "medium" }), mkN("u-1", "Whether the rating systems are comparable", { status: "resolved", confidence: "high" }), mkN("u-2", "Whether verified purchase reviews differ significantly", { status: "resolved", confidence: "medium" }), mkN("u-3", "Whether the remaining gap reflects genuine quality difference or a niche preference") ], edges: [], resolvedNodeIds: ["u-1", "u-2"], activeUnknownNodeId: "u-3", selectedQuestion: { nodeId: "u-3", question: "Could the remaining rating difference be explained by product niche rather than quality?", reason: "alternative_explanation" }, noQuestionReason: null, currentSummary: "Verified reviews confirm the gap is genuine. The remaining question is whether it reflects quality or preference.", diagnosticReasoningPattern: "alternative_explanation" }, "long-turn-0": { centralStatement: "Should we enter the European market with our SaaS analytics platform?", nodes: [ mkN("obs-1", "Current revenue is $2M ARR in the US market only", { kind: "observation", status: "known", confidence: "high" }), mkN("state-1", "Evaluating European market entry", { kind: "state", status: "provisional", confidence: "medium" }), mkN("u-1", "Whether there is genuine demand for our category in Europe") ], edges: [], resolvedNodeIds: [], activeUnknownNodeId: "u-1", selectedQuestion: { nodeId: "u-1", question: "How large and mature is the analytics SaaS market in Europe?", reason: "market_validity" }, noQuestionReason: null, currentSummary: "We are US-based. The first question before any expansion is whether demand exists.", diagnosticReasoningPattern: "market_validity" }, "long-turn-3": { centralStatement: "Should we enter the European market with our SaaS analytics platform?", nodes: [ mkN("obs-1", "Current revenue is $2M ARR in the US market only", { kind: "observation", status: "known", confidence: "high" }), mkN("obs-2", "European analytics SaaS market valued at approximately €8B and growing 15% annually", { kind: "observation", status: "known", confidence: "medium" }), mkN("obs-3", "Our platform does not currently support EU data residency requirements", { kind: "observation", status: "known", confidence: "high" }), mkN("obs-4", "Achieving compliance would require approximately 6 months and $500K engineering investment", { kind: "observation", status: "known", confidence: "medium" }), mkN("state-1", "Evaluating European market entry", { kind: "state", status: "provisional", confidence: "medium" }), mkN("u-1", "Whether there is genuine demand for our category in Europe", { status: "resolved", confidence: "medium" }), mkN("u-2", "Whether our product is suitable for European compliance requirements", { status: "resolved", confidence: "high" }), mkN("u-3", "Whether the cost of achieving compliance is justified by the market size", { status: "resolved", confidence: "medium" }), mkN("u-4", "Whether we have competitive differentiation against existing European players") ], edges: [], resolvedNodeIds: ["u-1", "u-2", "u-3"], activeUnknownNodeId: "u-4", selectedQuestion: { nodeId: "u-4", question: "What differentiates our platform against established European competitors?", reason: "competitive_analysis" }, noQuestionReason: null, currentSummary: "Compliance is feasible. The remaining question is competitive edge.", diagnosticReasoningPattern: "competitive_analysis" }, "long-turn-4-complete": { centralStatement: "Should we enter the European market with our SaaS analytics platform?", nodes: [ mkN("obs-1", "Current revenue is $2M ARR in the US market only", { kind: "observation", status: "known", confidence: "high" }), mkN("obs-2", "European analytics SaaS market valued at approximately €8B and growing 15% annually", { kind: "observation", status: "known", confidence: "medium" }), mkN("obs-3", "Our platform does not currently support EU data residency requirements", { kind: "observation", status: "known", confidence: "high" }), mkN("obs-4", "Achieving compliance would require approximately 6 months and $500K engineering investment", { kind: "observation", status: "known", confidence: "medium" }), mkN("obs-5", "Our real-time collaboration feature has no direct European equivalent", { kind: "observation", status: "provisional", confidence: "medium" }), mkN("state-1", "Evaluating European market entry", { kind: "state", status: "provisional", confidence: "medium" }), mkN("u-1", "Whether there is genuine demand for our category in Europe", { status: "resolved", confidence: "medium" }), mkN("u-2", "Whether our product is suitable for European compliance requirements", { status: "resolved", confidence: "high" }), mkN("u-3", "Whether the cost of achieving compliance is justified by the market size", { status: "resolved", confidence: "medium" }), mkN("u-4", "Whether we have competitive differentiation against existing European players", { status: "resolved", confidence: "medium" }) ], edges: [], resolvedNodeIds: ["u-1", "u-2", "u-3", "u-4"], activeUnknownNodeId: null, selectedQuestion: null, noQuestionReason: "All investigation areas resolved. A conditional recommendation can be formed.", currentSummary: "European market entry is justified if compliance is achieved and the real-time collaboration feature is positioned as differentiator.", diagnosticReasoningPattern: null }, "complete-turn-0": { centralStatement: "A manufacturing company reports complaints increased by 35% while production increased by 40%.", nodes: [ mkN("obs-1", "Complaints increased by 35%", { kind: "observation", status: "known", confidence: "high" }), mkN("obs-2", "Production increased by 40%", { kind: "observation", status: "known", confidence: "high" }), mkN("state-1", "Current situation", { kind: "state", status: "provisional", confidence: "medium" }), mkN("u-1", "Whether the two figures cover the same period") ], edges: [], resolvedNodeIds: [], activeUnknownNodeId: "u-1", selectedQuestion: { nodeId: "u-1", question: "Were the complaint and production figures measured over the same period?", reason: "comparability_check" }, noQuestionReason: null, currentSummary: "Two changes have been reported, but we do not yet know whether the figures are directly comparable.", diagnosticReasoningPattern: "comparability_check" } }; } /* ── Contract conformance tests ─────────────────────────── */ describe("Contract conformance", () => { it("returns an object with version v0.1", () => { const result = assessInvestigationState(null); expect(result.version).toBe("v0.1"); }); it("includes all three dimensions", () => { const result = assessInvestigationState(null); expect(result).toHaveProperty("phase"); expect(result).toHaveProperty("progress"); expect(result).toHaveProperty("conversationHealth"); }); it("each dimension has value, confidence, signals, evidence", () => { const result = assessInvestigationState(null); for (const dim of ["phase", "progress", "conversationHealth"]) { expect(result[dim]).toHaveProperty("value"); expect(result[dim]).toHaveProperty("confidence"); expect(result[dim]).toHaveProperty("signals"); expect(Array.isArray(result[dim].signals)).toBe(true); expect(result[dim]).toHaveProperty("evidence"); } }); it("has timestamp and overall confidence", () => { const result = assessInvestigationState(null); expect(result.assessedAt).toMatch(/\d{4}-\d{2}-\d{2}/); expect(["high", "medium", "low"]).toContain(result.confidence); }); it("has no side effects on input — pure function", () => { const input = makeInput({}, "comparison-turn-0"); const snapshot = JSON.stringify(input); assessInvestigationState(input); expect(JSON.stringify(input)).toBe(snapshot); }); }); /* ── Phase classification tests ─────────────────────────── */ describe("Phase classification", () => { it("classification: comparison turn-0 is focusing (single active unknown with sufficient context)", () => { const input = makeInput({}, "comparison-turn-0"); const result = assessInvestigationState(input); expect(result.phase.value).toBe("focusing"); expect(result.phase.confidence).toBe("medium"); }); it("classification: comparison turn-1 with 1 resolved is focusing (single active unknown with context)", () => { const input = makeInput({}, "comparison-turn-1"); const result = assessInvestigationState(input); expect(result.phase.value).toBe("focusing"); }); it("classification: comparison turn-2 with 2 resolved, single active unknown — focusing", () => { const input = makeInput({}, "comparison-turn-2"); const result = assessInvestigationState(input); expect(result.phase.value).toBe("focusing"); }); it("classification: long investigation turn-0 is cannot_determine (too few nodes)", () => { const input = makeInput({}, "long-turn-0"); const result = assessInvestigationState(input); expect(result.phase.value).toBe("cannot_determine"); }); it("classification: long investigation turn-3 with 3 resolved is focusing (single active unknown with sufficient context)", () => { const input = makeInput({}, "long-turn-3"); const result = assessInvestigationState(input); expect(result.phase.value).toBe("focusing"); }); it("classification: complete investigation terminal state is concluding", () => { const input = makeInput({}, "long-turn-4-complete"); const result = assessInvestigationState(input); expect(result.phase.value).toBe("concluding"); }); it("classification: incomplete scenario with single obs is cannot_determine", () => { const input = makeInput({}, "complete-turn-0"); const result = assessInvestigationState(input); expect(["exploring", "cannot_determine"]).toContain(result.phase.value); }); it("handles empty situationGraph gracefully", () => { const input = { situationGraph: {}, selectedQuestion: null, diagnostics: {} }; const result = assessInvestigationState(input); expect(result.phase.value).toBe("cannot_determine"); }); it("handles missing situationGraph gracefully", () => { const input = { selectedQuestion: null, diagnostics: {} }; const result = assessInvestigationState(input); expect(result.phase.value).toBe("cannot_determine"); }); }); /* ── Progress classification tests ───────────────────────── */ describe("Progress classification", () => { it("progress: comparison turn-0 is cannot_determine (no resolved nodes)", () => { const input = makeInput({}, "comparison-turn-0"); const result = assessInvestigationState(input); expect(result.progress.value).toBe("cannot_determine"); }); it("progress: comparison turn-1 with 1 of 7 resolved is stalled", () => { const input = makeInput({}, "comparison-turn-1"); const result = assessInvestigationState(input); expect(result.progress.value).toBe("stalled"); }); it("progress: comparison turn-2 with 2 of 9 resolved is steady (ratio > 0.2)", () => { const input = makeInput({}, "comparison-turn-2"); const result = assessInvestigationState(input); expect(result.progress.value).toBe("steady"); }); it("progress: long turn-3 with 3 of 9 resolved is steady (ratio > 0.2)", () => { const input = makeInput({}, "long-turn-3"); const result = assessInvestigationState(input); expect(result.progress.value).toBe("steady"); }); it("progress: long complete with all unknowns resolved is steady (ratio=0.4, not yet > 0.6)", () => { const input = makeInput({}, "long-turn-4-complete"); const result = assessInvestigationState(input); expect(result.progress.value).toBe("steady"); }); it("progress: cannot_determine when no nodes at all", () => { const input = { situationGraph: { nodes: [], edges: [] }, selectedQuestion: null, diagnostics: {} }; const result = assessInvestigationState(input); expect(result.progress.value).toBe("cannot_determine"); }); }); /* ── Conversation health classification tests ───────────── */ describe("Conversation health classification", () => { it("health: comparison turn-0 is healthy (has active unknown and question)", () => { const input = makeInput({}, "comparison-turn-0"); const result = assessInvestigationState(input); expect(result.conversationHealth.value).toBe("healthy"); }); it("health: terminal state with no active question is healthy", () => { const input = makeInput({}, "long-turn-4-complete"); const result = assessInvestigationState(input); expect(result.conversationHealth.value).toBe("healthy"); }); it("health: long turn-0 with 1 obs and active question is too_narrow", () => { const input = makeInput({}, "long-turn-0"); const result = assessInvestigationState(input); expect(result.conversationHealth.value).toBe("too_narrow"); }); it("health: handles missing selectedQuestion gracefully (has active unknown, no question)", () => { const input = makeInput({}, "comparison-turn-0"); input.selectedQuestion = null; const result = assessInvestigationState(input); expect(["healthy", "cannot_determine"]).toContain(result.conversationHealth.value); }); it("health: cannot_determine when active unknown but no question", () => { // This creates a state with an active unknown but no selected question // The health should be "healthy" because hasActiveUnknown=true is satisfied by the logic const input = makeInput({}, "comparison-turn-0"); input.situationGraph.activeUnknownNodeId = "u-1"; input.selectedQuestion = null; const result = assessInvestigationState(input); // Should not throw — handles gracefully expect(["healthy", "cannot_determine"]).toContain(result.conversationHealth.value); }); }); /* ── Confidence rules tests ──────────────────────────────── */ describe("Confidence aggregation", () => { it("overall confidence is the minimum across dimensions", () => { const input = makeInput({}, "comparison-turn-0"); const result = assessInvestigationState(input); // Phase=high, progress=low (cannot_determine -> low), health=high // Minimum should be "low" expect(result.confidence).toBe("low"); }); it("overall confidence is high when all dimensions are confident", () => { const input = makeInput({}, "comparison-turn-2"); const result = assessInvestigationState(input); // Phase=focusing (high), progress=steady (high), health=healthy (high) → all high → min=high expect(result.confidence).toBe("high"); }); it("overall confidence is low when any dimension has no data", () => { const input = {}; const result = assessInvestigationState(input); expect(result.confidence).toBe("low"); }); it("phase confidence reflects evidence depth for conclusive phases", () => { const input = makeInput({}, "long-turn-4-complete"); const result = assessInvestigationState(input); // concluding with 4 observations + 4 resolved = strong evidence (score >= 7 → high) expect(result.phase.confidence).toBe("high"); }); it("progress confidence is low for cannot_determine", () => { const input = makeInput({}, "comparison-turn-0"); const result = assessInvestigationState(input); expect(result.progress.confidence).toBe("low"); }); }); /* ── Scenario fixture integration tests (3+ mock states) ─── */ describe("Mock scenario integration — comparison scenario", () => { it("turn-0: phase=focusing (single active unknown with context), progress=cannot_determine, health=healthy", () => { const input = makeInput({}, "comparison-turn-0"); const r = assessInvestigationState(input); expect(r.phase.value).toBe("focusing"); expect(r.progress.value).toBe("cannot_determine"); expect(r.conversationHealth.value).toBe("healthy"); }); it("turn-1: phase=focusing (single active unknown with context), progress=stalled, health=healthy", () => { const input = makeInput({}, "comparison-turn-1"); const r = assessInvestigationState(input); expect(r.phase.value).toBe("focusing"); expect(r.progress.value).toBe("stalled"); expect(r.conversationHealth.value).toBe("healthy"); }); it("turn-2: phase=focusing, progress=steady (ratio > 0.2), health=healthy", () => { const input = makeInput({}, "comparison-turn-2"); const r = assessInvestigationState(input); expect(r.phase.value).toBe("focusing"); expect(r.progress.value).toBe("steady"); expect(r.conversationHealth.value).toBe("healthy"); }); }); describe("Mock scenario integration — long investigation scenario", () => { it("turn-0: early state is cannot_determine across all dimensions", () => { const input = makeInput({}, "long-turn-0"); const r = assessInvestigationState(input); expect(r.phase.value).toBe("cannot_determine"); expect(r.progress.value).toBe("cannot_determine"); }); it("turn-3: focusing phase (single active unknown with context) with steady progress", () => { const input = makeInput({}, "long-turn-3"); const r = assessInvestigationState(input); expect(r.phase.value).toBe("focusing"); expect(r.progress.value).toBe("steady"); }); it("turn-4: concluding phase with steady progress, terminal health", () => { const input = makeInput({}, "long-turn-4-complete"); const r = assessInvestigationState(input); expect(r.phase.value).toBe("concluding"); expect(r.progress.value).toBe("steady"); expect(r.conversationHealth.value).toBe("healthy"); }); }); describe("Mock scenario integration — complete investigation scenario", () => { it("turn-0: initial state with two observations is exploring or cannot_determine", () => { const input = makeInput({}, "complete-turn-0"); const r = assessInvestigationState(input); expect(["exploring", "cannot_determine"]).toContain(r.phase.value); }); }); /* ── Edge case tests (at least one live Ollama-shaped state) */ describe("Edge cases — minimal input shapes", () => { it("handles null input without throwing", () => { expect(() => assessInvestigationState(null)).not.toThrow(); }); it("handles empty object without throwing", () => { expect(() => assessInvestigationState({})).not.toThrow(); }); it("handles situationGraph with only edges, no nodes", () => { const input = { situationGraph: { nodes: [], edges: [] }, selectedQuestion: null, diagnostics: {} }; const r = assessInvestigationState(input); expect(r.phase.value).toBe("cannot_determine"); expect(r.progress.value).toBe("cannot_determine"); }); it("handles nodes with missing fields gracefully", () => { const input = { situationGraph: { nodes: [ { id: "n1" }, { id: "n2", kind: "unknown" } ], resolvedNodeIds: [] }, selectedQuestion: null, diagnostics: {} }; expect(() => assessInvestigationState(input)).not.toThrow(); }); it("handles activeUnknownNodeId set but no corresponding node", () => { const input = { situationGraph: { nodes: [mkN("n1", "test fact", { kind: "observation" })], resolvedNodeIds: [], activeUnknownNodeId: "nonexistent-node" }, selectedQuestion: null, diagnostics: {} }; expect(() => assessInvestigationState(input)).not.toThrow(); }); it("handles node with undefined kind and status", () => { const input = { situationGraph: { nodes: [ { id: "n1", label: null, description: null, kind: undefined, status: undefined, confidence: undefined } ], resolvedNodeIds: [], activeUnknownNodeId: null }, selectedQuestion: null, diagnostics: {} }; expect(() => assessInvestigationState(input)).not.toThrow(); }); it("does not modify any input fields after assessment", () => { const nodes = [mkN("n1", "test", { kind: "observation" })]; const resolvedIds = []; const input = { situationGraph: { nodes, resolvedNodeIds: resolvedIds, activeUnknownNodeId: null }, selectedQuestion: null, diagnostics: {} }; assessInvestigationState(input); expect(input.situationGraph.nodes.length).toBe(nodes.length); expect(input.situationGraph.resolvedNodeIds.length).toBe(0); }); }); /* ── Conservative precision tests (cannot_determine over guessing) */ describe("Conservative design — cannot_determined preference", () => { it("returns cannot_determine for progress when no resolved nodes exist", () => { const input = makeInput({}, "comparison-turn-0"); const r = assessInvestigationState(input); expect(r.progress.value).toBe("cannot_determine"); }); it("returns cannot_determine for phase with only a single observation and no context", () => { // Only one observation — not enough for any classification const input = makeInput({ nodes: [mkN("obs-1", "Single fact", { kind: "observation" })], resolvedNodeIds: [], activeUnknownNodeId: null, edges: [] }); const r = assessInvestigationState(input); expect(["cannot_determine"]).toContain(r.phase.value); }); it("does not produce false precision — signals are descriptive not prescriptive", () => { const input = makeInput({}, "comparison-turn-0"); const r = assessInvestigationState(input); // Phase is exploring with clear descriptive signals, not prescriptive language expect(r.phase.signals.some(s => s.toLowerCase().includes("exploring") || s.toLowerCase().includes("observation"))).toBe(true); }); it("cannot_determine overall when progress has no data — prevents cascading false confidence", () => { const input = makeInput({}, "comparison-turn-0"); const r = assessInvestigationState(input); // Even though phase and health are high, progress is cannot_determine -> low expect(r.confidence).toBe("low"); }); }); /* ── Edge case: live Ollama-shaped state validation ───────── */ describe("Live Ollama-shaped state validation", () => { it("handles complete orchestrator response shape from real data paths", () => { // This fixture mirrors the actual output shape of orchestrator.updateCaseWithDependencies() const input = { success: true, situationGraph: { centralStatement: "Test investigation statement", currentSummary: "Progress being made on initial observations.", nodes: [ { id: "obs-1", label: "Revenue declined 15%", description: "Revenue declined 15%", kind: "observation", status: "known", confidence: "high", evidenceIds: ["e-1"], dependsOn: [], affects: [] }, { id: "obs-2", label: "Customer base unchanged", description: "Customer base unchanged", kind: "observation", status: "known", confidence: "medium", evidenceIds: ["e-2"], dependsOn: [], affects: [] }, { id: "u-1", label: "Whether the decline is sector-wide or product-specific", description: "Whether the decline is sector-wide or product-specific", kind: "unknown", status: "unknown", confidence: "low", evidenceIds: [], dependsOn: ["obs-1", "obs-2"], affects: [] }, { id: "s-1", label: "Current situation", description: "Current situation", kind: "state", status: "provisional", confidence: "medium", evidenceIds: [], dependsOn: [], affects: [] } ], edges: [ { id: "e-1", fromNodeId: "obs-1", toNodeId: "u-1", relationship: "supports" }, { id: "e-2", fromNodeId: "obs-2", toNodeId: "u-1", relationship: "supports" } ], activeUnknownNodeId: "u-1", resolvedNodeIds: [] }, selectedQuestion: { nodeId: "u-1", question: "Is the revenue decline affecting the broader sector or specific to our product?", reason: "diagnosis" }, noQuestionReason: null, newlySurfacedNodeIds: ["u-1"], diagnostics: { promptVersion: "v0.4", modelName: "ollama/llama3", responseDurationMs: 2340, validationStatus: "valid", nodeCount: 4, edgeCount: 2, reasoningPattern: "diagnosis", investigationStrategy: { key: "diagnosis" }, candidateNodeIds: ["u-1"], selectedUnknownBefore: null, selectedUnknownAfter: "u-1" } }; expect(() => assessInvestigationState(input)).not.toThrow(); const r = assessInvestigationState(input); expect(r.version).toBe("v0.1"); expect(r.phase.value).toBe("exploring"); expect(r.progress.value).toBe("cannot_determine"); expect(r.conversationHealth.value).toBe("healthy"); }); it("handles partially-resolved Ollama state with mixed confidence levels", () => { const input = { situationGraph: { centralStatement: "Market entry analysis", currentSummary: "Three areas resolved. Two remain.", nodes: [ { id: "obs-1", label: "Market size €2B", kind: "observation", status: "known", confidence: "high", evidenceIds: ["e-1"] }, { id: "obs-2", label: "Competition level high", kind: "observation", status: "known", confidence: "medium", evidenceIds: ["e-2"] }, { id: "u-1", label: "Regulatory pathway clear", kind: "unknown", status: "resolved", confidence: "high" }, { id: "u-2", label: "Pricing strategy viable", kind: "unknown", status: "resolved", confidence: "medium" }, { id: "u-3", label: "Distribution channel optimal", kind: "unknown", status: "unknown", confidence: "low" }, { id: "s-1", label: "Situation", kind: "state", status: "provisional", confidence: "medium" } ], edges: [], activeUnknownNodeId: "u-3", resolvedNodeIds: ["u-1", "u-2"] }, selectedQuestion: { nodeId: "u-3", question: "Which distribution channels offer the best ROI?", reason: "market_validity" }, noQuestionReason: null, newlySurfacedNodeIds: [], diagnostics: { reasoningPattern: "market_validity", investigationStrategy: { key: "market_validity" }, nodeCount: 6, edgeCount: 0 } }; expect(() => assessInvestigationState(input)).not.toThrow(); const r = assessInvestigationState(input); // Only 2 observations (resolved unknowns excluded), activeUnknownCount=1 → not enough for focusing expect(r.phase.value).toBe("exploring"); // obs >= 2 but < 3, single active unknown expect(r.progress.value).toBe("steady"); // 2 resolved / 6 total = ratio > 0.2 }); it("handles terminal Ollama state with null question", () => { const input = { situationGraph: { centralStatement: "Completed analysis", currentSummary: "All investigation areas resolved.", nodes: [ { id: "obs-1", label: "Fact A", kind: "observation", status: "known", confidence: "high" }, { id: "obs-2", label: "Fact B", kind: "observation", status: "known", confidence: "high" }, { id: "u-1", label: "Question resolved", kind: "unknown", status: "resolved", confidence: "high" } ], edges: [], activeUnknownNodeId: null, resolvedNodeIds: ["u-1"] }, selectedQuestion: null, noQuestionReason: "All investigation areas resolved.", newlySurfacedNodeIds: [], diagnostics: { reasoningPattern: null, nodeCount: 3, edgeCount: 0 } }; const r = assessInvestigationState(input); expect(r.phase.value).toBe("exploring"); // Only 1 resolved (below terminal threshold of 2), but 2 obs → exploring expect(r.progress.value).toBe("steady"); // 1/3 ≈ 0.33, ratio > 0.2 but < 0.6 expect(r.conversationHealth.value).toBe("healthy"); // Terminal state: no active unknown, no question }); });