Skip to main content

Testing

import * as mod from "jsr:@nzip/lofi/testing";

The @nzip/lofi/testing public surface: Playwright-backed helpers for testing local-first behavior, including two-client fixtures, concurrent offline convergence, app-owned readiness waits, value-free failure artifacts, and a CDP virtual authenticator for headless WebAuthn flows.

The supported Playwright release is 1.61 — the playwright alias in the workspace import map. Install a matching Chromium with deno run -A npm:[email protected] install chromium.

The scenario surface (scenario, converge, and the row assertions) turns sync simulation testing into plain test declarations: named peers make concurrent and offline edits through the app's own schema, and the test asserts they converge. Headless scenarios drive real synced clients against a local sync server and need FFI permissions; browser scenarios drive the two-client Playwright fixture against a served app; scenario.fuzz runs seeded, replayable operation sequences for property-style coverage.

Alongside the browser fixtures, this entry re-exports the deterministic seams of the schema and runtime registries — key installation, registry clears, and fake runtime installation — so a test can isolate schema declarations, encrypted and shared-field key state, write handles, and verb dispatch without booting a runtime. Production code never calls these: the runtime owns each seam at boot, and calling one in an application corrupts the state the runtime maintains.

assertNoRow​

function

async function assertNoRow(peer: ScenarioPeer<unknown>, table: ScenarioTableRef<unknown>, id: string): Promise<void>

Assert that a peer's local view does not contain the row.

assertRow​

function

async function assertRow<Row>(peer: ScenarioPeer<unknown>, table: ScenarioTableRef<Row>, id: string, partial?: Partial<Row>): Promise<void>

Assert that a peer's local view contains the row, optionally matching a subset of its columns.

assertRowCount​

function

async function assertRowCount(peer: ScenarioPeer<unknown>, table: ScenarioTableRef<unknown>, expected: number): Promise<void>

Assert the number of rows a peer's local view of a table contains.

assertValueFreeState​

function

function assertValueFreeState(value: unknown, path: string): asserts value is ValueFreeState

Assert that value satisfies the value-free rule: strings and non-finite numbers are rejected, so state artifacts record shape, counts, and booleans but never user values or credentials. Snapshot callbacks passed to the fixtures are validated with this at capture time; call it directly to pre-validate a snapshot callback's output in a fast test.

ParameterDescription
valueThe candidate snapshot value.
pathThe label used for the offending location in error messages.

BrowserTestClient​

class

class BrowserTestClient {
constructor(name: ClientName, context: BrowserContext, page: Page, baseURL: string, createContext: (state?: MemoryStorageState) => Promise<BrowserContext>, redact: (value: string) => string, traceOnFailure: boolean);
get context(): BrowserContext;
get page(): Page;
get offline(): boolean;
get diagnostics(): readonly BrowserDiagnostic[];
startRecording(): Promise<void>;
goOffline(): Promise<void>;
goOnline(): Promise<void>;
reloadPage(): Promise<Page>;
restartPage(): Promise<Page>;
restartClient(options: { preserveIdentity?: boolean }): Promise<Page>;
captureTrace(path: string): Promise<boolean>;
close(): Promise<void>;
}

One browser client (context + page) whose identity and IndexedDB stay in memory. Records redacted diagnostics and can go offline/online, restart, and capture a sanitized trace.

BrowserUnavailableError​

class

class BrowserUnavailableError extends Error {
constructor(options?: ErrorOptions);
readonly name: string;
}

Thrown when Playwright's Chromium browser is not installed, with install guidance.

clearEffectDeclarations​

function

function clearEffectDeclarations(): void

Clears every declaration; deterministic-test seam.

clearEncryptedColumnKey​

function

function clearEncryptedColumnKey(): void

Forgets the installed key; encrypted columns fail closed afterwards.

clearEncryptedColumnRegistry​

function

function clearEncryptedColumnRegistry(): void

