feat(confidence-engine): separate investigation report routes

This commit is contained in:
2026-09-03 06:39:35 +01:00
parent 745026f0a0
commit 32e1b01767
8 changed files with 567 additions and 176 deletions
+212 -162
View File
@@ -1,187 +1,237 @@
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it } from "vitest";
import React from "react";
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
// jsdom does not implement scrollIntoView — mock it globally
Element.prototype.scrollIntoView = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: () => {} }),
}));
// ---------------------------------------------------------------------------
// Test target — bounded to ReasoningWorkspace milestone overview seam
// Route-level assertions for v0.55 architecture
// Portfolio = /index ; Investigation Report = dedicated page
// ---------------------------------------------------------------------------
function makeGraphResult(overrides = {}) {
return {
success: true,
situationGraph: {
centralStatement: "Complaints increased while production increased.",
currentSummary: "Nodes: 2 observation | Unknowns: 0 resolved",
activeUnknownNodeId: null,
resolvedNodeIds: ["n-unknown"],
nodes: [
{ id: "n-1", label: "Complaints up 35%", kind: "observation", status: "supported" },
{ id: "n-unknown", label: "Complaint rate denominator", kind: "unknown", status: "resolved" },
],
edges: [],
reconstruction: { plausibleInterpretations: [{ id: "i1", description: "Denominator was narrowed" }] },
function makeSnapshot(overrides = {}) {
const situationGraph = {
evidence: [
{ id: "e1", claim: "Complaints rose.", type: "finding" },
{ id: "e2", claim: "Production increased.", type: "finding" },
],
reconstruction: {
plausibleInterpretations: ["The denominator may have been narrowed."],
},
};
return {
scenario: "Complaints increased while production increased.",
situationGraph,
selectedQuestion: null,
updatedSituationGraph: null,
newlySurfacedNodeIds: [],
diagnostics: { modelName: "test", validationStatus: "valid" },
summary: "Production quality declined.",
updatedAt: new Date().toISOString(),
schemaVersion: 1,
investigationReport: null,
findings: [],
...overrides,
};
}
let ReasoningWorkspace;
describe("Portfolio page (v0.55 route architecture)", () => {
let Portfolio;
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (k) => k }),
initPromise: Promise.resolve(),
}));
describe("investigation overview UI seam (v0.54b)", () => {
beforeAll(async () => {
const mod = await import("@/components/reasoning-workspace.jsx");
ReasoningWorkspace = mod.default;
const mod = await import("@/app/page.jsx");
Portfolio = mod.default;
});
function renderWorkspace(overrides = {}) {
const props = {
scenario: "Complaints increased while production increased.",
status: "success",
updateStatus: "idle",
cuSynthesisLoading: false,
currentUnderstanding: "Production quality declined.",
result: makeGraphResult(),
answer: "",
setAnswer: vi.fn(),
onAnswerSubmit: vi.fn(),
lastSubmittedAnswer: "",
onRestart: vi.fn(),
focusedContributions: [],
onFocusedContribution: vi.fn(),
findings: [],
onUpdateFindingDisposition: vi.fn(),
onUpdateFindingProposition: vi.fn(),
onSummaryUpdate: vi.fn(),
onImmediateGraphChange: vi.fn(),
onSituationGraphChange: vi.fn(),
initialPostAnalyseStatus: "success",
overviewState: null,
setOverviewState: vi.fn(),
overviewLoading: false,
handleRequestOverview: vi.fn(),
...overrides,
};
return render(React.createElement(ReasoningWorkspace, props));
}
// 1. zero-Open-Questions milestone renders the review action
it("renders Review current understanding button when open unknowns are zero and clarified questions exist", async () => {
renderWorkspace();
await waitFor(() => {
const btn = screen.getByRole("button", { name: /Review current understanding/i });
expect(btn).toBeInTheDocument();
});
it("shows + Create new investigation when no investigation exists", async () => {
localStorage.clear();
render(React.createElement(Portfolio));
expect(await screen.findByText(/Create new investigation/i)).toBeInTheDocument();
expect(screen.queryByText(/Open investigation/i)).not.toBeInTheDocument();
});
// 7. no overview is requested before the user clicks
it("does not request overview before user clicks (handleRequestOverview not called)", async () => {
const mockHandleRequest = vi.fn();
renderWorkspace({ handleRequestOverview: mockHandleRequest });
expect(mockHandleRequest).not.toHaveBeenCalled();
it("shows Open investigation card when one persisted investigation exists", async () => {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(makeSnapshot()),
);
render(React.createElement(Portfolio));
expect(await screen.findByText(/Open investigation/i)).toBeInTheDocument();
localStorage.removeItem("confidence-engine-investigation");
});
// 2. clicking it calls the overview endpoint once
it("calls handleRequestOverview on button click", async () => {
const mockHandleRequest = vi.fn();
renderWorkspace({ handleRequestOverview: mockHandleRequest });
const btn = await screen.findByRole("button", { name: /Review current understanding/i });
fireEvent.click(btn);
expect(mockHandleRequest).toHaveBeenCalledTimes(1);
it("shows View report only when a persisted report exists", async () => {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(
makeSnapshot({
investigationReport: {
understanding: "We understand complaints rose.",
hasPlausibleInterpretations: false,
},
}),
),
);
render(React.createElement(Portfolio));
expect(await screen.findByText(/View report/i)).toBeInTheDocument();
localStorage.removeItem("confidence-engine-investigation");
});
// 3. loading state is visible
it("shows loading text while overviewLoading is true", async () => {
renderWorkspace({ overviewState: null, overviewLoading: true, handleRequestOverview: vi.fn() });
await waitFor(() => {
expect(screen.getByText(/Generating overview/i)).toBeInTheDocument();
});
});
// 4. returned understanding is rendered under the established-understanding section
it("renders overview understanding in What we understand section", async () => {
renderWorkspace({
overviewState: {
success: true,
understanding: "We understand that complaints rose alongside production increases.",
plausibleInterpretations: null,
},
});
await waitFor(() => {
const surface = screen.getByTestId("investigation-overview");
expect(surface).toBeInTheDocument();
});
const surface2 = document.querySelector('[data-testid="investigation-overview"]');
expect(surface2.textContent).toContain("What we understand");
expect(surface2.textContent).toContain("We understand that complaints rose alongside production increases.");
});
// 5. returned plausibleInterpretations is rendered separately and clearly qualified
it("renders plausible interpretations as a separate qualified section", async () => {
renderWorkspace({
overviewState: {
success: true,
understanding: "Production quality declined.",
plausibleInterpretations: "The denominator may have been narrowed, explaining the apparent complaint increase.",
},
});
const surface = screen.getByTestId("investigation-overview");
expect(surface).toBeInTheDocument();
expect(surface.textContent).toContain("What remains plausible");
expect(surface.textContent).toContain("The denominator may have been narrowed");
});
// 6. existing Current Understanding remains
it("existing Current Understanding text remains visible alongside overview", async () => {
renderWorkspace({
currentUnderstanding: "Production quality declined.",
overviewState: {
success: true,
understanding: "We understand that complaints rose alongside production increases.",
plausibleInterpretations: null,
},
});
const cuSurface = document.querySelector('[id="cu-scroll-target"]');
expect(cuSurface).toBeInTheDocument();
expect(cuSurface.textContent).toContain("Production quality declined.");
});
// 8. overview response does not mutate Current Understanding
it("does not call setCurrentUnderstanding or mutate existing CU", async () => {
const mockSetCU = vi.fn();
renderWorkspace({
currentUnderstanding: "Production quality declined.",
overviewState: {
success: true,
understanding: "Overview synthesis result.",
plausibleInterpretations: null,
},
setCurrentUnderstanding: mockSetCU,
});
expect(mockSetCU).not.toHaveBeenCalled();
});
// 9. clarified questions remain visible
it("clarified questions remain rendered when overview is shown", async () => {
renderWorkspace({
overviewState: {
success: true,
understanding: "Overview.",
plausibleInterpretations: null,
},
});
expect(screen.getByText(/Questions we have clarified/i)).toBeInTheDocument();
it("does not show View report when no report persists", async () => {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(makeSnapshot()),
);
render(React.createElement(Portfolio));
expect(screen.queryByText(/View report/i)).not.toBeInTheDocument();
localStorage.removeItem("confidence-engine-investigation");
});
});
describe("Investigation Report page (v0.55 route architecture)", () => {
let ReportPage;
beforeAll(async () => {
const mod = await import("@/app/investigations/[id]/report/page.jsx");
ReportPage = mod.default;
});
it("renders persisted report with Situation and What we understand", async () => {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(
makeSnapshot({
investigationReport: {
understanding: "We understand complaints rose alongside production increases.",
hasPlausibleInterpretations: false,
},
}),
),
);
render(React.createElement(ReportPage));
expect(await screen.findByText(/Investigation Report/i)).toBeInTheDocument();
expect(await screen.findByText(/Situation/i)).toBeInTheDocument();
expect(screen.getByText(/What we understand/i)).toBeInTheDocument();
expect(screen.getByText("We understand complaints rose alongside production increases.")).toBeInTheDocument();
localStorage.removeItem("confidence-engine-investigation");
});
it("does not show pending placeholder during pre-hydration", async () => {
render(React.createElement(ReportPage));
// During pre-hydration, the page should render Investigation Report title
// but NOT the "Report generation pending" placeholder (which is for post-hydration no-report)
const pending = await screen.findByText(/Investigation Report/i);
expect(pending).toBeInTheDocument();
});
it("shows skeleton after hydration when genuinely no report exists", async () => {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(makeSnapshot()),
);
render(React.createElement(ReportPage));
expect(await screen.findByText(/Investigation Report/i)).toBeInTheDocument();
// After hydration, if no report, skeleton should appear (not during pre-hydration)
const pending = await screen.findByText(/Report generation pending/i);
expect(pending).toBeInTheDocument();
localStorage.removeItem("confidence-engine-investigation");
});
it("conditionally renders What remains plausible only when hasPlausibleInterpretations is true", async () => {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(
makeSnapshot({
investigationReport: {
understanding: "Some understanding.",
hasPlausibleInterpretations: false,
plausibleInterpretations: "",
},
}),
),
);
render(React.createElement(ReportPage));
expect(await screen.findByText(/Investigation Report/i)).toBeInTheDocument();
expect(screen.queryByText(/What remains plausible/i)).not.toBeInTheDocument();
localStorage.removeItem("confidence-engine-investigation");
});
it("renders What remains plausible when hasPlausibleInterpretations is true", async () => {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(
makeSnapshot({
investigationReport: {
understanding: "Some understanding.",
hasPlausibleInterpretations: true,
plausibleInterpretations: "The denominator may have been narrowed.",
},
}),
),
);
render(React.createElement(ReportPage));
expect(await screen.findByText(/What remains plausible/i)).toBeInTheDocument();
localStorage.removeItem("confidence-engine-investigation");
});
it("renders Back to investigation link", async () => {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(
makeSnapshot({
investigationReport: {
understanding: "Some understanding.",
hasPlausibleInterpretations: false,
},
}),
),
);
render(React.createElement(ReportPage));
expect(await screen.findByText(/Back to investigation/i)).toBeInTheDocument();
localStorage.removeItem("confidence-engine-investigation");
});
it("renders Back to portfolio link", async () => {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(
makeSnapshot({
investigationReport: {
understanding: "Some understanding.",
hasPlausibleInterpretations: false,
},
}),
),
);
render(React.createElement(ReportPage));
expect(await screen.findByText(/Back to portfolio/i)).toBeInTheDocument();
localStorage.removeItem("confidence-engine-investigation");
});
it("shows skeleton loading state when no persisted report exists", async () => {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(makeSnapshot()),
);
render(React.createElement(ReportPage));
expect(await screen.findByText(/Investigation Report/i)).toBeInTheDocument();
// Skeleton should not have actual understanding content
expect(screen.queryByText(/Report generation pending/i)).toBeInTheDocument();
localStorage.removeItem("confidence-engine-investigation");
});
});
describe("Investigation page (v0.55 route architecture)", () => {
let InvestigationPage;
beforeAll(async () => {
const mod = await import("@/app/investigations/[id]/page.jsx");
InvestigationPage = mod.default;
});
it("renders Back to portfolio link", async () => {
render(React.createElement(InvestigationPage));
expect(await screen.findByText(/Back to portfolio/i)).toBeInTheDocument();
});
});