Files
confidence-engine/tests/ui/scenario-form.test.jsx
T

841 lines
27 KiB
React

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: "How the two observations were measured",
description:
"Need evidence about the measure used for each observation, because that could help explain revenue increased by 18%, but cash in the bank fell over the same period.",
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: "How the two observations were measured",
description:
"Need evidence about the measure used for each observation, because that could help explain revenue increased by 18%, but cash in the bank fell over the same period.",
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 how the two observations were measured?",
reason:
"Formulated from graph context using the evidence_gathering investigation strategy.",
},
},
selectedQuestion: {
nodeId: "n-child-1",
question:
"What evidence would clarify how the two observations were measured?",
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,
};
}
function makeCommercialUpdateSuccess(overrides = {}) {
return {
success: true,
stage: "update_applied",
updatedSituationGraph: {
centralStatement:
"I have developed a new reasoning method that aims to help people determine whether they have enough justified confidence to make a decision. I believe it could become a commercial product, but I do not yet know whether it solves a genuine problem, whether people would value it enough to pay for it, or whether it is fundamentally different from existing AI tools. Before investing significant time and money into building it further, I want to determine whether continuing development is commercially justified.",
currentSummary: "Commercial update summary",
activeUnknownNodeId: "n-other-people",
resolvedNodeIds: ["n-who"],
nodes: [
{
id: "n-commercial-parent",
label:
"Commercial justification for whether continuing development is commercially justified",
description:
"Need to know whether this solves a genuine problem, whether people would value it enough to pay for it, and whether it is commercially justified before continuing development.",
kind: "unknown",
status: "provisional",
confidence: "medium",
},
{
id: "n-who",
label: "Who experiences this problem",
description:
"Need to know who experiences this problem, because that must be clear before deciding whether it is commercially justified.",
kind: "unknown",
status: "resolved",
confidence: "medium",
value:
"I experience it myself when I am trying to decide whether a project, idea or investment is justified, but I do not yet know how common that problem is for other people.",
parentId: "n-commercial-parent",
},
{
id: "n-other-people",
label: "Whether other people experience this problem",
description:
"Need to know whether other people experience this problem, because that must be established before deciding whether the problem is broadly important.",
kind: "unknown",
status: "unknown",
confidence: "medium",
parentId: "n-commercial-parent",
},
],
edges: [
{
id: "e-other-parent",
fromNodeId: "n-other-people",
toNodeId: "n-commercial-parent",
relationship: "depends_on",
confidence: "medium",
description:
"This child unknown must be investigated before the broader parent explanation can be resolved.",
},
],
},
proposal: {
addedNodes: [],
updatedNodes: [{ nodeId: "n-who", newStatus: "resolved", reason: "answered" }],
addedEdges: [],
removedEdgeIds: [],
resolvedUnknownNodeIds: ["n-who"],
affectedNodeIds: ["n-commercial-parent"],
selectedQuestion: null,
},
selectedQuestion: {
nodeId: "n-other-people",
question: "What makes you think other people experience this problem too?",
reason:
"Formulated as a direct foundational question because this child unknown should be answered one step at a time.",
reasoningPattern: "decision",
questionFamily: "decision_foundation",
},
previousActiveUnknownNodeId: "n-who",
newActiveUnknownNodeId: "n-other-people",
affectedNodeIds: ["n-commercial-parent"],
resolvedUnknownNodeIds: ["n-who"],
diagnostics: {
unresolvedCandidateCount: 2,
eligibleCandidateCount: 1,
candidateNodeIds: ["n-other-people"],
resolvedCurrentTurnNodeIds: ["n-who"],
noQuestionReason: null,
},
...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(
<SituationGraphView
situationGraph={data.situationGraph}
selectedQuestion={data.selectedQuestion}
/>,
);
expect(html).toContain("Central statement");
expect(html).toContain("Complaints increased while production increased.");
});
it("renders active unknown", () => {
const data = makeGraphResult();
const html = renderToStaticMarkup(
<SituationGraphView
situationGraph={data.situationGraph}
selectedQuestion={data.selectedQuestion}
/>,
);
expect(html).toContain("Active unknown");
expect(html).toContain("Complaint rate denominator");
});
it("renders selected question exactly once", () => {
const data = makeGraphResult();
const html = renderToStaticMarkup(
<SituationGraphView
situationGraph={data.situationGraph}
selectedQuestion={data.selectedQuestion}
/>,
);
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(
<SituationGraphView
situationGraph={makeGraphResult().situationGraph}
selectedQuestion={null}
/>,
);
expect(html).not.toContain("Update situation");
});
it("renders the initial graph-backed commercial question when start-case reselection succeeds", () => {
const html = renderToStaticMarkup(
<ScenarioResultPanels
result={makeGraphResult({
situationGraph: {
...makeGraphResult().situationGraph,
activeUnknownNodeId: "n-who",
nodes: [
...makeGraphResult().situationGraph.nodes,
{
id: "n-who",
label: "Who experiences this problem",
description:
"Need to know who experiences this problem, because that must be clear before deciding whether it is commercially justified.",
kind: "unknown",
status: "unknown",
confidence: "medium",
parentId: "n-parent",
},
],
},
selectedQuestion: {
nodeId: "n-who",
question: "Who experiences this problem?",
reason: "Graph-backed commercial child selection.",
},
diagnostics: {
...makeGraphResult().diagnostics,
noQuestionReason: null,
},
})}
/>,
);
expect(html).toContain("Who experiences this problem?");
expect(html).toContain("Selected Question");
});
it("renders diagnostics", () => {
const html = renderToStaticMarkup(
<DiagnosticsView result={makeGraphResult()} />,
);
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(
<SituationGraphView
situationGraph={result.situationGraph}
selectedQuestion={result.selectedQuestion}
/>,
);
expect(html).not.toContain("Selected Question");
expect(html).not.toContain("Active unknown");
});
it("displays API error clearly", () => {
const html = renderToStaticMarkup(
<ScenarioResultPanels
status="error"
result={{ error: "Invalid start-case request" }}
/>,
);
expect(html).toContain("Error: Invalid start-case request");
});
it("renders expandable raw graph JSON", () => {
const data = makeGraphResult();
const html = renderToStaticMarkup(
<SituationGraphView
situationGraph={data.situationGraph}
selectedQuestion={data.selectedQuestion}
/>,
);
expect(html).toContain("Raw graph JSON");
expect(html).toContain("&quot;centralStatement&quot;");
});
it("resolved unknowns render", () => {
const html = renderToStaticMarkup(
<GraphUpdateView
updateResult={{
...makeUpdateSuccess(),
previousSituationGraph: makeGraphResult().situationGraph,
}}
/>,
);
expect(html).toContain("Resolved unknowns");
expect(html).toContain("Complaint rate denominator");
});
it("newly surfaced unknowns render", () => {
const html = renderToStaticMarkup(
<GraphUpdateView
updateResult={{
...makeUpdateSuccess(),
previousSituationGraph: makeGraphResult().situationGraph,
}}
/>,
);
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(
<GraphUpdateView
updateResult={{
...makeUpdateSuccess(),
previousSituationGraph: makeGraphResult().situationGraph,
}}
/>,
);
expect(html).toContain("Affected nodes");
expect(html).toContain("Quality deterioration");
});
it("renders validated next question when present", () => {
const html = renderToStaticMarkup(
<GraphUpdateView
updateResult={{
...makeUpdateSuccess(),
previousSituationGraph: makeGraphResult().situationGraph,
}}
/>,
);
expect(html).toContain(
"What evidence would clarify how the two observations were measured?",
);
});
it("no fake next question appears when there is none", () => {
const html = renderToStaticMarkup(
<GraphUpdateView
updateResult={{
...makeUpdateSuccess({
newActiveUnknownNodeId: null,
selectedQuestion: null,
proposal: {
...makeUpdateSuccess().proposal,
selectedQuestion: null,
},
}),
previousSituationGraph: makeGraphResult().situationGraph,
}}
/>,
);
expect(html).toContain("No next question selected yet.");
});
it("previous and new active unknowns render labels", () => {
const html = renderToStaticMarkup(
<GraphUpdateView
updateResult={{
...makeUpdateSuccess(),
previousSituationGraph: makeGraphResult().situationGraph,
}}
/>,
);
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(
<GraphUpdateView
updateResult={{
...makeUpdateSuccess(),
previousSituationGraph: makeGraphResult().situationGraph,
}}
/>,
);
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 how the two observations were measured?",
);
});
it("update view shows comparability progression without raw ids in the normal view", () => {
const html = renderToStaticMarkup(
<GraphUpdateView
updateResult={{
...makeUpdateSuccess({
selectedQuestion: {
nodeId: "n-next-unknown",
question:
"What evidence would clarify timing or measurement basis?",
reason: "A broad follow-up is now justified.",
},
}),
previousSituationGraph: makeGraphResult().situationGraph,
}}
/>,
);
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 how the two observations were measured?",
);
expect(html).not.toContain("reasoning:comparability");
});
it("situation graph marks newly surfaced and active unknowns", () => {
const html = renderToStaticMarkup(
<SituationGraphView
situationGraph={makeUpdateSuccess().updatedSituationGraph}
selectedQuestion={makeUpdateSuccess().selectedQuestion}
newlySurfacedNodeIds={["n-next-unknown"]}
/>,
);
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(
<GraphUpdateView
updateResult={makeUpdateSuccess()}
/>,
);
expect(html).toContain(
"What evidence would clarify how the two observations were measured?",
);
});
it("raw ids remain only in collapsed proposal details", () => {
const html = renderToStaticMarkup(
<GraphUpdateView
updateResult={{
...makeUpdateSuccess(),
previousSituationGraph: makeGraphResult().situationGraph,
}}
/>,
);
expect(html).toContain("Proposal details");
expect(html).toContain("&quot;resolvedUnknownNodeIds&quot;");
});
it("update diagnostics render valid values", () => {
const html = renderToStaticMarkup(
<DiagnosticsView
result={{
diagnostics: {
promptVersion: "v0.4",
modelName: "configured-model",
responseDurationMs: 456,
validationStatus: "valid",
nodeCount: 7,
edgeCount: 3,
graphReferenceValidation: { valid: true, errors: [] },
},
}}
/>,
);
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(
<UpdateErrorPanel
updateError={{
error: "Invalid graph update proposal",
proposalErrors: [{ message: "bad proposal" }],
}}
/>,
);
expect(html).toContain("Update error: Invalid graph update proposal");
expect(html).toContain("bad proposal");
});
it("failed update does not fabricate history", () => {
const html = renderToStaticMarkup(
<>
<UpdateErrorPanel
updateError={{
error: "Update case failed",
errors: [
'New unknown must be explicitly related to an answer-derived node: "nu_commercial_val"',
],
}}
/>
<GraphUpdateView updateResult={null} />
</>,
);
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(
<GraphUpdateView updateResult={makeUpdateSuccess()} />,
);
expect(html).toContain("Proposal details");
});
it("does not show the no-question fallback when a commercial follow-up sibling exists", () => {
const html = renderToStaticMarkup(
<GraphUpdateView updateResult={makeCommercialUpdateSuccess()} />,
);
expect(html).toContain(
"What makes you think other people experience this problem too?",
);
expect(html).toContain("New active unknown");
expect(html).not.toContain("No next question selected yet.");
});
});