Empties the encrypted-column registry; tests call this between schemas.

clearFingerprintPins​

function

function clearFingerprintPins(appId: string): void

Forgets every pin for an app; tests call this between scenarios.

clearSharedColumnRegistry​

function

function clearSharedColumnRegistry(): void

Empties the registry; tests call this between schemas.

clearSharedFieldKeys​

function

function clearSharedFieldKeys(): void

Empties every installed field key; tests and logout call this.

converge​

function

async function converge<A>(...args: readonly ((ScenarioPeer<A> | ScenarioPeerControls | ConvergeOptions<A>))[]): Promise<void>

Assert that all peers converge to identical views. Settles every peer's issued writes, then polls until every peer reads the same state, or fails at the deadline with a per-peer diff. All peers must be online. Headless peers compare their tables row by row; browser peers compare their app-supplied value-free snapshots. Accepts an options object after the peers: converge(alice, bob, { timeoutMs: 30_000 }).

ConvergenceScenarioError​

class

class ConvergenceScenarioError extends Error {
constructor(stage: ConvergenceStage, options?: ConvergenceScenarioErrorOptions);
readonly name: string;
readonly captureError?: unknown;
}

Thrown when a convergence scenario fails, naming the stage that failed via stage.

createTwoClientFixture​

function

async function createTwoClientFixture(options: TwoClientFixtureOptions): Promise<TwoClientFixture>

Launch (or reuse) a browser and build a TwoClientFixture: opens both clients at baseURL, applies the chosen identity mode with state kept in memory, and starts recording. Cleans up on any setup failure.

ParameterDescription
optionsThe base URL, identity mode, and optional browser, context, and artifact settings.

Returns: A ready fixture whose two clients are open at baseURL with identity applied.

Example

const fixture = await createTwoClientFixture({
baseURL,
identity: { mode: "shared", preparePrimary: (client) => ready(client) },
artifacts: { directory: "test-results" },
});
try {
await fixture.goOffline();
// ...apply edits on fixture.first and fixture.second, then converge...
} finally {
await fixture.close();
}

createWriteHandle​

function

function createWriteHandle<T>(writeId: string): { handle: WriteHandle<T>; controller: WriteHandleController<T> }

Creates a handle in saving together with its controller — the only path to a handle's lifecycle mutators. The ledger keeps the controller and hands the observe-only handle to application code.

generateFuzzPlan​

function

function generateFuzzPlan(input: FuzzPlanInput): FuzzPlan

Generate a fuzz plan: a deterministic, seed-replayable sequence of inserts, updates, removes, offline windows, and sync barriers across the peers. Updates and removes only target rows their peer has locally created or has observed through a sync barrier; offline windows are bounded; the plan ends with every peer back online.

getSharedFieldKey​

function

function getSharedFieldKey(scope: string, generation: number): Uint8Array | null

The installed key for a scope and generation, or null while pending.

installSharedFieldKey​

function

function installSharedFieldKey(scope: string, generation: number, key: Uint8Array): void

Installs an unwrapped field key and notifies keyring subscribers.

latestSharedFieldGeneration​

function

function latestSharedFieldGeneration(scope: string): number | null

The newest generation installed for a scope, or null when none is.

memoryPopKeyStore​

function

function memoryPopKeyStore(): PopKeyStore

A process-lifetime store for tests and non-browser environments.

ReadinessError​

class

class ReadinessError extends Error {
constructor(description: string, options?: ErrorOptions);
readonly name: string;
}

Thrown when the readiness predicate does not become true before the timeout.

redactDiagnosticText​

function

function redactDiagnosticText(value: string, secretValues: readonly string[]): string

Redact common credential forms from diagnostic text: URL userinfo, query, and fragment components; JSON and key=value secret assignments; bearer tokens; and every literal in secretValues. The fixtures apply this to all retained diagnostics; apply it likewise to any custom capture path.

