feat: connect UI to situation graph start flow
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
const ValidationIndicator = ({ status }) => {
|
const ValidationIndicator = ({ status }) => {
|
||||||
const styles = {
|
const styles = {
|
||||||
valid: "text-green-600",
|
valid: "text-green-600",
|
||||||
@@ -27,23 +29,53 @@ const validationIcons = {
|
|||||||
export default function DiagnosticsView({ result }) {
|
export default function DiagnosticsView({ result }) {
|
||||||
if (!result) return null;
|
if (!result) return null;
|
||||||
|
|
||||||
|
const diagnostics = result.diagnostics || result;
|
||||||
|
|
||||||
const metrics = [
|
const metrics = [
|
||||||
{ label: "Model", value: result.modelName || "?" },
|
{ label: "Model", value: diagnostics.modelName || result.modelName || "?" },
|
||||||
{ label: "Provider", value: "Ollama" },
|
{ label: "Provider", value: "Ollama" },
|
||||||
{ label: "Prompt version", value: result.promptVersion || "?" },
|
{
|
||||||
|
label: "Prompt version",
|
||||||
|
value: diagnostics.promptVersion || result.promptVersion || "?",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Duration",
|
label: "Duration",
|
||||||
value:
|
value:
|
||||||
result.responseDurationMs != null
|
diagnostics.responseDurationMs != null
|
||||||
? `${result.responseDurationMs}ms`
|
? `${diagnostics.responseDurationMs}ms`
|
||||||
: "?",
|
: "?",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Validation",
|
label: "Validation",
|
||||||
value: (
|
value: (
|
||||||
<ValidationIndicator status={result.validationStatus || "invalid"} />
|
<ValidationIndicator
|
||||||
|
status={diagnostics.validationStatus || result.validationStatus || "invalid"}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Node count",
|
||||||
|
value: diagnostics.nodeCount != null ? diagnostics.nodeCount : "?",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Edge count",
|
||||||
|
value: diagnostics.edgeCount != null ? diagnostics.edgeCount : "?",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Graph references",
|
||||||
|
value:
|
||||||
|
diagnostics.graphReferenceValidation == null
|
||||||
|
? "?"
|
||||||
|
: diagnostics.graphReferenceValidation.valid
|
||||||
|
? `${validationIcons.valid} valid`
|
||||||
|
: `${validationIcons.invalid} invalid`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const errors = [
|
||||||
|
...(result.errors || []),
|
||||||
|
...(result.validationErrors || []),
|
||||||
|
...(result.analysisErrors || []),
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -72,14 +104,14 @@ export default function DiagnosticsView({ result }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Errors if present */}
|
{/* Errors if present */}
|
||||||
{result.errors && result.errors.length > 0 && (
|
{errors.length > 0 && (
|
||||||
<details className="mt-3">
|
<details className="mt-3">
|
||||||
<summary className="cursor-pointer text-xs text-red-500 underline hover:text-red-700">
|
<summary className="cursor-pointer text-xs text-red-500 underline hover:text-red-700">
|
||||||
Validation errors ({result.errors.length})
|
Validation errors ({errors.length})
|
||||||
</summary>
|
</summary>
|
||||||
<ul className="mt-1 space-y-0.5 text-xs text-red-600">
|
<ul className="mt-1 space-y-0.5 text-xs text-red-600">
|
||||||
{result.errors.map((err, i) => (
|
{errors.map((err, i) => (
|
||||||
<li key={i}>{err}</li>
|
<li key={i}>{typeof err === "string" ? err : err?.message || JSON.stringify(err)}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
@@ -1,14 +1,59 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import ReconstructionView from "@/components/reconstruction-view";
|
|
||||||
import DiagnosticsView from "@/components/diagnostics-view";
|
import DiagnosticsView from "@/components/diagnostics-view";
|
||||||
|
import SituationGraphView from "@/components/situation-graph-view";
|
||||||
|
|
||||||
const MAX_LENGTH = 10000;
|
const MAX_LENGTH = 10000;
|
||||||
|
|
||||||
|
export async function submitScenarioForStartCase(fetchImpl, scenario) {
|
||||||
|
return fetchImpl("/api/cases/start", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ scenario }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScenarioResultPanels({ status, result }) {
|
||||||
|
if (!result) return null;
|
||||||
|
|
||||||
|
const hasGraph = Boolean(result.situationGraph);
|
||||||
|
const hasQuestion = Boolean(result.selectedQuestion?.question);
|
||||||
|
const hasDiagnostics = Boolean(result.diagnostics);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{status === "error" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{result.error && (
|
||||||
|
<div className="rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700 whitespace-pre-wrap">
|
||||||
|
Error: {result.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!hasGraph && !hasQuestion && (
|
||||||
|
<div className="rounded-lg border border-yellow-300 bg-yellow-50 px-4 py-2 text-sm text-yellow-800">
|
||||||
|
Validation failed — no structured graph output was produced.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(status === "success" || hasGraph || hasQuestion) && (
|
||||||
|
<SituationGraphView
|
||||||
|
situationGraph={result.situationGraph}
|
||||||
|
selectedQuestion={result.selectedQuestion}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasDiagnostics && <DiagnosticsView result={result} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function ScenarioForm() {
|
export default function ScenarioForm() {
|
||||||
const [scenario, setScenario] = useState("");
|
const [scenario, setScenario] = useState("");
|
||||||
const [status, setStatus] = useState("idle"); // idle | loading | error | success | partial
|
const [status, setStatus] = useState("idle"); // idle | loading | error | success
|
||||||
const [result, setResult] = useState(null);
|
const [result, setResult] = useState(null);
|
||||||
const textareaRef = useRef(null);
|
const textareaRef = useRef(null);
|
||||||
|
|
||||||
@@ -18,19 +63,11 @@ export default function ScenarioForm() {
|
|||||||
setResult(null);
|
setResult(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/analyse", {
|
const res = await submitScenarioForStartCase(fetch, scenario);
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ scenario }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
if (res.ok && data.validationStatus === "valid") {
|
if (res.ok && data.success) {
|
||||||
setStatus("success");
|
|
||||||
setResult(data);
|
|
||||||
} else if (data.success) {
|
|
||||||
// Success in analysis but validation may be partial
|
|
||||||
setStatus("success");
|
setStatus("success");
|
||||||
setResult(data);
|
setResult(data);
|
||||||
} else {
|
} else {
|
||||||
@@ -43,14 +80,6 @@ export default function ScenarioForm() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine if we have meaningful content to display
|
|
||||||
const hasClassification = result?.inputClassification;
|
|
||||||
const hasReconstruction = result?.reconstruction;
|
|
||||||
const hasNextQuestion = result?.nextQuestion;
|
|
||||||
const hasEvidence = result?.evidence && result.evidence.length > 0;
|
|
||||||
const hasMeaningfulContent =
|
|
||||||
hasClassification || hasReconstruction || hasNextQuestion || hasEvidence;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
@@ -76,38 +105,7 @@ export default function ScenarioForm() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Error state */}
|
<ScenarioResultPanels status={status} result={result} />
|
||||||
{status === "error" && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{result?.error && (
|
|
||||||
<div className="rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700 whitespace-pre-wrap">
|
|
||||||
Error: {result.error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* Show partial content even on validation failure */}
|
|
||||||
{(hasClassification || hasReconstruction) && (
|
|
||||||
<div className="rounded-lg border border-yellow-300 bg-yellow-50 px-4 py-2 text-sm text-yellow-800">
|
|
||||||
⚠ Partial result — some fields failed validation. Showing what was
|
|
||||||
accepted.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{hasReconstruction && (
|
|
||||||
<ReconstructionView reconstruction={result} partial />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Success state */}
|
|
||||||
{status === "success" && hasMeaningfulContent && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<ReconstructionView reconstruction={result} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Always show diagnostics when we have any result */}
|
|
||||||
{(hasClassification || hasReconstruction || hasNextQuestion) && (
|
|
||||||
<DiagnosticsView result={result} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{status === "loading" && (
|
{status === "loading" && (
|
||||||
<div className="py-12 text-center text-sm text-gray-400">
|
<div className="py-12 text-center text-sm text-gray-400">
|
||||||
@@ -123,13 +121,6 @@ export default function ScenarioForm() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Invalid result with no partial data */}
|
|
||||||
{status === "error" && !result?.error && !hasMeaningfulContent && (
|
|
||||||
<div className="rounded-lg border border-yellow-300 bg-yellow-50 px-4 py-2 text-sm text-yellow-800">
|
|
||||||
Validation failed — no structured output was produced.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
function NodeBadge({ children, tone = "gray" }) {
|
||||||
|
const tones = {
|
||||||
|
gray: "border-gray-200 bg-gray-50 text-gray-700",
|
||||||
|
blue: "border-blue-200 bg-blue-50 text-blue-700",
|
||||||
|
green: "border-green-200 bg-green-50 text-green-700",
|
||||||
|
yellow: "border-yellow-200 bg-yellow-50 text-yellow-700",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={`rounded-full border px-2 py-0.5 text-xs ${tones[tone] || tones.gray}`}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NodeGroup({ title, nodes }) {
|
||||||
|
if (!nodes?.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="rounded-lg border border-gray-200 bg-white p-4">
|
||||||
|
<h3 className="mb-3 text-sm font-semibold text-gray-700">
|
||||||
|
{title} ({nodes.length})
|
||||||
|
</h3>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{nodes.map((node) => (
|
||||||
|
<li key={node.id} className="rounded border border-gray-100 bg-gray-50 p-3 text-sm">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<span className="font-medium text-gray-900">{node.label}</span>
|
||||||
|
<NodeBadge tone="blue">{node.status}</NodeBadge>
|
||||||
|
<NodeBadge tone="green">{node.confidence}</NodeBadge>
|
||||||
|
{node.value != null && (
|
||||||
|
<NodeBadge tone="yellow">
|
||||||
|
{node.value}
|
||||||
|
{node.unit ? ` ${node.unit}` : ""}
|
||||||
|
</NodeBadge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{node.description && node.description !== node.label && (
|
||||||
|
<p className="mt-1 text-gray-600">{node.description}</p>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SituationGraphView({
|
||||||
|
situationGraph,
|
||||||
|
selectedQuestion,
|
||||||
|
}) {
|
||||||
|
if (!situationGraph) return null;
|
||||||
|
|
||||||
|
const activeUnknown = situationGraph.activeUnknownNodeId
|
||||||
|
? situationGraph.nodes.find((node) => node.id === situationGraph.activeUnknownNodeId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const nodesByKind = situationGraph.nodes.reduce((acc, node) => {
|
||||||
|
if (!acc[node.kind]) acc[node.kind] = [];
|
||||||
|
acc[node.kind].push(node);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{selectedQuestion?.question && (
|
||||||
|
<section className="rounded-lg border-2 border-green-300 bg-green-50 p-5">
|
||||||
|
<h2 className="mb-2 text-base font-bold text-green-800">Selected Question</h2>
|
||||||
|
<p className="text-base font-medium text-gray-900">{selectedQuestion.question}</p>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<section className="rounded-lg border border-gray-200 bg-white p-4">
|
||||||
|
<h2 className="mb-2 text-base font-semibold text-gray-900">Situation Graph</h2>
|
||||||
|
<dl className="space-y-2 text-sm">
|
||||||
|
<div>
|
||||||
|
<dt className="text-gray-500">Central statement</dt>
|
||||||
|
<dd className="font-medium text-gray-900">{situationGraph.centralStatement}</dd>
|
||||||
|
</div>
|
||||||
|
{situationGraph.currentSummary && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-gray-500">Current summary</dt>
|
||||||
|
<dd className="text-gray-800">{situationGraph.currentSummary}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{activeUnknown && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-gray-500">Active unknown</dt>
|
||||||
|
<dd className="text-gray-900">{activeUnknown.label}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<dt className="text-gray-500">Edge count</dt>
|
||||||
|
<dd className="text-gray-900">{situationGraph.edges.length}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{Object.entries(nodesByKind).map(([kind, nodes]) => (
|
||||||
|
<NodeGroup
|
||||||
|
key={kind}
|
||||||
|
title={kind.replace(/_/g, " ")}
|
||||||
|
nodes={nodes}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<details className="rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||||
|
<summary className="cursor-pointer text-sm font-medium text-gray-700 underline">
|
||||||
|
Raw graph JSON
|
||||||
|
</summary>
|
||||||
|
<pre className="mt-3 overflow-auto rounded bg-gray-900 p-3 text-xs text-green-400">
|
||||||
|
{JSON.stringify(situationGraph, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,10 +5,9 @@
|
|||||||
- Covered by `tests/app/api/cases-start-route.test.js`.
|
- Covered by `tests/app/api/cases-start-route.test.js`.
|
||||||
|
|
||||||
- `app/api/start-case/route.js`
|
- `app/api/start-case/route.js`
|
||||||
- Untracked earlier experiment / duplicate start route.
|
- Earlier experiment / duplicate start route.
|
||||||
- Not referenced by the current UI.
|
- No repository UI/test references were found.
|
||||||
- Still mentioned in untracked handoff docs.
|
- Deleted from the working tree during UI connection cleanup.
|
||||||
- Leave untracked for now; recommend deletion once route migration is explicitly confirmed.
|
|
||||||
|
|
||||||
- `app/api/update-case/route.js`
|
- `app/api/update-case/route.js`
|
||||||
- Untracked future `updateCase` work.
|
- Untracked future `updateCase` work.
|
||||||
@@ -16,5 +15,6 @@
|
|||||||
- Leave untracked for the current milestone.
|
- Leave untracked for the current milestone.
|
||||||
|
|
||||||
- Current UI status
|
- Current UI status
|
||||||
- `components/scenario-form.jsx` still calls `/api/analyse`.
|
- `components/scenario-form.jsx` now calls `/api/cases/start` for the main experimental flow.
|
||||||
- No active UI path currently calls `/api/cases/start`, `/api/start-case`, or `/api/update-case`.
|
- `/api/analyse` remains available for compatibility.
|
||||||
|
- No active UI path currently calls `/api/update-case`.
|
||||||
|
|||||||
+42
-27
@@ -1,55 +1,70 @@
|
|||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
test("v0.3 UI smoke test with live model response", async ({ page }) => {
|
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || "http://localhost:3000";
|
||||||
await page.goto("http://localhost:3000");
|
|
||||||
|
test.setTimeout(300000);
|
||||||
|
|
||||||
|
test("graph-backed start flow smoke test", async ({ page }) => {
|
||||||
|
await page.goto(BASE_URL);
|
||||||
|
|
||||||
// Page should load without error
|
// Page should load without error
|
||||||
await expect(page.getByText(/Confidence Engine/i)).toBeVisible();
|
await expect(page.getByText(/Confidence Engine/i)).toBeVisible();
|
||||||
|
|
||||||
// Type the scenario
|
// Type the scenario
|
||||||
const textarea = page.locator("textarea[placeholder*='Describe']");
|
const textarea = page.locator("textarea[placeholder*='Describe']");
|
||||||
await textarea.fill("Complaints increased by 35% while production increased by 40%.");
|
await textarea.fill(
|
||||||
|
"Complaints increased by 35% while production increased by 40%.",
|
||||||
|
);
|
||||||
|
await expect(textarea).toHaveValue(
|
||||||
|
"Complaints increased by 35% while production increased by 40%.",
|
||||||
|
);
|
||||||
|
|
||||||
// Button should be enabled
|
// Button should be enabled
|
||||||
await expect(page.getByRole("button", { name: /Analyse/i })).toBeEnabled();
|
await expect(page.getByRole("button", { name: /Analyse/i })).toBeEnabled();
|
||||||
|
|
||||||
// Click Analyse and wait for diagnostics panel
|
// Click Analyse and wait for graph-backed result
|
||||||
await page.getByRole("button", { name: /Analyse/i }).click();
|
await page.getByRole("button", { name: /Analyse/i }).click();
|
||||||
|
|
||||||
// Wait for result section (ReconstructionView rendered)
|
|
||||||
await expect(page.getByRole("heading", { name: /Next Question/i })).toBeVisible({ timeout: 180000 });
|
|
||||||
|
|
||||||
// Take screenshot of result page
|
await expect(
|
||||||
await page.screenshot({ path: "tests-results/smoke-v0.3.png", fullPage: true });
|
page.getByRole("heading", { name: /Selected Question/i }),
|
||||||
|
).toBeVisible({ timeout: 180000 });
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: /Situation Graph/i }),
|
||||||
|
).toBeVisible({ timeout: 180000 });
|
||||||
|
await expect(page.getByText(/Central statement/i)).toBeVisible();
|
||||||
|
await expect(page.getByText(/Active unknown/i)).toBeVisible();
|
||||||
|
await expect(page.getByText(/Error:/i)).toHaveCount(0);
|
||||||
|
|
||||||
// Verify diagnostics panel exists and contains relevant info
|
const rawJsonToggle = page.getByText(/Raw graph JSON/i);
|
||||||
const diagPanel = page.locator('details summary').first();
|
await expect(rawJsonToggle).toBeVisible();
|
||||||
if (await diagPanel.isVisible()) {
|
await rawJsonToggle.click();
|
||||||
console.log("Raw response viewer:", await diagPanel.innerText().catch(() => "not visible"));
|
await expect(page.getByText(/centralStatement/i)).toBeVisible();
|
||||||
}
|
|
||||||
|
|
||||||
// Get full body text for verification
|
// Get full body text for verification
|
||||||
const bodyText = await page.locator("body").innerText();
|
const bodyText = await page.locator("body").innerText();
|
||||||
|
|
||||||
console.log("\n=== UI Smoke Test Results ===");
|
console.log("\n=== UI Smoke Test Results ===");
|
||||||
console.log("Page title:", await page.title());
|
console.log("Page title:", await page.title());
|
||||||
console.log("Body content length:", bodyText.length);
|
console.log("Body content length:", bodyText.length);
|
||||||
|
|
||||||
// Check key content indicators
|
// Check key content indicators
|
||||||
const hasNextQ = bodyText.includes("Next Question");
|
const hasSelectedQuestion = bodyText.includes("Selected Question");
|
||||||
const hasComplaints = bodyText.includes("Complaint") || bodyText.includes("complaint");
|
const hasComplaints =
|
||||||
const hasProduction = bodyText.includes("production") || bodyText.includes("Production");
|
bodyText.includes("Complaint") || bodyText.includes("complaint");
|
||||||
const hasRateContext = bodyText.toLowerCase().includes("rate") ||
|
const hasProduction =
|
||||||
bodyText.toLowerCase().includes("unit") ||
|
bodyText.includes("production") || bodyText.includes("Production");
|
||||||
bodyText.toLowerCase().includes("denominator") ||
|
const hasRateContext =
|
||||||
bodyText.toLowerCase().includes("per-unit");
|
bodyText.toLowerCase().includes("rate") ||
|
||||||
|
bodyText.toLowerCase().includes("unit") ||
|
||||||
console.log("Has Next Question heading:", hasNextQ);
|
bodyText.toLowerCase().includes("denominator") ||
|
||||||
|
bodyText.toLowerCase().includes("per-unit");
|
||||||
|
|
||||||
|
console.log("Has selected question heading:", hasSelectedQuestion);
|
||||||
console.log("Has complaints reference:", hasComplaints);
|
console.log("Has complaints reference:", hasComplaints);
|
||||||
console.log("Has production reference:", hasProduction);
|
console.log("Has production reference:", hasProduction);
|
||||||
console.log("Has rate context (rate/unit/denominator):", hasRateContext);
|
console.log("Has rate context (rate/unit/denominator):", hasRateContext);
|
||||||
|
|
||||||
// Basic structural checks
|
// Basic structural checks
|
||||||
expect(bodyText.length).toBeGreaterThan(200);
|
expect(bodyText.length).toBeGreaterThan(200);
|
||||||
expect(hasNextQ).toBe(true);
|
expect(hasSelectedQuestion).toBe(true);
|
||||||
}, { timeout: 300000 });
|
});
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
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 SituationGraphView from "@/components/situation-graph-view.jsx";
|
||||||
|
import {
|
||||||
|
ScenarioResultPanels,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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("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"");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user