@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
|
@@ -10,21 +10,21 @@ import { testDetours } from '../detours.js';
|
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
|
|
12
12
|
const showTrail = trail('entity.show', {
|
|
13
|
+
blaze: (input: { id: string }) => Result.ok({ id: input.id }),
|
|
13
14
|
detours: {
|
|
14
15
|
related: ['entity.list'],
|
|
15
16
|
},
|
|
16
17
|
input: z.object({ id: z.string() }),
|
|
17
|
-
run: (input: { id: string }) => Result.ok({ id: input.id }),
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
const listTrail = trail('entity.list', {
|
|
21
|
+
blaze: () => Result.ok([]),
|
|
21
22
|
input: z.object({}),
|
|
22
|
-
run: () => Result.ok([]),
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
const noDetoursTrail = trail('entity.plain', {
|
|
26
|
+
blaze: () => Result.ok('ok'),
|
|
26
27
|
input: z.object({}),
|
|
27
|
-
run: () => Result.ok('ok'),
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import { NotFoundError, Result,
|
|
3
|
+
import { NotFoundError, Result, provision, trail, topo } from '@ontrails/core';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
|
|
6
6
|
import { testExamples } from '../examples.js';
|
|
@@ -10,6 +10,8 @@ import { testExamples } from '../examples.js';
|
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
|
|
12
12
|
const greetTrail = trail('greet', {
|
|
13
|
+
blaze: (input: { name: string }) =>
|
|
14
|
+
Result.ok({ greeting: `Hello, ${input.name}` }),
|
|
13
15
|
description: 'Greet someone',
|
|
14
16
|
examples: [
|
|
15
17
|
{
|
|
@@ -20,11 +22,11 @@ const greetTrail = trail('greet', {
|
|
|
20
22
|
],
|
|
21
23
|
input: z.object({ name: z.string() }),
|
|
22
24
|
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', {
|
|
28
|
+
blaze: (input: { query: string }) =>
|
|
29
|
+
Result.ok({ results: [`result for ${input.query}`] }),
|
|
28
30
|
description: 'Search for things',
|
|
29
31
|
examples: [
|
|
30
32
|
{
|
|
@@ -34,11 +36,15 @@ const searchTrail = trail('search', {
|
|
|
34
36
|
],
|
|
35
37
|
input: z.object({ query: z.string() }),
|
|
36
38
|
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', {
|
|
42
|
+
blaze: (input: { name: string }) => {
|
|
43
|
+
if (input.name === 'missing') {
|
|
44
|
+
return Result.err(new NotFoundError('Entity not found'));
|
|
45
|
+
}
|
|
46
|
+
return Result.ok({ id: 1, name: input.name });
|
|
47
|
+
},
|
|
42
48
|
description: 'Show entity',
|
|
43
49
|
examples: [
|
|
44
50
|
{
|
|
@@ -54,55 +60,52 @@ const entityTrail = trail('entity.show', {
|
|
|
54
60
|
],
|
|
55
61
|
input: z.object({ name: z.string() }),
|
|
56
62
|
output: z.object({ id: z.number(), name: z.string() }),
|
|
57
|
-
run: (input: { name: string }) => {
|
|
58
|
-
if (input.name === 'missing') {
|
|
59
|
-
return Result.err(new NotFoundError('Entity not found'));
|
|
60
|
-
}
|
|
61
|
-
return Result.ok({ id: 1, name: input.name });
|
|
62
|
-
},
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
const noExamplesTrail = trail('noexamples', {
|
|
66
|
+
blaze: (input: { x: number }) => Result.ok(input.x * 2),
|
|
66
67
|
input: z.object({ x: z.number() }),
|
|
67
|
-
run: (input: { x: number }) => Result.ok(input.x * 2),
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
const
|
|
70
|
+
const mockDbProvision = provision('db.mock.examples', {
|
|
71
71
|
create: () => Result.ok({ source: 'factory' }),
|
|
72
72
|
mock: () => ({ source: 'mock' }),
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
const
|
|
76
|
-
|
|
75
|
+
const mockProvisionTrail = trail('provision.mocked', {
|
|
76
|
+
blaze: (_input, ctx) =>
|
|
77
|
+
Result.ok({ source: mockDbProvision.from(ctx).source }),
|
|
78
|
+
description: 'Trail that reads from a mocked provision',
|
|
77
79
|
examples: [
|
|
78
80
|
{
|
|
79
81
|
expected: { source: 'mock' },
|
|
80
82
|
input: {},
|
|
81
|
-
name: 'Uses auto-resolved
|
|
83
|
+
name: 'Uses auto-resolved provision mock',
|
|
82
84
|
},
|
|
83
85
|
],
|
|
84
86
|
input: z.object({}),
|
|
85
87
|
output: z.object({ source: z.string() }),
|
|
86
|
-
|
|
87
|
-
services: [mockDbService],
|
|
88
|
+
provisions: [mockDbProvision],
|
|
88
89
|
});
|
|
89
90
|
|
|
90
|
-
const explicitOverrideTrail = trail('
|
|
91
|
-
|
|
91
|
+
const explicitOverrideTrail = trail('provision.override', {
|
|
92
|
+
blaze: (_input, ctx) =>
|
|
93
|
+
Result.ok({ source: mockDbProvision.from(ctx).source }),
|
|
94
|
+
description: 'Trail whose provision mock can be overridden explicitly',
|
|
92
95
|
examples: [
|
|
93
96
|
{
|
|
94
97
|
expected: { source: 'override' },
|
|
95
98
|
input: {},
|
|
96
|
-
name: 'Explicit
|
|
99
|
+
name: 'Explicit provision override wins over mock factory',
|
|
97
100
|
},
|
|
98
101
|
],
|
|
99
102
|
input: z.object({}),
|
|
100
103
|
output: z.object({ source: z.string() }),
|
|
101
|
-
|
|
102
|
-
services: [mockDbService],
|
|
104
|
+
provisions: [mockDbProvision],
|
|
103
105
|
});
|
|
104
106
|
|
|
105
107
|
const transformedInputTrail = trail('example.transformed', {
|
|
108
|
+
blaze: (input: { value: number }) => Result.ok({ value: input.value }),
|
|
106
109
|
description: 'Trail whose input schema transforms once',
|
|
107
110
|
examples: [
|
|
108
111
|
{
|
|
@@ -115,75 +118,64 @@ const transformedInputTrail = trail('example.transformed', {
|
|
|
115
118
|
.object({ value: z.string() })
|
|
116
119
|
.transform(({ value }) => ({ value: Number(value) + 1 })),
|
|
117
120
|
output: z.object({ value: z.number() }),
|
|
118
|
-
run: (input: { value: number }) => Result.ok({ value: input.value }),
|
|
119
121
|
});
|
|
120
122
|
|
|
121
|
-
const ctxOverrideTrail = trail('
|
|
122
|
-
|
|
123
|
+
const ctxOverrideTrail = trail('provision.ctx-override', {
|
|
124
|
+
blaze: (_input, ctx) =>
|
|
125
|
+
Result.ok({ source: mockDbProvision.from(ctx).source }),
|
|
126
|
+
description: 'Trail whose provision mock can be overridden by ctx.extensions',
|
|
123
127
|
examples: [
|
|
124
128
|
{
|
|
125
129
|
expected: { source: 'ctx' },
|
|
126
130
|
input: {},
|
|
127
|
-
name: 'Context extensions beat auto-resolved mock
|
|
131
|
+
name: 'Context extensions beat auto-resolved mock provisions',
|
|
128
132
|
},
|
|
129
133
|
],
|
|
130
134
|
input: z.object({}),
|
|
131
135
|
output: z.object({ source: z.string() }),
|
|
132
|
-
|
|
133
|
-
services: [mockDbService],
|
|
136
|
+
provisions: [mockDbProvision],
|
|
134
137
|
});
|
|
135
138
|
|
|
136
|
-
const
|
|
139
|
+
const undeclaredDbProvision = provision('db.undeclared.examples', {
|
|
137
140
|
create: () => Result.ok({ source: 'factory' }),
|
|
138
141
|
mock: () => ({ source: 'mock' }),
|
|
139
142
|
});
|
|
140
143
|
|
|
141
|
-
const
|
|
142
|
-
|
|
144
|
+
const undeclaredProvisionTrail = trail('provision.undeclared.examples', {
|
|
145
|
+
blaze: (_input, ctx) =>
|
|
146
|
+
Result.ok({ source: undeclaredDbProvision.from(ctx).source }),
|
|
147
|
+
description: 'Trail that uses a provision without declaring it',
|
|
143
148
|
examples: [
|
|
144
149
|
{
|
|
145
150
|
error: 'InternalError',
|
|
146
151
|
input: {},
|
|
147
|
-
name: 'Undeclared
|
|
152
|
+
name: 'Undeclared provisions stay unavailable during example execution',
|
|
148
153
|
},
|
|
149
154
|
],
|
|
150
155
|
input: z.object({}),
|
|
151
156
|
output: z.object({ source: z.string() }),
|
|
152
|
-
run: (_input, ctx) =>
|
|
153
|
-
Result.ok({ source: undeclaredDbService.from(ctx).source }),
|
|
154
157
|
});
|
|
155
|
-
const
|
|
158
|
+
const crossDbProvision = provision('db.mock.examples.crosses', {
|
|
156
159
|
create: () => Result.ok({ source: 'factory' }),
|
|
157
160
|
mock: () => ({ source: 'mock' }),
|
|
158
161
|
});
|
|
159
162
|
|
|
160
|
-
const
|
|
161
|
-
|
|
163
|
+
const crossLeafTrail = trail('provision.crosses.leaf', {
|
|
164
|
+
blaze: (_input, ctx) =>
|
|
165
|
+
Result.ok({ childSource: crossDbProvision.from(ctx).source }),
|
|
166
|
+
description: 'Leaf trail that resolves a provision inside a cross chain',
|
|
162
167
|
input: z.object({}),
|
|
163
168
|
output: z.object({ childSource: z.string() }),
|
|
164
|
-
|
|
165
|
-
Result.ok({ childSource: followedDbService.from(ctx).source }),
|
|
166
|
-
services: [followedDbService],
|
|
169
|
+
provisions: [crossDbProvision],
|
|
167
170
|
});
|
|
168
171
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
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'));
|
|
172
|
+
const crossRootTrail = trail('provision.crosses.root', {
|
|
173
|
+
blaze: async (_input, ctx) => {
|
|
174
|
+
if (!ctx.cross) {
|
|
175
|
+
return Result.err(new Error('cross not available'));
|
|
184
176
|
}
|
|
185
|
-
const childResult = await ctx.
|
|
186
|
-
'
|
|
177
|
+
const childResult = await ctx.cross<{ childSource: string }>(
|
|
178
|
+
'provision.crosses.leaf',
|
|
187
179
|
{}
|
|
188
180
|
);
|
|
189
181
|
if (childResult.isErr()) {
|
|
@@ -191,52 +183,52 @@ const followedRootTrail = trail('service.follow.root', {
|
|
|
191
183
|
}
|
|
192
184
|
return Result.ok({
|
|
193
185
|
childSource: childResult.value.childSource,
|
|
194
|
-
rootSource:
|
|
186
|
+
rootSource: crossDbProvision.from(ctx).source,
|
|
195
187
|
});
|
|
196
188
|
},
|
|
197
|
-
|
|
189
|
+
crosses: ['provision.crosses.leaf'],
|
|
190
|
+
description: 'Root trail that crosses a child trail using provisions',
|
|
191
|
+
examples: [
|
|
192
|
+
{
|
|
193
|
+
expected: { childSource: 'mock', rootSource: 'mock' },
|
|
194
|
+
input: {},
|
|
195
|
+
name: 'Propagates provision mocks through cross execution',
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
input: z.object({}),
|
|
199
|
+
output: z.object({ childSource: z.string(), rootSource: z.string() }),
|
|
200
|
+
provisions: [crossDbProvision],
|
|
198
201
|
});
|
|
199
202
|
|
|
200
203
|
// ---------------------------------------------------------------------------
|
|
201
|
-
// Composition trails (for
|
|
204
|
+
// Composition trails (for cross coverage)
|
|
202
205
|
// ---------------------------------------------------------------------------
|
|
203
206
|
|
|
204
207
|
const addTrail = trail('entity.add', {
|
|
208
|
+
blaze: (input: { name: string }) => Result.ok({ id: '1', name: input.name }),
|
|
205
209
|
description: 'Add an entity',
|
|
206
210
|
input: z.object({ name: z.string() }),
|
|
207
211
|
output: z.object({ id: z.string(), name: z.string() }),
|
|
208
|
-
run: (input: { name: string }) => Result.ok({ id: '1', name: input.name }),
|
|
209
212
|
});
|
|
210
213
|
|
|
211
214
|
const relateTrail = trail('entity.relate', {
|
|
215
|
+
blaze: (input: { from: string; to: string }) =>
|
|
216
|
+
Result.ok({ from: input.from, to: input.to }),
|
|
212
217
|
description: 'Relate entities',
|
|
213
218
|
input: z.object({ from: z.string(), to: z.string() }),
|
|
214
219
|
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 }),
|
|
217
220
|
});
|
|
218
221
|
|
|
219
222
|
const onboardTrail = trail('entity.onboard', {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
expected: { id: '1', name: 'Alpha' },
|
|
224
|
-
input: { name: 'Alpha' },
|
|
225
|
-
name: 'Onboard Alpha',
|
|
226
|
-
},
|
|
227
|
-
],
|
|
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) => {
|
|
232
|
-
if (!ctx.follow) {
|
|
233
|
-
return Result.err(new Error('follow not available'));
|
|
223
|
+
blaze: async (input: { name: string }, ctx) => {
|
|
224
|
+
if (!ctx.cross) {
|
|
225
|
+
return Result.err(new Error('cross not available'));
|
|
234
226
|
}
|
|
235
|
-
const addResult = await ctx.
|
|
227
|
+
const addResult = await ctx.cross('entity.add', input);
|
|
236
228
|
if (addResult.isErr()) {
|
|
237
229
|
return addResult;
|
|
238
230
|
}
|
|
239
|
-
const relateResult = await ctx.
|
|
231
|
+
const relateResult = await ctx.cross('entity.relate', {
|
|
240
232
|
from: 'root',
|
|
241
233
|
to: (addResult.value as { id: string }).id,
|
|
242
234
|
});
|
|
@@ -245,6 +237,17 @@ const onboardTrail = trail('entity.onboard', {
|
|
|
245
237
|
}
|
|
246
238
|
return Result.ok({ id: '1', name: input.name });
|
|
247
239
|
},
|
|
240
|
+
crosses: ['entity.add', 'entity.relate'],
|
|
241
|
+
description: 'Onboard a new entity',
|
|
242
|
+
examples: [
|
|
243
|
+
{
|
|
244
|
+
expected: { id: '1', name: 'Alpha' },
|
|
245
|
+
input: { name: 'Alpha' },
|
|
246
|
+
name: 'Onboard Alpha',
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
input: z.object({ name: z.string() }),
|
|
250
|
+
output: z.object({ id: z.string(), name: z.string() }),
|
|
248
251
|
});
|
|
249
252
|
|
|
250
253
|
// ---------------------------------------------------------------------------
|
|
@@ -293,25 +296,25 @@ describe('testExamples skips trails with no examples', () => {
|
|
|
293
296
|
});
|
|
294
297
|
});
|
|
295
298
|
|
|
296
|
-
describe('testExamples
|
|
299
|
+
describe('testExamples provision mocks', () => {
|
|
297
300
|
// eslint-disable-next-line jest/require-hook
|
|
298
301
|
testExamples(
|
|
299
|
-
topo('
|
|
300
|
-
|
|
301
|
-
|
|
302
|
+
topo('provision-mock-app', {
|
|
303
|
+
mockDbProvision,
|
|
304
|
+
mockProvisionTrail,
|
|
302
305
|
} as Record<string, unknown>)
|
|
303
306
|
);
|
|
304
307
|
});
|
|
305
308
|
|
|
306
|
-
describe('testExamples explicit
|
|
309
|
+
describe('testExamples explicit provision overrides', () => {
|
|
307
310
|
// eslint-disable-next-line jest/require-hook
|
|
308
311
|
testExamples(
|
|
309
|
-
topo('
|
|
312
|
+
topo('provision-override-app', {
|
|
310
313
|
explicitOverrideTrail,
|
|
311
|
-
|
|
314
|
+
mockDbProvision,
|
|
312
315
|
} as Record<string, unknown>),
|
|
313
316
|
{
|
|
314
|
-
|
|
317
|
+
provisions: { 'db.mock.examples': { source: 'override' } },
|
|
315
318
|
}
|
|
316
319
|
);
|
|
317
320
|
});
|
|
@@ -330,7 +333,7 @@ describe('testExamples context extension overrides', () => {
|
|
|
330
333
|
testExamples(
|
|
331
334
|
topo('ctx-override-app', {
|
|
332
335
|
ctxOverrideTrail,
|
|
333
|
-
|
|
336
|
+
mockDbProvision,
|
|
334
337
|
} as Record<string, unknown>),
|
|
335
338
|
{
|
|
336
339
|
ctx: {
|
|
@@ -340,17 +343,17 @@ describe('testExamples context extension overrides', () => {
|
|
|
340
343
|
);
|
|
341
344
|
});
|
|
342
345
|
|
|
343
|
-
describe('testExamples
|
|
346
|
+
describe('testExamples provision declarations', () => {
|
|
344
347
|
// eslint-disable-next-line jest/require-hook
|
|
345
348
|
testExamples(
|
|
346
|
-
topo('undeclared-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
+
topo('undeclared-provision-app', {
|
|
350
|
+
undeclaredDbProvision,
|
|
351
|
+
undeclaredProvisionTrail,
|
|
349
352
|
} as Record<string, unknown>)
|
|
350
353
|
);
|
|
351
354
|
});
|
|
352
355
|
|
|
353
|
-
describe('testExamples
|
|
356
|
+
describe('testExamples crossing coverage for trails with crossings', () => {
|
|
354
357
|
// eslint-disable-next-line jest/require-hook
|
|
355
358
|
testExamples(
|
|
356
359
|
topo('composition-app', {
|
|
@@ -361,73 +364,70 @@ describe('testExamples follow coverage for composition trails', () => {
|
|
|
361
364
|
);
|
|
362
365
|
});
|
|
363
366
|
|
|
364
|
-
describe('testExamples
|
|
367
|
+
describe('testExamples provision mocks through cross', () => {
|
|
365
368
|
// eslint-disable-next-line jest/require-hook
|
|
366
369
|
testExamples(
|
|
367
|
-
topo('
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
370
|
+
topo('provision-cross-app', {
|
|
371
|
+
crossDbProvision,
|
|
372
|
+
crossLeafTrail,
|
|
373
|
+
crossRootTrail,
|
|
371
374
|
} as Record<string, unknown>)
|
|
372
375
|
);
|
|
373
376
|
});
|
|
374
377
|
|
|
375
378
|
// ---------------------------------------------------------------------------
|
|
376
|
-
// Nested
|
|
379
|
+
// Nested cross chain: A → B → C
|
|
377
380
|
// ---------------------------------------------------------------------------
|
|
378
381
|
|
|
379
382
|
const leafTrail = trail('step.leaf', {
|
|
383
|
+
blaze: (input: { value: string }) => Result.ok({ leaf: input.value }),
|
|
380
384
|
description: 'Leaf trail in a nested chain',
|
|
381
385
|
input: z.object({ value: z.string() }),
|
|
382
386
|
output: z.object({ leaf: z.string() }),
|
|
383
|
-
run: (input: { value: string }) => Result.ok({ leaf: input.value }),
|
|
384
387
|
});
|
|
385
388
|
|
|
386
389
|
const middleTrail = trail('step.middle', {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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'));
|
|
390
|
+
blaze: async (input: { value: string }, ctx) => {
|
|
391
|
+
if (!ctx.cross) {
|
|
392
|
+
return Result.err(new Error('cross not available'));
|
|
394
393
|
}
|
|
395
|
-
const leafResult = await ctx.
|
|
394
|
+
const leafResult = await ctx.cross<{ leaf: string }>('step.leaf', input);
|
|
396
395
|
if (leafResult.isErr()) {
|
|
397
396
|
return leafResult;
|
|
398
397
|
}
|
|
399
398
|
return Result.ok({ middle: leafResult.value.leaf });
|
|
400
399
|
},
|
|
400
|
+
crosses: ['step.leaf'],
|
|
401
|
+
description: 'Middle trail that crosses the leaf',
|
|
402
|
+
input: z.object({ value: z.string() }),
|
|
403
|
+
output: z.object({ middle: z.string() }),
|
|
401
404
|
});
|
|
402
405
|
|
|
403
406
|
const rootTrail = trail('step.root', {
|
|
404
|
-
|
|
407
|
+
blaze: async (input: { value: string }, ctx) => {
|
|
408
|
+
if (!ctx.cross) {
|
|
409
|
+
return Result.err(new Error('cross not available'));
|
|
410
|
+
}
|
|
411
|
+
const midResult = await ctx.cross<{ middle: string }>('step.middle', input);
|
|
412
|
+
if (midResult.isErr()) {
|
|
413
|
+
return midResult;
|
|
414
|
+
}
|
|
415
|
+
return Result.ok({ root: midResult.value.middle });
|
|
416
|
+
},
|
|
417
|
+
crosses: ['step.middle'],
|
|
418
|
+
description: 'Root trail that crosses the middle trail',
|
|
405
419
|
examples: [
|
|
406
420
|
{
|
|
407
421
|
expected: { root: 'hello' },
|
|
408
422
|
input: { value: 'hello' },
|
|
409
|
-
name: 'Nested
|
|
423
|
+
name: 'Nested cross chain A→B→C',
|
|
410
424
|
},
|
|
411
425
|
],
|
|
412
|
-
follow: ['step.middle'],
|
|
413
426
|
input: z.object({ value: z.string() }),
|
|
414
427
|
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
428
|
});
|
|
429
429
|
|
|
430
|
-
describe('testExamples nested
|
|
430
|
+
describe('testExamples nested cross chain (A → B → C)', () => {
|
|
431
431
|
// eslint-disable-next-line jest/require-hook
|
|
432
432
|
testExamples(
|
|
433
433
|
topo('nested-chain-app', {
|
|
@@ -443,6 +443,16 @@ describe('testExamples nested follow chain (A → B → C)', () => {
|
|
|
443
443
|
// ---------------------------------------------------------------------------
|
|
444
444
|
|
|
445
445
|
const scopedTrail = trail('scoped.trail', {
|
|
446
|
+
blaze: (_input, ctx) => {
|
|
447
|
+
// Verify the permit was auto-minted with declared scopes
|
|
448
|
+
const permit = ctx.permit as
|
|
449
|
+
| { id: string; scopes: readonly string[] }
|
|
450
|
+
| undefined;
|
|
451
|
+
if (!permit || !permit.scopes.includes('admin')) {
|
|
452
|
+
return Result.err(new Error('Missing permit or scopes'));
|
|
453
|
+
}
|
|
454
|
+
return Result.ok({ ok: true });
|
|
455
|
+
},
|
|
446
456
|
description: 'Trail requiring admin scope',
|
|
447
457
|
examples: [
|
|
448
458
|
{
|
|
@@ -454,19 +464,16 @@ const scopedTrail = trail('scoped.trail', {
|
|
|
454
464
|
input: z.object({}),
|
|
455
465
|
output: z.object({ ok: z.boolean() }),
|
|
456
466
|
permit: { scopes: ['admin'] },
|
|
457
|
-
run: (_input, ctx) => {
|
|
458
|
-
// Verify the permit was auto-minted with declared scopes
|
|
459
|
-
const permit = ctx.permit as
|
|
460
|
-
| { id: string; scopes: readonly string[] }
|
|
461
|
-
| undefined;
|
|
462
|
-
if (!permit || !permit.scopes.includes('admin')) {
|
|
463
|
-
return Result.err(new Error('Missing permit or scopes'));
|
|
464
|
-
}
|
|
465
|
-
return Result.ok({ ok: true });
|
|
466
|
-
},
|
|
467
467
|
});
|
|
468
468
|
|
|
469
469
|
const publicTrail = trail('public.trail', {
|
|
470
|
+
blaze: (_input, ctx) => {
|
|
471
|
+
// Public trail should NOT get a permit
|
|
472
|
+
if (ctx.permit !== undefined) {
|
|
473
|
+
return Result.err(new Error('Unexpected permit on public trail'));
|
|
474
|
+
}
|
|
475
|
+
return Result.ok({ ok: true });
|
|
476
|
+
},
|
|
470
477
|
description: 'Public trail — no permit needed',
|
|
471
478
|
examples: [
|
|
472
479
|
{
|
|
@@ -478,13 +485,6 @@ const publicTrail = trail('public.trail', {
|
|
|
478
485
|
input: z.object({}),
|
|
479
486
|
output: z.object({ ok: z.boolean() }),
|
|
480
487
|
permit: 'public',
|
|
481
|
-
run: (_input, ctx) => {
|
|
482
|
-
// Public trail should NOT get a permit
|
|
483
|
-
if (ctx.permit !== undefined) {
|
|
484
|
-
return Result.err(new Error('Unexpected permit on public trail'));
|
|
485
|
-
}
|
|
486
|
-
return Result.ok({ ok: true });
|
|
487
|
-
},
|
|
488
488
|
});
|
|
489
489
|
|
|
490
490
|
describe('testExamples auto-minting permits', () => {
|
|
@@ -508,6 +508,8 @@ describe('testExamples auto-minting permits', () => {
|
|
|
508
508
|
|
|
509
509
|
describe('strictPermits skips auto-minting', () => {
|
|
510
510
|
const strictScopedTrail = trail('strict.scoped', {
|
|
511
|
+
blaze: (_input, ctx) =>
|
|
512
|
+
Result.ok({ hasPermit: ctx.permit !== undefined }),
|
|
511
513
|
description: 'Trail that expects no permit under strictPermits',
|
|
512
514
|
examples: [
|
|
513
515
|
{
|
|
@@ -519,7 +521,6 @@ describe('testExamples auto-minting permits', () => {
|
|
|
519
521
|
input: z.object({}),
|
|
520
522
|
output: z.object({ hasPermit: z.boolean() }),
|
|
521
523
|
permit: { scopes: ['admin'] },
|
|
522
|
-
run: (_input, ctx) => Result.ok({ hasPermit: ctx.permit !== undefined }),
|
|
523
524
|
});
|
|
524
525
|
|
|
525
526
|
// eslint-disable-next-line jest/require-hook
|
|
@@ -10,20 +10,20 @@ import { testTrail } from '../trail.js';
|
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
11
|
|
|
12
12
|
const greetTrail = trail('greet', {
|
|
13
|
+
blaze: (input: { name: string }) =>
|
|
14
|
+
Result.ok({ greeting: `Hello, ${input.name}` }),
|
|
13
15
|
input: z.object({ name: z.string() }),
|
|
14
16
|
output: z.object({ greeting: z.string() }),
|
|
15
|
-
run: (input: { name: string }) =>
|
|
16
|
-
Result.ok({ greeting: `Hello, ${input.name}` }),
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
const failTrail = trail('fail', {
|
|
20
|
-
input:
|
|
21
|
-
run: (input: { id: string }) => {
|
|
20
|
+
blaze: (input: { id: string }) => {
|
|
22
21
|
if (input.id === 'missing') {
|
|
23
22
|
return Result.err(new NotFoundError('Not found: missing'));
|
|
24
23
|
}
|
|
25
24
|
return Result.ok({ id: input.id });
|
|
26
25
|
},
|
|
26
|
+
input: z.object({ id: z.string() }),
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
// ---------------------------------------------------------------------------
|