ParameterDescription
valueThe diagnostic text to redact.
secretValuesLiteral credential values to remove wherever they appear.

Returns: The text with credential forms replaced by [redacted] markers.

runConcurrentOfflineConvergence​

function

async function runConcurrentOfflineConvergence<Edit, Client extends OfflineTestClient>(fixture: OfflineTestFixture<Client>, scenario: ConcurrentOfflineScenario<Edit, Client>): Promise<void>

Coordinates the transport lifecycle while the app owns edits, assertions and conflict semantics. Both offline edits are started in the same microtask.

scenario​

const

const scenario: ScenarioApi;

Declare simulation-test scenarios: named peers make concurrent and offline edits through the app's own schema, and the test asserts they converge.

scenario("offline rename vs remote delete", { app, permissions }, async ({ alice, bob }) => {
const doc = await alice.db.documents.insert({ title: "Untitled" });
await converge(alice, bob);

await alice.offline();
await alice.db.documents.update(doc.id, { title: "Draft" });
await bob.db.documents.remove(doc.id);
await alice.online();
await alice.settle();

await alice.restart(); // the live writer keeps its doomed rename until it restarts
await converge(alice, bob);
await assertNoRow(alice, app.documents, doc.id);
});

ScenarioError​

class

class ScenarioError extends Error {
constructor(stage: ScenarioStage, message: string, options?: ScenarioErrorOptions);
readonly name: string;
readonly peer?: string;
readonly details?: string;
}

Thrown when a scenario fails, naming the lifecycle stage that failed via stage.

setEncryptedColumnKey​

function

function setEncryptedColumnKey(key: Uint8Array): void

Installs the account-derived 32-byte master key for encrypted columns. The runtime calls this at boot (before creating the database client) and again whenever the account secret changes; tests inject a fixed key.

setMutationRuntime​

function

function setMutationRuntime(runtime: MutationRuntime): void

Installs the runtime half verbs dispatch through. The package runtime installs it at boot; application code never calls this. Tests install a deterministic fake to unit-test verbs without a booted runtime.

sharedColumnConfigs​

function

function sharedColumnConfigs(): SharedColumnConfig[]

Every registered shared-column configuration.

sharedKeyScope​

function

function sharedKeyScope(groupTable: string, groupId: string): string

The keyring scope of a group resource: "groupTable/groupId".

subscribeSharedKeyring​

function

function subscribeSharedKeyring(listener: () => void): () => void

Subscribes to keyring changes. Live-query stores resubscribe on change so rows previously surfaced as pending re-materialize into plaintext.

TwoClientFixture​

class

class TwoClientFixture {
constructor(browser: Browser, ownsBrowser: boolean, first: BrowserTestClient, second: BrowserTestClient, artifacts: FailureArtifactOptions | undefined);
readonly clients: readonly [BrowserTestClient, BrowserTestClient];
get first(): BrowserTestClient;
get second(): BrowserTestClient;
goOffline(): Promise<void>;
goOnline(): Promise<void>;
captureFailure(label: string, snapshot?: (client: BrowserTestClient) => Promise<ValueFreeState>): Promise<FailureArtifacts | undefined>;
close(): Promise<void>;
}

Coordinates a pair of BrowserTestClients sharing one browser, with helpers to take both clients offline/online and to capture redacted, value-free failure artifacts.

waitForReady​

function

function waitForReady(page: Page, predicate: () => boolean | Promise<boolean>, options?: ReadinessOptions): Promise<void>

function waitForReady<Argument>(page: Page, predicate: (argument: Argument) => boolean | Promise<boolean>, argument: Argument, options?: ReadinessOptions): Promise<void>

async function waitForReady<Argument>(page: Page, predicate: (argument: Argument) => boolean | Promise<boolean>, argumentOrOptions?: Argument | ReadinessOptions, maybeOptions?: ReadinessOptions): Promise<void>

