Short answer: test participant moderation against a recorded event clock, not wall-clock sleeps. For a stock trading watchlist, the invariant is simple: a participant who loses access must not receive later symbol updates, while an already-authorized participant still receives them in order. Make that invariant observable, replayable, and independent of network timing.

I run a one-person SaaS, so my useful unit is revenue per hour. A flaky realtime test burns an hour twice: once while it fails in CI, then again while I prove the failure was noise. The practical choice is to outsource timing to a deterministic harness and spend the saved time shipping the feature.

What should a moderation test prove for a stock watchlist?

Moderation is an authorization transition, not a UI event. Model each watchlist participant as a state machine: active, muted, removed, or rejoined. The test should assert what each state may observe, rather than assert that a callback fired after 200 milliseconds.

For a watchlist containing AAPL and NVDA, I use a tiny event log:

type Event = | { kind: "join"; participant: string } | { kind: "moderate"; participant: string; state: "muted" | "removed" } | { kind: "quote"; symbol: string; price: number; seq: number }; const log: Event[] = [ { kind: "join", participant: "trader-7" }, { kind: "quote", symbol: "AAPL", price: 214.12, seq: 1 }, { kind: "moderate", participant: "trader-7", state: "removed" }, { kind: "quote", symbol: "NVDA", price: 139.44, seq: 2 }, ]; 

The key assertion is a delivery set. trader-7 may see sequence 1, but sequence 2 must be absent after removal. A second participant should receive both, in sequence order. This catches a common race where the moderator updates the database but a fan-out worker has already copied the next message into a socket queue.

I hit that race in a local test by waiting for a timer and got an intermittent expected 1, received 2. The failure disappeared under a debugger. That was the clue: the test was measuring scheduler luck, not policy.

How does an event clock remove flaky timing?

Inject a clock with explicit turns. A moderation command, authorization read, and quote fan-out become steps in a trace that the test controls. No setTimeout, no “wait until eventually,” and no dependence on a busy CI runner.

type Delivery = { participant: string; event: Event }; class EventClock { private turn = 0; next(): number { return ++this.turn; } } function deliver( participant: string, state: "active" | "muted" | "removed", event: Event, ): Delivery[] { if (event.kind === "quote" && state !== "active") return []; return [{ participant, event }]; } const clock = new EventClock(); const trace = [ { turn: clock.next(), action: "authorize trader-7" }, { turn: clock.next(), action: "publish AAPL" }, { turn: clock.next(), action: "remove trader-7" }, { turn: clock.next(), action: "publish NVDA" }, ]; if (trace[3].turn <= trace[2].turn) { throw new Error("moderation must precede the next fan-out turn"); } 

In production, the clock is usually a queue or log offset, not this class. The test double only exposes the ordering contract. Keep the transport behind a generic interface so the same assertions can run against WebSocket, WebRTC data channels, or a broker-backed worker. WebRTC’s recommendation describes ordered and reliable data-channel options; the application still owns authorization and replay policy.

Failure modes worth replaying

The harness earns its keep when it replays ugly traces. I keep fixtures for duplicate quote sequence numbers, a reconnect that presents an old membership token, moderation arriving between authorization and enqueue, and a delayed read receipt. Each fixture names the expected visible sequence for every participant.

A muted participant is a useful edge case. In one product, mute meant “no quote payloads but presence is visible”; in another, it meant complete silence. Pick one semantic and test it explicitly. Ambiguity here becomes a support ticket, not a protocol problem.

My replay runner stores each fixture as a plain JSON trace with three columns: the event, the membership version observed, and the delivery decision. Before a test starts, it creates two participants with different roles, publishes one quote, and snapshots both inboxes. Then it applies moderation, advances the clock one turn, and publishes a second quote. The assertion compares complete sequences, including duplicates and gaps, instead of checking only the last message. When a trace fails, the runner prints the first divergent turn and the policy version that made the decision. That detail matters during a release: a queue retry can make a duplicate look like a moderation leak, while a stale membership cache can make a genuine leak look like a transport issue. I keep the trace in version control and run it unchanged against the in-memory dispatcher and the staging broker. The fixture is small enough to review in a pull request, yet it captures the race that a sleep-based test missed.

That is the whole test.

Healthtech has the same shape. For typing indicators and read receipts, a removed participant should not receive a later receipt, while the audit stream can retain the moderation event. The payload differs; the event-clock invariant does not.

For observability, emit the trace ID, membership version, and quote sequence in test logs. Do not log patient or account identifiers. A failed assertion should answer “which policy version admitted this event?” without requiring a packet capture.

What changes when this runs at scale?

At scale I would shard the event log by watchlist, persist a monotonic sequence per shard, and make membership reads versioned. The fan-out worker checks the membership version immediately before enqueueing. That does not make delivery instantaneous; it makes the boundary explicit and measurable.

The trade-off is real. A strict ordered channel can head-of-line block a fresh quote behind a slow client. An unordered channel lowers latency but pushes gap detection and reconciliation into the client. Choose ordered delivery for the policy stream, and consider a separate best-effort channel for rapidly changing prices.

The harness should also run against a real transport in a small nightly suite. Deterministic tests prove policy ordering; they cannot prove TLS setup, browser behavior, or a proxy’s idle timeout. Your mileage may vary across mobile networks, so record those tests separately instead of weakening the invariant.

Stick with wall-clock integration tests when the requirement itself is temporal, such as an expiry at 09:30 UTC. For moderation authorization, use the event clock first. It is faster to review, easier to replay, and less likely to hide a real access-control regression behind a green build.

References