@ontrails/testing 1.0.0-beta.15 → 1.0.0-beta.17

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 (95) hide show
  1. package/CHANGELOG.md +70 -1
  2. package/README.md +47 -3
  3. package/package.json +16 -7
  4. package/src/all.ts +32 -7
  5. package/src/context.ts +3 -0
  6. package/src/contracts.ts +1 -16
  7. package/src/examples.ts +19 -11
  8. package/src/harness-cli.ts +16 -3
  9. package/src/harness-http.ts +263 -0
  10. package/src/harness-mcp.ts +2 -0
  11. package/src/index.ts +18 -1
  12. package/src/logger.ts +3 -1
  13. package/src/scenario.ts +3 -5
  14. package/src/signals.ts +221 -0
  15. package/src/surface-parity.ts +356 -0
  16. package/src/types.ts +136 -1
  17. package/.turbo/turbo-build.log +0 -1
  18. package/.turbo/turbo-lint.log +0 -3
  19. package/.turbo/turbo-typecheck.log +0 -1
  20. package/dist/all.d.ts +0 -36
  21. package/dist/all.d.ts.map +0 -1
  22. package/dist/all.js +0 -99
  23. package/dist/all.js.map +0 -1
  24. package/dist/assertions.d.ts +0 -72
  25. package/dist/assertions.d.ts.map +0 -1
  26. package/dist/assertions.js +0 -238
  27. package/dist/assertions.js.map +0 -1
  28. package/dist/context.d.ts +0 -76
  29. package/dist/context.d.ts.map +0 -1
  30. package/dist/context.js +0 -118
  31. package/dist/context.js.map +0 -1
  32. package/dist/contracts.d.ts +0 -17
  33. package/dist/contracts.d.ts.map +0 -1
  34. package/dist/contracts.js +0 -75
  35. package/dist/contracts.js.map +0 -1
  36. package/dist/crosses.d.ts +0 -38
  37. package/dist/crosses.d.ts.map +0 -1
  38. package/dist/crosses.js +0 -223
  39. package/dist/crosses.js.map +0 -1
  40. package/dist/detours.d.ts +0 -13
  41. package/dist/detours.d.ts.map +0 -1
  42. package/dist/detours.js +0 -109
  43. package/dist/detours.js.map +0 -1
  44. package/dist/effective-examples.d.ts +0 -30
  45. package/dist/effective-examples.d.ts.map +0 -1
  46. package/dist/effective-examples.js +0 -227
  47. package/dist/effective-examples.js.map +0 -1
  48. package/dist/examples.d.ts +0 -23
  49. package/dist/examples.d.ts.map +0 -1
  50. package/dist/examples.js +0 -240
  51. package/dist/examples.js.map +0 -1
  52. package/dist/harness-cli.d.ts +0 -21
  53. package/dist/harness-cli.d.ts.map +0 -1
  54. package/dist/harness-cli.js +0 -205
  55. package/dist/harness-cli.js.map +0 -1
  56. package/dist/harness-mcp.d.ts +0 -21
  57. package/dist/harness-mcp.d.ts.map +0 -1
  58. package/dist/harness-mcp.js +0 -54
  59. package/dist/harness-mcp.js.map +0 -1
  60. package/dist/index.d.ts +0 -18
  61. package/dist/index.d.ts.map +0 -1
  62. package/dist/index.js +0 -19
  63. package/dist/index.js.map +0 -1
  64. package/dist/logger.d.ts +0 -15
  65. package/dist/logger.d.ts.map +0 -1
  66. package/dist/logger.js +0 -87
  67. package/dist/logger.js.map +0 -1
  68. package/dist/scenario.d.ts +0 -37
  69. package/dist/scenario.d.ts.map +0 -1
  70. package/dist/scenario.js +0 -235
  71. package/dist/scenario.js.map +0 -1
  72. package/dist/trail.d.ts +0 -20
  73. package/dist/trail.d.ts.map +0 -1
  74. package/dist/trail.js +0 -80
  75. package/dist/trail.js.map +0 -1
  76. package/dist/types.d.ts +0 -113
  77. package/dist/types.d.ts.map +0 -1
  78. package/dist/types.js +0 -5
  79. package/dist/types.js.map +0 -1
  80. package/src/__tests__/all.test.ts +0 -254
  81. package/src/__tests__/context.test.ts +0 -170
  82. package/src/__tests__/contracts.test.ts +0 -239
  83. package/src/__tests__/crosses.test.ts +0 -587
  84. package/src/__tests__/detours.test.ts +0 -212
  85. package/src/__tests__/effective-examples.test.ts +0 -203
  86. package/src/__tests__/examples.test.ts +0 -636
  87. package/src/__tests__/harness-cli.test.ts +0 -90
  88. package/src/__tests__/harness-mcp.test.ts +0 -37
  89. package/src/__tests__/logger.test.ts +0 -136
  90. package/src/__tests__/partial-match.test.ts +0 -126
  91. package/src/__tests__/scenario.test.ts +0 -381
  92. package/src/__tests__/trail.test.ts +0 -99
  93. package/tsconfig.json +0 -9
  94. package/tsconfig.tests.json +0 -10
  95. package/tsconfig.tsbuildinfo +0 -1
