428 lines
12 KiB
React
428 lines
12 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-next-unknown",
|
|
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,
|
|
},
|
|
],
|
|
edges: [],
|
|
},
|
|
proposal: {
|
|
addedNodes: [],
|
|
updatedNodes: [
|
|
{ nodeId: "n-unknown", newStatus: "resolved", reason: "answered" },
|
|
],
|
|
addedEdges: [],
|
|
removedEdgeIds: [],
|
|
resolvedUnknownNodeIds: ["n-unknown"],
|
|
affectedNodeIds: ["n-conclusion"],
|
|
},
|
|
affectedNodeIds: ["n-conclusion"],
|
|
resolvedUnknownNodeIds: ["n-unknown"],
|
|
previousActiveUnknownNodeId: "n-unknown",
|
|
newActiveUnknownNodeId: "n-next-unknown",
|
|
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(
|
|
<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 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("affected nodes render", () => {
|
|
const html = renderToStaticMarkup(
|
|
<GraphUpdateView
|
|
updateResult={{
|
|
...makeUpdateSuccess(),
|
|
previousSituationGraph: makeGraphResult().situationGraph,
|
|
}}
|
|
/>,
|
|
);
|
|
|
|
expect(html).toContain("Affected nodes");
|
|
expect(html).toContain("Quality deterioration");
|
|
});
|
|
|
|
it("no fake next question appears", () => {
|
|
const html = renderToStaticMarkup(
|
|
<GraphUpdateView
|
|
updateResult={{
|
|
...makeUpdateSuccess({ newActiveUnknownNodeId: 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("Unknown node (ID: n-next-unknown)");
|
|
});
|
|
|
|
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("proposal details remain collapsible", () => {
|
|
const html = renderToStaticMarkup(
|
|
<GraphUpdateView updateResult={makeUpdateSuccess()} />,
|
|
);
|
|
|
|
expect(html).toContain("Proposal details");
|
|
});
|
|
}); |