Persist capture data per session (field values, schema, links)

Capture mode state lived only in React memory and was lost when
reopening a session or remounting EngineProvider.

- Add per-session localStorage capture snapshot (schema, values, links)
- Restore on session mount; persist on field/schema/link changes
- Seed binder links from storage without spurious bus events
- Clean up capture key when session is deleted
- Integration test for reload persistence
This commit is contained in:
2026-06-08 01:23:48 +02:00
parent ba34ba868f
commit b28feaad42
8 changed files with 437 additions and 9 deletions

View File

@@ -0,0 +1,95 @@
/**
* CE-WP-0008 — per-session capture state round-trip.
*/
import { describe, expect, it } from "vitest";
import type { SessionId } from "@shared/ids";
import {
CAPTURE_STATE_VERSION,
captureStateKey,
defaultCaptureState,
loadCaptureState,
persistCapturePatch,
removeCaptureState,
saveCaptureState,
} from "./capture-persistence";
function memoryStorage(): Storage {
const map = new Map<string, string>();
return {
get length() {
return map.size;
},
clear: () => map.clear(),
getItem: (k) => map.get(k) ?? null,
key: (i) => [...map.keys()][i] ?? null,
removeItem: (k) => void map.delete(k),
setItem: (k, v) => void map.set(k, v),
};
}
const SESSION = "sess_capture" as SessionId;
describe("capture-persistence", () => {
it("uses a per-session storage key", () => {
expect(captureStateKey(SESSION)).toBe(
"citation-evidence:session:sess_capture:capture-state:v1",
);
});
it("round-trips schema, field values, and links", () => {
const storage = memoryStorage();
const state = defaultCaptureState();
const withData = {
...state,
fieldValues: { summary: "Tenant owes arrears", deadline: "2026-12-15" },
evidenceLinks: [
{
id: "evlink_1",
evidenceItemId: "evi_1",
targetType: "form-field",
targetId: "summary",
relation: "supports",
status: "candidate",
createdAt: "2026-06-08T00:00:00.000Z",
updatedAt: "2026-06-08T00:00:00.000Z",
},
],
} as const;
saveCaptureState(SESSION, withData, storage);
const loaded = loadCaptureState(SESSION, storage);
expect(loaded?.version).toBe(CAPTURE_STATE_VERSION);
expect(loaded?.fieldValues).toEqual(withData.fieldValues);
expect(loaded?.evidenceLinks).toHaveLength(1);
expect(loaded?.formSchema.id).toBe("demo-form");
});
it("persistCapturePatch merges without dropping other fields", () => {
const storage = memoryStorage();
saveCaptureState(
SESSION,
{
...defaultCaptureState(),
fieldValues: { amount: "1200" },
evidenceLinks: [],
},
storage,
);
persistCapturePatch(SESSION, { fieldValues: { amount: "1500", summary: "Updated" } }, storage);
const loaded = loadCaptureState(SESSION, storage);
expect(loaded?.fieldValues).toEqual({ amount: "1500", summary: "Updated" });
});
it("removeCaptureState clears the key", () => {
const storage = memoryStorage();
saveCaptureState(SESSION, defaultCaptureState(), storage);
removeCaptureState(SESSION, storage);
expect(loadCaptureState(SESSION, storage)).toBeNull();
});
});