Wait for an app-owned browser predicate that takes no argument. The predicate runs in the page and Playwright owns the timeout/polling, so tests do not need arbitrary sleeps.

ParameterDescription
pageThe Playwright page to poll.
predicateAn app-owned readiness check evaluated inside the page.
optionsOptional description, timeout, and polling configuration.

Returns: Resolves when the predicate becomes true; rejects with ReadinessError on timeout.

Example

import { waitForReady } from "@nzip/lofi/testing";

await waitForReady(client.page, () => document.querySelector(".task-list") !== null, {
description: "task list rendered",
});

withVirtualAuthenticator​

function

async function withVirtualAuthenticator(page: Page, options: VirtualAuthenticatorOptions): Promise<VirtualAuthenticatorHandle>

Install a CDP virtual authenticator on page and return a handle that removes it on VirtualAuthenticatorHandle.dispose. Defaults model an internal, resident, user-verifying platform authenticator that auto-satisfies presence.

WriteHandle​

class

class WriteHandle<T> implements PromiseLike<T> {
constructor(writeId: string);
get writeId(): string;
get batchId(): string | null;
get stage(): WriteStage;
get reason(): WriteRejection | null;
get saved(): Promise<T>;
get synced(): Promise<T>;
then<Fulfilled = T, Rejected = never>(onfulfilled?: ((value: T) => Fulfilled | PromiseLike<Fulfilled>) | null, onrejected?: ((reason: unknown) => Rejected | PromiseLike<Rejected>) | null): Promise<Fulfilled | Rejected>;
subscribe(listener: () => void): () => void;
}

A single write observed through the author-facing lifecycle.

await write (the thenable) resolves at saved with the write's value — for inserts, the created row. write.synced resolves when the store confirms the write and rejects with WriteRejectedError when the store denies it. stage and reason are current-state properties; subscribe notifies immediately and on every later change.

On a device without managed sync there is no store to confirm anything: local durability is settlement, and the handle reaches synced as soon as it is saved. In Preact components, render a handle with useWrite and the app-wide pending set with usePendingWrites.

Handles are issued by the runtime and are observe-only: the lifecycle mutators live on a controller the ledger keeps at construction, so no consumer of a handle can advance or settle it.

Example

const write = placeOrder({ sku, quantity }); // a verb returns a WriteHandle
const order = await write; // resolves at saved — safe to navigate
write.synced.catch((error) => {
if (error instanceof WriteRejectedError) showDenied(error.message);
});

BrowserDiagnostic​

interface

interface BrowserDiagnostic {
readonly client: ClientName;
readonly kind: "console" | "page-error" | "request-failed";
readonly level?: string;
readonly message: string;
readonly url?: string;
}

A redacted console, page-error, or failed-request record captured from a client.

BrowserScenarioContext​

interface

interface BrowserScenarioContext {
readonly alice: BrowserScenarioPeer;
readonly bob: BrowserScenarioPeer;
}

The peers a browser scenario body receives.

BrowserScenarioOptions​

interface

interface BrowserScenarioOptions {
baseURL: string | undefined;
identity: IdentityOptions;
ready: (peer: BrowserScenarioPeer) => Promise<void>;
snapshot: (peer: BrowserScenarioPeer) => Promise<ValueFreeState>;
artifacts?: FailureArtifactOptions;
timeoutMs?: number;
}

Configuration for a browser scenario.

BrowserScenarioPeer​

interface

interface BrowserScenarioPeer extends ScenarioPeerControls {
readonly client: BrowserTestClient;
readonly page: Page;
}

A browser scenario peer: the scenario controls over a real browser client. There is no typed db here — the app's data lives inside the page, so edits go through the page and state is observed through the scenario's value-free snapshot hook.

ClientName​

type

type ClientName = "first" | "second";

Identifies which of the fixture's two clients a value belongs to.

ConcurrentOfflineScenario​

interface

