Files
galaxy-game/ui/frontend/tests/store-idb-cache.test.ts
Ilia Denisov ecd2bc9348 phase 6: web storage layer (KeyStore, Cache, session)
KeyStore + Cache TS interfaces with WebCrypto non-extractable Ed25519
keys persisted via IndexedDB (idb), plus thin api/session.ts that
loads or creates the device session at app startup. Vitest unit
tests under fake-indexeddb cover both adapters; Playwright e2e
verifies the keypair survives reload and produces signatures still
verifiable under the persisted public key (gateway round-trip moves
to Phase 7's existing acceptance bullet).

Browser baseline: WebCrypto Ed25519 — Chrome >=137, Firefox >=130,
Safari >=17.4. No JS fallback; ui/docs/storage.md documents the
matrix and the WebKit non-determinism quirk.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 14:08:09 +02:00

81 lines
2.6 KiB
TypeScript

// IDBCache unit tests under JSDOM with `fake-indexeddb` standing in
// for the browser's IndexedDB factory. Each case opens a freshly
// named database so state cannot leak across tests.
import "fake-indexeddb/auto";
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import type { IDBPDatabase } from "idb";
import { IDBCache } from "../src/platform/store/idb-cache";
import { openGalaxyDB, type GalaxyDB } from "../src/platform/store/idb";
let db: IDBPDatabase<GalaxyDB>;
let dbName: string;
beforeEach(async () => {
dbName = `galaxy-ui-test-${crypto.randomUUID()}`;
db = await openGalaxyDB(dbName);
});
afterEach(async () => {
db.close();
indexedDB.deleteDatabase(dbName);
});
describe("IDBCache", () => {
test("round-trips a typed object", async () => {
const cache = new IDBCache(db);
const value = {
name: "ping",
payload: new Uint8Array([1, 2, 3]),
nested: { count: 7 },
};
await cache.put("commands", "k1", value);
const out = await cache.get<typeof value>("commands", "k1");
expect(out?.name).toBe("ping");
expect(out?.nested.count).toBe(7);
expect(Array.from(out?.payload ?? [])).toEqual([1, 2, 3]);
});
test("namespaces are isolated", async () => {
const cache = new IDBCache(db);
await cache.put("a", "shared-key", "from-a");
await cache.put("b", "shared-key", "from-b");
expect(await cache.get("a", "shared-key")).toBe("from-a");
expect(await cache.get("b", "shared-key")).toBe("from-b");
});
test("delete removes a single row without touching neighbours", async () => {
const cache = new IDBCache(db);
await cache.put("ns", "k1", "v1");
await cache.put("ns", "k2", "v2");
await cache.delete("ns", "k1");
expect(await cache.get("ns", "k1")).toBeUndefined();
expect(await cache.get("ns", "k2")).toBe("v2");
});
test("clear(namespace) wipes only that namespace", async () => {
const cache = new IDBCache(db);
await cache.put("a", "k1", "a1");
await cache.put("a", "k2", "a2");
await cache.put("b", "k1", "b1");
await cache.clear("a");
expect(await cache.get("a", "k1")).toBeUndefined();
expect(await cache.get("a", "k2")).toBeUndefined();
expect(await cache.get("b", "k1")).toBe("b1");
});
test("clear() wipes every namespace", async () => {
const cache = new IDBCache(db);
await cache.put("a", "k1", "a1");
await cache.put("b", "k1", "b1");
await cache.clear();
expect(await cache.get("a", "k1")).toBeUndefined();
expect(await cache.get("b", "k1")).toBeUndefined();
});
test("get on a missing key returns undefined", async () => {
const cache = new IDBCache(db);
expect(await cache.get("absent", "k")).toBeUndefined();
});
});