@ontrails/testing 1.0.0-beta.1 → 1.0.0-beta.10
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 +138 -0
- package/README.md +25 -7
- package/dist/context.d.ts +20 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +25 -0
- package/dist/context.js.map +1 -1
- package/dist/contracts.d.ts +1 -1
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +15 -5
- package/dist/contracts.js.map +1 -1
- package/dist/examples.d.ts +2 -2
- package/dist/examples.d.ts.map +1 -1
- package/dist/examples.js +43 -55
- package/dist/examples.js.map +1 -1
- package/dist/follows.d.ts +32 -0
- package/dist/follows.d.ts.map +1 -0
- package/dist/{hike.js → follows.js} +15 -15
- 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 +5 -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__/context.test.ts +36 -1
- package/src/__tests__/contracts.test.ts +30 -4
- package/src/__tests__/detours.test.ts +3 -3
- package/src/__tests__/examples.test.ts +21 -22
- package/src/__tests__/{hike.test.ts → follows.test.ts} +27 -28
- package/src/__tests__/trail.test.ts +4 -4
- package/src/context.ts +42 -1
- package/src/contracts.ts +19 -6
- package/src/examples.ts +55 -91
- package/src/{hike.ts → follows.ts} +30 -30
- package/src/harness-mcp.ts +5 -2
- package/src/index.ts +5 -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
|
@@ -10,20 +10,20 @@ import { testTrail } from '../trail.js';
|
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
|
|
12
12
|
const greetTrail = trail('greet', {
|
|
13
|
-
implementation: (input: { name: string }) =>
|
|
14
|
-
Result.ok({ greeting: `Hello, ${input.name}` }),
|
|
15
13
|
input: z.object({ name: z.string() }),
|
|
16
14
|
output: z.object({ greeting: z.string() }),
|
|
15
|
+
run: (input: { name: string }) =>
|
|
16
|
+
Result.ok({ greeting: `Hello, ${input.name}` }),
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
const failTrail = trail('fail', {
|
|
20
|
-
|
|
20
|
+
input: z.object({ id: z.string() }),
|
|
21
|
+
run: (input: { id: string }) => {
|
|
21
22
|
if (input.id === 'missing') {
|
|
22
23
|
return Result.err(new NotFoundError('Not found: missing'));
|
|
23
24
|
}
|
|
24
25
|
return Result.ok({ id: input.id });
|
|
25
26
|
},
|
|
26
|
-
input: z.object({ id: z.string() }),
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
// ---------------------------------------------------------------------------
|
package/src/context.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Test context factory for creating TrailContext instances suitable for testing.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { TrailContext } from '@ontrails/core';
|
|
5
|
+
import type { FollowFn, TrailContext } from '@ontrails/core';
|
|
6
|
+
import { Result } from '@ontrails/core';
|
|
6
7
|
|
|
7
8
|
import { createTestLogger } from './logger.js';
|
|
8
9
|
import type { TestTrailContextOptions } from './types.js';
|
|
@@ -28,6 +29,46 @@ export const createTestContext = (
|
|
|
28
29
|
workspaceRoot: overrides?.cwd ?? process.cwd(),
|
|
29
30
|
});
|
|
30
31
|
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// createFollowContext
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
export interface CreateFollowContextOptions {
|
|
37
|
+
readonly responses?: Record<string, Result<unknown, Error>> | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a mock `FollowFn` for testing composite trails.
|
|
42
|
+
*
|
|
43
|
+
* Returns preconfigured `Result` values keyed by trail ID. Calls to
|
|
44
|
+
* unregistered IDs return `Result.err` with a descriptive message.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* const follow = createFollowContext({
|
|
49
|
+
* responses: { 'entity.add': Result.ok({ id: '1', name: 'Alpha' }) },
|
|
50
|
+
* });
|
|
51
|
+
* const ctx = { ...createTestContext(), follow };
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export const createFollowContext = (
|
|
55
|
+
options?: CreateFollowContextOptions
|
|
56
|
+
): FollowFn => {
|
|
57
|
+
const responses = options?.responses ?? {};
|
|
58
|
+
return <O>(id: string, _input: unknown): Promise<Result<O, Error>> => {
|
|
59
|
+
const response = responses[id];
|
|
60
|
+
if (response === undefined) {
|
|
61
|
+
return Promise.resolve(
|
|
62
|
+
Result.err(new Error(`No mock response for follow("${id}")`)) as Result<
|
|
63
|
+
O,
|
|
64
|
+
Error
|
|
65
|
+
>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return Promise.resolve(response as Result<O, Error>);
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
31
72
|
/**
|
|
32
73
|
* Merge a Partial<TrailContext> into a test context.
|
|
33
74
|
* Used internally when the public API accepts Partial<TrailContext>.
|
package/src/contracts.ts
CHANGED
|
@@ -19,6 +19,18 @@ import { mergeTestContext } from './context.js';
|
|
|
19
19
|
// Helpers
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
|
|
22
|
+
/** Check if a trail requires follow() but the context doesn't provide it. */
|
|
23
|
+
const needsFollowContext = (
|
|
24
|
+
t: unknown,
|
|
25
|
+
resolveCtx: () => Partial<TrailContext> | undefined
|
|
26
|
+
): boolean => {
|
|
27
|
+
const spec = t as { follow?: readonly string[] };
|
|
28
|
+
if (!spec.follow || spec.follow.length === 0) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return !resolveCtx()?.follow;
|
|
32
|
+
};
|
|
33
|
+
|
|
22
34
|
const validateOutputSchema = (
|
|
23
35
|
outputSchema: z.ZodType,
|
|
24
36
|
value: unknown,
|
|
@@ -39,7 +51,7 @@ const validateOutputSchema = (
|
|
|
39
51
|
// ---------------------------------------------------------------------------
|
|
40
52
|
|
|
41
53
|
/**
|
|
42
|
-
* Verify that every trail
|
|
54
|
+
* Verify that every trail implementation output matches its declared
|
|
43
55
|
* output schema. Catches implementation-schema drift.
|
|
44
56
|
*
|
|
45
57
|
* Trails without output schemas or examples are skipped.
|
|
@@ -50,18 +62,19 @@ export const testContracts = (
|
|
|
50
62
|
): void => {
|
|
51
63
|
const resolveCtx =
|
|
52
64
|
typeof ctxOrFactory === 'function' ? ctxOrFactory : () => ctxOrFactory;
|
|
53
|
-
const
|
|
65
|
+
const allEntries = app.list() as Trail<unknown, unknown>[];
|
|
54
66
|
|
|
55
67
|
describe('contracts', () => {
|
|
56
|
-
describe.each(
|
|
57
|
-
const t = trailDef as Trail<unknown, unknown>;
|
|
58
|
-
|
|
68
|
+
describe.each(allEntries)('$id', (t) => {
|
|
59
69
|
if (t.output === undefined) {
|
|
60
70
|
return;
|
|
61
71
|
}
|
|
62
72
|
if (t.examples === undefined || t.examples.length === 0) {
|
|
63
73
|
return;
|
|
64
74
|
}
|
|
75
|
+
if (needsFollowContext(t, resolveCtx)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
65
78
|
|
|
66
79
|
const { examples, output: outputSchema } = t;
|
|
67
80
|
const successExamples = examples.filter((e) => e.error === undefined);
|
|
@@ -74,7 +87,7 @@ export const testContracts = (
|
|
|
74
87
|
const validated = validateInput(t.input, example.input);
|
|
75
88
|
const validatedInput = expectOk(validated);
|
|
76
89
|
|
|
77
|
-
const result = await t.
|
|
90
|
+
const result = await t.run(validatedInput, testCtx);
|
|
78
91
|
const resultValue = expectOk(result);
|
|
79
92
|
|
|
80
93
|
validateOutputSchema(outputSchema, resultValue, t.id, example.name);
|
package/src/examples.ts
CHANGED
|
@@ -3,14 +3,13 @@
|
|
|
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,
|
|
15
14
|
Topo,
|
|
16
15
|
TrailExample,
|
|
@@ -138,12 +137,12 @@ const runExample = async (
|
|
|
138
137
|
}
|
|
139
138
|
const validatedInput = expectOk(validated);
|
|
140
139
|
|
|
141
|
-
const result = await t.
|
|
140
|
+
const result = await t.run(validatedInput, testCtx);
|
|
142
141
|
assertProgressiveMatch(result, example, output);
|
|
143
142
|
};
|
|
144
143
|
|
|
145
144
|
// ---------------------------------------------------------------------------
|
|
146
|
-
//
|
|
145
|
+
// Follow coverage for composition trails
|
|
147
146
|
// ---------------------------------------------------------------------------
|
|
148
147
|
|
|
149
148
|
/**
|
|
@@ -172,7 +171,7 @@ const createCoverageFollow = (
|
|
|
172
171
|
if (validated.isErr()) {
|
|
173
172
|
return Promise.resolve(validated);
|
|
174
173
|
}
|
|
175
|
-
return Promise.resolve(trailDef.
|
|
174
|
+
return Promise.resolve(trailDef.run(validated.value, ctx));
|
|
176
175
|
}
|
|
177
176
|
|
|
178
177
|
return Promise.resolve(Result.ok());
|
|
@@ -181,17 +180,17 @@ const createCoverageFollow = (
|
|
|
181
180
|
};
|
|
182
181
|
|
|
183
182
|
/**
|
|
184
|
-
* Run a single example against a
|
|
183
|
+
* Run a single example against a composition trail, recording follow calls.
|
|
185
184
|
*/
|
|
186
|
-
const
|
|
187
|
-
|
|
185
|
+
const runCompositionExample = async (
|
|
186
|
+
trailDef: Trail<unknown, unknown>,
|
|
188
187
|
example: TrailExample<unknown, unknown>,
|
|
189
188
|
output: z.ZodType | undefined,
|
|
190
189
|
baseCtx: TrailContext,
|
|
191
190
|
called: Set<string>,
|
|
192
191
|
topo: Topo
|
|
193
192
|
): Promise<void> => {
|
|
194
|
-
const validated = validateInput(
|
|
193
|
+
const validated = validateInput(trailDef.input, example.input);
|
|
195
194
|
|
|
196
195
|
if (handleValidationError(validated, example)) {
|
|
197
196
|
return;
|
|
@@ -201,75 +200,10 @@ const runHikeExample = async (
|
|
|
201
200
|
const follow = createCoverageFollow(called, baseCtx.follow, topo, baseCtx);
|
|
202
201
|
const testCtx: TrailContext = { ...baseCtx, follow };
|
|
203
202
|
|
|
204
|
-
const result = await
|
|
203
|
+
const result = await trailDef.run(validatedInput, testCtx);
|
|
205
204
|
assertProgressiveMatch(result, example, output);
|
|
206
205
|
};
|
|
207
206
|
|
|
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
|
-
}
|
|
270
|
-
});
|
|
271
|
-
};
|
|
272
|
-
|
|
273
207
|
// ---------------------------------------------------------------------------
|
|
274
208
|
// testExamples
|
|
275
209
|
// ---------------------------------------------------------------------------
|
|
@@ -277,7 +211,7 @@ const describeHikeExamples = (
|
|
|
277
211
|
/**
|
|
278
212
|
* Generate describe/test blocks for every trail example in the app.
|
|
279
213
|
*
|
|
280
|
-
* For
|
|
214
|
+
* For trails with `follow` declarations and examples, also verifies that
|
|
281
215
|
* every declared follow ID was called at least once across all examples.
|
|
282
216
|
*
|
|
283
217
|
* One line in your test file:
|
|
@@ -291,24 +225,54 @@ export const testExamples = (
|
|
|
291
225
|
): void => {
|
|
292
226
|
const resolveCtx =
|
|
293
227
|
typeof ctxOrFactory === 'function' ? ctxOrFactory : () => ctxOrFactory;
|
|
294
|
-
const
|
|
228
|
+
const allTrails = app.list() as Trail<unknown, unknown>[];
|
|
295
229
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
230
|
+
const withExamples = allTrails.filter(
|
|
231
|
+
(t) => t.examples !== undefined && t.examples.length > 0
|
|
232
|
+
);
|
|
233
|
+
const simpleTrails = withExamples.filter((t) => t.follow.length === 0);
|
|
234
|
+
const compositionTrails = withExamples.filter((t) => t.follow.length > 0);
|
|
235
|
+
|
|
236
|
+
// Simple trails: run examples directly
|
|
237
|
+
if (simpleTrails.length > 0) {
|
|
238
|
+
describe.each(simpleTrails)('$id', (t) => {
|
|
239
|
+
const { examples, output } = t;
|
|
240
|
+
if (!examples) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
301
243
|
|
|
302
|
-
|
|
244
|
+
test.each([...examples])(
|
|
245
|
+
'example: $name',
|
|
246
|
+
async (example: TrailExample<unknown, unknown>) => {
|
|
247
|
+
const testCtx = mergeTestContext(resolveCtx());
|
|
248
|
+
await runExample(t, example, output, testCtx);
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
303
253
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
254
|
+
// Composition trails: use recording follow and check coverage
|
|
255
|
+
if (compositionTrails.length > 0) {
|
|
256
|
+
describe.each(compositionTrails)('$id', (t) => {
|
|
257
|
+
const { examples, output } = t;
|
|
258
|
+
if (!examples) {
|
|
259
|
+
return;
|
|
309
260
|
}
|
|
310
|
-
);
|
|
311
|
-
});
|
|
312
261
|
|
|
313
|
-
|
|
262
|
+
const called = new Set<string>();
|
|
263
|
+
|
|
264
|
+
test.each([...examples])(
|
|
265
|
+
'example: $name',
|
|
266
|
+
async (example: TrailExample<unknown, unknown>) => {
|
|
267
|
+
const baseCtx = mergeTestContext(resolveCtx());
|
|
268
|
+
await runCompositionExample(t, example, output, baseCtx, called, app);
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
test('follow coverage', () => {
|
|
273
|
+
const uncovered = t.follow.filter((id) => !called.has(id));
|
|
274
|
+
expect(uncovered).toEqual([]);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
314
278
|
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* testFollows — composition-aware scenario testing for trails with follow.
|
|
3
3
|
*
|
|
4
|
-
* Tests the
|
|
4
|
+
* Tests the follow graph: which trails were followed, in what order,
|
|
5
5
|
* and supports failure injection from followed trail examples.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, expect, test } from 'bun:test';
|
|
9
9
|
|
|
10
|
-
import type {
|
|
10
|
+
import type { AnyTrail, FollowFn, TrailContext } from '@ontrails/core';
|
|
11
11
|
import {
|
|
12
12
|
InternalError,
|
|
13
13
|
Result,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
expectOk,
|
|
23
23
|
} from './assertions.js';
|
|
24
24
|
import { mergeTestContext } from './context.js';
|
|
25
|
-
import type {
|
|
25
|
+
import type { FollowScenario } from './types.js';
|
|
26
26
|
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
// Follow trace
|
|
@@ -58,7 +58,7 @@ const findErrorExample = (
|
|
|
58
58
|
*/
|
|
59
59
|
const tryInjectError = (
|
|
60
60
|
id: string,
|
|
61
|
-
scenario:
|
|
61
|
+
scenario: FollowScenario,
|
|
62
62
|
trailsMap: ReadonlyMap<string, AnyTrail> | undefined
|
|
63
63
|
): Result<unknown, Error> | undefined => {
|
|
64
64
|
const injection = scenario.injectFromExample?.[id];
|
|
@@ -101,7 +101,7 @@ const executeFromMap = (
|
|
|
101
101
|
if (validated.isErr()) {
|
|
102
102
|
return validated;
|
|
103
103
|
}
|
|
104
|
-
return trailDef.
|
|
104
|
+
return trailDef.run(validated.value, ctx);
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
// ---------------------------------------------------------------------------
|
|
@@ -113,7 +113,7 @@ const executeFromMap = (
|
|
|
113
113
|
*/
|
|
114
114
|
const createRecordingFollow = (
|
|
115
115
|
trace: FollowRecord[],
|
|
116
|
-
scenario:
|
|
116
|
+
scenario: FollowScenario,
|
|
117
117
|
trailsMap: ReadonlyMap<string, AnyTrail> | undefined,
|
|
118
118
|
baseFollow: FollowFn | undefined,
|
|
119
119
|
ctx: TrailContext
|
|
@@ -148,8 +148,8 @@ const createRecordingFollow = (
|
|
|
148
148
|
|
|
149
149
|
const assertScenarioResult = (
|
|
150
150
|
result: Result<unknown, Error>,
|
|
151
|
-
scenario:
|
|
152
|
-
|
|
151
|
+
scenario: FollowScenario,
|
|
152
|
+
trailDef: AnyTrail
|
|
153
153
|
): void => {
|
|
154
154
|
if (scenario.expectValue !== undefined) {
|
|
155
155
|
assertFullMatch(result, scenario.expectValue);
|
|
@@ -162,13 +162,13 @@ const assertScenarioResult = (
|
|
|
162
162
|
}
|
|
163
163
|
} else if (scenario.expectOk === true) {
|
|
164
164
|
expect(result.isOk()).toBe(true);
|
|
165
|
-
assertSchemaMatch(result,
|
|
165
|
+
assertSchemaMatch(result, trailDef.output);
|
|
166
166
|
}
|
|
167
167
|
};
|
|
168
168
|
|
|
169
169
|
const assertFollowTrace = (
|
|
170
170
|
trace: readonly FollowRecord[],
|
|
171
|
-
scenario:
|
|
171
|
+
scenario: FollowScenario
|
|
172
172
|
): void => {
|
|
173
173
|
if (scenario.expectFollowed !== undefined) {
|
|
174
174
|
const followedIds = trace.map((r) => r.id);
|
|
@@ -185,7 +185,7 @@ const assertFollowTrace = (
|
|
|
185
185
|
|
|
186
186
|
const handleValidationError = (
|
|
187
187
|
validated: Result<unknown, Error>,
|
|
188
|
-
scenario:
|
|
188
|
+
scenario: FollowScenario
|
|
189
189
|
): boolean => {
|
|
190
190
|
if (!validated.isErr()) {
|
|
191
191
|
return false;
|
|
@@ -207,7 +207,7 @@ const handleValidationError = (
|
|
|
207
207
|
// ---------------------------------------------------------------------------
|
|
208
208
|
|
|
209
209
|
const buildTestContext = (
|
|
210
|
-
scenario:
|
|
210
|
+
scenario: FollowScenario,
|
|
211
211
|
ctx: Partial<TrailContext> | undefined,
|
|
212
212
|
trailsMap: ReadonlyMap<string, AnyTrail> | undefined
|
|
213
213
|
): { trace: FollowRecord[]; testCtx: TrailContext } => {
|
|
@@ -224,28 +224,28 @@ const buildTestContext = (
|
|
|
224
224
|
};
|
|
225
225
|
|
|
226
226
|
const runScenario = async (
|
|
227
|
-
|
|
228
|
-
scenario:
|
|
227
|
+
trailDef: AnyTrail,
|
|
228
|
+
scenario: FollowScenario,
|
|
229
229
|
ctx: Partial<TrailContext> | undefined,
|
|
230
230
|
trailsMap: ReadonlyMap<string, AnyTrail> | undefined
|
|
231
231
|
): Promise<void> => {
|
|
232
|
-
const validated = validateInput(
|
|
232
|
+
const validated = validateInput(trailDef.input, scenario.input);
|
|
233
233
|
if (handleValidationError(validated, scenario)) {
|
|
234
234
|
return;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
const { trace, testCtx } = buildTestContext(scenario, ctx, trailsMap);
|
|
238
|
-
const result = await
|
|
238
|
+
const result = await trailDef.run(expectOk(validated), testCtx);
|
|
239
239
|
assertFollowTrace(trace, scenario);
|
|
240
|
-
assertScenarioResult(result, scenario,
|
|
240
|
+
assertScenarioResult(result, scenario, trailDef);
|
|
241
241
|
};
|
|
242
242
|
|
|
243
243
|
// ---------------------------------------------------------------------------
|
|
244
|
-
//
|
|
244
|
+
// testFollows
|
|
245
245
|
// ---------------------------------------------------------------------------
|
|
246
246
|
|
|
247
|
-
/** Options for
|
|
248
|
-
export interface
|
|
247
|
+
/** Options for testFollows that provide trail definitions for injection. */
|
|
248
|
+
export interface TestFollowOptions {
|
|
249
249
|
/** Partial context overrides. */
|
|
250
250
|
readonly ctx?: Partial<TrailContext> | undefined;
|
|
251
251
|
/** Map of trail ID to trail definition, used for injectFromExample. */
|
|
@@ -253,11 +253,11 @@ export interface TestHikeOptions {
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
/**
|
|
256
|
-
* Generate a describe block for a
|
|
256
|
+
* Generate a describe block for a composition trail with one test per scenario.
|
|
257
257
|
*
|
|
258
258
|
* @example
|
|
259
259
|
* ```ts
|
|
260
|
-
*
|
|
260
|
+
* testFollows(onboardTrail, [
|
|
261
261
|
* {
|
|
262
262
|
* description: "follows add then relate",
|
|
263
263
|
* input: { name: "Alpha" },
|
|
@@ -267,16 +267,16 @@ export interface TestHikeOptions {
|
|
|
267
267
|
* ]);
|
|
268
268
|
* ```
|
|
269
269
|
*/
|
|
270
|
-
export const
|
|
271
|
-
|
|
272
|
-
scenarios: readonly
|
|
273
|
-
options?:
|
|
270
|
+
export const testFollows = (
|
|
271
|
+
trailDef: AnyTrail,
|
|
272
|
+
scenarios: readonly FollowScenario[],
|
|
273
|
+
options?: TestFollowOptions
|
|
274
274
|
): void => {
|
|
275
|
-
describe(
|
|
275
|
+
describe(trailDef.id, () => {
|
|
276
276
|
test.each([...scenarios])(
|
|
277
277
|
'$description',
|
|
278
|
-
async (scenario:
|
|
279
|
-
await runScenario(
|
|
278
|
+
async (scenario: FollowScenario) => {
|
|
279
|
+
await runScenario(trailDef, scenario, options?.ctx, options?.trails);
|
|
280
280
|
}
|
|
281
281
|
);
|
|
282
282
|
});
|
package/src/harness-mcp.ts
CHANGED
|
@@ -31,9 +31,12 @@ import type {
|
|
|
31
31
|
* ```
|
|
32
32
|
*/
|
|
33
33
|
export const createMcpHarness = (options: McpHarnessOptions): McpHarness => {
|
|
34
|
-
const
|
|
34
|
+
const toolsResult = buildMcpTools(options.app);
|
|
35
|
+
if (toolsResult.isErr()) {
|
|
36
|
+
throw toolsResult.error;
|
|
37
|
+
}
|
|
35
38
|
const toolMap = new Map<string, McpToolDefinition>();
|
|
36
|
-
for (const tool of
|
|
39
|
+
for (const tool of toolsResult.value) {
|
|
37
40
|
toolMap.set(tool.name, tool);
|
|
38
41
|
}
|
|
39
42
|
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Contract-driven testing
|
|
2
2
|
export { testAll } from './all.js';
|
|
3
3
|
export { testExamples } from './examples.js';
|
|
4
|
-
export {
|
|
4
|
+
export { testFollows } from './follows.js';
|
|
5
5
|
export { testTrail } from './trail.js';
|
|
6
6
|
export { testContracts } from './contracts.js';
|
|
7
7
|
export { testDetours } from './detours.js';
|
|
@@ -16,7 +16,7 @@ export {
|
|
|
16
16
|
} from './assertions.js';
|
|
17
17
|
|
|
18
18
|
// Mock factories
|
|
19
|
-
export { createTestContext } from './context.js';
|
|
19
|
+
export { createFollowContext, createTestContext } from './context.js';
|
|
20
20
|
export { createTestLogger } from './logger.js';
|
|
21
21
|
|
|
22
22
|
// Surface harnesses
|
|
@@ -24,10 +24,11 @@ export { createCliHarness } from './harness-cli.js';
|
|
|
24
24
|
export { createMcpHarness } from './harness-mcp.js';
|
|
25
25
|
|
|
26
26
|
// Types
|
|
27
|
-
export type {
|
|
27
|
+
export type { CreateFollowContextOptions } from './context.js';
|
|
28
|
+
export type { TestFollowOptions } from './follows.js';
|
|
28
29
|
|
|
29
30
|
export type {
|
|
30
|
-
|
|
31
|
+
FollowScenario,
|
|
31
32
|
TestScenario,
|
|
32
33
|
TestLogger,
|
|
33
34
|
TestTrailContextOptions,
|
package/src/trail.ts
CHANGED
|
@@ -82,7 +82,7 @@ const runScenario = async (
|
|
|
82
82
|
}
|
|
83
83
|
const validatedInput = expectOk(validated);
|
|
84
84
|
|
|
85
|
-
const result = await trailDef.
|
|
85
|
+
const result = await trailDef.run(validatedInput, testCtx);
|
|
86
86
|
assertScenarioResult(result, scenario, trailDef);
|
|
87
87
|
};
|
|
88
88
|
|
package/src/types.ts
CHANGED
|
@@ -26,11 +26,11 @@ export interface TestScenario {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
29
|
-
//
|
|
29
|
+
// Follow Scenario (for testFollows)
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
31
31
|
|
|
32
|
-
/** A test scenario for a
|
|
33
|
-
export interface
|
|
32
|
+
/** A test scenario for a trail's composition graph. */
|
|
33
|
+
export interface FollowScenario extends TestScenario {
|
|
34
34
|
/** Assert these trail IDs were followed, in order. */
|
|
35
35
|
readonly expectFollowed?: readonly string[] | undefined;
|
|
36
36
|
/** Assert follow counts per trail ID. */
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/all.ts","./src/assertions.ts","./src/context.ts","./src/contracts.ts","./src/detours.ts","./src/examples.ts","./src/
|
|
1
|
+
{"root":["./src/all.ts","./src/assertions.ts","./src/context.ts","./src/contracts.ts","./src/detours.ts","./src/examples.ts","./src/follows.ts","./src/harness-cli.ts","./src/harness-mcp.ts","./src/index.ts","./src/logger.ts","./src/trail.ts","./src/types.ts"],"version":"5.9.3"}
|
package/dist/hike.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* testHike — composition-aware scenario testing for hikes.
|
|
3
|
-
*
|
|
4
|
-
* Tests the composition graph: which trails were followed, in what order,
|
|
5
|
-
* and supports failure injection from followed trail examples.
|
|
6
|
-
*/
|
|
7
|
-
import type { AnyHike, AnyTrail, TrailContext } from '@ontrails/core';
|
|
8
|
-
import type { HikeScenario } from './types.js';
|
|
9
|
-
/** Options for testHike that provide trail definitions for injection. */
|
|
10
|
-
export interface TestHikeOptions {
|
|
11
|
-
/** Partial context overrides. */
|
|
12
|
-
readonly ctx?: Partial<TrailContext> | undefined;
|
|
13
|
-
/** Map of trail ID to trail definition, used for injectFromExample. */
|
|
14
|
-
readonly trails?: ReadonlyMap<string, AnyTrail> | undefined;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Generate a describe block for a hike with one test per scenario.
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* ```ts
|
|
21
|
-
* testHike(onboardHike, [
|
|
22
|
-
* {
|
|
23
|
-
* description: "follows add then relate",
|
|
24
|
-
* input: { name: "Alpha" },
|
|
25
|
-
* expectOk: true,
|
|
26
|
-
* expectFollowed: ["entity.add", "entity.relate"],
|
|
27
|
-
* },
|
|
28
|
-
* ]);
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
export declare const testHike: (hikeDef: AnyHike, scenarios: readonly HikeScenario[], options?: TestHikeOptions) => void;
|
|
32
|
-
//# sourceMappingURL=hike.d.ts.map
|
package/dist/hike.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hike.d.ts","sourceRoot":"","sources":["../src/hike.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAY,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAehF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA8N/C,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,iCAAiC;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,SAAS,CAAC;IACjD,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,SAAS,CAAC;CAC7D;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,QAAQ,GACnB,SAAS,OAAO,EAChB,WAAW,SAAS,YAAY,EAAE,EAClC,UAAU,eAAe,KACxB,IASF,CAAC"}
|
package/dist/hike.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hike.js","sourceRoot":"","sources":["../src/hike.ts"],"names":[],"mappings":"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"}
|