interface ConcurrentOfflineScenario<Edit, Client extends OfflineTestClient = BrowserTestClient> {
readonly edits: readonly [Edit, Edit];
readonly timeoutMs?: number;
readonly ready: (client: Client, signal: AbortSignal) => Promise<void>;
readonly apply: (client: Client, edit: Edit, signal: AbortSignal) => Promise<void>;
readonly locallyApplied: (client: Client, edit: Edit, signal: AbortSignal) => Promise<void>;
readonly whilePending?: (fixture: OfflineTestFixture<Client>, signal: AbortSignal) => Promise<void>;
readonly converged: (fixture: OfflineTestFixture<Client>, signal: AbortSignal) => Promise<void>;
readonly snapshot?: (client: Client) => Promise<ValueFreeState>;
readonly failureLabel?: string;
}

App-owned definition of a concurrent-offline convergence test: the two edits plus the hooks that assert readiness, apply and locally verify each edit, and confirm convergence after reconnection.

ConvergenceScenarioErrorOptions​

interface

interface ConvergenceScenarioErrorOptions extends ErrorOptions {
captureError?: unknown;
}

Options for ConvergenceScenarioError, extending ErrorOptions with the capture outcome.

ConvergenceStage​

type

type ConvergenceStage =
| "readiness"
| "going offline"
| "concurrent edits"
| "local persistence"
| "pending-work hook"
| "reconnection"
| "convergence";

The lifecycle stages the convergence runner moves through, in order.

ConvergeOptions​

interface

interface ConvergeOptions<A> {
tables?: readonly ScenarioTableKey<A>[];
timeoutMs?: number;
pollMs?: number;
counterColumns?: "presence" | "value";
}

Options accepted by converge alongside the peers.

DevicePublicKey​

type

type DevicePublicKey = {
alg: "ES256";
spki: string;
};

The device public key offered at the scope-down exchange.

EffectContext​

type

type EffectContext = {
journalId: string;
writeId: string;
verb: string | null;
table: string;
op: "insert" | "update" | "remove";
rowId: string;
writeCreatedAt: number;
fate: "synced" | "rejected";
cause: "denied" | "expired" | null;
code: string | null;
reason: string | null;
};

Delivery metadata passed to every effect handler. Delivery is at-least-once: a crash between handler start and journal completion re-runs the handler at the next boot, so handlers calling external services should pass EffectContext.journalId as an idempotency key.

FailureArtifactOptions​

interface

interface FailureArtifactOptions {
readonly directory: string;
readonly secretValues?: readonly string[];
readonly mask?: (client: BrowserTestClient) => readonly Locator[];
}

Configures where and how redacted failure artifacts are written on capture.

FailureArtifacts​

interface

interface FailureArtifacts {
readonly directory: string;
readonly files: readonly string[];
}

The directory and file paths produced by a failure capture.

FuzzColumn​

interface

interface FuzzColumn {
name: string;
type: string;
nullable: boolean;
hasDefault: boolean;
mergeStrategy?: string;
references?: string;
}

One column of a table, as the fuzz generator sees it.

FuzzOp​

type

type FuzzOp =
| { kind: "insert"; peer: string; table: string; values: Record<string, unknown>; ref: number }
| { kind: "update"; peer: string; table: string; ref: number; patch: Record<string, unknown> }
| { kind: "remove"; peer: string; table: string; ref: number }
| { kind: "offline"; peer: string }
| { kind: "online"; peer: string }
| { kind: "sync" };

One step of a fuzz plan. Row-targeting steps reference the insert that created the row by its ref — the index of that insert in the plan — since row ids are only assigned when the plan runs.

FuzzOpKind​

type

type FuzzOpKind =
| "insert"
| "update"
| "remove"
| "offline"
| "online"
| "sync";

The kinds of operations a fuzz plan is built from.

FuzzPlan​

interface

