@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.
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +166 -0
- package/README.md +25 -7
- package/dist/all.d.ts +2 -1
- package/dist/all.d.ts.map +1 -1
- package/dist/all.js.map +1 -1
- package/dist/context.d.ts +28 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +70 -11
- package/dist/context.js.map +1 -1
- package/dist/contracts.d.ts +3 -2
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +25 -10
- package/dist/contracts.js.map +1 -1
- package/dist/examples.d.ts +4 -3
- package/dist/examples.d.ts.map +1 -1
- package/dist/examples.js +63 -68
- package/dist/examples.js.map +1 -1
- package/dist/follows.d.ts +38 -0
- package/dist/follows.d.ts.map +1 -0
- package/dist/{hike.js → follows.js} +71 -28
- package/dist/follows.js.map +1 -0
- package/dist/harness-mcp.d.ts.map +1 -1
- package/dist/harness-mcp.js +5 -2
- package/dist/harness-mcp.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/trail.js +1 -1
- package/dist/trail.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/all.test.ts +64 -0
- package/src/__tests__/context.test.ts +92 -2
- package/src/__tests__/contracts.test.ts +122 -5
- package/src/__tests__/detours.test.ts +3 -3
- package/src/__tests__/examples.test.ts +285 -22
- package/src/__tests__/follows.test.ts +589 -0
- package/src/__tests__/trail.test.ts +4 -4
- package/src/all.ts +5 -1
- package/src/context.ts +121 -13
- package/src/contracts.ts +43 -12
- package/src/examples.ts +111 -105
- package/src/{hike.ts → follows.ts} +138 -43
- package/src/harness-mcp.ts +5 -2
- package/src/index.ts +6 -4
- package/src/trail.ts +1 -1
- package/src/types.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/hike.d.ts +0 -32
- package/dist/hike.d.ts.map +0 -1
- package/dist/hike.js.map +0 -1
- package/src/__tests__/hike.test.ts +0 -164
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import { NotFoundError, Result,
|
|
3
|
+
import { NotFoundError, Result, service, trail, topo } from '@ontrails/core';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
|
|
6
6
|
import { testExamples } from '../examples.js';
|
|
@@ -18,10 +18,10 @@ const greetTrail = trail('greet', {
|
|
|
18
18
|
name: 'Greet Alice',
|
|
19
19
|
},
|
|
20
20
|
],
|
|
21
|
-
implementation: (input: { name: string }) =>
|
|
22
|
-
Result.ok({ greeting: `Hello, ${input.name}` }),
|
|
23
21
|
input: z.object({ name: z.string() }),
|
|
24
22
|
output: z.object({ greeting: z.string() }),
|
|
23
|
+
run: (input: { name: string }) =>
|
|
24
|
+
Result.ok({ greeting: `Hello, ${input.name}` }),
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
const searchTrail = trail('search', {
|
|
@@ -32,10 +32,10 @@ const searchTrail = trail('search', {
|
|
|
32
32
|
name: 'Schema-only search',
|
|
33
33
|
},
|
|
34
34
|
],
|
|
35
|
-
implementation: (input: { query: string }) =>
|
|
36
|
-
Result.ok({ results: [`result for ${input.query}`] }),
|
|
37
35
|
input: z.object({ query: z.string() }),
|
|
38
36
|
output: z.object({ results: z.array(z.string()) }),
|
|
37
|
+
run: (input: { query: string }) =>
|
|
38
|
+
Result.ok({ results: [`result for ${input.query}`] }),
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
const entityTrail = trail('entity.show', {
|
|
@@ -52,42 +52,171 @@ const entityTrail = trail('entity.show', {
|
|
|
52
52
|
name: 'Entity not found returns NotFoundError',
|
|
53
53
|
},
|
|
54
54
|
],
|
|
55
|
-
|
|
55
|
+
input: z.object({ name: z.string() }),
|
|
56
|
+
output: z.object({ id: z.number(), name: z.string() }),
|
|
57
|
+
run: (input: { name: string }) => {
|
|
56
58
|
if (input.name === 'missing') {
|
|
57
59
|
return Result.err(new NotFoundError('Entity not found'));
|
|
58
60
|
}
|
|
59
61
|
return Result.ok({ id: 1, name: input.name });
|
|
60
62
|
},
|
|
61
|
-
input: z.object({ name: z.string() }),
|
|
62
|
-
output: z.object({ id: z.number(), name: z.string() }),
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
const noExamplesTrail = trail('noexamples', {
|
|
66
|
-
implementation: (input: { x: number }) => Result.ok(input.x * 2),
|
|
67
66
|
input: z.object({ x: z.number() }),
|
|
67
|
+
run: (input: { x: number }) => Result.ok(input.x * 2),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const mockDbService = service('db.mock.examples', {
|
|
71
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
72
|
+
mock: () => ({ source: 'mock' }),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const mockServiceTrail = trail('service.mocked', {
|
|
76
|
+
description: 'Trail that reads from a mocked service',
|
|
77
|
+
examples: [
|
|
78
|
+
{
|
|
79
|
+
expected: { source: 'mock' },
|
|
80
|
+
input: {},
|
|
81
|
+
name: 'Uses auto-resolved service mock',
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
input: z.object({}),
|
|
85
|
+
output: z.object({ source: z.string() }),
|
|
86
|
+
run: (_input, ctx) => Result.ok({ source: mockDbService.from(ctx).source }),
|
|
87
|
+
services: [mockDbService],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const explicitOverrideTrail = trail('service.override', {
|
|
91
|
+
description: 'Trail whose service mock can be overridden explicitly',
|
|
92
|
+
examples: [
|
|
93
|
+
{
|
|
94
|
+
expected: { source: 'override' },
|
|
95
|
+
input: {},
|
|
96
|
+
name: 'Explicit service override wins over mock factory',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
input: z.object({}),
|
|
100
|
+
output: z.object({ source: z.string() }),
|
|
101
|
+
run: (_input, ctx) => Result.ok({ source: mockDbService.from(ctx).source }),
|
|
102
|
+
services: [mockDbService],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const transformedInputTrail = trail('example.transformed', {
|
|
106
|
+
description: 'Trail whose input schema transforms once',
|
|
107
|
+
examples: [
|
|
108
|
+
{
|
|
109
|
+
expected: { value: 2 },
|
|
110
|
+
input: { value: '1' },
|
|
111
|
+
name: 'Raw example input is only transformed once',
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
input: z
|
|
115
|
+
.object({ value: z.string() })
|
|
116
|
+
.transform(({ value }) => ({ value: Number(value) + 1 })),
|
|
117
|
+
output: z.object({ value: z.number() }),
|
|
118
|
+
run: (input: { value: number }) => Result.ok({ value: input.value }),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const ctxOverrideTrail = trail('service.ctx-override', {
|
|
122
|
+
description: 'Trail whose service mock can be overridden by ctx.extensions',
|
|
123
|
+
examples: [
|
|
124
|
+
{
|
|
125
|
+
expected: { source: 'ctx' },
|
|
126
|
+
input: {},
|
|
127
|
+
name: 'Context extensions beat auto-resolved mock services',
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
input: z.object({}),
|
|
131
|
+
output: z.object({ source: z.string() }),
|
|
132
|
+
run: (_input, ctx) => Result.ok({ source: mockDbService.from(ctx).source }),
|
|
133
|
+
services: [mockDbService],
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const undeclaredDbService = service('db.undeclared.examples', {
|
|
137
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
138
|
+
mock: () => ({ source: 'mock' }),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const undeclaredServiceTrail = trail('service.undeclared.examples', {
|
|
142
|
+
description: 'Trail that uses a service without declaring it',
|
|
143
|
+
examples: [
|
|
144
|
+
{
|
|
145
|
+
error: 'InternalError',
|
|
146
|
+
input: {},
|
|
147
|
+
name: 'Undeclared services stay unavailable during example execution',
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
input: z.object({}),
|
|
151
|
+
output: z.object({ source: z.string() }),
|
|
152
|
+
run: (_input, ctx) =>
|
|
153
|
+
Result.ok({ source: undeclaredDbService.from(ctx).source }),
|
|
154
|
+
});
|
|
155
|
+
const followedDbService = service('db.mock.examples.follow', {
|
|
156
|
+
create: () => Result.ok({ source: 'factory' }),
|
|
157
|
+
mock: () => ({ source: 'mock' }),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const followedLeafTrail = trail('service.follow.leaf', {
|
|
161
|
+
description: 'Leaf trail that resolves a service inside a follow chain',
|
|
162
|
+
input: z.object({}),
|
|
163
|
+
output: z.object({ childSource: z.string() }),
|
|
164
|
+
run: (_input, ctx) =>
|
|
165
|
+
Result.ok({ childSource: followedDbService.from(ctx).source }),
|
|
166
|
+
services: [followedDbService],
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const followedRootTrail = trail('service.follow.root', {
|
|
170
|
+
description: 'Root trail that follows a child trail using services',
|
|
171
|
+
examples: [
|
|
172
|
+
{
|
|
173
|
+
expected: { childSource: 'mock', rootSource: 'mock' },
|
|
174
|
+
input: {},
|
|
175
|
+
name: 'Propagates service mocks through follow execution',
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
follow: ['service.follow.leaf'],
|
|
179
|
+
input: z.object({}),
|
|
180
|
+
output: z.object({ childSource: z.string(), rootSource: z.string() }),
|
|
181
|
+
run: async (_input, ctx) => {
|
|
182
|
+
if (!ctx.follow) {
|
|
183
|
+
return Result.err(new Error('follow not available'));
|
|
184
|
+
}
|
|
185
|
+
const childResult = await ctx.follow<{ childSource: string }>(
|
|
186
|
+
'service.follow.leaf',
|
|
187
|
+
{}
|
|
188
|
+
);
|
|
189
|
+
if (childResult.isErr()) {
|
|
190
|
+
return childResult;
|
|
191
|
+
}
|
|
192
|
+
return Result.ok({
|
|
193
|
+
childSource: childResult.value.childSource,
|
|
194
|
+
rootSource: followedDbService.from(ctx).source,
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
services: [followedDbService],
|
|
68
198
|
});
|
|
69
199
|
|
|
70
200
|
// ---------------------------------------------------------------------------
|
|
71
|
-
//
|
|
201
|
+
// Composition trails (for follow coverage)
|
|
72
202
|
// ---------------------------------------------------------------------------
|
|
73
203
|
|
|
74
204
|
const addTrail = trail('entity.add', {
|
|
75
205
|
description: 'Add an entity',
|
|
76
|
-
implementation: (input: { name: string }) =>
|
|
77
|
-
Result.ok({ id: '1', name: input.name }),
|
|
78
206
|
input: z.object({ name: z.string() }),
|
|
79
207
|
output: z.object({ id: z.string(), name: z.string() }),
|
|
208
|
+
run: (input: { name: string }) => Result.ok({ id: '1', name: input.name }),
|
|
80
209
|
});
|
|
81
210
|
|
|
82
211
|
const relateTrail = trail('entity.relate', {
|
|
83
212
|
description: 'Relate entities',
|
|
84
|
-
implementation: (input: { from: string; to: string }) =>
|
|
85
|
-
Result.ok({ from: input.from, to: input.to }),
|
|
86
213
|
input: z.object({ from: z.string(), to: z.string() }),
|
|
87
214
|
output: z.object({ from: z.string(), to: z.string() }),
|
|
215
|
+
run: (input: { from: string; to: string }) =>
|
|
216
|
+
Result.ok({ from: input.from, to: input.to }),
|
|
88
217
|
});
|
|
89
218
|
|
|
90
|
-
const
|
|
219
|
+
const onboardTrail = trail('entity.onboard', {
|
|
91
220
|
description: 'Onboard a new entity',
|
|
92
221
|
examples: [
|
|
93
222
|
{
|
|
@@ -96,8 +225,10 @@ const onboardHike = hike('entity.onboard', {
|
|
|
96
225
|
name: 'Onboard Alpha',
|
|
97
226
|
},
|
|
98
227
|
],
|
|
99
|
-
|
|
100
|
-
|
|
228
|
+
follow: ['entity.add', 'entity.relate'],
|
|
229
|
+
input: z.object({ name: z.string() }),
|
|
230
|
+
output: z.object({ id: z.string(), name: z.string() }),
|
|
231
|
+
run: async (input: { name: string }, ctx) => {
|
|
101
232
|
if (!ctx.follow) {
|
|
102
233
|
return Result.err(new Error('follow not available'));
|
|
103
234
|
}
|
|
@@ -114,8 +245,6 @@ const onboardHike = hike('entity.onboard', {
|
|
|
114
245
|
}
|
|
115
246
|
return Result.ok({ id: '1', name: input.name });
|
|
116
247
|
},
|
|
117
|
-
input: z.object({ name: z.string() }),
|
|
118
|
-
output: z.object({ id: z.string(), name: z.string() }),
|
|
119
248
|
});
|
|
120
249
|
|
|
121
250
|
// ---------------------------------------------------------------------------
|
|
@@ -164,13 +293,147 @@ describe('testExamples skips trails with no examples', () => {
|
|
|
164
293
|
});
|
|
165
294
|
});
|
|
166
295
|
|
|
167
|
-
describe('testExamples
|
|
296
|
+
describe('testExamples service mocks', () => {
|
|
297
|
+
// eslint-disable-next-line jest/require-hook
|
|
298
|
+
testExamples(
|
|
299
|
+
topo('service-mock-app', {
|
|
300
|
+
mockDbService,
|
|
301
|
+
mockServiceTrail,
|
|
302
|
+
} as Record<string, unknown>)
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('testExamples explicit service overrides', () => {
|
|
307
|
+
// eslint-disable-next-line jest/require-hook
|
|
308
|
+
testExamples(
|
|
309
|
+
topo('service-override-app', {
|
|
310
|
+
explicitOverrideTrail,
|
|
311
|
+
mockDbService,
|
|
312
|
+
} as Record<string, unknown>),
|
|
313
|
+
{
|
|
314
|
+
services: { 'db.mock.examples': { source: 'override' } },
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe('testExamples raw transformed input', () => {
|
|
320
|
+
// eslint-disable-next-line jest/require-hook
|
|
321
|
+
testExamples(
|
|
322
|
+
topo('transformed-input-app', {
|
|
323
|
+
transformedInputTrail,
|
|
324
|
+
} as Record<string, unknown>)
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('testExamples context extension overrides', () => {
|
|
168
329
|
// eslint-disable-next-line jest/require-hook
|
|
169
330
|
testExamples(
|
|
170
|
-
topo('
|
|
331
|
+
topo('ctx-override-app', {
|
|
332
|
+
ctxOverrideTrail,
|
|
333
|
+
mockDbService,
|
|
334
|
+
} as Record<string, unknown>),
|
|
335
|
+
{
|
|
336
|
+
ctx: {
|
|
337
|
+
extensions: { 'db.mock.examples': { source: 'ctx' } },
|
|
338
|
+
},
|
|
339
|
+
}
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe('testExamples service declarations', () => {
|
|
344
|
+
// eslint-disable-next-line jest/require-hook
|
|
345
|
+
testExamples(
|
|
346
|
+
topo('undeclared-service-app', {
|
|
347
|
+
undeclaredDbService,
|
|
348
|
+
undeclaredServiceTrail,
|
|
349
|
+
} as Record<string, unknown>)
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe('testExamples follow coverage for composition trails', () => {
|
|
354
|
+
// eslint-disable-next-line jest/require-hook
|
|
355
|
+
testExamples(
|
|
356
|
+
topo('composition-app', {
|
|
171
357
|
addTrail,
|
|
172
|
-
|
|
358
|
+
onboardTrail,
|
|
173
359
|
relateTrail,
|
|
174
360
|
} as Record<string, unknown>)
|
|
175
361
|
);
|
|
176
362
|
});
|
|
363
|
+
|
|
364
|
+
describe('testExamples service mocks through follow', () => {
|
|
365
|
+
// eslint-disable-next-line jest/require-hook
|
|
366
|
+
testExamples(
|
|
367
|
+
topo('service-follow-app', {
|
|
368
|
+
followedDbService,
|
|
369
|
+
followedLeafTrail,
|
|
370
|
+
followedRootTrail,
|
|
371
|
+
} as Record<string, unknown>)
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// Nested follow chain: A → B → C
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
const leafTrail = trail('step.leaf', {
|
|
380
|
+
description: 'Leaf trail in a nested chain',
|
|
381
|
+
input: z.object({ value: z.string() }),
|
|
382
|
+
output: z.object({ leaf: z.string() }),
|
|
383
|
+
run: (input: { value: string }) => Result.ok({ leaf: input.value }),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const middleTrail = trail('step.middle', {
|
|
387
|
+
description: 'Middle trail that follows the leaf',
|
|
388
|
+
follow: ['step.leaf'],
|
|
389
|
+
input: z.object({ value: z.string() }),
|
|
390
|
+
output: z.object({ middle: z.string() }),
|
|
391
|
+
run: async (input: { value: string }, ctx) => {
|
|
392
|
+
if (!ctx.follow) {
|
|
393
|
+
return Result.err(new Error('follow not available'));
|
|
394
|
+
}
|
|
395
|
+
const leafResult = await ctx.follow<{ leaf: string }>('step.leaf', input);
|
|
396
|
+
if (leafResult.isErr()) {
|
|
397
|
+
return leafResult;
|
|
398
|
+
}
|
|
399
|
+
return Result.ok({ middle: leafResult.value.leaf });
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const rootTrail = trail('step.root', {
|
|
404
|
+
description: 'Root trail that follows the middle trail',
|
|
405
|
+
examples: [
|
|
406
|
+
{
|
|
407
|
+
expected: { root: 'hello' },
|
|
408
|
+
input: { value: 'hello' },
|
|
409
|
+
name: 'Nested follow chain A→B→C',
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
follow: ['step.middle'],
|
|
413
|
+
input: z.object({ value: z.string() }),
|
|
414
|
+
output: z.object({ root: z.string() }),
|
|
415
|
+
run: async (input: { value: string }, ctx) => {
|
|
416
|
+
if (!ctx.follow) {
|
|
417
|
+
return Result.err(new Error('follow not available'));
|
|
418
|
+
}
|
|
419
|
+
const midResult = await ctx.follow<{ middle: string }>(
|
|
420
|
+
'step.middle',
|
|
421
|
+
input
|
|
422
|
+
);
|
|
423
|
+
if (midResult.isErr()) {
|
|
424
|
+
return midResult;
|
|
425
|
+
}
|
|
426
|
+
return Result.ok({ root: midResult.value.middle });
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe('testExamples nested follow chain (A → B → C)', () => {
|
|
431
|
+
// eslint-disable-next-line jest/require-hook
|
|
432
|
+
testExamples(
|
|
433
|
+
topo('nested-chain-app', {
|
|
434
|
+
leafTrail,
|
|
435
|
+
middleTrail,
|
|
436
|
+
rootTrail,
|
|
437
|
+
} as Record<string, unknown>)
|
|
438
|
+
);
|
|
439
|
+
});
|