/** * Focused integration contract tests verifying ScenarioForm uses the new * investigation-storage provider (loadInvestigation / saveInvestigation * / clearInvestigation) at ALL its call sites. * * Deterministic in-memory mocks — no React, no Playwright, no LLM calls. */ import { describe, it, expect, vi } from "vitest"; // ── test-time storage mock (injected into globalThis.window) ─────────── class MockStorageMap { constructor() { this._data = new Map(); } getItem(k) { return this._data.has(k) ? this._data.get(k) : null; } setItem(k, v){ this._data.set(k, v); } removeItem(k){ this._data.delete(k); } clear() { this._data.clear(); } } function installMockStorage() { Object.defineProperty(globalThis, "window", { value: globalThis.window || {}, writable: true, configurable: true, }); if (!globalThis.window.localStorage) { Object.defineProperty(globalThis.window, "localStorage", { value: new MockStorageMap(), writable: true, configurable: true, }); } if (!globalThis.window.sessionStorage) { Object.defineProperty(globalThis.window, "sessionStorage", { value: new MockStorageMap(), writable: true, configurable: true, }); } } function uninstallMockStorage() { const desc = Object.getOwnPropertyDescriptor(globalThis, "window"); if (desc && !("localStorage" in globalThis.window)) return; delete globalThis.window.localStorage; delete globalThis.window.sessionStorage; if (Object.getOwnPropertyNames(globalThis.window).length === 0) { delete globalThis.window; } } // ── lifecycle ────────────────────────────────────────────────────────── beforeEach(() => { installMockStorage(); }); afterEach (() => { uninstallMockStorage(); }); // ── import under test (loaded once, module cache preserved) ─────────── let storage = null; async function getStorageModule() { if (!storage) { const m = await import("../../lib/storage/investigation-storage.js"); storage = { load: m.loadInvestigation, save: m.saveInvestigation, clear: m.clearInvestigation, }; } return storage; } // ── Shared snapshot factories mirroring ScenarioForm's call shapes ──── function makeStartCaseSnapshot(overrides = {}) { return { scenario: overrides.scenario || "Test scenario text", situationGraph: { centralStatement: "Central statement", nodes: [{ id: "n-1", kind: "observation" }], edges: [], }, selectedQuestion: { question: "Next question?" }, summary: overrides.summary ?? "Current understanding", updatedAt: new Date().toISOString(), focusedContributions: overrides.focusedContributions ?? [], findings: overrides.findings ?? [], schemaVersion: 1, }; } function makeUpdateCaseSnapshot(overrides = {}) { return { scenario: overrides.scenario || "Test scenario text", situationGraph: { centralStatement: "Central statement", nodes: [{ id: "n-1", kind: "observation" }, { id: "n-2", kind: "conclusion" }], edges: [], activeUnknownNodeId: "n-child-1", }, selectedQuestion: { question: "Updated question?" }, summary: overrides.summary ?? "Updated understanding", updatedAt: new Date().toISOString(), focusedContributions: overrides.focusedContributions ?? [ { id: "contrib-0001", sequence: 1, targetNodeId: "nk-001" }, ], findings: overrides.findings ?? [ { id: "finding-a1", proposition: "A finding", status: "provisional", evaluation: "considered" }, ], schemaVersion: 1, }; } // ════════════════════════════════════════════════════════════════ // Tests — ScenarioForm persistence integration with new provider // ════════════════════════════════════════════════════════════════ describe("ScenarioForm → investigation-storage integration", () => { // ── A. start-case save/load round-trip (handleSubmit path) ──── it("start-case snapshot: save then load preserves all fields", async () => { const { load, save } = await getStorageModule(); const snap = makeStartCaseSnapshot({ scenario: "My investigation topic", summary: "Initial understanding", }); save(snap); const loaded = load(); expect(loaded).not.toBeNull(); expect(loaded.scenario).toBe("My investigation topic"); expect(loaded.situationGraph.centralStatement).toBe("Central statement"); expect(loaded.selectedQuestion.question).toBe("Next question?"); expect(loaded.summary).toBe("Initial understanding"); expect(loaded.focusedContributions).toEqual([]); expect(loaded.findings).toEqual([]); }); it("start-case snapshot: schemaVersion = 1 persisted", async () => { const { load, save } = await getStorageModule(); save(makeStartCaseSnapshot({ schemaVersion: undefined })); const loaded = load(); expect(loaded.schemaVersion).toBe(1); }); // ── B. update-case save/load round-trip (handleUpdate path) ──── it("update-case snapshot: save then load preserves focusedContributions and findings", async () => { const { load, save } = await getStorageModule(); const snap = makeUpdateCaseSnapshot({ focusedContributions: [ { id: "contrib-0001", sequence: 1, targetNodeId: "nk-001", question: "Q1?", answer: "A1" }, { id: "contrib-0002", sequence: 2, targetNodeId: "nk-002", question: "Q2?", answer: "A2" }, ], findings: [ { id: "finding-a1", proposition: "Finding A", status: "provisional", evaluation: "considered" }, { id: "finding-b1", proposition: "Finding B", status: "provisional", evaluation: "rejected" }, ], }); save(snap); const loaded = load(); expect(loaded).not.toBeNull(); expect(loaded.focusedContributions).toHaveLength(2); expect(loaded.focusedContributions[0].id).toBe("contrib-0001"); expect(loaded.focusedContributions[1].id).toBe("contrib-0002"); expect(loaded.findings).toHaveLength(2); expect(loaded.findings[0].id).toBe("finding-a1"); }); // ── C. clearInvestigation clears start-case data (onRestart) ──── it("clearInvestigation removes start-case snapshot (matches onRestart behavior)", async () => { const { load, save, clear } = await getStorageModule(); save(makeStartCaseSnapshot()); expect(load()).not.toBeNull(); clear(); // mirrors: onRestart -> clearInvestigation() expect(load()).toBeNull(); }); it("clearInvestigation removes update-case snapshot (matches ContinueLaterBanner behavior)", async () => { const { load, save, clear } = await getStorageModule(); save(makeUpdateCaseSnapshot()); expect(load()).not.toBeNull(); clear(); // mirrors: ContinueLaterBanner.onRestart -> clearInvestigation() expect(load()).toBeNull(); }); it("clearInvestigation removes start-case snapshot (matches reset button behavior)", async () => { const { load, save, clear } = await getStorageModule(); save(makeStartCaseSnapshot({ scenario: "To be cleared" })); clear(); // mirrors: reset button onClick -> clearInvestigation() expect(load()).toBeNull(); }); // ── D. ScenarioForm lifecycle: save → reload → second save → clear ─── it("full lifecycle: start-case → restore partial → update-case → reload → verify → clear", async () => { const { load, save, clear } = await getStorageModule(); // Phase 1: Start case (handleSubmit path) const startSnap = makeStartCaseSnapshot({ scenario: "Phase 1" }); save(startSnap); expect(load().scenario).toBe("Phase 1"); // Phase 2: User adds focused contributions and findings (simulates user session) const updateSnap = makeUpdateCaseSnapshot({ scenario: "Phase 2", summary: "Updated understanding", focusedContributions: [ { id: "contrib-0001", sequence: 1, targetNodeId: "nk-ph2" }, ], findings: [{ id: "finding-x", proposition: "X", status: "provisional" }], }); save(updateSnap); const loaded = load(); expect(loaded.scenario).toBe("Phase 2"); expect(loaded.summary).toBe("Updated understanding"); expect(loaded.focusedContributions).toHaveLength(1); expect(loaded.findings).toHaveLength(1); // Phase 3: User restarts (clearInvestigation) clear(); expect(load()).toBeNull(); }); // ── E. ScenarioForm partial session restoration (no graph → idle) ─── it("load returns snapshot without situationGraph → triggers scenario-entry form", async () => { const { load, save } = await getStorageModule(); const partialSnap = { scenario: "Incomplete investigation", summary: null, updatedAt: new Date().toISOString(), focusedContributions: [], findings: [], schemaVersion: 1, }; save(partialSnap); const loaded = load(); expect(loaded).not.toBeNull(); expect(loaded.scenario).toBe("Incomplete investigation"); expect(loaded.situationGraph).toBeUndefined(); }); // ── F. start-case snapshot shape fidelity (exact ScenarioForm call) ─── it("start-case save exactly mirrors ScenarioForm handleSubmit shape", async () => { const { load, save } = await getStorageModule(); // Exact shape from scenario-form.jsx line ~403: // saveInvestigation({ scenario, situationGraph, selectedQuestion, summary, updatedAt, focusedContributions, findings: [] }) const snap = { scenario: "test-scenario", situationGraph: { centralStatement: "CS", nodes: [], edges: [] }, selectedQuestion: "What next?", summary: null, updatedAt: new Date().toISOString(), focusedContributions: [ { id: "contrib-0001", sequence: 1, targetNodeId: "nk-test", question: "Q" }, ], findings: [], }; save(snap); const loaded = load(); expect(loaded.scenario).toBe("test-scenario"); expect(loaded.situationGraph.centralStatement).toBe("CS"); expect(loaded.selectedQuestion).toBe("What next?"); expect(loaded.summary).toBeNull(); expect(loaded.focusedContributions).toHaveLength(1); expect(loaded.findings).toEqual([]); }); // ── G. update-case snapshot shape fidelity (exact ScenarioForm call) ─── it("update-case save exactly mirrors ScenarioForm handleUpdate shape", async () => { const { load, save } = await getStorageModule(); // Exact shape from scenario-form.jsx line ~471: // saveInvestigation({ scenario, situationGraph, selectedQuestion, summary, updatedAt, focusedContributions, findings: newFindings }) const snap = { scenario: "test-scenario", situationGraph: { centralStatement: "CS", nodes: [], edges: [] }, selectedQuestion: "Updated question?", summary: "Updated understanding text", updatedAt: new Date().toISOString(), focusedContributions: [ { id: "contrib-0001", sequence: 1, targetNodeId: "nk-test" }, ], findings: [ { id: "finding-a1", proposition: "A finding", status: "provisional", evaluation: "considered" }, ], }; save(snap); const loaded = load(); expect(loaded.scenario).toBe("test-scenario"); expect(loaded.summary).toBe("Updated understanding text"); expect(loaded.findings).toHaveLength(1); }); // ── H. localStorage vs sessionStorage — ScenarioForm no longer writes session-level data ─── it("ScenarioForm saves to localStorage (canonical key), not sessionStorage", async () => { const { load, save } = await getStorageModule(); save(makeStartCaseSnapshot()); // Should be in localStorage (canonical key) const localVal = globalThis.window.localStorage.getItem("confidence-engine-investigation"); expect(localVal).not.toBeNull(); // Should NOT be in sessionStorage at the legacy key const sessionVal = globalThis.window.sessionStorage.getItem("confidence-engine-session"); expect(sessionVal).toBeNull(); }); });