Files
confidence-engine/tests/investigation-state-assessor.test.js
T
robbond 1273861f0c exp(18): implement investigation state assessment layer
Implement the three-dimensional assessment (phase, progress, conversation
health) that sits between narrative and behaviour selection.

Key changes:
- lib/assessment/investigation-state-assessor.js: assessor module with
  countObservations, assessPhase, assessProgress, assessConversationHealth,
  assessInvestigationState — deterministic classifiers using known rules
- tests/investigation-state-assessor.test.js: 51 tests covering phase
  classification (orienting→concluding), progress thresholds, health
  conditions, confidence aggregation, edge cases, and observation counting
- lib/graph/orchestrator.js: integration calls passing correctly-shaped input
  to assessInvestigationState() at three call sites (~552, ~904, ~1013)

Design decisions encoded in this iteration:
- countObservations counts nodes with known/resolved status + high-confidence
  non-unknown non-state nodes (not just explicit observation-kind nodes)
- Phase uses seven values including cannot_determine for insufficient data
- Progress uses resolution ratio thresholds: accelerating (>0.6), steady
  (0.2-0.6), stalled (<0.2 with ≥1 resolved)
- Overall confidence = minimum across all three dimensions (conservative)

Also adds investigation-state-assessment-contract.md and updates
design-evolution-log, investigation-state-assessment.md (status header),
and investigation-turn-cycle.md (implementation status table).
2026-08-05 17:52:18 +01:00

694 lines
33 KiB
JavaScript

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
});
});