import React from "react"; import { describe, expect, it, vi } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; import DiagnosticsView from "@/components/diagnostics-view.jsx"; import GraphUpdateView from "@/components/graph-update-view.jsx"; import SituationGraphView from "@/components/situation-graph-view.jsx"; import { ScenarioResultPanels, UpdateErrorPanel, submitAnswerForUpdateCase, submitScenarioForStartCase, } from "@/components/scenario-form.jsx"; function makeGraphResult(overrides = {}) { return { success: true, situationGraph: { centralStatement: "Complaints increased while production increased.", currentSummary: "Nodes: 2 observation, 1 unknown | Edges: 2 total | Unknowns: 1 unresolved", activeUnknownNodeId: "n-unknown", resolvedNodeIds: [], nodes: [ { id: "n-1", label: "Complaints up 35%", description: "Complaints increased by 35%", kind: "observation", status: "supported", confidence: "high", value: 35, unit: "%", }, { id: "n-2", label: "Production up 40%", description: "Production increased by 40%", kind: "observation", status: "supported", confidence: "high", value: 40, unit: "%", }, { id: "n-unknown", label: "Complaint rate denominator", description: "Need the denominator for complaint rate", kind: "unknown", status: "unknown", confidence: "medium", value: null, unit: null, }, ], edges: [ { id: "e1", fromNodeId: "n-1", toNodeId: "n-unknown" }, { id: "e2", fromNodeId: "n-2", toNodeId: "n-unknown" }, ], }, selectedQuestion: { question: "What denominator is being used for the complaint rate?", }, diagnostics: { modelName: "test", responseDurationMs: 1234, validationStatus: "valid", promptVersion: "test-prompt", nodeCount: 3, edgeCount: 2, graphReferenceValidation: { valid: true, errors: [] }, }, ...overrides, }; } function makeUpdateSuccess(overrides = {}) { return { success: true, stage: "update_applied", updatedSituationGraph: { centralStatement: "Complaints increased while production increased.", currentSummary: "Updated summary", activeUnknownNodeId: "n-child-1", resolvedNodeIds: ["n-unknown"], nodes: [ { id: "n-1", label: "Complaints up 35%", description: "Complaints increased by 35%", kind: "observation", status: "supported", confidence: "high", value: 35, unit: "%", }, { id: "n-conclusion", label: "Quality deterioration", description: "Quality deterioration conclusion", kind: "conclusion", status: "weakened", confidence: "medium", value: null, unit: null, }, { id: "n-unknown", label: "Complaint rate denominator", description: "Need the denominator for complaint rate", kind: "unknown", status: "resolved", confidence: "medium", value: "1.9 complaints per 100 units", unit: null, }, { id: "n-next-unknown", label: "Explanation for why revenue increased by 18%, but cash in the bank fell over the same period", description: "Need to understand what change or event could explain why these observations differ, because that is needed to investigate their relationship.", kind: "unknown", status: "unknown", confidence: "medium", value: null, unit: null, }, { id: "n-child-1", label: "Timing or measurement basis", description: "Need evidence about whether a timing or measurement-basis difference could explain revenue increased by 18%, but cash in the bank fell over the same period, because that would change how the observations should be interpreted.", kind: "unknown", status: "unknown", confidence: "medium", value: null, unit: null, parentId: "n-next-unknown", }, ], edges: [ { id: "e-rel-next", fromNodeId: "n-conclusion", toNodeId: "n-next-unknown", relationship: "depends_on", confidence: "medium", description: "This unresolved explanation arises from the now-assessed relationship between the observations.", }, { id: "e-child-next", fromNodeId: "n-child-1", toNodeId: "n-next-unknown", relationship: "depends_on", confidence: "medium", description: "This child unknown must be investigated before the broader parent explanation can be resolved.", }, ], }, proposal: { addedNodes: [ { id: "n-next-unknown", label: "Explanation for why revenue increased by 18%, but cash in the bank fell over the same period", description: "Need to understand what change or event could explain why these observations differ, because that is needed to investigate their relationship.", kind: "unknown", status: "unknown", confidence: "medium", value: null, unit: null, evidenceIds: [], dependsOn: ["n-conclusion"], affects: [], parentId: "n-conclusion", childIds: [], }, { id: "n-child-1", label: "Timing or measurement basis", description: "Need evidence about whether a timing or measurement-basis difference could explain revenue increased by 18%, but cash in the bank fell over the same period, because that would change how the observations should be interpreted.", kind: "unknown", status: "unknown", confidence: "medium", value: null, unit: null, evidenceIds: [], dependsOn: [], affects: [], parentId: "n-next-unknown", childIds: [], }, ], updatedNodes: [ { nodeId: "n-unknown", newStatus: "resolved", reason: "answered" }, ], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: ["n-unknown"], affectedNodeIds: ["n-conclusion"], selectedQuestion: { nodeId: "n-child-1", question: "What evidence would clarify timing or measurement basis?", reason: "Formulated from graph context using the evidence_gathering investigation strategy.", }, }, selectedQuestion: { nodeId: "n-child-1", question: "What evidence would clarify timing or measurement basis?", reason: "Formulated from graph context using the evidence_gathering investigation strategy.", }, affectedNodeIds: ["n-conclusion"], resolvedUnknownNodeIds: ["n-unknown"], previousActiveUnknownNodeId: "n-unknown", newActiveUnknownNodeId: "n-child-1", emergentReasoningNodeCreated: true, emergentReasoningNodeId: "n-next-unknown", emergentReasoningNodeReason: "Created a new unresolved reasoning unknown so the next justified question is backed by the graph.", atomicityAssessment: "composite", decompositionPerformed: true, childUnknownCount: 1, childNodeIds: ["n-child-1"], atomicityReason: "Decomposed a composite unknown into smaller broad candidate dimensions before asking the next question.", previousReasoningState: { comparabilityStatus: "uncertain", reasoningStages: [ { stage: "comparability", status: "uncertain", outcome: "Comparability between the observations is not yet established across period, scale, or measurement basis.", }, { stage: "relationship", status: "insufficient_information", outcome: "not assessed until comparability is established", }, ], }, reasoningState: { comparabilityStatus: "confirmed", relationshipStatus: "insufficient_information", reasoningStages: [ { stage: "comparability", status: "confirmed", outcome: "Comparability was confirmed by the user answer covering the same period and source basis.", }, { stage: "relationship", status: "insufficient_information", outcome: "There is not enough structure to classify the relationship safely.", }, ], }, changesApplied: { updatedNodeCount: 2, resolvedUnknownCount: 1, affectedNodeCount: 1, }, diagnostics: { responseDurationMs: 100 }, ...overrides, }; } describe("scenario-form UI helpers", () => { it("submits to /api/cases/start", async () => { const fetchImpl = vi.fn().mockResolvedValue({ ok: true }); await submitScenarioForStartCase(fetchImpl, "Scenario text"); expect(fetchImpl).toHaveBeenCalledWith( "/api/cases/start", expect.objectContaining({ method: "POST", headers: { "Content-Type": "application/json" }, }), ); }); it("empty answer is rejected without fetch", async () => { const fetchImpl = vi.fn(); const result = await submitAnswerForUpdateCase(fetchImpl, { situationGraph: { nodes: [] }, previousQuestion: "What changed?", answer: " ", }); expect(result.skipped).toBe(true); expect(fetchImpl).not.toHaveBeenCalled(); }); it("update request body contains graph, previousQuestion and answer", async () => { const fetchImpl = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ success: true }), }); const graph = { nodes: [{ id: "n1" }], edges: [] }; await submitAnswerForUpdateCase(fetchImpl, { situationGraph: graph, previousQuestion: "What changed?", answer: "The rate fell.", }); expect(fetchImpl).toHaveBeenCalledWith( "/api/cases/update", expect.objectContaining({ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ situationGraph: graph, previousQuestion: "What changed?", answer: "The rate fell.", }), }), ); }); }); describe("graph-backed UI rendering", () => { it("renders central statement from successful graph response", () => { const data = makeGraphResult(); const html = renderToStaticMarkup( , ); expect(html).toContain("Central statement"); expect(html).toContain("Complaints increased while production increased."); }); it("renders active unknown", () => { const data = makeGraphResult(); const html = renderToStaticMarkup( , ); expect(html).toContain("Active unknown"); expect(html).toContain("Complaint rate denominator"); }); it("renders selected question exactly once", () => { const data = makeGraphResult(); const html = renderToStaticMarkup( , ); expect( html.match(/What denominator is being used for the complaint rate\?/g) || [], ).toHaveLength(1); }); it("no answer form appears when selectedQuestion is null", () => { const html = renderToStaticMarkup( , ); expect(html).not.toContain("Update situation"); }); it("renders diagnostics", () => { const html = renderToStaticMarkup( , ); expect(html).toContain("Diagnostics"); expect(html).toContain("test"); expect(html).toContain("1234ms"); expect(html).toContain("Node count"); expect(html).toContain("Edge count"); expect(html).toContain("Graph references"); }); it("hides empty sections", () => { const base = makeGraphResult(); const result = makeGraphResult({ selectedQuestion: null, situationGraph: { ...base.situationGraph, activeUnknownNodeId: null, nodes: [base.situationGraph.nodes[0]], edges: [], }, }); const html = renderToStaticMarkup( , ); expect(html).not.toContain("Selected Question"); expect(html).not.toContain("Active unknown"); }); it("displays API error clearly", () => { const html = renderToStaticMarkup( , ); expect(html).toContain("Error: Invalid start-case request"); }); it("renders expandable raw graph JSON", () => { const data = makeGraphResult(); const html = renderToStaticMarkup( , ); expect(html).toContain("Raw graph JSON"); expect(html).toContain(""centralStatement""); }); it("resolved unknowns render", () => { const html = renderToStaticMarkup( , ); expect(html).toContain("Resolved unknowns"); expect(html).toContain("Complaint rate denominator"); }); it("newly surfaced unknowns render", () => { const html = renderToStaticMarkup( , ); expect(html).toContain("Newly surfaced unknowns"); expect(html).toContain( "Explanation for why revenue increased by 18%, but cash in the bank fell over the same period", ); }); it("affected nodes render", () => { const html = renderToStaticMarkup( , ); expect(html).toContain("Affected nodes"); expect(html).toContain("Quality deterioration"); }); it("renders validated next question when present", () => { const html = renderToStaticMarkup( , ); expect(html).toContain( "What evidence would clarify timing or measurement basis?", ); }); it("no fake next question appears when there is none", () => { const html = renderToStaticMarkup( , ); expect(html).toContain("No next question selected yet."); }); it("previous and new active unknowns render labels", () => { const html = renderToStaticMarkup( , ); expect(html).toContain("Previous active unknown"); expect(html).toContain("Complaint rate denominator"); expect(html).toContain("New active unknown"); expect(html).toContain( "Explanation for why revenue increased by 18%, but cash in the bank fell over the same period", ); }); it("successful update renders prior and new state together", () => { const html = renderToStaticMarkup( , ); expect(html).toContain("Previous active unknown"); expect(html).toContain("Resolved unknowns"); expect(html).toContain("Newly surfaced unknowns"); expect(html).toContain("New active unknown"); expect(html).toContain("Next question"); expect(html).toContain( "What evidence would clarify timing or measurement basis?", ); }); it("update view shows comparability progression without raw ids in the normal view", () => { const html = renderToStaticMarkup( , ); expect(html).toContain("Comparability:"); expect(html).toContain("uncertain → confirmed"); expect(html).toContain("Relationship status:"); expect(html).toContain("insufficient_information"); expect(html).toContain("Reasoning stages:"); expect(html).toContain("comparability: confirmed"); expect(html).toContain("relationship: insufficient_information"); expect(html).toContain( "What evidence would clarify timing or measurement basis?", ); expect(html).not.toContain("reasoning:comparability"); }); it("situation graph marks newly surfaced and active unknowns", () => { const html = renderToStaticMarkup( , ); expect(html).toContain("newly surfaced unknown"); expect(html).toContain("active unknown"); expect(html).toContain("resolved unknown"); }); it("disabled follow-up form is shown only as prototype limitation", () => { const html = renderToStaticMarkup( , ); expect(html).toContain( "What evidence would clarify timing or measurement basis?", ); }); it("raw ids remain only in collapsed proposal details", () => { const html = renderToStaticMarkup( , ); expect(html).toContain("Proposal details"); expect(html).toContain(""resolvedUnknownNodeIds""); }); it("update diagnostics render valid values", () => { const html = renderToStaticMarkup( , ); expect(html).toContain("v0.4"); expect(html).toContain("456ms"); expect(html).toContain("7"); expect(html).toContain("3"); expect(html).toContain("✅ valid"); }); it("structured update error renders", () => { const html = renderToStaticMarkup( , ); expect(html).toContain("Update error: Invalid graph update proposal"); expect(html).toContain("bad proposal"); }); it("failed update does not fabricate history", () => { const html = renderToStaticMarkup( <> , ); expect(html).toContain("Update error: Update case failed"); expect(html).toContain("nu_commercial_val"); expect(html).not.toContain("Previous active unknown"); expect(html).not.toContain("Resolved unknowns"); expect(html).not.toContain("Newly surfaced unknowns"); expect(html).not.toContain("New active unknown"); expect(html).not.toContain("Proposal details"); }); it("proposal details remain collapsible", () => { const html = renderToStaticMarkup( , ); expect(html).toContain("Proposal details"); }); });