1556 lines
47 KiB
React
1556 lines
47 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 ReasoningWorkspace, {
|
|
useLoadingStatus,
|
|
INITIAL_MESSAGES,
|
|
UPDATE_MESSAGES,
|
|
LoadingOverlay,
|
|
} from "@/components/reasoning-workspace.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(""centralStatement"");
|
|
});
|
|
|
|
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(""resolvedUnknownNodeIds"");
|
|
});
|
|
|
|
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.");
|
|
});
|
|
});
|
|
|
|
// ── ReasoningWorkspace tests ────────────────────────────────
|
|
describe("ReasoningWorkspace UI", () => {
|
|
function makeWorkspaceResult(overrides = {}) {
|
|
return {
|
|
success: true,
|
|
situationGraph: makeGraphResult().situationGraph,
|
|
selectedQuestion: { question: "What denominator is being used for the complaint rate?" },
|
|
newlySurfacedNodeIds: [],
|
|
diagnostics: {
|
|
modelName: "test",
|
|
responseDurationMs: 1234,
|
|
validationStatus: "valid",
|
|
nodeCount: 3,
|
|
edgeCount: 2,
|
|
graphReferenceValidation: { valid: true, errors: [] },
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeWorkspaceProps(overrides = {}) {
|
|
return {
|
|
status: "success",
|
|
updateStatus: "idle",
|
|
result: makeWorkspaceResult(),
|
|
answer: "",
|
|
setAnswer: vi.fn(),
|
|
onAnswerSubmit: vi.fn(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
it("renders by default with a successful start result", () => {
|
|
const html = renderToStaticMarkup(<ReasoningWorkspace {...makeWorkspaceProps()} />);
|
|
|
|
expect(html).toContain("Your situation");
|
|
expect(html).toContain("Complaints increased while production increased.");
|
|
});
|
|
|
|
it("shows what we've established section", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
{...makeWorkspaceProps({
|
|
result: makeWorkspaceResult({
|
|
situationGraph: {
|
|
...makeWorkspaceResult().situationGraph,
|
|
currentSummary: "Nodes: 2 observation, 1 unknown",
|
|
},
|
|
}),
|
|
})}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("What we've established");
|
|
expect(html).toContain("Nodes: 2 observation, 1 unknown");
|
|
});
|
|
|
|
it("prominently displays the current investigation", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace {...makeWorkspaceProps()} />,
|
|
);
|
|
|
|
expect(html).toContain("Current investigation");
|
|
expect(html).toContain("What denominator is being used for the complaint rate?");
|
|
});
|
|
|
|
it("shows confidence-building progress text", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
{...makeWorkspaceProps({
|
|
result: makeWorkspaceResult({
|
|
situationGraph: {
|
|
...makeWorkspaceResult().situationGraph,
|
|
resolvedNodeIds: ["n-1"],
|
|
},
|
|
}),
|
|
})}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("We are still building confidence about your situation");
|
|
expect(html).not.toContain("3 remaining");
|
|
});
|
|
|
|
it("explains areas that still need investigation in plain language", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace {...makeWorkspaceProps()} />,
|
|
);
|
|
|
|
// With one remaining unknown, plural "areas" or singular "area" should not appear as a bare count
|
|
expect(html).not.toContain("unknown nodes");
|
|
expect(html).not.toContain("unresolved nodes");
|
|
});
|
|
|
|
it("shows active unknown in plain language within progress card", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace {...makeWorkspaceProps()} />,
|
|
);
|
|
|
|
expect(html).toContain("Current focus");
|
|
expect(html).toContain("Complaint rate denominator");
|
|
});
|
|
|
|
it("shows no active investigation fallback when none available", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
{...makeWorkspaceProps({
|
|
result: makeWorkspaceResult({
|
|
situationGraph: {
|
|
...makeWorkspaceResult().situationGraph,
|
|
activeUnknownNodeId: null,
|
|
},
|
|
}),
|
|
})}
|
|
/>,
|
|
);
|
|
|
|
expect(html).not.toContain("Current focus");
|
|
});
|
|
|
|
it("renders the answer form when a question is available", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace {...makeWorkspaceProps()} />,
|
|
);
|
|
|
|
expect(html).toContain("Your response");
|
|
expect(html).toContain("Update situation");
|
|
});
|
|
|
|
it("renders developer details section (collapsed)", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace {...makeWorkspaceProps()} />,
|
|
);
|
|
|
|
expect(html).toContain("Developer details");
|
|
});
|
|
|
|
it("preserves the full situation graph in developer details", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace {...makeWorkspaceProps()} />,
|
|
);
|
|
|
|
expect(html).toContain("Situation Graph");
|
|
expect(html).toContain("Central statement");
|
|
});
|
|
|
|
it("preserves diagnostics in developer details", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace {...makeWorkspaceProps()} />,
|
|
);
|
|
|
|
expect(html).toContain("Diagnostics");
|
|
expect(html).toContain("test");
|
|
expect(html).toContain("1234ms");
|
|
});
|
|
|
|
it("user-facing sections show labels, not raw node IDs", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace {...makeWorkspaceProps()} />,
|
|
);
|
|
|
|
// Labels and human-readable text should be present
|
|
expect(html).toContain("Your situation");
|
|
expect(html).toContain("What we've established");
|
|
expect(html).toContain("Current investigation");
|
|
expect(html).toContain("Complaint rate denominator");
|
|
|
|
// Raw node IDs only appear in developer details (collapsed), not in user-facing sections
|
|
expect(html).toContain("Developer details");
|
|
});
|
|
|
|
it("technical graph counts appear only inside Developer details", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace {...makeWorkspaceProps()} />,
|
|
);
|
|
|
|
// Technical data is available in collapsed developer details
|
|
expect(html).toContain("Developer details");
|
|
|
|
// "3 remaining" no longer appears in the main view
|
|
expect(html).not.toContain("remaining");
|
|
});
|
|
|
|
it("progress card explains what is being investigated", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace {...makeWorkspaceProps()} />,
|
|
);
|
|
|
|
expect(html).toContain("Current focus");
|
|
expect(html).not.toContain("Reasoning progress");
|
|
});
|
|
|
|
it("active unknown is shown in plain language, not raw IDs", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace {...makeWorkspaceProps()} />,
|
|
);
|
|
|
|
// Plain language label appears
|
|
expect(html).toContain("Complaint rate denominator");
|
|
|
|
// Raw node ID does not appear outside developer details section in user context
|
|
// Developer details remains collapsed by default
|
|
expect(html).toContain("Developer details");
|
|
});
|
|
|
|
it("progress card handles zero remaining gracefully", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
{...makeWorkspaceProps({
|
|
result: makeWorkspaceResult({
|
|
situationGraph: {
|
|
...makeWorkspaceResult().situationGraph,
|
|
resolvedNodeIds: ["n-unknown"],
|
|
},
|
|
}),
|
|
})}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("All areas under investigation are now complete");
|
|
});
|
|
|
|
it("loading card appears immediately with spinner and heading", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="loading"
|
|
updateStatus="idle"
|
|
result={null}
|
|
answer=""
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("Working through your situation");
|
|
expect(html).toContain("Reading your situation");
|
|
});
|
|
|
|
it("textarea and button are present when canAnswer allows", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace {...makeWorkspaceProps()} />,
|
|
);
|
|
|
|
expect(html).toContain("Your response");
|
|
expect(html).toContain("Update situation");
|
|
});
|
|
|
|
it("loading message changes with mocked timers", () => {
|
|
vi.useFakeTimers();
|
|
|
|
const initialProps = {
|
|
status: "loading",
|
|
updateStatus: "idle",
|
|
result: null,
|
|
answer: "",
|
|
setAnswer: vi.fn(),
|
|
onAnswerSubmit: vi.fn(),
|
|
};
|
|
|
|
// Initial render — elapsed is 0
|
|
let html = renderToStaticMarkup(<ReasoningWorkspace {...initialProps} />);
|
|
expect(html).toContain("Reading your situation");
|
|
|
|
// Advance time by 15 seconds
|
|
vi.advanceTimersByTime(15000);
|
|
|
|
// After 16s elapsed, the second message should be active
|
|
// (useEffect fires in real React; here we verify via hook export)
|
|
expect(INITIAL_MESSAGES[1].min).toBe(10);
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("error state remains visible", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="error"
|
|
updateStatus="idle"
|
|
result={{ error: "Invalid start-case request" }}
|
|
answer=""
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("Error: Invalid start-case request");
|
|
});
|
|
|
|
it("no-question state is handled clearly", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="success"
|
|
updateStatus="idle"
|
|
result={makeWorkspaceResult({
|
|
selectedQuestion: null,
|
|
diagnostics: { ...makeWorkspaceResult().diagnostics, noQuestionReason: "All unknowns resolved" },
|
|
})}
|
|
answer=""
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("fully investigated");
|
|
});
|
|
|
|
it("update loading state appears with spinner heading", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="success"
|
|
updateStatus="loading"
|
|
result={makeWorkspaceResult()}
|
|
answer=""
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("Working through your situation");
|
|
});
|
|
|
|
it("elapsed time text appears during loading", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="loading"
|
|
updateStatus="idle"
|
|
result={null}
|
|
answer=""
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("This has been running for");
|
|
});
|
|
|
|
it("empty state shows guidance when idle", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="idle"
|
|
updateStatus="idle"
|
|
result={null}
|
|
answer=""
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
// Should not show any result panels or workspace
|
|
expect(html).not.toContain("Your situation");
|
|
expect(html).not.toContain("Current investigation");
|
|
});
|
|
|
|
it("update error is visible", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="success"
|
|
updateStatus="error"
|
|
result={{
|
|
situationGraph: makeWorkspaceResult().situationGraph,
|
|
selectedQuestion: null,
|
|
updateError: { error: "Invalid graph update proposal" },
|
|
}}
|
|
answer=""
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("Update error: Invalid graph update proposal");
|
|
});
|
|
|
|
it("message pools are exported for external use", () => {
|
|
expect(INITIAL_MESSAGES.length).toBeGreaterThan(0);
|
|
expect(UPDATE_MESSAGES.length).toBeGreaterThan(0);
|
|
expect(INITIAL_MESSAGES[0].min).toBe(0);
|
|
expect(UPDATE_MESSAGES[0].min).toBe(0);
|
|
});
|
|
|
|
it("no answer form shown when there is no question", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="success"
|
|
updateStatus="idle"
|
|
result={makeWorkspaceResult({ selectedQuestion: null })}
|
|
answer=""
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(html).not.toContain("Your response");
|
|
expect(html).not.toContain("Update situation");
|
|
});
|
|
|
|
it("update result merges into workspace correctly", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="success"
|
|
updateStatus="success"
|
|
result={makeWorkspaceResult({
|
|
situationGraph: makeUpdateSuccess().updatedSituationGraph,
|
|
selectedQuestion: makeUpdateSuccess().selectedQuestion,
|
|
})}
|
|
answer=""
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("Updated summary");
|
|
expect(html).toContain(
|
|
"What evidence would clarify how the two observations were measured?",
|
|
);
|
|
});
|
|
|
|
// ── Loading card UI tests ───────────────────────────────
|
|
|
|
it("LoadingOverlay shows spinner and heading when isLoading is true", () => {
|
|
const html = renderToStaticMarkup(
|
|
<LoadingOverlay
|
|
isLoading={true}
|
|
elapsed={0}
|
|
currentMessage="Reading your situation"
|
|
variant="initial"
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("Working through your situation");
|
|
expect(html).toContain("Reading your situation");
|
|
});
|
|
|
|
it("LoadingOverlay is hidden when isLoading is false", () => {
|
|
const html = renderToStaticMarkup(
|
|
<LoadingOverlay
|
|
isLoading={false}
|
|
elapsed={0}
|
|
currentMessage="Reading your situation"
|
|
variant="initial"
|
|
/>,
|
|
);
|
|
|
|
expect(html).not.toContain("Working through your situation");
|
|
});
|
|
|
|
it("LoadingOverlay shows correct initial status at elapsed 0", () => {
|
|
const html = renderToStaticMarkup(
|
|
<LoadingOverlay
|
|
isLoading={true}
|
|
elapsed={0}
|
|
currentMessage="Reading your situation"
|
|
variant="initial"
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("Reading your situation");
|
|
});
|
|
|
|
it("LoadingOverlay shows correct status at elapsed >= 10", () => {
|
|
const html = renderToStaticMarkup(
|
|
<LoadingOverlay
|
|
isLoading={true}
|
|
elapsed={12}
|
|
currentMessage="Building a structured understanding"
|
|
variant="initial"
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("Building a structured understanding");
|
|
});
|
|
|
|
it("LoadingOverlay shows correct status at elapsed >= 25", () => {
|
|
const html = renderToStaticMarkup(
|
|
<LoadingOverlay
|
|
isLoading={true}
|
|
elapsed={30}
|
|
currentMessage="Identifying what is known and still unclear"
|
|
variant="initial"
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("Identifying what is known and still unclear");
|
|
});
|
|
|
|
it("LoadingOverlay shows correct status at elapsed >= 45", () => {
|
|
const html = renderToStaticMarkup(
|
|
<LoadingOverlay
|
|
isLoading={true}
|
|
elapsed={50}
|
|
currentMessage="Selecting the next useful question"
|
|
variant="initial"
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("Selecting the next useful question");
|
|
});
|
|
|
|
it("LoadingOverlay shows reassurance text after 45s for initial variant", () => {
|
|
const html = renderToStaticMarkup(
|
|
<LoadingOverlay
|
|
isLoading={true}
|
|
elapsed={60}
|
|
currentMessage="Selecting the next useful question"
|
|
variant="initial"
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("This can take around a minute");
|
|
});
|
|
|
|
it("LoadingOverlay hides reassurance text for update variant regardless of elapsed", () => {
|
|
const html = renderToStaticMarkup(
|
|
<LoadingOverlay
|
|
isLoading={true}
|
|
elapsed={90}
|
|
currentMessage="Choosing the next question"
|
|
variant="update"
|
|
/>,
|
|
);
|
|
|
|
expect(html).not.toContain("This can take around a minute");
|
|
});
|
|
|
|
it("LoadingOverlay displays elapsed time", () => {
|
|
const html = renderToStaticMarkup(
|
|
<LoadingOverlay
|
|
isLoading={true}
|
|
elapsed={42}
|
|
currentMessage="Building a structured understanding"
|
|
variant="initial"
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("This has been running for 42s.");
|
|
});
|
|
|
|
it("LoadingOverlay has aria-busy and role for accessibility", () => {
|
|
const html = renderToStaticMarkup(
|
|
<LoadingOverlay
|
|
isLoading={true}
|
|
elapsed={0}
|
|
currentMessage="Reading your situation"
|
|
variant="initial"
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("aria-busy");
|
|
expect(html).toContain("role=\"status\"");
|
|
});
|
|
|
|
it("LoadingOverlay uses aria-live on elapsed time element", () => {
|
|
const html = renderToStaticMarkup(
|
|
<LoadingOverlay
|
|
isLoading={true}
|
|
elapsed={5}
|
|
currentMessage="Reading your situation"
|
|
variant="initial"
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("aria-live");
|
|
});
|
|
|
|
it("scenario-form hides Analyse button and shows loading card during initial analysis", () => {
|
|
// We verify ReasoningWorkspace renders LoadingOverlay for initial status=loading
|
|
// which is the direct proxy for the scenario form's loading behavior
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="loading"
|
|
updateStatus="idle"
|
|
result={null}
|
|
answer=""
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
// Loading overlay is present
|
|
expect(html).toContain("Working through your situation");
|
|
expect(html).toContain("Reading your situation");
|
|
|
|
// No result content appears during loading
|
|
expect(html).not.toContain("Your situation");
|
|
expect(html).not.toContain("Next question");
|
|
});
|
|
|
|
it("scenario-form shows update loading overlay when updateStatus is loading", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="success"
|
|
updateStatus="loading"
|
|
result={makeWorkspaceResult()}
|
|
answer="some answer"
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("Working through your situation");
|
|
expect(html).toContain("Considering your answer");
|
|
});
|
|
|
|
it("success state restores normal controls — no loading overlay", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="success"
|
|
updateStatus="idle"
|
|
result={makeWorkspaceResult()}
|
|
answer=""
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(html).not.toContain("Working through your situation");
|
|
expect(html).toContain("Current investigation");
|
|
expect(html).toContain("Update situation");
|
|
});
|
|
|
|
it("error state remains visible and shows no loading overlay", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="error"
|
|
updateStatus="idle"
|
|
result={{ error: "Invalid start-case request" }}
|
|
answer=""
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(html).not.toContain("Working through your situation");
|
|
expect(html).toContain("Error: Invalid start-case request");
|
|
});
|
|
|
|
it("spinner renders without breaking layout when reduced-motion is active", () => {
|
|
const html = renderToStaticMarkup(
|
|
<LoadingOverlay
|
|
isLoading={true}
|
|
elapsed={3}
|
|
currentMessage="Reading your situation"
|
|
variant="initial"
|
|
/>,
|
|
);
|
|
|
|
// Spinner element present (CSS media query in globals.css handles reduced-motion)
|
|
expect(html).toContain("rounded-full");
|
|
// No JS animation dependency for rendering
|
|
expect(html).not.toContain("animation-delay");
|
|
});
|
|
|
|
it("loading card appears while Analyse button is hidden (duplicate prevention)", () => {
|
|
// During status=loading: LoadingOverlay is shown and Analyse button is not rendered
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="loading"
|
|
updateStatus="idle"
|
|
result={null}
|
|
answer=""
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
// Loading card visible (main signal)
|
|
expect(html).toContain("Working through your situation");
|
|
|
|
// No form controls remain during loading
|
|
expect(html).not.toContain("Update situation");
|
|
});
|
|
|
|
it("investigation complete message shows confident language when satisfied", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="success"
|
|
updateStatus="idle"
|
|
result={makeWorkspaceResult({
|
|
selectedQuestion: null,
|
|
diagnostics: { ...makeWorkspaceResult().diagnostics, noQuestionReason: "All unknowns satisfied" },
|
|
})}
|
|
answer=""
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("fully investigated");
|
|
expect(html).not.toContain("There is no next question at the moment");
|
|
});
|
|
|
|
it("investigation complete shows confidence-building fallback", () => {
|
|
const html = renderToStaticMarkup(
|
|
<ReasoningWorkspace
|
|
status="success"
|
|
updateStatus="idle"
|
|
result={makeWorkspaceResult({
|
|
selectedQuestion: null,
|
|
diagnostics: { ...makeWorkspaceResult().diagnostics, noQuestionReason: null },
|
|
})}
|
|
answer=""
|
|
setAnswer={vi.fn()}
|
|
onAnswerSubmit={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("We have established enough for now");
|
|
});
|
|
}); |