@ontrails/testing 1.0.0-beta.4 → 1.0.0-beta.41

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 (91) hide show
  1. package/CHANGELOG.md +526 -12
  2. package/README.md +83 -18
  3. package/package.json +34 -5
  4. package/src/all-established.ts +168 -0
  5. package/src/all.ts +51 -12
  6. package/src/assertions.ts +253 -0
  7. package/src/cli.ts +6 -0
  8. package/src/composes.ts +433 -0
  9. package/src/context.ts +200 -14
  10. package/src/contracts.ts +44 -33
  11. package/src/detours.ts +155 -18
  12. package/src/effective-examples.ts +408 -0
  13. package/src/errors.ts +47 -0
  14. package/src/examples.ts +274 -113
  15. package/src/harness-cli.ts +83 -58
  16. package/src/harness-http.ts +341 -0
  17. package/src/harness-mcp.ts +49 -16
  18. package/src/http.ts +10 -0
  19. package/src/index.ts +22 -14
  20. package/src/logger.ts +3 -1
  21. package/src/mcp.ts +6 -0
  22. package/src/scenario.ts +375 -0
  23. package/src/signals.ts +221 -0
  24. package/src/surface-parity.ts +389 -0
  25. package/src/trail.ts +1 -1
  26. package/src/types.ts +24 -52
  27. package/.turbo/turbo-build.log +0 -1
  28. package/.turbo/turbo-lint.log +0 -3
  29. package/.turbo/turbo-typecheck.log +0 -1
  30. package/dist/all.d.ts +0 -30
  31. package/dist/all.d.ts.map +0 -1
  32. package/dist/all.js +0 -47
  33. package/dist/all.js.map +0 -1
  34. package/dist/assertions.d.ts +0 -49
  35. package/dist/assertions.d.ts.map +0 -1
  36. package/dist/assertions.js +0 -84
  37. package/dist/assertions.js.map +0 -1
  38. package/dist/context.d.ts +0 -19
  39. package/dist/context.d.ts.map +0 -1
  40. package/dist/context.js +0 -33
  41. package/dist/context.js.map +0 -1
  42. package/dist/contracts.d.ts +0 -16
  43. package/dist/contracts.d.ts.map +0 -1
  44. package/dist/contracts.js +0 -66
  45. package/dist/contracts.js.map +0 -1
  46. package/dist/detours.d.ts +0 -12
  47. package/dist/detours.d.ts.map +0 -1
  48. package/dist/detours.js +0 -30
  49. package/dist/detours.js.map +0 -1
  50. package/dist/examples.d.ts +0 -22
  51. package/dist/examples.d.ts.map +0 -1
  52. package/dist/examples.js +0 -175
  53. package/dist/examples.js.map +0 -1
  54. package/dist/follows.d.ts +0 -32
  55. package/dist/follows.d.ts.map +0 -1
  56. package/dist/follows.js +0 -169
  57. package/dist/follows.js.map +0 -1
  58. package/dist/harness-cli.d.ts +0 -21
  59. package/dist/harness-cli.d.ts.map +0 -1
  60. package/dist/harness-cli.js +0 -213
  61. package/dist/harness-cli.js.map +0 -1
  62. package/dist/harness-mcp.d.ts +0 -21
  63. package/dist/harness-mcp.d.ts.map +0 -1
  64. package/dist/harness-mcp.js +0 -50
  65. package/dist/harness-mcp.js.map +0 -1
  66. package/dist/index.d.ts +0 -14
  67. package/dist/index.d.ts.map +0 -1
  68. package/dist/index.js +0 -16
  69. package/dist/index.js.map +0 -1
  70. package/dist/logger.d.ts +0 -15
  71. package/dist/logger.d.ts.map +0 -1
  72. package/dist/logger.js +0 -87
  73. package/dist/logger.js.map +0 -1
  74. package/dist/trail.d.ts +0 -20
  75. package/dist/trail.d.ts.map +0 -1
  76. package/dist/trail.js +0 -80
  77. package/dist/trail.js.map +0 -1
  78. package/dist/types.d.ts +0 -80
  79. package/dist/types.d.ts.map +0 -1
  80. package/dist/types.js +0 -5
  81. package/dist/types.js.map +0 -1
  82. package/src/__tests__/context.test.ts +0 -60
  83. package/src/__tests__/contracts.test.ts +0 -94
  84. package/src/__tests__/detours.test.ts +0 -55
  85. package/src/__tests__/examples.test.ts +0 -175
  86. package/src/__tests__/follows.test.ts +0 -163
  87. package/src/__tests__/logger.test.ts +0 -136
  88. package/src/__tests__/trail.test.ts +0 -99
  89. package/src/follows.ts +0 -283
  90. package/tsconfig.json +0 -9
  91. package/tsconfig.tsbuildinfo +0 -1
