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.