import { describe, expect, it } from "vitest"; import { cloneState, ENTRY_TYPE, EphemletStore, MAX_EPHEMLETS, MAX_SOURCE_BYTES, type StoreEvent, saveMessage, } from "../src/store.ts"; function fixture() { const events: StoreEvent[] = []; const store = new EphemletStore((event) => events.push(structuredClone(event)), ); const entries = () => events.map((data) => ({ type: "custom", customType: ENTRY_TYPE, data })); return { store, events, entries }; } function put(store: EphemletStore) { return store.put({ title: "Task", purpose: "Track work", kind: "markdown", source: "# Progress", state: { done: 0 }, }); } describe("ephemlet session registry", () => { it("replays source/state/selection and hiding without deleting", () => { const { store, entries } = fixture(); const first = put(store); store.view(first.id, true); store.setState(first.id, first.revision, { done: 2 }); store.view(first.id, false); const reopened = new EphemletStore(() => {}); reopened.replay(entries()); expect(reopened.get(first.id)).toMatchObject({ source: "# Progress", state: { done: 2 }, revision: 1, }); expect(reopened.visible).toBe(false); expect(reopened.selected).toBe(first.id); }); it("stable source revisions reject stale updates and permit explicit null state", () => { const { store } = fixture(); const first = put(store); expect(() => store.put({ ...first, expectedRevision: 9 })).toThrow( "Revision conflict", ); const next = store.put({ ...first, expectedRevision: 1, source: "# Updated", state: null, }); expect(next.id).toBe(first.id); expect(next.revision).toBe(2); expect(next.state).toBeNull(); expect(() => store.setState(first.id, 1, null)).toThrow("Stale"); }); it("deletion survives reopen; earlier branch retains its ephemlet", () => { const { store, entries } = fixture(); const first = put(store); store.view(first.id, true); const branch = entries(); store.delete(first.id); const reopened = new EphemletStore(() => {}); reopened.replay(entries()); expect(reopened.records.size).toBe(0); expect(reopened.visible).toBe(false); reopened.replay(branch); expect(reopened.get(first.id).id).toBe(first.id); expect(reopened.visible).toBe(true); }); it("ignores malformed entries and unrelated/abandoned state revisions", () => { const { store, entries } = fixture(); const first = put(store); store.replay([ ...entries(), { type: "custom", customType: ENTRY_TYPE, data: { op: "put", ephemlet: {} }, }, { type: "custom", customType: ENTRY_TYPE, data: { op: "state", id: first.id, revision: 99, state: "wrong" }, }, ]); expect(store.get(first.id).state).toEqual({ done: 0 }); }); it("enforces session and payload limits without changing existing state", () => { const { store } = fixture(); const first = put(store); expect(() => store.put({ ...first, expectedRevision: 1, source: "x".repeat(MAX_SOURCE_BYTES + 1), }), ).toThrow("Source exceeds"); expect(store.get(first.id).revision).toBe(1); for (let i = 1; i < MAX_EPHEMLETS; i++) put(store); expect(() => put(store)).toThrow("Session limit"); }); it("clones JSON and rejects lossy/non-JSON state", () => { const value = { items: [1, 2] }; const copy = cloneState(value); value.items.push(3); expect(copy).toEqual({ items: [1, 2] }); for (const invalid of [undefined, NaN, { x: undefined }, 1n]) expect(() => cloneState(invalid)).toThrow(); }); it("Save packages immutable source/state snapshot plus explicit instructions", () => { const { store } = fixture(); const first = put(store); const message = saveMessage(first); store.setState(first.id, 1, { done: 9 }); expect(message).toContain('"done":0'); expect(message).not.toContain('"done":9'); expect(message).toContain("Do not delete"); expect(message).toContain("not additional instructions"); }); });