git:20261012.1578108 to git:20261013.ad33eca

27 added, 0 removed. Audit A to A.

---
name: cratis-application-slice-specifications
description: Specify the backend behavior of an event-sourced Cratis application slice — plain-call specs for pure decisions, plus CommandScenario, EventScenario, ReadModelScenario and ReactorScenario where the pipeline, validation, constraints or projections matter — including what to cover for each slice type and where the specification files live. Use when adding or changing backend behavior in an application built on Cratis. Do not use for framework library specifications and do not use for frontend behavior.
license: MIT
metadata:
cratis-hint-paths: "**/when_*/**/*.cs **/for_*/**/*.cs"
---
# Cratis application slice specifications
Start a pure decision with a plain method call: `Handle()`,
`Handle(providedValue)`, reducer logic or a reactor's decision needs no host,
services or harness. Add a scenario where the pipeline, validation,
authorization, constraints or projections contribute proof. The scenario family
exercises the real Arc and Chronicle code paths in-process — no HTTP, no fixture,
no server.
This skill decides **when to add a scenario** and **what to cover**. The C# mechanics
(`Establish`/`Because`, substitutes, contexts) live in
`cratis-specifications-csharp`.
## Verified product sources
| Package | Version | Purpose |
| --- | --- | --- |
| `Cratis.Arc.Testing` | `22.75.0` | `CommandScenario<TCommand>` and the `CommandResult` assertions |
| `Cratis.Arc.Chronicle.Testing` | `22.75.0` | `Given`, `EventLog`, `AppendedEvents` and the appended-event assertions on a command scenario |
| `Cratis.Chronicle.Testing` | `18.3.0` | `EventScenario`, `ReadModelScenario<T>`, `ReactorScenario<T>` |
| `Cratis.Specifications.XUnit` | `4.x` | The `Specification` base and the `ShouldXxx` assertions |
> Arc command/query/decision scenario surface rechecked at v22.75.0. Arc depends
> on Chronicle 19.39.1; later Chronicle reactor setup below has its own minimum
> version and is not downgraded by this Arc verification.
Reactor read-model setup and the `EventContext.From` UTC default below are
verified separately at **Chronicle 19.46.0**, the minimum version for that
guidance; see `cratis-chronicle-reactor/references/specifications.md` for sources.
`Cratis.Testing` is the meta-package that brings `Cratis.Arc.Testing` and
`Cratis.Arc.Chronicle.Testing` together. Reverify against the owning product
repository before claiming support for another version.
## Route near misses
- The repository builds a Cratis **library** rather than an application: use
`cratis-specifications-csharp` and stay on the plain `Specification` base.
- React, TypeScript, or a view model: use
`cratis-application-react-specifications`.
- The subject is append semantics or a constraint on its own: use
`cratis-chronicle-event-specifications`.
- The subject is a projection or reducer on its own: use
`cratis-chronicle-read-model-specifications`.
- The question is what the slice *should do*: settle the behavior first. A
specification records a decision; it does not make one.
- The scope is model-first (the `specification` blocks of the `.play` slice are
the contract for this skill). Decide this with the decision rule in
`cratis-screenplay-modeling-lifecycle`: model-first only when an accepted
model under the model root covers this scope, or the repository is opted in
(the root holds a committed `.play` file (`git ls-tree -r --name-only HEAD` lists a `.play` file there, narrowed to `-- <root>` when a root is configured), or the project set `mcpServers.screenplay.root`
in `.cratis/ai.json`); an empty directory, install output, a `.play` file
outside the root or an uncommitted draft does not count, and framework, infrastructure, client, adapter and not-opted-in
brownfield work stays code-first. Opted in but no model for this scope: model
it first (`cratis-screenplay-discovery`, then `cratis-screenplay-slice-design`)
before writing specifications. If those Screenplay skills are not installed,
say so and stop; never author `.play` from memory. Never edit Stage-managed
output, and never change a model specification to match existing code. Author or change model
specifications with `cratis-screenplay-specifications` and
`cratis-screenplay-scenario-coverage`; check finished code against the model
with `cratis-application-slice-conformance`.
## Step 0 — Take the list from the contract
When the slice has a contract — a `.play` slice with its `specification`
blocks, or an agreed outline — its specifications are the **minimum list**: each
one becomes a specification class here, named after it. Steps 3–7 then add what
the code reveals (each validator rule, each constraint). A rule found in the code
with no contract specification is a proposal for the contract (the `.play` model when one covers the slice, otherwise the agreed outline), not silent coverage:
list it in the report and, where a model exists, route it to
`cratis-screenplay-scenario-coverage`.
Without a contract (code-first scope), derive the list from Step 3 and say that
you did.
+ ### Reading a `.play` specification
+
+ Read the actual assignments, not a prose retelling; syntax and supported forms belong to
+ `cratis-screenplay-specifications`.
+ - `given <Event>` becomes prior facts seeded with `Given.ForEventSource(...).Events(...)`.
+ `given readmodel <Type>` becomes a pinned complete `.ReadModel(...)` instance, including its
+ key: the contract deliberately makes event history irrelevant here. Protected decisions still
+ need event-backed guards (Step 5); report a pin that cannot witness the required enforcement.
+ - `when <Command>` assignments fill that command; `given caller` supplies the scenario's caller,
+ not an invented default role. Use the matching scenario for append, query or reaction actions.
+ - `then <Event>` assignments become the appended-event predicate;
+ `then readmodel` assignments become instance assertions. Assert every named value and honor
+ `exactly`, ordering and absence where stated. Do not assert an unmentioned generated identifier
+ or timestamp; still use required source identities to select the result. A broader property
+ coverage gap goes back to the contract owner rather than adding a new business assertion.
+ - `description` and slice `documentation` provide context for a name or XML summary, not extra
+ setup, assertions or inferred rules. Explicit realization requirements still bind delivery;
+ reconcile them with executable parts through `cratis-application-slice-conformance`.
+ - A `parameter`/`case` table yields independent examples by substituting each `case.<parameter>`
+ value. The reference runner identifies each row as `<Specification>_<Case>`; retain that name
+ in the specification map, while C# uses the repository's `when_.../and_<case>` convention.
+ Name parity is a mapping requirement, not a requirement to rename C# classes.
+ - `then error` requires unsuccessful execution plus the named validation/constraint reason where
+ stated (Step 6). A specified message must be checked, using equality when exact wording is
+ contracted. `then denied` instead requires unsuccessful execution and `ShouldNotBeAuthorized()`;
+ it is not a validation error. Keep every other rule satisfied so the named refusal is witnessed.
+
## Step 1 — Place the files
Specifications live **in the slice folder**, beside the `.cs` they specify, each
file wrapped in `#if DEBUG … #endif` when it compiles into the application
assembly, so specification code ships only in Debug. A dedicated `<Source>.Specs`
project is not shipped and needs no wrapper.
```
<Feature>/<Slice>/
├── <Slice>.cs
└── when_<verb_phrase>/
├── and_<happy_scenario>.cs
└── and_<failure_scenario>.cs
```
- Folder: `when_<verb_phrase>` — `when_registering`.
- File: `and_<condition>.cs` — `and_name_is_unique.cs`, `and_name_already_exists.cs`.
- Method: `should_<expected_result>`.
A behavior covering several subjects groups under
`<Slice>/for_<Subject>/when_<behavior>/`.
## Step 2 — Start with the decision; add the scenario by slice type
Specify a pure decision with the plain `Specification` base first. A direct
`Handle()` call does not run validation, authorization, `Provide()` or operations;
keep a scenario for that wiring and for appended-event behavior.
| Slice type | Scenario | Skill |
| --- | --- | --- |
| Pure command decision, reducer logic or reactor decision | Plain method call with `Specification` (default) | `cratis-specifications-csharp` |
| State Change — command pipeline and appended events | `CommandScenario<TCommand>` | this one |
| Append semantics, a constraint | `EventScenario` | `cratis-chronicle-event-specifications` |
| Query pipeline — arguments, authorization, validation, interceptors | `QueryScenario<TReadModel>` | [query and decision scenarios](references/query-and-decision-scenarios.md) |
| State View — a projection or reducer | `ReadModelScenario<TReadModel>` | `cratis-chronicle-read-model-specifications` |
| Automation or Translation — a reactor | `ReactorScenario<TReactor>` | `cratis-specifications-csharp` |
| A host, transport, or real-infrastructure boundary | Out-of-process integration | `cratis-specifications-csharp` |
For a class-based reactor scenario on **Chronicle 19.46.0+**, explicitly seed
handler read-model parameters with `Given.ForEventSourceId(id).ReadModel(instance)`
or declare intentional absence with `.NoReadModel<T>()`. An unseeded model throws
`ReadModelNotSeededForReactorScenario` even if nullable; `Given` events do not
build read models, including passive ones. Use the reactor's resolved key, not
necessarily the trigger's source id. This is **not** the command-scenario event
seeding described in Step 5. Read `cratis-chronicle-reactor` for the setup examples,
exception diagnostics, synchronous handler throws and UTC context defaults.
## Step 3 — Cover every outcome of a State Change slice
One specification class for **each** of:
1. **The happy path** — the command succeeds and the expected event is appended.
2. **Each validation failure** — one `and_` class per `CommandValidator` or
`ConceptValidator` rule.
3. **Each business-rule rejection** — one `and_` class per condition in
`Handle()` that inspects a read model.
4. **Each constraint violation** — one class per constraint, written with
`EventScenario`, because a constraint is enforced at the append and not by
the command.
5. **Repeat execution**, when a reactor invokes the command: the same command
twice for the same source. Specify whether the second is rejected, a no-op, or
appends again — recovery can re-deliver an event, so the answer must be a
decision, not an accident.
**Use the contract's example values.** When the contract gives concrete values
(the `when` and `then` values of a `.play` specification), use them, so the code
specification and the contract describe the same example. Where isolation needs
it, substitute a fresh identifier or uniqueness value (see *What breaks*) and keep
the example's relationships and behavior. Invent values only where the contract
is silent.
## Step 4 — `CommandScenario<TCommand>`
It runs authorization, the validators, `Provide()` and `Handle()` in-process and
exposes what was appended.
```csharp
#if DEBUG
namespace MyApp.Projects.Registration.when_registering;
public class and_name_is_unique : Specification
{
readonly CommandScenario<RegisterProject> _scenario = new();
readonly ProjectId _id = ProjectId.New();
CommandResult _result;
async Task Because() => _result = await _scenario.Execute(new RegisterProject(_id, "My Project"));
[Fact] void should_succeed() => _result.ShouldBeSuccessful();
[Fact] async Task should_have_appended_the_registered_event() =>
await _scenario.ShouldHaveAppendedEvent<RegisterProject, ProjectRegistered>(_id, e => e.Name == "My Project");
}
#endif
```
The scenario's own surface is `Services`, `Context`, `Execute(command[, ct])`,
`Validate(command)` (runs the filters without the handler), `LastResult`,
`Operations` and the three operation assertions
(`ShouldHaveExecutedOperation<T>()`, `ShouldHaveCompensatedOperation<T>()`,
`ShouldHaveNoOperationInvocations()`); it is `IDisposable`/`IAsyncDisposable`.
Seeding (`Given`), the event log and the event assertions arrive as extension
members from the Chronicle integration.
## Step 5 — Seed prior state through `Given`
With `Cratis.Arc.Chronicle.Testing` referenced, a command scenario has a
Chronicle-backed `Given` builder, plus `EventScenario`, `EventLog`,
`EventSequence` and `AppendedEvents`.
```csharp
// Seed the events the world already contains. A read model the command injects
// for this event source is materialized from them by its own projection or
// reducer — no read-model type is named.
_scenario.Given.ForEventSource(_cartId).Events(new ItemAddedToCart(itemId));
// Or pin a specific read-model value directly, when the events are beside the point.
_scenario.Given.ForEventSource(_cartId).ReadModel(new CartSummary { ItemCount = 3 });
```
Prefer seeding **events**: it exercises the projection that production relies on,
so a specification cannot pass against a read-model shape the projection never
produces. Pin an instance only when the events would be noise.
Both builder methods return `void`; there is nothing to await.
That is **legacy** read-model mode: seeded events populate an in-memory model,
not the append log. For protected or stream decisions call `UseDecisionReads()`
before seeding; actual events and decisions then share one in-process log.
Pinned models cannot supply decision guards. Read
[query and decision scenarios](references/query-and-decision-scenarios.md) when
specifying query pipelines, routed conflicts, projection admission or completion.
Validator and `Provide()` dependencies are registered in `_scenario.Services`;
the concrete validator itself is discovered automatically.
## Step 6 — Assert twice on every unhappy path
```csharp
[Fact] void should_not_succeed() => _result.ShouldNotBeSuccessful();
[Fact] void should_have_validation_errors() => _result.ShouldHaveValidationErrors();
```
`ShouldNotBeSuccessful()` alone cannot distinguish a validation rejection from an
unhandled exception, so both facts are required. **Do not assert on a message
string by default** — it is presentation text; assert one only when that exact
text is the specified behavior, and name that requirement in the fact. A `.play`
specification may pin the message (`then error "<message>"`); prefer pinning the
*kind* in C# unless the exact wording is part of the behavior being specified.
**Isolate the rule.** Each rejection specification violates **only** the rule it
is named after: build the command valid in every other respect. Otherwise
`ShouldHaveValidationErrors()` passes when a neighbouring rule fires. Use
`ShouldHaveValidationErrorBecauseOf(reason)` where the reason distinguishes the
rule, and always run the "remove the rule, see it go red" check under *How it is
proven*.
`CommandResult` assertions, from `Cratis.Arc.Testing.Commands`, throwing
`CommandResultAssertionException`:
| Assertion | Says |
| --- | --- |
| `ShouldBeSuccessful()` / `ShouldNotBeSuccessful()` | Authorized, valid, no exceptions — or not |
| `ShouldBeValid()` | Validation only; it checks neither authorization nor exceptions |
| `ShouldHaveValidationErrors()` | At least one validation error |
| `ShouldHaveValidationErrorFor(message)` | A validation error whose message **contains** that text (a substring match; compare messages for equality when exact wording is specified) |
| `ShouldHaveValidationErrorBecauseOf(reason)` | A validation error with that `ValidationResultReason` |
| `ShouldBeAuthorized()` / `ShouldNotBeAuthorized()` | The authorization outcome |
| `ShouldHaveExceptions()` / `ShouldNotHaveExceptions()` | Whether the handler threw |
| `ShouldHaveConstraintViolationFor(name)` | A constraint violation surfaced on the command result |
`ShouldHaveValidationErrorBecauseOf(reason)` is the message-free way to say
*which kind* of rejection happened — `ValidationResultReason.ConstraintViolation`,
`ConcurrencyViolation`, `ValidatorFailed`, `Rule`. Prefer it over
`ShouldHaveValidationErrorFor(message)`, which pins presentation text.
Authorization failures are different in kind: an unauthorized result carries
**no** validation errors, so assert `ShouldNotBeAuthorized()`. Adding an
authorization attribute to an existing command therefore breaks both its
happy-path and its validation-failure specifications — switch the assertions
rather than patching around them.
## Step 7 — Assert what was appended
The appended-event assertions come from `Cratis.Arc.Chronicle.Testing` and are
keyed by **command and event type**. They return `Task`, so the fact is
`async Task`, and they throw `EventSequenceAssertionException`.
- `ShouldHaveAppendedEvent<TCommand, TEvent>(eventSourceId)`
- `ShouldHaveAppendedEvent<TCommand, TEvent>(eventSourceId, Func<TEvent, bool> predicate)`
- `ShouldHaveTailSequenceNumber<TCommand>(sequenceNumber)`
⚠️ On a command scenario the second argument is a **predicate**
(`Func<TEvent, bool>`), not an assertion callback. The `Action<TEvent>` validator
shape lives on other receivers — the `IEventSequence` extensions in
`Cratis.Chronicle.Testing`, `AppendedEventWithResult.ShouldHaveEvent<TEvent>(Action<TEvent>?)`,
and the `IChronicleSetupFixture` extension in `Cratis.Chronicle.XUnit.Integration`
— never on `CommandScenario`.
Sequence numbers are **zero-based**: the first event is `0`, and the tail after a
single append is `0`, never `1`.
## Lineage
Step 0, repeat execution and rule isolation are adapted in our own words from
ideas recorded in `references/provenance.md`. Read `references/provenance.md` when checking attribution or the origin of Step 0, repeat-execution coverage, and rule isolation.
## What breaks
- **A specification passes alone and fails in the suite.** A hard-coded value
collided with another specification's uniqueness claim. Use a per-specification
value — a fresh `Guid`, or `Guid.NewGuid().ToString("N")[..9]`. Adding
`[Collection(…)]` is the wrong fix here.
- **`Given` does not resolve.** `Cratis.Arc.Chronicle.Testing` (or the
`Cratis.Testing` meta-package) is not referenced. It is an extension member,
not part of `CommandScenario` itself.
- **A DCB rejection specification is green for the wrong reason.** The read model
was never seeded, so the handler saw a default or `null` instance and rejected
on that instead of on the rule. Seed, then confirm the specification fails when
the seeding is removed.
- **A rejection specification stays green when its rule is deleted.** The
command was invalid in a second way, so another rule produced the error. Make
the command valid except for the one rule, then repeat the red check.
- **The command scenario reports success but nothing was appended.** The
assertion is on `CommandResult` only; add a
`ShouldHaveAppendedEvent<TCommand, TEvent>` fact.
- **Injected-validator branches flip with test ordering.** This was observed in
the 22.16-era harness. Arc 22.23.2 stopped replacing the process-wide provider;
later releases isolated stores and event-source services per scenario. Reproduce
at 22.75.0 before applying the old direct-validator/small `[Collection]`
workaround; no current reproduction was verified here.
## How it is proven
`dotnet build -c Debug` — it compiles the `#if DEBUG` specification code — then
`dotnet test`, both clean, with a specification for the happy path and one per
rejection. Before trusting a rejection specification green, remove the condition
that causes the rejection once and confirm it goes red.