ecd2bc9348
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>
81 lines
2.6 KiB
TypeScript
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();
|
|
});
|
|
});
|