Skip to content

Testing: Network Control

The test harness gives you full control over network timing, letting you test optimistic updates, race conditions, reordering, and offline behavior — deterministically, with no real timers or real network connections.

Creating a Controlled Client

Use createControlledClient() to get a transport handle with manual flush control. Messages queue on both outbound (client→server) and inbound (server→client) sides until you explicitly flush them.

typescript
import { test, expect } from "bun:test";
import { createPoeAppTestHarness } from "poe-apps-sdk/v1/test-utils.js";
import { myBackendConfig } from "../synced-store/backend-config";
import type { MySchema } from "../synced-store/schema";

const harness = createPoeAppTestHarness<MySchema>({
  store: { backendConfig: myBackendConfig },
});
const { store, transport } = await harness.createControlledClient();

// Initialize the client (complete the pull handshake)
await transport.flushUntil(store.waitForServerData());

transport.flushUntil(store.waitForServerData()) is the standard way to initialize a controlled client before starting a scenario.

Disabling and Enabling Auto-Flush

Toggle each direction independently:

typescript
transport.outbound.disableAutoFlush();   // stop client→server
transport.inbound.disableAutoFlush();    // stop server→client
transport.outbound.enableAutoFlush();
transport.inbound.enableAutoFlush();

Flushing Messages

MethodEffect
transport.outbound.flushNext()Send the next queued outbound message
transport.inbound.flushOne()Deliver the next inbound message
transport.inbound.flushAll()Deliver all queued inbound messages
transport.step()One full round trip (outbound, then inbound)
transport.flushUntil(promise)Keep flushing until a promise resolves

Optimistic-Update Test

Mutations apply locally before any network round trip. Verify the local state updates before anything is sent:

typescript
test("optimistic updates visible immediately", async () => {
  const { store, transport } = await harness.createControlledClient();
  await transport.flushUntil(store.waitForServerData());

  transport.outbound.disableAutoFlush();

  await store.mutate.setValue({ key: "test", value: "optimistic" });

  // Value is readable locally, even though nothing has been sent
  const value = await store.query((tx) => tx.table("data").get("test"));
  expect(value).toBe("optimistic");

  harness.dispose();
});

Protocol-Aware Queue Introspection

When you need to filter queued messages by their protocol type (e.g., poke, push_response, pull_response), use the protocol helpers:

typescript
import { hasInboundType } from "poe-apps-sdk/v1/test-utils.js";

// Check what's queued
const hasPoke = transport.inbound.queue.some(hasInboundType("poke"));

// Deliver only messages matching a predicate
await transport.inbound.flushMatching(hasInboundType("poke"));

// Remove a message from the queue without delivering it
transport.inbound.remove(hasInboundType("push_response"));

For reordering and failure simulation, see testing-race-conditions.md and testing-network-failures.md.