package/src/examples.ts CHANGED
@@ -3,14 +3,17 @@
3
3
  *
4
4
  * Iterates every trail in the app's topo. For each trail with examples,
5
5
  * generates describe/test blocks using bun:test. Progressive assertion
6
- * determines which check to run per example. For trails with `follow`
7
- * declarations, checks that every declared follow was called at least once.
6
+ * determines which check to run per example. For trails with `composes`
7
+ * declarations, checks that every declared composing was called at least once.
8
8
  */
9
9
 
10
10
  import { describe, expect, test } from 'bun:test';
11
11
 
12
12
  import type {
13
- FollowFn,
13
+ ComposeFn,
14
+ ComposeOptions,
15
+ ExecuteTrailOptions,
16
+ ResourceOverrideMap,
14
17
  Topo,
15
18
  TrailExample,
16
19
  Trail,
@@ -18,20 +21,10 @@ import type {
18
21
  } from '@ontrails/core';
19
22
 
20
23
  import {
21
- AlreadyExistsError,
22
- AmbiguousError,
23
- AssertionError,
24
- AuthError,
25
- CancelledError,
26
- ConflictError,
27
- InternalError,
28
- NetworkError,
29
- NotFoundError,
30
- PermissionError,
31
- RateLimitError,
24
+ buildComposeValidationSchema,
25
+ executeTrail,
26
+ parseTrailIdVersionReference,
32
27
  Result,
33
- TimeoutError,
34
- TrailsError,
35
28
  ValidationError,
36
29
  validateInput,
37
30
  } from '@ontrails/core';
@@ -40,39 +33,29 @@ import type { z } from 'zod';
40
33
  import {
41
34
  assertErrorMatch,
42
35
  assertFullMatch,
36
+ assertPartialMatch,
43
37
  assertSchemaMatch,
44
- expectOk,
45
38
  } from './assertions.js';
46
- import { mergeTestContext } from './context.js';
47
-
48
- // ---------------------------------------------------------------------------
49
- // Error class name -> constructor map
50
- // ---------------------------------------------------------------------------
51
-
52
- const ERROR_MAP: Record<string, new (...args: never[]) => Error> = {
53
- AlreadyExistsError: AlreadyExistsError as new (...args: never[]) => Error,
54
- AmbiguousError: AmbiguousError as new (...args: never[]) => Error,
55
- AssertionError: AssertionError as new (...args: never[]) => Error,
56
- AuthError: AuthError as new (...args: never[]) => Error,
57
- CancelledError: CancelledError as new (...args: never[]) => Error,
58
- ConflictError: ConflictError as new (...args: never[]) => Error,
59
- InternalError: InternalError as new (...args: never[]) => Error,
60
- NetworkError: NetworkError as new (...args: never[]) => Error,
61
- NotFoundError: NotFoundError as new (...args: never[]) => Error,
62
- PermissionError: PermissionError as new (...args: never[]) => Error,
63
- RateLimitError: RateLimitError as new (...args: never[]) => Error,
64
- TimeoutError: TimeoutError as new (...args: never[]) => Error,
65
- TrailsError: TrailsError as unknown as new (...args: never[]) => Error,
66
- ValidationError: ValidationError as new (...args: never[]) => Error,
39
+ import {
40
+ defaultCreatePermit,
41
+ mergeResourceOverrides,
42
+ mergeTestContext,
43
+ normalizeTestExecutionOptions,
44
+ createMockResources,
45
+ } from './context.js';
46
+ import type { PermittedTrail, TestExecutionOptions } from './context.js';
47
+ import {
48
+ deriveTrailExampleTargets,
49
+ isDerivedExample,
50
+ } from './effective-examples.js';
51
+ import type { TrailExampleTarget } from './effective-examples.js';
52
+ import { resolveErrorClass } from './errors.js';
53
+ import { withSignalAssertions } from './signals.js';
54
+
55
+ type TestingExecuteTrailOptions = ExecuteTrailOptions & {
56
+ readonly validationSchema?: ReturnType<typeof buildComposeValidationSchema>;
67
57
  };
68
58
 
69
- /**
70
- * Resolve an error class name string to the actual constructor.
71
- * Falls back to generic Error if the name is not in the core taxonomy.
72
- */
73
- const resolveErrorClass = (name: string): (new (...args: never[]) => Error) =>
74
- ERROR_MAP[name] ?? (Error as new (...args: never[]) => Error);
75
-
76
59
  // ---------------------------------------------------------------------------
77
60
  // Helpers
78
61
  // ---------------------------------------------------------------------------
@@ -83,16 +66,14 @@ const assertProgressiveMatch = (
83
66
  output: z.ZodType | undefined
84
67
  ): void => {
85
68
  if (example.expected !== undefined) {
86
- assertFullMatch(result, example.expected);
87
- return;
69
+ return assertFullMatch(result, example.expected);
70
+ }
71
+ if (example.expectedMatch !== undefined) {
72
+ return assertPartialMatch(result, example.expectedMatch);
88
73
  }
89
-
90
74
  if (example.error !== undefined) {
91
- const errorClass = resolveErrorClass(example.error);
92
- assertErrorMatch(result, errorClass);
93
- return;
75
+ return assertErrorMatch(result, resolveErrorClass(example.error));
94
76
  }
95
-
96
77
  assertSchemaMatch(result, output);
97
78
  };
98
79
 
@@ -121,87 +102,221 @@ const handleValidationError = (
121
102
  };
122
103
 
123
104
  /**
124
- * Run a single example against a trail.
125
- * Handles validation, execution, and assertions.
105
+ * Apply auto-permit: if the trail declares scoped permits and the context
106
+ * doesn't already have a permit, create one and merge it into the context.
126
107
  */
127
- const runExample = async (
128
- t: Trail<unknown, unknown>,
108
+ const applyAutoPermit = (
109
+ ctx: TrailContext,
110
+ trailDef: PermittedTrail,
111
+ opts: TestExecutionOptions
112
+ ): TrailContext => {
113
+ if (opts.strictPermits) {
114
+ return ctx;
115
+ }
116
+ if (ctx.permit !== undefined) {
117
+ return ctx;
118
+ }
119
+ const create = opts.createPermit ?? defaultCreatePermit;
120
+ const permit = create(trailDef);
121
+ if (!permit) {
122
+ return ctx;
123
+ }
124
+ return { ...ctx, permit };
125
+ };
126
+
127
+ const runTargetExample = async (
128
+ target: TrailExampleTarget,
129
129
  example: TrailExample<unknown, unknown>,
130
- output: z.ZodType | undefined,
131
- testCtx: TrailContext
130
+ testCtx: TrailContext,
131
+ resources?: ResourceOverrideMap,
132
+ opts?: TestExecutionOptions
132
133
  ): Promise<void> => {
133
- const validated = validateInput(t.input, example.input);
134
+ const { output, trail: t } = target;
135
+ const ctx = opts ? applyAutoPermit(testCtx, t, opts) : testCtx;
136
+ const signals = withSignalAssertions(ctx, example);
137
+ const validated = validateInput(target.input, example.input);
134
138
 
135
139
  if (handleValidationError(validated, example)) {
140
+ signals.assert();
136
141
  return;
137
142
  }
138
- const validatedInput = expectOk(validated);
139
143
 
140
- const result = await t.run(validatedInput, testCtx);
144
+ const result = await executeTrail(t, example.input, {
145
+ ctx: signals.ctx,
146
+ resources: resources ?? opts?.resources,
147
+ ...(target.version === undefined ? {} : { version: target.version }),
148
+ });
141
149
  assertProgressiveMatch(result, example, output);
150
+ signals.assert();
151
+ };
152
+
153
+ /**
154
+ * Run a single example against a trail.
155
+ * Handles validation, execution, and assertions.
156
+ */
157
+ export const runExample = async (
158
+ t: Trail<unknown, unknown, unknown>,
159
+ example: TrailExample<unknown, unknown>,
160
+ output: z.ZodType | undefined,
161
+ testCtx: TrailContext,
162
+ resources?: ResourceOverrideMap,
163
+ opts?: TestExecutionOptions
164
+ ): Promise<void> => {
165
+ await runTargetExample(
166
+ {
167
+ composes: t.composes,
168
+ current: true,
169
+ examples: [example],
170
+ id: t.id,
171
+ input: t.input,
172
+ output,
173
+ trail: t,
174
+ },
175
+ example,
176
+ testCtx,
177
+ resources,
178
+ opts
179
+ );
142
180
  };
143
181
 
144
182
  // ---------------------------------------------------------------------------
145
- // Follow coverage for composition trails
183
+ // Composing coverage for trails with compositions
146
184
  // ---------------------------------------------------------------------------
147
185
 
148
186
  /**
149
- * Build a recording follow function that tracks which trail IDs are called.
187
+ * Build a recording compose function that tracks which trail IDs are called.
150
188
  *
151
- * Delegates to `baseFollow` when available, otherwise looks up the trail
189
+ * Delegates to `baseCompose` when available, otherwise looks up the trail
152
190
  * in the topo and executes it with validated input. Falls back to
153
191
  * `Result.ok()` when neither is available.
154
192
  */
155
- const createCoverageFollow = (
193
+ const createCoverageCompose = (
156
194
  called: Set<string>,
157
- baseFollow: FollowFn | undefined,
195
+ baseCompose: ComposeFn | undefined,
158
196
  topo: Topo,
159
- ctx: TrailContext
160
- ): FollowFn => {
161
- const follow = (id: string, input: unknown) => {
162
- called.add(id);
197
+ ctx: TrailContext,
198
+ resources?: ResourceOverrideMap
199
+ ): ComposeFn => {
200
+ const invokeCompose = async (
201
+ idOrTrail: string | { readonly id: string },
202
+ input: unknown,
203
+ self: ComposeFn,
204
+ composeOptions?: ComposeOptions | undefined
205
+ ) => {
206
+ const parsed =
207
+ typeof idOrTrail === 'string'
208
+ ? parseTrailIdVersionReference(idOrTrail)
209
+ : Result.ok({ id: idOrTrail.id });
210
+ if (parsed.isErr()) {
211
+ return parsed;
212
+ }
213
+ const parsedVersion =
214
+ 'version' in parsed.value ? parsed.value.version : undefined;
215
+ if (parsedVersion !== undefined && composeOptions?.version !== undefined) {
216
+ return Result.err(
217
+ new ValidationError(
218
+ `Trail "${parsed.value.id}" version was provided both in the id reference and ctx.compose() options`
219
+ )
220
+ );
221
+ }
163
222
 
164
- if (baseFollow !== undefined) {
165
- return baseFollow(id, input);
223
+ const { id } = parsed.value;
224
+ called.add(id);
225
+ const version = composeOptions?.version ?? parsedVersion;
226
+
227
+ if (baseCompose !== undefined) {
228
+ const forwardedOptions =
229
+ parsedVersion === undefined
230
+ ? composeOptions
231
+ : { ...composeOptions, version: parsedVersion };
232
+ return await baseCompose(id, input, forwardedOptions);
166
233
  }
167
234
 
168
235
  const trailDef = topo.get(id);
169
236
  if (trailDef !== undefined) {
170
- const validated = validateInput(trailDef.input, input);
171
- if (validated.isErr()) {
172
- return Promise.resolve(validated);
173
- }
174
- return Promise.resolve(trailDef.run(validated.value, ctx));
237
+ const options: TestingExecuteTrailOptions = {
238
+ ctx: { ...ctx, compose: self },
239
+ resources,
240
+ ...(version === undefined ? {} : { version }),
241
+ validationSchema: buildComposeValidationSchema(trailDef),
242
+ };
243
+ return await executeTrail(trailDef, input, options);
175
244
  }
176
245
 
177
- return Promise.resolve(Result.ok());
246
+ return Result.ok();
178
247
  };
179
- return follow as FollowFn;
248
+
249
+ // Accepts either a trail object (typed compose), a string id (untyped),
250
+ // or a batch of `[target, input]` tuples.
251
+ const compose = async function compose(
252
+ idOrTrail:
253
+ | string
254
+ | { readonly id: string }
255
+ | readonly (readonly [string | { readonly id: string }, unknown])[],
256
+ inputOrOptions?: unknown,
257
+ singleOptions?: ComposeOptions
258
+ ) {
259
+ if (Array.isArray(idOrTrail)) {
260
+ return await Promise.all(
261
+ idOrTrail.map(([target, batchInput]) =>
262
+ invokeCompose(target, batchInput, compose as ComposeFn)
263
+ )
264
+ );
265
+ }
266
+
267
+ return await invokeCompose(
268
+ idOrTrail as string | { readonly id: string },
269
+ inputOrOptions,
270
+ compose as ComposeFn,
271
+ singleOptions
272
+ );
273
+ } as ComposeFn;
274
+
275
+ return compose;
180
276
  };
181
277
 
182
278
  /**
183
- * Run a single example against a composition trail, recording follow calls.
279
+ * Run a single example against a trail with compositions, recording compose calls.
184
280
  */
185
281
  const runCompositionExample = async (
186
- trailDef: Trail<unknown, unknown>,
282
+ target: TrailExampleTarget,
187
283
  example: TrailExample<unknown, unknown>,
188
- output: z.ZodType | undefined,
189
284
  baseCtx: TrailContext,
190
285
  called: Set<string>,
191
- topo: Topo
286
+ topo: Topo,
287
+ resources?: ResourceOverrideMap,
288
+ opts?: TestExecutionOptions
192
289
  ): Promise<void> => {
193
- const validated = validateInput(trailDef.input, example.input);
290
+ const { output, trail: trailDef } = target;
291
+ const permittedCtx = opts
292
+ ? applyAutoPermit(baseCtx, trailDef, opts)
293
+ : baseCtx;
294
+ const signals = withSignalAssertions(permittedCtx, example);
295
+ const validated = validateInput(target.input, example.input);
194
296
 
195
297
  if (handleValidationError(validated, example)) {
298
+ signals.assert();
196
299
  return;
197
300
  }
198
- const validatedInput = expectOk(validated);
199
-
200
- const follow = createCoverageFollow(called, baseCtx.follow, topo, baseCtx);
201
- const testCtx: TrailContext = { ...baseCtx, follow };
202
301
 
203
- const result = await trailDef.run(validatedInput, testCtx);
302
+ const compose = createCoverageCompose(
303
+ called,
304
+ signals.ctx.compose,
305
+ topo,
306
+ signals.ctx,
307
+ resources
308
+ );
309
+ const testCtx: TrailContext = { ...signals.ctx, compose };
310
+
311
+ // Top-level trail validates against trail.input (not merged composeInput).
312
+ // Merged validation only applies to compose targets in executeFromMap/createCoverageCompose.
313
+ const result = await executeTrail(trailDef, example.input, {
314
+ ctx: testCtx,
315
+ resources: resources ?? opts?.resources,
316
+ ...(target.version === undefined ? {} : { version: target.version }),
317
+ });
204
318
  assertProgressiveMatch(result, example, output);
319
+ signals.assert();
205
320
  };
206
321
 
207
322
  // ---------------------------------------------------------------------------
@@ -211,32 +326,33 @@ const runCompositionExample = async (
211
326
  /**
212
327
  * Generate describe/test blocks for every trail example in the app.
213
328
  *
214
- * For trails with `follow` declarations and examples, also verifies that
215
- * every declared follow ID was called at least once across all examples.
329
+ * For trails with `composes` declarations and examples, also verifies that
330
+ * every declared composed ID was called at least once across all examples.
216
331
  *
217
332
  * One line in your test file:
218
333
  * ```ts
219
- * testExamples(app);
334
+ * testExamples(graph);
220
335
  * ```
221
336
  */
222
337
  export const testExamples = (
223
338
  app: Topo,
224
- ctxOrFactory?: Partial<TrailContext> | (() => Partial<TrailContext>)
339
+ ctxOrFactory?:
340
+ | Partial<TrailContext>
341
+ | TestExecutionOptions
342
+ | (() => Partial<TrailContext> | TestExecutionOptions)
225
343
  ): void => {
226
- const resolveCtx =
344
+ const resolveInput =
227
345
  typeof ctxOrFactory === 'function' ? ctxOrFactory : () => ctxOrFactory;
228
- const allTrails = app.list() as Trail<unknown, unknown>[];
229
-
230
- const withExamples = allTrails.filter(
231
- (t) => t.examples !== undefined && t.examples.length > 0
232
- );
233
- const simpleTrails = withExamples.filter((t) => t.follow.length === 0);
234
- const compositionTrails = withExamples.filter((t) => t.follow.length > 0);
346
+ const withExamples = (app.list() as Trail<unknown, unknown, unknown>[])
347
+ .flatMap(deriveTrailExampleTargets)
348
+ .filter((target) => target.examples.length > 0);
349
+ const simpleTrails = withExamples.filter((t) => t.composes.length === 0);
350
+ const compositionTrails = withExamples.filter((t) => t.composes.length > 0);
235
351
 
236
352
  // Simple trails: run examples directly
237
353
  if (simpleTrails.length > 0) {
238
354
  describe.each(simpleTrails)('$id', (t) => {
239
- const { examples, output } = t;
355
+ const { examples } = t;
240
356
  if (!examples) {
241
357
  return;
242
358
  }
@@ -244,35 +360,80 @@ export const testExamples = (
244
360
  test.each([...examples])(
245
361
  'example: $name',
246
362
  async (example: TrailExample<unknown, unknown>) => {
247
- const testCtx = mergeTestContext(resolveCtx());
248
- await runExample(t, example, output, testCtx);
363
+ const resolved = normalizeTestExecutionOptions(resolveInput());
364
+ const resources = mergeResourceOverrides(
365
+ await createMockResources(app),
366
+ resolved.ctx,
367
+ resolved.resources
368
+ );
369
+ const testCtx = mergeTestContext(resolved.ctx);
370
+ await runTargetExample(t, example, testCtx, resources, resolved);
249
371
  }
250
372
  );
251
373
  });
252
374
  }
253
375
 
254
- // Composition trails: use recording follow and check coverage
376
+ // Composition trails: use recording compose and check coverage.
377
+ //
378
+ // Composing coverage only runs against AUTHORED examples. Entity-derived
379
+ // fixtures are opportunistic coverage that may not exercise every
380
+ // `ctx.compose()` branch in the trail, so asserting coverage against them
381
+ // would produce false failures for trails whose authored intent was a
382
+ // single path. When a trail has zero authored examples the coverage
383
+ // assertion is skipped entirely — the derived-example runs still
384
+ // execute, but they are not required to cover declared compositions.
255
385
  if (compositionTrails.length > 0) {
256
386
  describe.each(compositionTrails)('$id', (t) => {
257
- const { examples, output } = t;
387
+ const { examples } = t;
258
388
  if (!examples) {
259
389
  return;
260
390
  }
261
391
 
262
- const called = new Set<string>();
392
+ const composedFromAuthored = new Set<string>();
393
+ const hasAuthoredExamples = examples.some(
394
+ (example) => !isDerivedExample(example)
395
+ );
396
+
397
+ // Only record compose calls from authored examples. Derived fixtures
398
+ // execute normally but do not contribute to coverage — the sink map
399
+ // puts each example in the right bucket without an inline
400
+ // conditional inside the test body.
401
+ const discardSink = new Set<string>();
402
+ const pickCoverageSink = (
403
+ example: TrailExample<unknown, unknown>
404
+ ): Set<string> =>
405
+ isDerivedExample(example) ? discardSink : composedFromAuthored;
263
406
 
264
407
  test.each([...examples])(
265
408
  'example: $name',
266
409
  async (example: TrailExample<unknown, unknown>) => {
267
- const baseCtx = mergeTestContext(resolveCtx());
268
- await runCompositionExample(t, example, output, baseCtx, called, app);
410
+ const resolved = normalizeTestExecutionOptions(resolveInput());
411
+ const resources = mergeResourceOverrides(
412
+ await createMockResources(app),
413
+ resolved.ctx,
414
+ resolved.resources
415
+ );
416
+ const baseCtx = mergeTestContext(resolved.ctx);
417
+ await runCompositionExample(
418
+ t,
419
+ example,
420
+ baseCtx,
421
+ pickCoverageSink(example),
422
+ app,
423
+ resources,
424
+ resolved
425
+ );
269
426
  }
270
427
  );
271
428
 
272
- test('follow coverage', () => {
273
- const uncovered = t.follow.filter((id) => !called.has(id));
274
- expect(uncovered).toEqual([]);
275
- });
429
+ if (hasAuthoredExamples) {
430
+ test('composing coverage', () => {
431
+ const uncovered = t.composes.filter(
432
+ (id) => !composedFromAuthored.has(id)
433
+ );
434
+ expect(uncovered).toEqual([]);
435
+ });
436
+ }
276
437
  });
277
438
  }
278
439
  };