@ontrails/testing 1.0.0-beta.14 → 1.0.0-beta.16

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 (88) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +12 -12
  3. package/package.json +15 -6
  4. package/src/all.ts +153 -16
  5. package/src/assertions.ts +253 -0
  6. package/src/context.ts +67 -38
  7. package/src/contracts.ts +15 -24
  8. package/src/crosses.ts +93 -51
  9. package/src/detours.ts +155 -18
  10. package/src/effective-examples.ts +350 -0
  11. package/src/examples.ts +139 -63
  12. package/src/harness-cli.ts +19 -11
  13. package/src/harness-mcp.ts +9 -8
  14. package/src/index.ts +14 -4
  15. package/src/logger.ts +3 -1
  16. package/src/scenario.ts +368 -0
  17. package/src/signals.ts +221 -0
  18. package/src/types.ts +64 -6
  19. package/.turbo/turbo-build.log +0 -1
  20. package/.turbo/turbo-lint.log +0 -3
  21. package/.turbo/turbo-typecheck.log +0 -1
  22. package/dist/all.d.ts +0 -31
  23. package/dist/all.d.ts.map +0 -1
  24. package/dist/all.js +0 -47
  25. package/dist/all.js.map +0 -1
  26. package/dist/assertions.d.ts +0 -49
  27. package/dist/assertions.d.ts.map +0 -1
  28. package/dist/assertions.js +0 -84
  29. package/dist/assertions.js.map +0 -1
  30. package/dist/context.d.ts +0 -75
  31. package/dist/context.d.ts.map +0 -1
  32. package/dist/context.js +0 -107
  33. package/dist/context.js.map +0 -1
  34. package/dist/contracts.d.ts +0 -17
  35. package/dist/contracts.d.ts.map +0 -1
  36. package/dist/contracts.js +0 -71
  37. package/dist/contracts.js.map +0 -1
  38. package/dist/crosses.d.ts +0 -38
  39. package/dist/crosses.d.ts.map +0 -1
  40. package/dist/crosses.js +0 -213
  41. package/dist/crosses.js.map +0 -1
  42. package/dist/detours.d.ts +0 -12
  43. package/dist/detours.d.ts.map +0 -1
  44. package/dist/detours.js +0 -30
  45. package/dist/detours.js.map +0 -1
  46. package/dist/examples.d.ts +0 -23
  47. package/dist/examples.d.ts.map +0 -1
  48. package/dist/examples.js +0 -202
  49. package/dist/examples.js.map +0 -1
  50. package/dist/follows.d.ts +0 -38
  51. package/dist/follows.d.ts.map +0 -1
  52. package/dist/follows.js +0 -212
  53. package/dist/follows.js.map +0 -1
  54. package/dist/harness-cli.d.ts +0 -21
  55. package/dist/harness-cli.d.ts.map +0 -1
  56. package/dist/harness-cli.js +0 -200
  57. package/dist/harness-cli.js.map +0 -1
  58. package/dist/harness-mcp.d.ts +0 -21
  59. package/dist/harness-mcp.d.ts.map +0 -1
  60. package/dist/harness-mcp.js +0 -53
  61. package/dist/harness-mcp.js.map +0 -1
  62. package/dist/index.d.ts +0 -16
  63. package/dist/index.d.ts.map +0 -1
  64. package/dist/index.js +0 -16
  65. package/dist/index.js.map +0 -1
  66. package/dist/logger.d.ts +0 -15
  67. package/dist/logger.d.ts.map +0 -1
  68. package/dist/logger.js +0 -87
  69. package/dist/logger.js.map +0 -1
  70. package/dist/trail.d.ts +0 -20
  71. package/dist/trail.d.ts.map +0 -1
  72. package/dist/trail.js +0 -80
  73. package/dist/trail.js.map +0 -1
  74. package/dist/types.d.ts +0 -80
  75. package/dist/types.d.ts.map +0 -1
  76. package/dist/types.js +0 -5
  77. package/dist/types.js.map +0 -1
  78. package/src/__tests__/all.test.ts +0 -135
  79. package/src/__tests__/context.test.ts +0 -150
  80. package/src/__tests__/contracts.test.ts +0 -185
  81. package/src/__tests__/crosses.test.ts +0 -587
  82. package/src/__tests__/detours.test.ts +0 -55
  83. package/src/__tests__/examples.test.ts +0 -534
  84. package/src/__tests__/harness-cli.test.ts +0 -66
  85. package/src/__tests__/logger.test.ts +0 -136
  86. package/src/__tests__/trail.test.ts +0 -99
  87. package/tsconfig.json +0 -9
  88. package/tsconfig.tsbuildinfo +0 -1