interface FuzzPlan {
seed: number;
ops: readonly FuzzOp[];
skippedTables: readonly string[];
}

A generated fuzz plan: the operations plus what the generator left out.

FuzzPlanInput​

interface

interface FuzzPlanInput {
seed: number;
steps: number;
peers: readonly string[];
tables: { [table: string]: readonly FuzzColumn[] };
weights?: Partial<Record<FuzzOpKind, number>>;
}

Input to the pure fuzz-plan generator.

FuzzScenarioOptions​

interface

interface FuzzScenarioOptions<A extends ScenarioApp> extends ScenarioConfig<A> {
seed?: number;
steps?: number;
tables?: readonly string[];
weights?: Partial<Record<FuzzOpKind, number>>;
}

Options for a fuzz scenario, extending the scenario config with fuzz knobs.

IdentityOptions​

type

type IdentityOptions = { readonly mode: "shared"; readonly preparePrimary: (client: BrowserTestClient) => Promise<void> } | { readonly mode: "isolated"; readonly prepare?: (client: BrowserTestClient) => Promise<void> };

How the two clients obtain identity: shared clones the primary's prepared state (in memory) into the second client, isolated prepares each client on its own.

MemoryStorageState​

type

type MemoryStorageState = Awaited<ReturnType<BrowserContext["storageState"]>>;

Playwright storage state held only in memory — cookies plus per-origin localStorage and IndexedDB — used to clone a prepared identity into a fresh browser context without an on-disk state file.

MutationDescriptor​

type

type MutationDescriptor = {
readonly verbName: string;
readonly op: MutationOp<unknown, unknown>;
readonly units: readonly EffectUnit<{ id: string }>[];
readonly expiresAfterMs: number | null;
};

The registered declaration the runtime dispatches for one verb.

MutationRuntime​

type

type MutationRuntime = {
dispatch(descriptor: MutationDescriptor, args: readonly unknown[]): WriteHandle<unknown>;
dispatchChained(descriptor: MutationDescriptor, args: readonly unknown[], parentJournalId: string): Promise<void>;
recordLog(label: string, context: EffectContext): void;
recordTrace?(label: string | null, context: EffectContext): void;
recordDebug?(event: string, context: EffectContext): void;
enqueueNotice?(input: NoticeInput, context: EffectContext): Promise<void>;
applyMark?(table: TableProxy<unknown, unknown>, rowId: string, patch: Record<string, unknown>): Promise<void>;
unitRegistered?(name: string): void;
};

The runtime half installed by the package runtime before verbs are called.

NoticeInput​

type

type NoticeInput = {
message: string;
tone: "info" | "success" | "warning" | "error";
ttlMs: number | null;
};

One durable notice a notice unit enqueues. The queue is persistent and UI-agnostic: entries may be created at a boot re-arm with nothing mounted, and a component renders them later. tone classifies the message for the render; ttlMs bounds its life when the author sets no explicit dismissal.

OfflineTestClient​

interface

interface OfflineTestClient {
readonly offline: boolean;
goOffline(): Promise<void>;
goOnline(): Promise<void>;
}

Minimal client contract the convergence runner needs: offline state and toggles.

OfflineTestFixture​

interface

interface OfflineTestFixture<Client extends OfflineTestClient> {
readonly clients: readonly [Client, Client];
readonly first: Client;
readonly second: Client;
goOffline(): Promise<void>;
goOnline(): Promise<void>;
captureFailure(label: string, snapshot?: (client: Client) => Promise<ValueFreeState>): Promise<unknown>;
}

Minimal two-client fixture contract required to drive an offline scenario.

PopKeyStore​

type

type PopKeyStore = {
get(keyId: string): Promise<CryptoKeyPair | null>;
put(keyId: string, pair: CryptoKeyPair): Promise<void>;
};

Storage for the device keypair; IndexedDB in browsers.

ReadinessOptions​

interface

