@ontrails/testing 1.0.0-beta.10 → 1.0.0-beta.12
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/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +47 -0
- package/dist/all.d.ts +2 -1
- package/dist/all.d.ts.map +1 -1
- package/dist/all.js.map +1 -1
- package/dist/context.d.ts +39 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +60 -13
- package/dist/context.js.map +1 -1
- package/dist/contracts.d.ts +2 -1
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +13 -8
- package/dist/contracts.js.map +1 -1
- package/dist/examples.d.ts +2 -1
- package/dist/examples.d.ts.map +1 -1
- package/dist/examples.js +49 -22
- package/dist/examples.js.map +1 -1
- package/dist/follows.d.ts +7 -1
- package/dist/follows.d.ts.map +1 -1
- package/dist/follows.js +60 -17
- package/dist/follows.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/all.test.ts +64 -0
- package/src/__tests__/context.test.ts +58 -3
- package/src/__tests__/contracts.test.ts +92 -1
- package/src/__tests__/examples.test.ts +359 -1
- package/src/__tests__/follows.test.ts +427 -1
- package/src/all.ts +5 -1
- package/src/context.ts +126 -15
- package/src/contracts.ts +28 -10
- package/src/examples.ts +94 -22
- package/src/follows.ts +112 -17
- package/src/index.ts +10 -1
package/src/contracts.ts
CHANGED
|
@@ -9,11 +9,17 @@
|
|
|
9
9
|
import { describe, test } from 'bun:test';
|
|
10
10
|
|
|
11
11
|
import type { Topo, TrailExample, Trail, TrailContext } from '@ontrails/core';
|
|
12
|
-
import { formatZodIssues, validateInput } from '@ontrails/core';
|
|
12
|
+
import { executeTrail, formatZodIssues, validateInput } from '@ontrails/core';
|
|
13
13
|
import type { z } from 'zod';
|
|
14
14
|
|
|
15
15
|
import { expectOk } from './assertions.js';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
mergeServiceOverrides,
|
|
18
|
+
mergeTestContext,
|
|
19
|
+
normalizeTestExecutionOptions,
|
|
20
|
+
resolveMockServices,
|
|
21
|
+
} from './context.js';
|
|
22
|
+
import type { TestExecutionOptions } from './context.js';
|
|
17
23
|
|
|
18
24
|
// ---------------------------------------------------------------------------
|
|
19
25
|
// Helpers
|
|
@@ -22,13 +28,13 @@ import { mergeTestContext } from './context.js';
|
|
|
22
28
|
/** Check if a trail requires follow() but the context doesn't provide it. */
|
|
23
29
|
const needsFollowContext = (
|
|
24
30
|
t: unknown,
|
|
25
|
-
resolveCtx: () => Partial<TrailContext> | undefined
|
|
31
|
+
resolveCtx: () => Partial<TrailContext> | TestExecutionOptions | undefined
|
|
26
32
|
): boolean => {
|
|
27
33
|
const spec = t as { follow?: readonly string[] };
|
|
28
34
|
if (!spec.follow || spec.follow.length === 0) {
|
|
29
35
|
return false;
|
|
30
36
|
}
|
|
31
|
-
return !resolveCtx()?.follow;
|
|
37
|
+
return !normalizeTestExecutionOptions(resolveCtx()).ctx?.follow;
|
|
32
38
|
};
|
|
33
39
|
|
|
34
40
|
const validateOutputSchema = (
|
|
@@ -58,9 +64,12 @@ const validateOutputSchema = (
|
|
|
58
64
|
*/
|
|
59
65
|
export const testContracts = (
|
|
60
66
|
app: Topo,
|
|
61
|
-
ctxOrFactory?:
|
|
67
|
+
ctxOrFactory?:
|
|
68
|
+
| Partial<TrailContext>
|
|
69
|
+
| TestExecutionOptions
|
|
70
|
+
| (() => Partial<TrailContext> | TestExecutionOptions)
|
|
62
71
|
): void => {
|
|
63
|
-
const
|
|
72
|
+
const resolveInput =
|
|
64
73
|
typeof ctxOrFactory === 'function' ? ctxOrFactory : () => ctxOrFactory;
|
|
65
74
|
const allEntries = app.list() as Trail<unknown, unknown>[];
|
|
66
75
|
|
|
@@ -72,7 +81,7 @@ export const testContracts = (
|
|
|
72
81
|
if (t.examples === undefined || t.examples.length === 0) {
|
|
73
82
|
return;
|
|
74
83
|
}
|
|
75
|
-
if (needsFollowContext(t,
|
|
84
|
+
if (needsFollowContext(t, resolveInput)) {
|
|
76
85
|
return;
|
|
77
86
|
}
|
|
78
87
|
|
|
@@ -82,12 +91,21 @@ export const testContracts = (
|
|
|
82
91
|
test.each(successExamples)(
|
|
83
92
|
'contract: $name',
|
|
84
93
|
async (example: TrailExample<unknown, unknown>) => {
|
|
85
|
-
const
|
|
94
|
+
const resolved = normalizeTestExecutionOptions(resolveInput());
|
|
95
|
+
const services = mergeServiceOverrides(
|
|
96
|
+
await resolveMockServices(app),
|
|
97
|
+
resolved.ctx,
|
|
98
|
+
resolved.services
|
|
99
|
+
);
|
|
100
|
+
const testCtx = mergeTestContext(resolved.ctx);
|
|
86
101
|
|
|
87
102
|
const validated = validateInput(t.input, example.input);
|
|
88
|
-
|
|
103
|
+
expectOk(validated);
|
|
89
104
|
|
|
90
|
-
const result = await t.
|
|
105
|
+
const result = await executeTrail(t, example.input, {
|
|
106
|
+
ctx: testCtx,
|
|
107
|
+
services,
|
|
108
|
+
});
|
|
91
109
|
const resultValue = expectOk(result);
|
|
92
110
|
|
|
93
111
|
validateOutputSchema(outputSchema, resultValue, t.id, example.name);
|
package/src/examples.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { describe, expect, test } from 'bun:test';
|
|
|
11
11
|
|
|
12
12
|
import type {
|
|
13
13
|
FollowFn,
|
|
14
|
+
ServiceOverrideMap,
|
|
14
15
|
Topo,
|
|
15
16
|
TrailExample,
|
|
16
17
|
Trail,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
NotFoundError,
|
|
30
31
|
PermissionError,
|
|
31
32
|
RateLimitError,
|
|
33
|
+
executeTrail,
|
|
32
34
|
Result,
|
|
33
35
|
TimeoutError,
|
|
34
36
|
TrailsError,
|
|
@@ -41,9 +43,15 @@ import {
|
|
|
41
43
|
assertErrorMatch,
|
|
42
44
|
assertFullMatch,
|
|
43
45
|
assertSchemaMatch,
|
|
44
|
-
expectOk,
|
|
45
46
|
} from './assertions.js';
|
|
46
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
defaultMintPermit,
|
|
49
|
+
mergeServiceOverrides,
|
|
50
|
+
mergeTestContext,
|
|
51
|
+
normalizeTestExecutionOptions,
|
|
52
|
+
resolveMockServices,
|
|
53
|
+
} from './context.js';
|
|
54
|
+
import type { MintableTrail, TestExecutionOptions } from './context.js';
|
|
47
55
|
|
|
48
56
|
// ---------------------------------------------------------------------------
|
|
49
57
|
// Error class name -> constructor map
|
|
@@ -120,6 +128,29 @@ const handleValidationError = (
|
|
|
120
128
|
);
|
|
121
129
|
};
|
|
122
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Apply auto-minting: if the trail declares scoped permits and the context
|
|
133
|
+
* doesn't already have a permit, mint one and merge it into the context.
|
|
134
|
+
*/
|
|
135
|
+
const applyAutoMint = (
|
|
136
|
+
ctx: TrailContext,
|
|
137
|
+
trailDef: MintableTrail,
|
|
138
|
+
opts: TestExecutionOptions
|
|
139
|
+
): TrailContext => {
|
|
140
|
+
if (opts.strictPermits) {
|
|
141
|
+
return ctx;
|
|
142
|
+
}
|
|
143
|
+
if (ctx.permit !== undefined) {
|
|
144
|
+
return ctx;
|
|
145
|
+
}
|
|
146
|
+
const mint = opts.mintPermit ?? defaultMintPermit;
|
|
147
|
+
const permit = mint(trailDef);
|
|
148
|
+
if (!permit) {
|
|
149
|
+
return ctx;
|
|
150
|
+
}
|
|
151
|
+
return { ...ctx, permit };
|
|
152
|
+
};
|
|
153
|
+
|
|
123
154
|
/**
|
|
124
155
|
* Run a single example against a trail.
|
|
125
156
|
* Handles validation, execution, and assertions.
|
|
@@ -128,16 +159,22 @@ const runExample = async (
|
|
|
128
159
|
t: Trail<unknown, unknown>,
|
|
129
160
|
example: TrailExample<unknown, unknown>,
|
|
130
161
|
output: z.ZodType | undefined,
|
|
131
|
-
testCtx: TrailContext
|
|
162
|
+
testCtx: TrailContext,
|
|
163
|
+
services?: ServiceOverrideMap,
|
|
164
|
+
opts?: TestExecutionOptions
|
|
132
165
|
): Promise<void> => {
|
|
133
166
|
const validated = validateInput(t.input, example.input);
|
|
134
167
|
|
|
135
168
|
if (handleValidationError(validated, example)) {
|
|
136
169
|
return;
|
|
137
170
|
}
|
|
138
|
-
const validatedInput = expectOk(validated);
|
|
139
171
|
|
|
140
|
-
const
|
|
172
|
+
const ctx = opts ? applyAutoMint(testCtx, t, opts) : testCtx;
|
|
173
|
+
|
|
174
|
+
const result = await executeTrail(t, example.input, {
|
|
175
|
+
ctx,
|
|
176
|
+
services,
|
|
177
|
+
});
|
|
141
178
|
assertProgressiveMatch(result, example, output);
|
|
142
179
|
};
|
|
143
180
|
|
|
@@ -156,7 +193,8 @@ const createCoverageFollow = (
|
|
|
156
193
|
called: Set<string>,
|
|
157
194
|
baseFollow: FollowFn | undefined,
|
|
158
195
|
topo: Topo,
|
|
159
|
-
ctx: TrailContext
|
|
196
|
+
ctx: TrailContext,
|
|
197
|
+
services?: ServiceOverrideMap
|
|
160
198
|
): FollowFn => {
|
|
161
199
|
const follow = (id: string, input: unknown) => {
|
|
162
200
|
called.add(id);
|
|
@@ -167,11 +205,10 @@ const createCoverageFollow = (
|
|
|
167
205
|
|
|
168
206
|
const trailDef = topo.get(id);
|
|
169
207
|
if (trailDef !== undefined) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
return Promise.resolve(trailDef.run(validated.value, ctx));
|
|
208
|
+
return executeTrail(trailDef, input, {
|
|
209
|
+
ctx: { ...ctx, follow },
|
|
210
|
+
services,
|
|
211
|
+
});
|
|
175
212
|
}
|
|
176
213
|
|
|
177
214
|
return Promise.resolve(Result.ok());
|
|
@@ -188,19 +225,30 @@ const runCompositionExample = async (
|
|
|
188
225
|
output: z.ZodType | undefined,
|
|
189
226
|
baseCtx: TrailContext,
|
|
190
227
|
called: Set<string>,
|
|
191
|
-
topo: Topo
|
|
228
|
+
topo: Topo,
|
|
229
|
+
services?: ServiceOverrideMap,
|
|
230
|
+
opts?: TestExecutionOptions
|
|
192
231
|
): Promise<void> => {
|
|
193
232
|
const validated = validateInput(trailDef.input, example.input);
|
|
194
233
|
|
|
195
234
|
if (handleValidationError(validated, example)) {
|
|
196
235
|
return;
|
|
197
236
|
}
|
|
198
|
-
const validatedInput = expectOk(validated);
|
|
199
237
|
|
|
200
|
-
const
|
|
201
|
-
const
|
|
238
|
+
const mintedCtx = opts ? applyAutoMint(baseCtx, trailDef, opts) : baseCtx;
|
|
239
|
+
const follow = createCoverageFollow(
|
|
240
|
+
called,
|
|
241
|
+
mintedCtx.follow,
|
|
242
|
+
topo,
|
|
243
|
+
mintedCtx,
|
|
244
|
+
services
|
|
245
|
+
);
|
|
246
|
+
const testCtx: TrailContext = { ...mintedCtx, follow };
|
|
202
247
|
|
|
203
|
-
const result = await trailDef.
|
|
248
|
+
const result = await executeTrail(trailDef, example.input, {
|
|
249
|
+
ctx: testCtx,
|
|
250
|
+
services,
|
|
251
|
+
});
|
|
204
252
|
assertProgressiveMatch(result, example, output);
|
|
205
253
|
};
|
|
206
254
|
|
|
@@ -221,9 +269,12 @@ const runCompositionExample = async (
|
|
|
221
269
|
*/
|
|
222
270
|
export const testExamples = (
|
|
223
271
|
app: Topo,
|
|
224
|
-
ctxOrFactory?:
|
|
272
|
+
ctxOrFactory?:
|
|
273
|
+
| Partial<TrailContext>
|
|
274
|
+
| TestExecutionOptions
|
|
275
|
+
| (() => Partial<TrailContext> | TestExecutionOptions)
|
|
225
276
|
): void => {
|
|
226
|
-
const
|
|
277
|
+
const resolveInput =
|
|
227
278
|
typeof ctxOrFactory === 'function' ? ctxOrFactory : () => ctxOrFactory;
|
|
228
279
|
const allTrails = app.list() as Trail<unknown, unknown>[];
|
|
229
280
|
|
|
@@ -244,8 +295,14 @@ export const testExamples = (
|
|
|
244
295
|
test.each([...examples])(
|
|
245
296
|
'example: $name',
|
|
246
297
|
async (example: TrailExample<unknown, unknown>) => {
|
|
247
|
-
const
|
|
248
|
-
|
|
298
|
+
const resolved = normalizeTestExecutionOptions(resolveInput());
|
|
299
|
+
const services = mergeServiceOverrides(
|
|
300
|
+
await resolveMockServices(app),
|
|
301
|
+
resolved.ctx,
|
|
302
|
+
resolved.services
|
|
303
|
+
);
|
|
304
|
+
const testCtx = mergeTestContext(resolved.ctx);
|
|
305
|
+
await runExample(t, example, output, testCtx, services, resolved);
|
|
249
306
|
}
|
|
250
307
|
);
|
|
251
308
|
});
|
|
@@ -264,8 +321,23 @@ export const testExamples = (
|
|
|
264
321
|
test.each([...examples])(
|
|
265
322
|
'example: $name',
|
|
266
323
|
async (example: TrailExample<unknown, unknown>) => {
|
|
267
|
-
const
|
|
268
|
-
|
|
324
|
+
const resolved = normalizeTestExecutionOptions(resolveInput());
|
|
325
|
+
const services = mergeServiceOverrides(
|
|
326
|
+
await resolveMockServices(app),
|
|
327
|
+
resolved.ctx,
|
|
328
|
+
resolved.services
|
|
329
|
+
);
|
|
330
|
+
const baseCtx = mergeTestContext(resolved.ctx);
|
|
331
|
+
await runCompositionExample(
|
|
332
|
+
t,
|
|
333
|
+
example,
|
|
334
|
+
output,
|
|
335
|
+
baseCtx,
|
|
336
|
+
called,
|
|
337
|
+
app,
|
|
338
|
+
services,
|
|
339
|
+
resolved
|
|
340
|
+
);
|
|
269
341
|
}
|
|
270
342
|
);
|
|
271
343
|
|
package/src/follows.ts
CHANGED
|
@@ -7,8 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import { describe, expect, test } from 'bun:test';
|
|
9
9
|
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
AnyTrail,
|
|
12
|
+
FollowFn,
|
|
13
|
+
ServiceOverrideMap,
|
|
14
|
+
TrailContext,
|
|
15
|
+
} from '@ontrails/core';
|
|
11
16
|
import {
|
|
17
|
+
executeTrail,
|
|
12
18
|
InternalError,
|
|
13
19
|
Result,
|
|
14
20
|
ValidationError,
|
|
@@ -19,9 +25,8 @@ import {
|
|
|
19
25
|
assertErrorMatch,
|
|
20
26
|
assertFullMatch,
|
|
21
27
|
assertSchemaMatch,
|
|
22
|
-
expectOk,
|
|
23
28
|
} from './assertions.js';
|
|
24
|
-
import { mergeTestContext } from './context.js';
|
|
29
|
+
import { mergeServiceOverrides, mergeTestContext } from './context.js';
|
|
25
30
|
import type { FollowScenario } from './types.js';
|
|
26
31
|
|
|
27
32
|
// ---------------------------------------------------------------------------
|
|
@@ -33,6 +38,58 @@ interface FollowRecord {
|
|
|
33
38
|
readonly input: unknown;
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
const collectDeclaredServices = (
|
|
42
|
+
trailDef: AnyTrail,
|
|
43
|
+
trailsMap: ReadonlyMap<string, AnyTrail> | undefined
|
|
44
|
+
): AnyTrail['services'] => {
|
|
45
|
+
const seenServiceIds = new Set<string>();
|
|
46
|
+
const seenTrailIds = new Set<string>();
|
|
47
|
+
const services: AnyTrail['services'][number][] = [];
|
|
48
|
+
|
|
49
|
+
const collect = (candidate: AnyTrail): void => {
|
|
50
|
+
for (const declaredService of candidate.services) {
|
|
51
|
+
if (seenServiceIds.has(declaredService.id)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
seenServiceIds.add(declaredService.id);
|
|
55
|
+
services.push(declaredService);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const visit = (candidate: AnyTrail): void => {
|
|
60
|
+
if (seenTrailIds.has(candidate.id)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
seenTrailIds.add(candidate.id);
|
|
64
|
+
collect(candidate);
|
|
65
|
+
for (const followedId of candidate.follow) {
|
|
66
|
+
const followedTrail = trailsMap?.get(followedId);
|
|
67
|
+
if (followedTrail) {
|
|
68
|
+
visit(followedTrail);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
visit(trailDef);
|
|
74
|
+
return services;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const resolveMockServices = async (
|
|
78
|
+
trailDef: AnyTrail,
|
|
79
|
+
trailsMap: ReadonlyMap<string, AnyTrail> | undefined
|
|
80
|
+
): Promise<ServiceOverrideMap> => {
|
|
81
|
+
const services: Record<string, unknown> = {};
|
|
82
|
+
|
|
83
|
+
for (const declaredService of collectDeclaredServices(trailDef, trailsMap)) {
|
|
84
|
+
if (!declaredService.mock) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
services[declaredService.id] = await declaredService.mock();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return services;
|
|
91
|
+
};
|
|
92
|
+
|
|
36
93
|
// ---------------------------------------------------------------------------
|
|
37
94
|
// Injection helpers
|
|
38
95
|
// ---------------------------------------------------------------------------
|
|
@@ -90,18 +147,20 @@ const executeFromMap = (
|
|
|
90
147
|
id: string,
|
|
91
148
|
input: unknown,
|
|
92
149
|
trailsMap: ReadonlyMap<string, AnyTrail> | undefined,
|
|
93
|
-
ctx: TrailContext
|
|
150
|
+
ctx: TrailContext,
|
|
151
|
+
services: ServiceOverrideMap | undefined,
|
|
152
|
+
follow?: FollowFn
|
|
94
153
|
): Result<unknown, Error> | Promise<Result<unknown, Error>> | undefined => {
|
|
95
154
|
const trailDef = trailsMap?.get(id);
|
|
96
155
|
if (trailDef === undefined) {
|
|
97
156
|
return undefined;
|
|
98
157
|
}
|
|
99
158
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
159
|
+
const nestedCtx = follow ? { ...ctx, follow } : ctx;
|
|
160
|
+
return executeTrail(trailDef, input, {
|
|
161
|
+
ctx: nestedCtx,
|
|
162
|
+
services,
|
|
163
|
+
});
|
|
105
164
|
};
|
|
106
165
|
|
|
107
166
|
// ---------------------------------------------------------------------------
|
|
@@ -116,7 +175,8 @@ const createRecordingFollow = (
|
|
|
116
175
|
scenario: FollowScenario,
|
|
117
176
|
trailsMap: ReadonlyMap<string, AnyTrail> | undefined,
|
|
118
177
|
baseFollow: FollowFn | undefined,
|
|
119
|
-
ctx: TrailContext
|
|
178
|
+
ctx: TrailContext,
|
|
179
|
+
services: ServiceOverrideMap | undefined
|
|
120
180
|
): FollowFn => {
|
|
121
181
|
// The generic O on FollowFn is erased at runtime; the cast is safe
|
|
122
182
|
// because callers narrow via isOk/isErr before accessing the value.
|
|
@@ -132,7 +192,14 @@ const createRecordingFollow = (
|
|
|
132
192
|
return baseFollow(id, input);
|
|
133
193
|
}
|
|
134
194
|
|
|
135
|
-
const executed = executeFromMap(
|
|
195
|
+
const executed = executeFromMap(
|
|
196
|
+
id,
|
|
197
|
+
input,
|
|
198
|
+
trailsMap,
|
|
199
|
+
ctx,
|
|
200
|
+
services,
|
|
201
|
+
follow as FollowFn
|
|
202
|
+
);
|
|
136
203
|
if (executed !== undefined) {
|
|
137
204
|
return Promise.resolve(executed);
|
|
138
205
|
}
|
|
@@ -209,7 +276,8 @@ const handleValidationError = (
|
|
|
209
276
|
const buildTestContext = (
|
|
210
277
|
scenario: FollowScenario,
|
|
211
278
|
ctx: Partial<TrailContext> | undefined,
|
|
212
|
-
trailsMap: ReadonlyMap<string, AnyTrail> | undefined
|
|
279
|
+
trailsMap: ReadonlyMap<string, AnyTrail> | undefined,
|
|
280
|
+
services: ServiceOverrideMap | undefined
|
|
213
281
|
): { trace: FollowRecord[]; testCtx: TrailContext } => {
|
|
214
282
|
const trace: FollowRecord[] = [];
|
|
215
283
|
const baseCtx = mergeTestContext(ctx);
|
|
@@ -218,7 +286,8 @@ const buildTestContext = (
|
|
|
218
286
|
scenario,
|
|
219
287
|
trailsMap,
|
|
220
288
|
baseCtx.follow,
|
|
221
|
-
baseCtx
|
|
289
|
+
baseCtx,
|
|
290
|
+
services
|
|
222
291
|
);
|
|
223
292
|
return { testCtx: { ...baseCtx, follow }, trace };
|
|
224
293
|
};
|
|
@@ -227,15 +296,24 @@ const runScenario = async (
|
|
|
227
296
|
trailDef: AnyTrail,
|
|
228
297
|
scenario: FollowScenario,
|
|
229
298
|
ctx: Partial<TrailContext> | undefined,
|
|
230
|
-
trailsMap: ReadonlyMap<string, AnyTrail> | undefined
|
|
299
|
+
trailsMap: ReadonlyMap<string, AnyTrail> | undefined,
|
|
300
|
+
services: ServiceOverrideMap | undefined
|
|
231
301
|
): Promise<void> => {
|
|
232
302
|
const validated = validateInput(trailDef.input, scenario.input);
|
|
233
303
|
if (handleValidationError(validated, scenario)) {
|
|
234
304
|
return;
|
|
235
305
|
}
|
|
236
306
|
|
|
237
|
-
const { trace, testCtx } = buildTestContext(
|
|
238
|
-
|
|
307
|
+
const { trace, testCtx } = buildTestContext(
|
|
308
|
+
scenario,
|
|
309
|
+
ctx,
|
|
310
|
+
trailsMap,
|
|
311
|
+
services
|
|
312
|
+
);
|
|
313
|
+
const result = await executeTrail(trailDef, scenario.input, {
|
|
314
|
+
ctx: testCtx,
|
|
315
|
+
services,
|
|
316
|
+
});
|
|
239
317
|
assertFollowTrace(trace, scenario);
|
|
240
318
|
assertScenarioResult(result, scenario, trailDef);
|
|
241
319
|
};
|
|
@@ -248,6 +326,12 @@ const runScenario = async (
|
|
|
248
326
|
export interface TestFollowOptions {
|
|
249
327
|
/** Partial context overrides. */
|
|
250
328
|
readonly ctx?: Partial<TrailContext> | undefined;
|
|
329
|
+
/**
|
|
330
|
+
* Explicit service overrides merged on top of auto-resolved mocks for every
|
|
331
|
+
* scenario. Values are passed by reference — provide immutable objects, or
|
|
332
|
+
* use `mock()` on the service definition to get a fresh instance per run.
|
|
333
|
+
*/
|
|
334
|
+
readonly services?: ServiceOverrideMap | undefined;
|
|
251
335
|
/** Map of trail ID to trail definition, used for injectFromExample. */
|
|
252
336
|
readonly trails?: ReadonlyMap<string, AnyTrail> | undefined;
|
|
253
337
|
}
|
|
@@ -276,7 +360,18 @@ export const testFollows = (
|
|
|
276
360
|
test.each([...scenarios])(
|
|
277
361
|
'$description',
|
|
278
362
|
async (scenario: FollowScenario) => {
|
|
279
|
-
|
|
363
|
+
const services = mergeServiceOverrides(
|
|
364
|
+
await resolveMockServices(trailDef, options?.trails),
|
|
365
|
+
options?.ctx,
|
|
366
|
+
options?.services
|
|
367
|
+
);
|
|
368
|
+
await runScenario(
|
|
369
|
+
trailDef,
|
|
370
|
+
scenario,
|
|
371
|
+
options?.ctx,
|
|
372
|
+
options?.trails,
|
|
373
|
+
services
|
|
374
|
+
);
|
|
280
375
|
}
|
|
281
376
|
);
|
|
282
377
|
});
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,11 @@ export {
|
|
|
16
16
|
} from './assertions.js';
|
|
17
17
|
|
|
18
18
|
// Mock factories
|
|
19
|
-
export {
|
|
19
|
+
export {
|
|
20
|
+
createFollowContext,
|
|
21
|
+
createTestContext,
|
|
22
|
+
defaultMintPermit,
|
|
23
|
+
} from './context.js';
|
|
20
24
|
export { createTestLogger } from './logger.js';
|
|
21
25
|
|
|
22
26
|
// Surface harnesses
|
|
@@ -25,6 +29,11 @@ export { createMcpHarness } from './harness-mcp.js';
|
|
|
25
29
|
|
|
26
30
|
// Types
|
|
27
31
|
export type { CreateFollowContextOptions } from './context.js';
|
|
32
|
+
export type {
|
|
33
|
+
MintableTrail,
|
|
34
|
+
MintedPermit,
|
|
35
|
+
TestExecutionOptions,
|
|
36
|
+
} from './context.js';
|
|
28
37
|
export type { TestFollowOptions } from './follows.js';
|
|
29
38
|
|
|
30
39
|
export type {
|