@ontrails/testing 0.2.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.
- package/CHANGELOG.md +807 -0
- package/README.md +157 -0
- package/package.json +57 -0
- package/src/all-established.ts +168 -0
- package/src/all.ts +94 -0
- package/src/assertions.ts +361 -0
- package/src/cli.ts +6 -0
- package/src/composes.ts +433 -0
- package/src/context.ts +228 -0
- package/src/contracts.ts +109 -0
- package/src/detours.ts +181 -0
- package/src/effective-examples.ts +408 -0
- package/src/errors.ts +47 -0
- package/src/examples.ts +439 -0
- package/src/harness-cli.ts +335 -0
- package/src/harness-http.ts +341 -0
- package/src/harness-mcp.ts +98 -0
- package/src/http.ts +10 -0
- package/src/index.ts +48 -0
- package/src/logger.ts +127 -0
- package/src/mcp.ts +6 -0
- package/src/scenario.ts +375 -0
- package/src/signals.ts +221 -0
- package/src/surface-parity.ts +389 -0
- package/src/trail.ts +116 -0
- package/src/types.ts +89 -0
package/src/errors.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
errorClasses,
|
|
3
|
+
InternalError,
|
|
4
|
+
RetryExhaustedError,
|
|
5
|
+
TrailsError,
|
|
6
|
+
} from '@ontrails/core';
|
|
7
|
+
|
|
8
|
+
type ErrorConstructor = new (...args: never[]) => Error;
|
|
9
|
+
type MessageErrorConstructor = new (message: string) => Error;
|
|
10
|
+
|
|
11
|
+
const ERROR_CLASS_BY_NAME = new Map<string, ErrorConstructor>([
|
|
12
|
+
...errorClasses.map(
|
|
13
|
+
(entry) => [entry.name, entry.ctor as ErrorConstructor] as const
|
|
14
|
+
),
|
|
15
|
+
['TrailsError', TrailsError as unknown as ErrorConstructor],
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve an error class name string to the actual constructor.
|
|
20
|
+
* Falls back to generic Error if the name is not in the core taxonomy.
|
|
21
|
+
*/
|
|
22
|
+
export const resolveErrorClass = (name: string): ErrorConstructor =>
|
|
23
|
+
ERROR_CLASS_BY_NAME.get(name) ?? (Error as ErrorConstructor);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create an error instance for an authored example error name.
|
|
27
|
+
*/
|
|
28
|
+
export const createErrorFromName = (name: string): Error => {
|
|
29
|
+
if (name === 'TrailsError') {
|
|
30
|
+
return new InternalError(name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const entry = errorClasses.find((candidate) => candidate.name === name);
|
|
34
|
+
if (entry === undefined) {
|
|
35
|
+
return new Error(name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (entry.name === 'RetryExhaustedError') {
|
|
39
|
+
return new RetryExhaustedError(new InternalError(name), {
|
|
40
|
+
attempts: 1,
|
|
41
|
+
detour: 'testComposes',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ErrorClass = entry.ctor as unknown as MessageErrorConstructor;
|
|
46
|
+
return new ErrorClass(name);
|
|
47
|
+
};
|
package/src/examples.ts
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* testExamples — the headline one-liner.
|
|
3
|
+
*
|
|
4
|
+
* Iterates every trail in the app's topo. For each trail with examples,
|
|
5
|
+
* generates describe/test blocks using bun:test. Progressive assertion
|
|
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
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from 'bun:test';
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
ComposeFn,
|
|
14
|
+
ComposeOptions,
|
|
15
|
+
ExecuteTrailOptions,
|
|
16
|
+
ResourceOverrideMap,
|
|
17
|
+
Topo,
|
|
18
|
+
TrailExample,
|
|
19
|
+
Trail,
|
|
20
|
+
TrailContext,
|
|
21
|
+
} from '@ontrails/core';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
buildComposeValidationSchema,
|
|
25
|
+
executeTrail,
|
|
26
|
+
parseTrailIdVersionReference,
|
|
27
|
+
Result,
|
|
28
|
+
ValidationError,
|
|
29
|
+
validateInput,
|
|
30
|
+
} from '@ontrails/core';
|
|
31
|
+
import type { z } from 'zod';
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
assertErrorMatch,
|
|
35
|
+
assertFullMatch,
|
|
36
|
+
assertPartialMatch,
|
|
37
|
+
assertSchemaMatch,
|
|
38
|
+
} from './assertions.js';
|
|
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>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Helpers
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
const assertProgressiveMatch = (
|
|
64
|
+
result: Result<unknown, Error>,
|
|
65
|
+
example: TrailExample<unknown, unknown>,
|
|
66
|
+
output: z.ZodType | undefined
|
|
67
|
+
): void => {
|
|
68
|
+
if (example.expected !== undefined) {
|
|
69
|
+
return assertFullMatch(result, example.expected);
|
|
70
|
+
}
|
|
71
|
+
if (example.expectedMatch !== undefined) {
|
|
72
|
+
return assertPartialMatch(result, example.expectedMatch);
|
|
73
|
+
}
|
|
74
|
+
if (example.error !== undefined) {
|
|
75
|
+
return assertErrorMatch(result, resolveErrorClass(example.error));
|
|
76
|
+
}
|
|
77
|
+
assertSchemaMatch(result, output);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Handle input validation failure for an example.
|
|
82
|
+
* Returns true if the validation error was expected (and assertions passed).
|
|
83
|
+
* Throws if the validation error was unexpected.
|
|
84
|
+
*/
|
|
85
|
+
const handleValidationError = (
|
|
86
|
+
validated: Result<unknown, Error>,
|
|
87
|
+
example: TrailExample<unknown, unknown>
|
|
88
|
+
): boolean => {
|
|
89
|
+
if (!validated.isErr()) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (example.error !== undefined) {
|
|
94
|
+
const errorClass = resolveErrorClass(example.error);
|
|
95
|
+
expect(validated.error).toBeInstanceOf(errorClass);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
throw new Error(
|
|
100
|
+
`Example "${example.name}" has invalid input: ${validated.error.message}`
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
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.
|
|
107
|
+
*/
|
|
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
|
+
example: TrailExample<unknown, unknown>,
|
|
130
|
+
testCtx: TrailContext,
|
|
131
|
+
resources?: ResourceOverrideMap,
|
|
132
|
+
opts?: TestExecutionOptions
|
|
133
|
+
): Promise<void> => {
|
|
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);
|
|
138
|
+
|
|
139
|
+
if (handleValidationError(validated, example)) {
|
|
140
|
+
signals.assert();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
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
|
+
});
|
|
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
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Composing coverage for trails with compositions
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Build a recording compose function that tracks which trail IDs are called.
|
|
188
|
+
*
|
|
189
|
+
* Delegates to `baseCompose` when available, otherwise looks up the trail
|
|
190
|
+
* in the topo and executes it with validated input. Falls back to
|
|
191
|
+
* `Result.ok()` when neither is available.
|
|
192
|
+
*/
|
|
193
|
+
const createCoverageCompose = (
|
|
194
|
+
called: Set<string>,
|
|
195
|
+
baseCompose: ComposeFn | undefined,
|
|
196
|
+
topo: Topo,
|
|
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
|
+
}
|
|
222
|
+
|
|
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);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const trailDef = topo.get(id);
|
|
236
|
+
if (trailDef !== undefined) {
|
|
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);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return Result.ok();
|
|
247
|
+
};
|
|
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;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Run a single example against a trail with compositions, recording compose calls.
|
|
280
|
+
*/
|
|
281
|
+
const runCompositionExample = async (
|
|
282
|
+
target: TrailExampleTarget,
|
|
283
|
+
example: TrailExample<unknown, unknown>,
|
|
284
|
+
baseCtx: TrailContext,
|
|
285
|
+
called: Set<string>,
|
|
286
|
+
topo: Topo,
|
|
287
|
+
resources?: ResourceOverrideMap,
|
|
288
|
+
opts?: TestExecutionOptions
|
|
289
|
+
): Promise<void> => {
|
|
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);
|
|
296
|
+
|
|
297
|
+
if (handleValidationError(validated, example)) {
|
|
298
|
+
signals.assert();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
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
|
+
});
|
|
318
|
+
assertProgressiveMatch(result, example, output);
|
|
319
|
+
signals.assert();
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// testExamples
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Generate describe/test blocks for every trail example in the app.
|
|
328
|
+
*
|
|
329
|
+
* For trails with `composes` declarations and examples, also verifies that
|
|
330
|
+
* every declared composed ID was called at least once across all examples.
|
|
331
|
+
*
|
|
332
|
+
* One line in your test file:
|
|
333
|
+
* ```ts
|
|
334
|
+
* testExamples(graph);
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
337
|
+
export const testExamples = (
|
|
338
|
+
app: Topo,
|
|
339
|
+
ctxOrFactory?:
|
|
340
|
+
| Partial<TrailContext>
|
|
341
|
+
| TestExecutionOptions
|
|
342
|
+
| (() => Partial<TrailContext> | TestExecutionOptions)
|
|
343
|
+
): void => {
|
|
344
|
+
const resolveInput =
|
|
345
|
+
typeof ctxOrFactory === 'function' ? ctxOrFactory : () => ctxOrFactory;
|
|
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);
|
|
351
|
+
|
|
352
|
+
// Simple trails: run examples directly
|
|
353
|
+
if (simpleTrails.length > 0) {
|
|
354
|
+
describe.each(simpleTrails)('$id', (t) => {
|
|
355
|
+
const { examples } = t;
|
|
356
|
+
if (!examples) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
test.each([...examples])(
|
|
361
|
+
'example: $name',
|
|
362
|
+
async (example: TrailExample<unknown, unknown>) => {
|
|
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);
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
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.
|
|
385
|
+
if (compositionTrails.length > 0) {
|
|
386
|
+
describe.each(compositionTrails)('$id', (t) => {
|
|
387
|
+
const { examples } = t;
|
|
388
|
+
if (!examples) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
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;
|
|
406
|
+
|
|
407
|
+
test.each([...examples])(
|
|
408
|
+
'example: $name',
|
|
409
|
+
async (example: TrailExample<unknown, unknown>) => {
|
|
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
|
+
);
|
|
426
|
+
}
|
|
427
|
+
);
|
|
428
|
+
|
|
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
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
};
|