interface ReadinessOptions {
description?: string;
timeoutMs?: number;
polling?: "raf" | number;
}

Options controlling waitForReady's description, timeout, and polling.

SafeContextOptions​

type

type SafeContextOptions = Omit<BrowserContextOptions, "storageState"> & { storageState?: never };

Playwright context options with storageState forbidden, so identity never leaves memory via an on-disk state file.

ScenarioApi​

interface

interface ScenarioApi {
fuzz<A extends ScenarioApp>(name: string, options: FuzzScenarioOptions<A>): void;
browser(name: string, options: BrowserScenarioOptions, body: (context: BrowserScenarioContext) => Promise<void>): void;
}

The scenario entry points. The call signature registers a headless scenario; ScenarioApi.fuzz and ScenarioApi.browser register the fuzz and browser flavors. Each call registers one Deno.test, so scenarios are declared at the top level of a test module.

ScenarioApp​

type

type ScenarioApp = {
readonly wasmSchema: { [table: string]: { readonly columns: readonly { readonly name: string; readonly nullable: boolean; readonly default?: unknown; readonly references?: string; readonly column_type: { readonly type: string }; readonly merge_strategy?: string }[] } };
};

The app-object shape the headless adapter needs: table handles plus the compiled schema.

ScenarioConfig​

interface

interface ScenarioConfig<A extends ScenarioApp> {
app: A;
permissions: CompiledPermissions;
timeoutMs?: number;
}

The app under test plus the scenario's own settings.

ScenarioContext​

interface

interface ScenarioContext<A> {
readonly alice: ScenarioPeer<A>;
readonly bob: ScenarioPeer<A>;
addPeer(name: string): Promise<ScenarioPeer<A>>;
}

The peers a scenario body receives, plus a factory for additional ones.

ScenarioDb​

type

type ScenarioDb<A> = { [mapped type] };

The app's tables as scenario facades, keyed by the app's own table names.

ScenarioErrorOptions​

interface

interface ScenarioErrorOptions extends ErrorOptions {
peer?: string;
details?: string;
}

Options for ScenarioError, extending ErrorOptions with scenario context.

ScenarioPeer​

interface

interface ScenarioPeer<A> extends ScenarioPeerControls {
readonly db: ScenarioDb<A>;
restart(): Promise<void>;
}

A headless scenario peer: a real synced client with the scenario controls plus the app's typed table API. Writes issued through db apply to this peer's local view immediately — pair them with ScenarioPeerControls.settle or a convergence assertion when the scenario needs them visible elsewhere.

ScenarioPeerControls​

interface

interface ScenarioPeerControls {
readonly name: string;
readonly isOffline: boolean;
offline(): Promise<void>;
online(): Promise<void>;
settle(options?: ScenarioSettleOptions): Promise<void>;
}

The controls every scenario peer implements, headless or browser: a stable name, an offline window toggle, and a durability barrier for issued writes.

ScenarioReadOptions​

interface

interface ScenarioReadOptions {
tier?: "local" | "global";
}

Options for reads through a ScenarioTable.

ScenarioSettleOptions​

interface

interface ScenarioSettleOptions {
timeoutMs?: number;
}

Options for ScenarioPeerControls.settle.

ScenarioStage​

type

type ScenarioStage =
| "boot"
| "deploy"
| "peers"
| "body"
| "convergence"
| "assertion"
| "teardown";

The lifecycle stages a scenario moves through, in order.

ScenarioTable​

interface

interface ScenarioTable<Row, Init, Where> {
insert(values: Init): Promise<Row>;
update(id: string, patch: Partial<Init>): Promise<void>;
remove(id: string): Promise<void>;
all(where?: Where, options?: ScenarioReadOptions): Promise<readonly Row[]>;
get(id: string, options?: ScenarioReadOptions): Promise<Row | undefined>;
}

