@luckystack/test-runner 0.10.2 → 0.12.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 (40) hide show
  1. package/AGENTS.md +7 -0
  2. package/CHANGELOG.md +13 -0
  3. package/README.md +13 -68
  4. package/dist/authEnforcementCheck.d.ts +12 -0
  5. package/dist/contractCheck.d.ts +24 -0
  6. package/dist/csrfEnforcementCheck.d.ts +25 -0
  7. package/dist/customTests.d.ts +145 -0
  8. package/dist/extensionRegistry.d.ts +83 -0
  9. package/dist/fuzzCheck.d.ts +12 -0
  10. package/dist/index.d.ts +49 -745
  11. package/dist/index.js.map +1 -1
  12. package/dist/probeRequest.d.ts +48 -0
  13. package/dist/rateLimitCheck.d.ts +14 -0
  14. package/dist/reporter.d.ts +3 -0
  15. package/dist/resetServerState.d.ts +13 -0
  16. package/dist/resolveTestBaseUrl.d.ts +7 -0
  17. package/dist/resolveTestEnvironment.d.ts +21 -0
  18. package/dist/runAllTests.d.ts +88 -0
  19. package/dist/runAuthEnforcementTests.d.ts +17 -0
  20. package/dist/runContractTests.d.ts +22 -0
  21. package/dist/runCsrfEnforcementTests.d.ts +29 -0
  22. package/dist/runFuzzTests.d.ts +16 -0
  23. package/dist/runRateLimitTests.d.ts +38 -0
  24. package/dist/runRegisteredLayers.d.ts +24 -0
  25. package/dist/sampleInputFor.d.ts +13 -0
  26. package/dist/schemaSampleInput.d.ts +12 -0
  27. package/dist/socketProbeTransport.d.ts +9 -0
  28. package/dist/streamWatcher.d.ts +37 -0
  29. package/dist/testDataMarker.d.ts +2 -0
  30. package/dist/testLayerHelpers.d.ts +55 -0
  31. package/dist/testSocket.d.ts +58 -0
  32. package/dist/types.d.ts +46 -0
  33. package/dist/walkEndpoints.d.ts +6 -0
  34. package/docs/auth-tests.md +4 -223
  35. package/docs/contract-tests.md +11 -307
  36. package/docs/extension-hooks.md +4 -467
  37. package/docs/fuzz-tests.md +4 -245
  38. package/docs/rate-limit-tests.md +9 -261
  39. package/package.json +6 -6
  40. package/CLAUDE.md +0 -117
@@ -1,470 +1,7 @@
1
- # Extension Hooks: Reporter, Layer and Fixture Registries
1
+ # Custom test extensions
2
2
 
3
- The four built-in layers (contract, auth, rate-limit, fuzz) cover the framework envelope and the security guards every LuckyStack project ships. The extension registry is where consumers add their own checks — CORS enforcement, multi-tenant isolation, business rules, GDPR data-flow checks — and pipe the combined results into Slack, a build dashboard, or any webhook. The hooks are intentionally narrow: three slots (layers, fixtures, reporters), each replace-by-key, all clearable from a single test-only function.
3
+ Built-in sweeps validate the framework envelope and common guards. Consumer extensions are for business authorization, tenant boundaries, CORS and other project-specific requirements that generated route metadata cannot prove. Fixtures and reporters exist to share setup and results without forcing those checks into framework code. The installed types and extension source define registration keys and result shapes.
4
4
 
5
- ## Slots
5
+ Registered layers see the same route selection as the sweeps: sync routes when sync metadata is passed, narrowed by the same filter. A sync route has no HTTP method, so a layer must not assume one. Each call also receives the schema-valid sample payload the contract sweep sends, so a layer can drive a route past input validation instead of only exercising its rejection path. Sample strings carry the run's test-data marker; rows created this way are not removed by the test reset.
6
6
 
