Skip to content

Testing

Everything above the transport can be tested without a network, because the transport is an interface and one implementation is a connected pair of streams.

final (clientSide, serverSide) = InMemoryTransport.pair();
final client = AhpConnection(clientSide);
serverSide.incoming.listen((message) {
// Answer as a host would.
});

pair() returns two transports that deliver to each other. Everything above — JSON-RPC framing, reducers, the mirror, reconciliation — runs exactly as it does against a real host, because none of it can tell the difference.

Reducers stamp modifiedAt, so they read a clock. Inject it rather than letting them reach for DateTime.now():

final mirror = AhpStateMirror(
clientId: 'test',
now: () => DateTime.fromMillisecondsSinceEpoch(9999),
);

The fixture corpora depend on this — they assert exact timestamps, and 9999ms is the value upstream froze them at.

Unlike Go’s process-global SetNowProvider, the seam here is a constructor parameter, because Dart tests share an isolate and a global would leak between them.

Reducers are pure functions. Most of the time you do not need a transport at all:

final next = chatReducer(state, action, clientId: 'test', now: frozen);
expect(next.activeTurn?.responseParts, hasLength(2));

This is how the 246 upstream fixtures run, and it is the fastest way to pin down behaviour.

Releasing the last lease does not unsubscribe immediately — the runtime holds it for five seconds so navigating away and back is cheap. In a widget test that timer is still pending when the test ends, and flutter_test fails on it.

AhpRuntime(
connect: connect,
clientId: 'test',
lingerDuration: Duration.zero,
);

Zero means teardown runs inline, with no timer to leave pending.

final actions = client.actions.listen(mirror.applyEnvelope);
await client.initialize(params); // ← in this order

actions is a broadcast stream. Reversing these lines loses every envelope sent during the handshake, and nothing reports it — the test just sees state that disagrees with what the fake host sent, some distance from the cause.

Terminal window
dart test

416 tests: 246 reducer fixtures, 39 round-trip fixtures, and the rest covering reconciliation, the runtime, and the transport.

The two fixture harnesses read the vendored corpora under upstream/ and hard-fail if they cannot find them. That guard is deliberate — a fixture suite that silently passes because it found no fixtures is worse than one that fails. It also means those two cannot run from a published archive, which is why test/ is not shipped.