package/src/detours.ts CHANGED
@@ -1,42 +1,179 @@
1
1
  /**
2
- * testDetours — verify that all detour targets exist in the topo.
2
+ * testDetours — validate the live detour contract for every trail.
3
3
  *
4
- * Pure structural validation. No implementation execution needed.
4
+ * Pure structural validation. No blaze or detour recovery execution needed.
5
5
  */
6
6
 
7
- import { describe, expect, test } from 'bun:test';
7
+ import { describe, test } from 'bun:test';
8
8
 
9
+ import { TrailsError } from '@ontrails/core';
9
10
  import type { Topo, Trail } from '@ontrails/core';
10
11
 
11
- // ---------------------------------------------------------------------------
12
- // testDetours
13
- // ---------------------------------------------------------------------------
12
+ interface RuntimeDetour {
13
+ readonly on?: unknown;
14
+ readonly recover?: unknown;
15
+ }
16
+
17
+ const isErrorConstructor = (
18
+ value: unknown
19
+ ): value is abstract new (...args: never[]) => Error => {
20
+ if (typeof value !== 'function') {
21
+ return false;
22
+ }
23
+
24
+ const { prototype } = value as { prototype?: unknown };
25
+ return prototype instanceof Error;
26
+ };
27
+
28
+ const detourLabel = (trailId: string, index: number, detour: RuntimeDetour) => {
29
+ if (typeof detour.on === 'function') {
30
+ const { name } = detour.on as { name?: unknown };
31
+ if (typeof name === 'string' && name.length > 0) {
32
+ return `${trailId} detour[${index}] on ${name}`;
33
+ }
34
+ }
35
+
36
+ return `${trailId} detour[${index}]`;
37
+ };
38
+
39
+ const assertValidOn = (
40
+ trailId: string,
41
+ index: number,
42
+ detour: RuntimeDetour
43
+ ): void => {
44
+ if (isErrorConstructor(detour.on)) {
45
+ return;
46
+ }
47
+
48
+ throw new Error(
49
+ `${detourLabel(trailId, index, detour)} must declare a real error constructor in on:`
50
+ );
51
+ };
52
+
53
+ const assertCallableRecover = (
54
+ trailId: string,
55
+ index: number,
56
+ detour: RuntimeDetour
57
+ ): void => {
58
+ if (typeof detour.recover === 'function') {
59
+ return;
60
+ }
61
+
62
+ throw new Error(
63
+ `${detourLabel(trailId, index, detour)} must declare a callable recover function`
64
+ );
65
+ };
66
+
67
+ const sameOrSubtype = (
68
+ candidate: abstract new (...args: never[]) => Error,
69
+ ancestor: abstract new (...args: never[]) => Error
70
+ ): boolean => {
71
+ if (candidate === ancestor) {
72
+ return true;
73
+ }
74
+
75
+ let current = Object.getPrototypeOf(candidate.prototype);
76
+ while (current && typeof current === 'object') {
77
+ const ctor = (current as { constructor?: unknown }).constructor;
78
+ if (ctor === ancestor) {
79
+ return true;
80
+ }
81
+ current = Object.getPrototypeOf(current);
82
+ }
83
+
84
+ return false;
85
+ };
86
+
87
+ const getShadowingDetour = (
88
+ detours: readonly RuntimeDetour[],
89
+ index: number
90
+ ):
91
+ | {
92
+ readonly index: number;
93
+ readonly on: abstract new (...args: never[]) => Error;
94
+ }
95
+ | undefined => {
96
+ const detour = detours[index];
97
+ if (!detour || !isErrorConstructor(detour.on)) {
98
+ return undefined;
99
+ }
100
+
101
+ for (let previousIndex = 0; previousIndex < index; previousIndex += 1) {
102
+ const previous = detours[previousIndex];
103
+ if (!previous || !isErrorConstructor(previous.on)) {
104
+ continue;
105
+ }
106
+
107
+ if (sameOrSubtype(detour.on, previous.on)) {
108
+ return { index: previousIndex, on: previous.on };
109
+ }
110
+ }
111
+
112
+ return undefined;
113
+ };
114
+
115
+ const assertNotShadowed = (
116
+ trailId: string,
117
+ detours: readonly RuntimeDetour[],
118
+ index: number
119
+ ): void => {
120
+ const detour = detours[index];
121
+ if (!detour || !isErrorConstructor(detour.on)) {
122
+ return;
123
+ }
124
+
125
+ const shadowing = getShadowingDetour(detours, index);
126
+ if (!shadowing) {
127
+ return;
128
+ }
129
+
130
+ const previousName = shadowing.on.name || TrailsError.name;
131
+ const currentName = detour.on.name || TrailsError.name;
132
+ throw new Error(
133
+ `${trailId} detour[${index}] on ${currentName} is shadowed by earlier detour[${shadowing.index}] on ${previousName}`
134
+ );
135
+ };
14
136
 
