@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.
Files changed (49) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +138 -0
  3. package/README.md +25 -7
  4. package/dist/context.d.ts +20 -1
  5. package/dist/context.d.ts.map +1 -1
  6. package/dist/context.js +25 -0
  7. package/dist/context.js.map +1 -1
  8. package/dist/contracts.d.ts +1 -1
  9. package/dist/contracts.d.ts.map +1 -1
  10. package/dist/contracts.js +15 -5
  11. package/dist/contracts.js.map +1 -1
  12. package/dist/examples.d.ts +2 -2
  13. package/dist/examples.d.ts.map +1 -1
  14. package/dist/examples.js +43 -55
  15. package/dist/examples.js.map +1 -1
  16. package/dist/follows.d.ts +32 -0
  17. package/dist/follows.d.ts.map +1 -0
  18. package/dist/{hike.js → follows.js} +15 -15
  19. package/dist/follows.js.map +1 -0
  20. package/dist/harness-mcp.d.ts.map +1 -1
  21. package/dist/harness-mcp.js +5 -2
  22. package/dist/harness-mcp.js.map +1 -1
  23. package/dist/index.d.ts +5 -4
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +2 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/trail.js +1 -1
  28. package/dist/trail.js.map +1 -1
  29. package/dist/types.d.ts +2 -2
  30. package/dist/types.d.ts.map +1 -1
  31. package/package.json +6 -6
  32. package/src/__tests__/context.test.ts +36 -1
  33. package/src/__tests__/contracts.test.ts +30 -4
  34. package/src/__tests__/detours.test.ts +3 -3
  35. package/src/__tests__/examples.test.ts +21 -22
  36. package/src/__tests__/{hike.test.ts → follows.test.ts} +27 -28
  37. package/src/__tests__/trail.test.ts +4 -4
  38. package/src/context.ts +42 -1
  39. package/src/contracts.ts +19 -6
  40. package/src/examples.ts +55 -91
  41. package/src/{hike.ts → follows.ts} +30 -30
  42. package/src/harness-mcp.ts +5 -2
  43. package/src/index.ts +5 -4
  44. package/src/trail.ts +1 -1
  45. package/src/types.ts +3 -3
  46. package/tsconfig.tsbuildinfo +1 -1
  47. package/dist/hike.d.ts +0 -32
  48. package/dist/hike.d.ts.map +0 -1
  49. 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
