---
title: Specify occurrences and receipts in process
editUrl: https://github.com/Cratis/Chronicle/edit/main/Documentation/occurrences/testing.mdx
description: Drive trigger deliveries, effect receipts, lease expiry, retries and lost replies with isolated in-process scenarios.
lastUpdated: 2026-10-20T00:00:00.000Z
---


Use `Cratis.Chronicle.Testing` to specify a trigger handler's effects, retries and recovery without a running kernel. Each scenario owns isolated in-memory storage. Recording and delivery are separate actions: advancing time never invokes a handler.

The examples below use `Cratis.Chronicle.Testing`, `.Events` and `.Occurrences`, together with `Cratis.Chronicle.Occurrences`. `ImportRequested` denotes your attributed trigger and `ImportHandler` its `IOccurrenceHandler<ImportRequested>` implementation. Dispose each store and scenario after its specification.

## Choose the advertised kernel capabilities

An `EventStoreForTesting` advertises `Current()` by default. This includes precise event schemas, occurrences and effect receipts. Scenarios now discover precise schemas rather than the legacy fallback shape.

For example, create `using var store = new EventStoreForTesting();`, then call `store.Capabilities.Old();` before exercising a receipt enrollment. `unit.UseEffectReceipt(key)` throws `EffectReceiptsNotSupported`. `store.Capabilities.Unknown();` models a skipped or failed compatibility check and also fails closed. `store.Capabilities.Current();` restores the current advertisement.

Schema discovery happens during store construction. To choose the advertisement before discovery, create a `KernelCapabilitiesForTesting`, call `Old()` or `Unknown()`, and pass it to `new EventStoreForTesting(null, artifacts, null, new ScenarioTime(), capabilities)`. Changing the switch afterward changes operation admission, not schemas already discovered.

## Commit an event and its receipt together

Use the real unit of work with the scenario's store. For example:

1. Create `using var unit = new UnitOfWork(CorrelationId.New(), _ => { }, store);` from `Cratis.Chronicle.Transactions`.
2. Enroll `unit.UseEffectReceipt(context.EffectKey("import"));`.
3. Stage an event with `unit.AddEvent(EventSequenceId.Log, "import", new ImportCompleted(), Causation.Unknown());`.
4. Run `await unit.Commit();` and inspect `unit.IsSuccess` and `unit.EffectReceipt`.
5. Read `await store.Occurrences.GetEffectReceipt(context.EffectKey("import"));` to recover the winning result.

The append and receipt use the real kernel append path and the production in-memory storage's atomic write. Repeating the effect key in a fresh unit returns `AlreadyRecorded` and appends no second batch, even when the repeated handler computes different content. Receipt-only operations accept `NoEvents` or `Refused`, for example `await store.Occurrences.RecordEffectReceipt(key, new NoEvents());`.

For a validate-only example, enroll the key, then call `unit.AddEvents(EventSequenceId.Log, [], [new("source", new ConcurrencyScope(EventSequenceNumber.BeforeFirst, "source"))]);` before committing. The empty batch validates that source's scope and records `NoEvents` only if it still matches. A conflicting append writes no receipt. Explicit scopes use `Cratis.Chronicle.EventSequences.Concurrency`; protected decisions use their normal owning completion capability.

## Record and deliver a typed trigger

Create `using var scenario = new OccurrenceScenario<ImportHandler>();`. Register constructor dependencies in `scenario.Services` before the first operation. The scenario supplies its own `IEventStore`, `IEventLog` and `IUnitOfWorkManager`.

Record with `var recorded = await scenario.Record(new ImportRequested(), "import-42");`, then invoke `var outcome = await scenario.DeliverNext();`. Successful handling returns `Completed`. `await scenario.StatusOf(recorded.Id)` returns the durable snapshot, including attempts, epoch, next attempt and receipts. With nothing claimable, delivery returns `NothingDue`.

The scenario activates a fresh handler in a fresh dependency scope for each attempt. It runs the real occurrence service and delivery policy over in-memory storage. It deliberately replaces the Orleans queue's reminder/timer shell with serialized calls: there are no background workers or automatic lease renewals. This is not a test of silo recovery, network transport, production persistence or lifecycle fan-out.

## Build an occurrence context

For a handler or command specification that needs a context without delivery, use `new OccurrenceContextBuilder().For<ImportRequested>().WithIdempotencyId("import-42").BuildDetached()`.

