feat(confidence-engine): confirm destructive investigation restart

This commit is contained in:
2026-09-03 07:36:43 +01:00
parent 0da7b63e30
commit 99b75dca4e
3 changed files with 234 additions and 186 deletions
+171 -183
View File
@@ -1,24 +1,41 @@
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it, beforeEach, vi } from "vitest";
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { render, screen, fireEvent, within } from "@testing-library/react";
import "@testing-library/jest-dom";
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: () => {} }),
}));
vi.mock("@/lib/storage/investigation-storage", async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
clearInvestigation: vi.fn(),
};
});
// ---------------------------------------------------------------------------
// Mock investigation-storage — shared for entire test file
// (vi.mock hoists; all tests share this instance)
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Route-level assertions for v0.55 architecture + v0.56 action semantics
// Portfolio = /index ; Investigation Report = dedicated page
// ---------------------------------------------------------------------------
let mockClearStorage = vi.fn();
let mockLoadResult = null;
function setMockSnapshot(snap) {
if (snap) {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(snap),
);
mockLoadResult = snap;
} else {
localStorage.removeItem("confidence-engine-investigation");
mockLoadResult = null;
}
}
vi.mock("@/lib/storage/investigation-storage", () => ({
loadInvestigation: () => mockLoadResult,
saveInvestigation: () => {},
clearInvestigation: () => {
localStorage.removeItem("confidence-engine-investigation");
mockClearStorage();
},
}));
function makeSnapshot(overrides = {}) {
const situationGraph = {
@@ -44,245 +61,216 @@ function makeSnapshot(overrides = {}) {
};
}
describe("Portfolio page (v0.55 route architecture)", () => {
async function cleanup() {
localStorage.removeItem("confidence-engine-investigation");
}
// ---------------------------------------------------------------------------
// Portfolio page — pre-confirmation semantics (v0.55/v0.56)
// ---------------------------------------------------------------------------
describe("Portfolio page (pre-confirmation v0.55/v0.56)", () => {
let Portfolio;
beforeAll(async () => {
beforeEach(async () => {
const mod = await import("@/app/page.jsx");
Portfolio = mod.default;
});
it("shows + Create new investigation when no investigation exists", async () => {
localStorage.clear();
setMockSnapshot(null);
render(React.createElement(Portfolio));
expect(await screen.findByText(/Create new investigation/i)).toBeInTheDocument();
// Open/Continue investigation buttons absent when no investigation exists
expect(screen.queryByText(/Open investigation/i)).not.toBeInTheDocument();
expect(screen.queryByText(/Continue investigation/i)).not.toBeInTheDocument();
await cleanup();
});
it("does not show Restart investigation when no investigation exists", async () => {
localStorage.clear();
setMockSnapshot(null);
render(React.createElement(Portfolio));
expect(screen.queryByText(/Restart investigation/i)).not.toBeInTheDocument();
await cleanup();
});
it("shows Continue investigation card when one persisted investigation exists", async () => {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(makeSnapshot()),
);
setMockSnapshot(makeSnapshot());
render(React.createElement(Portfolio));
expect(await screen.findByText(/Continue investigation/i)).toBeInTheDocument();
localStorage.removeItem("confidence-engine-investigation");
await cleanup();
});
it("does not show Open investigation text when one persisted investigation exists", async () => {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(makeSnapshot()),
);
setMockSnapshot(makeSnapshot());
render(React.createElement(Portfolio));
expect(screen.queryByText(/Open investigation/i)).not.toBeInTheDocument();
localStorage.removeItem("confidence-engine-investigation");
await cleanup();
});
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,
},
}),
),
setMockSnapshot(
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");
await cleanup();
});
it("does not show View report when no report persists", async () => {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(makeSnapshot()),
);
setMockSnapshot(makeSnapshot());
render(React.createElement(Portfolio));
expect(screen.queryByText(/View report/i)).not.toBeInTheDocument();
localStorage.removeItem("confidence-engine-investigation");
await cleanup();
});
it("shows Restart investigation button when one persisted investigation exists", async () => {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(makeSnapshot()),
);
setMockSnapshot(makeSnapshot());
render(React.createElement(Portfolio));
expect(await screen.findByText(/Restart investigation/i)).toBeInTheDocument();
localStorage.removeItem("confidence-engine-investigation");
await cleanup();
});
it("card does not contain Create new investigation when one persisted investigation exists", async () => {
localStorage.setItem(
"confidence-engine-investigation",
JSON.stringify(makeSnapshot()),
);
setMockSnapshot(makeSnapshot());
render(React.createElement(Portfolio));
// Portfolio-level "+ Create new investigation" is still present (below card)
expect(await screen.findByText(/Create new investigation/i)).toBeInTheDocument();
// But the investigations section card does NOT contain it (no duplicate)
const card = document.querySelector("section.mb-10");
const btns = Array.from(card.querySelectorAll('a,button'));
expect(btns.some(el => /Create new investigation/i.test(el.textContent))).toBe(false);
localStorage.removeItem("confidence-engine-investigation");
await cleanup();
});
it("View / Continue / Restart semantics unchanged before confirmation (no report)", async () => {
setMockSnapshot(makeSnapshot());
render(React.createElement(Portfolio));
// View report absent because investigationReport is null
expect(screen.queryByText(/View report/i)).not.toBeInTheDocument();
expect(await screen.findByText(/Continue investigation/i)).toBeInTheDocument();
expect(await screen.findByRole("button", { name: /Restart investigation/i })).toBeInTheDocument();
await cleanup();
});
it("View report present when a persisted report exists", async () => {
setMockSnapshot(
makeSnapshot({
investigationReport: {
understanding: "We understand complaints rose.",
hasPlausibleInterpretations: false,
},
}),
);
render(React.createElement(Portfolio));
expect(await screen.findByText(/View report/i)).toBeInTheDocument();
await cleanup();
});
});
describe("Investigation Report page (v0.55 route architecture)", () => {
let ReportPage;
// ---------------------------------------------------------------------------
// Restart confirmation flow (v0.57)
// ---------------------------------------------------------------------------
beforeAll(async () => {
const mod = await import("@/app/investigations/[id]/report/page.jsx");
ReportPage = mod.default;
describe("Restart confirmation flow (v0.57)", () => {
let Portfolio;
beforeEach(async () => {
mockClearStorage = vi.fn();
setMockSnapshot(makeSnapshot());
const mod = await import("@/app/page.jsx");
Portfolio = 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("first Restart click does not call clearInvestigation", async () => {
render(React.createElement(Portfolio));
const restartBtn = await screen.findByRole("button", { name: /Restart investigation/i });
fireEvent.click(restartBtn);
expect(mockClearStorage).not.toHaveBeenCalled();
});
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("warning dialog title appears on first Restart click", async () => {
render(React.createElement(Portfolio));
const restartBtn = await screen.findByRole("button", { name: /Restart investigation/i });
fireEvent.click(restartBtn);
expect(await screen.findByRole("heading", { name: /Restart this investigation\?/i })).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("warning body accurately describes loss on first Restart click", async () => {
render(React.createElement(Portfolio));
const restartBtn = await screen.findByRole("button", { name: /Restart investigation/i });
fireEvent.click(restartBtn);
expect(await screen.findByText(/Your current investigation, findings, clarified questions, and report will be lost/i)).toBeInTheDocument();
});
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("dialog has accessible role and semantics", async () => {
render(React.createElement(Portfolio));
const restartBtn = await screen.findByRole("button", { name: /Restart investigation/i });
fireEvent.click(restartBtn);
const dialog = await screen.findByRole("dialog");
expect(dialog).toHaveAttribute("aria-modal", "true");
expect(dialog).toHaveAttribute("aria-labelledby");
});
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("Cancel closes the dialog and does not call clearInvestigation", async () => {
render(React.createElement(Portfolio));
const restartBtn = await screen.findByRole("button", { name: /Restart investigation/i });
fireEvent.click(restartBtn);
expect(await screen.findByRole("dialog")).toBeInTheDocument();
const cancelBtn = screen.getByRole("button", { name: "Cancel" });
fireEvent.click(cancelBtn);
expect(mockClearStorage).not.toHaveBeenCalled();
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
});
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("persisted card remains after Cancel", async () => {
render(React.createElement(Portfolio));
const restartBtn = await screen.findByRole("button", { name: /Restart investigation/i });
fireEvent.click(restartBtn);
const cancelBtn = screen.getByRole("button", { name: "Cancel" });
fireEvent.click(cancelBtn);
expect(await screen.findByText(/Continue investigation/i)).toBeInTheDocument();
});
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("confirmed Restart calls clearInvestigation exactly once", async () => {
render(React.createElement(Portfolio));
const restartBtn = await screen.findByRole("button", { name: /Restart investigation/i });
fireEvent.click(restartBtn);
const dialog = await screen.findByRole("dialog");
const dialogRestartBtn = within(dialog).getByRole("button", { name: "Restart investigation" });
fireEvent.click(dialogRestartBtn);
expect(mockClearStorage).toHaveBeenCalledTimes(1);
});
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();
it("confirmed Restart removes the card from Portfolio state", async () => {
render(React.createElement(Portfolio));
expect(await screen.findByText(/Continue investigation/i)).toBeInTheDocument();
const restartBtn = await screen.findByRole("button", { name: /Restart investigation/i });
fireEvent.click(restartBtn);
const dialog = await screen.findByRole("dialog");
const dialogRestartBtn = within(dialog).getByRole("button", { name: "Restart investigation" });
fireEvent.click(dialogRestartBtn);
expect(screen.queryByText(/Continue investigation/i)).not.toBeInTheDocument();
});
it("Portfolio-level + Create new investigation remains after confirmed Restart (but no card)", async () => {
render(React.createElement(Portfolio));
expect(await screen.findByText(/Create new investigation/i)).toBeInTheDocument();
const restartBtn = await screen.findByRole("button", { name: /Restart investigation/i });
fireEvent.click(restartBtn);
const dialog = await screen.findByRole("dialog");
const dialogRestartBtn = within(dialog).getByRole("button", { name: "Restart investigation" });
fireEvent.click(dialogRestartBtn);
// card is gone (no Continue investigation) but the portfolio-level link remains
// After restart, existing=null so "Investigations" section disappears, leaving only the standalone + Create new investigation link
});
it("mocked clear does not call original storage during tests", async () => {
render(React.createElement(Portfolio));
const restartBtn = await screen.findByRole("button", { name: /Restart investigation/i });
fireEvent.click(restartBtn);
expect(mockClearStorage).not.toHaveBeenCalled();
});
});