@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.
- package/CHANGELOG.md +526 -12
- package/README.md +83 -18
- package/package.json +34 -5
- package/src/all-established.ts +168 -0
- package/src/all.ts +51 -12
- package/src/assertions.ts +253 -0
- package/src/cli.ts +6 -0
- package/src/composes.ts +433 -0
- package/src/context.ts +200 -14
- package/src/contracts.ts +44 -33
- package/src/detours.ts +155 -18
- package/src/effective-examples.ts +408 -0
- package/src/errors.ts +47 -0
- package/src/examples.ts +274 -113
- package/src/harness-cli.ts +83 -58
- package/src/harness-http.ts +341 -0
- package/src/harness-mcp.ts +49 -16
- package/src/http.ts +10 -0
- package/src/index.ts +22 -14
- package/src/logger.ts +3 -1
- 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 +1 -1
- package/src/types.ts +24 -52
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/all.d.ts +0 -30
- package/dist/all.d.ts.map +0 -1
- package/dist/all.js +0 -47
- package/dist/all.js.map +0 -1
- package/dist/assertions.d.ts +0 -49
- package/dist/assertions.d.ts.map +0 -1
- package/dist/assertions.js +0 -84
- package/dist/assertions.js.map +0 -1
- package/dist/context.d.ts +0 -19
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -33
- package/dist/context.js.map +0 -1
- package/dist/contracts.d.ts +0 -16
- package/dist/contracts.d.ts.map +0 -1
- package/dist/contracts.js +0 -66
- package/dist/contracts.js.map +0 -1
- package/dist/detours.d.ts +0 -12
- package/dist/detours.d.ts.map +0 -1
- package/dist/detours.js +0 -30
- package/dist/detours.js.map +0 -1
- package/dist/examples.d.ts +0 -22
- package/dist/examples.d.ts.map +0 -1
- package/dist/examples.js +0 -175
- package/dist/examples.js.map +0 -1
- package/dist/follows.d.ts +0 -32
- package/dist/follows.d.ts.map +0 -1
- package/dist/follows.js +0 -169
- package/dist/follows.js.map +0 -1
- package/dist/harness-cli.d.ts +0 -21
- package/dist/harness-cli.d.ts.map +0 -1
- package/dist/harness-cli.js +0 -213
- package/dist/harness-cli.js.map +0 -1
- package/dist/harness-mcp.d.ts +0 -21
- package/dist/harness-mcp.d.ts.map +0 -1
- package/dist/harness-mcp.js +0 -50
- package/dist/harness-mcp.js.map +0 -1
- package/dist/index.d.ts +0 -14
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -16
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts +0 -15
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -87
- package/dist/logger.js.map +0 -1
- package/dist/trail.d.ts +0 -20
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -80
- package/dist/trail.js.map +0 -1
- package/dist/types.d.ts +0 -80
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/src/__tests__/context.test.ts +0 -60
- package/src/__tests__/contracts.test.ts +0 -94
- package/src/__tests__/detours.test.ts +0 -55
- package/src/__tests__/examples.test.ts +0 -175
- package/src/__tests__/follows.test.ts +0 -163
- package/src/__tests__/logger.test.ts +0 -136
- package/src/__tests__/trail.test.ts +0 -99
- package/src/follows.ts +0 -283
- package/tsconfig.json +0 -9
- 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 `
|
|
7
|
-
* declarations, checks that every declared
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
125
|
-
*
|
|
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
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
|
|
130
|
+
testCtx: TrailContext,
|
|
131
|
+
resources?: ResourceOverrideMap,
|
|
132
|
+
opts?: TestExecutionOptions
|
|
132
133
|
): Promise<void> => {
|
|
133
|
-
const
|
|
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.
|
|
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
|
-
//
|
|
183
|
+
// Composing coverage for trails with compositions
|
|
146
184
|
// ---------------------------------------------------------------------------
|
|
147
185
|
|
|
148
186
|
/**
|
|
149
|
-
* Build a recording
|
|
187
|
+
* Build a recording compose function that tracks which trail IDs are called.
|
|
150
188
|
*
|
|
151
|
-
* Delegates to `
|
|
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
|
|
193
|
+
const createCoverageCompose = (
|
|
156
194
|
called: Set<string>,
|
|
157
|
-
|
|
195
|
+
baseCompose: ComposeFn | undefined,
|
|
158
196
|
topo: Topo,
|
|
159
|
-
ctx: TrailContext
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
246
|
+
return Result.ok();
|
|
178
247
|
};
|
|
179
|
-
|
|
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
|
|
279
|
+
* Run a single example against a trail with compositions, recording compose calls.
|
|
184
280
|
*/
|
|
185
281
|
const runCompositionExample = async (
|
|
186
|
-
|
|
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
|
|
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
|
|
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 `
|
|
215
|
-
* every declared
|
|
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(
|
|
334
|
+
* testExamples(graph);
|
|
220
335
|
* ```
|
|
221
336
|
*/
|
|
222
337
|
export const testExamples = (
|
|
223
338
|
app: Topo,
|
|
224
|
-
ctxOrFactory?:
|
|
339
|
+
ctxOrFactory?:
|
|
340
|
+
| Partial<TrailContext>
|
|
341
|
+
| TestExecutionOptions
|
|
342
|
+
| (() => Partial<TrailContext> | TestExecutionOptions)
|
|
225
343
|
): void => {
|
|
226
|
-
const
|
|
344
|
+
const resolveInput =
|
|
227
345
|
typeof ctxOrFactory === 'function' ? ctxOrFactory : () => ctxOrFactory;
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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
|
|
248
|
-
|
|
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
|
|
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
|
|
387
|
+
const { examples } = t;
|
|
258
388
|
if (!examples) {
|
|
259
389
|
return;
|
|
260
390
|
}
|
|
261
391
|
|
|
262
|
-
const
|
|
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
|
|
268
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
};
|