Files
tech-log-frontend/tests/unit/indexeddb-runtime.test.ts
T

1141 lines
33 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { createIndexedDbRuntime } from "../../src/adapters/storage/indexeddb/indexeddb-runtime.ts";
import { indexedDbPhysicalDatabaseName } from "../../src/adapters/storage/indexeddb/indexeddb-governance.ts";
import type {
IndexedDbObservation,
IndexedDbRuntimeDependencies,
} from "../../src/adapters/storage/indexeddb/indexeddb-types.ts";
import { mapIndexedDbException } from "../../src/adapters/storage/indexeddb/indexeddb-failure.ts";
import { MemoryIndexedDbFactory } from "../helpers/memory-indexeddb.ts";
type Item = Readonly<{
label: string;
rank: number;
}>;
type Query = Readonly<{
limit: number;
}>;
const TEST_SCOPE = Object.freeze({
authorityToken: "authoritytoken_001",
namespaceToken: "namespacetoken_001",
partitionToken: "partitiontoken_001",
accountScope: "ORIGIN_SHARED" as const,
});
const TEST_STORAGE_POLICY = Object.freeze({
owner: "platform-storage",
namespace: "indexeddb-runtime-test",
classification: "INTERNAL" as const,
authority: "SERVER" as const,
accountScope: "ORIGIN_SHARED" as const,
retention: Object.freeze({ kind: "EXPLICIT_DELETE" as const }),
softBudgetBytes: 1_000_000,
hardBudgetBytes: 2_000_000,
evictionPriority: "SYNCED_COPY" as const,
logoutAction: "KEEP_ORIGIN_SHARED" as const,
accountDeletionAction: "KEEP_ORIGIN_SHARED" as const,
pressureAction: "RETAIN" as const,
unavailableFallback: "ONLINE_ONLY" as const,
});
async function sha256Fingerprint(value: unknown): Promise<string> {
const bytes = new TextEncoder().encode(JSON.stringify(value));
const digest = await globalThis.crypto.subtle.digest(
"SHA-256",
bytes,
);
return Array.from(
new Uint8Array(digest),
(byte) => byte.toString(16).padStart(2, "0"),
).join("");
}
function dependencies(
memory: MemoryIndexedDbFactory,
overrides: Partial<
IndexedDbRuntimeDependencies<Item, Item, Query>
> = {},
): IndexedDbRuntimeDependencies<Item, Item, Query> {
return {
scope: TEST_SCOPE,
storagePolicy: TEST_STORAGE_POLICY,
schemaVersion: 1,
recordStore: "records",
governanceStore: "governance",
retentionStore: "retention",
retentionEligibilityIndex: "by-eligibility",
lifecycleMetadataStores: [],
idempotencyStore: "receipts",
idempotencyExpiryIndex: "by-expiry",
receiptRetentionMs: 60_000,
maxIdempotencyReceipts: 1_000,
migrations: [
{
id: "schema-v1",
fromVersion: 0,
toVersion: 1,
operations: [
{
kind: "CREATE_STORE",
name: "records",
keyPath: "key",
indexes: [
{
name: "by-rank",
keyPath: "payload.rank",
},
],
},
{
kind: "CREATE_STORE",
name: "governance",
keyPath: "bindingKey",
},
{
kind: "CREATE_STORE",
name: "retention",
keyPath: "recordKey",
indexes: [
{
name: "by-eligibility",
keyPath: "eligibleAtEpochMs",
},
],
},
{
kind: "CREATE_STORE",
name: "receipts",
keyPath: "idempotencyKey",
indexes: [
{
name: "by-expiry",
keyPath: "expiresAtEpochMs",
},
],
},
],
},
],
codec: {
currentVersion: 1,
encode: (value) => ({ ok: true, value }),
measureStoredBytes: (value) =>
new TextEncoder().encode(JSON.stringify(value)).byteLength,
decode: (version, value) =>
version === 1 &&
value !== null &&
typeof value === "object" &&
typeof (value as Partial<Item>).label === "string" &&
typeof (value as Partial<Item>).rank === "number"
? { ok: true, value: value as Item }
: { ok: false },
fingerprint: (value) =>
sha256Fingerprint([value.rank, value.label]),
},
queryPolicy: {
plan: (query) => ({
index: "by-rank",
direction: "next",
limit: query.limit,
}),
},
factory: memory.factory,
nowEpochMilliseconds: () => 1_000,
authorizeLifecycle: () => ({
authorized: true,
proofToken: "authorityproof_001",
}),
...overrides,
};
}
async function waitForTransaction(
memory: MemoryIndexedDbFactory,
): Promise<void> {
for (let attempt = 0; attempt < 20; attempt += 1) {
if (memory.lastTransaction?.mode === "readwrite") return;
await new Promise<void>((resolve) => {
globalThis.setTimeout(resolve, 0);
});
}
throw new Error("Timed out waiting for a fake readwrite transaction.");
}
describe("IndexedDB runtime", () => {
it("migrates, applies CAS atomically, replays idempotency keys, and pages by policy", async () => {
const memory = new MemoryIndexedDbFactory();
const observations: IndexedDbObservation[] = [];
const statuses: string[] = [];
const runtime = createIndexedDbRuntime(
dependencies(memory, {
observe: (event) => observations.push(event),
}),
);
runtime.subscribeStatus((status) => statuses.push(status.kind));
expect(await runtime.open()).toEqual({ ok: true, value: undefined });
expect(runtime.getStatus()).toEqual({
kind: "READY",
schemaVersion: 1,
});
const first = {
key: "record-alpha",
value: { label: "alpha", rank: 2 },
expectedRevision: null,
idempotencyKey: "put-alpha-v1",
} as const;
expect(await runtime.compareAndSwap(first)).toEqual({
ok: true,
value: {
key: "record-alpha",
revision: 1,
replayed: false,
},
});
const storedFingerprint = (
memory.readRaw(
"receipts",
"put-alpha-v1",
) as Readonly<{ fingerprint: string }>
).fingerprint;
expect(storedFingerprint).toMatch(/^[a-f0-9]{64}$/u);
expect(storedFingerprint).not.toContain("alpha");
expect(await runtime.compareAndSwap(first)).toEqual({
ok: true,
value: {
key: "record-alpha",
revision: 1,
replayed: true,
},
});
expect(
await runtime.compareAndSwap({
...first,
value: { label: "changed", rank: 2 },
}),
).toMatchObject({
ok: false,
error: { code: "CONFLICT" },
});
expect(
await runtime.compareAndSwap({
key: "record-beta",
value: { label: "beta", rank: 1 },
expectedRevision: null,
idempotencyKey: "put-beta-v1",
}),
).toMatchObject({
ok: true,
value: { revision: 1, replayed: false },
});
expect(
await runtime.compareAndSwap({
key: "record-alpha",
value: { label: "alpha-v2", rank: 3 },
expectedRevision: 1,
idempotencyKey: "put-alpha-v2",
}),
).toMatchObject({
ok: true,
value: { revision: 2, replayed: false },
});
expect(
await runtime.compareAndSwap({
key: "record-alpha",
value: { label: "stale", rank: 4 },
expectedRevision: 1,
idempotencyKey: "stale-alpha",
}),
).toMatchObject({
ok: false,
error: { code: "CONFLICT" },
});
expect(await runtime.read("record-alpha")).toEqual({
ok: true,
value: {
value: { label: "alpha-v2", rank: 3 },
revision: 2,
},
});
const firstPage = await runtime.query({ limit: 1 });
expect(firstPage).toMatchObject({
ok: true,
value: {
items: [{ label: "beta", rank: 1 }],
},
});
expect(firstPage.ok && firstPage.value.nextCursor).not.toBeNull();
const secondPage = await runtime.query(
{ limit: 1 },
firstPage.ok ? firstPage.value.nextCursor : null,
);
expect(secondPage).toEqual({
ok: true,
value: {
items: [{ label: "alpha-v2", rank: 3 }],
nextCursor: null,
},
});
const deletion = {
key: "record-alpha",
expectedRevision: 2,
idempotencyKey: "delete-alpha-v2",
} as const;
expect(await runtime.remove(deletion)).toMatchObject({
ok: true,
value: { revision: 3, replayed: false },
});
expect(await runtime.remove(deletion)).toMatchObject({
ok: true,
value: { revision: 3, replayed: true },
});
expect(await runtime.read("record-alpha")).toEqual({
ok: true,
value: null,
});
expect(statuses).toEqual(["OPENING", "READY"]);
expect(JSON.stringify(observations)).not.toMatch(
/record-alpha|record-beta|alpha-v2|put-alpha/,
);
expect(observations).toContainEqual({
operation: "INDEXEDDB_MIGRATE",
outcome: "SUCCESS",
schemaVersion: 1,
countBucket: "1",
});
runtime.close();
expect(runtime.getStatus()).toEqual({ kind: "DISPOSED" });
});
it("waits for transaction complete and rolls request successes back on commit failure", async () => {
const memory = new MemoryIndexedDbFactory();
const runtime = createIndexedDbRuntime(dependencies(memory));
expect(await runtime.open()).toMatchObject({ ok: true });
memory.failNextWriteCommit(
new DOMException("Do not expose this value.", "QuotaExceededError"),
);
expect(
await runtime.compareAndSwap({
key: "rollback-target",
value: { label: "never-committed", rank: 1 },
expectedRevision: null,
idempotencyKey: "rollback-operation",
}),
).toEqual({
ok: false,
error: {
code: "QUOTA_EXCEEDED",
operation: "INDEXEDDB_WRITE",
retryable: false,
recovery: "READ_ONLY",
},
});
expect(await runtime.read("rollback-target")).toEqual({
ok: true,
value: null,
});
});
it("aborts the whole in-flight transaction and cannot commit a late request", async () => {
const memory = new MemoryIndexedDbFactory();
const runtime = createIndexedDbRuntime(dependencies(memory));
expect(await runtime.open()).toMatchObject({ ok: true });
memory.pauseTransactions();
const controller = new AbortController();
const pending = runtime.compareAndSwap({
key: "abort-target",
value: { label: "late", rank: 1 },
expectedRevision: null,
idempotencyKey: "abort-operation",
signal: controller.signal,
});
await waitForTransaction(memory);
controller.abort();
expect(await pending).toEqual({
ok: false,
error: {
code: "ABORTED",
operation: "INDEXEDDB_WRITE",
retryable: false,
recovery: "NONE",
},
});
memory.resumeTransactions();
await Promise.resolve();
expect(await runtime.read("abort-target")).toEqual({
ok: true,
value: null,
});
});
it("shares one native open request across concurrent callers", async () => {
const memory = new MemoryIndexedDbFactory();
const nativeOpen = vi.spyOn(memory.factory, "open");
const runtime = createIndexedDbRuntime(dependencies(memory));
const results = await Promise.all([
runtime.open(),
runtime.open(),
runtime.open(),
]);
expect(nativeOpen).toHaveBeenCalledOnce();
expect(results).toEqual([
{ ok: true, value: undefined },
{ ok: true, value: undefined },
{ ok: true, value: undefined },
]);
});
it("isolates a caller abort from the shared native open request", async () => {
const memory = new MemoryIndexedDbFactory();
memory.blockNextOpen();
const nativeOpen = vi.spyOn(memory.factory, "open");
const runtime = createIndexedDbRuntime(dependencies(memory));
const cancelledCaller = new AbortController();
const surviving = runtime.open();
const cancelled = runtime.open(cancelledCaller.signal);
await Promise.resolve();
cancelledCaller.abort();
expect(await cancelled).toMatchObject({
ok: false,
error: { code: "ABORTED", operation: "INDEXEDDB_OPEN" },
});
expect(nativeOpen).toHaveBeenCalledOnce();
memory.releaseBlockedOpen();
expect(await surviving).toEqual({ ok: true, value: undefined });
expect(runtime.getStatus()).toEqual({ kind: "READY", schemaVersion: 1 });
});
it("times out a blocked upgrade, then closes a late successful connection", async () => {
const memory = new MemoryIndexedDbFactory();
memory.blockNextOpen();
let timeout: (() => void) | undefined;
const runtime = createIndexedDbRuntime(
dependencies(memory, {
blockedTimeoutMs: 25,
scheduler: {
setTimeout: (callback) => {
timeout = callback;
return "blocked-timer";
},
clearTimeout: () => undefined,
},
}),
);
const pending = runtime.open();
await Promise.resolve();
expect(runtime.getStatus()).toEqual({
kind: "BLOCKED",
currentVersion: 0,
targetVersion: 1,
});
timeout?.();
expect(await pending).toMatchObject({
ok: false,
error: {
code: "BLOCKED",
recovery: "RELOAD_OTHER_CONTEXTS",
},
});
memory.releaseBlockedOpen();
await Promise.resolve();
expect(memory.isConnectionClosed()).toBe(true);
expect(runtime.getStatus()).toEqual({
kind: "CLOSED",
reason: "NOT_OPENED",
});
});
it("retains native open ownership after a blocked timeout until late success", async () => {
const memory = new MemoryIndexedDbFactory();
memory.blockNextOpen();
const nativeOpen = vi.spyOn(memory.factory, "open");
let timeout: (() => void) | undefined;
const runtime = createIndexedDbRuntime(
dependencies(memory, {
blockedTimeoutMs: 25,
scheduler: {
setTimeout: (callback) => {
timeout = callback;
return "blocked-timer";
},
clearTimeout: () => undefined,
},
}),
);
const first = runtime.open();
await Promise.resolve();
timeout?.();
await expect(first).resolves.toMatchObject({
ok: false,
error: { code: "BLOCKED" },
});
const second = runtime.open();
await Promise.resolve();
const nativeOpenCountBeforeLateSuccess = nativeOpen.mock.calls.length;
memory.releaseBlockedOpen();
await expect(second).resolves.toMatchObject({
ok: false,
error: { code: "BLOCKED" },
});
await Promise.resolve();
expect(nativeOpenCountBeforeLateSuccess).toBe(1);
expect(memory.isConnectionClosed()).toBe(true);
expect(runtime.getStatus()).toEqual({
kind: "CLOSED",
reason: "NOT_OPENED",
});
});
it("closes immediately on versionchange and isolates listener failures", async () => {
const memory = new MemoryIndexedDbFactory();
const onVersionChange = vi.fn(() => {
throw new Error("listener failure");
});
const runtime = createIndexedDbRuntime(
dependencies(memory, { onVersionChange }),
);
runtime.subscribeStatus(() => {
throw new Error("subscriber failure");
});
expect(await runtime.open()).toMatchObject({ ok: true });
memory.triggerVersionChange(2);
expect(runtime.getStatus()).toEqual({
kind: "CLOSED",
reason: "VERSION_CHANGE",
});
expect(memory.isConnectionClosed()).toBe(true);
expect(onVersionChange).toHaveBeenCalledOnce();
expect(await runtime.read("after-versionchange")).toMatchObject({
ok: false,
error: { code: "UNAVAILABLE", recovery: "REOPEN" },
});
});
it("uses a closed, cross-realm-safe DOMException mapping", () => {
expect(
mapIndexedDbException(
{ name: "QuotaExceededError", message: "private-record-key" },
"INDEXEDDB_WRITE",
),
).toEqual({
ok: false,
error: {
code: "QUOTA_EXCEEDED",
operation: "INDEXEDDB_WRITE",
retryable: false,
recovery: "READ_ONLY",
},
});
expect(
JSON.stringify(
mapIndexedDbException(
{ name: "SomethingNew", message: "private-record-key" },
"INDEXEDDB_READ",
),
),
).not.toContain("private-record-key");
});
it("replays only inside the configured receipt window and expires safely", async () => {
const memory = new MemoryIndexedDbFactory();
let currentTime = 1_000;
const runtime = createIndexedDbRuntime(
dependencies(memory, {
receiptRetentionMs: 100,
nowEpochMilliseconds: () => currentTime,
}),
);
expect(await runtime.open()).toMatchObject({ ok: true });
const operation = {
key: "retention-record",
value: { label: "retained", rank: 1 },
expectedRevision: null,
idempotencyKey: "retention-operation",
} as const;
expect(await runtime.compareAndSwap(operation)).toMatchObject({
ok: true,
value: { replayed: false },
});
expect(
memory.readRaw("receipts", "retention-operation"),
).toMatchObject({ expiresAtEpochMs: 1_100 });
currentTime = 1_099;
expect(await runtime.compareAndSwap(operation)).toMatchObject({
ok: true,
value: { replayed: true },
});
currentTime = 1_100;
expect(await runtime.compareAndSwap(operation)).toMatchObject({
ok: false,
error: { code: "CONFLICT" },
});
expect(
memory.readRaw("receipts", "retention-operation"),
).toBeDefined();
expect(await runtime.read("retention-record")).toMatchObject({
ok: true,
value: { revision: 1 },
});
});
it("rejects non-digest fingerprints before storing PII in a receipt", async () => {
const memory = new MemoryIndexedDbFactory();
const base = dependencies(memory);
const runtime = createIndexedDbRuntime({
...base,
codec: {
...base.codec,
fingerprint: () => "private-user-label",
},
});
expect(await runtime.open()).toMatchObject({ ok: true });
expect(
await runtime.compareAndSwap({
key: "private-key",
value: { label: "private-user-label", rank: 1 },
expectedRevision: null,
idempotencyKey: "private-operation",
}),
).toMatchObject({
ok: false,
error: { code: "INVALID_INPUT" },
});
expect(
memory.readRaw("receipts", "private-operation"),
).toBeUndefined();
});
it("rejects adversarial cursor/range keys before native conversion", async () => {
const memory = new MemoryIndexedDbFactory();
let rangeKey: unknown = "safe";
const runtime = createIndexedDbRuntime(
dependencies(memory, {
queryPolicy: {
plan: () => ({
limit: 1,
range: {
kind: "ONLY",
value: rangeKey,
} as never,
}),
},
}),
);
expect(await runtime.open()).toMatchObject({ ok: true });
rangeKey = new ArrayBuffer(4_097);
expect(await runtime.query({ limit: 1 })).toMatchObject({
ok: false,
error: { code: "INVALID_INPUT" },
});
rangeKey = Array.from({ length: 65 }, (_, index) => index);
expect(await runtime.query({ limit: 1 })).toMatchObject({
ok: false,
error: { code: "INVALID_INPUT" },
});
let nested: unknown = "leaf";
for (let depth = 0; depth < 10; depth += 1) {
nested = [nested];
}
expect(
await runtime.query(
{ limit: 1 },
{
indexKey: nested,
primaryKey: "primary",
} as never,
),
).toMatchObject({
ok: false,
error: { code: "INVALID_INPUT" },
});
});
it("rejects destructive DDL in the additive-only migration planner", () => {
const memory = new MemoryIndexedDbFactory();
const base = dependencies(memory);
expect(() =>
createIndexedDbRuntime({
...base,
migrations: [
{
id: "destructive-v1",
fromVersion: 0,
toVersion: 1,
operations: [
{
kind: "DELETE_STORE",
name: "records",
} as never,
],
},
],
}),
).toThrowError(
expect.objectContaining({ name: "InvalidStateError" }),
);
});
it("fails closed when the migrated schema omits a required runtime store", async () => {
const memory = new MemoryIndexedDbFactory();
const base = dependencies(memory);
const runtime = createIndexedDbRuntime({
...base,
migrations: [
{
id: "incomplete-v1",
fromVersion: 0,
toVersion: 1,
operations: [
{
kind: "CREATE_STORE",
name: "records",
keyPath: "key",
},
],
},
],
});
expect(await runtime.open()).toEqual({
ok: false,
error: {
code: "MIGRATION_FAILED",
operation: "INDEXEDDB_MIGRATE",
retryable: false,
recovery: "READ_ONLY",
},
});
expect(memory.isConnectionClosed()).toBe(true);
});
it("derives physical identity only from opaque scope and rejects an override", () => {
const memory = new MemoryIndexedDbFactory();
const physicalName = indexedDbPhysicalDatabaseName(TEST_SCOPE);
expect(physicalName).toBe(
"ca-idb-v1:authoritytoken_001.namespacetoken_001.partitiontoken_001",
);
expect(physicalName).not.toContain(
TEST_STORAGE_POLICY.namespace,
);
expect(() =>
createIndexedDbRuntime(
dependencies(memory, {
databaseNameAssertion: "customer-account-42",
}),
),
).toThrow(TypeError);
});
it("fails closed when the same physical scope is rebound to another policy", async () => {
const memory = new MemoryIndexedDbFactory();
const first = createIndexedDbRuntime(dependencies(memory));
expect(await first.open()).toMatchObject({ ok: true });
first.close();
const rebound = createIndexedDbRuntime(
dependencies(memory, {
storagePolicy: {
...TEST_STORAGE_POLICY,
namespace: "different-dataset-policy",
},
}),
);
expect(await rebound.open()).toMatchObject({
ok: false,
error: {
code: "POLICY_REJECTED",
operation: "INDEXEDDB_OPEN",
},
});
});
it("snapshots governance and enforces TTL logically before bounded deletion", async () => {
const memory = new MemoryIndexedDbFactory();
let epochMs = 1_000;
const authorizeLifecycle = vi.fn((_request: unknown) => ({
authorized: true as const,
proofToken: "ttl_authority_proof_001",
}));
const mutableScope = structuredClone(TEST_SCOPE);
const mutablePolicy = {
...structuredClone(TEST_STORAGE_POLICY),
retention: { kind: "TTL" as const, maxAgeMs: 10 },
};
const mutablePlan = {
index: "by-rank",
direction: "next" as const,
limit: 10,
};
const observations: IndexedDbObservation[] = [];
const mutableConfiguration = dependencies(memory, {
scope: mutableScope,
storagePolicy: mutablePolicy,
keyRange: memory.keyRange,
nowEpochMilliseconds: () => epochMs,
nowMonotonicMilliseconds: () => 0,
authorizeLifecycle,
observe: (event) => observations.push(event),
queryPolicy: { plan: () => mutablePlan },
});
const runtime = createIndexedDbRuntime(mutableConfiguration);
(
mutableScope as unknown as { authorityToken: string }
).authorityToken = "mutated_business_id";
(
mutablePolicy as unknown as {
namespace: string;
retention: { maxAgeMs: number };
}
).namespace = "mutated-policy";
(
mutablePolicy as unknown as {
retention: { maxAgeMs: number };
}
).retention.maxAgeMs = 100_000;
(
mutableConfiguration as unknown as {
recordStore: string;
schemaVersion: number;
maxIdempotencyReceipts: number;
}
).recordStore = "mutated-record-store";
(
mutableConfiguration as unknown as {
schemaVersion: number;
}
).schemaVersion = 99;
(
mutableConfiguration as unknown as {
maxIdempotencyReceipts: number;
}
).maxIdempotencyReceipts = 0;
(
mutableConfiguration.codec as unknown as {
measureStoredBytes: () => number;
}
).measureStoredBytes = () => Number.NaN;
expect(await runtime.open()).toMatchObject({ ok: true });
expect(
await runtime.compareAndSwap({
key: "ttl-record",
value: { label: "expires", rank: 1 },
expectedRevision: null,
idempotencyKey: "ttl-create",
}),
).toMatchObject({ ok: true });
const pendingQuery = runtime.query({ limit: 10 });
mutablePlan.limit = 0;
(
mutablePlan as unknown as { index: string }
).index = "mutated-index";
expect(await pendingQuery).toMatchObject({
ok: true,
value: { items: [{ label: "expires", rank: 1 }] },
});
mutablePlan.limit = 10;
mutablePlan.index = "by-rank";
epochMs = 1_011;
expect(await runtime.read("ttl-record")).toMatchObject({
ok: false,
error: { code: "EXPIRED_RESOURCE" },
});
expect(await runtime.query({ limit: 10 })).toEqual({
ok: true,
value: { items: [], nextCursor: null },
});
expect(
await runtime.enforceLifecycleBatch({
action: "RETENTION_SWEEP",
maxRows: 10,
maxDurationMs: 1_000,
}),
).toMatchObject({
ok: true,
value: { state: "COMPLETE", deletedRows: 1 },
});
expect(authorizeLifecycle).toHaveBeenCalledOnce();
expect(authorizeLifecycle.mock.calls[0]?.[0]).toMatchObject({
scope: TEST_SCOPE,
storagePolicy: {
namespace: TEST_STORAGE_POLICY.namespace,
retention: { kind: "TTL", maxAgeMs: 10 },
},
});
expect(
JSON.stringify([
memory.readRaw("governance", "dataset-binding"),
memory.readRaw("governance", "dataset-budget"),
memory.readRaw("receipts", "ttl-create"),
observations,
]),
).not.toContain("ttl_authority_proof_001");
});
it("requires lifecycle authority and only deletes sync-confirmed rows", async () => {
const memory = new MemoryIndexedDbFactory();
const authorizeLifecycle = vi
.fn()
.mockReturnValueOnce({ authorized: false })
.mockReturnValue({
authorized: true,
proofToken: "sync_authority_proof_001",
});
const runtime = createIndexedDbRuntime(
dependencies(memory, {
scope: {
...TEST_SCOPE,
accountScope: "OPAQUE_PARTITION",
},
storagePolicy: {
...TEST_STORAGE_POLICY,
accountScope: "OPAQUE_PARTITION",
retention: { kind: "UNTIL_SYNCED" },
logoutAction: "EXPORT_THEN_PURGE",
accountDeletionAction: "PURGE_PARTITION",
},
keyRange:
memory.keyRange as NonNullable<
IndexedDbRuntimeDependencies<Item, Item, Query>["keyRange"]
>,
authorizeLifecycle,
nowMonotonicMilliseconds: () => 0,
}),
);
expect(await runtime.open()).toMatchObject({ ok: true });
expect(
await runtime.compareAndSwap({
key: "sync-record",
value: { label: "pending", rank: 1 },
expectedRevision: null,
idempotencyKey: "sync-pending",
synchronization: "PENDING",
}),
).toMatchObject({ ok: true });
expect(
await runtime.enforceLifecycleBatch({
action: "RETENTION_SWEEP",
maxRows: 10,
maxDurationMs: 1_000,
}),
).toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
expect(memory.readRaw("records", "sync-record")).toBeDefined();
expect(
await runtime.compareAndSwap({
key: "sync-record",
value: { label: "confirmed", rank: 1 },
expectedRevision: 1,
idempotencyKey: "sync-confirmed",
synchronization: "CONFIRMED",
}),
).toMatchObject({ ok: true });
expect(
await runtime.enforceLifecycleBatch({
action: "RETENTION_SWEEP",
maxRows: 10,
maxDurationMs: 1_000,
}),
).toMatchObject({
ok: true,
value: { deletedRows: 1 },
});
});
it("purges records, orphan metadata, receipts, and counters in bounded lifecycle batches", async () => {
const memory = new MemoryIndexedDbFactory();
const base = dependencies(memory);
const authorizeLifecycle = vi.fn(() => ({
authorized: true as const,
proofToken: "session_authority_proof_001",
}));
const runtime = createIndexedDbRuntime({
...base,
storagePolicy: {
...TEST_STORAGE_POLICY,
retention: { kind: "SESSION" },
},
lifecycleMetadataStores: ["lifecycle-metadata"],
migrations: [
{
...base.migrations[0]!,
operations: [
...base.migrations[0]!.operations,
{
kind: "CREATE_STORE",
name: "lifecycle-metadata",
keyPath: "metadataKey",
},
],
},
],
authorizeLifecycle,
nowMonotonicMilliseconds: () => 0,
});
expect(await runtime.open()).toMatchObject({ ok: true });
memory.seed("lifecycle-metadata", {
metadataKey: "migration-checkpoint",
lastRecordKey: "session-record",
});
expect(
await runtime.compareAndSwap({
key: "session-record",
value: { label: "session", rank: 1 },
expectedRevision: null,
idempotencyKey: "session-create",
}),
).toMatchObject({ ok: true });
const receipts = [];
for (let batch = 0; batch < 5; batch += 1) {
const receipt = await runtime.enforceLifecycleBatch({
action: "SESSION_END",
maxRows: 1,
maxDurationMs: 1_000,
});
receipts.push(receipt);
if (receipt.ok && receipt.value.state === "COMPLETE") break;
}
expect(receipts).toHaveLength(3);
expect(receipts.at(-1)).toMatchObject({
ok: true,
value: { state: "COMPLETE", deletedRows: 1 },
});
expect(authorizeLifecycle).toHaveBeenCalledTimes(3);
expect(memory.readRaw("records", "session-record")).toBeUndefined();
expect(memory.readRaw("retention", "session-record")).toBeUndefined();
expect(memory.readRaw("receipts", "session-create")).toBeUndefined();
expect(
memory.readRaw(
"lifecycle-metadata",
"migration-checkpoint",
),
).toBeUndefined();
expect(
memory.readRaw("governance", "dataset-budget"),
).toMatchObject({ usedBytes: 0, receiptCount: 0 });
});
it("rejects invalid measurements, byte overage, and receipt-count overage atomically", async () => {
const invalidMemory = new MemoryIndexedDbFactory();
const invalidBase = dependencies(invalidMemory);
const invalid = createIndexedDbRuntime({
...invalidBase,
codec: {
...invalidBase.codec,
measureStoredBytes: () => Number.NaN,
},
});
expect(await invalid.open()).toMatchObject({ ok: true });
expect(
await invalid.compareAndSwap({
key: "invalid-measure",
value: { label: "invalid", rank: 1 },
expectedRevision: null,
idempotencyKey: "invalid-measure-create",
}),
).toMatchObject({
ok: false,
error: { code: "INVALID_INPUT" },
});
const boundedMemory = new MemoryIndexedDbFactory();
const boundedBase = dependencies(boundedMemory);
const bounded = createIndexedDbRuntime({
...boundedBase,
maxIdempotencyReceipts: 1,
storagePolicy: {
...TEST_STORAGE_POLICY,
softBudgetBytes: 500,
hardBudgetBytes: 620,
},
codec: {
...boundedBase.codec,
measureStoredBytes: () => 10,
},
});
expect(await bounded.open()).toMatchObject({ ok: true });
expect(
await bounded.compareAndSwap({
key: "a",
value: { label: "fits", rank: 1 },
expectedRevision: null,
idempotencyKey: "first-receipt",
}),
).toMatchObject({ ok: true });
expect(
await bounded.compareAndSwap({
key: "b",
value: { label: "receipt-cap", rank: 2 },
expectedRevision: null,
idempotencyKey: "second-receipt",
}),
).toMatchObject({
ok: false,
error: { code: "LIMIT_EXCEEDED" },
});
expect(boundedMemory.readRaw("records", "b")).toBeUndefined();
const hardMemory = new MemoryIndexedDbFactory();
const hardBase = dependencies(hardMemory);
const hardBounded = createIndexedDbRuntime({
...hardBase,
storagePolicy: {
...TEST_STORAGE_POLICY,
softBudgetBytes: 500,
hardBudgetBytes: 520,
},
codec: {
...hardBase.codec,
measureStoredBytes: () => 10,
},
});
expect(await hardBounded.open()).toMatchObject({ ok: true });
expect(
await hardBounded.compareAndSwap({
key: "x",
value: { label: "too-large", rank: 3 },
expectedRevision: null,
idempotencyKey: "hard-limit-receipt",
}),
).toMatchObject({
ok: false,
error: { code: "LIMIT_EXCEEDED" },
});
expect(hardMemory.readRaw("records", "x")).toBeUndefined();
});
});