Feature/product platform foundation v0.62 #1
+2
-2
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { listInvestigations, clearInvestigation } from "@/lib/storage/investigation-storage";
|
||||
import { listInvestigations, restartInvestigation } from "@/lib/storage/investigation-storage";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -98,7 +98,7 @@ function Portfolio() {
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowRestartConfirm(null);
|
||||
try { clearInvestigation(summary.id); } catch (_) { /* storage must not crash caller */ }
|
||||
try { restartInvestigation(summary.id); } catch (_) { /* storage must not crash caller */ }
|
||||
setSummaries(listInvestigations());
|
||||
}}
|
||||
className="rounded-lg border border-red-400 bg-white px-4 py-2 text-sm font-medium text-red-700 hover:bg-red-50 transition"
|
||||
|
||||
@@ -6,7 +6,7 @@ import DiagnosticsView from "@/components/diagnostics-view";
|
||||
import ReasoningWorkspace, { LoadingOverlay, ContinueLaterBanner } from "@/components/reasoning-workspace";
|
||||
import { mockFetch, AVAILABLE_SCENARIOS } from "@/lib/mocks/confidence-engine/mock-client";
|
||||
import { deriveFindingsFromContributions, normalizeFindings } from "@/lib/graph/finding-helpers";
|
||||
import { loadInvestigation, saveInvestigation, clearInvestigation } from "@/lib/storage/investigation-storage";
|
||||
import { loadInvestigation, saveInvestigation, restartInvestigation, clearInvestigation } from "@/lib/storage/investigation-storage";
|
||||
|
||||
/* Compile-time env resolution — NEXT_PUBLIC_ vars are injected by Next.js at build */
|
||||
const MOCK_ENABLED = process.env.NEXT_PUBLIC_CONFIDENCE_ENGINE_MOCKS === "true";
|
||||
@@ -947,7 +947,7 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) {
|
||||
setResult((prev) => ({ ...(prev ?? {}), situationGraph: nextGraph }));
|
||||
}}
|
||||
onRestart={() => {
|
||||
clearInvestigation();
|
||||
restartInvestigation(investigationId);
|
||||
setInvestigationRevision(0);
|
||||
setStatus("idle");
|
||||
setResult(null);
|
||||
@@ -968,7 +968,7 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) {
|
||||
|
||||
{/* ── Continue later banner when session was restored ── */}
|
||||
{status === "success" && result?.updatedAt && (
|
||||
<ContinueLaterBanner onRestart={() => { clearInvestigation(); setInvestigationRevision(0); setStatus("idle"); setResult(null); setAnswer(""); setUpdateStatus("idle"); setCurrentUnderstanding(null); setFocusedContributions([]); setFindings([]); }} />
|
||||
<ContinueLaterBanner onRestart={() => { restartInvestigation(investigationId); setInvestigationRevision(0); setStatus("idle"); setResult(null); setAnswer(""); setUpdateStatus("idle"); setCurrentUnderstanding(null); setFocusedContributions([]); setFindings([]); }} />
|
||||
)}
|
||||
|
||||
{/* Reset button after successful analysis */}
|
||||
@@ -976,7 +976,7 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) {
|
||||
<div className="text-center">
|
||||
<button
|
||||
onClick={() => {
|
||||
clearInvestigation();
|
||||
restartInvestigation(investigationId);
|
||||
setInvestigationRevision(0);
|
||||
setScenario("");
|
||||
setStatus("idle");
|
||||
|
||||
@@ -665,6 +665,56 @@ v0.60h: Migrate Report route to use route `[id]` for all identity operations (in
|
||||
|
||||
---
|
||||
|
||||
## v0.60j — Semantic Restart within Investigation Container
|
||||
|
||||
**Purpose:** Implement `restartInvestigation(id)` that preserves the durable Investigation container (id, scenario) while clearing all reasoning/report state. Migrate Portfolio and ScenarioForm Restart callers from `clearInvestigation()` to `restartInvestigation(investigationId)`.
|
||||
|
||||
### What was implemented
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `lib/storage/providers/local-storage.js` | Added `restartInvestigation(id)` — preserves id/scenario/schemaVersion; resets situationGraph→null, selectedQuestion→null, summary→null, focusedContributions→[], findings→[], investigationReport→null, investigationRevision→0, updatedAt→new ISO |
|
||||
| `lib/storage/investigation-storage.js` | Imported and re-exported `restartInvestigation`; added semantic contract doc (missing id → no-op, no singleton fallback) |
|
||||
| `app/page.jsx` | Portfolio card Restart: replaced `clearInvestigation(summary.id)` with `restartInvestigation(summary.id)` — preserves container, clears reasoning |
|
||||
| `components/scenario-form.jsx` | Three callers migrated: ReasoningWorkspace onRestart, ContinueLaterBanner onRestart, "Start new investigation" button — all call `restartInvestigation(investigationId)` instead of `clearInvestigation()` |
|
||||
|
||||
### Deterministic evidence
|
||||
|
||||
- **Storage tests:** 44/44 PASS (investigation-storage.test.js)
|
||||
- **UI contract tests:** 7/7 PASS (v060j-restart-contract.test.jsx)
|
||||
- **Exact command:** `npx vitest run tests/storage/investigation-storage.test.js tests/ui/v060j-restart-contract.test.jsx`
|
||||
- **First run result:** 51/51 PASS, no reruns
|
||||
|
||||
### Semantic contract of restartInvestigation(id)
|
||||
|
||||
**Preserved (container-level):** id, scenario, schemaVersion
|
||||
**Reset:** situationGraph→null, selectedQuestion→null, summary→null, focusedContributions→[], findings→[], investigationReport→null, investigationRevision→0, updatedAt→new timestamp
|
||||
|
||||
### Portfolio caller migrated
|
||||
|
||||
- Portfolio Restart confirmation dialog → `restartInvestigation(summary.id)` ✓
|
||||
- No longer calls `clearInvestigation()` ✓
|
||||
- Card id passed correctly (inv-a, inv-b verified by UI test)
|
||||
|
||||
### ScenarioForm three callers migrated
|
||||
|
||||
| Path | Location | Contract | Observability |
|
||||
|---|---|---|---|
|
||||
| 1 — ReasoningWorkspace onRestart | line ~949 | `restartInvestigation(investigationId)` | Source-inspected (PATH 1 NOT DIRECTLY OBSERVABLE IN BOUNDED APPARATUS) |
|
||||
| 2 — ContinueLaterBanner onRestart | line ~971 | `restartInvestigation(investigationId)` | **Directly exercised** by UI test |
|
||||
| 3 — "Start new investigation" button | line ~979 | `restartInvestigation(investigationId)` | **Directly exercised** by UI test |
|
||||
|
||||
### UI apparatus
|
||||
|
||||
- ScenarioForm direct caller assertions added: YES (2 directly observable via ContinueLaterBanner and StartNew buttons)
|
||||
- ReasoningWorkspace onRestart confirmed by bounded source inspection
|
||||
- No localStorage used, no provider imported, no module-cache manipulation
|
||||
- All mocks at application-facing boundary only
|
||||
|
||||
### Live verification pending
|
||||
|
||||
---
|
||||
|
||||
## Next restart point
|
||||
|
||||
Consult `docs/design-evolution/README.md` for progressive loading of product reasoning and provenance chronology; load the relevant chapter only when a specific historical question requires it.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Owns the canonical identity contract: snapshot.id is the sole save identity.
|
||||
// Delegates to the concrete localStorage provider internally.
|
||||
|
||||
import { loadInvestigation as _load, saveInvestigation as _save, clearInvestigation as _clear, listInvestigations as _list } from "./providers/local-storage.js";
|
||||
import { loadInvestigation as _load, saveInvestigation as _save, clearInvestigation as _clear, listInvestigations as _list, restartInvestigation as _restart } from "./providers/local-storage.js";
|
||||
|
||||
/**
|
||||
* Canonical save contract: snapshot.id is the sole identity authority.
|
||||
@@ -40,3 +40,15 @@ export function clearInvestigation(id) {
|
||||
export function listInvestigations() {
|
||||
return _list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic restart: reset reasoning/report state within the Investigation container.
|
||||
*
|
||||
* The Investigation is NOT deleted or replaced. Its durable `id` and `scenario` (container)
|
||||
* are preserved. All reasoning-state fields are cleared so a new clean pass can begin.
|
||||
*
|
||||
* Missing/invalid id → silently no-op (does NOT fall back to legacy singleton).
|
||||
*/
|
||||
export function restartInvestigation(id) {
|
||||
return _restart(id);
|
||||
}
|
||||
|
||||
@@ -175,6 +175,41 @@ export function listInvestigations() {
|
||||
return summaries;
|
||||
}
|
||||
|
||||
// ── restartInvestigation (semantic reset within container) ───────────
|
||||
|
||||
/**
|
||||
* Resets the Investigation to a clean state suitable for a new reasoning pass.
|
||||
*
|
||||
* Preserved: id, scenario, schemaVersion (container-level identity/framing).
|
||||
* Reset: situationGraph → null, selectedQuestion → null, summary → null,
|
||||
* focusedContributions → [], findings → [], investigationReport → null,
|
||||
* investigationRevision → 0, updatedAt → new ISO timestamp.
|
||||
*/
|
||||
export function restartInvestigation(id) {
|
||||
const storage = _getTargetStorage();
|
||||
if (!storage) return;
|
||||
|
||||
try {
|
||||
const key = id != null ? `${INVESTIGATION_PREFIX}${id}` : CANONICAL_KEY;
|
||||
const raw = safeGet(storage, key);
|
||||
if (raw === null) return; // nothing to restart
|
||||
const record = JSON.parse(raw);
|
||||
if (!isPlainObject(record)) return;
|
||||
|
||||
// Preserve container fields, reset reasoning-state fields
|
||||
record.situationGraph = null;
|
||||
record.selectedQuestion = null;
|
||||
record.summary = null;
|
||||
record.focusedContributions = [];
|
||||
record.findings = [];
|
||||
record.investigationReport = null;
|
||||
record.investigationRevision = 0;
|
||||
record.updatedAt = new Date().toISOString();
|
||||
|
||||
_persist(storage, key, JSON.stringify(record));
|
||||
} catch (_) { /* storage errors must not crash caller */ }
|
||||
}
|
||||
|
||||
// ── internals ────────────────────────────────────────────────────────
|
||||
|
||||
function _persist(storage, key, value) {
|
||||
|
||||
@@ -600,3 +600,218 @@ describe("investigation-storage v0.60g1 listing", () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// ── v0.60j — restartInvestigation semantic reset ─────────────────────
|
||||
|
||||
describe("investigation-storage v0.60j restart", () => {
|
||||
|
||||
function newId() {
|
||||
return `inv-${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
|
||||
async function getWrapper() {
|
||||
vi.resetModules();
|
||||
const m = await import("../../lib/storage/investigation-storage.js");
|
||||
return {
|
||||
restart: m.restartInvestigation,
|
||||
load: m.loadInvestigation,
|
||||
list: m.listInvestigations,
|
||||
save: m.saveInvestigation,
|
||||
};
|
||||
}
|
||||
|
||||
function makeRestartedSnapshot(overrides = {}) {
|
||||
return {
|
||||
id: overrides.id ?? newId(),
|
||||
scenario: "Test scenario text",
|
||||
situationGraph: { nodes: [{ id: "n1" }], edges: [] },
|
||||
selectedQuestion: { question: "Why?" },
|
||||
summary: "Previous understanding",
|
||||
focusedContributions: [{ evidence: "some evidence" }],
|
||||
findings: [{ id: "f1", proposition: "A finding" }],
|
||||
investigationReport: { understanding: "report text", generatedFromRevision: 5 },
|
||||
investigationRevision: 3,
|
||||
updatedAt: "2026-09-01T10:00:00.000Z",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
if (globalThis.window && globalThis.window.localStorage) {
|
||||
try { globalThis.window.localStorage.clear(); } catch(e) {}
|
||||
}
|
||||
});
|
||||
|
||||
// ── A — restart preserves identity ──────────────────────────────
|
||||
|
||||
it("restart preserves the Investigation durable id", async () => {
|
||||
const { save, load, restart } = await getWrapper();
|
||||
|
||||
const snap = makeRestartedSnapshot({ id: "inv-a" });
|
||||
save(snap);
|
||||
restart("inv-a");
|
||||
|
||||
const loaded = load("inv-a");
|
||||
expect(loaded).not.toBeNull();
|
||||
expect(loaded.id).toBe("inv-a");
|
||||
});
|
||||
|
||||
// ── B — restart preserves container-required state ──────────────
|
||||
|
||||
it("restart preserves scenario framing", async () => {
|
||||
const { save, load, restart } = await getWrapper();
|
||||
|
||||
const snap = makeRestartedSnapshot({ id: "inv-b" });
|
||||
save(snap);
|
||||
restart("inv-b");
|
||||
|
||||
const loaded = load("inv-b");
|
||||
expect(loaded.scenario).toBe("Test scenario text");
|
||||
});
|
||||
|
||||
it("restart preserves schemaVersion", async () => {
|
||||
const { save, load, restart } = await getWrapper();
|
||||
|
||||
const snap = makeRestartedSnapshot({ id: "inv-c" });
|
||||
save(snap);
|
||||
restart("inv-c");
|
||||
|
||||
const loaded = load("inv-c");
|
||||
expect(loaded.schemaVersion).toBe(1);
|
||||
});
|
||||
|
||||
// ── C — restart removes previous reasoning/report state ────────
|
||||
|
||||
it("restart clears situationGraph", async () => {
|
||||
const { save, load, restart } = await getWrapper();
|
||||
|
||||
const snap = makeRestartedSnapshot({ id: "inv-d" });
|
||||
save(snap);
|
||||
restart("inv-d");
|
||||
|
||||
const loaded = load("inv-d");
|
||||
expect(loaded.situationGraph).toBe(null);
|
||||
});
|
||||
|
||||
it("restart clears selectedQuestion", async () => {
|
||||
const { save, load, restart } = await getWrapper();
|
||||
|
||||
const snap = makeRestartedSnapshot({ id: "inv-e" });
|
||||
save(snap);
|
||||
restart("inv-e");
|
||||
|
||||
const loaded = load("inv-e");
|
||||
expect(loaded.selectedQuestion).toBe(null);
|
||||
});
|
||||
|
||||
it("restart clears summary (current understanding)", async () => {
|
||||
const { save, load, restart } = await getWrapper();
|
||||
|
||||
const snap = makeRestartedSnapshot({ id: "inv-f" });
|
||||
save(snap);
|
||||
restart("inv-f");
|
||||
|
||||
const loaded = load("inv-f");
|
||||
expect(loaded.summary).toBe(null);
|
||||
});
|
||||
|
||||
it("restart clears focusedContributions", async () => {
|
||||
const { save, load, restart } = await getWrapper();
|
||||
|
||||
const snap = makeRestartedSnapshot({ id: "inv-g" });
|
||||
save(snap);
|
||||
restart("inv-g");
|
||||
|
||||
const loaded = load("inv-g");
|
||||
expect(loaded.focusedContributions).toEqual([]);
|
||||
});
|
||||
|
||||
it("restart clears findings", async () => {
|
||||
const { save, load, restart } = await getWrapper();
|
||||
|
||||
const snap = makeRestartedSnapshot({ id: "inv-h" });
|
||||
save(snap);
|
||||
restart("inv-h");
|
||||
|
||||
const loaded = load("inv-h");
|
||||
expect(loaded.findings).toEqual([]);
|
||||
});
|
||||
|
||||
it("restart clears investigationReport (stale Report must not survive)", async () => {
|
||||
const { save, load, restart } = await getWrapper();
|
||||
|
||||
const snap = makeRestartedSnapshot({ id: "inv-i" });
|
||||
save(snap);
|
||||
restart("inv-i");
|
||||
|
||||
const loaded = load("inv-i");
|
||||
expect(loaded.investigationReport).toBe(null);
|
||||
});
|
||||
|
||||
it("restart resets investigationRevision to 0", async () => {
|
||||
const { save, load, restart } = await getWrapper();
|
||||
|
||||
const snap = makeRestartedSnapshot({ id: "inv-j" });
|
||||
save(snap);
|
||||
restart("inv-j");
|
||||
|
||||
const loaded = load("inv-j");
|
||||
expect(loaded.investigationRevision).toBe(0);
|
||||
});
|
||||
|
||||
// ── D — restart is isolated ────────────────────────────────────
|
||||
|
||||
it("restart A does not affect B", async () => {
|
||||
const { save, load, restart } = await getWrapper();
|
||||
|
||||
save(makeRestartedSnapshot({ id: "inv-a" }));
|
||||
save(makeRestartedSnapshot({ id: "inv-b" }));
|
||||
|
||||
restart("inv-a");
|
||||
|
||||
const bLoaded = load("inv-b");
|
||||
expect(bLoaded.id).toBe("inv-b");
|
||||
expect(bLoaded.scenario).toBe("Test scenario text");
|
||||
expect(bLoaded.situationGraph).not.toBeNull(); // B untouched
|
||||
expect(bLoaded.findings.length).toBe(1); // B findings preserved
|
||||
});
|
||||
|
||||
// ── E — listing remains after restart ──────────────────────────
|
||||
|
||||
it("restart does not remove Investigation from listInvestigations", async () => {
|
||||
const { save, load, restart, list } = await getWrapper();
|
||||
|
||||
save(makeRestartedSnapshot({ id: "inv-k" }));
|
||||
const before = list();
|
||||
expect(before.length).toBe(1);
|
||||
|
||||
restart("inv-k");
|
||||
|
||||
const after = list();
|
||||
expect(after.length).toBe(1);
|
||||
expect(after[0].id).toBe("inv-k");
|
||||
});
|
||||
|
||||
// ── F — missing identity does not restart singleton ────────────
|
||||
|
||||
it("restart with undefined id is silently no-op (no singleton fallback)", async () => {
|
||||
const { save, load, restart } = await getWrapper();
|
||||
|
||||
// Set up a legacy singleton entry
|
||||
if (globalThis.window && globalThis.window.localStorage) {
|
||||
globalThis.window.localStorage.setItem(
|
||||
"confidence-engine-investigation",
|
||||
JSON.stringify(makeRestartedSnapshot({ id: "legacy-singleton" }))
|
||||
);
|
||||
}
|
||||
|
||||
restart(undefined); // should be no-op, not touch singleton
|
||||
|
||||
// Singleton entry still exists (unchanged)
|
||||
const raw = globalThis.window?.localStorage.getItem("confidence-engine-investigation");
|
||||
expect(raw).not.toBeNull();
|
||||
const parsed = JSON.parse(raw);
|
||||
expect(parsed.id).toBe("legacy-singleton");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* 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.
|
||||
});
|
||||
Reference in New Issue
Block a user