@cratis/pi 0.0.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.
Files changed (125) hide show
  1. package/README.md +43 -0
  2. package/package.json +24 -0
  3. package/skills/cratis-application-react-specifications/LICENSE +2 -0
  4. package/skills/cratis-application-react-specifications/SKILL.md +192 -0
  5. package/skills/cratis-application-slice-specifications/LICENSE +2 -0
  6. package/skills/cratis-application-slice-specifications/SKILL.md +222 -0
  7. package/skills/cratis-arc-authentication-authorization-and-identity/LICENSE +2 -0
  8. package/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +233 -0
  9. package/skills/cratis-arc-authentication-authorization-and-identity/references/authentication.md +140 -0
  10. package/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +112 -0
  11. package/skills/cratis-arc-authentication-authorization-and-identity/references/frontend.md +102 -0
  12. package/skills/cratis-arc-authentication-authorization-and-identity/references/local-development.md +85 -0
  13. package/skills/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +87 -0
  14. package/skills/cratis-arc-command/LICENSE +2 -0
  15. package/skills/cratis-arc-command/SKILL.md +286 -0
  16. package/skills/cratis-arc-command/references/command-result.md +111 -0
  17. package/skills/cratis-arc-command/references/handler-shapes.md +135 -0
  18. package/skills/cratis-arc-command/references/proxy-generation.md +87 -0
  19. package/skills/cratis-arc-command/references/read-model-injection.md +97 -0
  20. package/skills/cratis-arc-command-execution/LICENSE +2 -0
  21. package/skills/cratis-arc-command-execution/SKILL.md +148 -0
  22. package/skills/cratis-arc-command-validation/LICENSE +2 -0
  23. package/skills/cratis-arc-command-validation/SKILL.md +230 -0
  24. package/skills/cratis-arc-ef-core-migration/LICENSE +2 -0
  25. package/skills/cratis-arc-ef-core-migration/SKILL.md +231 -0
  26. package/skills/cratis-arc-observable-query-http/LICENSE +2 -0
  27. package/skills/cratis-arc-observable-query-http/SKILL.md +157 -0
  28. package/skills/cratis-arc-query-paging/LICENSE +2 -0
  29. package/skills/cratis-arc-query-paging/SKILL.md +173 -0
  30. package/skills/cratis-arc-react-page/LICENSE +2 -0
  31. package/skills/cratis-arc-react-page/SKILL.md +291 -0
  32. package/skills/cratis-arc-react-page/references/data-page.md +125 -0
  33. package/skills/cratis-arc-react-page/references/data-tables.md +136 -0
  34. package/skills/cratis-arc-react-page/references/dialogs.md +238 -0
  35. package/skills/cratis-arc-react-page/references/mvvm.md +150 -0
  36. package/skills/cratis-arc-react-page/references/queries-and-commands.md +200 -0
  37. package/skills/cratis-chronicle-cli-operations/LICENSE +2 -0
  38. package/skills/cratis-chronicle-cli-operations/SKILL.md +199 -0
  39. package/skills/cratis-chronicle-client-dotnet/LICENSE +2 -0
  40. package/skills/cratis-chronicle-client-dotnet/SKILL.md +433 -0
  41. package/skills/cratis-chronicle-client-elixir/LICENSE +2 -0
  42. package/skills/cratis-chronicle-client-elixir/SKILL.md +429 -0
  43. package/skills/cratis-chronicle-client-kotlin/LICENSE +2 -0
  44. package/skills/cratis-chronicle-client-kotlin/SKILL.md +498 -0
  45. package/skills/cratis-chronicle-client-typescript/LICENSE +2 -0
  46. package/skills/cratis-chronicle-client-typescript/SKILL.md +396 -0
  47. package/skills/cratis-chronicle-compliance/LICENSE +2 -0
  48. package/skills/cratis-chronicle-compliance/SKILL.md +246 -0
  49. package/skills/cratis-chronicle-event-constraints/LICENSE +2 -0
  50. package/skills/cratis-chronicle-event-constraints/SKILL.md +227 -0
  51. package/skills/cratis-chronicle-event-modeling/LICENSE +2 -0
  52. package/skills/cratis-chronicle-event-modeling/SKILL.md +157 -0
  53. package/skills/cratis-chronicle-event-specifications/LICENSE +2 -0
  54. package/skills/cratis-chronicle-event-specifications/SKILL.md +200 -0
  55. package/skills/cratis-chronicle-event-type-migration/LICENSE +2 -0
  56. package/skills/cratis-chronicle-event-type-migration/SKILL.md +235 -0
  57. package/skills/cratis-chronicle-mcp-inspection/LICENSE +2 -0
  58. package/skills/cratis-chronicle-mcp-inspection/SKILL.md +79 -0
  59. package/skills/cratis-chronicle-mcp-inspection/references/blocked-tools.md +12 -0
  60. package/skills/cratis-chronicle-mcp-inspection/references/observational-tools.md +8 -0
  61. package/skills/cratis-chronicle-multi-tenancy/LICENSE +2 -0
  62. package/skills/cratis-chronicle-multi-tenancy/SKILL.md +198 -0
  63. package/skills/cratis-chronicle-projection/LICENSE +2 -0
  64. package/skills/cratis-chronicle-projection/SKILL.md +162 -0
  65. package/skills/cratis-chronicle-projection/references/fluent-builder.md +126 -0
  66. package/skills/cratis-chronicle-projection/references/model-bound-attributes.md +118 -0
  67. package/skills/cratis-chronicle-reactor/LICENSE +2 -0
  68. package/skills/cratis-chronicle-reactor/SKILL.md +344 -0
  69. package/skills/cratis-chronicle-read-model/LICENSE +2 -0
  70. package/skills/cratis-chronicle-read-model/SKILL.md +193 -0
  71. package/skills/cratis-chronicle-read-model/references/queries.md +142 -0
  72. package/skills/cratis-chronicle-read-model-specifications/LICENSE +2 -0
  73. package/skills/cratis-chronicle-read-model-specifications/SKILL.md +176 -0
  74. package/skills/cratis-chronicle-reducer/LICENSE +2 -0
  75. package/skills/cratis-chronicle-reducer/SKILL.md +173 -0
  76. package/skills/cratis-chronicle-web-workbench/LICENSE +2 -0
  77. package/skills/cratis-chronicle-web-workbench/SKILL.md +177 -0
  78. package/skills/cratis-cli-terminal-workbench/LICENSE +2 -0
  79. package/skills/cratis-cli-terminal-workbench/SKILL.md +156 -0
  80. package/skills/cratis-code-review/LICENSE +2 -0
  81. package/skills/cratis-code-review/SKILL.md +177 -0
  82. package/skills/cratis-components-accessibility/LICENSE +2 -0
  83. package/skills/cratis-components-accessibility/SKILL.md +176 -0
  84. package/skills/cratis-components-schema-editor/LICENSE +2 -0
  85. package/skills/cratis-components-schema-editor/SKILL.md +238 -0
  86. package/skills/cratis-components-stepper-command-dialog/LICENSE +2 -0
  87. package/skills/cratis-components-stepper-command-dialog/SKILL.md +209 -0
  88. package/skills/cratis-components-styling/LICENSE +2 -0
  89. package/skills/cratis-components-styling/SKILL.md +221 -0
  90. package/skills/cratis-components-toolbar/LICENSE +2 -0
  91. package/skills/cratis-components-toolbar/SKILL.md +259 -0
  92. package/skills/cratis-documentation-writing/LICENSE +2 -0
  93. package/skills/cratis-documentation-writing/SKILL.md +121 -0
  94. package/skills/cratis-event-model-diagram/LICENSE +2 -0
  95. package/skills/cratis-event-model-diagram/SKILL.md +142 -0
  96. package/skills/cratis-fundamentals-concept/LICENSE +2 -0
  97. package/skills/cratis-fundamentals-concept/SKILL.md +198 -0
  98. package/skills/cratis-fundamentals-type-discovery/LICENSE +2 -0
  99. package/skills/cratis-fundamentals-type-discovery/SKILL.md +152 -0
  100. package/skills/cratis-governed-release-methodology/LICENSE +2 -0
  101. package/skills/cratis-governed-release-methodology/SKILL.md +297 -0
  102. package/skills/cratis-lens-browser-extension/LICENSE +2 -0
  103. package/skills/cratis-lens-browser-extension/SKILL.md +239 -0
  104. package/skills/cratis-performance-review/LICENSE +2 -0
  105. package/skills/cratis-performance-review/SKILL.md +139 -0
  106. package/skills/cratis-screenplay-model-authoring/LICENSE +2 -0
  107. package/skills/cratis-screenplay-model-authoring/SKILL.md +231 -0
  108. package/skills/cratis-security-review/LICENSE +2 -0
  109. package/skills/cratis-security-review/SKILL.md +141 -0
  110. package/skills/cratis-specification-by-example/LICENSE +2 -0
  111. package/skills/cratis-specification-by-example/SKILL.md +188 -0
  112. package/skills/cratis-specifications-csharp/LICENSE +2 -0
  113. package/skills/cratis-specifications-csharp/SKILL.md +230 -0
  114. package/skills/cratis-specifications-csharp/references/application-scenarios.md +191 -0
  115. package/skills/cratis-specifications-csharp/references/csharp-patterns.md +142 -0
  116. package/skills/cratis-specifications-csharp/references/integration-specs.md +126 -0
  117. package/skills/cratis-specifications-typescript/LICENSE +2 -0
  118. package/skills/cratis-specifications-typescript/SKILL.md +202 -0
  119. package/skills/cratis-specifications-typescript/references/typescript-patterns.md +168 -0
  120. package/skills/cratis-stage-rendering-and-sandbox/LICENSE +2 -0
  121. package/skills/cratis-stage-rendering-and-sandbox/SKILL.md +230 -0
  122. package/skills/cratis-studio-mcp-safety-guidance/LICENSE +2 -0
  123. package/skills/cratis-studio-mcp-safety-guidance/SKILL.md +69 -0
  124. package/skills/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +12 -0
  125. package/skills/cratis-studio-mcp-safety-guidance/references/observational-tools.md +8 -0
