@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.
Files changed (55) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +166 -0
  3. package/README.md +25 -7
  4. package/dist/all.d.ts +2 -1
  5. package/dist/all.d.ts.map +1 -1
  6. package/dist/all.js.map +1 -1
  7. package/dist/context.d.ts +28 -2
  8. package/dist/context.d.ts.map +1 -1
  9. package/dist/context.js +70 -11
  10. package/dist/context.js.map +1 -1
  11. package/dist/contracts.d.ts +3 -2
  12. package/dist/contracts.d.ts.map +1 -1
  13. package/dist/contracts.js +25 -10
  14. package/dist/contracts.js.map +1 -1
  15. package/dist/examples.d.ts +4 -3
  16. package/dist/examples.d.ts.map +1 -1
  17. package/dist/examples.js +63 -68
  18. package/dist/examples.js.map +1 -1
  19. package/dist/follows.d.ts +38 -0
  20. package/dist/follows.d.ts.map +1 -0
  21. package/dist/{hike.js → follows.js} +71 -28
  22. package/dist/follows.js.map +1 -0
  23. package/dist/harness-mcp.d.ts.map +1 -1
  24. package/dist/harness-mcp.js +5 -2
  25. package/dist/harness-mcp.js.map +1 -1
  26. package/dist/index.d.ts +6 -4
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/trail.js +1 -1
  31. package/dist/trail.js.map +1 -1
  32. package/dist/types.d.ts +2 -2
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +6 -6
  35. package/src/__tests__/all.test.ts +64 -0
  36. package/src/__tests__/context.test.ts +92 -2
  37. package/src/__tests__/contracts.test.ts +122 -5
  38. package/src/__tests__/detours.test.ts +3 -3
  39. package/src/__tests__/examples.test.ts +285 -22
  40. package/src/__tests__/follows.test.ts +589 -0
  41. package/src/__tests__/trail.test.ts +4 -4
  42. package/src/all.ts +5 -1
  43. package/src/context.ts +121 -13
  44. package/src/contracts.ts +43 -12
  45. package/src/examples.ts +111 -105
  46. package/src/{hike.ts → follows.ts} +138 -43
  47. package/src/harness-mcp.ts +5 -2
  48. package/src/index.ts +6 -4
  49. package/src/trail.ts +1 -1
  50. package/src/types.ts +3 -3
  51. package/tsconfig.tsbuildinfo +1 -1
  52. package/dist/hike.d.ts +0 -32
  53. package/dist/hike.d.ts.map +0 -1
  54. package/dist/hike.js.map +0 -1
  55. package/src/__tests__/hike.test.ts +0 -164
