feat(confidence-engine): v0.60g1 list investigation summaries
Recover to clean v0.60f then implement only the storage listing contract. - Add listInvestigations() to provider: enumerate by prefix, project lightweight summary (id, scenario, updatedAt, investigationRevision, reportExists, reportGeneratedFromRevision), sort by updatedAt desc - Add application-facing wrapper in investigation-storage.js - Add 8 deterministic tests covering all listing invariants (coexistence, correct IDs, lightweight projection, legacy exclusion, unrelated exclusion, independent update, ordering, malformed skip) - Fix MockStorageMap WebStorage API compatibility (.length + .key(i)) - Portfolio NOT migrated — that is v0.60g2
This commit is contained in:
@@ -9,11 +9,16 @@ 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(); }
|
||||
constructor() {
|
||||
this._data = new Map();
|
||||
this._keys = []; // ordered key list for WebStorage API compatibility
|
||||
}
|
||||
get length() { return this._data.size; }
|
||||
getItem(k) { return this._data.has(k) ? this._data.get(k) : null; }
|
||||
setItem(k, v) { this._data.set(k, String(v)); if (!this._keys.includes(k)) this._keys.push(k); }
|
||||
removeItem(k) { this._data.delete(k); const idx = this._keys.indexOf(k); if (idx !== -1) this._keys.splice(idx, 1); }
|
||||
clear() { this._data.clear(); this._keys.length = 0; }
|
||||
key(n) { return this._keys[n] ?? null; }
|
||||
}
|
||||
|
||||
function installMockStorage() {
|
||||
@@ -23,20 +28,18 @@ function installMockStorage() {
|
||||
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,
|
||||
});
|
||||
}
|
||||
// Always force-install mock storage (previous uninstall may have left window
|
||||
// without own properties but with inherited ones in some environments)
|
||||
Object.defineProperty(globalThis.window, "localStorage", {
|
||||
value: new MockStorageMap(),
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(globalThis.window, "sessionStorage", {
|
||||
value: new MockStorageMap(),
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
function uninstallMockStorage() {
|
||||
@@ -431,3 +434,169 @@ describe("investigation-storage v0.60d canonical identity", () => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// ── v0.60g1 — listInvestigations lightweight summaries ───────────────
|
||||
|
||||
describe("investigation-storage v0.60g1 listing", () => {
|
||||
|
||||
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 {
|
||||
list: m.listInvestigations,
|
||||
save: m.saveInvestigation,
|
||||
load: m.loadInvestigation,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
if (globalThis.window && globalThis.window.localStorage) {
|
||||
try { globalThis.window.localStorage.clear(); } catch(e) {}
|
||||
}
|
||||
});
|
||||
|
||||
// ── 1. Two durable-ID Investigations coexist ────────────────────
|
||||
|
||||
it("listInvestigations returns summaries for two durable-ID Investigations", async () => {
|
||||
const { list, save } = await getWrapper();
|
||||
save(makeSnapshot({ id: newId(), scenario: "Scenario A" }));
|
||||
save(makeSnapshot({ id: newId(), scenario: "Scenario B" }));
|
||||
|
||||
const summaries = list();
|
||||
expect(summaries.length).toBe(2);
|
||||
});
|
||||
|
||||
// ── 2. Correct identities ───────────────────────────────────────
|
||||
|
||||
it("each summary carries its correct durable ID", async () => {
|
||||
const { list, save } = await getWrapper();
|
||||
const idA = newId();
|
||||
const idB = newId();
|
||||
save(makeSnapshot({ id: idA, scenario: "Scenario A" }));
|
||||
save(makeSnapshot({ id: idB, scenario: "Scenario B" }));
|
||||
|
||||
const summaries = list();
|
||||
expect(summaries.map(s => s.id)).toContain(idA);
|
||||
expect(summaries.map(s => s.id)).toContain(idB);
|
||||
});
|
||||
|
||||
// ── 3. Lightweight projection ───────────────────────────────────
|
||||
|
||||
it("summary contains portfolio fields but not canonical payload", async () => {
|
||||
const { list, save } = await getWrapper();
|
||||
save(makeSnapshot({ id: newId(), scenario: "Scenario A" }));
|
||||
|
||||
const summaries = list();
|
||||
const summary = summaries[0];
|
||||
|
||||
// Portfolio-required fields present
|
||||
expect(summary).toHaveProperty("id");
|
||||
expect(summary).toHaveProperty("scenario");
|
||||
expect(summary).toHaveProperty("updatedAt");
|
||||
expect(summary).toHaveProperty("investigationRevision");
|
||||
expect(summary).toHaveProperty("reportExists");
|
||||
expect(summary).toHaveProperty("reportGeneratedFromRevision");
|
||||
|
||||
// Canonical payload fields NOT exposed
|
||||
expect(summary.situationGraph).toBeUndefined();
|
||||
expect(summary.findings).toBeUndefined();
|
||||
expect(summary.investigationReport).toBeUndefined();
|
||||
});
|
||||
|
||||
// ── 4. Legacy singleton exclusion ───────────────────────────────
|
||||
|
||||
it("legacy singleton is not included in listing", async () => {
|
||||
const { list } = await getWrapper();
|
||||
|
||||
globalThis.window.localStorage.setItem(
|
||||
"confidence-engine-investigation",
|
||||
JSON.stringify(makeSnapshot({ legacy: true }))
|
||||
);
|
||||
|
||||
const summaries = list();
|
||||
expect(summaries.length).toBe(0);
|
||||
});
|
||||
|
||||
// ── 5. Unrelated storage exclusion ──────────────────────────────
|
||||
|
||||
it("unrelated localStorage record is not included", async () => {
|
||||
const { list, save } = await getWrapper();
|
||||
|
||||
globalThis.window.localStorage.setItem(
|
||||
"confidence-engine-investigation-metadata",
|
||||
'{"data":42}'
|
||||
);
|
||||
save(makeSnapshot({ id: newId(), scenario: "Scenario X" }));
|
||||
|
||||
const summaries = list();
|
||||
expect(summaries.length).toBe(1);
|
||||
});
|
||||
|
||||
// ── 6. Independent update preserves other Investigation ─────────
|
||||
|
||||
it("updating inv-a does not replace or remove inv-b", async () => {
|
||||
const { list, save } = await getWrapper();
|
||||
|
||||
const idA = newId();
|
||||
const idB = newId();
|
||||
|
||||
save(makeSnapshot({ id: idA, scenario: "Scenario A" }));
|
||||
save(makeSnapshot({ id: idB, scenario: "Scenario B" }));
|
||||
|
||||
// Update inv-a
|
||||
save(makeSnapshot({ id: idA, scenario: "Updated Scenario A" }));
|
||||
|
||||
const summaries = list();
|
||||
expect(summaries.length).toBe(2);
|
||||
|
||||
const aSummary = summaries.find(s => s.id === idA);
|
||||
const bSummary = summaries.find(s => s.id === idB);
|
||||
|
||||
expect(aSummary.scenario).toBe("Updated Scenario A");
|
||||
expect(bSummary.scenario).toBe("Scenario B");
|
||||
});
|
||||
|
||||
// ── 7. Deterministic ordering by updatedAt descending ───────────
|
||||
|
||||
it("most recently updated Investigation listed first", async () => {
|
||||
const { list, save } = await getWrapper();
|
||||
|
||||
const idA = newId();
|
||||
const idB = newId();
|
||||
|
||||
save(makeSnapshot({
|
||||
id: idA,
|
||||
scenario: "Scenario A",
|
||||
updatedAt: "2026-09-01T10:00:00.000Z",
|
||||
}));
|
||||
save(makeSnapshot({
|
||||
id: idB,
|
||||
scenario: "Scenario B",
|
||||
updatedAt: "2026-09-02T10:00:00.000Z",
|
||||
}));
|
||||
|
||||
const summaries = list();
|
||||
expect(summaries[0].id).toBe(idB);
|
||||
expect(summaries[1].id).toBe(idA);
|
||||
});
|
||||
|
||||
// ── 8. Malformed durable-ID entry skipped ──────────────────────
|
||||
|
||||
it("malformed durable-ID entry does not prevent listing valid records", async () => {
|
||||
globalThis.window.localStorage.setItem(
|
||||
"confidence-engine-investigation:inv-malformed",
|
||||
"not valid json {{{"
|
||||
);
|
||||
|
||||
const { list, save } = await getWrapper();
|
||||
save(makeSnapshot({ id: newId(), scenario: "Valid Scenario" }));
|
||||
|
||||
const summaries = list();
|
||||
expect(summaries.length).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user