@@ -0,0 +1,191 @@
1
+ # In-process scenario specifications
2
+
3
+ The four scenario helpers exercise the real Arc and Chronicle pipelines
4
+ in-process. They are the default for event-sourced *application* behavior. A
5
+ library or framework repository reaches for one only to test the very engine it
6
+ provides.
7
+
8
+ Verified against `Cratis.Arc.Testing` `22.10.4` and `Cratis.Chronicle.Testing`
9
+ `17.0.0`.
10
+
11
+ | Helper | Exercises | Use for |
12
+ | --- | --- | --- |
13
+ | `CommandScenario<TCommand>` | Authorization, validators, `Provide()`, `Handle()`, appended events | State Change behavior |
14
+ | `EventScenario` | Chronicle append semantics without the command pipeline | Constraints, raw sequencing, concurrency |
15
+ | `ReadModelScenario<TReadModel>` | Projection or reducer state from a sequence of events | State View behavior |
16
+ | `ReactorScenario<TReactor>` | Reactor invocation and its side effects | Automation and Translation behavior |
17
+
18
+ Wrap every scenario specification file in `#if DEBUG … #endif` so specification
19
+ code compiles only in Debug.
20
+
21
+ ## `CommandScenario<TCommand>`
22
+
23
+ ```csharp
24
+ #if DEBUG
25
+ namespace <RootNamespace>.<Feature>.when_<behavior>;
26
+
27
+ public class and_<condition> : Specification
28
+ {
29
+ readonly CommandScenario<<CommandType>> _scenario = new();
30
+ readonly <IdentityType> _id = <IdentityType>.New();
31
+ CommandResult _result;
32
+
33
+ async Task Because() =>
34
+ _result = await _scenario.Execute(new <CommandType>(_id, <arguments>));
35
+
36
+ [Fact] void should_succeed() => _result.ShouldBeSuccessful();
37
+
38
+ [Fact] async Task should_append_<event>() =>
39
+ await _scenario.ShouldHaveAppendedEvent<<CommandType>, <EventType>>(
40
+ _id, appended => appended.<Property> == <expected>);
41
+ }
42
+ #endif
43
+ ```
44
+
45
+ `CommandScenario<TCommand>` itself exposes exactly `Services`, `Context`,
46
+ `Execute`, and `Validate`. Everything else — seeding, the event log, and the
47
+ assertions — arrives as extension members from `Cratis.Arc.Chronicle.Testing`.
48
+
49
+ - **Event assertions** are extension methods keyed by command *and* event type:
50
+ `ShouldHaveAppendedEvent<TCommand, TEvent>(eventSourceId)`, its
51
+ `(eventSourceId, Func<TEvent, bool> predicate)` overload, and
52
+ `ShouldHaveTailSequenceNumber<TCommand>(…)`. They return `Task`, so the fact is
53
+ `async Task`.
54
+ - **`CommandResult` assertions** come from `Cratis.Arc.Testing.Commands` and
55
+ throw `CommandResultAssertionException` on failure: `ShouldBeSuccessful()`
56
+ (authorized, valid, no exceptions), `ShouldNotBeSuccessful()`,
57
+ `ShouldBeValid()` (validation only — it does not check authorization or
58
+ exceptions), `ShouldHaveValidationErrors()`,
59
+ `ShouldHaveValidationErrorFor(message)`, `ShouldBeAuthorized()`,
60
+ `ShouldNotBeAuthorized()`, `ShouldHaveExceptions()`,
61
+ `ShouldNotHaveExceptions()`.
62
+ - **Seed prior state through `Given`.** With `Cratis.Arc.Chronicle.Testing`
63
+ referenced, a command scenario carries a Chronicle-backed `Given` builder as an
64
+ extension member, alongside `EventScenario`, `EventLog`, `EventSequence` and
65
+ `AppendedEvents`. `Given.ForEventSource(id).Events(…)` seeds the events an
66
+ injected read model is then materialized from by its own projection or
67
+ reducer; `Given.ForEventSource(id).ReadModel(instance)` pins a specific
68
+ read-model value instead. Both return `void`. Prefer seeding events — it
69
+ exercises the projection production depends on, so the specification cannot
70
+ pass against a read-model shape the projection never produces.
71
+ - **Validator and `Provide()` dependencies** are registered in
72
+ `_scenario.Services`; the concrete validator is discovered automatically.
73
+
74
+ ### Unhappy paths assert twice
75
+
76
+ ```csharp
77
+ [Fact] void should_not_succeed() => _result.ShouldNotBeSuccessful();
78
+ [Fact] void should_have_validation_errors() => _result.ShouldHaveValidationErrors();
79
+ ```
80
+
81
+ `ShouldNotBeSuccessful()` alone cannot distinguish a validation rejection from
82
+ an unhandled exception, so both facts are required. Authorization failures are
83
+ different: an unauthorized result carries **no** validation errors, so assert
84
+ `ShouldNotBeAuthorized()` instead. Adding an authorization attribute to an
85
+ existing command therefore breaks both its happy-path and its validation-failure
86
+ specifications — switch those assertions rather than patching around them.
87
+
88
+ Never assert on a message string.
89
+
90
+ > **Validator state can be order-sensitive.** The command-scenario pipeline can
91
+ > cache enough state that injected-validator branches depend on execution order
92
+ > when xUnit runs classes in parallel. Use `CommandScenario` for the valid path,
93
+ > test rejected state variants by instantiating the validator directly, and only
94
+ > for that case put the command's specifications in a small xUnit `[Collection]`.
95
+
96
+ ## `EventScenario`
97
+
98
+ ```csharp
99
+ readonly EventScenario _scenario = new();
100
+ IAppendResult _result;
101
+
102
+ async Task Establish() =>
103
+ await _scenario.Given.ForEventSource(<IdentityType>.New())
104
+ .Events(new <EventType>(<arguments>));
105
+
106
+ async Task Because() =>
107
+ _result = await _scenario.EventLog.Append(
108
+ <IdentityType>.New(), new <EventType>(<arguments>));
109
+
110
+ [Fact] void should_fail() => _result.ShouldBeFailed();
111
+
112
+ [Fact] void should_violate_<constraint>() =>
113
+ _result.ShouldHaveConstraintViolationFor(<ConstraintNames>.<Name>);
114
+ ```
115
+
116
+ `IAppendResult` assertions throw `AppendResultAssertionException`:
117
+ `ShouldBeSuccessful()`, `ShouldBeFailed()`, `ShouldHaveConstraintViolations()`
118
+ and `ShouldNotHaveConstraintViolations()`,
119
+ `ShouldHaveConstraintViolationFor(name)`,
120
+ `ShouldHaveConcurrencyViolations()` and its negation, `ShouldHaveErrors()` and
121
+ its negation. Assert the constraint **name**, never the message.
122
+
123
+ ⚠️ `EventScenario` wires a no-op concurrency-scope strategy, so a concurrency
124
+ violation can never occur inside it. `ShouldHaveConcurrencyViolations()` cannot
125
+ pass there and its negation passes vacuously — specify concurrency against the
126
+ real kernel with an out-of-process integration specification instead.
127
+
128
+ ## `ReadModelScenario<TReadModel>`
129
+
130
+ ```csharp
131
+ ReadModelScenario<<ReadModelType>> _scenario = null!;
132
+
133
+ void Establish() => _scenario = new();
134
+
135
+ async Task Because() =>
136
+ await _scenario.Given.ForEventSource(_id)
137
+ .Events(new <EventType>(<arguments>), new <OtherEventType>());
138
+
139
+ [Fact] void should_<expected_state>() =>
140
+ Assert.True(_scenario.Instance!.<Property>);
141
+ ```
142
+
143
+ The helper auto-detects a model-bound projection, a fluent projection, or a
144
+ reducer. Use the named-parameter constructor when services or initial state are
145
+ needed: `new ReadModelScenario<<ReadModelType>>(initialState: null,
146
+ serviceProvider: services)`. Pre-seed a keyed read model with
147
+ `Given.ForEventSourceId(id).ReadModel(instance)` for code that calls
148
+ `IReadModels.GetInstanceById`. Cross-stream projections are supported — seed each
149
+ contributing stream with its own `Given.ForEventSource(…)`.
150
+
151
+ To bridge read models into a command scenario, build a focused
152
+ `ReadModelScenario<T>`, seed it, then register its read models into the command
153
+ scenario with `_scenario.Services.AddSingleton(readModelScenario.ReadModels)`.
154
+ For non-key searches, register a lookup double in the scenario services instead.
155
+
156
+ Do not pre-emptively skip a read-model assertion. Assume scalar concept, enum,
157
+ and identifier properties populate; if one does not, investigate the projection.
158
+ Skip only on a reproduced harness gap, and put the specific reason in the skip
159
+ message.
160
+
161
+ ## `ReactorScenario<TReactor>`
162
+
163
+ ```csharp
164
+ void Establish()
165
+ {
166
+ _<collaborator> = Substitute.For<<CollaboratorType>>();
167
+ _scenario = new(new ServiceCollection()
168
+ .AddSingleton(_<collaborator>)
169
+ .BuildServiceProvider());
170
+ }
171
+
172
+ async Task Because() =>
173
+ await _scenario.Given.ForEventSource(_id).Events(new <EventType>(<arguments>));
174
+
175
+ [Fact] async Task should_<side_effect>() =>
176
+ await _<collaborator>.Received(1).<Method>(<expected>);
177
+ ```
178
+
179
+ Construct with an `IServiceProvider` of substituted collaborators and assert on
180
+ those substitutes after `Given` fires the events.
181
+
182
+ ## Application conventions
183
+
184
+ - **Use a per-specification value for anything under a uniqueness check** — a
185
+ fresh `Guid`, or a truncated `Guid.NewGuid().ToString("N")[..9]`. Hardcoded
186
+ values cause order-dependent flakes. Do not add `[Collection(…)]` to work
187
+ around a collision; that is the wrong fix here.
188
+ - **Sequence numbers are zero-based.** The first event is `0`, the tail of two
189
+ events is `1`. The tail after a single append is `0`, never `1`.
190
+ - A behavior covering several subjects groups its specifications under
191
+ `<Slice>/for_<Subject>/when_<behavior>/`.
@@ -0,0 +1,142 @@
1
+ # C# specification patterns
2
+
3
+ Detail for the plain `Cratis.Specifications` surface: phases, substitution,
4
+ assertions, and exception capture. Verified against `Cratis.Specifications`
5
+ `4.1.0` and NSubstitute as consumed by that package.
6
+
7
+ ## Phases
8
+
9
+ | Method | Purpose | Notes |
10
+ | --- | --- | --- |
11
+ | `void Establish()` | Setup, before `Because()` | One per class in the chain, run base-first. Never call `base.Establish()` |
12
+ | `void Because()` | The single action under test | Only in a concrete specification, never in a `given/` context |
13
+ | `[Fact] void should_*()` | One assertion per fact | Expression-body form, no blank lines between facts |
14
+ | `void Destroy()` | Teardown after each specification | Optional |
15
+
16
+ Every phase may be `async Task`.
17
+
18
+ ## Layered contexts
19
+
20
+ ```csharp
21
+ // given/all_dependencies.cs — substitute every collaborator
22
+ public class all_dependencies : Specification
23
+ {
24
+ protected <CollaboratorType> _<collaborator>;
25
+ protected <OtherCollaboratorType> _<otherCollaborator>;
26
+
27
+ void Establish()
28
+ {
29
+ _<collaborator> = Substitute.For<<CollaboratorType>>();
30
+ _<otherCollaborator> = Substitute.For<<OtherCollaboratorType>>();
31
+ }
32
+ }
33
+
34
+ // given/a_<system_under_test>.cs — build the system under test
35
+ public class a_<system_under_test> : all_dependencies
36
+ {
37
+ protected <ClassName> _<sut>;
38
+
39
+ void Establish() => _<sut> = new(_<collaborator>, _<otherCollaborator>);
40
+ }
41
+
42
+ // when_<behavior>/and_<condition>.cs — the concrete specification
43
+ public class and_<condition> : given.a_<system_under_test>
44
+ {
45
+ void Because() => _<sut>.<Action>(<input>);
46
+
47
+ [Fact] void should_<expected_outcome>() =>
48
+ _<collaborator>.Received(1).<Method>(Arg.Any<<ArgumentType>>());
49
+ }
50
+ ```
51
+
52
+ A context captures the world as it exists *before* the action. It never
53
+ contains the action itself.
54
+
55
+ ## Substitution patterns
56
+
57
+ ```csharp
58
+ // Create
59
+ _<collaborator> = Substitute.For<<CollaboratorType>>();
60
+
61
+ // Return values
62
+ _<collaborator>.<Method>(Arg.Any<string>()).Returns(<value>);
63
+ _<collaborator>.<AsyncMethod>(Arg.Any<int>()).Returns(Task.FromResult(<value>));
64
+
65
+ // Argument matchers
66
+ Arg.Is<<RequestType>>(request => request.Id == _expectedId)
67
+
68
+ // Verify
69
+ _<collaborator>.Received(1).<Method>(Arg.Any<string>());
70
+ _<collaborator>.DidNotReceive().<OtherMethod>(Arg.Any<int>());
71
+
72
+ // Capture an argument
73
+ _<collaborator>
74
+ .When(collaborator => collaborator.<Method>(Arg.Any<<ArgumentType>>()))
75
+ .Do(call => _captured = call.Arg<<ArgumentType>>());
76
+
77
+ // Throw from a substitute
78
+ _<collaborator>.<Method>(Arg.Any<<ArgumentType>>())
79
+ .Throws(new <ExceptionType>());
80
+ ```
81
+
82
+ ## Assertion extension methods
83
+
84
+ From `Cratis.Specifications`:
85
+
86
+ | Method | Example |
87
+ | --- | --- |
88
+ | `.ShouldEqual(expected)` | `_result.ShouldEqual(<expected>)` |
89
+ | `.ShouldBeTrue()` / `.ShouldBeFalse()` | `_flag.ShouldBeTrue()` |
90
+ | `.ShouldBeNull()` / `.ShouldNotBeNull()` | `_error.ShouldBeNull()` |
91
+ | `.ShouldBeEmpty()` / `.ShouldNotBeEmpty()` | `_items.ShouldBeEmpty()` |
92
+ | `.ShouldContain(item)` / `.ShouldNotContain(item)` | `_items.ShouldContain(<expected>)` |
93
+ | `.ShouldContainOnly(items)` | `_items.ShouldContainOnly(<expectedItems>)` |
94
+ | `.ShouldBeOfExactType<T>()` | `_event.ShouldBeOfExactType<<EventType>>()` |
95
+ | `.ShouldBeGreaterThan(n)` / `.ShouldBeLessThan(n)` | `_count.ShouldBeGreaterThan(0)` |
96
+
97
+ Assert on values and types. Never assert on a presentation message string — it
98
+ is text, not behavior.
99
+
100
+ ## Catching exceptions
101
+
102
+ ```csharp
103
+ Exception? _error;
104
+
105
+ async Task Because() => _error = await Catch.Exception(_<sut>.<Action>);
106
+
107
+ [Fact] void should_fail() => _error.ShouldNotBeNull();
108
+ [Fact] void should_fail_with_<reason>() =>
109
+ _error.ShouldBeOfExactType<<ExceptionType>>();
110
+ ```
111
+
112
+ Use `Catch.Exception` rather than a `try`/`catch` in `Because()`; the captured
113
+ exception is the outcome the facts assert on.
114
+
115
+ ## Usings
116
+
117
+ `GlobalUsings.Specs.cs` supplies `Xunit`, `NSubstitute`, and
118
+ `Cratis.Specifications`. Do not repeat them per file, and do not add a using for
119
+ the namespace of the system under test.
120
+
121
+ Order non-aliased namespaces first, then a blank line, then
122
+ `using <alias> = …` sorted by alias name. Alias a type whose short name collides
123
+ with a namespace segment, choosing a domain-meaningful alias rather than a
124
+ technical `Command`, `Event`, or `Component` suffix.
125
+
126
+ ## One outcome per file
127
+
128
+ ```
129
+ # Single outcome — one file
130
+ for_<ClassName>/when_<behavior>.cs
131
+
132
+ # Multiple outcomes — a folder
133
+ for_<ClassName>/when_<behavior>/
134
+ and_<condition>.cs
135
+ and_<other_condition>.cs
136
+ with_<state>.cs
137
+ without_<requirement>.cs
138
+ ```
139
+
140
+ Allowed prefixes: `and_`, `with_`, `without_`, `having_`, `given_`. Each
141
+ distinct outcome is its own file, so a failure names the outcome that broke
142
+ without reading a multi-assertion file.
@@ -0,0 +1,126 @@
1
+ # Out-of-process Chronicle integration specifications
2
+
3
+ Reserve these for the host, transport, serialization, or real-infrastructure
4
+ boundary the in-process scenario helpers cannot reach. They exercise a complete
5
+ behavior — HTTP request through command handling, append, constraint checking,
6
+ and projection — against a real Chronicle event store.
7
+
8
+ They live under `when_<behavior>/` **inside the behavior's own folder**, not in a
9
+ `for_<Type>/` unit folder: there is no isolated unit, the whole slice is under
10
+ test. Never mix unit and integration specifications in one folder.
11
+
12
+ Verified against `Cratis.Chronicle.Testing` `17.0.0`.
13
+
14
+ ## Structure
15
+
16
+ ```csharp
17
+ using context = <RootNamespace>.<Feature>.when_<behavior>.and_<condition>.context;
18
+
19
+ namespace <RootNamespace>.<Feature>.when_<behavior>;
20
+
21
+ [Collection(ChronicleCollection.Name)]
22
+ public class and_<condition>(context context) : Given<context>(context)
23
+ {
24
+ public class context(ChronicleOutOfProcessFixture fixture)
25
+ : given.an_http_client(fixture)
26
+ {
27
+ public CommandResult<<ResponseType>>? Result;
28
+
29
+ async Task Because() =>
30
+ Result = await Client.ExecuteCommand<<CommandType>, <ResponseType>>(
31
+ "<route>", new <CommandType>(<arguments>));
32
+ }
33
+
34
+ [Fact] void should_succeed() => Context.Result!.IsSuccess.ShouldBeTrue();
35
+
36
+ [Fact] void should_append_one_event() =>
37
+ Context.ShouldHaveTailSequenceNumber(EventSequenceNumber.First);
38
+
39
+ [Fact] void should_append_<event>() =>
40
+ Context.ShouldHaveAppendedEvent<<EventType>>(
41
+ EventSequenceNumber.First,
42
+ Context.Result!.Response,
43
+ appended => appended.<Property>.Value.ShouldEqual(<expected>));
44
+ }
45
+ ```
46
+
47
+ ## Seeding preconditions
48
+
49
+ Append events in `async Task Establish()` before `Because()` runs:
50
+
51
+ ```csharp
52
+ public class context(ChronicleOutOfProcessFixture fixture)
53
+ : given.an_http_client(fixture)
54
+ {
55
+ public const string <ExistingValue> = "<value>";
56
+ public CommandResult<object>? Result;
57
+
58
+ async Task Establish() =>
59
+ await EventStore.EventLog.Append(
60
+ <IdentityType>.New(), new <EventType>(<ExistingValue>));
61
+
62
+ async Task Because() =>
63
+ Result = await Client.ExecuteCommand<<CommandType>>(
64
+ "<route>", new <CommandType>(<ExistingValue>));
65
+ }
66
+ ```
67
+
68
+ ```csharp
69
+ [Fact] void should_not_succeed() => Context.Result!.IsSuccess.ShouldBeFalse();
70
+
71
+ [Fact] void should_not_append_further_events() =>
72
+ Context.ShouldHaveTailSequenceNumber(EventSequenceNumber.First);
73
+ ```
74
+
75
+ ## Command execution overloads
76
+
77
+ ```csharp
78
+ // No typed response — returns CommandResult<object>?
79
+ Result = await Client.ExecuteCommand<<CommandType>>(route, command);
80
+
81
+ // Typed response — returns CommandResult<TResult>?
82
+ Result = await Client.ExecuteCommand<<CommandType>, <ResponseType>>(route, command);
83
+ ```
84
+
85
+ ## Assertion helpers
86
+
87
+ | Helper | Verifies |
88
+ | --- | --- |
89
+ | `Context.Result!.IsSuccess.ShouldBeTrue()` | The command succeeded |
90
+ | `Context.Result!.IsSuccess.ShouldBeFalse()` | The command failed |
91
+ | `Context.ShouldHaveTailSequenceNumber(EventSequenceNumber.First)` | The log holds exactly one event |
92
+ | `Context.ShouldHaveTailSequenceNumber(n)` | The log tail is at sequence `n` |
93
+ | `Context.ShouldHaveAppendedEvent<TEvent>(seq, eventSourceId, validator)` | A specific event was appended with the expected values |
94
+
95
+ Sequence numbers are zero-based: `EventSequenceNumber.First` is `0`, so the tail
96
+ after a single append is `0`.
97
+
98
+ ## Asynchronous follow-ups
99
+
100
+ When a reactor fires after the command and appends further events, collect them
101
+ rather than sleeping. Start the collector **before** the triggering action:
102
+
103
+ ```csharp
104
+ _collector = StartCollectingAppends();
105
+ // … perform the action …
106
+ await _collector.WaitForCount(2, TimeSpan.FromSeconds(10));
107
+
108
+ [Fact] void should_append_<event>() =>
109
+ _collector.ShouldHaveEvent<<EventType>>(appended => appended.<Property> == <expected>);
110
+
111
+ void Destroy() => _collector.Dispose();
112
+ ```
113
+
114
+ `IEventAppendCollection` is part of the Chronicle testing API. The timeout is a
115
+ deadline, not a sleep — it turns a hang into a named failure.
116
+
117
+ ## Rules
118
+
119
+ - `context` is an inner `public class` inheriting `given.an_http_client(fixture)`.
120
+ - Add the `using context = <full.namespace>.context;` alias at the top of the
121
+ file.
122
+ - `[Collection(ChronicleCollection.Name)]` goes on the outer class and is
123
+ required for isolation.
124
+ - `Establish` seeds preconditions; `Because` performs the action under test.
125
+ - Declare `Result` nullable.
126
+ - The outer class receives `context` through xUnit constructor injection.
@@ -0,0 +1,2 @@
1
+ Copyright (c) Cratis. All rights reserved.
2
+ Licensed under the MIT license. See LICENSE file in the project root for full license information.
@@ -0,0 +1,202 @@
1
+ ---
2
+ name: cratis-specifications-typescript
3
+ description: Write TypeScript specifications in the Cratis BDD style using the given() helper, reusable context classes, Sinon stubbing, and the Chai .should fluent interface. Use when adding or restructuring TypeScript specifications, building a given/ context class, or laying out a for_/when_ specification folder. Do not use for C# specifications, and do not use it to decide what the code under specification should do.
4
+ license: MIT
5
+ ---
6
+
7
+ # Cratis TypeScript specifications
8
+
9
+ Specifications describe behavior, not implementation. The `given()` helper and
10
+ its context class are the TypeScript counterpart of the C# `Specification`
11
+ base: setup is separated from the action, and each `it()` verifies one outcome.
12
+
13
+ ## Verified product sources
14
+
15
+ This skill is verified against the frameworks these specifications run on:
16
+
17
+ | Framework | Role |
18
+ | --- | --- |
19
+ | Vitest | Test runner (`yarn test` from the package root) |
20
+ | Mocha-style structure | `describe`, `it`, `beforeEach`, `afterEach` |
21
+ | Chai | Assertions, always through the `.should` fluent interface |
22
+ | Sinon | Stubbing and call verification |
23
+
24
+ The `given()` helper is supplied by the Cratis TypeScript package under
25
+ specification and is imported from that package root. Reverify the helper's
26
+ location and signature against the owning repository before relying on a
27
+ different import path.
28
+
29
+ ## Route near misses
30
+
31
+ - The specification is C#: use `cratis-specifications-csharp` instead.
32
+ - The subject is an application view model or React component rather than a
33
+ Cratis TypeScript package: the plain `describe`/`beforeEach` frontend style
34
+ applies, not the `given()` helper in this skill.
35
+ - The question is what the code under specification *should do*: resolve the
36
+ behavior first; this skill only specifies already decided behavior.
37
+
38
+ ## Step 1 — Create the folder structure
39
+
40
+ Specifications live alongside the source they describe:
41
+
42
+ ```
43
+ for_<ClassName>/
44
+ ├── given/
45
+ │ └── a_<system_under_test>.ts ← reusable context class
46
+ ├── when_<behavior>/ ← a behavior with multiple outcomes
47
+ │ ├── with_<condition>.ts
48
+ │ ├── without_<condition>.ts
49
+ │ └── and_<condition>.ts
50
+ └── when_<simple_behavior>.ts ← a single outcome is a single file
51
+ ```
52
+
53
+ Paths read as English sentences. Allowed outcome prefixes are `and_`, `with_`,
54
+ `without_`, `having_`, and `given_`.
55
+
56
+ **`when` belongs only in a `when_<behavior>` folder name.** A specification file
57
+ or non-`when_` folder must never contain the word `when`. Fold the context into
58
+ the `when_` folder name and use preposition files for the outcomes. Add a
59
+ sub-folder under `when_` only when that condition has its own multiple outcomes.
60
+
61
+ ## Step 2 — Write a reusable context class
62
+
63
+ ```ts
64
+ // for_<ClassName>/given/a_<system_under_test>.ts
65
+ import sinon from 'sinon';
66
+ import { <ClassName> } from '../../../<ClassName>';
67
+
68
+ export class a_<system_under_test> {
69
+ <collaborator>: sinon.StubbedInstance<<CollaboratorType>>;
70
+ <sut>: <ClassName>;
71
+
72
+ constructor() {
73
+ this.<collaborator> = sinon.createStubInstance(<CollaboratorClass>);
74
+ this.<collaborator>.<method>.returns(<default>);
75
+ this.<sut> = new <ClassName>(this.<collaborator>);
76
+ }
77
+ }
78
+ ```
79
+
80
+ Context properties are **public** — the specification reads them through the
81
+ `context` parameter. This differs from C#, where `given/` fields are
82
+ `protected`. Name the class `a_` or `an_` so it reads as "given a service, when
83
+ registering".
84
+
85
+ A context captures the world as it exists *before* the action. Never put the
86
+ action under specification in a context.
87
+
88
+ ## Step 3 — Write the specification
89
+
90
+ ```ts
91
+ // for_<ClassName>/when_<behavior>/with_<condition>.ts
92
+ import { a_<system_under_test> } from '../given/a_<system_under_test>';
93
+ import { given } from '../../given';
94
+
95
+ describe('when <behavior> with <condition>', given(a_<system_under_test>, context => {
96
+ let result: <ResultType>;
97
+
98
+ beforeEach(async () => {
99
+ result = await context.<sut>.<action>(<input>);
100
+ });
101
+
102
+ it('should <expected outcome>', () => {
103
+ result.should.equal(<expected>);
104
+ });
105
+
106
+ it('should <other expected outcome>', () => {
107
+ context.<collaborator>.<method>.calledOnce.should.be.true;
108
+ });
109
+ }));
110
+ ```
111
+
112
+ `given()` instantiates the context class per specification so setup stays
113
+ isolated. `beforeEach` performs the single action under specification; each
114
+ `it()` asserts one outcome.
115
+
116
+ ## Step 4 — Skip the context when nothing is shared
117
+
118
+ A behavior with no shared setup needs no context class:
119
+
120
+ ```ts
121
+ describe('when <behavior>', () => {
122
+ let result: <ResultType>;
123
+
124
+ beforeEach(() => {
125
+ result = <TypeUnderSpecification>.<action>(<input>);
126
+ });
127
+
128
+ it('should <expected outcome>', () => {
129
+ result.<property>.should.equal(<expected>);
130
+ });
131
+ });
132
+ ```
133
+
134
+ ## Step 5 — Assert with the `.should` fluent interface
135
+
136
+ **Always use `.should`. Never use `expect()`.** The fluent style reads as a
137
+ sentence and matches the rest of the corpus.
138
+
139
+ ```ts
140
+ value.should.equal(<expected>);
141
+ value.should.deep.equal(<expectedObject>);
142
+ flag.should.be.true;
143
+ value.should.be.null;
144
+ value.should.not.be.undefined;
145
+ items.should.contain(<expected>);
146
+ items.should.have.lengthOf(<count>);
147
+ items.should.be.empty;
148
+ value.should.be.instanceOf(<Type>);
149
+ (() => <throwingCall>()).should.throw(<ErrorType>);
150
+ ```
151
+
152
+ Full Sinon and Chai detail is in
153
+ [typescript-patterns.md](references/typescript-patterns.md).
154
+
155
+ ## Step 6 — Apply the naming conventions
156
+
157
+ | Element | Convention | Example |
158
+ | --- | --- | --- |
159
+ | `describe()` text | Natural-language sentence | `'when registering with a valid name'` |
160
+ | `it()` text | Starts with "should", **uses spaces** | `'should append an event'` |
161
+ | Context class | `a_` or `an_` prefix | `an_author_service` |
162
+ | Specification file | Preposition prefix | `with_valid_name.ts` |
163
+
164
+ Use spaces in `it()` descriptions, never underscores — they appear in runner
165
+ output as human-readable sentences. This is the deliberate difference from the C#
166
+ `should_` method naming.
167
+
168
+ ## Step 7 — Keep one behavior per file
169
+
170
+ Do not mix orthogonal behaviors in one specification. Separate folders per
171
+ behavior keep failures precise:
172
+
173
+ ```
174
+ when_items_are_added_as_delta/and_item_is_identified_by_a_guid.ts
175
+ when_items_are_removed_as_delta/and_item_is_identified_by_a_guid.ts
176
+ ```
177
+
178
+ A single file validating both add-delta and remove-delta behavior tangles
179
+ unrelated outcomes and hides which one broke.
180
+
181
+ ## What not to specify
182
+
183
+ - Simple property getters and setters.
184
+ - Properties that return a constructor parameter directly.
185
+ - Trivial delegation.
186
+ - Anything a `describe` beginning "when getting" or "when returning" would
187
+ describe — that is a getter, not a behavior.
188
+
189
+ ## Verify
190
+
191
+ - Each specification file states one `describe`, one action in `beforeEach`, and
192
+ one or more single-outcome `it()` assertions.
193
+ - No path contains `when` outside a `when_<behavior>` folder name.
194
+ - Outcome files use an allowed preposition prefix.
195
+ - Context classes carry public properties, are named `a_`/`an_`, and contain no
196
+ action under specification.
197
+ - Every assertion uses `.should`; no `expect()` appears.
198
+ - `it()` descriptions start with "should" and use spaces.
199
+ - Stubs are restored between specifications where Sinon state would otherwise
200
+ leak.
201
+ - Nothing trivial is specified.
202
+ - `yarn test` passes from the package root.