@cratis/pi 7.36.1 → 7.38.0

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.
Files changed (96) hide show
  1. package/package/corpus/harnesses/copilot/instructions/ai-distribution.instructions.md +109 -0
  2. package/package/corpus/harnesses/copilot/instructions/application-profile.instructions.md +81 -0
  3. package/package/corpus/harnesses/copilot/instructions/capability-is-not-authority.instructions.md +45 -0
  4. package/package/corpus/harnesses/copilot/instructions/code-quality.csharp.instructions.md +92 -0
  5. package/package/corpus/harnesses/copilot/instructions/code-quality.instructions.md +83 -0
  6. package/package/corpus/harnesses/copilot/instructions/code-quality.typescript.instructions.md +90 -0
  7. package/package/corpus/harnesses/copilot/instructions/components.instructions.md +300 -0
  8. package/package/corpus/harnesses/copilot/instructions/concepts.instructions.md +138 -0
  9. package/package/corpus/harnesses/copilot/instructions/cratis-folder-is-configuration.instructions.md +29 -0
  10. package/package/corpus/harnesses/copilot/instructions/csharp.instructions.md +357 -0
  11. package/package/corpus/harnesses/copilot/instructions/dialogs.instructions.md +278 -0
  12. package/package/corpus/harnesses/copilot/instructions/documentation-structure-and-formatting.instructions.md +162 -0
  13. package/package/corpus/harnesses/copilot/instructions/documentation.instructions.md +90 -0
  14. package/package/corpus/harnesses/copilot/instructions/editing-cratis-docs.instructions.md +74 -0
  15. package/package/corpus/harnesses/copilot/instructions/efcore.instructions.md +84 -0
  16. package/package/corpus/harnesses/copilot/instructions/efcore.specs.instructions.md +45 -0
  17. package/package/corpus/harnesses/copilot/instructions/elixir.instructions.md +50 -0
  18. package/package/corpus/harnesses/copilot/instructions/exit-codes-and-wrappers.instructions.md +34 -0
  19. package/package/corpus/harnesses/copilot/instructions/framework.instructions.md +55 -0
  20. package/package/corpus/harnesses/copilot/instructions/frontend-quality.instructions.md +60 -0
  21. package/package/corpus/harnesses/copilot/instructions/frontend-testing.instructions.md +127 -0
  22. package/package/corpus/harnesses/copilot/instructions/git-commits.instructions.md +139 -0
  23. package/package/corpus/harnesses/copilot/instructions/github-actions.instructions.md +87 -0
  24. package/package/corpus/harnesses/copilot/instructions/glossary.instructions.md +63 -0
  25. package/package/corpus/harnesses/copilot/instructions/go-engineering.instructions.md +61 -0
  26. package/package/corpus/harnesses/copilot/instructions/go.instructions.md +116 -0
  27. package/package/corpus/harnesses/copilot/instructions/guards-and-fuses.instructions.md +61 -0
  28. package/package/corpus/harnesses/copilot/instructions/java.instructions.md +138 -0
  29. package/package/corpus/harnesses/copilot/instructions/kotlin.instructions.md +175 -0
  30. package/package/corpus/harnesses/copilot/instructions/local-work-artifacts.instructions.md +56 -0
  31. package/package/corpus/harnesses/copilot/instructions/new-repository-intake.instructions.md +35 -0
  32. package/package/corpus/harnesses/copilot/instructions/orleans.instructions.md +97 -0
  33. package/package/corpus/harnesses/copilot/instructions/profiles.instructions.md +403 -0
  34. package/package/corpus/harnesses/copilot/instructions/pull-requests.instructions.md +176 -0
  35. package/package/corpus/harnesses/copilot/instructions/react.instructions.md +203 -0
  36. package/package/corpus/harnesses/copilot/instructions/reactors.instructions.md +336 -0
  37. package/package/corpus/harnesses/copilot/instructions/rtk.instructions.md +48 -0
  38. package/package/corpus/harnesses/copilot/instructions/specs.csharp.instructions.md +147 -0
  39. package/package/corpus/harnesses/copilot/instructions/specs.instructions.md +133 -0
  40. package/package/corpus/harnesses/copilot/instructions/specs.scenarios.csharp.instructions.md +281 -0
  41. package/package/corpus/harnesses/copilot/instructions/specs.typescript.instructions.md +141 -0
  42. package/package/corpus/harnesses/copilot/instructions/storybook.instructions.md +85 -0
  43. package/package/corpus/harnesses/copilot/instructions/typescript.instructions.md +151 -0
  44. package/package/corpus/harnesses/copilot/instructions/verification-discipline.instructions.md +27 -0
  45. package/package/corpus/harnesses/copilot/instructions/vertical-slices.instructions.md +414 -0
  46. package/package/corpus/harnesses/copilot/instructions/web-fetching.instructions.md +12 -0
  47. package/package/corpus/harnesses/copilot/instructions/writing-correct-examples.instructions.md +57 -0
  48. package/package/corpus/harnesses/copilot/instructions/writing-cratis-docs.instructions.md +72 -0
  49. package/package/corpus/hooks/scripts/cratis-nuget-pins.txt +6 -0
  50. package/package/corpus/product-sync.json +7 -3
  51. package/package/corpus/rules/orleans.md +4 -0
  52. package/package/corpus/rules/profiles.md +3 -3
  53. package/package/corpus/skills/cratis-arc-observable-query-http/SKILL.md +17 -0
  54. package/package/corpus/skills/cratis-arc-observable-query-http/verification.json +12 -0
  55. package/package/corpus/skills/cratis-arc-react-page/SKILL.md +5 -1
  56. package/package/corpus/skills/cratis-arc-react-page/verification.json +3 -0
  57. package/package/corpus/skills/cratis-chronicle-client-kotlin/SKILL.md +2 -2
  58. package/package/corpus/skills/cratis-components-common-controls/SKILL.md +149 -0
  59. package/package/corpus/skills/cratis-components-common-controls/verification.json +15 -0
  60. package/package/corpus/skills/cratis-components-data-surfaces/SKILL.md +185 -0
  61. package/package/corpus/skills/cratis-components-data-surfaces/verification.json +16 -0
  62. package/package/corpus/skills/cratis-fundamentals-telemetry/SKILL.md +125 -0
  63. package/package/corpus/skills/cratis-fundamentals-telemetry/verification.json +15 -0
  64. package/package/corpus/skills/cratis-orleans-jobs/SKILL.md +115 -0
  65. package/package/corpus/skills/cratis-orleans-jobs/references/state-machines.md +64 -0
  66. package/package/corpus/skills/cratis-orleans-jobs/verification.json +20 -0
  67. package/package/corpus/skills/cratis-screenplay-discovery/SKILL.md +11 -6
  68. package/package/corpus/skills/cratis-screenplay-discovery/references/facilitation.md +9 -5
  69. package/package/corpus/skills/cratis-screenplay-discovery/references/human-workshop.md +1 -1
  70. package/package/corpus/skills/cratis-screenplay-discovery/references/plotting-and-handoff.md +28 -4
  71. package/package/corpus/skills/cratis-screenplay-discovery/references/storming-loop.md +2 -1
  72. package/package/corpus/skills/cratis-screenplay-discovery/verification.json +5 -0
  73. package/package/corpus/skills/cratis-screenplay-model-authoring/SKILL.md +23 -29
  74. package/package/corpus/skills/cratis-screenplay-model-authoring/references/mcp-loop.md +15 -1
  75. package/package/corpus/skills/cratis-screenplay-model-authoring/references/model-notes.md +208 -0
  76. package/package/corpus/skills/cratis-screenplay-model-authoring/verification.json +20 -1
  77. package/package/corpus/skills/cratis-screenplay-model-review/SKILL.md +19 -36
  78. package/package/corpus/skills/cratis-screenplay-model-review/references/report-template.md +26 -0
  79. package/package/corpus/skills/cratis-screenplay-model-review/verification.json +7 -0
  80. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/SKILL.md +14 -16
  81. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/core-rules-digest.md +8 -3
  82. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/handoff-template.md +5 -2
  83. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/intake.md +21 -0
  84. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/reasoning-notes.md +10 -6
  85. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/stop-or-assume.md +32 -1
  86. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/references/worked-example.md +5 -2
  87. package/package/corpus/skills/cratis-screenplay-modeling-lifecycle/verification.json +5 -0
  88. package/package/corpus/skills/cratis-screenplay-toolchain/SKILL.md +4 -0
  89. package/package/corpus/skills/cratis-screenplay-toolchain/references/screenplay-contract.json +1716 -183
  90. package/package/corpus/skills/cratis-screenplay-toolchain/references/screenplay-contract.version +1 -1
  91. package/package/corpus/skills/cratis-screenplay-toolchain/references/versions.md +40 -4
  92. package/package/corpus/skills/cratis-screenplay-toolchain/verification.json +6 -0
  93. package/package/corpus/skills/cratis-stage-rendering-and-sandbox/references/sandbox-and-specrunner.md +26 -0
  94. package/package/corpus/skills/cratis-stage-rendering-and-sandbox/verification.json +3 -0
  95. package/package/profile-catalog.json +6 -2
  96. package/package.json +1 -1
