marmar-context / web / context / test / workspace-store.test.ts
workspace-store.test.ts
Raw
import { describe, it, expect } from "vitest";
import {
  nextRecord,
  WorkspaceConflict,
  workspaceContent,
} from "../src/workspace-store";
import { CASES } from "../server/cases";
import { extractLocal } from "../server/context";
import type { Workspace } from "../src/workspace";
const scenario = CASES[0];
const workspace: Workspace = {
  version: 1,
  savedAt: "2026-10-24T12:00:00Z",
  scenario,
  sources: scenario.sources,
  sourceId: scenario.sources[0].id,
  draft: extractLocal(scenario.sources),
  review: {},
  useModel: false,
  editing: true,
  editText: "Unfinished source edit",
  report: null,
};
describe("durable workspace revision protection", () => {
  it("creates a valid first record that preserves unfinished work", () => {
    const saved = nextRecord(undefined, workspace, null);
    expect(saved.revision).toBeTruthy();
    expect(JSON.parse(saved.payload).editText).toBe("Unfinished source edit");
  });
  it("does not revise unchanged work just because another tab opens it", () => {
    const saved = nextRecord(undefined, workspace, null);
    expect(
      nextRecord(saved, { ...workspace, savedAt: "later" }, saved.revision)
    ).toBe(saved);
  });
  it("rejects a stale tab without mutating newer work", () => {
    const saved = nextRecord(undefined, workspace, null);
    const newer = nextRecord(
      saved,
      { ...workspace, editText: "newer text" },
      saved.revision
    );
    const before = JSON.stringify(newer);
    expect(() =>
      nextRecord(
        newer,
        { ...workspace, editText: "stale edit" },
        saved.revision
      )
    ).toThrow(WorkspaceConflict);
    expect(JSON.stringify(newer)).toBe(before);
  });
  it("protects an existing workspace from an old tab-only migration", () => {
    const saved = nextRecord(undefined, workspace, null);
    expect(() => nextRecord(saved, workspace, null)).toThrow(WorkspaceConflict);
  });
  it("requires a fresh revision even when stale content happens to match", () => {
    const saved = nextRecord(undefined, workspace, null);
    expect(() => nextRecord(saved, workspace, "stale")).toThrow(
      WorkspaceConflict
    );
  });
  it("refuses invalid writes and leaves the last good record intact", () => {
    const saved = nextRecord(undefined, workspace, null);
    const before = saved.payload;
    expect(() =>
      nextRecord(saved, { ...workspace, sources: [] }, saved.revision)
    ).toThrow();
    expect(saved.payload).toBe(before);
  });
  it("ignores transient save time but detects source edits and review decisions", () => {
    expect(workspaceContent({ ...workspace, savedAt: "later" })).toBe(
      workspaceContent(workspace)
    );
    expect(
      workspaceContent({ ...workspace, review: { fact: "exclude" } })
    ).not.toBe(workspaceContent(workspace));
    expect(workspaceContent({ ...workspace, editText: "different" })).not.toBe(
      workspaceContent(workspace)
    );
  });
  it("strips accidental top-level credential properties from persisted data", () => {
    const input = { ...workspace, apiKey: "synthetic-test-secret" };
    expect(nextRecord(undefined, input, null).payload).not.toContain(
      "synthetic-test-secret"
    );
  });
});

// Exercise request/transaction failures without changing the browser or real storage.
describe("browser storage failures", () => {
  it("retries opening after browser storage is denied", async () => {
    const { vi } = await import("vitest");
    const { WorkspaceStore } = await import("../src/workspace-store");
    let opens = 0;
    const open = () => {
      opens++;
      const request: Record<string, unknown> = {};
      queueMicrotask(() => {
        request.error = new DOMException("Storage denied", "SecurityError");
        (request.onerror as () => void)();
      });
      return request;
    };
    vi.stubGlobal("indexedDB", { open });
    try {
      const store = new WorkspaceStore("test");
      await expect(store.read()).rejects.toThrow("Storage denied");
      await expect(store.read()).rejects.toThrow("Storage denied");
      expect(opens).toBe(2);
    } finally {
      vi.unstubAllGlobals();
    }
  });
  it("preserves the committed record on quota failure and supports retry", async () => {
    const { vi } = await import("vitest");
    const { WorkspaceStore } = await import("../src/workspace-store");
    let committed = nextRecord(undefined, workspace, null);
    const original = committed;
    let full = true;
    const database = {
      transaction: () => {
        const tx: Record<string, any> = {};
        tx.abort = () => queueMicrotask(() => tx.onabort());
        tx.objectStore = () => ({
          get: () => {
            const request: Record<string, any> = {};
            queueMicrotask(() => {
              request.result = committed;
              request.onsuccess();
            });
            return request;
          },
          put: (value: typeof committed) => {
            if (full)
              throw new DOMException("Quota exceeded", "QuotaExceededError");
            committed = value;
            queueMicrotask(() => tx.oncomplete());
          },
        });
        return tx;
      },
    };
    vi.stubGlobal("indexedDB", {
      open: () => {
        const request: Record<string, any> = {};
        queueMicrotask(() => {
          request.result = database;
          request.onsuccess();
        });
        return request;
      },
    });
    try {
      const store = new WorkspaceStore("test");
      const changed = {
        ...workspace,
        editText: "Recovered after space is available",
      };
      await expect(store.save(changed, original.revision)).rejects.toThrow(
        "Quota exceeded"
      );
      expect(committed).toBe(original);
      full = false;
      const revision = await store.save(changed, original.revision);
      expect(revision).not.toBe(original.revision);
      expect(JSON.parse(committed.payload).editText).toBe(changed.editText);
    } finally {
      vi.unstubAllGlobals();
    }
  });
});