@drunkcoding/dknet-implementation-skills 0.1.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 (36) hide show
  1. package/.claude-plugin/marketplace.json +22 -0
  2. package/.claude-plugin/plugin.json +38 -0
  3. package/LICENSE +21 -0
  4. package/README.md +261 -0
  5. package/agents/dknet-architect.md +49 -0
  6. package/agents/dknet-bdd-engineer.md +62 -0
  7. package/agents/dknet-implementer.md +75 -0
  8. package/package.json +52 -0
  9. package/plugin.json +26 -0
  10. package/skills/README.md +47 -0
  11. package/skills/dknet-auth-and-ownership/SKILL.md +418 -0
  12. package/skills/dknet-bdd-tests/SKILL.md +355 -0
  13. package/skills/dknet-bdd-tests/checklist.md +39 -0
  14. package/skills/dknet-crud/SKILL.md +483 -0
  15. package/skills/dknet-ddd-principles/SKILL.md +87 -0
  16. package/skills/dknet-docs/SKILL.md +296 -0
  17. package/skills/dknet-docs/checklist.md +58 -0
  18. package/skills/dknet-docs/templates/README-template.md +68 -0
  19. package/skills/dknet-docs/templates/api-reference-template.md +275 -0
  20. package/skills/dknet-docs/templates/architecture-template.md +166 -0
  21. package/skills/dknet-docs/templates/data-model-template.md +99 -0
  22. package/skills/dknet-docs/templates/events-template.md +155 -0
  23. package/skills/dknet-dto-mapping/SKILL.md +278 -0
  24. package/skills/dknet-efcore-config/SKILL.md +379 -0
  25. package/skills/dknet-endpoint/SKILL.md +458 -0
  26. package/skills/dknet-entity/SKILL.md +483 -0
  27. package/skills/dknet-feature/SKILL.md +139 -0
  28. package/skills/dknet-feature-lifecycle/SKILL.md +144 -0
  29. package/skills/dknet-feature-remove/SKILL.md +131 -0
  30. package/skills/dknet-messaging-events/SKILL.md +395 -0
  31. package/skills/dknet-package-adoption/SKILL.md +252 -0
  32. package/skills/dknet-platform-config/SKILL.md +342 -0
  33. package/skills/dknet-project-structure/SKILL.md +148 -0
  34. package/skills/dknet-queries-specs/SKILL.md +330 -0
  35. package/skills/dknet-scaffold/SKILL.md +209 -0
  36. package/skills/dknet-unit-tests/SKILL.md +382 -0
