@ontrails/testing 1.0.0-beta.2 → 1.0.0-beta.21

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 (91) hide show
  1. package/CHANGELOG.md +396 -5
  2. package/README.md +86 -22
  3. package/package.json +34 -5
  4. package/src/all-established.ts +168 -0
  5. package/src/all.ts +51 -12
  6. package/src/assertions.ts +253 -0
  7. package/src/cli.ts +6 -0
  8. package/src/composes.ts +433 -0
  9. package/src/context.ts +200 -14
  10. package/src/contracts.ts +45 -21
  11. package/src/detours.ts +155 -18
  12. package/src/effective-examples.ts +414 -0
  13. package/src/errors.ts +47 -0
  14. package/src/examples.ts +302 -177
  15. package/src/harness-cli.ts +83 -58
  16. package/src/harness-http.ts +341 -0
  17. package/src/harness-mcp.ts +46 -16
  18. package/src/http.ts +10 -0
  19. package/src/index.ts +22 -14
  20. package/src/logger.ts +3 -1
  21. package/src/mcp.ts +6 -0
  22. package/src/scenario.ts +375 -0
  23. package/src/signals.ts +221 -0
  24. package/src/surface-parity.ts +389 -0
  25. package/src/trail.ts +1 -1
  26. package/src/types.ts +24 -52
  27. package/.turbo/turbo-build.log +0 -1
  28. package/.turbo/turbo-lint.log +0 -3
  29. package/.turbo/turbo-typecheck.log +0 -1
  30. package/dist/all.d.ts +0 -30
  31. package/dist/all.d.ts.map +0 -1
  32. package/dist/all.js +0 -47
  33. package/dist/all.js.map +0 -1
  34. package/dist/assertions.d.ts +0 -49
  35. package/dist/assertions.d.ts.map +0 -1
  36. package/dist/assertions.js +0 -84
  37. package/dist/assertions.js.map +0 -1
  38. package/dist/context.d.ts +0 -19
  39. package/dist/context.d.ts.map +0 -1
  40. package/dist/context.js +0 -33
  41. package/dist/context.js.map +0 -1
  42. package/dist/contracts.d.ts +0 -16
  43. package/dist/contracts.d.ts.map +0 -1
  44. package/dist/contracts.js +0 -56
  45. package/dist/contracts.js.map +0 -1
  46. package/dist/detours.d.ts +0 -12
  47. package/dist/detours.d.ts.map +0 -1
  48. package/dist/detours.js +0 -30
  49. package/dist/detours.js.map +0 -1
  50. package/dist/examples.d.ts +0 -22
  51. package/dist/examples.d.ts.map +0 -1
  52. package/dist/examples.js +0 -187
  53. package/dist/examples.js.map +0 -1
  54. package/dist/harness-cli.d.ts +0 -21
  55. package/dist/harness-cli.d.ts.map +0 -1
  56. package/dist/harness-cli.js +0 -213
  57. package/dist/harness-cli.js.map +0 -1
  58. package/dist/harness-mcp.d.ts +0 -21
  59. package/dist/harness-mcp.d.ts.map +0 -1
  60. package/dist/harness-mcp.js +0 -50
  61. package/dist/harness-mcp.js.map +0 -1
  62. package/dist/hike.d.ts +0 -32
  63. package/dist/hike.d.ts.map +0 -1
  64. package/dist/hike.js +0 -169
  65. package/dist/hike.js.map +0 -1
  66. package/dist/index.d.ts +0 -14
  67. package/dist/index.d.ts.map +0 -1
  68. package/dist/index.js +0 -16
  69. package/dist/index.js.map +0 -1
  70. package/dist/logger.d.ts +0 -15
  71. package/dist/logger.d.ts.map +0 -1
  72. package/dist/logger.js +0 -87
  73. package/dist/logger.js.map +0 -1
  74. package/dist/trail.d.ts +0 -20
  75. package/dist/trail.d.ts.map +0 -1
  76. package/dist/trail.js +0 -80
  77. package/dist/trail.js.map +0 -1
  78. package/dist/types.d.ts +0 -80
  79. package/dist/types.d.ts.map +0 -1
  80. package/dist/types.js +0 -5
  81. package/dist/types.js.map +0 -1
  82. package/src/__tests__/context.test.ts +0 -60
  83. package/src/__tests__/contracts.test.ts +0 -68
  84. package/src/__tests__/detours.test.ts +0 -55
  85. package/src/__tests__/examples.test.ts +0 -176
  86. package/src/__tests__/hike.test.ts +0 -164
  87. package/src/__tests__/logger.test.ts +0 -136
  88. package/src/__tests__/trail.test.ts +0 -99
  89. package/src/hike.ts +0 -283
  90. package/tsconfig.json +0 -9
  91. package/tsconfig.tsbuildinfo +0 -1