@@ -1,636 +0,0 @@
1
- import { describe, test } from 'bun:test';
2
-
3
- import {
4
- ConflictError,
5
- contour,
6
- DerivationError,
7
- NotFoundError,
8
- Result,
9
- RetryExhaustedError,
10
- resource,
11
- trail,
12
- topo,
13
- } from '@ontrails/core';
14
- import { z } from 'zod';
15
-
16
- import { testExamples } from '../examples.js';
17
-
18
- // ---------------------------------------------------------------------------
19
- // Test trails
20
- // ---------------------------------------------------------------------------
21
-
22
- const greetTrail = trail('greet', {
23
- blaze: (input: { name: string }) =>
24
- Result.ok({ greeting: `Hello, ${input.name}` }),
25
- description: 'Greet someone',
26
- examples: [
27
- {
28
- expected: { greeting: 'Hello, Alice' },
29
- input: { name: 'Alice' },
30
- name: 'Greet Alice',
31
- },
32
- ],
33
- input: z.object({ name: z.string() }),
34
- output: z.object({ greeting: z.string() }),
35
- });
36
-
37
- const searchTrail = trail('search', {
38
- blaze: (input: { query: string }) =>
39
- Result.ok({ results: [`result for ${input.query}`] }),
40
- description: 'Search for things',
41
- examples: [
42
- {
43
- input: { query: 'test' },
44
- name: 'Schema-only search',
45
- },
46
- ],
47
- input: z.object({ query: z.string() }),
48
- output: z.object({ results: z.array(z.string()) }),
49
- });
50
-
51
- const entityTrail = trail('entity.show', {
52
- blaze: (input: { name: string }) => {
53
- if (input.name === 'missing') {
54
- return Result.err(new NotFoundError('Entity not found'));
55
- }
56
- return Result.ok({ id: 1, name: input.name });
57
- },
58
- description: 'Show entity',
59
- examples: [
60
- {
61
- expected: { id: 1, name: 'Alpha' },
62
- input: { name: 'Alpha' },
63
- name: 'Show entity by name',
64
- },
65
- {
66
- error: 'NotFoundError',
67
- input: { name: 'missing' },
68
- name: 'Entity not found returns NotFoundError',
69
- },
70
- ],
71
- input: z.object({ name: z.string() }),
72
- output: z.object({ id: z.number(), name: z.string() }),
73
- });
74
-
75
- const noExamplesTrail = trail('noexamples', {
76
- blaze: (input: { x: number }) => Result.ok(input.x * 2),
77
- input: z.object({ x: z.number() }),
78
- });
79
-
80
- const taxonomyErrorTrail = trail('taxonomy.errors', {
81
- blaze: (input: { type: 'derivation' | 'retry' }) => {
82
- if (input.type === 'derivation') {
83
- return Result.err(new DerivationError('could not derive projection'));
84
- }
85
- return Result.err(
86
- new RetryExhaustedError(new ConflictError('version mismatch'), {
87
- attempts: 3,
88
- detour: 'ConflictError',
89
- })
90
- );
91
- },
92
- examples: [
93
- {
94
- error: 'DerivationError',
95
- input: { type: 'derivation' },
96
- name: 'Derivation failure returns DerivationError',
97
- },
98
- {
99
- error: 'RetryExhaustedError',
100
- input: { type: 'retry' },
101
- name: 'Exhausted detour returns RetryExhaustedError',
102
- },
103
- ],
104
- input: z.object({ type: z.enum(['derivation', 'retry']) }),
105
- });
106
-
107
- const mockDbResource = resource('db.mock.examples', {
108
- create: () => Result.ok({ source: 'factory' }),
109
- mock: () => ({ source: 'mock' }),
110
- });
111
-
112
- const mockResourceTrail = trail('resource.mocked', {
113
- blaze: (_input, ctx) =>
114
- Result.ok({ source: mockDbResource.from(ctx).source }),
115
- description: 'Trail that reads from a mocked resource',
116
- examples: [
117
- {
118
- expected: { source: 'mock' },
119
- input: {},
120
- name: 'Uses auto-resolved resource mock',
121
- },
122
- ],
123
- input: z.object({}),
124
- output: z.object({ source: z.string() }),
125
- resources: [mockDbResource],
126
- });
127
-
128
- const explicitOverrideTrail = trail('resource.override', {
129
- blaze: (_input, ctx) =>
130
- Result.ok({ source: mockDbResource.from(ctx).source }),
131
- description: 'Trail whose resource mock can be overridden explicitly',
132
- examples: [
133
- {
134
- expected: { source: 'override' },
135
- input: {},
136
- name: 'Explicit resource override wins over mock factory',
137
- },
138
- ],
139
- input: z.object({}),
140
- output: z.object({ source: z.string() }),
141
- resources: [mockDbResource],
142
- });
143
-
144
- const transformedInputTrail = trail('example.transformed', {
145
- blaze: (input: { value: number }) => Result.ok({ value: input.value }),
146
- description: 'Trail whose input schema transforms once',
147
- examples: [
148
- {
149
- expected: { value: 2 },
150
- input: { value: '1' },
151
- name: 'Raw example input is only transformed once',
152
- },
153
- ],
154
- input: z
155
- .object({ value: z.string() })
156
- .transform(({ value }) => ({ value: Number(value) + 1 })),
157
- output: z.object({ value: z.number() }),
158
- });
159
-
160
- const ctxOverrideTrail = trail('resource.ctx-override', {
161
- blaze: (_input, ctx) =>
162
- Result.ok({ source: mockDbResource.from(ctx).source }),
163
- description: 'Trail whose resource mock can be overridden by ctx.extensions',
164
- examples: [
165
- {
166
- expected: { source: 'ctx' },
167
- input: {},
168
- name: 'Context extensions beat auto-resolved mock resources',
169
- },
170
- ],
171
- input: z.object({}),
172
- output: z.object({ source: z.string() }),
173
- resources: [mockDbResource],
174
- });
175
-
176
- const undeclaredDbResource = resource('db.undeclared.examples', {
177
- create: () => Result.ok({ source: 'factory' }),
178
- mock: () => ({ source: 'mock' }),
179
- });
180
-
181
- const undeclaredResourceTrail = trail('resource.undeclared.examples', {
182
- blaze: (_input, ctx) =>
183
- Result.ok({ source: undeclaredDbResource.from(ctx).source }),
184
- description: 'Trail that uses a resource without declaring it',
185
- examples: [
186
- {
187
- error: 'InternalError',
188
- input: {},
189
- name: 'Undeclared resources stay unavailable during example execution',
190
- },
191
- ],
192
- input: z.object({}),
193
- output: z.object({ source: z.string() }),
194
- });
195
- const crossDbResource = resource('db.mock.examples.crosses', {
196
- create: () => Result.ok({ source: 'factory' }),
197
- mock: () => ({ source: 'mock' }),
198
- });
199
-
200
- const crossLeafTrail = trail('resource.crosses.leaf', {
201
- blaze: (_input, ctx) =>
202
- Result.ok({ childSource: crossDbResource.from(ctx).source }),
203
- description: 'Leaf trail that resolves a resource inside a cross chain',
204
- input: z.object({}),
205
- output: z.object({ childSource: z.string() }),
206
- resources: [crossDbResource],
207
- });
208
-
209
- const crossRootTrail = trail('resource.crosses.root', {
210
- blaze: async (_input, ctx) => {
211
- if (!ctx.cross) {
212
- return Result.err(new Error('cross not available'));
213
- }
214
- const childResult = await ctx.cross<{ childSource: string }>(
215
- 'resource.crosses.leaf',
216
- {}
217
- );
218
- if (childResult.isErr()) {
219
- return childResult;
220
- }
221
- return Result.ok({
222
- childSource: childResult.value.childSource,
223
- rootSource: crossDbResource.from(ctx).source,
224
- });
225
- },
226
- crosses: ['resource.crosses.leaf'],
227
- description: 'Root trail that crosses a child trail using resources',
228
- examples: [
229
- {
230
- expected: { childSource: 'mock', rootSource: 'mock' },
231
- input: {},
232
- name: 'Propagates resource mocks through cross execution',
233
- },
234
- ],
235
- input: z.object({}),
236
- output: z.object({ childSource: z.string(), rootSource: z.string() }),
237
- resources: [crossDbResource],
238
- });
239
-
240
- // ---------------------------------------------------------------------------
241
- // Composition trails (for cross coverage)
242
- // ---------------------------------------------------------------------------
243
-
244
- const addTrail = trail('entity.add', {
245
- blaze: (input: { name: string }) => Result.ok({ id: '1', name: input.name }),
246
- description: 'Add an entity',
247
- input: z.object({ name: z.string() }),
248
- output: z.object({ id: z.string(), name: z.string() }),
249
- });
250
-
251
- const relateTrail = trail('entity.relate', {
252
- blaze: (input: { from: string; to: string }) =>
253
- Result.ok({ from: input.from, to: input.to }),
254
- description: 'Relate entities',
255
- input: z.object({ from: z.string(), to: z.string() }),
256
- output: z.object({ from: z.string(), to: z.string() }),
257
- });
258
-
259
- const onboardTrail = trail('entity.onboard', {
260
- blaze: async (input: { name: string }, ctx) => {
261
- if (!ctx.cross) {
262
- return Result.err(new Error('cross not available'));
263
- }
264
- const addResult = await ctx.cross('entity.add', input);
265
- if (addResult.isErr()) {
266
- return addResult;
267
- }
268
- const relateResult = await ctx.cross('entity.relate', {
269
- from: 'root',
270
- to: (addResult.value as { id: string }).id,
271
- });
272
- if (relateResult.isErr()) {
273
- return relateResult;
274
- }
275
- return Result.ok({ id: '1', name: input.name });
276
- },
277
- crosses: ['entity.add', 'entity.relate'],
278
- description: 'Onboard a new entity',
279
- examples: [
280
- {
281
- expected: { id: '1', name: 'Alpha' },
282
- input: { name: 'Alpha' },
283
- name: 'Onboard Alpha',
284
- },
285
- ],
286
- input: z.object({ name: z.string() }),
287
- output: z.object({ id: z.string(), name: z.string() }),
288
- });
289
-
290
- // ---------------------------------------------------------------------------
291
- // Tests
292
- // ---------------------------------------------------------------------------
293
-
294
- describe('testExamples', () => {
295
- // eslint-disable-next-line jest/require-hook
296
- testExamples(
297
- topo('test-app', {
298
- entityTrail,
299
- greetTrail,
300
- noExamplesTrail,
301
- searchTrail,
302
- taxonomyErrorTrail,
303
- } as Record<string, unknown>)
304
- );
305
-
306
- // Also test with custom context (static form)
307
- describe('with custom context', () => {
308
- // eslint-disable-next-line jest/require-hook
309
- testExamples(topo('ctx-app', { greetTrail } as Record<string, unknown>), {
310
- requestId: 'custom-request',
311
- });
312
- });
313
-
314
- // Test with factory form (each test gets a fresh context)
315
- describe('with context factory', () => {
316
- // eslint-disable-next-line jest/require-hook
317
- testExamples(
318
- topo('factory-app', { greetTrail } as Record<string, unknown>),
319
- () => ({ requestId: `factory-${crypto.randomUUID()}` })
320
- );
321
- });
322
- });
323
-
324
- describe('testExamples skips trails with no examples', () => {
325
- // eslint-disable-next-line jest/require-hook
326
- testExamples(
327
- topo('skip-app', {
328
- noExamplesTrail,
329
- } as Record<string, unknown>)
330
- );
331
-
332
- test('no-op marker', () => {
333
- // This test exists so the describe block is not empty
334
- });
335
- });
336
-
337
- describe('testExamples resource mocks', () => {
338
- // eslint-disable-next-line jest/require-hook
339
- testExamples(
340
- topo('resource-mock-app', {
341
- mockDbResource,
342
- mockResourceTrail,
343
- } as Record<string, unknown>)
344
- );
345
- });
346
-
347
- describe('testExamples explicit resource overrides', () => {
348
- // eslint-disable-next-line jest/require-hook
349
- testExamples(
350
- topo('resource-override-app', {
351
- explicitOverrideTrail,
352
- mockDbResource,
353
- } as Record<string, unknown>),
354
- {
355
- resources: { 'db.mock.examples': { source: 'override' } },
356
- }
357
- );
358
- });
359
-
360
- describe('testExamples raw transformed input', () => {
361
- // eslint-disable-next-line jest/require-hook
362
- testExamples(
363
- topo('transformed-input-app', {
364
- transformedInputTrail,
365
- } as Record<string, unknown>)
366
- );
367
- });
368
-
369
- describe('testExamples context extension overrides', () => {
370
- // eslint-disable-next-line jest/require-hook
371
- testExamples(
372
- topo('ctx-override-app', {
373
- ctxOverrideTrail,
374
- mockDbResource,
375
- } as Record<string, unknown>),
376
- {
377
- ctx: {
378
- extensions: { 'db.mock.examples': { source: 'ctx' } },
379
- },
380
- }
381
- );
382
- });
383
-
384
- describe('testExamples resource declarations', () => {
385
- // eslint-disable-next-line jest/require-hook
386
- testExamples(
387
- topo('undeclared-resource-app', {
388
- undeclaredDbResource,
389
- undeclaredResourceTrail,
390
- } as Record<string, unknown>)
391
- );
392
- });
393
-
394
- describe('testExamples crossing coverage for trails with crossings', () => {
395
- // eslint-disable-next-line jest/require-hook
396
- testExamples(
397
- topo('composition-app', {
398
- addTrail,
399
- onboardTrail,
400
- relateTrail,
401
- } as Record<string, unknown>)
402
- );
403
- });
404
-
405
- describe('testExamples resource mocks through cross', () => {
406
- // eslint-disable-next-line jest/require-hook
407
- testExamples(
408
- topo('resource-cross-app', {
409
- crossDbResource,
410
- crossLeafTrail,
411
- crossRootTrail,
412
- } as Record<string, unknown>)
413
- );
414
- });
415
-
416
- // ---------------------------------------------------------------------------
417
- // Nested cross chain: A → B → C
418
- // ---------------------------------------------------------------------------
419
-
420
- const leafTrail = trail('step.leaf', {
421
- blaze: (input: { value: string }) => Result.ok({ leaf: input.value }),
422
- description: 'Leaf trail in a nested chain',
423
- input: z.object({ value: z.string() }),
424
- output: z.object({ leaf: z.string() }),
425
- });
426
-
427
- const middleTrail = trail('step.middle', {
428
- blaze: async (input: { value: string }, ctx) => {
429
- if (!ctx.cross) {
430
- return Result.err(new Error('cross not available'));
431
- }
432
- const leafResult = await ctx.cross<{ leaf: string }>('step.leaf', input);
433
- if (leafResult.isErr()) {
434
- return leafResult;
435
- }
436
- return Result.ok({ middle: leafResult.value.leaf });
437
- },
438
- crosses: ['step.leaf'],
439
- description: 'Middle trail that crosses the leaf',
440
- input: z.object({ value: z.string() }),
441
- output: z.object({ middle: z.string() }),
442
- });
443
-
444
- const rootTrail = trail('step.root', {
445
- blaze: async (input: { value: string }, ctx) => {
446
- if (!ctx.cross) {
447
- return Result.err(new Error('cross not available'));
448
- }
449
- const midResult = await ctx.cross<{ middle: string }>('step.middle', input);
450
- if (midResult.isErr()) {
451
- return midResult;
452
- }
453
- return Result.ok({ root: midResult.value.middle });
454
- },
455
- crosses: ['step.middle'],
456
- description: 'Root trail that crosses the middle trail',
457
- examples: [
458
- {
459
- expected: { root: 'hello' },
460
- input: { value: 'hello' },
461
- name: 'Nested cross chain A→B→C',
462
- },
463
- ],
464
- input: z.object({ value: z.string() }),
465
- output: z.object({ root: z.string() }),
466
- });
467
-
468
- describe('testExamples nested cross chain (A → B → C)', () => {
469
- // eslint-disable-next-line jest/require-hook
470
- testExamples(
471
- topo('nested-chain-app', {
472
- leafTrail,
473
- middleTrail,
474
- rootTrail,
475
- } as Record<string, unknown>)
476
- );
477
- });
478
-
479
- // ---------------------------------------------------------------------------
480
- // Auto-minting permit tests (B3)
481
- // ---------------------------------------------------------------------------
482
-
483
- const scopedTrail = trail('scoped.trail', {
484
- blaze: (_input, ctx) => {
485
- // Verify the permit was auto-minted with declared scopes
486
- const permit = ctx.permit as
487
- | { id: string; scopes: readonly string[] }
488
- | undefined;
489
- if (!permit || !permit.scopes.includes('admin')) {
490
- return Result.err(new Error('Missing permit or scopes'));
491
- }
492
- return Result.ok({ ok: true });
493
- },
494
- description: 'Trail requiring admin scope',
495
- examples: [
496
- {
497
- expected: { ok: true },
498
- input: {},
499
- name: 'Runs with auto-minted permit',
500
- },
501
- ],
502
- input: z.object({}),
503
- output: z.object({ ok: z.boolean() }),
504
- permit: { scopes: ['admin'] },
505
- });
506
-
507
- const publicTrail = trail('public.trail', {
508
- blaze: (_input, ctx) => {
509
- // Public trail should NOT get a permit
510
- if (ctx.permit !== undefined) {
511
- return Result.err(new Error('Unexpected permit on public trail'));
512
- }
513
- return Result.ok({ ok: true });
514
- },
515
- description: 'Public trail — no permit needed',
516
- examples: [
517
- {
518
- expected: { ok: true },
519
- input: {},
520
- name: 'Runs without a permit',
521
- },
522
- ],
523
- input: z.object({}),
524
- output: z.object({ ok: z.boolean() }),
525
- permit: 'public',
526
- });
527
-
528
- describe('testExamples auto-minting permits', () => {
529
- describe('scoped trail gets auto-minted permit', () => {
530
- // eslint-disable-next-line jest/require-hook
531
- testExamples(
532
- topo('mint-scoped-app', {
533
- scopedTrail,
534
- } as Record<string, unknown>)
535
- );
536
- });
537
-
538
- describe('public trail does NOT get a permit', () => {
539
- // eslint-disable-next-line jest/require-hook
540
- testExamples(
541
- topo('mint-public-app', {
542
- publicTrail,
543
- } as Record<string, unknown>)
544
- );
545
- });
546
-
547
- describe('strictPermits skips auto-minting', () => {
548
- const strictScopedTrail = trail('strict.scoped', {
549
- blaze: (_input, ctx) =>
550
- Result.ok({ hasPermit: ctx.permit !== undefined }),
551
- description: 'Trail that expects no permit under strictPermits',
552
- examples: [
553
- {
554
- expected: { hasPermit: false },
555
- input: {},
556
- name: 'No auto-minted permit when strictPermits is true',
557
- },
558
- ],
559
- input: z.object({}),
560
- output: z.object({ hasPermit: z.boolean() }),
561
- permit: { scopes: ['admin'] },
562
- });
563
-
564
- // eslint-disable-next-line jest/require-hook
565
- testExamples(
566
- topo('strict-app', {
567
- strictScopedTrail,
568
- } as Record<string, unknown>),
569
- { strictPermits: true }
570
- );
571
- });
572
- });
573
-
574
- // ---------------------------------------------------------------------------
575
- // Derived-fixture crossing coverage regression
576
- // ---------------------------------------------------------------------------
577
- //
578
- // A composition trail whose only examples come from contour-derived
579
- // fixtures must not fail crossing-coverage — derived inputs are not
580
- // guaranteed to exercise every declared cross.
581
-
582
- const itemContour = contour(
583
- 'item',
584
- {
585
- id: z.string(),
586
- name: z.string(),
587
- },
588
- {
589
- examples: [{ id: 'abc', name: 'Widget' }],
590
- identity: 'id',
591
- }
592
- );
593
-
594
- const helperTrail = trail('derived.helper', {
595
- blaze: (input: { id: string }) => Result.ok({ id: input.id, ok: true }),
596
- description: 'Helper referenced by a conditional cross',
597
- input: z.object({ id: z.string() }),
598
- output: z.object({ id: z.string(), ok: z.boolean() }),
599
- });
600
-
601
- const conditionalCrossTrail = trail('derived.conditional', {
602
- blaze: async (input: { id: string; name: string }, ctx) => {
603
- // The conditional cross is never taken for derived fixtures because
604
- // `shouldCross` is always false in the synthesized input. This is
605
- // exactly the case the provenance gate exists to protect: if we
606
- // asserted crossing coverage against derived examples, this trail
607
- // would fail even though its declaration is accurate for authored
608
- // use.
609
- const { shouldCross } = input as { shouldCross?: boolean };
610
- if (shouldCross && ctx.cross) {
611
- const result = await ctx.cross<{ id: string; ok: boolean }>(
612
- 'derived.helper',
613
- { id: input.id }
614
- );
615
- if (result.isErr()) {
616
- return result;
617
- }
618
- }
619
- return Result.ok({ id: input.id, name: input.name });
620
- },
621
- contours: [itemContour],
622
- crosses: ['derived.helper'],
623
- description: 'Composition trail with a cross that derived fixtures skip',
624
- input: z.object({ id: z.string(), name: z.string() }),
625
- output: z.object({ id: z.string(), name: z.string() }),
626
- });
627
-
628
- describe('testExamples derived-fixture crossing coverage is gated', () => {
629
- // eslint-disable-next-line jest/require-hook
630
- testExamples(
631
- topo('derived-coverage-app', {
632
- conditionalCrossTrail,
633
- helperTrail,
634
- } as Record<string, unknown>)
635
- );
636
- });