@ontrails/testing 1.0.0-beta.1 → 1.0.0-beta.11
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 +166 -0
- package/README.md +25 -7
- 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 +28 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +70 -11
- package/dist/context.js.map +1 -1
- package/dist/contracts.d.ts +3 -2
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +25 -10
- package/dist/contracts.js.map +1 -1
- package/dist/examples.d.ts +4 -3
- package/dist/examples.d.ts.map +1 -1
- package/dist/examples.js +63 -68
- package/dist/examples.js.map +1 -1
- package/dist/follows.d.ts +38 -0
- package/dist/follows.d.ts.map +1 -0
- package/dist/{hike.js → follows.js} +71 -28
- package/dist/follows.js.map +1 -0
- package/dist/harness-mcp.d.ts.map +1 -1
- package/dist/harness-mcp.js +5 -2
- package/dist/harness-mcp.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/trail.js +1 -1
- package/dist/trail.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/all.test.ts +64 -0
- package/src/__tests__/context.test.ts +92 -2
- package/src/__tests__/contracts.test.ts +122 -5
- package/src/__tests__/detours.test.ts +3 -3
- package/src/__tests__/examples.test.ts +285 -22
- package/src/__tests__/follows.test.ts +589 -0
- package/src/__tests__/trail.test.ts +4 -4
- package/src/all.ts +5 -1
- package/src/context.ts +121 -13
- package/src/contracts.ts +43 -12
- package/src/examples.ts +111 -105
- package/src/{hike.ts → follows.ts} +138 -43
- package/src/harness-mcp.ts +5 -2
- package/src/index.ts +6 -4
- package/src/trail.ts +1 -1
- package/src/types.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/hike.d.ts +0 -32
- package/dist/hike.d.ts.map +0 -1
- package/dist/hike.js.map +0 -1
- package/src/__tests__/hike.test.ts +0 -164
package/src/context.ts
CHANGED
|
@@ -2,11 +2,21 @@
|
|
|
2
2
|
* Test context factory for creating TrailContext instances suitable for testing.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
FollowFn,
|
|
7
|
+
ServiceOverrideMap,
|
|
8
|
+
Topo,
|
|
9
|
+
TrailContext,
|
|
10
|
+
} from '@ontrails/core';
|
|
11
|
+
import { Result, createServiceLookup } from '@ontrails/core';
|
|
6
12
|
|
|
7
13
|
import { createTestLogger } from './logger.js';
|
|
8
14
|
import type { TestTrailContextOptions } from './types.js';
|
|
9
15
|
|
|
16
|
+
type MutableTrailContext = {
|
|
17
|
+
-readonly [K in keyof TrailContext]: TrailContext[K];
|
|
18
|
+
};
|
|
19
|
+
|
|
10
20
|
// ---------------------------------------------------------------------------
|
|
11
21
|
// createTestContext
|
|
12
22
|
// ---------------------------------------------------------------------------
|
|
@@ -20,23 +30,121 @@ import type { TestTrailContextOptions } from './types.js';
|
|
|
20
30
|
*/
|
|
21
31
|
export const createTestContext = (
|
|
22
32
|
overrides?: TestTrailContextOptions
|
|
23
|
-
): TrailContext =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
): TrailContext => {
|
|
34
|
+
const cwd = overrides?.cwd ?? process.cwd();
|
|
35
|
+
const ctx = {
|
|
36
|
+
cwd,
|
|
37
|
+
env: overrides?.env ?? { TRAILS_ENV: 'test' },
|
|
38
|
+
extensions: undefined,
|
|
39
|
+
logger: overrides?.logger ?? createTestLogger(),
|
|
40
|
+
requestId: overrides?.requestId ?? 'test-request-001',
|
|
41
|
+
signal: overrides?.signal ?? new AbortController().signal,
|
|
42
|
+
workspaceRoot: cwd,
|
|
43
|
+
} as MutableTrailContext;
|
|
44
|
+
ctx.service = createServiceLookup(() => ctx);
|
|
45
|
+
return ctx;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// createFollowContext
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
export interface CreateFollowContextOptions {
|
|
53
|
+
readonly responses?: Record<string, Result<unknown, Error>> | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TestExecutionOptions {
|
|
57
|
+
readonly ctx?: Partial<TrailContext> | undefined;
|
|
58
|
+
readonly services?: ServiceOverrideMap | undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a mock `FollowFn` for testing composite trails.
|
|
63
|
+
*
|
|
64
|
+
* Returns preconfigured `Result` values keyed by trail ID. Calls to
|
|
65
|
+
* unregistered IDs return `Result.err` with a descriptive message.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* const follow = createFollowContext({
|
|
70
|
+
* responses: { 'entity.add': Result.ok({ id: '1', name: 'Alpha' }) },
|
|
71
|
+
* });
|
|
72
|
+
* const ctx = { ...createTestContext(), follow };
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export const createFollowContext = (
|
|
76
|
+
options?: CreateFollowContextOptions
|
|
77
|
+
): FollowFn => {
|
|
78
|
+
const responses = options?.responses ?? {};
|
|
79
|
+
return <O>(id: string, _input: unknown): Promise<Result<O, Error>> => {
|
|
80
|
+
const response = responses[id];
|
|
81
|
+
if (response === undefined) {
|
|
82
|
+
return Promise.resolve(
|
|
83
|
+
Result.err(new Error(`No mock response for follow("${id}")`)) as Result<
|
|
84
|
+
O,
|
|
85
|
+
Error
|
|
86
|
+
>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return Promise.resolve(response as Result<O, Error>);
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const isTestExecutionOptions = (
|
|
94
|
+
input: Partial<TrailContext> | TestExecutionOptions | undefined
|
|
95
|
+
): input is TestExecutionOptions =>
|
|
96
|
+
input !== undefined &&
|
|
97
|
+
(Object.hasOwn(input, 'ctx') || Object.hasOwn(input, 'services'));
|
|
98
|
+
|
|
99
|
+
export const normalizeTestExecutionOptions = (
|
|
100
|
+
input?: Partial<TrailContext> | TestExecutionOptions
|
|
101
|
+
): TestExecutionOptions =>
|
|
102
|
+
isTestExecutionOptions(input) ? input : { ctx: input };
|
|
103
|
+
|
|
104
|
+
export const mergeServiceOverrides = (
|
|
105
|
+
autoResolved: ServiceOverrideMap,
|
|
106
|
+
ctx: Partial<TrailContext> | undefined,
|
|
107
|
+
explicit: ServiceOverrideMap | undefined
|
|
108
|
+
): ServiceOverrideMap => ({
|
|
109
|
+
...autoResolved,
|
|
110
|
+
...ctx?.extensions,
|
|
111
|
+
...explicit,
|
|
29
112
|
});
|
|
30
113
|
|
|
114
|
+
const buildMockServices = async (app: Topo): Promise<ServiceOverrideMap> => {
|
|
115
|
+
const services: Record<string, unknown> = {};
|
|
116
|
+
for (const declaredService of app.listServices()) {
|
|
117
|
+
if (!declaredService.mock) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
services[declaredService.id] = await declaredService.mock();
|
|
121
|
+
}
|
|
122
|
+
return services;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export const resolveMockServices = async (
|
|
126
|
+
app: Topo
|
|
127
|
+
): Promise<ServiceOverrideMap> => await buildMockServices(app);
|
|
128
|
+
|
|
31
129
|
/**
|
|
32
130
|
* Merge a Partial<TrailContext> into a test context.
|
|
33
131
|
* Used internally when the public API accepts Partial<TrailContext>.
|
|
34
132
|
*/
|
|
35
|
-
export const mergeTestContext = (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
133
|
+
export const mergeTestContext = (
|
|
134
|
+
ctx?: Partial<TrailContext>,
|
|
135
|
+
services?: ServiceOverrideMap
|
|
136
|
+
): TrailContext => {
|
|
40
137
|
const base = createTestContext();
|
|
41
|
-
|
|
138
|
+
const extensions = {
|
|
139
|
+
...base.extensions,
|
|
140
|
+
...ctx?.extensions,
|
|
141
|
+
...services,
|
|
142
|
+
};
|
|
143
|
+
const merged = {
|
|
144
|
+
...base,
|
|
145
|
+
...ctx,
|
|
146
|
+
extensions: Object.keys(extensions).length === 0 ? undefined : extensions,
|
|
147
|
+
} as MutableTrailContext;
|
|
148
|
+
merged.service = createServiceLookup(() => merged);
|
|
149
|
+
return merged;
|
|
42
150
|
};
|
package/src/contracts.ts
CHANGED
|
@@ -9,16 +9,34 @@
|
|
|
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
|
|
20
26
|
// ---------------------------------------------------------------------------
|
|
21
27
|
|
|
28
|
+
/** Check if a trail requires follow() but the context doesn't provide it. */
|
|
29
|
+
const needsFollowContext = (
|
|
30
|
+
t: unknown,
|
|
31
|
+
resolveCtx: () => Partial<TrailContext> | TestExecutionOptions | undefined
|
|
32
|
+
): boolean => {
|
|
33
|
+
const spec = t as { follow?: readonly string[] };
|
|
34
|
+
if (!spec.follow || spec.follow.length === 0) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return !normalizeTestExecutionOptions(resolveCtx()).ctx?.follow;
|
|
38
|
+
};
|
|
39
|
+
|
|
22
40
|
const validateOutputSchema = (
|
|
23
41
|
outputSchema: z.ZodType,
|
|
24
42
|
value: unknown,
|
|
@@ -39,29 +57,33 @@ const validateOutputSchema = (
|
|
|
39
57
|
// ---------------------------------------------------------------------------
|
|
40
58
|
|
|
41
59
|
/**
|
|
42
|
-
* Verify that every trail
|
|
60
|
+
* Verify that every trail implementation output matches its declared
|
|
43
61
|
* output schema. Catches implementation-schema drift.
|
|
44
62
|
*
|
|
45
63
|
* Trails without output schemas or examples are skipped.
|
|
46
64
|
*/
|
|
47
65
|
export const testContracts = (
|
|
48
66
|
app: Topo,
|
|
49
|
-
ctxOrFactory?:
|
|
67
|
+
ctxOrFactory?:
|
|
68
|
+
| Partial<TrailContext>
|
|
69
|
+
| TestExecutionOptions
|
|
70
|
+
| (() => Partial<TrailContext> | TestExecutionOptions)
|
|
50
71
|
): void => {
|
|
51
|
-
const
|
|
72
|
+
const resolveInput =
|
|
52
73
|
typeof ctxOrFactory === 'function' ? ctxOrFactory : () => ctxOrFactory;
|
|
53
|
-
const
|
|
74
|
+
const allEntries = app.list() as Trail<unknown, unknown>[];
|
|
54
75
|
|
|
55
76
|
describe('contracts', () => {
|
|
56
|
-
describe.each(
|
|
57
|
-
const t = trailDef as Trail<unknown, unknown>;
|
|
58
|
-
|
|
77
|
+
describe.each(allEntries)('$id', (t) => {
|
|
59
78
|
if (t.output === undefined) {
|
|
60
79
|
return;
|
|
61
80
|
}
|
|
62
81
|
if (t.examples === undefined || t.examples.length === 0) {
|
|
63
82
|
return;
|
|
64
83
|
}
|
|
84
|
+
if (needsFollowContext(t, resolveInput)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
65
87
|
|
|
66
88
|
const { examples, output: outputSchema } = t;
|
|
67
89
|
const successExamples = examples.filter((e) => e.error === undefined);
|
|
@@ -69,12 +91,21 @@ export const testContracts = (
|
|
|
69
91
|
test.each(successExamples)(
|
|
70
92
|
'contract: $name',
|
|
71
93
|
async (example: TrailExample<unknown, unknown>) => {
|
|
72
|
-
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);
|
|
73
101
|
|
|
74
102
|
const validated = validateInput(t.input, example.input);
|
|
75
|
-
|
|
103
|
+
expectOk(validated);
|
|
76
104
|
|
|
77
|
-
const result = await t.
|
|
105
|
+
const result = await executeTrail(t, example.input, {
|
|
106
|
+
ctx: testCtx,
|
|
107
|
+
services,
|
|
108
|
+
});
|
|
78
109
|
const resultValue = expectOk(result);
|
|
79
110
|
|
|
80
111
|
validateOutputSchema(outputSchema, resultValue, t.id, example.name);
|
package/src/examples.ts
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
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
|
|
6
|
+
* determines which check to run per example. For trails with `follow`
|
|
7
7
|
* declarations, checks that every declared follow 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
|
-
AnyHike,
|
|
14
13
|
FollowFn,
|
|
14
|
+
ServiceOverrideMap,
|
|
15
15
|
Topo,
|
|
16
16
|
TrailExample,
|
|
17
17
|
Trail,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
NotFoundError,
|
|
31
31
|
PermissionError,
|
|
32
32
|
RateLimitError,
|
|
33
|
+
executeTrail,
|
|
33
34
|
Result,
|
|
34
35
|
TimeoutError,
|
|
35
36
|
TrailsError,
|
|
@@ -42,9 +43,14 @@ import {
|
|
|
42
43
|
assertErrorMatch,
|
|
43
44
|
assertFullMatch,
|
|
44
45
|
assertSchemaMatch,
|
|
45
|
-
expectOk,
|
|
46
46
|
} from './assertions.js';
|
|
47
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
mergeServiceOverrides,
|
|
49
|
+
mergeTestContext,
|
|
50
|
+
normalizeTestExecutionOptions,
|
|
51
|
+
resolveMockServices,
|
|
52
|
+
} from './context.js';
|
|
53
|
+
import type { TestExecutionOptions } from './context.js';
|
|
48
54
|
|
|
49
55
|
// ---------------------------------------------------------------------------
|
|
50
56
|
// Error class name -> constructor map
|
|
@@ -129,21 +135,24 @@ const runExample = async (
|
|
|
129
135
|
t: Trail<unknown, unknown>,
|
|
130
136
|
example: TrailExample<unknown, unknown>,
|
|
131
137
|
output: z.ZodType | undefined,
|
|
132
|
-
testCtx: TrailContext
|
|
138
|
+
testCtx: TrailContext,
|
|
139
|
+
services?: ServiceOverrideMap
|
|
133
140
|
): Promise<void> => {
|
|
134
141
|
const validated = validateInput(t.input, example.input);
|
|
135
142
|
|
|
136
143
|
if (handleValidationError(validated, example)) {
|
|
137
144
|
return;
|
|
138
145
|
}
|
|
139
|
-
const validatedInput = expectOk(validated);
|
|
140
146
|
|
|
141
|
-
const result = await t.
|
|
147
|
+
const result = await executeTrail(t, example.input, {
|
|
148
|
+
ctx: testCtx,
|
|
149
|
+
services,
|
|
150
|
+
});
|
|
142
151
|
assertProgressiveMatch(result, example, output);
|
|
143
152
|
};
|
|
144
153
|
|
|
145
154
|
// ---------------------------------------------------------------------------
|
|
146
|
-
//
|
|
155
|
+
// Follow coverage for composition trails
|
|
147
156
|
// ---------------------------------------------------------------------------
|
|
148
157
|
|
|
149
158
|
/**
|
|
@@ -157,7 +166,8 @@ const createCoverageFollow = (
|
|
|
157
166
|
called: Set<string>,
|
|
158
167
|
baseFollow: FollowFn | undefined,
|
|
159
168
|
topo: Topo,
|
|
160
|
-
ctx: TrailContext
|
|
169
|
+
ctx: TrailContext,
|
|
170
|
+
services?: ServiceOverrideMap
|
|
161
171
|
): FollowFn => {
|
|
162
172
|
const follow = (id: string, input: unknown) => {
|
|
163
173
|
called.add(id);
|
|
@@ -168,11 +178,10 @@ const createCoverageFollow = (
|
|
|
168
178
|
|
|
169
179
|
const trailDef = topo.get(id);
|
|
170
180
|
if (trailDef !== undefined) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
return Promise.resolve(trailDef.implementation(validated.value, ctx));
|
|
181
|
+
return executeTrail(trailDef, input, {
|
|
182
|
+
ctx: { ...ctx, follow },
|
|
183
|
+
services,
|
|
184
|
+
});
|
|
176
185
|
}
|
|
177
186
|
|
|
178
187
|
return Promise.resolve(Result.ok());
|
|
@@ -181,93 +190,37 @@ const createCoverageFollow = (
|
|
|
181
190
|
};
|
|
182
191
|
|
|
183
192
|
/**
|
|
184
|
-
* Run a single example against a
|
|
193
|
+
* Run a single example against a composition trail, recording follow calls.
|
|
185
194
|
*/
|
|
186
|
-
const
|
|
187
|
-
|
|
195
|
+
const runCompositionExample = async (
|
|
196
|
+
trailDef: Trail<unknown, unknown>,
|
|
188
197
|
example: TrailExample<unknown, unknown>,
|
|
189
198
|
output: z.ZodType | undefined,
|
|
190
199
|
baseCtx: TrailContext,
|
|
191
200
|
called: Set<string>,
|
|
192
|
-
topo: Topo
|
|
201
|
+
topo: Topo,
|
|
202
|
+
services?: ServiceOverrideMap
|
|
193
203
|
): Promise<void> => {
|
|
194
|
-
const validated = validateInput(
|
|
204
|
+
const validated = validateInput(trailDef.input, example.input);
|
|
195
205
|
|
|
196
206
|
if (handleValidationError(validated, example)) {
|
|
197
207
|
return;
|
|
198
208
|
}
|
|
199
|
-
const validatedInput = expectOk(validated);
|
|
200
209
|
|
|
201
|
-
const follow = createCoverageFollow(
|
|
210
|
+
const follow = createCoverageFollow(
|
|
211
|
+
called,
|
|
212
|
+
baseCtx.follow,
|
|
213
|
+
topo,
|
|
214
|
+
baseCtx,
|
|
215
|
+
services
|
|
216
|
+
);
|
|
202
217
|
const testCtx: TrailContext = { ...baseCtx, follow };
|
|
203
218
|
|
|
204
|
-
const result = await
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
|
-
// Hike entry with examples pre-validated
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
|
|
212
|
-
interface HikeWithExamples {
|
|
213
|
-
readonly hikeDef: AnyHike;
|
|
214
|
-
readonly hikeId: string;
|
|
215
|
-
readonly examples: readonly TrailExample<unknown, unknown>[];
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const collectHikesWithExamples = (app: Topo): readonly HikeWithExamples[] =>
|
|
219
|
-
[...app.hikes]
|
|
220
|
-
.filter(([, h]) => h.examples !== undefined && h.examples.length > 0)
|
|
221
|
-
.map(([hikeId, hikeDef]) => ({
|
|
222
|
-
examples: hikeDef.examples as readonly TrailExample<unknown, unknown>[],
|
|
223
|
-
hikeDef,
|
|
224
|
-
hikeId,
|
|
225
|
-
}));
|
|
226
|
-
|
|
227
|
-
// ---------------------------------------------------------------------------
|
|
228
|
-
// Hike example describe blocks
|
|
229
|
-
// ---------------------------------------------------------------------------
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Generate describe/test blocks for hikes with follows coverage.
|
|
233
|
-
*
|
|
234
|
-
* Always uses a recording follow so that follows coverage can be checked.
|
|
235
|
-
* Hikes without `follows` still run their examples but skip the coverage test.
|
|
236
|
-
*/
|
|
237
|
-
const describeHikeExamples = (
|
|
238
|
-
hikesWithExamples: readonly HikeWithExamples[],
|
|
239
|
-
resolveCtx: () => Partial<TrailContext> | undefined,
|
|
240
|
-
topo: Topo
|
|
241
|
-
): void => {
|
|
242
|
-
if (hikesWithExamples.length === 0) {
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
describe.each([...hikesWithExamples])('$hikeId', ({ hikeDef, examples }) => {
|
|
247
|
-
const called = new Set<string>();
|
|
248
|
-
|
|
249
|
-
test.each([...examples])(
|
|
250
|
-
'example: $name',
|
|
251
|
-
async (example: TrailExample<unknown, unknown>) => {
|
|
252
|
-
const baseCtx = mergeTestContext(resolveCtx());
|
|
253
|
-
await runHikeExample(
|
|
254
|
-
hikeDef,
|
|
255
|
-
example,
|
|
256
|
-
hikeDef.output,
|
|
257
|
-
baseCtx,
|
|
258
|
-
called,
|
|
259
|
-
topo
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
if (hikeDef.follows.length > 0) {
|
|
265
|
-
test('follows coverage', () => {
|
|
266
|
-
const uncovered = hikeDef.follows.filter((id) => !called.has(id));
|
|
267
|
-
expect(uncovered).toEqual([]);
|
|
268
|
-
});
|
|
269
|
-
}
|
|
219
|
+
const result = await executeTrail(trailDef, example.input, {
|
|
220
|
+
ctx: testCtx,
|
|
221
|
+
services,
|
|
270
222
|
});
|
|
223
|
+
assertProgressiveMatch(result, example, output);
|
|
271
224
|
};
|
|
272
225
|
|
|
273
226
|
// ---------------------------------------------------------------------------
|
|
@@ -277,7 +230,7 @@ const describeHikeExamples = (
|
|
|
277
230
|
/**
|
|
278
231
|
* Generate describe/test blocks for every trail example in the app.
|
|
279
232
|
*
|
|
280
|
-
* For
|
|
233
|
+
* For trails with `follow` declarations and examples, also verifies that
|
|
281
234
|
* every declared follow ID was called at least once across all examples.
|
|
282
235
|
*
|
|
283
236
|
* One line in your test file:
|
|
@@ -287,28 +240,81 @@ const describeHikeExamples = (
|
|
|
287
240
|
*/
|
|
288
241
|
export const testExamples = (
|
|
289
242
|
app: Topo,
|
|
290
|
-
ctxOrFactory?:
|
|
243
|
+
ctxOrFactory?:
|
|
244
|
+
| Partial<TrailContext>
|
|
245
|
+
| TestExecutionOptions
|
|
246
|
+
| (() => Partial<TrailContext> | TestExecutionOptions)
|
|
291
247
|
): void => {
|
|
292
|
-
const
|
|
248
|
+
const resolveInput =
|
|
293
249
|
typeof ctxOrFactory === 'function' ? ctxOrFactory : () => ctxOrFactory;
|
|
294
|
-
const
|
|
250
|
+
const allTrails = app.list() as Trail<unknown, unknown>[];
|
|
295
251
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
252
|
+
const withExamples = allTrails.filter(
|
|
253
|
+
(t) => t.examples !== undefined && t.examples.length > 0
|
|
254
|
+
);
|
|
255
|
+
const simpleTrails = withExamples.filter((t) => t.follow.length === 0);
|
|
256
|
+
const compositionTrails = withExamples.filter((t) => t.follow.length > 0);
|
|
257
|
+
|
|
258
|
+
// Simple trails: run examples directly
|
|
259
|
+
if (simpleTrails.length > 0) {
|
|
260
|
+
describe.each(simpleTrails)('$id', (t) => {
|
|
261
|
+
const { examples, output } = t;
|
|
262
|
+
if (!examples) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
301
265
|
|
|
302
|
-
|
|
266
|
+
test.each([...examples])(
|
|
267
|
+
'example: $name',
|
|
268
|
+
async (example: TrailExample<unknown, unknown>) => {
|
|
269
|
+
const resolved = normalizeTestExecutionOptions(resolveInput());
|
|
270
|
+
const services = mergeServiceOverrides(
|
|
271
|
+
await resolveMockServices(app),
|
|
272
|
+
resolved.ctx,
|
|
273
|
+
resolved.services
|
|
274
|
+
);
|
|
275
|
+
const testCtx = mergeTestContext(resolved.ctx);
|
|
276
|
+
await runExample(t, example, output, testCtx, services);
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
}
|
|
303
281
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
282
|
+
// Composition trails: use recording follow and check coverage
|
|
283
|
+
if (compositionTrails.length > 0) {
|
|
284
|
+
describe.each(compositionTrails)('$id', (t) => {
|
|
285
|
+
const { examples, output } = t;
|
|
286
|
+
if (!examples) {
|
|
287
|
+
return;
|
|
309
288
|
}
|
|
310
|
-
);
|
|
311
|
-
});
|
|
312
289
|
|
|
313
|
-
|
|
290
|
+
const called = new Set<string>();
|
|
291
|
+
|
|
292
|
+
test.each([...examples])(
|
|
293
|
+
'example: $name',
|
|
294
|
+
async (example: TrailExample<unknown, unknown>) => {
|
|
295
|
+
const resolved = normalizeTestExecutionOptions(resolveInput());
|
|
296
|
+
const services = mergeServiceOverrides(
|
|
297
|
+
await resolveMockServices(app),
|
|
298
|
+
resolved.ctx,
|
|
299
|
+
resolved.services
|
|
300
|
+
);
|
|
301
|
+
const baseCtx = mergeTestContext(resolved.ctx);
|
|
302
|
+
await runCompositionExample(
|
|
303
|
+
t,
|
|
304
|
+
example,
|
|
305
|
+
output,
|
|
306
|
+
baseCtx,
|
|
307
|
+
called,
|
|
308
|
+
app,
|
|
309
|
+
services
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
test('follow coverage', () => {
|
|
315
|
+
const uncovered = t.follow.filter((id) => !called.has(id));
|
|
316
|
+
expect(uncovered).toEqual([]);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
}
|
|
314
320
|
};
|