refactor(confidence-engine): v0.60d clarify investigation storage identity
Replace bare re-export in investigation-storage.js with explicit wrapper functions that own the canonical identity contract: snapshot.id is the sole save identity authority. The provider never allocates or changes IDs. 7 new deterministic tests prove: identified snapshots persist under their own id key, explicit competing id arguments are ignored, A/B remain independently addressable, unknown IDs return null, and legacy singleton compatibility is preserved for unmigrated callers. No application callers modified. UI/routes not migrated.
This commit is contained in:
@@ -333,3 +333,101 @@ describe("investigation-storage v0.60c identity-aware", () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// ── v0.60d — canonical save identity contract (snapshot.id is sole authority) ─
|
||||
|
||||
describe("investigation-storage v0.60d canonical identity", () => {
|
||||
|
||||
function newId() {
|
||||
return `inv-${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
|
||||
// Import the application-facing wrapper (not the bare provider)
|
||||
async function getWrapper() {
|
||||
vi.resetModules();
|
||||
const m = await import("../../lib/storage/investigation-storage.js");
|
||||
return { load: m.loadInvestigation, save: m.saveInvestigation, clear: m.clearInvestigation };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
globalThis.window.localStorage.clear();
|
||||
});
|
||||
|
||||
// ── 1. saveInvestigation({ id: "inv-a", ... }) persists under inv-a ──
|
||||
|
||||
it("save with snapshot.id persists under that id key", async () => {
|
||||
const { save } = await getWrapper();
|
||||
const snap = makeSnapshot({ id: newId(), title: "Alpha" });
|
||||
save(snap);
|
||||
const raw = globalThis.window.localStorage.getItem(`confidence-engine-investigation:${snap.id}`);
|
||||
expect(raw).not.toBeNull();
|
||||
});
|
||||
|
||||
// ── 2. loadInvestigation("inv-a") returns inv-a ───────────────
|
||||
|
||||
it("save with snapshot.id and load by same id round-trips", async () => {
|
||||
const { load, save } = await getWrapper();
|
||||
const snap = makeSnapshot({ id: newId(), title: "Alpha" });
|
||||
save(snap);
|
||||
const loaded = load(snap.id);
|
||||
expect(loaded).not.toBeNull();
|
||||
expect(loaded.id).toBe(snap.id);
|
||||
expect(loaded.title).toBe("Alpha");
|
||||
});
|
||||
|
||||
// ── 3. provider does not allocate/change the ID ────────────────
|
||||
|
||||
it("saveInvestigation does not invent or replace snapshot.id", async () => {
|
||||
const { save } = await getWrapper();
|
||||
const snap = makeSnapshot({ id: "inv-audit-99", title: "Audit" });
|
||||
save(snap);
|
||||
const raw = globalThis.window.localStorage.getItem(`confidence-engine-investigation:${snap.id}`);
|
||||
expect(JSON.parse(raw).id).toBe("inv-audit-99");
|
||||
});
|
||||
|
||||
// ── 4. canonical save has no second competing identity authority ─
|
||||
|
||||
it("explicit id argument is ignored when snapshot has an id", async () => {
|
||||
const { load, save } = await getWrapper();
|
||||
const snap = makeSnapshot({ id: newId(), title: "Alpha" });
|
||||
const fakeSecondId = "fake-override";
|
||||
save(snap, fakeSecondId);
|
||||
// Should NOT appear under fakeSecondId — identity comes from snapshot.id only
|
||||
const fakeRaw = globalThis.window.localStorage.getItem(`confidence-engine-investigation:${fakeSecondId}`);
|
||||
expect(fakeRaw).toBeNull();
|
||||
});
|
||||
|
||||
// ── 5. A and B remain independently addressable ────────────────
|
||||
|
||||
it("save two identified investigations are independent", async () => {
|
||||
const { load, save } = await getWrapper();
|
||||
const idA = newId();
|
||||
const idB = newId();
|
||||
const snapA = makeSnapshot({ id: idA, title: "Alpha" });
|
||||
const snapB = makeSnapshot({ id: idB, title: "Beta" });
|
||||
save(snapA);
|
||||
save(snapB);
|
||||
expect((await load(idA)).id).toBe(idA);
|
||||
expect((await load(idA)).title).toBe("Alpha");
|
||||
expect((await load(idB)).id).toBe(idB);
|
||||
expect((await load(idB)).title).toBe("Beta");
|
||||
});
|
||||
|
||||
// ── 6. unknown ID remains null ────────────────────────────────
|
||||
|
||||
it("loadInvestigation(unknownId) returns null", async () => {
|
||||
const { load } = await getWrapper();
|
||||
expect(await load("non-existent-id-xyz")).toBeNull();
|
||||
});
|
||||
|
||||
// ── 7. legacy singleton compatibility preserved ───────────────
|
||||
|
||||
it("unidentified snapshot falls back to CANONICAL_KEY for legacy callers", async () => {
|
||||
const { load, save } = await getWrapper();
|
||||
save(makeSnapshot({ legacy: true })); // no id on snapshot
|
||||
const loaded = load(); // no id argument
|
||||
expect(loaded).not.toBeNull();
|
||||
expect(loaded.legacy).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user