@ontrails/testing 1.0.0-beta.12 → 1.0.0-beta.13
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/CHANGELOG.md +58 -27
- package/README.md +9 -9
- package/dist/context.d.ts +11 -11
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +23 -21
- package/dist/context.js.map +1 -1
- package/dist/contracts.js +8 -8
- package/dist/contracts.js.map +1 -1
- package/dist/crosses.d.ts +38 -0
- package/dist/crosses.d.ts.map +1 -0
- package/dist/crosses.js +213 -0
- package/dist/crosses.js.map +1 -0
- package/dist/examples.d.ts +4 -4
- package/dist/examples.js +31 -31
- package/dist/examples.js.map +1 -1
- package/dist/harness-mcp.d.ts +1 -1
- package/dist/harness-mcp.js +2 -2
- package/dist/harness-mcp.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- 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 +8 -8
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/all.test.ts +20 -18
- package/src/__tests__/context.test.ts +27 -27
- package/src/__tests__/contracts.test.ts +29 -29
- package/src/__tests__/{follows.test.ts → crosses.test.ts} +193 -195
- package/src/__tests__/detours.test.ts +3 -3
- package/src/__tests__/examples.test.ts +145 -144
- package/src/__tests__/trail.test.ts +4 -4
- package/src/context.ts +36 -32
- package/src/contracts.ts +12 -12
- package/src/{follows.ts → crosses.ts} +97 -92
- package/src/examples.ts +43 -43
- package/src/harness-mcp.ts +2 -2
- package/src/index.ts +6 -6
- package/src/trail.ts +1 -1
- package/src/types.ts +9 -9
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { describe } from 'bun:test';
|
|
2
2
|
|
|
3
3
|
import type { AnyTrail, TrailContext } from '@ontrails/core';
|
|
4
|
-
import { InternalError, Result,
|
|
4
|
+
import { InternalError, Result, provision, trail } from '@ontrails/core';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { testCrosses } from '../crosses.js';
|
|
8
8
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
-
// Test trails (
|
|
10
|
+
// Test trails (trails with crossings)
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
|
|
13
13
|
const addTrail = trail('entity.add', {
|
|
14
|
+
blaze: (input: { name: string }) => Result.ok({ id: '1', name: input.name }),
|
|
14
15
|
description: 'Add an entity',
|
|
15
16
|
examples: [
|
|
16
17
|
{ input: { name: 'Alpha' }, name: 'success' },
|
|
@@ -23,15 +24,14 @@ const addTrail = trail('entity.add', {
|
|
|
23
24
|
],
|
|
24
25
|
input: z.object({ name: z.string() }),
|
|
25
26
|
output: z.object({ id: z.string(), name: z.string() }),
|
|
26
|
-
run: (input: { name: string }) => Result.ok({ id: '1', name: input.name }),
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
const relateTrail = trail('entity.relate', {
|
|
30
|
+
blaze: (input: { from: string; to: string }) =>
|
|
31
|
+
Result.ok({ from: input.from, to: input.to }),
|
|
30
32
|
description: 'Relate two entities',
|
|
31
33
|
input: z.object({ from: z.string(), to: z.string() }),
|
|
32
34
|
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
35
|
});
|
|
36
36
|
|
|
37
37
|
// ---------------------------------------------------------------------------
|
|
@@ -39,17 +39,14 @@ const relateTrail = trail('entity.relate', {
|
|
|
39
39
|
// ---------------------------------------------------------------------------
|
|
40
40
|
|
|
41
41
|
const onboardTrail = trail('entity.onboard', {
|
|
42
|
-
|
|
43
|
-
input: z.object({ name: z.string(), relatedTo: z.string() }),
|
|
44
|
-
output: z.object({ name: z.string(), relatedTo: z.string() }),
|
|
45
|
-
run: async (
|
|
42
|
+
blaze: async (
|
|
46
43
|
input: { name: string; relatedTo: string },
|
|
47
44
|
ctx: TrailContext
|
|
48
45
|
) => {
|
|
49
|
-
if (!ctx.
|
|
50
|
-
return Result.err(new Error('
|
|
46
|
+
if (!ctx.cross) {
|
|
47
|
+
return Result.err(new Error('cross not available'));
|
|
51
48
|
}
|
|
52
|
-
const addResult = await ctx.
|
|
49
|
+
const addResult = await ctx.cross<{ id: string; name: string }>(
|
|
53
50
|
'entity.add',
|
|
54
51
|
{ name: input.name }
|
|
55
52
|
);
|
|
@@ -57,7 +54,7 @@ const onboardTrail = trail('entity.onboard', {
|
|
|
57
54
|
return Result.err(addResult.error);
|
|
58
55
|
}
|
|
59
56
|
|
|
60
|
-
const relateResult = await ctx.
|
|
57
|
+
const relateResult = await ctx.cross<{ from: string; to: string }>(
|
|
61
58
|
'entity.relate',
|
|
62
59
|
{ from: addResult.value.name, to: input.relatedTo }
|
|
63
60
|
);
|
|
@@ -70,6 +67,9 @@ const onboardTrail = trail('entity.onboard', {
|
|
|
70
67
|
relatedTo: relateResult.value.to,
|
|
71
68
|
});
|
|
72
69
|
},
|
|
70
|
+
crosses: ['entity.add', 'entity.relate'],
|
|
71
|
+
input: z.object({ name: z.string(), relatedTo: z.string() }),
|
|
72
|
+
output: z.object({ name: z.string(), relatedTo: z.string() }),
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
const trailsMap = new Map<string, AnyTrail>([
|
|
@@ -83,9 +83,9 @@ const trailsMap = new Map<string, AnyTrail>([
|
|
|
83
83
|
|
|
84
84
|
const opts = { trails: trailsMap };
|
|
85
85
|
|
|
86
|
-
describe('
|
|
86
|
+
describe('testCrosses: expectOk', () => {
|
|
87
87
|
// eslint-disable-next-line jest/require-hook
|
|
88
|
-
|
|
88
|
+
testCrosses(
|
|
89
89
|
onboardTrail,
|
|
90
90
|
[
|
|
91
91
|
{
|
|
@@ -98,14 +98,14 @@ describe('testFollows: expectOk', () => {
|
|
|
98
98
|
);
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
describe('
|
|
101
|
+
describe('testCrosses: expectCrossed', () => {
|
|
102
102
|
// eslint-disable-next-line jest/require-hook
|
|
103
|
-
|
|
103
|
+
testCrosses(
|
|
104
104
|
onboardTrail,
|
|
105
105
|
[
|
|
106
106
|
{
|
|
107
|
-
description: '
|
|
108
|
-
|
|
107
|
+
description: 'crosses add then relate in order',
|
|
108
|
+
expectCrossed: ['entity.add', 'entity.relate'],
|
|
109
109
|
expectOk: true,
|
|
110
110
|
input: { name: 'Alpha', relatedTo: 'Beta' },
|
|
111
111
|
},
|
|
@@ -114,14 +114,14 @@ describe('testFollows: expectFollowed', () => {
|
|
|
114
114
|
);
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
-
describe('
|
|
117
|
+
describe('testCrosses: expectCrossedCount', () => {
|
|
118
118
|
// eslint-disable-next-line jest/require-hook
|
|
119
|
-
|
|
119
|
+
testCrosses(
|
|
120
120
|
onboardTrail,
|
|
121
121
|
[
|
|
122
122
|
{
|
|
123
|
-
description: 'each trail
|
|
124
|
-
|
|
123
|
+
description: 'each trail crossed exactly once',
|
|
124
|
+
expectCrossedCount: { 'entity.add': 1, 'entity.relate': 1 },
|
|
125
125
|
expectOk: true,
|
|
126
126
|
input: { name: 'Alpha', relatedTo: 'Beta' },
|
|
127
127
|
},
|
|
@@ -130,9 +130,9 @@ describe('testFollows: expectFollowedCount', () => {
|
|
|
130
130
|
);
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
describe('
|
|
133
|
+
describe('testCrosses: injectFromExample', () => {
|
|
134
134
|
// eslint-disable-next-line jest/require-hook
|
|
135
|
-
|
|
135
|
+
testCrosses(
|
|
136
136
|
onboardTrail,
|
|
137
137
|
[
|
|
138
138
|
{
|
|
@@ -147,9 +147,9 @@ describe('testFollows: injectFromExample', () => {
|
|
|
147
147
|
);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
-
describe('
|
|
150
|
+
describe('testCrosses: expectValue', () => {
|
|
151
151
|
// eslint-disable-next-line jest/require-hook
|
|
152
|
-
|
|
152
|
+
testCrosses(
|
|
153
153
|
onboardTrail,
|
|
154
154
|
[
|
|
155
155
|
{
|
|
@@ -163,51 +163,48 @@ describe('testFollows: expectValue', () => {
|
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
// ---------------------------------------------------------------------------
|
|
166
|
-
// Nested
|
|
166
|
+
// Nested cross chain: A → B → C
|
|
167
167
|
// ---------------------------------------------------------------------------
|
|
168
168
|
|
|
169
169
|
const leafTrail = trail('step.leaf', {
|
|
170
|
+
blaze: (input: { value: string }) => Result.ok({ leaf: input.value }),
|
|
170
171
|
description: 'Leaf trail in a nested chain',
|
|
171
172
|
input: z.object({ value: z.string() }),
|
|
172
173
|
output: z.object({ leaf: z.string() }),
|
|
173
|
-
run: (input: { value: string }) => Result.ok({ leaf: input.value }),
|
|
174
174
|
});
|
|
175
175
|
|
|
176
176
|
const middleTrail = trail('step.middle', {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
output: z.object({ middle: z.string() }),
|
|
181
|
-
run: async (input: { value: string }, ctx: TrailContext) => {
|
|
182
|
-
if (!ctx.follow) {
|
|
183
|
-
return Result.err(new Error('follow not available'));
|
|
177
|
+
blaze: async (input: { value: string }, ctx: TrailContext) => {
|
|
178
|
+
if (!ctx.cross) {
|
|
179
|
+
return Result.err(new Error('cross not available'));
|
|
184
180
|
}
|
|
185
|
-
const leafResult = await ctx.
|
|
181
|
+
const leafResult = await ctx.cross<{ leaf: string }>('step.leaf', input);
|
|
186
182
|
if (leafResult.isErr()) {
|
|
187
183
|
return leafResult;
|
|
188
184
|
}
|
|
189
185
|
return Result.ok({ middle: leafResult.value.leaf });
|
|
190
186
|
},
|
|
187
|
+
crosses: ['step.leaf'],
|
|
188
|
+
description: 'Middle trail that crosses the leaf',
|
|
189
|
+
input: z.object({ value: z.string() }),
|
|
190
|
+
output: z.object({ middle: z.string() }),
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
const nestedRootTrail = trail('step.root', {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
output: z.object({ root: z.string() }),
|
|
198
|
-
run: async (input: { value: string }, ctx: TrailContext) => {
|
|
199
|
-
if (!ctx.follow) {
|
|
200
|
-
return Result.err(new Error('follow not available'));
|
|
194
|
+
blaze: async (input: { value: string }, ctx: TrailContext) => {
|
|
195
|
+
if (!ctx.cross) {
|
|
196
|
+
return Result.err(new Error('cross not available'));
|
|
201
197
|
}
|
|
202
|
-
const midResult = await ctx.
|
|
203
|
-
'step.middle',
|
|
204
|
-
input
|
|
205
|
-
);
|
|
198
|
+
const midResult = await ctx.cross<{ middle: string }>('step.middle', input);
|
|
206
199
|
if (midResult.isErr()) {
|
|
207
200
|
return midResult;
|
|
208
201
|
}
|
|
209
202
|
return Result.ok({ root: midResult.value.middle });
|
|
210
203
|
},
|
|
204
|
+
crosses: ['step.middle'],
|
|
205
|
+
description: 'Root trail that crosses the middle trail',
|
|
206
|
+
input: z.object({ value: z.string() }),
|
|
207
|
+
output: z.object({ root: z.string() }),
|
|
211
208
|
});
|
|
212
209
|
|
|
213
210
|
const nestedTrailsMap = new Map<string, AnyTrail>([
|
|
@@ -215,13 +212,13 @@ const nestedTrailsMap = new Map<string, AnyTrail>([
|
|
|
215
212
|
['step.middle', middleTrail],
|
|
216
213
|
]);
|
|
217
214
|
|
|
218
|
-
describe('
|
|
215
|
+
describe('testCrosses: nested cross chain (A → B → C)', () => {
|
|
219
216
|
// eslint-disable-next-line jest/require-hook
|
|
220
|
-
|
|
217
|
+
testCrosses(
|
|
221
218
|
nestedRootTrail,
|
|
222
219
|
[
|
|
223
220
|
{
|
|
224
|
-
description: 'nested ctx.
|
|
221
|
+
description: 'nested ctx.cross works through A → B → C',
|
|
225
222
|
expectOk: true,
|
|
226
223
|
expectValue: { root: 'hello' },
|
|
227
224
|
input: { value: 'hello' },
|
|
@@ -231,31 +228,27 @@ describe('testFollows: nested follow chain (A → B → C)', () => {
|
|
|
231
228
|
);
|
|
232
229
|
});
|
|
233
230
|
|
|
234
|
-
const
|
|
231
|
+
const mockDbProvision = provision('db.mock.crosses', {
|
|
235
232
|
create: () => Result.ok({ source: 'factory' }),
|
|
236
233
|
mock: () => ({ source: 'mock' }),
|
|
237
234
|
});
|
|
238
235
|
|
|
239
|
-
const
|
|
240
|
-
|
|
236
|
+
const provisionLeafTrail = trail('provision.leaf', {
|
|
237
|
+
blaze: (_input, ctx) =>
|
|
238
|
+
Result.ok({ childSource: mockDbProvision.from(ctx).source }),
|
|
239
|
+
description: 'Leaf trail that reads from a provision',
|
|
241
240
|
input: z.object({}),
|
|
242
241
|
output: z.object({ childSource: z.string() }),
|
|
243
|
-
|
|
244
|
-
Result.ok({ childSource: mockDbService.from(ctx).source }),
|
|
245
|
-
services: [mockDbService],
|
|
242
|
+
provisions: [mockDbProvision],
|
|
246
243
|
});
|
|
247
244
|
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
output: z.object({ childSource: z.string(), rootSource: z.string() }),
|
|
253
|
-
run: async (_input, ctx: TrailContext) => {
|
|
254
|
-
if (!ctx.follow) {
|
|
255
|
-
return Result.err(new Error('follow not available'));
|
|
245
|
+
const provisionRootTrail = trail('provision.root', {
|
|
246
|
+
blaze: async (_input, ctx: TrailContext) => {
|
|
247
|
+
if (!ctx.cross) {
|
|
248
|
+
return Result.err(new Error('cross not available'));
|
|
256
249
|
}
|
|
257
|
-
const childResult = await ctx.
|
|
258
|
-
'
|
|
250
|
+
const childResult = await ctx.cross<{ childSource: string }>(
|
|
251
|
+
'provision.leaf',
|
|
259
252
|
{}
|
|
260
253
|
);
|
|
261
254
|
if (childResult.isErr()) {
|
|
@@ -264,46 +257,46 @@ const serviceRootTrail = trail('service.root', {
|
|
|
264
257
|
|
|
265
258
|
return Result.ok({
|
|
266
259
|
childSource: childResult.value.childSource,
|
|
267
|
-
rootSource:
|
|
260
|
+
rootSource: mockDbProvision.from(ctx).source,
|
|
268
261
|
});
|
|
269
262
|
},
|
|
270
|
-
|
|
263
|
+
crosses: ['provision.leaf'],
|
|
264
|
+
description:
|
|
265
|
+
'Root trail that reads from a provision and crosses a child trail',
|
|
266
|
+
input: z.object({}),
|
|
267
|
+
output: z.object({ childSource: z.string(), rootSource: z.string() }),
|
|
268
|
+
provisions: [mockDbProvision],
|
|
271
269
|
});
|
|
272
270
|
|
|
273
|
-
const
|
|
274
|
-
['
|
|
271
|
+
const provisionTrailsMap = new Map<string, AnyTrail>([
|
|
272
|
+
['provision.leaf', provisionLeafTrail],
|
|
275
273
|
]);
|
|
276
274
|
|
|
277
|
-
const
|
|
275
|
+
const statefulMockDbProvision = provision('db.mock.crosses.stateful', {
|
|
278
276
|
create: () => Result.ok({ count: 0 }),
|
|
279
277
|
mock: () => ({ count: 0 }),
|
|
280
278
|
});
|
|
281
279
|
|
|
282
|
-
const
|
|
283
|
-
|
|
280
|
+
const statefulProvisionLeafTrail = trail('provision.stateful.leaf', {
|
|
281
|
+
blaze: (_input, ctx) =>
|
|
282
|
+
Result.ok({ childCount: statefulMockDbProvision.from(ctx).count }),
|
|
283
|
+
description: 'Leaf trail that observes the current mock provision state',
|
|
284
284
|
input: z.object({}),
|
|
285
285
|
output: z.object({ childCount: z.number() }),
|
|
286
|
-
|
|
287
|
-
Result.ok({ childCount: statefulMockDbService.from(ctx).count }),
|
|
288
|
-
services: [statefulMockDbService],
|
|
286
|
+
provisions: [statefulMockDbProvision],
|
|
289
287
|
});
|
|
290
288
|
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
input: z.object({}),
|
|
296
|
-
output: z.object({ childCount: z.number(), rootCount: z.number() }),
|
|
297
|
-
run: async (_input, ctx: TrailContext) => {
|
|
298
|
-
if (!ctx.follow) {
|
|
299
|
-
return Result.err(new Error('follow not available'));
|
|
289
|
+
const statefulProvisionRootTrail = trail('provision.stateful.root', {
|
|
290
|
+
blaze: async (_input, ctx: TrailContext) => {
|
|
291
|
+
if (!ctx.cross) {
|
|
292
|
+
return Result.err(new Error('cross not available'));
|
|
300
293
|
}
|
|
301
294
|
|
|
302
|
-
const
|
|
303
|
-
|
|
295
|
+
const statefulProvision = statefulMockDbProvision.from(ctx);
|
|
296
|
+
statefulProvision.count += 1;
|
|
304
297
|
|
|
305
|
-
const childResult = await ctx.
|
|
306
|
-
'
|
|
298
|
+
const childResult = await ctx.cross<{ childCount: number }>(
|
|
299
|
+
'provision.stateful.leaf',
|
|
307
300
|
{}
|
|
308
301
|
);
|
|
309
302
|
if (childResult.isErr()) {
|
|
@@ -312,45 +305,46 @@ const statefulServiceRootTrail = trail('service.stateful.root', {
|
|
|
312
305
|
|
|
313
306
|
return Result.ok({
|
|
314
307
|
childCount: childResult.value.childCount,
|
|
315
|
-
rootCount:
|
|
308
|
+
rootCount: statefulProvision.count,
|
|
316
309
|
});
|
|
317
310
|
},
|
|
318
|
-
|
|
311
|
+
crosses: ['provision.stateful.leaf'],
|
|
312
|
+
description:
|
|
313
|
+
'Root trail that mutates a mocked provision and crosses a child trail',
|
|
314
|
+
input: z.object({}),
|
|
315
|
+
output: z.object({ childCount: z.number(), rootCount: z.number() }),
|
|
316
|
+
provisions: [statefulMockDbProvision],
|
|
319
317
|
});
|
|
320
318
|
|
|
321
|
-
const
|
|
322
|
-
['
|
|
319
|
+
const statefulProvisionTrailsMap = new Map<string, AnyTrail>([
|
|
320
|
+
['provision.stateful.leaf', statefulProvisionLeafTrail],
|
|
323
321
|
]);
|
|
324
322
|
|
|
325
|
-
const
|
|
323
|
+
const scenarioStateProvision = provision('db.mock.scenarios', {
|
|
326
324
|
create: () => Result.ok({ count: 0 }),
|
|
327
325
|
mock: () => ({ count: 0 }),
|
|
328
326
|
});
|
|
329
327
|
|
|
330
|
-
const scenarioLeafTrail = trail('
|
|
328
|
+
const scenarioLeafTrail = trail('provision.scenario.leaf', {
|
|
329
|
+
blaze: (_input, ctx) =>
|
|
330
|
+
Result.ok({ count: scenarioStateProvision.from(ctx).count }),
|
|
331
331
|
description: 'Leaf trail that reads mutable scenario state',
|
|
332
332
|
input: z.object({}),
|
|
333
333
|
output: z.object({ count: z.number() }),
|
|
334
|
-
|
|
335
|
-
Result.ok({ count: scenarioStateService.from(ctx).count }),
|
|
336
|
-
services: [scenarioStateService],
|
|
334
|
+
provisions: [scenarioStateProvision],
|
|
337
335
|
});
|
|
338
336
|
|
|
339
|
-
const scenarioRootTrail = trail('
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
output: z.object({ count: z.number() }),
|
|
344
|
-
run: async (_input, ctx: TrailContext) => {
|
|
345
|
-
if (!ctx.follow) {
|
|
346
|
-
return Result.err(new Error('follow not available'));
|
|
337
|
+
const scenarioRootTrail = trail('provision.scenario.root', {
|
|
338
|
+
blaze: async (_input, ctx: TrailContext) => {
|
|
339
|
+
if (!ctx.cross) {
|
|
340
|
+
return Result.err(new Error('cross not available'));
|
|
347
341
|
}
|
|
348
342
|
|
|
349
|
-
const state =
|
|
343
|
+
const state = scenarioStateProvision.from(ctx);
|
|
350
344
|
state.count += 1;
|
|
351
345
|
|
|
352
|
-
const leafResult = await ctx.
|
|
353
|
-
'
|
|
346
|
+
const leafResult = await ctx.cross<{ count: number }>(
|
|
347
|
+
'provision.scenario.leaf',
|
|
354
348
|
{}
|
|
355
349
|
);
|
|
356
350
|
if (leafResult.isErr()) {
|
|
@@ -359,34 +353,32 @@ const scenarioRootTrail = trail('service.scenario.root', {
|
|
|
359
353
|
|
|
360
354
|
return Result.ok({ count: leafResult.value.count });
|
|
361
355
|
},
|
|
362
|
-
|
|
356
|
+
crosses: ['provision.scenario.leaf'],
|
|
357
|
+
description: 'Root trail that mutates scenario state and crosses a leaf',
|
|
358
|
+
input: z.object({}),
|
|
359
|
+
output: z.object({ count: z.number() }),
|
|
360
|
+
provisions: [scenarioStateProvision],
|
|
363
361
|
});
|
|
364
362
|
|
|
365
|
-
const
|
|
366
|
-
['
|
|
363
|
+
const scenarioProvisionTrailsMap = new Map<string, AnyTrail>([
|
|
364
|
+
['provision.scenario.leaf', scenarioLeafTrail],
|
|
367
365
|
]);
|
|
368
366
|
|
|
369
|
-
const
|
|
370
|
-
|
|
367
|
+
const transformedCrossLeafTrail = trail('cross.transformed.leaf', {
|
|
368
|
+
blaze: (input: { value: number }) => Result.ok({ value: input.value }),
|
|
369
|
+
description: 'Leaf trail in a transformed cross chain',
|
|
371
370
|
input: z.object({ value: z.number() }),
|
|
372
371
|
output: z.object({ value: z.number() }),
|
|
373
|
-
run: (input: { value: number }) => Result.ok({ value: input.value }),
|
|
374
372
|
});
|
|
375
373
|
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
.object({ value: z.string() })
|
|
381
|
-
.transform(({ value }) => ({ value: Number(value) + 1 })),
|
|
382
|
-
output: z.object({ root: z.number() }),
|
|
383
|
-
run: async (input: { value: number }, ctx: TrailContext) => {
|
|
384
|
-
if (!ctx.follow) {
|
|
385
|
-
return Result.err(new Error('follow not available'));
|
|
374
|
+
const transformedCrossRootTrail = trail('cross.transformed.root', {
|
|
375
|
+
blaze: async (input: { value: number }, ctx: TrailContext) => {
|
|
376
|
+
if (!ctx.cross) {
|
|
377
|
+
return Result.err(new Error('cross not available'));
|
|
386
378
|
}
|
|
387
379
|
|
|
388
|
-
const leafResult = await ctx.
|
|
389
|
-
'
|
|
380
|
+
const leafResult = await ctx.cross<{ value: number }>(
|
|
381
|
+
'cross.transformed.leaf',
|
|
390
382
|
{ value: input.value }
|
|
391
383
|
);
|
|
392
384
|
if (leafResult.isErr()) {
|
|
@@ -395,38 +387,40 @@ const transformedFollowRootTrail = trail('follow.transformed.root', {
|
|
|
395
387
|
|
|
396
388
|
return Result.ok({ root: leafResult.value.value });
|
|
397
389
|
},
|
|
390
|
+
crosses: ['cross.transformed.leaf'],
|
|
391
|
+
description: 'Root trail that transforms input once before crossing',
|
|
392
|
+
input: z
|
|
393
|
+
.object({ value: z.string() })
|
|
394
|
+
.transform(({ value }) => ({ value: Number(value) + 1 })),
|
|
395
|
+
output: z.object({ root: z.number() }),
|
|
398
396
|
});
|
|
399
397
|
|
|
400
|
-
const
|
|
401
|
-
['
|
|
398
|
+
const transformedCrossTrailsMap = new Map<string, AnyTrail>([
|
|
399
|
+
['cross.transformed.leaf', transformedCrossLeafTrail],
|
|
402
400
|
]);
|
|
403
401
|
|
|
404
|
-
const
|
|
402
|
+
const undeclaredCrossProvision = provision('db.undeclared.crosses', {
|
|
405
403
|
create: () => Result.ok({ source: 'factory' }),
|
|
406
404
|
mock: () => ({ source: 'mock' }),
|
|
407
405
|
});
|
|
408
406
|
|
|
409
|
-
const
|
|
410
|
-
|
|
407
|
+
const undeclaredProvisionLeafTrail = trail('provision.undeclared.leaf', {
|
|
408
|
+
blaze: (_input, ctx) =>
|
|
409
|
+
Result.ok({ childSource: undeclaredCrossProvision.from(ctx).source }),
|
|
410
|
+
description: 'Leaf trail that declares the shared provision',
|
|
411
411
|
input: z.object({}),
|
|
412
412
|
output: z.object({ childSource: z.string() }),
|
|
413
|
-
|
|
414
|
-
Result.ok({ childSource: undeclaredFollowService.from(ctx).source }),
|
|
415
|
-
services: [undeclaredFollowService],
|
|
413
|
+
provisions: [undeclaredCrossProvision],
|
|
416
414
|
});
|
|
417
415
|
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
output: z.object({ childSource: z.string(), rootSource: z.string() }),
|
|
423
|
-
run: async (_input, ctx: TrailContext) => {
|
|
424
|
-
if (!ctx.follow) {
|
|
425
|
-
return Result.err(new Error('follow not available'));
|
|
416
|
+
const undeclaredProvisionRootTrail = trail('provision.undeclared.root', {
|
|
417
|
+
blaze: async (_input, ctx: TrailContext) => {
|
|
418
|
+
if (!ctx.cross) {
|
|
419
|
+
return Result.err(new Error('cross not available'));
|
|
426
420
|
}
|
|
427
421
|
|
|
428
|
-
const childResult = await ctx.
|
|
429
|
-
'
|
|
422
|
+
const childResult = await ctx.cross<{ childSource: string }>(
|
|
423
|
+
'provision.undeclared.leaf',
|
|
430
424
|
{}
|
|
431
425
|
);
|
|
432
426
|
if (childResult.isErr()) {
|
|
@@ -435,92 +429,96 @@ const undeclaredServiceRootTrail = trail('service.undeclared.root', {
|
|
|
435
429
|
|
|
436
430
|
return Result.ok({
|
|
437
431
|
childSource: childResult.value.childSource,
|
|
438
|
-
rootSource:
|
|
432
|
+
rootSource: undeclaredCrossProvision.from(ctx).source,
|
|
439
433
|
});
|
|
440
434
|
},
|
|
435
|
+
crosses: ['provision.undeclared.leaf'],
|
|
436
|
+
description: 'Root trail that uses a provision without declaring it',
|
|
437
|
+
input: z.object({}),
|
|
438
|
+
output: z.object({ childSource: z.string(), rootSource: z.string() }),
|
|
441
439
|
});
|
|
442
440
|
|
|
443
|
-
const
|
|
444
|
-
['
|
|
441
|
+
const undeclaredProvisionTrailsMap = new Map<string, AnyTrail>([
|
|
442
|
+
['provision.undeclared.leaf', undeclaredProvisionLeafTrail],
|
|
445
443
|
]);
|
|
446
444
|
|
|
447
|
-
const
|
|
445
|
+
const unrelatedCrossProvision = provision('db.unrelated.crosses', {
|
|
448
446
|
create: () => Result.ok({ source: 'factory' }),
|
|
449
447
|
mock: () => {
|
|
450
448
|
throw new Error('unrelated mock should not be resolved');
|
|
451
449
|
},
|
|
452
450
|
});
|
|
453
451
|
|
|
454
|
-
const
|
|
452
|
+
const unrelatedProvisionTrail = trail('provision.unrelated', {
|
|
453
|
+
blaze: (_input, ctx) =>
|
|
454
|
+
Result.ok({ source: unrelatedCrossProvision.from(ctx).source }),
|
|
455
455
|
description: 'Trail that should not be traversed or mocked',
|
|
456
456
|
input: z.object({}),
|
|
457
457
|
output: z.object({ source: z.string() }),
|
|
458
|
-
|
|
459
|
-
Result.ok({ source: unrelatedFollowService.from(ctx).source }),
|
|
460
|
-
services: [unrelatedFollowService],
|
|
458
|
+
provisions: [unrelatedCrossProvision],
|
|
461
459
|
});
|
|
462
460
|
|
|
463
|
-
const
|
|
464
|
-
['
|
|
461
|
+
const unrelatedProvisionTrailsMap = new Map<string, AnyTrail>([
|
|
462
|
+
['provision.unrelated', unrelatedProvisionTrail],
|
|
465
463
|
]);
|
|
466
464
|
|
|
467
|
-
describe('
|
|
465
|
+
describe('testCrosses provision mocks', () => {
|
|
468
466
|
// eslint-disable-next-line jest/require-hook
|
|
469
|
-
|
|
470
|
-
|
|
467
|
+
testCrosses(
|
|
468
|
+
provisionRootTrail,
|
|
471
469
|
[
|
|
472
470
|
{
|
|
473
|
-
description: 'propagates auto-resolved
|
|
471
|
+
description: 'propagates auto-resolved provision mocks through cross',
|
|
474
472
|
expectValue: { childSource: 'mock', rootSource: 'mock' },
|
|
475
473
|
input: {},
|
|
476
474
|
},
|
|
477
475
|
],
|
|
478
|
-
{ trails:
|
|
476
|
+
{ trails: provisionTrailsMap }
|
|
479
477
|
);
|
|
480
478
|
});
|
|
481
479
|
|
|
482
|
-
describe('
|
|
480
|
+
describe('testCrosses provision mocks are fresh per scenario', () => {
|
|
483
481
|
// eslint-disable-next-line jest/require-hook
|
|
484
|
-
|
|
482
|
+
testCrosses(
|
|
485
483
|
scenarioRootTrail,
|
|
486
484
|
[
|
|
487
485
|
{
|
|
488
|
-
description: 'first scenario sees a fresh mutable
|
|
486
|
+
description: 'first scenario sees a fresh mutable provision',
|
|
489
487
|
expectValue: { count: 1 },
|
|
490
488
|
input: {},
|
|
491
489
|
},
|
|
492
490
|
{
|
|
493
|
-
description: 'second scenario also sees a fresh mutable
|
|
491
|
+
description: 'second scenario also sees a fresh mutable provision',
|
|
494
492
|
expectValue: { count: 1 },
|
|
495
493
|
input: {},
|
|
496
494
|
},
|
|
497
495
|
],
|
|
498
|
-
{ trails:
|
|
496
|
+
{ trails: scenarioProvisionTrailsMap }
|
|
499
497
|
);
|
|
500
498
|
});
|
|
501
499
|
|
|
502
|
-
describe('
|
|
500
|
+
describe('testCrosses explicit provision overrides', () => {
|
|
503
501
|
// eslint-disable-next-line jest/require-hook
|
|
504
|
-
|
|
505
|
-
|
|
502
|
+
testCrosses(
|
|
503
|
+
provisionRootTrail,
|
|
506
504
|
[
|
|
507
505
|
{
|
|
508
|
-
description: 'propagates explicit
|
|
506
|
+
description: 'propagates explicit provision overrides through cross',
|
|
509
507
|
expectValue: { childSource: 'override', rootSource: 'override' },
|
|
510
508
|
input: {},
|
|
511
509
|
},
|
|
512
510
|
],
|
|
513
511
|
{
|
|
514
|
-
|
|
515
|
-
trails:
|
|
512
|
+
provisions: { 'db.mock.crosses': { source: 'override' } },
|
|
513
|
+
trails: provisionTrailsMap,
|
|
516
514
|
}
|
|
517
515
|
);
|
|
518
516
|
});
|
|
519
517
|
|
|
520
|
-
describe('
|
|
518
|
+
describe('testCrosses raw transformed input', () => {
|
|
521
519
|
// eslint-disable-next-line jest/require-hook
|
|
522
|
-
|
|
523
|
-
|
|
520
|
+
testCrosses(
|
|
521
|
+
transformedCrossRootTrail,
|
|
524
522
|
[
|
|
525
523
|
{
|
|
526
524
|
description: 'raw scenario input is only transformed once',
|
|
@@ -528,62 +526,62 @@ describe('testFollows raw transformed input', () => {
|
|
|
528
526
|
input: { value: '1' },
|
|
529
527
|
},
|
|
530
528
|
],
|
|
531
|
-
{ trails:
|
|
529
|
+
{ trails: transformedCrossTrailsMap }
|
|
532
530
|
);
|
|
533
531
|
});
|
|
534
532
|
|
|
535
|
-
describe('
|
|
533
|
+
describe('testCrosses provision declarations', () => {
|
|
536
534
|
// eslint-disable-next-line jest/require-hook
|
|
537
|
-
|
|
538
|
-
|
|
535
|
+
testCrosses(
|
|
536
|
+
undeclaredProvisionRootTrail,
|
|
539
537
|
[
|
|
540
538
|
{
|
|
541
|
-
description: 'fails when the root trail omits a required
|
|
539
|
+
description: 'fails when the root trail omits a required provision',
|
|
542
540
|
expectErr: InternalError,
|
|
543
|
-
expectErrMessage:
|
|
541
|
+
expectErrMessage: undeclaredCrossProvision.id,
|
|
544
542
|
input: {},
|
|
545
543
|
},
|
|
546
544
|
],
|
|
547
|
-
{ trails:
|
|
545
|
+
{ trails: undeclaredProvisionTrailsMap }
|
|
548
546
|
);
|
|
549
547
|
});
|
|
550
548
|
|
|
551
|
-
describe('
|
|
549
|
+
describe('testCrosses only resolves mocks for trails under test', () => {
|
|
552
550
|
// eslint-disable-next-line jest/require-hook
|
|
553
|
-
|
|
554
|
-
|
|
551
|
+
testCrosses(
|
|
552
|
+
provisionRootTrail,
|
|
555
553
|
[
|
|
556
554
|
{
|
|
557
|
-
description: 'unrelated
|
|
555
|
+
description: 'unrelated provision mocks are not resolved',
|
|
558
556
|
expectValue: { childSource: 'mock', rootSource: 'mock' },
|
|
559
557
|
input: {},
|
|
560
558
|
},
|
|
561
559
|
],
|
|
562
560
|
{
|
|
563
561
|
trails: new Map<string, AnyTrail>([
|
|
564
|
-
...
|
|
565
|
-
...
|
|
562
|
+
...provisionTrailsMap.entries(),
|
|
563
|
+
...unrelatedProvisionTrailsMap.entries(),
|
|
566
564
|
]),
|
|
567
565
|
}
|
|
568
566
|
);
|
|
569
567
|
});
|
|
570
568
|
|
|
571
|
-
describe('
|
|
569
|
+
describe('testCrosses fresh provision mocks per scenario', () => {
|
|
572
570
|
// eslint-disable-next-line jest/require-hook
|
|
573
|
-
|
|
574
|
-
|
|
571
|
+
testCrosses(
|
|
572
|
+
statefulProvisionRootTrail,
|
|
575
573
|
[
|
|
576
574
|
{
|
|
577
|
-
description: 'first scenario gets a fresh
|
|
575
|
+
description: 'first scenario gets a fresh provision mock',
|
|
578
576
|
expectValue: { childCount: 1, rootCount: 1 },
|
|
579
577
|
input: {},
|
|
580
578
|
},
|
|
581
579
|
{
|
|
582
|
-
description: 'second scenario also gets a fresh
|
|
580
|
+
description: 'second scenario also gets a fresh provision mock',
|
|
583
581
|
expectValue: { childCount: 1, rootCount: 1 },
|
|
584
582
|
input: {},
|
|
585
583
|
},
|
|
586
584
|
],
|
|
587
|
-
{ trails:
|
|
585
|
+
{ trails: statefulProvisionTrailsMap }
|
|
588
586
|
);
|
|
589
587
|
});
|