@@ -1,14 +1,20 @@
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 {
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,10 +25,9 @@ import {
19
25
  assertErrorMatch,
20
26
  assertFullMatch,
21
27
  assertSchemaMatch,
22
- expectOk,
23
28
  } from './assertions.js';
24
- import { mergeTestContext } from './context.js';
25
- import type { HikeScenario } from './types.js';
29
+ import { mergeServiceOverrides, mergeTestContext } from './context.js';
30
+ import type { FollowScenario } from './types.js';
26
31
 
27
32
  // ---------------------------------------------------------------------------
28
33
  // Follow trace
@@ -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
  // ---------------------------------------------------------------------------
@@ -58,7 +115,7 @@ const findErrorExample = (
58
115
  */
59
116
  const tryInjectError = (
60
117
  id: string,
61
- scenario: HikeScenario,
118
+ scenario: FollowScenario,
62
119
  trailsMap: ReadonlyMap<string, AnyTrail> | undefined
63
120
  ): Result<unknown, Error> | undefined => {
64
121
  const injection = scenario.injectFromExample?.[id];
@@ -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 validated = validateInput(trailDef.input, input);
101
- if (validated.isErr()) {
102
- return validated;
103
- }
104
- return trailDef.implementation(validated.value, ctx);
159
+ const nestedCtx = follow ? { ...ctx, follow } : ctx;
160
+ return executeTrail(trailDef, input, {
161
+ ctx: nestedCtx,
162
+ services,
163
+ });
105
164
  };
106
165
 
107
166
  // ---------------------------------------------------------------------------
@@ -113,10 +172,11 @@ const executeFromMap = (
113
172
  */
114
173
  const createRecordingFollow = (
115
174
  trace: FollowRecord[],
116
- scenario: HikeScenario,
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(id, input, trailsMap, ctx);
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
  }
@@ -148,8 +215,8 @@ const createRecordingFollow = (
148
215
 
149
216
  const assertScenarioResult = (
150
217
  result: Result<unknown, Error>,
151
- scenario: HikeScenario,
152
- hikeDef: AnyHike
218
+ scenario: FollowScenario,
219
+ trailDef: AnyTrail
153
220
  ): void => {
154
221
  if (scenario.expectValue !== undefined) {
155
222
  assertFullMatch(result, scenario.expectValue);
@@ -162,13 +229,13 @@ const assertScenarioResult = (
162
229
  }
163
230
  } else if (scenario.expectOk === true) {
164
231
  expect(result.isOk()).toBe(true);
165
- assertSchemaMatch(result, hikeDef.output);
232
+ assertSchemaMatch(result, trailDef.output);
166
233
  }
167
234
  };
168
235
 
169
236
  const assertFollowTrace = (
170
237
  trace: readonly FollowRecord[],
171
- scenario: HikeScenario
238
+ scenario: FollowScenario
172
239
  ): void => {
173
240
  if (scenario.expectFollowed !== undefined) {
174
241
  const followedIds = trace.map((r) => r.id);
@@ -185,7 +252,7 @@ const assertFollowTrace = (
185
252
 
186
253
  const handleValidationError = (
187
254
  validated: Result<unknown, Error>,
188
- scenario: HikeScenario
255
+ scenario: FollowScenario
189
256
  ): boolean => {
190
257
  if (!validated.isErr()) {
191
258
  return false;
@@ -207,9 +274,10 @@ const handleValidationError = (
207
274
  // ---------------------------------------------------------------------------
208
275
 
209
276
  const buildTestContext = (
210
- scenario: HikeScenario,
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,46 +286,62 @@ 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
  };
225
294
 
226
295
  const runScenario = async (
227
- hikeDef: AnyHike,
228
- scenario: HikeScenario,
296
+ trailDef: AnyTrail,
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
- const validated = validateInput(hikeDef.input, scenario.input);
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(scenario, ctx, trailsMap);
238
- const result = await hikeDef.implementation(expectOk(validated), testCtx);
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
- assertScenarioResult(result, scenario, hikeDef);
318
+ assertScenarioResult(result, scenario, trailDef);
241
319
  };
242
320
 
243
321
  // ---------------------------------------------------------------------------
244
- // testHike
322
+ // testFollows
245
323
  // ---------------------------------------------------------------------------
246
324
 
247
- /** Options for testHike that provide trail definitions for injection. */
248
- export interface TestHikeOptions {
325
+ /** Options for testFollows that provide trail definitions for injection. */
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
  }
254
338
 
255
339
  /**
256
- * Generate a describe block for a hike with one test per scenario.
340
+ * Generate a describe block for a composition trail with one test per scenario.
257
341
  *
258
342
  * @example
259
343
  * ```ts
260
- * testHike(onboardHike, [
344
+ * testFollows(onboardTrail, [
261
345
  * {
262
346
  * description: "follows add then relate",
263
347
  * input: { name: "Alpha" },
@@ -267,16 +351,27 @@ export interface TestHikeOptions {
267
351
  * ]);
268
352
  * ```
269
353
  */
270
- export const testHike = (
271
- hikeDef: AnyHike,
272
- scenarios: readonly HikeScenario[],
273
- options?: TestHikeOptions
354
+ export const testFollows = (
355
+ trailDef: AnyTrail,
356
+ scenarios: readonly FollowScenario[],
357
+ options?: TestFollowOptions
274
358
  ): void => {
275
- describe(hikeDef.id, () => {
359
+ describe(trailDef.id, () => {
276
360
  test.each([...scenarios])(
277
361
  '$description',
278
- async (scenario: HikeScenario) => {
279
- await runScenario(hikeDef, scenario, options?.ctx, options?.trails);
362
+ async (scenario: FollowScenario) => {
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
  });
@@ -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,12 @@ 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 { TestExecutionOptions } from './context.js';
29
+ export type { TestFollowOptions } from './follows.js';
28
30
 
29
31
  export type {
30
- HikeScenario,
32
+ FollowScenario,
31
33
  TestScenario,
32
34
  TestLogger,
33
35
  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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"}
@@ -1,164 +0,0 @@
1
- import { describe } from 'bun:test';
2
-
3
- import type { AnyTrail, TrailContext } from '@ontrails/core';
4
- import { Result, hike, trail } from '@ontrails/core';
5
- import { z } from 'zod';
6
-
7
- import { testHike } from '../hike.js';
8
-
9
- // ---------------------------------------------------------------------------
10
- // Test trails (followed by hikes)
11
- // ---------------------------------------------------------------------------
12
-
13
- const addTrail = trail('entity.add', {
14
- description: 'Add an entity',
15
- examples: [
16
- { input: { name: 'Alpha' }, name: 'success' },
17
- {
18
- description: 'duplicate name',
19
- error: 'AlreadyExistsError',
20
- input: { name: '' },
21
- name: 'duplicate',
22
- },
23
- ],
24
- implementation: (input: { name: string }) =>
25
- Result.ok({ id: '1', name: input.name }),
26
- input: z.object({ name: z.string() }),
27
- output: z.object({ id: z.string(), name: z.string() }),
28
- });
29
-
30
- const relateTrail = trail('entity.relate', {
31
- description: 'Relate two entities',
32
- implementation: (input: { from: string; to: string }) =>
33
- Result.ok({ from: input.from, to: input.to }),
34
- input: z.object({ from: z.string(), to: z.string() }),
35
- output: z.object({ from: z.string(), to: z.string() }),
36
- });
37
-
38
- // ---------------------------------------------------------------------------
39
- // Test hike
40
- // ---------------------------------------------------------------------------
41
-
42
- const onboardHike = hike('entity.onboard', {
43
- follows: ['entity.add', 'entity.relate'],
44
- implementation: async (
45
- input: { name: string; relatedTo: string },
46
- ctx: TrailContext
47
- ) => {
48
- if (!ctx.follow) {
49
- return Result.err(new Error('follow not available'));
50
- }
51
- const addResult = await ctx.follow<{ id: string; name: string }>(
52
- 'entity.add',
53
- { name: input.name }
54
- );
55
- if (addResult.isErr()) {
56
- return Result.err(addResult.error);
57
- }
58
-
59
- const relateResult = await ctx.follow<{ from: string; to: string }>(
60
- 'entity.relate',
61
- { from: addResult.value.name, to: input.relatedTo }
62
- );
63
- if (relateResult.isErr()) {
64
- return Result.err(relateResult.error);
65
- }
66
-
67
- return Result.ok({
68
- name: addResult.value.name,
69
- relatedTo: relateResult.value.to,
70
- });
71
- },
72
- input: z.object({ name: z.string(), relatedTo: z.string() }),
73
- output: z.object({ name: z.string(), relatedTo: z.string() }),
74
- });
75
-
76
- const trailsMap = new Map<string, AnyTrail>([
77
- ['entity.add', addTrail],
78
- ['entity.relate', relateTrail],
79
- ]);
80
-
81
- // ---------------------------------------------------------------------------
82
- // Tests
83
- // ---------------------------------------------------------------------------
84
-
85
- const opts = { trails: trailsMap };
86
-
87
- describe('testHike: expectOk', () => {
88
- // eslint-disable-next-line jest/require-hook
89
- testHike(
90
- onboardHike,
91
- [
92
- {
93
- description: 'basic onboard succeeds',
94
- expectOk: true,
95
- input: { name: 'Alpha', relatedTo: 'Beta' },
96
- },
97
- ],
98
- opts
99
- );
100
- });
101
-
102
- describe('testHike: expectFollowed', () => {
103
- // eslint-disable-next-line jest/require-hook
104
- testHike(
105
- onboardHike,
106
- [
107
- {
108
- description: 'follows add then relate in order',
109
- expectFollowed: ['entity.add', 'entity.relate'],
110
- expectOk: true,
111
- input: { name: 'Alpha', relatedTo: 'Beta' },
112
- },
113
- ],
114
- opts
115
- );
116
- });
117
-
118
- describe('testHike: expectFollowedCount', () => {
119
- // eslint-disable-next-line jest/require-hook
120
- testHike(
121
- onboardHike,
122
- [
123
- {
124
- description: 'each trail followed exactly once',
125
- expectFollowedCount: { 'entity.add': 1, 'entity.relate': 1 },
126
- expectOk: true,
127
- input: { name: 'Alpha', relatedTo: 'Beta' },
128
- },
129
- ],
130
- opts
131
- );
132
- });
133
-
134
- describe('testHike: injectFromExample', () => {
135
- // eslint-disable-next-line jest/require-hook
136
- testHike(
137
- onboardHike,
138
- [
139
- {
140
- description: 'inject duplicate error from add trail example',
141
- expectErr: Error,
142
- expectErrMessage: 'AlreadyExistsError',
143
- injectFromExample: { 'entity.add': 'duplicate' },
144
- input: { name: 'Alpha', relatedTo: 'Beta' },
145
- },
146
- ],
147
- opts
148
- );
149
- });
150
-
151
- describe('testHike: expectValue', () => {
152
- // eslint-disable-next-line jest/require-hook
153
- testHike(
154
- onboardHike,
155
- [
156
- {
157
- description: 'exact value match',
158
- expectValue: { name: 'Alpha', relatedTo: 'Beta' },
159
- input: { name: 'Alpha', relatedTo: 'Beta' },
160
- },
161
- ],
162
- opts
163
- );
164
- });