@@ -0,0 +1,133 @@
1
+ ---
2
+ applyTo: "**/for_*/**/*.*, **/when_*/**/*.*"
3
+ paths:
4
+ - "**/for_*/**/*.*"
5
+ - "**/when_*/**/*.*"
6
+ ---
7
+ <!-- cratis-ai: generated Copilot instructions. Do not edit; rules/ is the source. -->
8
+
9
+ # How to Write Specs
10
+
11
+ We call automated tests **specs** (specifications), not tests. This is deliberate — specs are executable documentation that describe what the system *does*, written in the language of the domain. A new developer should be able to read the spec folder structure like a table of contents and understand the system's behavior without opening a single source file.
12
+
13
+ This philosophy comes from Specification by Example and BDD (Behavior Driven Development). The goal is human-readable, navigable specifications that double as a living contract.
14
+
15
+ ## Core Philosophy
16
+
17
+ - **Specify behaviors, not implementations.** A spec should verify what a method *promises from its signature* — its contract with callers. If the implementation changes but the contract holds, specs should still pass. When they don't, the spec was testing the wrong thing.
18
+ - **One behavior, one spec.** Every public method that performs an action gets its own `when_` folder or file. Never bundle multiple behaviors into one spec — it obscures what broke and why.
19
+ - **Specs are documentation first.** The folder tree, class names, and `should_` assertions form a specification anyone can read. Optimize for readability over DRY. A little repetition in setup is fine if it makes the spec self-contained and clear.
20
+ - **Do not test logging** — it is too fragile and provides no value. Don't test simple delegation or trivial getters either. The cost of maintaining these specs exceeds the value they provide.
21
+
22
+ ## Folder & File Structure
23
+
24
+ Specs mirror the source structure and read like a sentence when you trace the path: `for_Changeset / when_adding_changes / and_there_are_differences`. This is not accidental — the folder hierarchy *is* the specification. Every level adds context:
25
+
26
+ ```
27
+ for_<TypeUnderTest>/
28
+ ├── given/
29
+ │ ├── all_dependencies ← common DI/mock setup
30
+ │ └── a_<descriptive_name> ← reusable context
31
+ ├── when_<behavior>/ ← folder for behaviors with multiple outcomes
32
+ │ ├── given/ ← behavior-specific context (optional)
33
+ │ │ └── a_<specific_setup>
34
+ │ ├── and_<condition>.cs ← spec file for one outcome
35
+ │ ├── and_<condition>/ ← OR a sub-folder when that condition itself has multiple outcomes
36
+ │ │ ├── with_<data_state>.cs
37
+ │ │ └── without_<requirement>.cs
38
+ │ ├── with_<data_state>.cs
39
+ │ └── without_<requirement>.cs
40
+ └── when_<simple_behavior>.cs ← single file for single-outcome behaviors
41
+ ```
42
+
43
+ The `and_`, `with_`, `without_`, `having_`, `given_` prepositions work **both** as file names and as folder names. Use a folder when there are multiple outcomes under that condition; use a file when there is only one.
44
+
45
+ **Naming conventions — read them as English sentences:**
46
+ | Element | Pattern | Reads as... |
47
+ |---|---|---|
48
+ | Unit folder | `for_<ClassName>` | "For the Changeset..." |
49
+ | Behavior folder | `when_<action>` | "...when adding changes..." |
50
+ | Condition folder or spec file | Descriptive preposition | "...and there are differences" |
51
+ | Assertion | `should <expected result>` | "...it should return true" |
52
+
53
+ **Allowed prepositions for spec file/class names (and sub-folder names):**
54
+ - `and_*` — additional conditions or compound scenarios
55
+ - `with_*` / `without_*` — specific data or state present/absent
56
+ - `having_*` — possession or state-based conditions
57
+ - `given_*` — precondition scenarios
58
+
59
+ **Critical naming rule — never embed `when` in a spec file or folder name:**
60
+ `when` belongs **only** in `when_<behavior>` folder names. A spec file, spec class, or non-`when_` folder must **never** contain the word `when` anywhere in it. If the name starts with a preposition (`with_`, `and_`, etc.) but also contains `_when_` somewhere in the middle (e.g. `with_a_registered_migration_when_appending_a_generation_1_event`), you have two "whens" in the sentence — which is always wrong.
61
+
62
+ The fix is to fold the context into the `when_` folder name itself, then use preposition files/folders for the outcomes:
63
+
64
+ ```
65
+ # ❌ Wrong — double when in the sentence path
66
+ when_appending_event_with_migrations/
67
+ └── with_a_registered_migration_when_appending_a_generation_1_event.cs
68
+
69
+ # ❌ Still wrong — unnecessary extra level when a single flat file suffices
70
+ when_appending_event_with_migrations/
71
+ └── and_event_is_generation_1/
72
+ └── with_a_registered_migration.cs
73
+
74
+ # ✅ Correct — context baked into the when_ folder; outcomes are flat files
75
+ when_appending_event_with_registered_migration/
76
+ ├── and_event_is_generation_1.cs
77
+ ├── and_event_is_generation_2.cs
78
+ └── and_event_has_default_value.cs
79
+ ```
80
+
81
+ A sub-folder under `when_` is only needed when that condition has its **own** multiple outcomes that warrant further breakdown. If there is only one outcome per condition, use a flat file. This applies to **all languages** (C#, TypeScript, etc.).
82
+
83
+ ## What to Specify
84
+
85
+ The goal is to cover *decisions and transformations* — code where bugs hide. Simple plumbing that the compiler already validates is noise.
86
+
87
+ **Write specs for:**
88
+ - Public methods that perform actions (behaviors)
89
+ - Methods with branching logic or business rules
90
+ - Methods that coordinate between dependencies
91
+
92
+ **Do NOT write specs for:**
93
+ - Simple auto-properties (`public string Name { get; set; }`)
94
+ - Properties returning constructor parameters (`public Key Key => key;`)
95
+ - Simple delegation (`public IEnumerable<Property> Properties => mapper.Properties;`)
96
+ - Trivial null checks or basic validation without complex logic
97
+ - Getters returning injected dependencies
98
+
99
+ **Avoid file names starting with:** `when_getting_*`, `when_returning_*` — if a spec name starts with "getting" or "returning", it's probably testing a simple getter, which is not worth specifying.
100
+
101
+ ## Multiple Outcomes
102
+
103
+ Each distinct outcome deserves its own spec file. This keeps specs small, focused, and independently verifiable. When a spec fails, you immediately know *which* outcome broke — no debugging through a multi-assertion file.
104
+
105
+ - When a behavior has multiple outcomes, create a `when_<behavior>/` folder with separate files for each outcome.
106
+ - For simple behaviors with a single outcome, use a single file: `when_<behavior>`.
107
+ - Never write a single file that tests an entire class.
108
+
109
+ ## Reusable Context
110
+
111
+ Contexts capture the "given" part of a specification — the world as it exists before the action under test. They prevent duplicating setup across specs while keeping each spec readable.
112
+
113
+ - Place in `given/` folder within the unit folder.
114
+ - Name with `a_` or `an_` prefix (e.g. `an_observer`, `a_command_pipeline`). This reads naturally: "given an observer, when handling..."
115
+ - More specific contexts can use descriptive names (e.g. `two_queries`, `existing_query`).
116
+ - Context properties must be accessible to the specs that use them — see language-specific instructions for the exact access modifiers and naming conventions.
117
+ - Use the setup phase for context initialization — **never** put the action under test in a reusable context. The action under test belongs only in the concrete spec.
118
+ - Contexts can build on each other in layers: `all_dependencies → a_reactor_handler → when_handling`.
119
+ - Consider creating `all_dependencies` as a root context that mocks all common dependencies. This avoids duplicating mock creation across unrelated specs.
120
+
121
+ ## Formatting
122
+
123
+ - Keep assertions concise — prefer single-line assertions where the logic is readable.
124
+ - Don't add blank lines between related assertions for the same behavior.
125
+
126
+ ## Language-specific guides
127
+
128
+ This file is the shared base. For the concrete patterns:
129
+
130
+ - [specs.csharp.md](./specs.csharp.instructions.md) — C#: the universal `Cratis.Specifications` base + NSubstitute (both profiles; this is what framework/library specs use).
131
+ - [specs.scenarios.csharp.md](./specs.scenarios.csharp.instructions.md) — C# **application** profile: the in-process scenario family (`CommandScenario`, `EventScenario`, `ReadModelScenario`, `ReactorScenario`) + out-of-process Chronicle integration.
132
+ - [specs.typescript.md](./specs.typescript.instructions.md) — TypeScript framework-package specs (`given()` helper).
133
+ - [frontend-testing.md](./frontend-testing.instructions.md) — application frontend specs (view models, React components).
@@ -0,0 +1,281 @@
1
+ ---
2
+ applyTo: "**/for_*/**/*.cs, **/when_*/**/*.cs"
3
+ paths:
4
+ - "**/for_*/**/*.cs"
5
+ - "**/when_*/**/*.cs"
6
+ profile: application
7
+ ---
8
+ <!-- cratis-ai: generated Copilot instructions. Do not edit; rules/ is the source. -->
9
+
10
+ # Application Specs — the in-process scenario family
11
+
12
+ > **Application profile.** This file covers spec-writing for **event-sourced Cratis applications** — exercising commands, projections, reducers, reactors, and constraints with plain-call specs for pure decisions and the in-process scenario family where the pipeline matters. It builds on the universal [specs.csharp.md](./specs.csharp.instructions.md) (the `Specification` base + NSubstitute). **Framework / library** specs predominantly use that plain base instead; a framework repo reaches for a scenario helper here only when testing the very engine it provides — Arc tests its command pipeline with `CommandScenario`, Chronicle tests its event/projection/reactor engine with `EventScenario`/`ReadModelScenario`/`ReactorScenario`. That is the minority case, not the general framework testing mode.
13
+
14
+ Specs are **mandatory for every slice type**, including reactors.
15
+
16
+ ## Start with the plain call; add a scenario where it adds proof
17
+
18
+ A command's `Handle()` is an ordinary method; with dependencies resolved by `Provide()`, its decision can be a pure function of its arguments. Reducer logic and a reactor's decision can be pure functions too. **Write the plain call as the default spec for a pure decision**, with no host, services or harness. Add a scenario helper where the framework's composition is part of what you must prove: the command pipeline, validation, authorization, constraints, and projections (declarative, so only a scenario can exercise them). The scenario helpers are additive; use both only when the slice needs both kinds of proof. For application behavior prefer the in-process helpers over full out-of-process Chronicle host specs.
19
+
20
+ | Tool | Tests | Use when |
21
+ |---|---|---|
22
+ | `Specification` (plain call) | `Handle()`, `Handle(providedValue)`, reducer logic or a reactor's decision as a method call; or an isolated class with mocked collaborators | **default** for a decision that is a pure function of its arguments |
23
+ | `CommandScenario<TCommand>` | the real Arc command pipeline — authorization + validators + `Provide()` + `Handle()` + appended events | the pipeline, validation, authorization, or appended events matter (State Change slices) |
24
+ | `QueryScenario<TReadModel>` | query pipeline: argument conversion, authorization, validation, interceptors; `Observe` buffers emissions | query composition matters; real MongoDB observations still need infrastructure |
25
+ | `EventScenario` | Chronicle-level append semantics, no command pipeline | constraint violations, raw stream sequencing/concurrency |
26
+ | `ReadModelScenario<TReadModel>` | projection/reducer state from a sequence of events (auto-detects model-bound / fluent / reducer) | **default for projections**, which are declarative (State View slices) |
27
+ | `ReactorScenario<TReactor>` | reactor handler invocation + side effects via mocked services | the reactor wiring and side effects (Automation / Translation slices) |
28
+
29
+ > **Out-of-process Chronicle integration specs are an advanced case** — reserve them for host wiring, real infrastructure, serialization/transport, or end-to-end boundaries the scenario helpers can't exercise. They are not the default vertical-slice test shape.
30
+
31
+ **Wrap every spec file in `#if DEBUG … #endif` when the specs live in the application assembly** beside the slice, as vertical slices place them, so spec code ships only in Debug (the Debug build gate validates it and also regenerates proxies). A dedicated spec project (`<Source>.Specs`) is never shipped and needs no wrapper; that is the case Arc's documentation describes.
32
+
33
+ ### The plain call — the default for a pure decision
34
+
35
+ ```csharp
36
+ #if DEBUG
37
+ namespace MyApp.Authors.Registration.when_registering_an_author;
38
+
39
+ public class and_all_information_is_valid : Specification
40
+ {
41
+ readonly AuthorId _id = AuthorId.New();
42
+ AuthorRegistered _event;
43
+
44
+ void Because() => _event = new RegisterAuthor(_id, "Jane Austen").Handle();
45
+
46
+ [Fact] void should_register_the_author() => _event.Name.ShouldEqual("Jane Austen");
47
+ }
48
+ #endif
49
+ ```
50
+
51
+ Calling `Handle()` yourself runs no validation, authorization, `Provide()` or operations, so it proves the decision only. Add a `CommandScenario` spec for what the pipeline contributes.
52
+
53
+ ### `CommandScenario<TCommand>` — the command pipeline
54
+
55
+ Runs authorization, validators, `Provide()` (when present), and `Handle()` in-process, and exposes the appended events.
56
+
57
+ ```csharp
58
+ #if DEBUG
59
+ namespace MyApp.Authors.Registration.when_registering_an_author;
60
+
61
+ public class and_all_information_is_valid : Specification
62
+ {
63
+ readonly CommandScenario<RegisterAuthor> _scenario = new();
64
+ readonly AuthorId _id = AuthorId.New();
65
+ CommandResult _result;
66
+
67
+ async Task Because() => _result = await _scenario.Execute(new RegisterAuthor(_id, "Jane Austen"));
68
+
69
+ [Fact] void should_succeed() => _result.ShouldBeSuccessful();
70
+ [Fact] async Task should_have_appended_registered_event() =>
71
+ await _scenario.ShouldHaveAppendedEvent<RegisterAuthor, AuthorRegistered>(_id, e => e.Name == "Jane Austen");
72
+ }
73
+ #endif
74
+ ```
75
+
76
+ - `CommandScenario<TCommand>` exposes `Services` (an `IServiceCollection`), `Context`, `Execute(command[, ct])`, `Validate(command)`, `LastResult`, `Operations` and the operation assertions `ShouldHaveExecutedOperation<T>()` / `ShouldHaveCompensatedOperation<T>()` / `ShouldHaveNoOperationInvocations()`; with the Chronicle integration it also gains `Given`, `EventScenario`, `EventLog`, `EventSequence` and `AppendedEvents` as extension members. Everything else is configured through `Services` and asserted with extension methods.
77
+ - **Event assertions are extension methods on the scenario** (from the Chronicle testing package), keyed by **command + event** type: `await _scenario.ShouldHaveAppendedEvent<TCommand, TEvent>(eventSourceId)` or the `(eventSourceId, Func<TEvent,bool> predicate)` overload; plus `await _scenario.ShouldHaveTailSequenceNumber<TCommand>(...)`. They return `Task`, so the fact is `async Task`.
78
+ - **`CommandResult` assertions** (Arc extensions): `ShouldBeSuccessful()`, `ShouldNotBeSuccessful()`, `ShouldBeValid()`, `ShouldHaveValidationErrors()`, `ShouldHaveValidationErrorFor(message)`, `ShouldBeAuthorized()`, `ShouldNotBeAuthorized()`, `ShouldHaveExceptions()`/`ShouldNotHaveExceptions()`.
79
+ - **Seed prior state through `_scenario.Given` or `_scenario.Services`.** `_scenario.Given.ForEventSource(id).Events(...)` materializes the seeded events into an **in-memory read-model dictionary** the harness answers keyed reads from — in **legacy mode** it never reaches the event log (see the false-greens list below). To populate advisory read models the validator/`Provide()`/`Handle()` inject, substitute `IReadModels` and register it (`_scenario.Services.Replace(new ServiceDescriptor(typeof(IReadModels), mock))`, mocking `GetInstanceById(...)`) or register projections with `_scenario.Services.AddReadModels(...)`.
80
+ - **Validator/`Provide()` dependencies:** register them in `_scenario.Services`; Arc testing discovers the concrete validator automatically. When several specs need different injected validator states, test rejected variants by instantiating the validator directly (per-scenario state can be order-sensitive under parallel xUnit).
81
+ - **`Provide()`:** drive it through `CommandScenario` end-to-end; when the handler's decision is pure given provided data, test `Handle(providedValue)` directly as the default decision spec and keep the scenario for the wiring.
82
+ - **Command operations:** use **cratis-arc-command-operation** for decision/adapter specs and real `CommandScenario` execution, forward failure, compensation, and Chronicle commit rejection; a direct `Handle()` does not execute operations.
83
+
84
+ #### Validation-failure assertions — non-negotiable
85
+
86
+ Every unhappy-path spec asserts **both**:
87
+
88
+ ```csharp
89
+ [Fact] void should_not_succeed() => _result.ShouldNotBeSuccessful();
90
+ [Fact] void should_have_validation_errors() => _result.ShouldHaveValidationErrors();
91
+ ```
92
+
93
+ `ShouldNotBeSuccessful()` alone can't tell a validation rejection from an unhandled exception. **Do not assert on message strings by default** — they're presentation text; say which kind of rejection happened with `ShouldHaveValidationErrorBecauseOf(reason)`, or a constraint by name. Assert message text only when that exact text is the specified behavior, for example a rule whose wording the model pins, and say so in the fact's name. `ShouldHaveValidationErrorFor(message)` matches a substring, so it does not prove exact wording; compare for equality instead, for example `_result.ValidationResults.Select(_ => _.Message).ShouldContain("<exact text>")`. Authorization failures use `ShouldNotBeAuthorized()` (an unauthorized result has *no* validation errors, so `ShouldHaveValidationErrors()` would silently flip). To exercise a command that carries authorization attributes, register the identity the authorization evaluator reads into `_scenario.Services` (substitute the identity provider your app uses). **Adding `[Roles]` to an existing command breaks its happy-path AND validation-failure `.Execute()` specs** — an unauthorized result is not successful and carries no validation errors; switch those assertions to `ShouldNotBeAuthorized()`.
94
+
95
+ `CommandResult` assertions (from `Cratis.Arc.Testing.Commands`; failures throw `CommandResultAssertionException`): `ShouldBeSuccessful()` (`isAuthorized && isValid && !hasExceptions`), `ShouldNotBeSuccessful()`, `ShouldBeValid()` (no validation errors — does *not* check authz/exceptions), `ShouldHaveValidationErrors()`, `ShouldHaveValidationErrorFor(message)`, `ShouldHaveValidationErrorBecauseOf(ValidationResultReason)`, `ShouldHaveConstraintViolationFor(name)`, `ShouldBeAuthorized()`, `ShouldNotBeAuthorized()`, `ShouldHaveExceptions()`.
96
+
97
+ #### False greens the harness lets through
98
+
99
+ Each of these passes while proving nothing. They are the reason the two-assertion rule above exists.
100
+
101
+ 1. **A discarded assertion `Task`.** The `IEventSequence` assertions `ShouldHaveAppendedEvent`/`ShouldHaveTailSequenceNumber` (Chronicle Testing) are genuinely `async`: in a `void [Fact]` that does not `await` them the exception lands on a `Task` nobody observes and the spec passes regardless of behavior, while the compiler's `CS4014` stays silent outside an `async` method. (The same-named `CommandScenario` overloads in Arc's Chronicle.Testing throw synchronously and only *return* a completed `Task`, so those fail even when discarded — but you cannot tell the two apart at the call site.) Make the fact `async Task` and `await` every `Task`-returning `Should*`. Chronicle's analyzer **`CHR0039` (`NonAwaitedAssertion`)** reports any discarded `Task`/`ValueTask`-returning `Should*` call from a Cratis testing package (Chronicle's and Arc's alike).
102
+ 2. **A dependency the spec forgot to seed.** When a validator's constructor, `Provide()` or `Handle()` asks for a read model that does not exist for the command's key, the validator is never built and no rule runs — yet the command is rejected (one validation result, reason **`DependencyUnavailable`**, HTTP 400), so `ShouldNotBeSuccessful()` passes and keeps passing with the rule it is named after deleted. `ShouldHaveValidationErrors()` **refuses** a result whose only errors carry that reason and tells you to seed the read model — or, when that rejection *is* the behavior under spec, assert it deliberately with `ShouldHaveValidationErrorBecauseOf(ValidationResultReason.DependencyUnavailable)`. A missing *service* (not a read model) surfaces as an exception instead (`ShouldHaveExceptions()`), and `ShouldNotBeSuccessful()` alone cannot tell the two apart from a genuine rejection.
103
+ 3. **`.Validate()` authorizes first.** `Validate(command)` runs the command filters, and authorization filters are ordered before validation. A validate-only spec on a `[Roles]`/`[Authorize]` command with no identity registered is denied for authorization — the rule it is named after never ran. Register the identity, or assert `ShouldNotBeAuthorized()` and say so in the name.
104
+ 4. **Legacy-mode constraint preconditions go to the event log, not `CommandScenario.Given`.** `_scenario.Given.ForEventSource(id).Events(...)` on a `CommandScenario` seeds an **in-memory read-model materialization only**; nothing reaches the event log a `[Unique]`/`IConstraint` is enforced against, so a "second registration with the same name" spec passes because the first one was never appended. Seed constraint preconditions through **`EventScenario`**'s `Given`, and assert the violation by constraint **name** (`ShouldHaveConstraintViolationFor`) — the same helper exists on `CommandResult` and on `IAppendResult`.
105
+ 5. **Legacy `GetInstances<T>` is not harness-backed.** Under legacy `CommandScenario` the read-model harness answers keyed reads (`GetInstanceById`) from its seeded dictionary and forwards `GetInstances<T>`/`GetSnapshotsById`/`Watch` to the real client, which is not there. Code under spec that enumerates a read model needs a fake lookup registered in `Services`, not `Given`.
106
+ 6. **One child is not a children spec.** `[ChildrenFrom<T>]` without `key:` identifies children by the **event source id**, so every child event on one stream upserts the same child and a one-child spec stays green with the key wrong. Drive **two** children with distinct keys and assert the count.
107
+ 7. **Independently derived expectations.** An expected value computed in the spec with the same expression as the code under spec agrees with it by construction. Write the expected literal.
108
+
109
+ The `ReadModelScenario` and `EventScenario` equivalents of these live in the **cratis-chronicle-read-model-specifications** and **cratis-chronicle-event-specifications** skills.
110
+
111
+ > **Historical validator-state order-sensitivity:** observed in the 22.16-era harness. Arc 22.23.2 stopped replacing the process-wide provider; later releases isolated scenario stores and event-source services. Reproduce at 22.75.0 before applying the old direct-validator/small `[Collection]` workaround. No current reproduction is verified here.
112
+
113
+ #### Query and protected-decision modes (Arc v22.75.0)
114
+
115
+ `QueryScenario<TReadModel>` (`Cratis.Arc.Testing.Queries`) runs `Perform(nameof(Model.Method),
116
+ arguments?, paging?, sorting?)`; `Observe(...)` returns `QueryObservation.Initial`
117
+ and buffered `Next()`. Configure principal/policies in `Services`; schemes fail
118
+ closed without HTTP. Chronicle query `Given` can seed keyed models/events.
119
+
120
+ For guarded commands use `UseDecisionReads()` **before** seeding events. Given
121
+ state, guarded reads and appends share one real in-process log, unlike legacy
122
+ mode above. Pinned models cannot supply guards. Use `OnRoute(route)` and
123
+ `AppendConcurrently` to prove conflicts; `ShouldHaveCompletedStream` proves
124
+ completion and `DecisionReadAdmissions.ShouldAdmitDecisionReadsIn(assembly)`
125
+ checks admission. See **cratis-application-slice-specifications**,
126
+ `references/query-and-decision-scenarios.md`, for sources and limitations.
127
+
128
+ ### `EventScenario` — constraints & append semantics
129
+
130
+ ```csharp
131
+ readonly EventScenario _scenario = new();
132
+ IAppendResult _result;
133
+
134
+ async Task Establish() =>
135
+ await _scenario.Given.ForEventSource(AuthorId.New()).Events(new AuthorRegistered("Jane Austen"));
136
+
137
+ async Task Because() =>
138
+ _result = await _scenario.EventLog.Append(AuthorId.New(), new AuthorRegistered("Jane Austen"));
139
+
140
+ [Fact] void should_be_failed() => _result.ShouldBeFailed();
141
+ [Fact] void should_violate_unique_constraint() => _result.ShouldHaveConstraintViolationFor(AuthorConstraintNames.UniqueName);
142
+ ```
143
+
144
+ `IAppendResult` assertions (failures throw `AppendResultAssertionException`): `ShouldBeSuccessful()`, `ShouldBeFailed()`, `ShouldHaveConstraintViolations()`/`ShouldNotHave…`, `ShouldHaveConstraintViolationFor(name)`, `ShouldHaveConcurrencyViolations()`/`ShouldNotHave…`, `ShouldHaveErrors()`/`ShouldNotHave…`. Assert the constraint **name** by default; assert exact message text only when that wording is the specified behavior, and name that behavior in the fact.
145
+
146
+ ### `ReadModelScenario<TReadModel>` — declarative projections and reducer wiring
147
+
148
+ The default for declarative projections; pure reducer decisions start with a direct call. Add this scenario for reducer dispatch, filtering, metadata or read-model wiring. It drives events into the projection/reducer and asserts the resulting state. Use xUnit `Assert.*` on `_scenario.Instance`.
149
+
150
+ ```csharp
151
+ ReadModelScenario<Author> _scenario = null!;
152
+ void Establish() => _scenario = new();
153
+
154
+ async Task Because() =>
155
+ await _scenario.Given.ForEventSource(_id).Events(new AuthorRegistered("Jane Austen"), new AuthorArchived());
156
+
157
+ [Fact] void should_be_archived() => Assert.True(_scenario.Instance!.IsArchived);
158
+ ```
159
+
160
+ Use the named-parameter constructor when services/initial state are needed: `new ReadModelScenario<Author>(initialState: null, serviceProvider: services)`. Pre-seed a keyed read model with `Given.ForEventSourceId(id).ReadModel(instance)` for code that calls `IReadModels.GetInstanceById`. Cross-stream projections (`UsingKey`/`UsingParentKey`) are supported — seed each contributing stream with its own `Given.ForEventSource(...)`.
161
+
162
+ **Bridging `IReadModels` into a `CommandScenario`** (current harness limitation): when a command handler injects `IReadModels` directly, build a focused `ReadModelScenario<T>`, seed it via `Given.ForEventSourceId(id).ReadModel(...)`, then register its read models into the command scenario — `_scenario.Services.AddSingleton(readModelScenario.ReadModels)`. Pass `scenario.ReadModels` into code under test for keyed `GetInstanceById` reads; for non-key searches/filters, register a fake lookup or an `IMongoCollection<T>` mock in the scenario services instead.
163
+
164
+ **Chronicle v19.50.0 scenario additions** (`Source/Clients/Testing/ReadModels/`):
165
+ `WithProjection(Action<IProjectionBuilderFor<T>>)` overrides discovery, including
166
+ reducers. `Given.ForEventSource(id).OnStream(eventSourceType, eventStreamType,
167
+ eventStreamId).Events(...)` seeds routing for filtered reducers; unrouted events
168
+ do not reach a differently filtered handler. Dispose scenarios in `Destroy()`.
169
+ Per-run `Defaults(clientArtifactsProvider, serviceProvider, options)` supports
170
+ `InMemoryClientArtifactsProvider` and `ExplicitArtifacts` without global discovery.
171
+
172
+ **Skip discipline:** don't pre-emptively `[Fact(Skip=...)]` a read-model assertion — assume scalar `ConceptAs<string>`, enum, and identifier properties populate correctly; if one fails, investigate the projection. Only skip on a *reproduced* harness gap, and put the specific reason in the skip message.
173
+
174
+ ### `ReactorScenario<TReactor>` — Automation / Translation
175
+
176
+ Construct with an `IServiceProvider` of NSubstitute mocks; assert on the mocks after `Given` fires events. Import `Cratis.Chronicle.Testing.Reactors;`.
177
+
178
+ ```csharp
179
+ void Establish()
180
+ {
181
+ _service = Substitute.For<IMyService>();
182
+ _scenario = new(new ServiceCollection().AddSingleton(_service).BuildServiceProvider());
183
+ }
184
+ async Task Because() => await _scenario.Given.ForEventSource(_id).Events(new AuthorRegistered("Jane Austen"));
185
+ [Fact] async Task should_notify() => await _service.Received(1).Notify("Jane Austen");
186
+ ```
187
+
188
+ In **Chronicle v19.50.0** (`Source/Clients/Testing/Reactors/`), `OnStream` also
189
+ applies to class-based and fluent reactors. `ProducedEvents` carries payload and
190
+ destination; `ShouldHaveProduced<TEvent>(eventSourceId)` or the predicate
191
+ `((produced, @event) => ...)` verifies cross-stream routing. The non-generic
192
+ `ReactorScenario(reactorId, defaults)` exercises an explicitly registered reactor.
193
+ Dispose each scenario in `Destroy()`; await async assertions (CHR0039).
194
+
195
+ #### Read-model setup — Chronicle 19.46.0+
196
+
197
+ In a class-based `ReactorScenario<TReactor>`, `Given` events invoke the reactor;
198
+ they **do not materialize read models**, even `[Passive]` ones. For each
199
+ read-model handler parameter, choose one setup before `Because()`:
200
+
201
+ ```csharp
202
+ // Present-state specification: supply the instance the handler must receive.
203
+ void Establish() => _scenario.Given.ForEventSourceId(_id).ReadModel(_author);
204
+
205
+ // Separate absence specification: deliberately let an Author? parameter receive null.
206
+ void Establish() => _scenario.Given.ForEventSourceId(_id).NoReadModel<Author>();
207
+ ```
208
+
209
+ These are alternatives in **separate specification classes**, not two
210
+ `Establish()` methods in one class. Both builders return
211
+ `ReactorSourceGivenBuilder<TReactor>` for chaining; neither setup is async.
212
+ Use the triggering event-source id by default, or the key returned by the
213
+ reactor's `ICanResolveReadModelKey.Resolve` when implemented. An absence
214
+ declaration is for that exact type and key and does not delete a seeded model.
215
+
216
+ If a recognized read-model parameter resolves to `null` without declared absence,
217
+ `Events(...)` throws `ReadModelNotSeededForReactorScenario` from
218
+ `Cratis.Chronicle.Testing.Reactors`, even if the parameter is nullable. The
219
+ message names the full type and missing key, explains `ReadModel(...)` and
220
+ `NoReadModel<T>()`, and lists seeded keys for that type when any exist. Fix the
221
+ fixture or its key; do not add `NoReadModel<T>()` just to silence missing setup.
222
+ The guard applies to `EventStoreForTesting`; it does not alter production
223
+ resolution or an explicitly supplied event store of another type.
224
+
225
+ **Handler failures:** in Chronicle 19.46.0+, a synchronous handler throw surfaces
226
+ its own exception, not `TargetInvocationException`, with its handler stack
227
+ preserved. Use `await Catch.Exception(() => _scenario.Given.ForEventSource(_id).Events(...))`
228
+ and assert the handler's exception type.
229
+
230
+ **Occurrence time:** in Chronicle 19.46.0+, `EventContext.From(...)` uses
231
+ `DateTimeOffset.UtcNow` when `occurred` is omitted (previously local `Now`).
232
+ `ReactorScenario` builds its contexts through this helper, as do
233
+ `EventContext.Empty` and `EmptyWithEventSourceId(...)`. Explicit occurrence times
234
+ keep their supplied offset; the positional constructor has no such default.
235
+
236
+ ## Out-of-process Chronicle integration specs (advanced)
237
+
238
+ 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).
239
+
240
+ ```csharp
241
+ using context = MyApp.Authors.Registration.when_registering.and_name_already_exists.context;
242
+
243
+ namespace MyApp.Authors.Registration.when_registering;
244
+
245
+ [Collection(ChronicleCollection.Name)]
246
+ public class and_name_already_exists(context context) : Given<context>(context)
247
+ {
248
+ public class context(ChronicleOutOfProcessFixture fixture) : given.an_http_client(fixture)
249
+ {
250
+ public const string AuthorName = "John Doe";
251
+ public CommandResult<object>? Result;
252
+
253
+ async Task Establish() => await EventStore.EventLog.Append(AuthorId.New(), new AuthorRegistered(AuthorName));
254
+ async Task Because() => Result = await Client.ExecuteCommand<RegisterAuthor>("/api/authors/register", new RegisterAuthor(AuthorName));
255
+ }
256
+
257
+ [Fact] void should_not_be_successful() => Context.Result!.IsSuccess.ShouldBeFalse();
258
+ [Fact] void should_have_appended_only_one_event() => Context.ShouldHaveTailSequenceNumber(EventSequenceNumber.First);
259
+ }
260
+ ```
261
+
262
+ - `ChronicleCollection` is a project-defined `[CollectionDefinition]` class over `ChronicleOutOfProcessFixture` (Chronicle ships the fixture, not the collection).
263
+ - `ExecuteCommand<TCommand>(url, cmd)` returns `CommandResult<object>?`; `ExecuteCommand<TCommand, TResult>(url, cmd)` returns `CommandResult<TResult>?`.
264
+ - Helpers: `Context.ShouldHaveTailSequenceNumber(n)` (First = 0), `Context.ShouldHaveAppendedEvent<TEvent>(seq, eventSourceId, validator)`.
265
+
266
+ **Async reactor follow-ups** — when a reactor fires after the command and appends further events, collect them: start the collector **before** the triggering act with `_collector = StartCollectingAppends()`, then `await _collector.WaitForCount(2, TimeSpan.FromSeconds(10))`, assert via `_collector.ShouldHaveEvent<TEvent>(e => ...)`, and `_collector.Dispose()` in `Destroy()`. (`IEventAppendCollection` from the Chronicle testing API.)
267
+
268
+ ## Application spec conventions
269
+
270
+ These supplement the universal conventions in [specs.csharp.md](./specs.csharp.instructions.md#conventions):
271
+
272
+ - **Per-test values for anything in a uniqueness check** — `$"{Guid.NewGuid():N}@example.com"`, a fresh `Guid`, or a truncated id `Guid.NewGuid().ToString("N")[..9]`. Hardcoded values cause order-dependent flakes. Do not add `[Collection(...)]` to work around *collisions* (it's the wrong fix here).
273
+ - **Sequence numbers are zero-based** — first event = `0`, tail of two events = `1`, tail of three = `2`. Never write "tail of 1" for a single appended event; the tail of `[A]` is `0`. (A common first-pass defect.)
274
+ - A slice covering **multiple subjects** (several read models/constraints/aspects) groups its specs under `<Slice>/for_<Subject>/when_<behavior>/`.
275
+
276
+ ## See also
277
+
278
+ - [specs.csharp.md](./specs.csharp.instructions.md) — the universal `Specification` + NSubstitute base this builds on (and what framework specs use).
279
+ - [vertical-slices.md](./vertical-slices.instructions.md) — what each artifact promises (the contract under spec).
280
+ - [efcore.specs.md](./efcore.specs.instructions.md) — `DbContext` specs with SQLite in-memory.
281
+ - skills: **cratis-application-slice-specifications**, **cratis-chronicle-event-specifications**, **cratis-chronicle-read-model-specifications**.
@@ -0,0 +1,141 @@
1
+ ---
2
+ applyTo: "**/for_*/**/*.ts, **/when_*/**/*.ts"
3
+ paths:
4
+ - "**/for_*/**/*.ts"
5
+ - "**/when_*/**/*.ts"
6
+ ---
7
+ <!-- cratis-ai: generated Copilot instructions. Do not edit; rules/ is the source. -->
8
+
9
+ # How to Write TypeScript Specs
10
+
11
+ Extends the base [specs.md](./specs.instructions.md) with TypeScript-specific conventions.
12
+
13
+ > **Which TS spec guide?** For **application frontend** specs — view models, React components, helpers in a Cratis application — use [frontend-testing.md](./frontend-testing.instructions.md) (plain `describe`/`beforeEach`, view models constructible without React). This file covers the **`given()`-helper** style used inside Cratis framework TypeScript packages (`@cratis/*`). Use it when contributing to the framework packages themselves.
14
+
15
+ TypeScript specs follow the same BDD philosophy as C# specs — they describe behaviors, not implementations. The `given()` helper and context classes mirror the Cratis.Specifications pattern on the C# side: setup is separated from the action, and each `it()` assertion verifies a single outcome.
16
+
17
+ ## Frameworks
18
+
19
+ - [Vitest](https://vitest.dev/) for running tests.
20
+ - [Mocha](https://mochajs.org) for test structure (`describe`, `it`, `beforeEach`).
21
+ - [SinonJS](https://sinonjs.org) for mocking/stubbing.
22
+ - [Chai](https://www.chaijs.com) for assertions — **always use the `.should` fluent interface**, never `expect()`. The fluent style reads as a natural sentence: `result.should.equal(expected)`.
23
+ - Run tests with `yarn test` from each package.
24
+
25
+ ## File Structure
26
+
27
+ Tests live alongside source code in `for_`, `when_`, or `given_` folders:
28
+
29
+ ```
30
+ for_EventsCommandResponseValueHandler/
31
+ ├── given/
32
+ │ └── an_events_command_response_value_handler.ts
33
+ ├── when_checking_can_handle/
34
+ │ ├── with_valid_events_collection.ts
35
+ │ ├── with_null_value.ts
36
+ │ └── without_event_source_id.ts
37
+ └── when_handling/
38
+ ├── empty_events_collection.ts
39
+ └── multiple_events_collection.ts
40
+ ```
41
+
42
+ ## BDD Pattern with `given()` Helper
43
+
44
+ The `given()` function is the TypeScript counterpart of the C# `Specification` base class. It instantiates a context class (the "given") **once when the `describe` is registered**, passes that instance to the suite callback, and leaves the per-behavior action to a `beforeEach` you write. Because the context is shared by every `it()` in the `describe`, keep one action per `describe` and let `it()` blocks only assert — that is what keeps Establish/Because/should consistent across both stacks.
45
+
46
+ ```typescript
47
+ import { an_events_command_response_value_handler } from '../given/an_events_command_response_value_handler';
48
+ import { given } from '../../../given';
49
+
50
+ describe('when checking can handle with valid events collection', given(an_events_command_response_value_handler, context => {
51
+ let result: boolean;
52
+
53
+ beforeEach(() => {
54
+ result = context.handler.canHandle(context.commandContext, [new TestEvent('Test')]);
55
+ });
56
+
57
+ it('should return true', () => {
58
+ result.should.be.true;
59
+ });
60
+ }));
61
+ ```
62
+
63
+ For behaviors with multiple outcomes, include "when \<behavior\>" as a prefix in the `describe` text.
64
+
65
+ ## Reusable Context Classes
66
+
67
+ Context classes play the same role as `given/` classes in C# — they capture preconditions that multiple specs share. Unlike C# (where fields are `protected`), TypeScript context properties are public because tests access them directly through the `context` parameter.
68
+
69
+ ```typescript
70
+ export class an_events_command_response_value_handler {
71
+ handler: EventsCommandResponseValueHandler;
72
+ commandContext: CommandContext;
73
+
74
+ constructor() {
75
+ this.commandContext = /* setup */;
76
+ this.handler = new EventsCommandResponseValueHandler(/* deps */);
77
+ }
78
+ }
79
+ ```
80
+
81
+ - Properties are public (not protected) — tests access them via `context.propertyName`.
82
+ - `given` is a package-local helper file, not a published export. Import it with the relative path to that file, for example `import { given } from '../../given';`; create it if absent. Fundamentals v7.23.0 constructs parameterless contexts; Arc v22.75.0 passes the Mocha Suite and binds callback `this` to it.
83
+ - Configure Vitest's setup file to call `chai.should()` and register `sinon-chai` and `chai-as-promised`. Load the reflection polyfill for metadata-dependent specifications.
84
+ - Simple tests without shared setup don't need a reusable context.
85
+
86
+ ## Simple Test Pattern (without context)
87
+
88
+ ```typescript
89
+ describe('when replacing route parameters', () => {
90
+ let result: { route: string; unusedParameters: object };
91
+
92
+ beforeEach(() => {
93
+ result = UrlHelpers.replaceRouteParameters('/api/items/{id}', { id: '123' });
94
+ });
95
+
96
+ it('should replace the route parameter', () => {
97
+ result.route.should.equal('/api/items/123');
98
+ });
99
+ });
100
+ ```
101
+
102
+ ## Naming Conventions
103
+
104
+ TypeScript specs use spaces in `it()` descriptions (unlike C# which uses underscores) because they appear in test runner output as human-readable sentences.
105
+
106
+ - Use **spaces** (not underscores) in `it()` descriptions:
107
+ - ✅ `it('should return invalid result', ...)`
108
+ - ❌ `it('should_return_invalid_result', ...)`
109
+ - Start `it()` descriptions with "should".
110
+ - `describe()` text describes the scenario in natural language.
111
+
112
+ ## Assertions — Chai Fluent Interface
113
+
114
+ **Always use the `.should` fluent interface. Never use `expect()`.** The `.should` style reads as a natural English sentence — `value.should.equal(expected)` vs `expect(value).to.equal(expected)` — and matches the project's preference for code that reads like prose.
115
+
116
+ ```typescript
117
+ value.should.equal(expected);
118
+ value.should.be.true;
119
+ value.should.be.false;
120
+ value.should.be.null;
121
+ value.should.not.be.null;
122
+ value.should.deep.equal(expected);
123
+ value.should.be.instanceOf(Type);
124
+ array.should.contain(item);
125
+ array.should.have.lengthOf(3);
126
+ (() => throwingFn()).should.throw(ErrorType);
127
+ ```
128
+
129
+ ## Mocking with Sinon
130
+
131
+ ```typescript
132
+ import sinon from 'sinon';
133
+
134
+ const stub = sinon.createStubInstance(ConcreteClass);
135
+ const fetchStub = sinon.stub(globalThis, 'fetch');
136
+ fetchStub.resolves({ ok: true, json: async () => ({ /* data */ }) });
137
+ ```
138
+
139
+ ## Async
140
+
141
+ `beforeEach`, `afterEach`, and `it` callbacks can all be `async` when needed.