@ontrails/testing 1.0.0-beta.4 → 1.0.0-beta.41

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 +526 -12
  2. package/README.md +83 -18
  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 +44 -33
  11. package/src/detours.ts +155 -18
  12. package/src/effective-examples.ts +408 -0
  13. package/src/errors.ts +47 -0
  14. package/src/examples.ts +274 -113
  15. package/src/harness-cli.ts +83 -58
  16. package/src/harness-http.ts +341 -0
  17. package/src/harness-mcp.ts +49 -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 -66
  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 -175
  53. package/dist/examples.js.map +0 -1
  54. package/dist/follows.d.ts +0 -32
  55. package/dist/follows.d.ts.map +0 -1
  56. package/dist/follows.js +0 -169
  57. package/dist/follows.js.map +0 -1
  58. package/dist/harness-cli.d.ts +0 -21
  59. package/dist/harness-cli.d.ts.map +0 -1
  60. package/dist/harness-cli.js +0 -213
  61. package/dist/harness-cli.js.map +0 -1
  62. package/dist/harness-mcp.d.ts +0 -21
  63. package/dist/harness-mcp.d.ts.map +0 -1
  64. package/dist/harness-mcp.js +0 -50
  65. package/dist/harness-mcp.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 -94
  84. package/src/__tests__/detours.test.ts +0 -55
  85. package/src/__tests__/examples.test.ts +0 -175
  86. package/src/__tests__/follows.test.ts +0 -163
  87. package/src/__tests__/logger.test.ts +0 -136
  88. package/src/__tests__/trail.test.ts +0 -99
  89. package/src/follows.ts +0 -283
  90. package/tsconfig.json +0 -9
  91. package/tsconfig.tsbuildinfo +0 -1
