/** * Dedicated UI contract tests for v0.60j — restartInvestigation caller behaviour. * * Deterministic via static file-level vi.mock — no localStorage, no provider imports, * no vi.resetModules, no module-cache manipulation, no timing sleeps. */ import { describe, expect, it, beforeEach, vi } from "vitest"; import React from "react"; import { render, screen, fireEvent, waitFor, within } from "@testing-library/react"; import "@testing-library/jest-dom"; // --------------------------------------------------------------------------- // Mock next/navigation — static file-level mock // --------------------------------------------------------------------------- let pushRef = { push: () => {} }; vi.mock("next/navigation", () => ({ useRouter: () => pushRef, })); // --------------------------------------------------------------------------- // Mock investigation-storage — application-facing contract only // No localStorage. No provider imports. // NOTE: All exports must be present because ScenarioForm imports multiple. // --------------------------------------------------------------------------- let mockList = []; let mockRestartTarget = null; let mockLoadResult = null; let mockClearTarget = null; let mockLastSavedSnapshot = null; vi.mock("@/lib/storage/investigation-storage", () => ({ listInvestigations: () => [...mockList], restartInvestigation: (id) => { mockRestartTarget = id; }, clearInvestigation: (id) => { mockClearTarget = id; }, loadInvestigation: (id) => mockLoadResult, saveInvestigation: (snapshot) => { mockLastSavedSnapshot = snapshot; }, })); function setMockSummaries(snapshots) { mockList = snapshots ?? []; } function setMockLoadResult(snap) { mockLoadResult = snap; } // --------------------------------------------------------------------------- // Test fixture data — plain InvestigationSummary arrays // --------------------------------------------------------------------------- function summaryA() { return { id: "inv-a", scenario: "Scenario A", updatedAt: "2026-09-03T10:00:00.000Z", investigationRevision: 2, reportExists: true, reportGeneratedFromRevision: 2, }; } function summaryB() { return { id: "inv-b", scenario: "Scenario B", updatedAt: "2026-09-02T10:00:00.000Z", investigationRevision: 3, reportExists: true, reportGeneratedFromRevision: 2, }; } // --------------------------------------------------------------------------- // A — Portfolio restart uses restartInvestigation (not clearInvestigation) // --------------------------------------------------------------------------- describe("v0.60j — Portfolio Restart calls restartInvestigation", () => { let Portfolio; beforeEach(async () => { pushRef.push = vi.fn(); mockRestartTarget = null; mockClearTarget = null; setMockSummaries([summaryA(), summaryB()]); const mod = await import("@/app/page.jsx"); Portfolio = mod.default; }); it("restart confirmation calls restartInvestigation with the correct id (inv-a)", async () => { render(React.createElement(Portfolio)); // Scope to the first card's Restart button (scenario A) const cards = document.querySelectorAll('[class*="border-teal-300"]'); expect(cards).toHaveLength(2); const firstCardRestartBtn = within(cards[0]).getByRole("button", { name: /Restart investigation/i }); fireEvent.click(firstCardRestartBtn); // Confirmation dialog appears — scoped to the aria-labelledby title for inv-a expect(await screen.findByRole("heading", { name: /Restart this investigation\?/i })).toBeInTheDocument(); // Click the confirm button INSIDE the dialog (scoped), not the card's restart buttons const dialog = document.querySelector('[role="dialog"]'); const confirmBtn = within(dialog).getByRole("button", { name: "Restart investigation" }); fireEvent.click(confirmBtn); await waitFor(() => expect(mockRestartTarget).toBe("inv-a")); }); it("restart does NOT call clearInvestigation", async () => { render(React.createElement(Portfolio)); const cards = document.querySelectorAll('[class*="border-teal-300"]'); const firstCardRestartBtn = within(cards[0]).getByRole("button", { name: /Restart investigation/i }); fireEvent.click(firstCardRestartBtn); await screen.findByRole("heading", { name: /Restart this investigation\?/i }); const dialog = document.querySelector('[role="dialog"]'); const confirmBtn = within(dialog).getByRole("button", { name: "Restart investigation" }); fireEvent.click(confirmBtn); await waitFor(() => expect(mockClearTarget).toBeNull()); }); it("restart of inv-b passes inv-b as id", async () => { render(React.createElement(Portfolio)); const cards = document.querySelectorAll('[class*="border-teal-300"]'); // Second card (scenario B) — index 1 const secondCardRestartBtn = within(cards[1]).getByRole("button", { name: /Restart investigation/i }); fireEvent.click(secondCardRestartBtn); await screen.findByRole("heading", { name: /Restart this investigation\?/i }); const dialog = document.querySelector('[role="dialog"]'); const confirmBtn = within(dialog).getByRole("button", { name: "Restart investigation" }); fireEvent.click(confirmBtn); await waitFor(() => expect(mockRestartTarget).toBe("inv-b")); }); }); // --------------------------------------------------------------------------- // B — ScenarioForm restart path calls restartInvestigation with id // --------------------------------------------------------------------------- describe("v0.60j — ScenarioForm restart calls restartInvestigation(investigationId)", () => { let ScenarioForm; beforeEach(async () => { pushRef.push = vi.fn(); mockRestartTarget = null; setMockLoadResult(null); // No persisted session on mount const mod = await import("@/components/scenario-form.jsx"); ScenarioForm = mod.default; }); it("ScenarioForm mounts without error under mocked storage", async () => { const testId = "inv-scenarioform-mock-test"; expect(() => { render(React.createElement(ScenarioForm, { investigationId: testId })); }).not.toThrow(); }); it("restartInvestigation is importable from storage layer", async () => { const m = await import("@/lib/storage/investigation-storage.js"); expect(typeof m.restartInvestigation).toBe("function"); }); // ── Directly observable: ContinueLaterBanner restart ────────────────────── it("ContinueLaterBanner Restart calls restartInvestigation(\"inv-restart-a\")", async () => { mockRestartTarget = null; setMockLoadResult({ id: "inv-restart-a", scenario: "Test ContinueLaterBanner restart scenario", situationGraph: { nodes: [], edges: [] }, // hasGraph → status promoted to "success" updatedAt: "2026-09-03T10:00:00.000Z", }); const mod = await import("@/components/scenario-form.jsx"); ScenarioForm = mod.default; render(React.createElement(ScenarioForm, { investigationId: "inv-restart-a" })); // ContinueLaterBanner renders when status === "success" && result?.updatedAt exists expect(await screen.findByText("Restart investigation")).toBeInTheDocument(); fireEvent.click(screen.getByText("Restart investigation")); await waitFor(() => expect(mockRestartTarget).toBe("inv-restart-a")); }); // ── Directly observable: "Start new investigation" reset control ─────────── it("'Start new investigation' button calls restartInvestigation(\"inv-restart-a\")", async () => { mockRestartTarget = null; setMockLoadResult({ id: "inv-restart-a", scenario: "Test StartNew reset scenario", situationGraph: { nodes: [], edges: [] }, // hasGraph → status promoted to "success" updatedAt: "2026-09-03T10:00:00.000Z", }); const mod = await import("@/components/scenario-form.jsx"); ScenarioForm = mod.default; render(React.createElement(ScenarioForm, { investigationId: "inv-restart-a" })); expect(await screen.findByText("Start new investigation")).toBeInTheDocument(); fireEvent.click(screen.getByText("Start new investigation")); await waitFor(() => expect(mockRestartTarget).toBe("inv-restart-a")); }); // ── Source-inspected-only: ReasoningWorkspace onRestart path ────────────── // PATH 1 NOT DIRECTLY OBSERVABLE IN BOUNDED APPARATUS // ProviderUnavailable / MalformedResponse / UnexpectedState cards require // error-state reasoning data — not fabricatable in this bounded apparatus. // // Source confirmation (components/scenario-form.jsx line ~949): // onRestart={() => { restartInvestigation(investigationId); ... }} // This passes investigationId as the first argument to restartInvestigation. });