@@ -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
- });
@@ -1,136 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
-
3
- import { createTestLogger } from '../logger.js';
4
-
5
- // ---------------------------------------------------------------------------
6
- // Tests
7
- // ---------------------------------------------------------------------------
8
-
9
- describe('createTestLogger: basic operations', () => {
10
- test('captures info entries', () => {
11
- const logger = createTestLogger();
12
- logger.info('hello');
13
- logger.warn('world');
14
- expect(logger.entries).toHaveLength(2);
15
- const [first] = logger.entries;
16
- expect(first).toBeDefined();
17
- expect(first?.level).toBe('info');
18
- expect(first?.message).toBe('hello');
19
- });
20
-
21
- test('captures warn entries', () => {
22
- const logger = createTestLogger();
23
- logger.info('hello');
24
- logger.warn('world');
25
- const [, second] = logger.entries;
26
- expect(second).toBeDefined();
27
- expect(second?.level).toBe('warn');
28
- expect(second?.message).toBe('world');
29
- });
30
-
31
- test('entries contain all logged records', () => {
32
- const logger = createTestLogger();
33
- logger.trace('t');
34
- logger.debug('d');
35
- logger.info('i');
36
- logger.warn('w');
37
- logger.error('e');
38
- logger.fatal('f');
39
- expect(logger.entries).toHaveLength(6);
40
- });
41
-
42
- test('clear() empties the entries array', () => {
43
- const logger = createTestLogger();
44
- logger.info('one');
45
- logger.info('two');
46
- expect(logger.entries).toHaveLength(2);
47
- logger.clear();
48
- expect(logger.entries).toHaveLength(0);
49
- });
50
-
51
- test('find() filters entries by predicate', () => {
52
- const logger = createTestLogger();
53
- logger.info('alpha');
54
- logger.warn('beta');
55
- logger.info('gamma');
56
-
57
- const warnings = logger.find((r) => r.level === 'warn');
58
- expect(warnings).toHaveLength(1);
59
- expect(warnings[0]?.message).toBe('beta');
60
- });
61
- });
62
-
63
- describe('createTestLogger: assertLogged', () => {
64
- test('passes when matching entry exists', () => {
65
- const logger = createTestLogger();
66
- logger.info('operation completed');
67
- expect(() => logger.assertLogged('info', 'completed')).not.toThrow();
68
- });
69
-
70
- test('fails when no matching entry exists', () => {
71
- const logger = createTestLogger();
72
- logger.info('something else');
73
- expect(() => logger.assertLogged('error', 'not found')).toThrow(
74
- /Expected a log entry/
75
- );
76
- });
77
- });
78
-
79
- describe('createTestLogger: child loggers', () => {
80
- test('child() captures to the same entries array', () => {
81
- const logger = createTestLogger();
82
- const child = logger.child({ component: 'db' });
83
-
84
- logger.info('parent message');
85
- child.info('child message');
86
-
87
- expect(logger.entries).toHaveLength(2);
88
- expect(logger.entries[0]?.message).toBe('parent message');
89
- expect(logger.entries[1]?.message).toBe('child message');
90
- expect(logger.entries[1]?.metadata).toEqual({ component: 'db' });
91
- });
92
-
93
- test('child logger inherits parent metadata', () => {
94
- const logger = createTestLogger();
95
- const child = logger.child({ requestId: 'abc' });
96
- const grandchild = child.child({ trailId: 'greet' });
97
-
98
- grandchild.info('deep log');
99
-
100
- expect(logger.entries).toHaveLength(1);
101
- expect(logger.entries[0]?.metadata).toEqual({
102
- requestId: 'abc',
103
- trailId: 'greet',
104
- });
105
- });
106
- });
107
-
108
- describe('createTestLogger: configuration', () => {
109
- test('respects minimum log level', () => {
110
- const logger = createTestLogger({ level: 'warn' });
111
- logger.debug('should be filtered');
112
- logger.info('also filtered');
113
- logger.warn('should appear');
114
- logger.error('also appears');
115
- expect(logger.entries).toHaveLength(2);
116
- expect(logger.entries[0]?.level).toBe('warn');
117
- expect(logger.entries[1]?.level).toBe('error');
118
- });
119
-
120
- test('has name property', () => {
121
- const logger = createTestLogger();
122
- expect(logger.name).toBe('test');
123
- });
124
-
125
- test('entries have timestamps', () => {
126
- const logger = createTestLogger();
127
- logger.info('timestamped');
128
- expect(logger.entries[0]?.timestamp).toBeInstanceOf(Date);
129
- });
130
-
131
- test('entries have category matching logger name', () => {
132
- const logger = createTestLogger();
133
- logger.info('categorized');
134
- expect(logger.entries[0]?.category).toBe('test');
135
- });
136
- });
@@ -1,99 +0,0 @@
1
- import { describe } from 'bun:test';
2
-
3
- import { NotFoundError, Result, ValidationError, trail } from '@ontrails/core';
4
- import { z } from 'zod';
5
-
6
- import { testTrail } from '../trail.js';
7
-
8
- // ---------------------------------------------------------------------------
9
- // Test trails
10
- // ---------------------------------------------------------------------------
11
-
12
- const greetTrail = trail('greet', {
13
- implementation: (input: { name: string }) =>
14
- Result.ok({ greeting: `Hello, ${input.name}` }),
15
- input: z.object({ name: z.string() }),
16
- output: z.object({ greeting: z.string() }),
17
- });
18
-
19
- const failTrail = trail('fail', {
20
- implementation: (input: { id: string }) => {
21
- if (input.id === 'missing') {
22
- return Result.err(new NotFoundError('Not found: missing'));
23
- }
24
- return Result.ok({ id: input.id });
25
- },
26
- input: z.object({ id: z.string() }),
27
- });
28
-
29
- // ---------------------------------------------------------------------------
30
- // Tests
31
- //
32
- // Each call to testTrail registers describe/test blocks.
33
- // We call them at the describe scope so bun:test can discover them.
34
- // ---------------------------------------------------------------------------
35
-
36
- describe('testTrail: expectOk', () => {
37
- // eslint-disable-next-line jest/require-hook
38
- testTrail(greetTrail, [
39
- { description: 'valid greeting', expectOk: true, input: { name: 'Bob' } },
40
- ]);
41
- });
42
-
43
- describe('testTrail: expectValue', () => {
44
- // eslint-disable-next-line jest/require-hook
45
- testTrail(greetTrail, [
46
- {
47
- description: 'exact match',
48
- expectValue: { greeting: 'Hello, Charlie' },
49
- input: { name: 'Charlie' },
50
- },
51
- ]);
52
- });
53
-
54
- describe('testTrail: expectErr', () => {
55
- // eslint-disable-next-line jest/require-hook
56
- testTrail(failTrail, [
57
- {
58
- description: 'not found error',
59
- expectErr: NotFoundError,
60
- input: { id: 'missing' },
61
- },
62
- ]);
63
- });
64
-
65
- describe('testTrail: expectErrMessage', () => {
66
- // eslint-disable-next-line jest/require-hook
67
- testTrail(failTrail, [
68
- {
69
- description: 'error message contains substring',
70
- expectErr: NotFoundError,
71
- expectErrMessage: 'Not found',
72
- input: { id: 'missing' },
73
- },
74
- ]);
75
- });
76
-
77
- describe('testTrail: invalid input with ValidationError', () => {
78
- // eslint-disable-next-line jest/require-hook
79
- testTrail(greetTrail, [
80
- {
81
- description: 'invalid input caught by validation',
82
- expectErr: ValidationError,
83
- input: { name: 123 },
84
- },
85
- ]);
86
- });
87
-
88
- describe('testTrail: multiple scenarios', () => {
89
- // eslint-disable-next-line jest/require-hook
90
- testTrail(greetTrail, [
91
- { description: 'scenario 1', expectOk: true, input: { name: 'A' } },
92
- { description: 'scenario 2', expectOk: true, input: { name: 'B' } },
93
- {
94
- description: 'scenario 3',
95
- expectValue: { greeting: 'Hello, C' },
96
- input: { name: 'C' },
97
- },
98
- ]);
99
- });
package/src/hike.ts DELETED
@@ -1,283 +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
-
8
- import { describe, expect, test } from 'bun:test';
9
-
10
- import type { AnyHike, AnyTrail, FollowFn, TrailContext } from '@ontrails/core';
11
- import {
12
- InternalError,
13
- Result,
14
- ValidationError,
15
- validateInput,
16
- } from '@ontrails/core';
17
-
18
- import {
19
- assertErrorMatch,
20
- assertFullMatch,
21
- assertSchemaMatch,
22
- expectOk,
23
- } from './assertions.js';
24
- import { mergeTestContext } from './context.js';
25
- import type { HikeScenario } from './types.js';
26
-
27
- // ---------------------------------------------------------------------------
28
- // Follow trace
29
- // ---------------------------------------------------------------------------
30
-
31
- interface FollowRecord {
32
- readonly id: string;
33
- readonly input: unknown;
34
- }
35
-
36
- // ---------------------------------------------------------------------------
37
- // Injection helpers
38
- // ---------------------------------------------------------------------------
39
-
40
- /**
41
- * Find an error example on a trail by name or description substring.
42
- */
43
- const findErrorExample = (
44
- trailDef: AnyTrail,
45
- description: string
46
- ): string | undefined => {
47
- const example = trailDef.examples?.find(
48
- (ex) =>
49
- ex.error !== undefined &&
50
- (ex.description?.includes(description) || ex.name.includes(description))
51
- );
52
- return example?.error;
53
- };
54
-
55
- /**
56
- * Try to inject an error from a followed trail's example.
57
- * Returns undefined when no injection is configured for this trail ID.
58
- */
59
- const tryInjectError = (
60
- id: string,
61
- scenario: HikeScenario,
62
- trailsMap: ReadonlyMap<string, AnyTrail> | undefined
63
- ): Result<unknown, Error> | undefined => {
64
- const injection = scenario.injectFromExample?.[id];
65
- if (injection === undefined) {
66
- return undefined;
67
- }
68
-
69
- const trailDef = trailsMap?.get(id);
70
- if (trailDef === undefined) {
71
- return Result.err(
72
- new InternalError(`Cannot inject: trail "${id}" not in topo`)
73
- );
74
- }
75
- const errorName = findErrorExample(trailDef, injection);
76
- if (errorName === undefined) {
77
- return Result.err(
78
- new InternalError(
79
- `No error example matching "${injection}" on trail "${id}"`
80
- )
81
- );
82
- }
83
- return Result.err(new Error(errorName));
84
- };
85
-
86
- /**
87
- * Execute a trail from the map, validating input first.
88
- */
89
- const executeFromMap = (
90
- id: string,
91
- input: unknown,
92
- trailsMap: ReadonlyMap<string, AnyTrail> | undefined,
93
- ctx: TrailContext
94
- ): Result<unknown, Error> | Promise<Result<unknown, Error>> | undefined => {
95
- const trailDef = trailsMap?.get(id);
96
- if (trailDef === undefined) {
97
- return undefined;
98
- }
99
-
100
- const validated = validateInput(trailDef.input, input);
101
- if (validated.isErr()) {
102
- return validated;
103
- }
104
- return trailDef.implementation(validated.value, ctx);
105
- };
106
-
107
- // ---------------------------------------------------------------------------
108
- // Follow factory
109
- // ---------------------------------------------------------------------------
110
-
111
- /**
112
- * Build a recording follow function that optionally injects errors.
113
- */
114
- const createRecordingFollow = (
115
- trace: FollowRecord[],
116
- scenario: HikeScenario,
117
- trailsMap: ReadonlyMap<string, AnyTrail> | undefined,
118
- baseFollow: FollowFn | undefined,
119
- ctx: TrailContext
120
- ): FollowFn => {
121
- // The generic O on FollowFn is erased at runtime; the cast is safe
122
- // because callers narrow via isOk/isErr before accessing the value.
123
- const follow = (id: string, input: unknown) => {
124
- trace.push({ id, input });
125
-
126
- const injected = tryInjectError(id, scenario, trailsMap);
127
- if (injected !== undefined) {
128
- return Promise.resolve(injected);
129
- }
130
-
131
- if (baseFollow !== undefined) {
132
- return baseFollow(id, input);
133
- }
134
-
135
- const executed = executeFromMap(id, input, trailsMap, ctx);
136
- if (executed !== undefined) {
137
- return Promise.resolve(executed);
138
- }
139
-
140
- return Promise.resolve(Result.ok());
141
- };
142
- return follow as FollowFn;
143
- };
144
-
145
- // ---------------------------------------------------------------------------
146
- // Scenario assertions
147
- // ---------------------------------------------------------------------------
148
-
149
- const assertScenarioResult = (
150
- result: Result<unknown, Error>,
151
- scenario: HikeScenario,
152
- hikeDef: AnyHike
153
- ): void => {
154
- if (scenario.expectValue !== undefined) {
155
- assertFullMatch(result, scenario.expectValue);
156
- } else if (scenario.expectErr !== undefined) {
157
- assertErrorMatch(result, scenario.expectErr, scenario.expectErrMessage);
158
- } else if (scenario.expectErrMessage !== undefined) {
159
- expect(result.isErr()).toBe(true);
160
- if (result.isErr()) {
161
- expect(result.error.message).toContain(scenario.expectErrMessage);
162
- }
163
- } else if (scenario.expectOk === true) {
164
- expect(result.isOk()).toBe(true);
165
- assertSchemaMatch(result, hikeDef.output);
166
- }
167
- };
168
-
169
- const assertFollowTrace = (
170
- trace: readonly FollowRecord[],
171
- scenario: HikeScenario
172
- ): void => {
173
- if (scenario.expectFollowed !== undefined) {
174
- const followedIds = trace.map((r) => r.id);
175
- expect(followedIds).toEqual([...scenario.expectFollowed]);
176
- }
177
- if (scenario.expectFollowedCount !== undefined) {
178
- const counts: Record<string, number> = {};
179
- for (const record of trace) {
180
- counts[record.id] = (counts[record.id] ?? 0) + 1;
181
- }
182
- expect(counts).toEqual({ ...scenario.expectFollowedCount });
183
- }
184
- };
185
-
186
- const handleValidationError = (
187
- validated: Result<unknown, Error>,
188
- scenario: HikeScenario
189
- ): boolean => {
190
- if (!validated.isErr()) {
191
- return false;
192
- }
193
- if (scenario.expectErr === ValidationError) {
194
- expect(validated.error).toBeInstanceOf(ValidationError);
195
- if (scenario.expectErrMessage !== undefined) {
196
- expect(validated.error.message).toContain(scenario.expectErrMessage);
197
- }
198
- return true;
199
- }
200
- throw new Error(
201
- `Input validation failed unexpectedly: ${validated.error.message}`
202
- );
203
- };
204
-
205
- // ---------------------------------------------------------------------------
206
- // Scenario runner
207
- // ---------------------------------------------------------------------------
208
-
209
- const buildTestContext = (
210
- scenario: HikeScenario,
211
- ctx: Partial<TrailContext> | undefined,
212
- trailsMap: ReadonlyMap<string, AnyTrail> | undefined
213
- ): { trace: FollowRecord[]; testCtx: TrailContext } => {
214
- const trace: FollowRecord[] = [];
215
- const baseCtx = mergeTestContext(ctx);
216
- const follow = createRecordingFollow(
217
- trace,
218
- scenario,
219
- trailsMap,
220
- baseCtx.follow,
221
- baseCtx
222
- );
223
- return { testCtx: { ...baseCtx, follow }, trace };
224
- };
225
-
226
- const runScenario = async (
227
- hikeDef: AnyHike,
228
- scenario: HikeScenario,
229
- ctx: Partial<TrailContext> | undefined,
230
- trailsMap: ReadonlyMap<string, AnyTrail> | undefined
231
- ): Promise<void> => {
232
- const validated = validateInput(hikeDef.input, scenario.input);
233
- if (handleValidationError(validated, scenario)) {
234
- return;
235
- }
236
-
237
- const { trace, testCtx } = buildTestContext(scenario, ctx, trailsMap);
238
- const result = await hikeDef.implementation(expectOk(validated), testCtx);
239
- assertFollowTrace(trace, scenario);
240
- assertScenarioResult(result, scenario, hikeDef);
241
- };
242
-
243
- // ---------------------------------------------------------------------------
244
- // testHike
245
- // ---------------------------------------------------------------------------
246
-
247
- /** Options for testHike that provide trail definitions for injection. */
248
- export interface TestHikeOptions {
249
- /** Partial context overrides. */
250
- readonly ctx?: Partial<TrailContext> | undefined;
251
- /** Map of trail ID to trail definition, used for injectFromExample. */
252
- readonly trails?: ReadonlyMap<string, AnyTrail> | undefined;
253
- }
254
-
255
- /**
256
- * Generate a describe block for a hike with one test per scenario.
257
- *
258
- * @example
259
- * ```ts
260
- * testHike(onboardHike, [
261
- * {
262
- * description: "follows add then relate",
263
- * input: { name: "Alpha" },
264
- * expectOk: true,
265
- * expectFollowed: ["entity.add", "entity.relate"],
266
- * },
267
- * ]);
268
- * ```
269
- */
270
- export const testHike = (
271
- hikeDef: AnyHike,
272
- scenarios: readonly HikeScenario[],
273
- options?: TestHikeOptions
274
- ): void => {
275
- describe(hikeDef.id, () => {
276
- test.each([...scenarios])(
277
- '$description',
278
- async (scenario: HikeScenario) => {
279
- await runScenario(hikeDef, scenario, options?.ctx, options?.trails);
280
- }
281
- );
282
- });
283
- };
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src"
6
- },
7
- "include": ["src"],
8
- "exclude": ["**/__tests__/**", "**/*.test.ts", "dist"]
9
- }
@@ -1 +0,0 @@
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"}