@@ -1,163 +0,0 @@
1
- import { describe } from 'bun:test';
2
-
3
- import type { AnyTrail, TrailContext } from '@ontrails/core';
4
- import { Result, trail } from '@ontrails/core';
5
- import { z } from 'zod';
6
-
7
- import { testFollows } from '../follows.js';
8
-
9
- // ---------------------------------------------------------------------------
10
- // Test trails (followed by composition trail)
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
- input: z.object({ name: z.string() }),
25
- output: z.object({ id: z.string(), name: z.string() }),
26
- run: (input: { name: string }) => Result.ok({ id: '1', name: input.name }),
27
- });
28
-
29
- const relateTrail = trail('entity.relate', {
30
- description: 'Relate two entities',
31
- input: z.object({ from: z.string(), to: z.string() }),
32
- output: z.object({ from: z.string(), to: z.string() }),
33
- run: (input: { from: string; to: string }) =>
34
- Result.ok({ from: input.from, to: input.to }),
35
- });
36
-
37
- // ---------------------------------------------------------------------------
38
- // Composition trail
39
- // ---------------------------------------------------------------------------
40
-
41
- const onboardTrail = trail('entity.onboard', {
42
- follow: ['entity.add', 'entity.relate'],
43
- input: z.object({ name: z.string(), relatedTo: z.string() }),
44
- output: z.object({ name: z.string(), relatedTo: z.string() }),
45
- run: async (
46
- input: { name: string; relatedTo: string },
47
- ctx: TrailContext
48
- ) => {
49
- if (!ctx.follow) {
50
- return Result.err(new Error('follow not available'));
51
- }
52
- const addResult = await ctx.follow<{ id: string; name: string }>(
53
- 'entity.add',
54
- { name: input.name }
55
- );
56
- if (addResult.isErr()) {
57
- return Result.err(addResult.error);
58
- }
59
-
60
- const relateResult = await ctx.follow<{ from: string; to: string }>(
61
- 'entity.relate',
62
- { from: addResult.value.name, to: input.relatedTo }
63
- );
64
- if (relateResult.isErr()) {
65
- return Result.err(relateResult.error);
66
- }
67
-
68
- return Result.ok({
69
- name: addResult.value.name,
70
- relatedTo: relateResult.value.to,
71
- });
72
- },
73
- });
74
-
75
- const trailsMap = new Map<string, AnyTrail>([
76
- ['entity.add', addTrail],
77
- ['entity.relate', relateTrail],
78
- ]);
79
-
80
- // ---------------------------------------------------------------------------
81
- // Tests
82
- // ---------------------------------------------------------------------------
83
-
84
- const opts = { trails: trailsMap };
85
-
86
- describe('testFollows: expectOk', () => {
87
- // eslint-disable-next-line jest/require-hook
88
- testFollows(
89
- onboardTrail,
90
- [
91
- {
92
- description: 'basic onboard succeeds',
93
- expectOk: true,
94
- input: { name: 'Alpha', relatedTo: 'Beta' },
95
- },
96
- ],
97
- opts
98
- );
99
- });
100
-
101
- describe('testFollows: expectFollowed', () => {
102
- // eslint-disable-next-line jest/require-hook
103
- testFollows(
104
- onboardTrail,
105
- [
106
- {
107
- description: 'follows add then relate in order',
108
- expectFollowed: ['entity.add', 'entity.relate'],
109
- expectOk: true,
110
- input: { name: 'Alpha', relatedTo: 'Beta' },
111
- },
112
- ],
113
- opts
114
- );
115
- });
116
-
117
- describe('testFollows: expectFollowedCount', () => {
118
- // eslint-disable-next-line jest/require-hook
119
- testFollows(
120
- onboardTrail,
121
- [
122
- {
123
- description: 'each trail followed exactly once',
124
- expectFollowedCount: { 'entity.add': 1, 'entity.relate': 1 },
125
- expectOk: true,
126
- input: { name: 'Alpha', relatedTo: 'Beta' },
127
- },
128
- ],
129
- opts
130
- );
131
- });
132
-
133
- describe('testFollows: injectFromExample', () => {
134
- // eslint-disable-next-line jest/require-hook
135
- testFollows(
136
- onboardTrail,
137
- [
138
- {
139
- description: 'inject duplicate error from add trail example',
140
- expectErr: Error,
141
- expectErrMessage: 'AlreadyExistsError',
142
- injectFromExample: { 'entity.add': 'duplicate' },
143
- input: { name: 'Alpha', relatedTo: 'Beta' },
144
- },
145
- ],
146
- opts
147
- );
148
- });
149
-
150
- describe('testFollows: expectValue', () => {
151
- // eslint-disable-next-line jest/require-hook
152
- testFollows(
153
- onboardTrail,
154
- [
155
- {
156
- description: 'exact value match',
157
- expectValue: { name: 'Alpha', relatedTo: 'Beta' },
158
- input: { name: 'Alpha', relatedTo: 'Beta' },
159
- },
160
- ],
161
- opts
162
- );
163
- });
@@ -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
- input: z.object({ name: z.string() }),
14
- output: z.object({ greeting: z.string() }),
15
- run: (input: { name: string }) =>
16
- Result.ok({ greeting: `Hello, ${input.name}` }),
17
- });
18
-
19
- const failTrail = trail('fail', {
20
- input: z.object({ id: z.string() }),
21
- run: (input: { id: string }) => {
22
- if (input.id === 'missing') {
23
- return Result.err(new NotFoundError('Not found: missing'));
24
- }
25
- return Result.ok({ id: input.id });
26
- },
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/follows.ts DELETED
@@ -1,283 +0,0 @@
1
- /**
2
- * testFollows — composition-aware scenario testing for trails with follow.
3
- *
4
- * Tests the follow 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 { 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 { FollowScenario } 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: FollowScenario,
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.run(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: FollowScenario,
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: FollowScenario,
152
- trailDef: AnyTrail
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, trailDef.output);
166
- }
167
- };
168
-
169
- const assertFollowTrace = (
170
- trace: readonly FollowRecord[],
171
- scenario: FollowScenario
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: FollowScenario
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: FollowScenario,
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
- trailDef: AnyTrail,
228
- scenario: FollowScenario,
229
- ctx: Partial<TrailContext> | undefined,
230
- trailsMap: ReadonlyMap<string, AnyTrail> | undefined
231
- ): Promise<void> => {
232
- const validated = validateInput(trailDef.input, scenario.input);
233
- if (handleValidationError(validated, scenario)) {
234
- return;
235
- }
236
-
237
- const { trace, testCtx } = buildTestContext(scenario, ctx, trailsMap);
238
- const result = await trailDef.run(expectOk(validated), testCtx);
239
- assertFollowTrace(trace, scenario);
240
- assertScenarioResult(result, scenario, trailDef);
241
- };
242
-
243
- // ---------------------------------------------------------------------------
244
- // testFollows
245
- // ---------------------------------------------------------------------------
246
-
247
- /** Options for testFollows that provide trail definitions for injection. */
248
- export interface TestFollowOptions {
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 composition trail with one test per scenario.
257
- *
258
- * @example
259
- * ```ts
260
- * testFollows(onboardTrail, [
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 testFollows = (
271
- trailDef: AnyTrail,
272
- scenarios: readonly FollowScenario[],
273
- options?: TestFollowOptions
274
- ): void => {
275
- describe(trailDef.id, () => {
276
- test.each([...scenarios])(
277
- '$description',
278
- async (scenario: FollowScenario) => {
279
- await runScenario(trailDef, 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/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"}