/** * Focused provider-level contract tests for investigation-storage. * * 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, String(v)); } removeItem(k){ this._data.delete(k); } clear() { this._data.clear(); } } function installMockStorage() { // Define window with mock localStorage and sessionStorage as properties 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 (fresh module each time) ──────────────────────── async function getModule() { // Clear all cached modules to get fresh bindings per test vi.resetModules(); const m = await import("../../lib/storage/providers/local-storage.js"); return { load: m.loadInvestigation, save: m.saveInvestigation, clear: m.clearInvestigation }; } // ── shared snapshot factories ────────────────────────────────────────── function makeSnapshot(overrides = {}) { return { schemaVersion: overrides.schemaVersion ?? 1, situationGraph: { nodes: [{ id: "n1", label: "A" }], edges: [] }, openQuestions: ["q1"], assumptions: [], observations: [], uncertainties: [], relationships: [], messages: [], ...overrides, }; } function legacySnapshot() { return { situationGraph: { nodes: [{ id: "old" }], edges: [] }, legacy: true }; } // ════════════════════════════════════════════════════════════════ // Tests // ════════════════════════════════════════════════════════════════ describe("investigation-storage provider", () => { // ── 1. save → load round trip ─────────────────────────────────── it("save then load returns the same snapshot (round-trip)", async () => { const { load, save } = await getModule(); const snap = makeSnapshot({ situationGraph: { nodes: [{ id: "x1" }], edges: [] }, openQuestions: ["why?"] }); save(snap); const loaded = load(); expect(loaded).not.toBeNull(); expect(loaded.situationGraph.nodes[0].id).toBe("x1"); expect(loaded.openQuestions).toEqual(["why?"]); }); // ── 2. schemaVersion = 1 persisted ────────────────────────────── it("persisted record includes schemaVersion = 1", async () => { const { load, save } = await getModule(); const snap = makeSnapshot({ schemaVersion: undefined }); save(snap); const loaded = load(); expect(loaded.schemaVersion).toBe(1); }); // ── 3. caller snapshot object not mutated ─────────────────────── it("save does not mutate the caller's snapshot object", async () => { const { save } = await getModule(); const snap = { nodes: [{ id: "a" }] }; const before = JSON.stringify(snap); save(snap); expect(JSON.stringify(snap)).toBe(before); }); // ── 4. clear → load returns null ──────────────────────────────── it("clear then load returns null", async () => { const { load, save, clear } = await getModule(); save(makeSnapshot()); expect(load()).not.toBeNull(); clear(); expect(load()).toBeNull(); }); // ── 5. malformed localStorage → null, no throw ───────────────── it("malformed localStorage returns null without throwing", async () => { const { load } = await getModule(); globalThis.window.localStorage.setItem( "confidence-engine-investigation", "not valid json {{{" ); expect(() => load()).not.toThrow(); expect(load()).toBeNull(); }); // ── 6. localStorage takes precedence over sessionStorage ──────── it("localStorage value used even when legacy sessionStorage exists", async () => { const { load, save } = await getModule(); const localSnap = makeSnapshot({ source: "local" }); const sessionSnap = legacySnapshot(); // no schemaVersion save(localSnap); globalThis.window.sessionStorage.setItem( "confidence-engine-session", JSON.stringify(sessionSnap) ); const loaded = load(); expect(loaded).not.toBeNull(); expect(loaded.source).toBe("local"); // from localStorage, not sessionStorage }); // ── 7. empty localStorage + valid legacy sessionStorage → legacy returned ── it("empty canonical key returns legacy snapshot when no local state", async () => { const { load, save } = await getModule(); save(makeSnapshot({ source: "local" })); // populate a value globalThis.window.localStorage.clear(); // remove it (canonical key gone) globalThis.window.sessionStorage.setItem( "confidence-engine-session", JSON.stringify(legacySnapshot()) ); const loaded = load(); expect(loaded).not.toBeNull(); expect(loaded.legacy).toBe(true); // came from sessionStorage }); // ── 8. legacy snapshot copied into localStorage ───────────────── it("legacy migration copies snapshot into canonical key", async () => { const { load } = await getModule(); globalThis.window.localStorage.clear(); globalThis.window.sessionStorage.setItem( "confidence-engine-session", JSON.stringify(legacySnapshot()) ); load(); // triggers migration const migrated = globalThis.window.localStorage.getItem("confidence-engine-investigation"); expect(migrated).not.toBeNull(); expect(JSON.parse(migrated).legacy).toBe(true); expect(JSON.parse(migrated).schemaVersion).toBe(1); }); // ── 9. unrelated keys untouched ───────────────────────────────── it("unrelated storage keys remain untouched", async () => { const { load, save } = await getModule(); globalThis.window.localStorage.setItem("unrelated-key", '{"data":42}'); globalThis.window.sessionStorage.setItem("unrelated-session", "hello"); save(makeSnapshot()); load(); expect(globalThis.window.localStorage.getItem("unrelated-key")).toBe('{"data":42}'); expect(globalThis.window.sessionStorage.getItem("unrelated-session")).toBe("hello"); }); // ── 10. non-browser environment safely returns null / no-op ───── it("non-browser environment returns null load, safe no-op save/clear", async () => { uninstallMockStorage(); // removes window entirely const { load, save, clear } = await getModule(); expect(load()).toBeNull(); // no browser → null expect(() => save(makeSnapshot())).not.toThrow(); // no-op without crash expect(() => clear()).not.toThrow(); // no-op without crash installMockStorage(); // restore for remaining tests }); });