One table of a headless peer's typed facade: the app's insert/update/remove writes, applied to the peer's local view immediately (online or offline), plus local-view reads. Writes issued here are tracked by the owning peer so ScenarioPeerControls.settle can wait for their global durability.

ScenarioTableKey​

type

type ScenarioTableKey<A> = { [mapped type] }[keyof A] & string;

The table names of an app: every key whose value is a table handle. The app object's non-table members (union, wasmSchema) are excluded by shape, not by name, so future non-table members stay excluded too.

ScenarioTableRef​

type

type ScenarioTableRef<Row> = {
readonly _table: string;
readonly _rowType: Row;
};

A reference to one of the app's tables, as the row assertions accept it: any of the app object's own table handles (e.g. app.documents) matches.

SharedColumnConfig​

type

type SharedColumnConfig = {
label: string;
kind: "text" | "json";
group: string;
groupIdColumn: string;
keys: string;
directory: string;
};

The wiring one shared column declares: where its group and keys live.

TwoClientFixtureOptions​

interface

interface TwoClientFixtureOptions {
readonly baseURL: string;
readonly identity: IdentityOptions;
readonly browser?: Browser;
readonly context?: SafeContextOptions;
readonly artifacts?: FailureArtifactOptions;
readonly traceOnFailure?: boolean;
}

Options for constructing a TwoClientFixture via createTwoClientFixture.

ValueFreeState​

type

type ValueFreeState =
| null
| boolean
| number
| { [key: string]: ValueFreeState }
| readonly ValueFreeState[];

A JSON-like value restricted to booleans, finite numbers, arrays, and plain objects. Strings are excluded so state snapshots record shape and counts but never user values or credentials.

VirtualAuthenticatorCredential​

interface

interface VirtualAuthenticatorCredential {
readonly credentialId: string;
readonly isResidentCredential: boolean;
readonly rpId?: string;
readonly privateKey: string;
readonly userHandle?: string;
readonly signCount: number;
readonly largeBlob?: string;
}

Serializable CDP credential used to copy one virtual passkey between browser profiles.

VirtualAuthenticatorHandle​

interface

interface VirtualAuthenticatorHandle {
readonly authenticatorId: string;
credentials(): Promise<VirtualAuthenticatorCredential[]>;
addCredential(credential: VirtualAuthenticatorCredential): Promise<void>;
clearCredentials(): Promise<void>;
dispose(): Promise<void>;
}

A handle to an installed virtual authenticator; call dispose to remove it.

VirtualAuthenticatorOptions​

interface

interface VirtualAuthenticatorOptions {
readonly protocol?: "ctap2" | "u2f";
readonly transport?: "usb" | "nfc" | "ble" | "cable" | "internal";
readonly hasResidentKey?: boolean;
readonly hasUserVerification?: boolean;
readonly isUserVerified?: boolean;
readonly automaticPresenceSimulation?: boolean;
}

Options for withVirtualAuthenticator.

WriteHandleController​

type

type WriteHandleController<T> = {
setBatchId(batchId: string | null): void;
advance(stage: Exclude<WriteStage, "rejected">, value?: T): void;
reject(rejection: WriteRejection): void;
fail(error: unknown): void;
};

The runtime-only mutator half of a write handle. Only the write ledger holds a handle's controller — the handle itself exposes no way to advance or settle, so application code can observe a write's fate but never forge it.

WriteRejection​

type

type WriteRejection = {
cause: "denied" | "expired";
code: string | null;
reason: string;
};

Why a write settled as rejected: the structured cause, code, and reason.

WriteStage​

type

type WriteStage =
| "saving"
| "saved"
| "syncing"
| "synced"
| "rejected";

The closed, framework-owned write lifecycle. Stages are monotonic: saving → saved → syncing → synced | rejected. syncing is reserved for a runtime that can observe the transport; the current storage engine exposes no such signal, so today writes move from saved directly to synced or rejected and no handle ever reports syncing. Branch on saved vs settled, not on seeing syncing.