15
137
  /**
16
- * Verify that every trail's detour targets reference trails that
17
- * actually exist in the app's topo.
138
+ * Verify that every trail's detours match the live runtime contract:
139
+ * `on` must be an error constructor, `recover` must be callable, and
140
+ * later detours must not be shadowed by earlier broader `on` types.
18
141
  */
19
142
  export const testDetours = (app: Topo): void => {
20
143
  const trailEntries = [...app.trails];
21
144
 
22
145
  describe('detours', () => {
23
146
  describe.each(trailEntries)('%s', (_id, trailDef) => {
24
- const t = trailDef as Trail<unknown, unknown>;
147
+ const trail = trailDef as Trail<unknown, unknown, unknown>;
25
148
 
26
- if (t.detours === undefined) {
149
+ if (trail.detours.length === 0) {
27
150
  return;
28
151
  }
29
152
 
30
- const { detours } = t;
31
- const testCases = Object.entries(detours).flatMap(
32
- ([detourName, targets]) =>
33
- targets.map((targetId) => ({ detourName, targetId }))
153
+ const detourCases = trail.detours.map((detour, index) => ({
154
+ detour,
155
+ index,
156
+ trailId: trail.id,
157
+ }));
158
+
159
+ test.each(detourCases)(
160
+ '$trailId detour[$index] uses an error constructor',
161
+ ({ detour, index, trailId }) => {
162
+ assertValidOn(trailId, index, detour);
163
+ }
164
+ );
165
+
166
+ test.each(detourCases)(
167
+ '$trailId detour[$index] provides a callable recover',
168
+ ({ detour, index, trailId }) => {
169
+ assertCallableRecover(trailId, index, detour);
170
+ }
34
171
  );
35
172
 
36
- test.each(testCases)(
37
- 'detour "$detourName" -> "$targetId" exists',
38
- ({ targetId }) => {
39
- expect(app.has(targetId)).toBe(true);
173
+ test.each(detourCases)(
174
+ '$trailId detour[$index] is not shadowed by an earlier detour',
175
+ ({ index, trailId }) => {
176
+ assertNotShadowed(trailId, trail.detours, index);
40
177
  }
41
178
  );
42
179
  });
@@ -0,0 +1,350 @@
1
+ import type { AnyContour, Trail, TrailExample } from '@ontrails/core';
2
+ import { getContourReferences } from '@ontrails/core';
3
+ import { z } from 'zod';
4
+
5
+ type ExampleRecord = Readonly<Record<string, unknown>>;
6
+
7
+ /**
8
+ * Tracks examples that `deriveTrailExamples` synthesizes from contour
9
+ * fixtures. Authored examples are passed through untouched and never
10
+ * appear here, so consumers can distinguish the two by identity.
11
+ *
12
+ * Exposed via `isDerivedExample` so downstream testing helpers (e.g.
13
+ * `testExamples` crossing coverage) can relax invariants that only make
14
+ * sense for authored inputs.
15
+ */
16
+ const derivedExamples = new WeakSet<TrailExample<unknown, unknown>>();
17
+
18
+ /**
19
+ * Returns `true` if the given example was synthesized from contour fixtures
20
+ * by `deriveTrailExamples`, `false` if it was authored on the trail.
21
+ */
22
+ export const isDerivedExample = (
23
+ example: TrailExample<unknown, unknown>
24
+ ): boolean => derivedExamples.has(example);
25
+
26
+ interface ContourFixture {
27
+ readonly contour: AnyContour;
28
+ readonly example: ExampleRecord;
29
+ readonly index: number;
30
+ }
31
+
32
+ const capitalize = (value: string): string =>
33
+ value.length === 0 ? value : value.slice(0, 1).toUpperCase() + value.slice(1);
34
+
35
+ const collectReferenceMap = (
36
+ contours: readonly AnyContour[]
37
+ ): ReadonlyMap<string, ReturnType<typeof getContourReferences>> => {
38
+ const contourNames = new Set(contours.map((contour) => contour.name));
39
+
40
+ return new Map(
41
+ contours.map((contour) => [
42
+ contour.name,
43
+ getContourReferences(contour).filter((reference) =>
44
+ contourNames.has(reference.contour)
45
+ ),
46
+ ])
47
+ );
48
+ };
49
+
50
+ const getIdentityValue = (fixture: ContourFixture): unknown =>
51
+ fixture.example[fixture.contour.identity];
52
+
53
+ const candidateMatchesSelectedReference = (
54
+ candidate: ContourFixture,
55
+ target: ContourFixture,
56
+ reference: ReturnType<typeof getContourReferences>[number]
57
+ ): boolean =>
58
+ Object.is(candidate.example[reference.field], getIdentityValue(target));
59
+
60
+ const selectedMatchesCandidateReference = (
61
+ fixture: ContourFixture,
62
+ candidate: ContourFixture,
63
+ reference: ReturnType<typeof getContourReferences>[number]
64
+ ): boolean =>
65
+ Object.is(fixture.example[reference.field], getIdentityValue(candidate));
66
+
67
+ const matchesCandidateReferences = (
68
+ candidate: ContourFixture,
69
+ selected: readonly ContourFixture[],
70
+ referencesByContour: ReadonlyMap<
71
+ string,
72
+ ReturnType<typeof getContourReferences>
73
+ >
74
+ ): boolean => {
75
+ const candidateReferences =
76
+ referencesByContour.get(candidate.contour.name) ?? [];
77
+
78
+ for (const reference of candidateReferences) {
79
+ const target = selected.find(
80
+ (fixture) => fixture.contour.name === reference.contour
81
+ );
82
+ if (target === undefined) {
83
+ continue;
84
+ }
85
+ if (!candidateMatchesSelectedReference(candidate, target, reference)) {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ return true;
91
+ };
92
+
93
+ const matchesSelectedReferences = (
94
+ candidate: ContourFixture,
95
+ selected: readonly ContourFixture[],
96
+ referencesByContour: ReadonlyMap<
97
+ string,
98
+ ReturnType<typeof getContourReferences>
99
+ >
100
+ ): boolean => {
101
+ for (const fixture of selected) {
102
+ const fixtureReferences =
103
+ referencesByContour.get(fixture.contour.name) ?? [];
104
+ for (const reference of fixtureReferences) {
105
+ if (reference.contour !== candidate.contour.name) {
106
+ continue;
107
+ }
108
+ if (!selectedMatchesCandidateReference(fixture, candidate, reference)) {
109
+ return false;
110
+ }
111
+ }
112
+ }
113
+
114
+ return true;
115
+ };
116
+
117
+ const matchesKnownReferences = (
118
+ candidate: ContourFixture,
119
+ selected: readonly ContourFixture[],
120
+ referencesByContour: ReadonlyMap<
121
+ string,
122
+ ReturnType<typeof getContourReferences>
123
+ >
124
+ ): boolean =>
125
+ matchesCandidateReferences(candidate, selected, referencesByContour) &&
126
+ matchesSelectedReferences(candidate, selected, referencesByContour);
127
+
128
+ const selectContourFixtures = (
129
+ contours: readonly AnyContour[],
130
+ referencesByContour: ReadonlyMap<
131
+ string,
132
+ ReturnType<typeof getContourReferences>
133
+ >,
134
+ index = 0,
135
+ selected: readonly ContourFixture[] = []
136
+ ): readonly (readonly ContourFixture[])[] => {
137
+ const contour = contours[index];
138
+ if (contour === undefined) {
139
+ return [selected];
140
+ }
141
+
142
+ const examples = contour.examples ?? [];
143
+ const matchingFixtures = examples.flatMap((example, exampleIndex) => {
144
+ const fixture = {
145
+ contour,
146
+ example: example as ExampleRecord,
147
+ index: exampleIndex,
148
+ } satisfies ContourFixture;
149
+
150
+ if (!matchesKnownReferences(fixture, selected, referencesByContour)) {
151
+ return [];
152
+ }
153
+
154
+ return selectContourFixtures(contours, referencesByContour, index + 1, [
155
+ ...selected,
156
+ fixture,
157
+ ]);
158
+ });
159
+
160
+ return matchingFixtures;
161
+ };
162
+
163
+ /**
164
+ * Merge selected contour fixtures into a single candidate input object.
165
+ *
166
+ * The resulting record contains:
167
+ * - `<contour>`: the full fixture payload keyed by contour name.
168
+ * - `<contour><Identity>`: the fixture's identity value on a prefixed key.
169
+ * - `<contour><Field>`: every fixture field on a prefixed key.
170
+ * - Unqualified `<field>` keys: first-write-wins across contours.
171
+ *
172
+ * The first-write-wins behaviour on unqualified keys is intentional but can
173
+ * silently drop a later contour's value when two contours share a field name
174
+ * (e.g. both declare `id`). The prefixed aliases above are unambiguous and
175
+ * always written, so schemas that consume the prefixed form are unaffected;
176
+ * schemas that rely on the bare field name should disambiguate via the
177
+ * prefixed alias instead.
178
+ */
179
+ const buildDerivedInput = (
180
+ fixtures: readonly ContourFixture[]
181
+ ): Record<string, unknown> => {
182
+ const candidate: Record<string, unknown> = {};
183
+
184
+ for (const fixture of fixtures) {
185
+ candidate[fixture.contour.name] = fixture.example;
186
+ candidate[
187
+ `${fixture.contour.name}${capitalize(fixture.contour.identity)}`
188
+ ] = getIdentityValue(fixture);
189
+
190
+ for (const [field, value] of Object.entries(fixture.example)) {
191
+ if (!Object.hasOwn(candidate, field)) {
192
+ candidate[field] = value;
193
+ }
194
+
195
+ candidate[`${fixture.contour.name}${capitalize(field)}`] = value;
196
+ }
197
+ }
198
+
199
+ return candidate;
200
+ };
201
+
202
+ /**
203
+ * Project the merged candidate input down to keys the trail's input schema
204
+ * knows about.
205
+ *
206
+ * `buildDerivedInput` emits synthesized prefixed aliases (e.g. `userEmail`)
207
+ * alongside bare field names. Strict schemas (`z.object(...).strict()`)
208
+ * reject any unknown key, which means an otherwise valid derived fixture
209
+ * would silently fail `safeParse` just because of the synthesized aliases.
210
+ * When the input is a `ZodObject`, trim the candidate to its declared keys
211
+ * before validation. Non-object inputs pass through unchanged — they are
212
+ * validated as-is and can decide for themselves.
213
+ */
214
+ const projectInputForSchema = (
215
+ inputSchema: Trail<unknown, unknown, unknown>['input'],
216
+ candidate: Record<string, unknown>
217
+ ): Record<string, unknown> => {
218
+ if (!(inputSchema instanceof z.ZodObject)) {
219
+ return candidate;
220
+ }
221
+
222
+ const known = Object.keys(inputSchema.shape);
223
+ const projected: Record<string, unknown> = {};
224
+ for (const key of known) {
225
+ if (Object.hasOwn(candidate, key)) {
226
+ projected[key] = candidate[key];
227
+ }
228
+ }
229
+ return projected;
230
+ };
231
+
232
+ /**
233
+ * Derive an expected output value from the selected contour fixtures when
234
+ * exactly one fixture's payload satisfies the trail's output schema.
235
+ *
236
+ * Returns `undefined` when the trail has no output schema, when no fixture
237
+ * matches, or when more than one matches — callers should then leave the
238
+ * derived example without an `expected` and fall back to schema-only
239
+ * validation. We intentionally do **not** infer `expected` from the merged
240
+ * candidate input: input and output schemas frequently overlap structurally
241
+ * but represent different semantics, so inferring from the input would
242
+ * produce false deep-equality failures.
243
+ */
244
+ const deriveExpectedValue = (
245
+ trail: Trail<unknown, unknown, unknown>,
246
+ fixtures: readonly ContourFixture[]
247
+ ): unknown => {
248
+ if (trail.output === undefined) {
249
+ return undefined;
250
+ }
251
+
252
+ const outputSchema = trail.output;
253
+ const contourMatches = fixtures
254
+ .map((fixture) => outputSchema.safeParse(fixture.example))
255
+ .filter((candidate) => candidate.success);
256
+
257
+ if (contourMatches.length !== 1) {
258
+ return undefined;
259
+ }
260
+
261
+ const [singleMatch] = contourMatches;
262
+ if (singleMatch === undefined) {
263
+ return undefined;
264
+ }
265
+ return singleMatch.data;
266
+ };
267
+
268
+ const formatFixtureName = (
269
+ fixtures: readonly ContourFixture[],
270
+ index: number
271
+ ): string => {
272
+ const label = fixtures
273
+ .map((fixture) => {
274
+ const identity = getIdentityValue(fixture);
275
+ const fallback = fixture.index + 1;
276
+ return `${fixture.contour.name}:${String(identity ?? fallback)}`;
277
+ })
278
+ .join(', ');
279
+
280
+ return label.length > 0
281
+ ? `Derived fixture ${index + 1} (${label})`
282
+ : `Derived fixture ${index + 1}`;
283
+ };
284
+
285
+ /**
286
+ * Prefer authored trail examples and fall back to contour-derived fixtures.
287
+ *
288
+ * Examples returned by this helper come from one of two provenances:
289
+ * - **Authored.** When `trail.examples` is non-empty, its entries are
290
+ * returned verbatim. These are the developer's stated intent and carry
291
+ * full invariants — including crossing-coverage assertions in
292
+ * `testExamples`.
293
+ * - **Derived.** When there are no authored examples but the trail has
294
+ * contours with examples, candidate inputs are synthesized from contour
295
+ * fixtures and validated against `trail.input`. These are opportunistic
296
+ * coverage that exists to let `testAll(app)` exercise contour-backed
297
+ * trails without per-test setup; they are not guaranteed to exercise
298
+ * every composition branch, so consumers should relax invariants that
299
+ * only make sense for authored inputs (see `isDerivedExample`).
300
+ *
301
+ * Contour examples stay as the raw input payload so Trails validation /
302
+ * transforms still happen exactly once inside the normal test execution
303
+ * path. Derived examples are additionally tagged via a module-level
304
+ * `WeakSet` so consumers can detect them without widening the public
305
+ * `TrailExample` shape.
306
+ */
307
+ export const deriveTrailExamples = (
308
+ trail: Trail<unknown, unknown, unknown>
309
+ ): readonly TrailExample<unknown, unknown>[] => {
310
+ if (trail.examples !== undefined && trail.examples.length > 0) {
311
+ return trail.examples;
312
+ }
313
+
314
+ if (trail.contours.length === 0) {
315
+ return [];
316
+ }
317
+
318
+ if (
319
+ trail.contours.some(
320
+ (contour) =>
321
+ contour.examples === undefined || contour.examples.length === 0
322
+ )
323
+ ) {
324
+ return [];
325
+ }
326
+
327
+ const referencesByContour = collectReferenceMap(trail.contours);
328
+ const fixtureSets = selectContourFixtures(
329
+ trail.contours,
330
+ referencesByContour
331
+ );
332
+
333
+ return fixtureSets.flatMap((fixtures, index) => {
334
+ const merged = buildDerivedInput(fixtures);
335
+ const input = projectInputForSchema(trail.input, merged);
336
+ const validated = trail.input.safeParse(input);
337
+ if (!validated.success) {
338
+ return [];
339
+ }
340
+
341
+ const expected = deriveExpectedValue(trail, fixtures);
342
+ const derived: TrailExample<unknown, unknown> = {
343
+ ...(expected === undefined ? {} : { expected }),
344
+ input,
345
+ name: formatFixtureName(fixtures, index),
346
+ };
347
+ derivedExamples.add(derived);
348
+ return [derived];
349
+ });
350
+ };