- implementation: (input: { id: string }) => {
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's implementation output matches its declared
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 trailEntries = [...app.trails];
65
+ const allEntries = app.list() as Trail<unknown, unknown>[];
54
66
 
55
67
  describe('contracts', () => {
56
- describe.each(trailEntries)('%s', (_id, trailDef) => {
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.implementation(validatedInput, testCtx);
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 hikes with `follows`
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.implementation(validatedInput, testCtx);
140
+ const result = await t.run(validatedInput, testCtx);
142
141
  assertProgressiveMatch(result, example, output);
143
142
  };
144
143
 
145
144
  // ---------------------------------------------------------------------------
146
- // Follows coverage for hikes
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.implementation(validated.value, ctx));
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 hike, recording follow calls.
183
+ * Run a single example against a composition trail, recording follow calls.
185
184
  */
186
- const runHikeExample = async (
187
- hikeDef: AnyHike,
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(hikeDef.input, example.input);
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 hikeDef.implementation(validatedInput, testCtx);
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 hikes with `follows` declarations and examples, also verifies that
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 trailEntries = [...app.trails];
228
+ const allTrails = app.list() as Trail<unknown, unknown>[];
295
229
 
296
- describe.each(trailEntries)('%s', (_id, trailDef) => {
297
- const t = trailDef as Trail<unknown, unknown>;
298
- if (t.examples === undefined || t.examples.length === 0) {
299
- return;
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
- const { examples, output } = t;
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
- test.each([...examples])(
305
- 'example: $name',
306
- async (example: TrailExample<unknown, unknown>) => {
307
- const testCtx = mergeTestContext(resolveCtx());
308
- await runExample(t, example, output, testCtx);
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
- describeHikeExamples(collectHikesWithExamples(app), resolveCtx, app);
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
- * testHike — composition-aware scenario testing for hikes.
2
+ * testFollows — composition-aware scenario testing for trails with follow.
3
3
  *
4
- * Tests the composition graph: which trails were followed, in what order,
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 { AnyHike, AnyTrail, FollowFn, TrailContext } from '@ontrails/core';
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 { HikeScenario } from './types.js';
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: HikeScenario,
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.implementation(validated.value, ctx);
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: HikeScenario,
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: HikeScenario,
152
- hikeDef: AnyHike
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, hikeDef.output);
165
+ assertSchemaMatch(result, trailDef.output);
166
166
  }
167
167
  };
168
168
 
169
169
  const assertFollowTrace = (
170
170
  trace: readonly FollowRecord[],
171
- scenario: HikeScenario
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: HikeScenario
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: HikeScenario,
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
- hikeDef: AnyHike,
228
- scenario: HikeScenario,
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(hikeDef.input, scenario.input);
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 hikeDef.implementation(expectOk(validated), testCtx);
238
+ const result = await trailDef.run(expectOk(validated), testCtx);
239
239
  assertFollowTrace(trace, scenario);
240
- assertScenarioResult(result, scenario, hikeDef);
240
+ assertScenarioResult(result, scenario, trailDef);
241
241
  };
242
242
 
243
243
  // ---------------------------------------------------------------------------
244
- // testHike
244
+ // testFollows
245
245
  // ---------------------------------------------------------------------------
246
246
 
247
- /** Options for testHike that provide trail definitions for injection. */
248
- export interface TestHikeOptions {
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 hike with one test per scenario.
256
+ * Generate a describe block for a composition trail with one test per scenario.
257
257
  *
258
258
  * @example
259
259
  * ```ts
260
- * testHike(onboardHike, [
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 testHike = (
271
- hikeDef: AnyHike,
272
- scenarios: readonly HikeScenario[],
273
- options?: TestHikeOptions
270
+ export const testFollows = (
271
+ trailDef: AnyTrail,
272
+ scenarios: readonly FollowScenario[],
273
+ options?: TestFollowOptions
274
274
  ): void => {
275
- describe(hikeDef.id, () => {
275
+ describe(trailDef.id, () => {
276
276
  test.each([...scenarios])(
277
277
  '$description',
278
- async (scenario: HikeScenario) => {
279
- await runScenario(hikeDef, scenario, options?.ctx, options?.trails);
278
+ async (scenario: FollowScenario) => {
279
+ await runScenario(trailDef, scenario, options?.ctx, options?.trails);
280
280
  }
281
281
  );
282
282
  });
@@ -31,9 +31,12 @@ import type {
31
31
  * ```
32
32
  */
33
33
  export const createMcpHarness = (options: McpHarnessOptions): McpHarness => {
34
- const tools = buildMcpTools(options.app);
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 tools) {
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 { testHike } from './hike.js';
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 { TestHikeOptions } from './hike.js';
27
+ export type { CreateFollowContextOptions } from './context.js';
28
+ export type { TestFollowOptions } from './follows.js';
28
29
 
29
30
  export type {
30
- HikeScenario,
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.implementation(validatedInput, testCtx);
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
- // Hike Scenario (for testHike)
29
+ // Follow Scenario (for testFollows)
30
30
  // ---------------------------------------------------------------------------
31
31
 
32
- /** A test scenario for a hike's composition graph. */
33
- export interface HikeScenario extends TestScenario {
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. */
@@ -1 +1 @@
1
- {"root":["./src/all.ts","./src/assertions.ts","./src/context.ts","./src/contracts.ts","./src/detours.ts","./src/examples.ts","./src/harness-cli.ts","./src/harness-mcp.ts","./src/hike.ts","./src/index.ts","./src/logger.ts","./src/trail.ts","./src/types.ts"],"version":"5.9.3"}
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
@@ -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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"}