Caches and events
On this page
An event is a fact an action records, such as "an order was placed", and a subscriber is code that runs when one is delivered (Subscribers). Events run after the action that emits them commits. In a test, what a step emits is delivered before the next line runs, so a test checks an event's effects on the line after the call:
- Events are delivered after each step of the test commits and before
the next statement runs, at the test's clock, until no events are left.
A test checks their effects, such as a sent reminder in
requests. A subscriber that fails fails the test, as below. - Caches are copies used only by display reads from routes (Caches). In a test they are on and start empty, in an in-memory cache that behaves like Redis, so a route test sees the same invalidation as production. Tests can't read, fill or clear a cache directly. Actions never read caches, so only route tests meet them.
Steps and delivery
A step is a call of an action or fixture, a route test's request, a
schedule's .run() or clock.advance(...). Once a step comes back, what
is due in the test's outbox, the list of events waiting to be delivered,
is delivered as the runner delivers it anywhere: as the system, limited
to its event's tenant. Then what those subscribers emit is delivered in
turn, until nothing is due.
The order subscribers run in
Subscribers run in an order shuffled by the run's seed, the one the
report prints and --seed gives again, mixed with the test's own
identity. Each delivery is drawn from all those due at that moment. So a
test that relies on the order its subscribers run in fails under some
seed, and rerunning it with that seed fails the same way.
Retries
A delivery whose subscriber fails in a way worth trying again waits its
full backoff: 30 seconds, and then twice as long each time. There is no
random jitter, since a test has no randomness to draw it from
(Retries and giving up).
The test's clock moves only when the test moves it, so the retry runs
once clock.advance reaches it.
A fake failing a subscriber's call to a provider is retried and given up
the same way, and a test scripts it with fakes:. A subscriber's own
failure worth retrying tests its retries too.
A delivery that fails for good
A delivery fails for good when its failure isn't worth retrying, when it
fails on its last attempt, or when it hits a fault. It ends the test there
as test.subscriber_failed, naming
the event, the subscriber and the attempt.
What a route's settle: delivers while its request waits belongs to the
request, and the response says how it ended
(Waiting for follow-on work).
An example
Here placing an order emits OrderPlaced, and cook_placed cooks it
once a chef is on shift:
use kitchen/entities { Order, OrderNotFound }use kitchen/failures { NoChef }use kitchen/queries { chefs }/// Cooks an order, once a chef is on shift.action cook(order_id: Order.id) fails OrderNotFound, NoChef -> Unit { if (list.count(chefs()) == 0) { reject NoChef {} } attempt Order.update(order_id) { set: { cooked_at: clock.now } }}
use kitchen/actions { cook }use kitchen/events { OrderPlaced }/// Cooks each order placed.subscriber cook_placed { on: OrderPlaced run: cook(event.order_id)}
use kitchen/entities { Chef, Order }use kitchen/actions { start_shift }test "an order is cooked before the next line" { clock: "2026-10-20T18:00:00Z" var chef: Chef = start_shift() assert chef.on_shift var order: Order = place_order() var cooked: Order = attempt Order.get(order.id) assert cooked.cooked_at == "2026-10-20T18:00:00Z"}test "an order placed before the shift is cooked when its retry comes round" { clock: "2026-10-20T18:00:00Z" var order: Order = place_order() var chef: Chef = start_shift() assert chef.on_shift var waiting: Order = attempt Order.get(order.id) assert waiting.cooked_at == none clock.advance(30s) var cooked: Order = attempt Order.get(order.id) assert cooked.cooked_at == "2026-10-20T18:00:30Z"}
The first test reads the order cooked on the line after placing it.
In the second, the delivery fails with NoChef, which is worth
retrying, so it waits 30 seconds. Starting the shift doesn't move the
clock, and the retry runs once clock.advance(30s) reaches it.
Not built yet: caches, which need the Redis driver, and running a batch to the end in a test. A batch run a step starts is left as it was started (Provider calls and capabilities in tests).
Markdown for agents: /docs/caches-and-events.md.Built from Jadpo main at a707efb.