7
- | Slot | Purpose | Cardinality |
8
- | --- | --- | --- |
9
- | Test layers | Additional per-endpoint checks. Each layer has a unique `name`. | Many. Replace-by-name. |
10
- | Fixtures | Realistic valid/invalid payloads keyed by a type identifier. | Many. Replace-by-typeKey. |
11
- | Reporter | Single sink for per-result and per-summary events plus optional webhook. | One. Replace by re-registering or unregister with `null`. |
12
-
13
- The registries are in-memory and process-local. They survive across calls within one process and reset only via `resetTestExtensionsForTests()`. There is no persistence — registration happens at module load.
14
-
15
- ## Functions
16
-
17
- ### `logContractResult(result)`
18
-
19
- Pretty-print one `ContractCheckResult` to stdout. Wire it as `onResult` in any of the four built-in sweeps to get live per-endpoint output.
20
-
21
- Signature:
22
-
23
- ```ts
24
- logContractResult(result: ContractCheckResult): void
25
- ```
26
-
27
- Output format:
28
-
29
- - Pass success: `[PASS] POST /api/billing/getInvoice/v1 12ms http=200`
30
- - Pass typed error: `[PASS] POST /api/auth/login/v1 8ms http=200 (error: auth.required)`
31
- - Skip: `[SKIP] POST /api/uploads/fileUpload/v1 0ms Explicitly skipped`
32
- - Fail: `[FAIL] POST /api/billing/cancelSubscription/v1 14ms http=500 reason=fuzz payload produced non-envelope response: ...`
33
-
34
- Status tag is one of `[PASS]`, `[FAIL]`, `[SKIP]`. `http=-` appears when no HTTP status is captured (network error, abort). `durationMs` is always present.
35
-
36
- ### `logContractSummary(summary)`
37
-
38
- Pretty-print one `RunContractSummary` to stdout. Call it once at the end of a sweep — typically after `logContractResult` has streamed the individual lines.
39
-
40
- Signature:
41
-
42
- ```ts
43
- logContractSummary(summary: RunContractSummary): void
44
- ```
45
-
46
- Output:
47
-
48
- ```
49
- Contract tests: 47/50 passed, 2 failed, 1 skipped
50
-
51
- Failures:
52
- - /api/billing/cancelSubscription/v1: fuzz payload produced non-envelope response: { nested: ...
53
- - /api/integrations/stripeWebhook/v1: Response missing standard `status` envelope
54
- ```
55
-
56
- The header line is always printed. The failures block is only printed when `summary.failed > 0`. Skipped endpoints are counted but not enumerated.
57
-
58
- ### `registerTestLayer(layer)`
59
-
60
- Add (or replace by name) a custom test layer in the registry.
61
-
62
- Signature:
63
-
64
- ```ts
65
- registerTestLayer(layer: TestLayer): void
66
-
67
- interface TestLayer {
68
- name: string;
69
- run: (input: TestLayerInput) => Promise<TestLayerResult> | TestLayerResult;
70
- }
71
-
72
- interface TestLayerInput {
73
- endpoint: string; // resolved route, e.g. 'api/billing/getInvoice/v1' or 'sync/chat/send/v1'
74
- method?: string; // HTTP method, undefined for sync endpoints
75
- authToken?: string; // bearer/session token for auth-aware checks
76
- sampleInput?: unknown; // Zod-valid sample payload (same one the contract sweep sends), `{}` without schemas
77
- }
78
-
79
- interface TestLayerResult {
80
- passed: boolean;
81
- message?: string; // short reason key, i18n-style on failure
82
- metadata?: Record<string, unknown>;
83
- }
84
- ```
85
-
86
- Replace-by-name: registering a layer with a name that already exists overwrites the previous entry. Use this to update a layer mid-run, e.g. after fetching a fresh auth token.
87
-
88
- The built-in sweeps (contract/auth/rate-limit/fuzz) do **not** run registered layers — they only cover the framework envelope, and `runAllTests` does not call into the registry. To execute the registered layers, call **`runRegisteredLayers({ apiMethodMap })`** (documented below): it walks every registered layer against every endpoint, fans each result through the registered reporter, and POSTs the webhook. You can still drive the registry by hand — read it with `listTestLayers()`, iterate `walkEndpoints`, and call `layer.run(input)` per endpoint — but `runRegisteredLayers` is the supported entry point and adds error-isolation the manual loop does not.
89
-
90
- ### `listTestLayers()`
91
-
92
- Return the current registry snapshot.
93
-
94
- Signature:
95
-
96
- ```ts
97
- listTestLayers(): TestLayer[]
98
- ```
99
-
100
- Order is registration order (Map iteration order). The returned array is a fresh copy — mutating it does not affect the registry.
101
-
102
- ### `runRegisteredLayers(input)`
103
-
104
- Run every registered layer against every endpoint in `apiMethodMap` (plus every sync route in `syncMetaMap` when given) and emit the aggregate through the registered reporter. This is the shipped entry point for the layer registry — the built-in sweeps never touch it, and it never runs the built-in sweeps.
105
-
106
- Signature:
107
-
108
- ```ts
109
- runRegisteredLayers(input: RunRegisteredLayersInput): Promise<TestSummary>
110
-
111
- interface RunRegisteredLayersInput {
112
- apiMethodMap: ApiMethodMap;
113
- syncMetaMap?: SyncMetaMap; // also run layers against every sync route
114
- authToken?: string; // forwarded to each layer's run({ authToken })
115
- skip?: string[]; // '<page>/<name>' or '<page>/<name>/<version>'
116
- filter?: string; // substring on '<page>/<name>/<version>', same as the sweeps
117
- apiInputSchemas?: InputSchemaMap; // source of each API route's sampleInput
118
- syncInputSchemas?: InputSchemaMap; // source of each sync route's sampleInput
119
- inputFor?: (endpoint: EndpointDescriptor) => unknown; // custom sample factory; wins over the schema maps
120
- }
121
- ```
122
-
123
- Behavior (read from `src/runRegisteredLayers.ts`):
124
-
125
- - **Cartesian walk.** For each layer from `listTestLayers()` (registration order) × each selected endpoint (API routes, plus sync routes when `syncMetaMap` is given, narrowed by `filter`), it calls `layer.run({ endpoint, method, authToken, sampleInput })` — `method` is `undefined` for sync routes, and `sampleInput` comes from `inputFor` or the schema maps (strings tagged with the run's test-data marker), so a layer can exercise the route past validation. `skip` uses the same two-tier matching as the built-in sweeps; skipped endpoints produce no result.
126
- - **Error isolation.** Each `layer.run` is wrapped in `tryCatch`. A throwing layer (or one returning a falsy result) does not abort the walk — it is recorded as a failed `TestResult` whose `message` is the thrown error's message, or `'layer returned no result'`. Each result's `durationMs` is measured around the call. This is the behavior the manual `listTestLayers()` loop lacks, where a throw propagates and skips the rest.
127
- - **Reporter fan-out.** After each result it calls `reporter.onResult?.(result)`; after the walk it calls `reporter.onSummary?.(summary)`. Both are wrapped in `tryCatch`, so a throwing reporter callback never fails the run — unlike the built-in sweeps, whose `onResult` is called un-wrapped.
128
- - **Webhook.** If the reporter declares a `webhookUrl`, the summary is POSTed to it: `Content-Type: application/json`, plus `Authorization: Bearer <token>` when `webhookAuth?.type === 'bearer'`, body `JSON.stringify(summary)`, with a 10s `AbortController` timeout. The POST is best-effort (`tryCatch`-wrapped) — a failure never fails the run. A plaintext-`http:` URL pointed at a non-loopback host logs a `console.warn` (the summary carries endpoint paths, error codes, and any bearer token in clear) but is never blocked, since the URL is consumer-self-registered.
129
- - **Result.** Returns the `TestSummary` (`totalLayers`, `totalEndpoints`, `passed`, `failed`, `results`). Unlike the built-in sweeps, the per-result shape here is `TestResult` (the registry shape), not `ContractCheckResult`.
130
-
131
- ### `registerTestReporter(reporter)`
132
-
133
- Set the reporter slot, or clear it by passing `null`.
134
-
135
- Signature:
136
-
137
- ```ts
138
- registerTestReporter(reporter: TestReporter | null): void
139
-
140
- interface TestReporter {
141
- onResult?: (result: TestResult) => void | Promise<void>;
142
- onSummary?: (summary: TestSummary) => void | Promise<void>;
143
- webhookUrl?: string;
144
- webhookAuth?: { type: 'bearer'; token: string };
145
- }
146
-
147
- interface TestResult {
148
- layer: string;
149
- endpoint: string;
150
- passed: boolean;
151
- message?: string;
152
- durationMs?: number;
153
- metadata?: Record<string, unknown>;
154
- }
155
-
156
- interface TestSummary {
157
- totalLayers: number;
158
- totalEndpoints: number;
159
- passed: number;
160
- failed: number;
161
- results: TestResult[];
162
- }
163
- ```
164
-
165
- Replace-by-overwrite: only one reporter is active. To compose multiple sinks, write a single reporter that fans out internally.
166
-
167
- `runRegisteredLayers` drives this reporter directly: it emits a `TestResult` per layer×endpoint through `onResult`, calls `onSummary` once at the end, and POSTs `webhookUrl`. The built-in sweeps do NOT — they emit `ContractCheckResult` shapes, not `TestResult`, so for them the reporter is a coordination surface your cross-layer aggregator consumes: collect each built-in sweep's results, map them into `TestResult`, and emit through this reporter so all sinks see one stream.
168
-
169
- ### `getTestReporter()`
170
-
171
- Read the active reporter or `null` if none is registered.
172
-
173
- Signature:
174
-
175
- ```ts
176
- getTestReporter(): TestReporter | null
177
- ```
178
-
179
- ### `resetTestExtensionsForTests()`
180
-
181
- Clear all three registries (layers, fixtures, reporter). Intended for the runner's own internal tests — calling it from a production test harness will wipe everything other modules registered at load time, which usually breaks fixture-dependent layers.
182
-
183
- Signature:
184
-
185
- ```ts
186
- resetTestExtensionsForTests(): void
187
- ```
188
-
189
- The name advertises the intent: do not import this from production paths. There is no convenience "clear only layers" — by design, to keep the surface small.
190
-
191
- ## Webhook contract
192
-
193
- When `reporter.webhookUrl` is set, **`runRegisteredLayers` POSTs the JSON-serialised `TestSummary` to that URL automatically** at the end of its run — you do not wire it yourself. The built-in sweeps (contract/auth/rate-limit/fuzz) do NOT: if you drive only those, the POST is your aggregator's job. The behavior `runRegisteredLayers` implements (and a hand-rolled aggregator should mirror):
194
-
195
- - **Method:** `POST`.
196
- - **Headers:** `Content-Type: application/json`, plus `Authorization: Bearer <token>` when `webhookAuth?.type === 'bearer'`.
197
- - **Body:** `JSON.stringify(summary)`. `TestSummary.results` can be large — clip or compress on the producer side if the receiver has body limits.
198
- - **Timeout:** a 10s `AbortController` timeout, with the POST wrapped in `tryCatch`. A webhook failure does not fail the run.
199
- - **Plaintext warning:** a `webhookUrl` whose protocol is `http:` and whose host is not loopback logs a `console.warn` before POSTing — the summary (endpoint paths, error codes, metadata) and any bearer token go over the wire unencrypted. The warning never blocks the POST.
200
- - **Retries:** none. Add idempotency at the receiver if you want retries; the runner is a one-shot test driver, not a queue.
201
-
202
- Concrete example of a webhook-emitting reporter:
203
-
204
- ```ts
205
- import { registerTestReporter } from '@luckystack/test-runner';
206
-
207
- registerTestReporter({
208
- webhookUrl: 'https://hooks.slack.com/services/T000/B000/XXX',
209
- onSummary: async (summary) => {
210
- const url = 'https://hooks.slack.com/services/T000/B000/XXX';
211
- await fetch(url, {
212
- method: 'POST',
213
- headers: { 'Content-Type': 'application/json' },
214
- body: JSON.stringify({
215
- text: `LuckyStack tests: ${summary.passed}/${summary.totalEndpoints} passed`,
216
- }),
217
- });
218
- },
219
- });
220
- ```
221
-
222
- ## Lifecycle and error isolation
223
-
224
- - **Reporter callbacks** (`onResult`, `onSummary`) and **layer callbacks** (`run`) execute inline on the caller's task. The runner does not wrap them in `tryCatch`. A throw propagates out of the sweep and skips subsequent endpoints — handle errors inside your callback if you want isolation.
225
- - **Async callbacks** are NOT awaited by the built-in sweeps' `onResult` plumbing. If you do async work, push results into an in-memory buffer and flush in `onSummary` (which the aggregator can await).
226
- - **Order:** `onResult` fires per individual result in sweep order; `onSummary` fires exactly once at the end of the aggregator's run. Built-in sweeps emit `ContractCheckResult` through their own `onResult` parameter — the cross-layer reporter sees `TestResult` only when your aggregator forwards mapped results to it.
227
-
228
- ## Types
229
-
230
- ```ts
231
- interface TestLayer {
232
- name: string;
233
- run: (input: TestLayerInput) => Promise<TestLayerResult> | TestLayerResult;
234
- }
235
-
236
- interface TestLayerInput {
237
- endpoint: string;
238
- method?: string;
239
- authToken?: string;
240
- sampleInput?: unknown;
241
- }
242
-
243
- interface TestLayerResult {
244
- passed: boolean;
245
- message?: string;
246
- metadata?: Record<string, unknown>;
247
- }
248
-
249
- interface TestFixture<TPayload = unknown> {
250
- valid: TPayload[];
251
- invalid: TPayload[];
252
- }
253
-
254
- interface TestResult {
255
- layer: string;
256
- endpoint: string;
257
- passed: boolean;
258
- message?: string;
259
- durationMs?: number;
260
- metadata?: Record<string, unknown>;
261
- }
262
-
263
- interface TestSummary {
264
- totalLayers: number;
265
- totalEndpoints: number;
266
- passed: number;
267
- failed: number;
268
- results: TestResult[];
269
- }
270
-
271
- interface TestReporter {
272
- onResult?: (result: TestResult) => void | Promise<void>;
273
- onSummary?: (summary: TestSummary) => void | Promise<void>;
274
- webhookUrl?: string;
275
- webhookAuth?: { type: 'bearer'; token: string };
276
- }
277
- ```
278
-
279
- `ContractCheckResult` and `RunContractSummary` (the shapes emitted by the four built-in sweeps) live in `contract-tests.md`.
280
-
281
- ## Examples
282
-
283
- ### Live progress with the built-in pretty printer
284
-
285
- ```ts
286
- import {
287
- runContractTests,
288
- runFuzzTests,
289
- logContractResult,
290
- logContractSummary,
291
- } from '@luckystack/test-runner';
292
-
293
- const contract = await runContractTests({
294
- apiMethodMap,
295
- baseUrl,
296
- onResult: logContractResult,
297
- });
298
- logContractSummary(contract);
299
-
300
- const fuzz = await runFuzzTests({
301
- apiMethodMap,
302
- baseUrl,
303
- onResult: logContractResult,
304
- });
305
- logContractSummary(fuzz);
306
- ```
307
-
308
- ### Custom CORS-enforcement layer
309
-
310
- ```ts
311
- import { registerTestLayer } from '@luckystack/test-runner';
312
-
313
- registerTestLayer({
314
- name: 'cors',
315
- run: async ({ endpoint }) => {
316
- const response = await fetch(`${process.env.TEST_BASE_URL ?? 'http://127.0.0.1:80'}/${endpoint}`, {
317
- method: 'OPTIONS',
318
- headers: {
319
- Origin: 'https://evil.example.com',
320
- 'Access-Control-Request-Method': 'POST',
321
- },
322
- });
323
- const allowed = response.headers.get('Access-Control-Allow-Origin');
324
- if (allowed === '*' || allowed === 'https://evil.example.com') {
325
- return {
326
- passed: false,
327
- message: 'cors.openOrigin',
328
- metadata: { allowed },
329
- };
330
- }
331
- return { passed: true };
332
- },
333
- });
334
- ```
335
-
336
- A test harness then walks `listTestLayers()` per endpoint and aggregates results through the registered reporter.
337
-
338
- ### Multi-tenant isolation layer (combining fixtures and layers)
339
-
340
- ```ts
341
- import {
342
- registerTestFixture,
343
- registerTestLayer,
344
- getTestFixture,
345
- } from '@luckystack/test-runner';
346
-
347
- registerTestFixture('api/billing/getInvoice/v1', {
348
- valid: [{ invoiceId: 'inv_tenant_a' }],
349
- invalid: [{ invoiceId: 'inv_tenant_b_owned_by_other_tenant' }],
350
- });
351
-
352
- registerTestLayer({
353
- name: 'multi-tenant-isolation',
354
- run: async ({ endpoint, authToken }) => {
355
- const fixture = getTestFixture(endpoint);
356
- if (!fixture || fixture.invalid.length === 0) return { passed: true };
357
-
358
- const probe = fixture.invalid[0];
359
- const response = await fetch(`${process.env.TEST_BASE_URL ?? 'http://127.0.0.1:80'}/${endpoint}`, {
360
- method: 'POST',
361
- headers: {
362
- 'Content-Type': 'application/json',
363
- Authorization: `Bearer ${authToken ?? ''}`,
364
- },
365
- body: JSON.stringify(probe),
366
- });
367
- const body = await response.json() as { status?: string; errorCode?: string };
368
- if (body.status === 'success') {
369
- return {
370
- passed: false,
371
- message: 'tenant.crossAccess',
372
- metadata: { probe },
373
- };
374
- }
375
- return { passed: true };
376
- },
377
- });
378
- ```
379
-
380
- ### Slack webhook reporter
381
-
382
- ```ts
383
- import { registerTestReporter } from '@luckystack/test-runner';
384
-
385
- registerTestReporter({
386
- webhookUrl: process.env.SLACK_WEBHOOK_URL,
387
- onSummary: async (summary) => {
388
- const url = process.env.SLACK_WEBHOOK_URL;
389
- if (!url) return;
390
- const text = summary.failed > 0
391
- ? `LuckyStack tests FAILED: ${summary.failed}/${summary.totalEndpoints} endpoints`
392
- : `LuckyStack tests passed: ${summary.passed}/${summary.totalEndpoints}`;
393
- await fetch(url, {
394
- method: 'POST',
395
- headers: { 'Content-Type': 'application/json' },
396
- body: JSON.stringify({ text }),
397
- });
398
- },
399
- });
400
- ```
401
-
402
- ### Buffering reporter that forwards built-in sweep results
403
-
404
- The built-in sweeps emit `ContractCheckResult`. The reporter sees `TestResult`. Bridge them in your aggregator:
405
-
406
- ```ts
407
- import {
408
- runContractTests,
409
- runAuthEnforcementTests,
410
- registerTestReporter,
411
- getTestReporter,
412
- } from '@luckystack/test-runner';
413
-
414
- const buffered: TestResult[] = [];
415
-
416
- registerTestReporter({
417
- onResult: (r) => buffered.push(r),
418
- onSummary: async (summary) => {
419
- console.log('Final summary:', summary);
420
- },
421
- });
422
-
423
- const forward = (layer: string) => (result: ContractCheckResult) => {
424
- getTestReporter()?.onResult?.({
425
- layer,
426
- endpoint: result.endpoint.fullPath,
427
- passed: result.status === 'pass',
428
- message: result.reason,
429
- durationMs: result.durationMs,
430
- metadata: {
431
- httpStatus: result.httpStatus,
432
- errorCode: result.errorCode,
433
- responseStatus: result.responseStatus,
434
- },
435
- });
436
- };
437
-
438
- const contract = await runContractTests({
439
- apiMethodMap,
440
- baseUrl,
441
- onResult: forward('contract'),
442
- });
443
- const auth = await runAuthEnforcementTests({
444
- apiMethodMap,
445
- apiMetaMap,
446
- baseUrl,
447
- onResult: forward('auth'),
448
- });
449
-
450
- await getTestReporter()?.onSummary?.({
451
- totalLayers: 2,
452
- totalEndpoints: contract.total + auth.total,
453
- passed: buffered.filter(r => r.passed).length,
454
- failed: buffered.filter(r => !r.passed).length,
455
- results: buffered,
456
- });
457
- ```
458
-
459
- ## Edge cases and gotchas
460
-
461
- - **Throwing inside `onResult` kills the sweep.** The built-in sweeps call `input.onResult?.(result)` without `tryCatch`. Wrap your callback in your own `tryCatch` if it can throw.
462
- - **Async `onResult` is fire-and-forget for built-in sweeps.** Returning a promise does not pause the iteration. Push into a buffer; await in `onSummary`.
463
- - **`registerTestReporter(null)` removes the reporter.** `getTestReporter()` returns `null` after that. There is no "remove specific callback" — the slot is whole-object replace-only.
464
- - **`registerTestLayer` with a duplicate name silently replaces** the previous layer. To verify, call `listTestLayers()` and check the length / `name`s before kicking off the sweep.
465
- - **`resetTestExtensionsForTests()` is destructive.** It clears layers, fixtures, AND the reporter. Use it in `beforeEach`-style hooks for the runner's own tests, never in production harnesses.
466
- - **Webhook failures.** The runner does not own the webhook POST. Your reporter implements it; handle 4xx/5xx and timeouts there. A webhook that throws while inside `onSummary` will bubble up to your aggregator.
467
- - **`TestResult.layer` is a string, not a union.** That keeps the registry permissive: built-in layers can also feed in (`'contract'`, `'auth'`, `'rate-limit'`, `'fuzz'`) plus any custom names you registered. Establish a project-wide convention for layer names so the reporter can route on them.
468
- - **No deregistration for fixtures.** `registerTestFixture(typeKey, { valid: [], invalid: [] })` is the closest you get; pass empty arrays to neutralize. `resetTestExtensionsForTests()` is the nuclear option.
469
- - **Layer `run` callbacks are not concurrent.** A consumer aggregator that iterates `listTestLayers()` per endpoint is expected to await each layer sequentially. If you want concurrency, write it explicitly in your aggregator (with `Promise.allSettled`) — the registry imposes no policy.
470
- - **`logContractResult` uses `console.log` only.** No file output, no log levels. To capture output, redirect stdout or write your own reporter on top of the result shape.
7
+ Reporter failures should not turn a correct endpoint into a failed test, but they can hide alerts: monitor reporting separately. A webhook can transmit endpoint names, error details and bearer credentials; use trusted HTTPS destinations and do not put secrets into summary data. A warning about plaintext transport is not protection against exposure. Test-only extension resets are destructive and must not run in production or in a long-running shared harness.