@@ -0,0 +1,355 @@
1
+ ---
2
+ name: dknet-bdd-tests
3
+ description: Create and maintain Reqnroll + NUnit BDD .feature scenarios and step bindings in Minimal.App.BDDTests — request/status/response-body scenarios and domain-event side effects observed via log capture, for the hand-written PurchaseOrder and generator-driven Product samples. Use when adding or updating HTTP-facing scenarios for a DKNet.Templates feature. Result-level Result-object assertions, architecture rules and pure functional tests belong in the `dknet-unit-tests` skill instead — do not duplicate a behavior here that xUnit already covers. Invoke as `/dknet-bdd-tests <Feature>` to scaffold it for a feature.
4
+ metadata:
5
+ kind: workflow
6
+ arguments: "<Feature> e.g. Orders"
7
+ allowed-tools: Read, Grep, Glob, Edit, Write, Bash
8
+ ---
9
+
10
+ Usage: `/dknet-bdd-tests <Feature> e.g. Orders`
11
+
12
+ # BDD tests (Minimal.App.BDDTests)
13
+
14
+ ## What BDD owns vs xUnit
15
+
16
+ BDD owns user-facing HTTP behavior: request → status code → response body, and domain-event side effects
17
+ observed through captured log lines. xUnit (`dknet-unit-tests`) owns architecture/convention rules, pure
18
+ functional tests (entity methods, validators, specs), and result-level integration assertions on the
19
+ handler's `IResult`/`IResultBase` object. Schema/model/migration assertions never belong in BDD. Don't
20
+ write a BDD scenario for a rule already proven at the `Result` level in xUnit unless it's the one place
21
+ that rule is reachable over HTTP — and don't add a duplicate xUnit HTTP test for a rule a BDD scenario
22
+ already proves.
23
+
24
+ Both shipped suites are teaching material: every scenario must be about the `PurchaseOrder` (manual) or
25
+ `Product` (automated) sample's business behavior, never about logging, health checks, CORS, security
26
+ headers, rate limits, or config binding.
27
+
28
+ ## Infrastructure
29
+
30
+ `Support/BddApiFactory : TestApiFactoryBase("bdd-tests")` (built on the same base as the xUnit suite's
31
+ `ApiFixture` — see `dknet-unit-tests` for what the base class provides) overrides two things:
32
+
33
+ - `AddFeatureOverrides` sets `FeatureManagement:RequireAuthorization = "false"` unconditionally, and
34
+ `ConnectionStrings:Redis` only for the one `@redis`-tagged scenario.
35
+ - `ConfigureTestServices` swaps in `UnexpectedErrorTriggerMapper` (an `IMapper` that throws when mapping a
36
+ `PurchaseOrder` named `"__drk1515-unexpected-error-trigger__"`, the suite's only reachable seam for a
37
+ genuine unhandled exception through a real route) and, for the `@redis` scenario, swaps the idempotency
38
+ key store for the Redis-backed one.
39
+
40
+ `Support/ApiHooks` is a static `[Binding]` class:
41
+
42
+ ```csharp
43
+ [BeforeTestRun]
44
+ public static void BeforeTestRun()
45
+ {
46
+ _factory = new BddApiFactory();
47
+ _client = _factory.CreateClient();
48
+ }
49
+
50
+ [BeforeScenario(Order = 0)]
51
+ public async Task BeforeScenarioAsync()
52
+ {
53
+ await _factory.ResetDatabaseAsync();
54
+ _factory.LogCapture.Clear();
55
+ objectContainer.RegisterInstanceAs<HttpClient>(_client);
56
+ objectContainer.RegisterInstanceAs(_factory);
57
+ objectContainer.RegisterInstanceAs(new ScenarioState());
58
+ }
59
+ ```
60
+
61
+ The host boots once for the whole run; every scenario gets a reset database, a cleared `LogCapture`, and
62
+ fresh `ScenarioState`. **Never add a second `[BeforeTestRun]`** — Reqnroll runs it at assembly level, and a
63
+ second one races the shared host's startup/teardown.
64
+
65
+ `Support/ScenarioState` is a plain bag (`Response`, `ResponseBody`) a step class writes to and later steps
66
+ read from — inject it by constructor, same as `HttpClient` and `BddApiFactory`.
67
+
68
+ `Support/CommonSteps` holds steps shared by more than one feature so exact wording isn't duplicated (which
69
+ Reqnroll would flag as ambiguous): `the request is rejected`, `the response status is (\d+)`, `the response
70
+ is (\d+)` (a second wording for the same assertion — the acceptance criteria used frozen text, not
71
+ paraphrased to match house style), `catalogue-ops holds the scope "..."` /
72
+ `holds every scope the operation needs` / `is signed in` (no-ops — the BDD host runs with
73
+ `RequireAuthorization = false`, so no policy is ever evaluated; scope-gated behavior is xUnit's
74
+ `AuthOnApiFixture` territory), `the response body carries a trace identifier`, `... carries a code naming
75
+ the rule that refused` (asserts the `precondition.` prefix plus a rule segment, not just a non-empty
76
+ string), and `the response names the field it refused`. Reuse these before writing a new one with the same
77
+ meaning.
78
+
79
+ ## Layout
80
+
81
+ ```
82
+ Features/<Domain>/<Name>.feature
83
+ Features/<Domain>/Steps/<Name>Steps.cs
84
+ ```
85
+
86
+ Each step class is `[Binding]`, constructor-injects `HttpClient client, ScenarioState state, BddApiFactory
87
+ factory` (and any other step class it needs data from — `ProductSteps` injects `PurchaseOrderSteps` to look
88
+ up a purchase order id created cross-feature). Step regex must match feature text exactly; a phrase
89
+ typo'd between the `.feature` and the `[Given]/[When]/[Then]` attribute leaves the step undefined at run
90
+ time, not a compile error. Tag scenarios by domain (`@PurchaseOrder`, `@Product`) and, for cross-cutting
91
+ integration scenarios, `@integration`, filterable with `--filter "TestCategory=PurchaseOrder"`. Use
92
+ `Scenario Outline` + `Examples` for a validation table (several inputs, one shared assertion shape) instead
93
+ of repeating near-identical scenarios.
94
+
95
+ ## Worked example — PurchaseOrder (manual mode, full feature)
96
+
97
+ `Features/PurchaseOrders/PurchaseOrder.feature`:
98
+
99
+ ```gherkin
100
+ @PurchaseOrder
101
+ Feature: Purchase order lifecycle (manual sample)
102
+ Background:
103
+ Given the service is running with no Redis connection configured
104
+
105
+ Scenario: Creating a purchase order persists it and returns its details
106
+ When I create a purchase order for customer "Acme Pte Ltd" with amount 250.00
107
+ Then the response status is 201
108
+ And the purchase order response has customer name "Acme Pte Ltd" and amount 250.00
109
+ And the purchase order response status is "placed"
110
+
111
+ Scenario: Replaying the same idempotency key on create does not create a second order
112
+ When I create a purchase order for customer "Acme Pte Ltd" with amount 250.00 using idempotency key "11111111-1111-1111-1111-111111111111"
113
+ And I replay the same create request with idempotency key "11111111-1111-1111-1111-111111111111"
114
+ Then both responses report the same purchase order id
115
+
116
+ Scenario: Cancelling a purchase order succeeds once and fails the second time
117
+ Given a purchase order exists for customer "Initech LLC" with amount 50.00
118
+ When I cancel that purchase order
119
+ Then the response status is 200
120
+ When I cancel that purchase order again
121
+ Then the response status is 409
122
+
123
+ Scenario Outline: Creating a purchase order rejects invalid input
124
+ When I create a purchase order for customer "<customerName>" with amount <amount>
125
+ Then the response status is 400
126
+ Examples:
127
+ | customerName | amount |
128
+ | | 100.00 |
129
+ | Acme Pte Ltd | 0 |
130
+
131
+ Scenario: Creating a purchase order without an idempotency key is rejected
132
+ When I create a purchase order for customer "Acme Pte Ltd" with amount 100.00 without an idempotency key
133
+ Then the request is rejected
134
+ ```
135
+
136
+ Key step bodies from `Steps/PurchaseOrderSteps.cs` — a fresh `Guid.NewGuid()` per plain create, a caller
137
+ supplied key for the idempotency scenarios, and the header on every create:
138
+
139
+ ```csharp
140
+ [When(@"I create a purchase order for customer ""(.*)"" with amount (.*)")]
141
+ public Task WhenICreateAPurchaseOrder(string customerName, decimal amount) =>
142
+ CreateAsync(customerName, amount, Guid.NewGuid().ToString());
143
+
144
+ private async Task CreateAsync(string customerName, decimal amount, string idempotencyKey)
145
+ {
146
+ using var request = new HttpRequestMessage(HttpMethod.Post, "/v1/purchase-orders")
147
+ {
148
+ Content = JsonContent.Create(new { customerName, amount })
149
+ };
150
+ request.Headers.Add("X-Idempotency-Key", idempotencyKey);
151
+ state.Response = await client.SendAsync(request);
152
+ state.ResponseBody = await state.Response.Content.ReadAsStringAsync();
153
+ if (state.Response.IsSuccessStatusCode)
154
+ {
155
+ var dto = JsonSerializer.Deserialize<PurchaseOrderDto>(state.ResponseBody, SharedConsts.JsonSerializerOptions);
156
+ _lastId = dto!.Id;
157
+ }
158
+ }
159
+ ```
160
+
161
+ Update/cancel/delete/get reuse `_lastId` from the last create; list deserializes the response as a bare
162
+ JSON array (`List<PurchaseOrderDto>`, the hand-mapped route's own shape) via
163
+ `SharedConsts.JsonSerializerOptions` — always deserialize through that options instance, not a fresh
164
+ default one, so casing matches what the API actually emits.
165
+
166
+ ## Worked example — Product (automated mode: event log + precondition 409)
167
+
168
+ `Features/Products/Product.feature` proves the generator-driven CRUD slice. Two scenarios worth reading in
169
+ full:
170
+
171
+ ```gherkin
172
+ Scenario: Creating a product raises the internal ProductCreatedEvent
173
+ When I create a product named "Widget" with price 9.99
174
+ Then a log line reports the automated sample product was created
175
+
176
+ @integration
177
+ Scenario: A product name that is already taken is refused
178
+ Given catalogue-ops holds the scope "products.write"
179
+ And the product "Widget" priced 9.99 SGD exists
180
+ When catalogue-ops creates the product "Widget" priced 12.00 SGD
181
+ Then the response is 409
182
+ And exactly 1 product is named "Widget"
183
+ ```
184
+
185
+ The log-line step just asserts on `LogCapture.Messages` — no polling needed here because the response body
186
+ already round-trips through the handler that logs synchronously before returning; if a future scenario
187
+ asserts on an *async consumer's* log line instead (a domain event's own subscriber, not the handler that
188
+ raised it), poll with `Eventually.IsTrueAsync(...)` from `Minimal.App.TestSupport` rather than asserting
189
+ immediately — the in-memory bus publishes non-blocking:
190
+
191
+ ```csharp
192
+ [Then("a log line reports the automated sample product was created")]
193
+ public void ThenALogLineReportsTheAutomatedSampleProductWasCreated() =>
194
+ factory.LogCapture.Messages.ShouldContain(m => m.Contains("AutomatedSample product created", StringComparison.Ordinal));
195
+ ```
196
+
197
+ Product create sends **no** `X-Idempotency-Key` header at all — the generated create route has no
198
+ `.RequiredIdempotentKey()` call, so a replayed request is a fresh create, not a replay. Never assert
199
+ idempotent-replay behavior on a generated create route.
200
+
201
+ Another scenario in the same file documents a real, permanent gap rather than a bug to fix:
202
+
203
+ ```gherkin
204
+ Scenario: A negative price is still accepted — a known and accepted limitation of the generated path
205
+ When I create a product named "Broken Widget" with price -1
206
+ Then the response status is 201
207
+ ```
208
+
209
+ `[Range(0.01, double.MaxValue)]` is forwarded onto the generated request but never evaluated, because the
210
+ .NET 10 minimal-API validation source generator can't see through `DKNet.AspCore.Extensions`'s generic
211
+ `MapPost<TRequest,TResponse>` wrapper. **Never assert 400 for a DataAnnotations violation on a generated
212
+ create/update route** — write the scenario the way this one is written, asserting the (surprising) 201.
213
+
214
+ `ProductList.feature` is a regression fence around the generic `MapGetList<Product, Guid, ProductDto>()`
215
+ list route: paging envelope (`totalItemCount`/`hasNextPage`/`hasPreviousPage`), `pageSize` clamped (not
216
+ rejected) above the max, `orderBy`/`desc`, `filter=field:op:value` (including the `In` operator and
217
+ multiple filters ANDed), `search` (substring, 2-char minimum), and two security-boundary scenarios: an
218
+ unknown filter field 400s, and filtering by an excluded DTO column (`ownedBy`, excluded via
219
+ `[GenerateDto(... Exclude ...)]`) 400s rather than silently matching against the underlying entity. Summarize
220
+ new list-contract scenarios the same way when a feature's list route gains its own filters or computed DTO
221
+ fields.
222
+
223
+ ## Assertion depth
224
+
225
+ Assert status code, then the fields the scenario is actually about — not just "success". The manual
226
+ sample's error body shape: `errors[]` (each with `message`, optional `code`/`field`), `code`, `traceId`
227
+ (see `CommonSteps`'s trace-identifier and precondition-code steps above). Prefer a structured JSON property
228
+ check (`JsonDocument.Parse(...).RootElement.GetProperty(...)`) over a raw substring match on the response
229
+ body when the contract has a real shape to check against; a substring check is fine for a fixed literal
230
+ like `"status":"placed"` where the surrounding shape isn't in question.
231
+
232
+ ## Mode differences
233
+
234
+ - **manual** (PurchaseOrder): create requires `X-Idempotency-Key`; a missing key is rejected, a replayed
235
+ key returns the same order id. Acting user comes from `[FromClaim(ClaimTypes.Name)] ByUser`.
236
+ - **auto** (Product): create has no idempotency key and does not enforce DataAnnotations — never write a
237
+ scenario expecting either. The acting user for audit stamps comes from the demo identity: the BDD host
238
+ runs with `EnableDemoAuthentication = true` (`appsettings.Testing.json`) and
239
+ `RequireAuthorization = false` (`BddApiFactory`), so every request is the built-in demonstration caller,
240
+ not an anonymous one — `catalogue-ops holds the scope "..."` steps are no-ops for exactly that reason.
241
+
242
+ ## Commands
243
+
244
+ ```bash
245
+ dotnet test ApiEndpoints/Minimal.App.BDDTests/Minimal.App.BDDTests.csproj --filter "TestCategory=PurchaseOrder"
246
+ ```
247
+
248
+ ## Step-by-step
249
+
250
+ 1. Confirm the behavior is HTTP-shaped (request → status → body) or an event side-effect via log capture —
251
+ otherwise it belongs in `dknet-unit-tests`.
252
+ 2. Add `Features/<Domain>/<Name>.feature` under the right tag; reuse `CommonSteps` wording before coining
253
+ new phrasing for something already covered (rejected request, status code, trace id, precondition code).
254
+ 3. Add `Features/<Domain>/Steps/<Name>Steps.cs`, `[Binding]`, constructor-inject `HttpClient`,
255
+ `ScenarioState`, `BddApiFactory`, and any other step class whose state you need.
256
+ 4. Manual-mode create: generate a fresh `Guid.NewGuid()` for `X-Idempotency-Key` in each `[When]` step that
257
+ creates a resource, unless the scenario is specifically about idempotency (use a fixed key there).
258
+ 5. Cover: happy path, not-found, a guarded transition refused on retry, and (auto mode) the event's log
259
+ line and the generated route's un-enforced-DataAnnotations behavior where relevant.
260
+ 6. Run the filtered test for the tag before the full BDD project.
261
+
262
+ ## Common mistakes
263
+
264
+ - **What you might expect**: a generated Product create route rejects an idempotency replay like
265
+ PurchaseOrder does. **What actually happens**: it creates a second product. **Why**: the generated route
266
+ has no `.RequiredIdempotentKey()` call — there is nothing to replay against.
267
+ - **What you might expect**: a negative price on `Product` create returns 400. **What actually happens**:
268
+ 201. **Why**: the forwarded `[Range]` DataAnnotation is never evaluated on a generic-wrapper route — this
269
+ is documented, accepted behavior, not a bug to "fix" with a failing scenario.
270
+ - **What you might expect**: asserting a log line right after the response comes back is safe. **What
271
+ actually happens**: it's sometimes missing for an async consumer. **Why**: the in-memory bus publishes
272
+ non-blocking; poll instead of asserting immediately when the log line comes from a separate event
273
+ consumer rather than the handling code itself.
274
+ - **What you might expect**: adding a second `[BeforeTestRun]` hook in a new feature's step file to set up
275
+ feature-specific state. **What actually happens**: it races `ApiHooks`'s host bootstrap. **Why**: Reqnroll
276
+ runs every `[BeforeTestRun]` at assembly level with no defined ordering guarantee between classes; put
277
+ one-time setup in `ApiHooks` or in `[BeforeScenario]` instead.
278
+ - **What you might expect**: forgetting `X-Idempotency-Key` on a manual-sample create just needs a retry.
279
+ **What actually happens**: the request is rejected outright (400/`the request is rejected`) — the header
280
+ is required, not merely deduplicating.
281
+
282
+ ---
283
+
284
+ # Workflow: `/dknet-bdd-tests`
285
+
286
+ The procedure an agent follows when invoked with arguments. The reference sections above are the rules it applies.
287
+
288
+ You are DKNet BDD Test Engineer.
289
+
290
+ Your job is to create, update, and validate BDD scenarios for this repository with contract-first assertions and deterministic step bindings.
291
+
292
+ ### User Input
293
+
294
+ $ARGUMENTS
295
+
296
+ ### Required Skill Loading
297
+
298
+ Before any BDD design or edits:
299
+ 1. Load and follow the BDD skill at the reference sections above.
300
+ 2. Use this skill's `checklist.md` as the completion gate.
301
+
302
+ ### Scope
303
+
304
+ Work only on BDD test artifacts and closely related support wiring:
305
+ - `ApiEndpoints/Minimal.App.BDDTests/Features/**/*.feature`
306
+ - `ApiEndpoints/Minimal.App.BDDTests/Features/**/Steps/*.cs`
307
+ - `ApiEndpoints/Minimal.App.BDDTests/Support/*.cs`
308
+ - `ApiEndpoints/Minimal.App.BDDTests/*.csproj`
309
+
310
+ ### Constraints
311
+
312
+ - Use `specs/<feature>/contracts/*` as the assertion source of truth.
313
+ - Treat `docs/features/**` and `specs/**` as reference context for scenario coverage and wording.
314
+ - Keep step phrases and `[Given]/[When]/[Then]` attributes exactly matched.
315
+ - Validate response at three levels whenever applicable:
316
+ - HTTP status code
317
+ - response structure (`isSuccess`, `value`, `errors`, required objects/arrays)
318
+ - key data fields and expected values
319
+ - Use `SharedConsts.JsonSerializerOptions` for request serialization.
320
+ - Include required request headers when contracts require them. A **manual-flow** create route
321
+ requires a fresh `Guid.NewGuid()` `X-Idempotency-Key` per `[When]` step; an **automated-flow**
322
+ generated create route has no idempotency filter, so do not assert replay behavior against it.
323
+ - Assert only behavior the endpoint actually has. An automated-flow route does not enforce its
324
+ forwarded DataAnnotations — a scenario expecting `400` from an out-of-range value will fail against
325
+ a `201`. Cover that gap by asserting what happens, or leave it to the manual flow.
326
+ - Do not implement unrelated domain/business logic outside BDD test scope.
327
+
328
+ ### Workflow
329
+
330
+ 1. Build context map from:
331
+ - `docs/features/<feature>/`
332
+ - `specs/<feature>/spec.md`
333
+ - `specs/<feature>/contracts/*`
334
+ 2. Produce or update `.feature` scenarios:
335
+ - Happy path
336
+ - Business-rule failure
337
+ - Validation failure
338
+ 3. Implement/adjust step bindings in `Steps/*.cs`.
339
+ 4. Run validation:
340
+ - `dotnet build -c Release`
341
+ - `dotnet test ApiEndpoints/Minimal.App.BDDTests`
342
+ 5. Report:
343
+ - changed files
344
+ - scenario count
345
+ - pass/fail results
346
+ - unresolved contract gaps (if any)
347
+
348
+ ### Output Format
349
+
350
+ Always provide:
351
+ 1. BDD phase status
352
+ 2. Artifacts changed
353
+ 3. Assertion coverage summary (status + shape + key fields)
354
+ 4. Test results summary
355
+ 5. Remaining risks or blockers
@@ -0,0 +1,39 @@
1
+ # BDD Scenario Skill Checklist
2
+
3
+ Use this checklist before considering BDD scenario work complete.
4
+
5
+ ## Context Coverage
6
+
7
+ - [ ] Confirmed the behavior is HTTP-shaped (request → status → body) or an event side effect via log
8
+ capture — otherwise it belongs in the `dknet-unit-tests` skill instead
9
+ - [ ] Reviewed the feature's AppServices request/handler code as the assertion source of truth (status
10
+ codes, error codes, response fields)
11
+ - [ ] Checked `CommonSteps` for an existing step with the same meaning before writing a new one
12
+ - [ ] Captured at least one edge case (not-found, a guarded transition refused on retry, invalid input)
13
+
14
+ ## Scenario Quality
15
+
16
+ - [ ] `.feature` file has clear business title and purpose
17
+ - [ ] Includes happy path scenario
18
+ - [ ] Includes business-rule failure scenario
19
+ - [ ] Includes validation failure scenario
20
+ - [ ] Uses stable domain language (no implementation jargon)
21
+
22
+ ## Binding Quality
23
+
24
+ - [ ] Every step has exactly one matching `[Given]/[When]/[Then]` binding
25
+ - [ ] Constructor injection uses scenario-registered dependencies
26
+ - [ ] Request serialization uses `SharedConsts.JsonSerializerOptions`
27
+ - [ ] Required headers (for example idempotency) are present
28
+ - [ ] Assertions verify status code, contract-defined JSON structure, and key data fields
29
+ - [ ] For generated response DTO contracts, assertions include representative entity-derived fields
30
+ - [ ] Success assertions validate required `value` fields (not only success flag)
31
+ - [ ] Failure assertions validate `errors` array/object shape and expected messages/codes
32
+ - [ ] No assertion relies only on substring matching when structured contract fields exist
33
+
34
+ ## Validation
35
+
36
+ - [ ] `dotnet build -c Release` succeeds
37
+ - [ ] `dotnet test ApiEndpoints/Minimal.App.BDDTests/Minimal.App.BDDTests.csproj` passes
38
+ - [ ] No undefined or pending Reqnroll steps
39
+ - [ ] Scenario names are readable in test output