`ForTrigger(id)` selects an explicit trigger. Call it before `WithIdempotencyId` so identity derivation uses the intended trigger. `WithId(id)`, `Kind(kind)`, `RecordedAt(instant)`, `DueAt(instant)`, `Attempt(number)` and `Epoch(number)` select individual metadata values. Attempts and epochs default to one.

A detached context owns no durable lease: receipts still deduplicate, but completing its token returns `OccurrenceLeaseLost`. For a real lease, use `await new OccurrenceContextBuilder(store.Time).For<ImportRequested>().WithIdempotencyId("import-42").RecordAndClaimIn(store)` instead. This seeds a synthetic occurrence with an empty payload and claims it; it does not validate a payload or invoke a handler. The trigger queue must be empty. Recorded contexts default to the store's clock; detached contexts use the builder's clock.

## Advance time and retry explicitly

`ScenarioTime` starts at `2026-01-01T00:00:00Z`, or at the instant supplied to its constructor. `Now` and `GetUtcNow()` report UTC.

For example, call `await scenario.DeliverNextFailing(new ImportFailed());` with your exception type. This substitutes a handler failure without invoking the handler, and records normal backoff. Read the snapshot, then call `scenario.Time.AdvanceTo(snapshot.NextAttempt);` and `await scenario.DeliverNext();`. Delivery before the backoff expires returns `NothingDue`. Ordinary handler exceptions follow the same failure path; exhausted attempts become quarantined.

Use `scenario.Time.Advance(TimeSpan.FromSeconds(60));` to expire a lease. Moving backward through either advance method throws `ScenarioTimeCannotMoveBackwards` and preserves the current instant.

To share an explicit clock with event scenarios, construct `new Defaults(artifacts, null, null, time)` and pass those defaults to `EventScenario` or `OccurrenceScenario<ImportHandler>`. `EventScenario.Time` exposes that clock. Event append timestamps and receipt timestamps still use the kernel's system clock; this driver controls occurrence recording, lease expiry and backoff only.

## Lose a reply after commit

Queue a fault with `scenario.Replies.DropNext(DroppedReply.Append);` before delivery. The next successful **receipted or keyed** append (any append shape) commits, then throws `Grpc.Core.RpcException` with `StatusCode.Unavailable`. For a receipted handler append, the handler attempt fails, but its receipt survives. On retry with the same effect key, the original outcome wins and no duplicate events are appended.

For keyed appends through `store.EventLog`, queue the same fault with `store.Replies.DropNext(DroppedReply.Append);`. Retry `AppendOnce` or `AppendManyOnce` with the same `IdempotencyKey` after reply loss; the retry reports `AlreadyAppended` without duplicating events. The named-tag variants behave the same way.

Other fault points are `RecordEffectReceipt`, `RecordOccurrence`, `ClaimOccurrences`, `CompleteOccurrence` and `FailOccurrence`. For example, `store.Replies.DropNext(DroppedReply.RecordEffectReceipt, 2);` discards two successful receipt-only replies. `Dropped` counts replies actually discarded, not faults queued. Refused mutations and ordinary appends without a receipt or idempotency key do not consume these faults.

An unknown outcome is not permission to execute an unrelated replacement. Inspect durable state or retry with the same identity. A lost claim remains leased until expiry. A repeated completion at the same epoch returns `AlreadyCompleted`.

To model a worker lost after its effects but before completion, call `await scenario.DeliverWithoutCompleting();`. It returns `Delivered`, leaving the successful lease intact. Advance past its expiry and deliver again to check receipt-based recovery.

## Assert on collected state

After delivery, assertions are synchronous:

- `scenario.ShouldHaveReceipt(context.EffectKey("import"));` checks the collected receipt identity.
- `scenario.ShouldHaveAppended<ImportCompleted>();` checks events collected from the scenario's event log; a predicate overload checks their content.
- `scenario.ShouldBeQuarantined(recorded.Id);` checks a collected occurrence snapshot.

Delivery collects its occurrence and the event log automatically. After a direct service mutation, await `scenario.StatusOf(id)` to refresh that occurrence before asserting on it. Assertions perform no hidden asynchronous work and throw `OccurrenceScenarioAssertionFailed` when an expectation is unmet.

Use [integration specifications](/chronicle/testing/) against a running kernel for restart, clustering and provider durability. See [effect receipts](/chronicle/occurrences/effect-receipts/) for the production commit and recovery contract.
