Testing
posipaki ships helpers for writing actor tests under posipaki/testing. The
core idea: instead of polling state, you collect the messages an actor emits
and wait for a match — event-driven, with a timeout and a readable failure
message.
Collecting messages
createCollector(filter) returns a plugin plus a collector object. Install the
plugin via addPlugins when you spawn, then await collector.resolved():
import { defineActor, defineMessages } from "posipaki";
import { createCollector } from "posipaki/testing";
const Emitter = defineActor({
name: "emitter",
inMessages: defineMessages<{ type: "POKE"; n: number }>(),
outMessages: defineMessages<{ type: "PONG"; n: number }>(),
setup: () => ({ count: 0 }),
handlers: {
POKE(msg) {
this.state.count += msg.n;
this.emit({ type: "PONG", n: this.state.count });
},
},
});
const collector = createCollector<{ type: "PONG"; n: number }>({ type: "PONG" });
const proc = await Emitter.spawn({}, { addPlugins: [collector.plugin] });
await proc.ready();
proc.send({ type: "POKE", n: 1 });
const result = await collector.resolved();
if (!result.ok) throw new Error(result.detail);
// collector.messages[0] is { type: "PONG", n: 1 }
The collector observes every message the actor emits (onEmit), scoped to the
actor and its children by default.
Matching
A match spec can be three things:
- a message literal — matches the latest emitted message
(
{ type: "PONG" }) - a sequence — matches the tail of history, in order
(
[{ type: "PING" }, { type: "PONG" }]) - a predicate —
(msg, history) => booleanfor anything custom
times(spec, n) matches once the nth occurrence has arrived:
import { times } from "posipaki/testing";
proc.send({ type: "POKE", n: 1 });
proc.send({ type: "POKE", n: 1 });
await collector.next(times({ type: "PONG" }, 3)); // wait for the 3rd PONG
resolved() waits on the current filter (4.5s default). next(filter) swaps
in a new filter and waits. On timeout or actor exit the result is { ok: false, detail } with what was expected and what was seen.
Waiting on state
When the thing you care about is state rather than emitted messages, subscribe for the next change:
import { nextState } from "posipaki/testing";
proc.send({ type: "POKE", n: 5 });
const state = await nextState(proc); // resolves on the next state change
nextMessage(proc) is the same idea for the message channel.
Cleaning up
createRootTracker() remembers every root it was installed on, so a test can
tear everything down in one call:
import { createRootTracker } from "posipaki/testing";
const tracker = createRootTracker();
const proc = await Emitter.spawn({}, { addPlugins: [tracker.plugin, collector.plugin] });
// ... run the test ...
await tracker.stopAll(); // exit every root still running
Scoping and names
The collector filters by process name with the same tree-prefix language as the
debug logger — "*", an exact name, or "prefix:*" for a subtree. Pass
{ scope } to createCollector, or use pnameMatch directly for your own
assertions.