@cratis/pi 7.28.0 → 7.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -278,4 +278,34 @@ async Task Because() => await _scenario.Given.ForEventSource(_id).Events(new Inv
278
278
  [Fact] async Task should_notify_about_the_issued_invoice() => await _notifications.Received(1).Notify(Arg.Is<string>(_ => _.Contains("INV-1001")));
279
279
  ```
280
280
 
281
+ ### Read-model dependencies in scenarios — Chronicle 19.46.0+
282
+
283
+ `ReactorScenario<TReactor>` does **not** build read models from `Given` events,
284
+ including `[Passive]` models. Seed the handler's read-model parameter explicitly
285
+ before firing the trigger:
286
+
287
+ ```csharp
288
+ void Establish() => _scenario.Given.ForEventSourceId(_id).ReadModel(_order);
289
+ async Task Because() => await _scenario.Given.ForEventSource(_id).Events(_orderPlaced);
290
+ ```
291
+
292
+ In a separate absence specification, replace the seeding with
293
+ `_scenario.Given.ForEventSourceId(_id).NoReadModel<Order>()` to deliberately pass
294
+ `null` to an `Order?` parameter. Without either setup, resolution to `null` throws
295
+ `ReadModelNotSeededForReactorScenario` (`Cratis.Chronicle.Testing.Reactors`), even
296
+ for a nullable parameter. Its message names the type and missing key, suggests
297
+ both setup methods, and lists other seeded keys for that type when any exist.
298
+ Use the key from `ICanResolveReadModelKey.Resolve` for seeding or absence when
299
+ implemented, not the triggering source id. `NoReadModel<T>()` does not remove a
300
+ seeded instance. This guard applies to `EventStoreForTesting`, not an explicitly
301
+ supplied store of another type; production read-model resolution is unchanged.
302
+
303
+ Also in Chronicle 19.46.0+, `Events(...)` surfaces a synchronously throwing
304
+ handler's own exception, preserving its stack, rather than the reflection
305
+ `TargetInvocationException`. Capture it with `Catch.Exception` and assert the
306
+ handler's exception type. `EventContext.From(...)` now defaults an omitted
307
+ `occurred` to `DateTimeOffset.UtcNow`, not local `Now`; scenario contexts,
308
+ `EventContext.Empty` and `EmptyWithEventSourceId(...)` use that helper. An
309
+ explicit occurrence time is preserved, not normalized to UTC.
310
+
281
311
  For reactors that return side-effect events, assert the resulting appends through the scenario's event store; for non-event side effects, assert on the mocked services (as above). See [specs.scenarios.csharp.md](./specs.scenarios.csharp.md) for the full `*Scenario` family.
@@ -160,6 +160,47 @@ async Task Because() => await _scenario.Given.ForEventSource(_id).Events(new Aut
160
160
  [Fact] async Task should_notify() => await _service.Received(1).Notify("Jane Austen");
161
161
  ```
162
162
 
163
+ #### Read-model setup — Chronicle 19.46.0+
164
+
165
+ In a class-based `ReactorScenario<TReactor>`, `Given` events invoke the reactor;
166
+ they **do not materialize read models**, even `[Passive]` ones. For each
167
+ read-model handler parameter, choose one setup before `Because()`:
168
+
169
+ ```csharp
170
+ // Present-state specification: supply the instance the handler must receive.
171
+ void Establish() => _scenario.Given.ForEventSourceId(_id).ReadModel(_author);
172
+
173
+ // Separate absence specification: deliberately let an Author? parameter receive null.
174
+ void Establish() => _scenario.Given.ForEventSourceId(_id).NoReadModel<Author>();
175
+ ```
176
+
177
+ These are alternatives in **separate specification classes**, not two
178
+ `Establish()` methods in one class. Both builders return
179
+ `ReactorSourceGivenBuilder<TReactor>` for chaining; neither setup is async.
180
+ Use the triggering event-source id by default, or the key returned by the
181
+ reactor's `ICanResolveReadModelKey.Resolve` when implemented. An absence
182
+ declaration is for that exact type and key and does not delete a seeded model.
183
+
184
+ If a recognized read-model parameter resolves to `null` without declared absence,
185
+ `Events(...)` throws `ReadModelNotSeededForReactorScenario` from
186
+ `Cratis.Chronicle.Testing.Reactors`, even if the parameter is nullable. The
187
+ message names the full type and missing key, explains `ReadModel(...)` and
188
+ `NoReadModel<T>()`, and lists seeded keys for that type when any exist. Fix the
189
+ fixture or its key; do not add `NoReadModel<T>()` just to silence missing setup.
190
+ The guard applies to `EventStoreForTesting`; it does not alter production
191
+ resolution or an explicitly supplied event store of another type.
192
+
193
+ **Handler failures:** in Chronicle 19.46.0+, a synchronous handler throw surfaces
194
+ its own exception, not `TargetInvocationException`, with its handler stack
195
+ preserved. Use `await Catch.Exception(() => _scenario.Given.ForEventSource(_id).Events(...))`
196
+ and assert the handler's exception type.
197
+
198
+ **Occurrence time:** in Chronicle 19.46.0+, `EventContext.From(...)` uses
199
+ `DateTimeOffset.UtcNow` when `occurred` is omitted (previously local `Now`).
200
+ `ReactorScenario` builds its contexts through this helper, as do
201
+ `EventContext.Empty` and `EmptyWithEventSourceId(...)`. Explicit occurrence times
202
+ keep their supplied offset; the positional constructor has no such default.
203
+
163
204
  ## Out-of-process Chronicle integration specs (advanced)
164
205
 
165
206
  Reserve these for the host/transport boundary the scenario helpers can't reach. They test a complete slice — HTTP request → command → append → constraint → projection — against a real Chronicle store, and live under `when_<behavior>/` directly inside the slice folder (no `for_` folder; the "unit" is the whole slice).
@@ -30,6 +30,10 @@ This skill decides **when to add a scenario** and **what to cover**. The C# mech
30
30
 
31
31
  > Re-verified at the versions above by **symbol and signature**: every type, attribute and member this skill names exists at that tag, and the public surface it describes is unchanged since the previous verification (Chronicle 16.45.x / Arc 22.10.4 — the Chronicle 16→18 client diff is converters, options and doc comments; no type was removed or renamed). Behavior claims were verified at the earlier tag unless a section says otherwise.
32
32
 
33
+ Reactor read-model setup and the `EventContext.From` UTC default below are
34
+ verified separately at **Chronicle 19.46.0**, the minimum version for that
35
+ guidance; see `cratis-chronicle-reactor/references/specifications.md` for sources.
36
+
33
37
  `Cratis.Testing` is the meta-package that brings `Cratis.Arc.Testing` and
34
38
  `Cratis.Arc.Chronicle.Testing` together. Reverify against the owning product
35
39
  repository before claiming support for another version.
@@ -112,6 +116,15 @@ keep a scenario for that wiring and for appended-event behavior.
112
116
  | Automation or Translation — a reactor | `ReactorScenario<TReactor>` | `cratis-specifications-csharp` |
113
117
  | A host, transport, or real-infrastructure boundary | Out-of-process integration | `cratis-specifications-csharp` |
114
118
 
119
+ For a class-based reactor scenario on **Chronicle 19.46.0+**, explicitly seed
120
+ handler read-model parameters with `Given.ForEventSourceId(id).ReadModel(instance)`
121
+ or declare intentional absence with `.NoReadModel<T>()`. An unseeded model throws
122
+ `ReadModelNotSeededForReactorScenario` even if nullable; `Given` events do not
123
+ build read models, including passive ones. Use the reactor's resolved key, not
124
+ necessarily the trigger's source id. This is **not** the command-scenario event
125
+ seeding described in Step 5. Read `cratis-chronicle-reactor` for the setup examples,
126
+ exception diagnostics, synchronous handler throws and UTC context defaults.
127
+
115
128
  ## Step 3 — Cover every outcome of a State Change slice
116
129
 
117
130
  One specification class for **each** of:
@@ -45,6 +45,11 @@ This skill is verified against these exact sources:
45
45
  | `Cratis.Chronicle` | `18.3.0` | `Cratis.Chronicle.Reactors`, `Cratis.Chronicle.Reactors.SideEffects`, `Cratis.Chronicle.EventSequences`, `Cratis.Chronicle.Events` |
46
46
  | `Cratis.Chronicle.CodeAnalysis` | `18.3.0` | `CHR0004`, `CHR0005`, `CHR0008`, `CHR0013`, `CHR0022`, `CHR0031`, `CHR0032` |
47
47
 
48
+ Scenario read-model seeding/absence, synchronous handler exception propagation,
49
+ and the `EventContext.From` UTC default are verified separately at **Chronicle
50
+ 19.46.0**; they require **19.46.0+**. See the source paths in
51
+ [references/specifications.md](references/specifications.md).
52
+
48
53
  Reverify product sources before claiming support for another version.
49
54
 
50
55
  > **There is no `ReactorSideEffect` type.** It does not exist in Chronicle, in
@@ -363,7 +368,17 @@ when they serve that concern.
363
368
 
364
369
  Start with `Specification` plus a direct call for pure reactor decisions; add `ReactorScenario<TReactor>` only where invocation, dependency wiring or side effects contribute proof. Specify the contract, not only the happy path: field lineage, contract-authorized filtering, repeated delivery, and unrelated events.
365
370
 
366
- Read [references/specifications.md](references/specifications.md) when writing or reviewing reactor specifications.
371
+ In Chronicle **19.46.0+**, a handler's unseeded read-model parameter throws
372
+ `ReadModelNotSeededForReactorScenario`, even if nullable. Seed it through
373
+ `Given.ForEventSourceId(id).ReadModel(instance)`, or declare intentional absence
374
+ with `.NoReadModel<T>()`. Use the resolved read-model key; `Given` events do not
375
+ build read models, including passive ones. A synchronous handler throw surfaces
376
+ the handler's exception rather than a reflection wrapper, and the scenario's
377
+ `EventContext.Occurred` defaults to UTC.
378
+
379
+ Read [references/specifications.md](references/specifications.md) before writing
380
+ or reviewing reactor specifications; it shows both setup alternatives and the
381
+ scope of these scenario-only guards.
367
382
 
368
383
  ## Verify
369
384
 
@@ -1,3 +1,12 @@
1
+ # Reactor specifications
2
+
3
+ ## Contents
4
+
5
+ - Specifications
6
+ - Read-model setup — Chronicle 19.46.0+
7
+ - Exceptions and occurrence time — Chronicle 19.46.0+
8
+ - Contract coverage
9
+
1
10
  ## Specifications
2
11
 
3
12
  Start with `Specification` plus a direct call for pure reactor decisions,
@@ -9,6 +18,82 @@ non-event side effects; for handlers that return events, assert the resulting
9
18
  appends through the scenario's event store. Cover the replay path separately
10
19
  when the reactor has `[OnceOnly]` or `[Replay]` handlers.
11
20
 
21
+ ## Read-model setup — Chronicle 19.46.0+
22
+
23
+ Class-based `ReactorScenario<TReactor>` does not build read models from `Given`
24
+ events, **including passive models**. Seed a pre-built instance in `Establish()`:
25
+
26
+ ```csharp
27
+ void Establish() => _scenario.Given.ForEventSourceId(_id).ReadModel(_order);
28
+ async Task Because() => await _scenario.Given.ForEventSource(_id).Events(_orderPlaced);
29
+ ```
30
+
31
+ For a **separate absence specification**, use this setup instead, so an `Order?`
32
+ handler parameter deliberately receives `null`:
33
+
34
+ ```csharp
35
+ void Establish() => _scenario.Given.ForEventSourceId(_id).NoReadModel<Order>();
36
+ ```
37
+
38
+ The builders live in `Cratis.Chronicle.Testing.Reactors`. Exact signatures on
39
+ `ReactorSourceGivenBuilder<TReactor>` (`where TReactor : class, IReactor`):
40
+
41
+ ```csharp
42
+ public ReactorSourceGivenBuilder<TReactor> ReadModel<TReadModel>(TReadModel readModel)
43
+ where TReadModel : class;
44
+ public ReactorSourceGivenBuilder<TReactor> NoReadModel<TReadModel>()
45
+ where TReadModel : class;
46
+ public Task Events(params object[] events);
47
+ ```
48
+
49
+ Both `Given.ForEventSource(EventSourceId)` and
50
+ `Given.ForEventSourceId(EventSourceId)` return this builder. Seed or declare
51
+ absence using the triggering source id by default, or the key returned by
52
+ `ICanResolveReadModelKey.Resolve` when implemented. Declarations are shared
53
+ across the scenario's builders, match the **exact type and key**, and do not
54
+ remove a seeded instance. Do not declare absence to hide forgotten setup.
55
+
56
+ If a recognized read-model parameter resolves to `null` without declared
57
+ absence, `Events(...)` throws `ReadModelNotSeededForReactorScenario` from
58
+ `Cratis.Chronicle.Testing.Reactors`, **even for nullable parameters**. Its message
59
+ names the full read-model type and key, tells you to seed via `ReadModel(...)`
60
+ or declare `NoReadModel<T>()`, and lists other seeded keys for that type when
61
+ any exist. Check the key first if the fixture already seeds that type. This
62
+ guard applies only when the scenario uses `EventStoreForTesting` (including an
63
+ explicit instance); an explicitly supplied store of another type and production
64
+ read-model resolution are unchanged.
65
+
66
+ ## Exceptions and occurrence time — Chronicle 19.46.0+
67
+
68
+ `Events(...)` rethrows a synchronously throwing handler's **own exception**,
69
+ unwrapping the `TargetInvocationException` previously surfaced by reflection,
70
+ and preserves the handler stack. Capture with
71
+ `await Catch.Exception(() => _scenario.Given.ForEventSource(_id).Events(_orderPlaced))`
72
+ and assert the handler's exception type, not a reflection wrapper.
73
+
74
+ `EventContext.From(...)` defaults omitted `occurred` to `DateTimeOffset.UtcNow`
75
+ (previously `DateTimeOffset.Now`). `ReactorScenario` uses this helper, as do
76
+ `EventContext.Empty` and `EmptyWithEventSourceId(...)`. Explicit timestamps keep
77
+ their supplied offset; the positional constructor does not default `Occurred`.
78
+ For deterministic time-dependent plain-call specs, supply a fixed occurrence
79
+ time rather than asserting an exact current instant.
80
+
81
+ Verified against **Chronicle tag `v19.46.0`**:
82
+
83
+ - `Source/Clients/Testing/Reactors/ReactorSourceGivenBuilder.cs` and
84
+ `ReactorScenarioGivenBuilder.cs`: seeding, absence and builder signatures.
85
+ - `Source/Clients/Testing/Reactors/ScenarioReactorMethodArgumentsResolver.cs` and
86
+ `ReadModelNotSeededForReactorScenario.cs`: guard scope, resolved key and message.
87
+ - `Source/Clients/Testing/Reactors/ReactorScenario.cs`: synchronous exception
88
+ unwrapping and context creation.
89
+ - `Source/Clients/DotNET/Events/EventContext.cs`: UTC default and explicit time.
90
+ - `Source/Clients/Testing.Specs/Reactors/for_ReactorScenario/when_resolving_read_model_dependencies/`:
91
+ seeded, absent, nullable, passive, custom-key and explicit-store examples.
92
+ - `Source/Clients/Testing.Specs/Reactors/for_ReactorScenario/when_a_reactor_handler_throws_synchronously.cs`
93
+ and `when_a_reactor_reads_the_occurrence_time.cs`: handler exception and UTC specs.
94
+
95
+ ## Contract coverage
96
+
12
97
  Specify the contract, not only the happy path:
13
98
 
14
99
  - **Field lineage.** For each returned command or event, a specification asserts
@@ -22,6 +22,11 @@ This skill is verified against these exact public releases:
22
22
  | `Cratis.Arc.Testing` | `22.16.0` | `CommandScenario<TCommand>` and `CommandResult` assertions |
23
23
  | `Cratis.Chronicle.Testing` | `18.3.0` | `EventScenario`, `ReadModelScenario<T>`, `ReactorScenario<T>` |
24
24
 
25
+ Reactor read-model seeding/absence, synchronous handler exceptions and the
26
+ `EventContext.From` UTC default are verified separately at **Chronicle 19.46.0**
27
+ and require **19.46.0+**; see the ReactorScenario section in
28
+ [application-scenarios.md](references/application-scenarios.md).
29
+
25
30
  Generated-value scenario pinning is verified separately against
26
31
  `Cratis.Arc.Testing` **22.68.0** (introduced in **22.67.0**).
27
32
 
@@ -83,6 +88,14 @@ never produces a concurrency violation. `ShouldHaveConcurrencyViolations()`
83
88
  cannot pass for such an append and its negation passes vacuously — specify concurrency against the
84
89
  real kernel with an out-of-process integration specification instead.
85
90
 
91
+ For a class-based `ReactorScenario<TReactor>` on **Chronicle 19.46.0+**, seed each
92
+ handler read-model parameter with `Given.ForEventSourceId(id).ReadModel(instance)`
93
+ or declare intentional absence with `.NoReadModel<T>()`; `Given` events do not
94
+ build it, even if passive. Missing setup throws
95
+ `ReadModelNotSeededForReactorScenario` even for a nullable parameter. Use the
96
+ resolved read-model key. See the ReactorScenario reference for diagnostics,
97
+ exception capture and UTC occurrence-time defaults.
98
+
86
99
  Specification projects are named `<Source>.Specs` and run on xUnit.
87
100
 
88
101
  ### Pin server-generated Handle values
@@ -262,6 +262,47 @@ async Task Because() =>
262
262
  Construct with an `IServiceProvider` of substituted collaborators and assert on
263
263
  those substitutes after `Given` fires the events.
264
264
 
265
+ ### Read-model parameters — Chronicle 19.46.0+
266
+
267
+ `Given` events invoke the reactor; they **do not build read models**, including
268
+ passive ones. In the present-state specification, seed a pre-built instance:
269
+
270
+ ```csharp
271
+ void Establish() => _scenario.Given.ForEventSourceId(_id).ReadModel(_author);
272
+ ```
273
+
274
+ In a **separate absence specification**, deliberately let an `Author?` handler
275
+ parameter receive `null`:
276
+
277
+ ```csharp
278
+ void Establish() => _scenario.Given.ForEventSourceId(_id).NoReadModel<Author>();
279
+ ```
280
+
281
+ Both return `ReactorSourceGivenBuilder<TReactor>` for chaining, not a `Task`.
282
+ Use the triggering source id by default or `ICanResolveReadModelKey.Resolve`'s
283
+ key when implemented. Absence matches the exact type and key and does not remove
284
+ a seeded instance. Missing setup throws `ReadModelNotSeededForReactorScenario`
285
+ (`Cratis.Chronicle.Testing.Reactors`), **even for nullable parameters**. Its
286
+ message names the type/key, explains both setups, and lists other seeded keys
287
+ for that type when any exist. Fix the fixture/key rather than hiding missing
288
+ setup with `NoReadModel<T>()`. This guard applies to `EventStoreForTesting`, not
289
+ production resolution or an explicitly supplied store of another type.
290
+
291
+ Also in **Chronicle 19.46.0+**, a synchronous handler throw surfaces its own
292
+ exception, not reflection's `TargetInvocationException`, preserving the handler
293
+ stack. Capture with `await Catch.Exception(() => _scenario.Given.ForEventSource(_id).Events(...))`
294
+ and assert the handler's exception type.
295
+
296
+ `EventContext.From(...)` defaults omitted `occurred` to `DateTimeOffset.UtcNow`,
297
+ previously local `Now`. Scenario contexts, `EventContext.Empty` and
298
+ `EmptyWithEventSourceId(...)` use this helper; explicit times retain their offset,
299
+ and the positional constructor has no such default.
300
+
301
+ Sources at **Chronicle tag `v19.46.0`**:
302
+ `Source/Clients/Testing/Reactors/{ReactorScenario,ReactorSourceGivenBuilder,ScenarioReactorMethodArgumentsResolver,ReadModelNotSeededForReactorScenario}.cs`
303
+ and `Source/Clients/DotNET/Events/EventContext.cs`. For exact signatures and
304
+ source-backed examples, read `cratis-chronicle-reactor/references/specifications.md`.
305
+
265
306
  ## Application conventions
266
307
 
267
308
  - **Use a per-specification fixture value for anything under a shared uniqueness
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/pi",
3
- "version": "7.28.0",
3
+ "version": "7.28.1",
4
4
  "description": "Configuration-aware Cratis AI integration for Pi",
5
5
  "type": "module",
6
6
  "files": [