@ontrails/testing 1.0.0-beta.14 → 1.0.0-beta.16
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 +69 -0
- package/README.md +12 -12
- package/package.json +15 -6
- package/src/all.ts +153 -16
- package/src/assertions.ts +253 -0
- package/src/context.ts +67 -38
- package/src/contracts.ts +15 -24
- package/src/crosses.ts +93 -51
- package/src/detours.ts +155 -18
- package/src/effective-examples.ts +350 -0
- package/src/examples.ts +139 -63
- package/src/harness-cli.ts +19 -11
- package/src/harness-mcp.ts +9 -8
- package/src/index.ts +14 -4
- package/src/logger.ts +3 -1
- package/src/scenario.ts +368 -0
- package/src/signals.ts +221 -0
- package/src/types.ts +64 -6
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/all.d.ts +0 -31
- package/dist/all.d.ts.map +0 -1
- package/dist/all.js +0 -47
- package/dist/all.js.map +0 -1
- package/dist/assertions.d.ts +0 -49
- package/dist/assertions.d.ts.map +0 -1
- package/dist/assertions.js +0 -84
- package/dist/assertions.js.map +0 -1
- package/dist/context.d.ts +0 -75
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -107
- package/dist/context.js.map +0 -1
- package/dist/contracts.d.ts +0 -17
- package/dist/contracts.d.ts.map +0 -1
- package/dist/contracts.js +0 -71
- package/dist/contracts.js.map +0 -1
- package/dist/crosses.d.ts +0 -38
- package/dist/crosses.d.ts.map +0 -1
- package/dist/crosses.js +0 -213
- package/dist/crosses.js.map +0 -1
- package/dist/detours.d.ts +0 -12
- package/dist/detours.d.ts.map +0 -1
- package/dist/detours.js +0 -30
- package/dist/detours.js.map +0 -1
- package/dist/examples.d.ts +0 -23
- package/dist/examples.d.ts.map +0 -1
- package/dist/examples.js +0 -202
- package/dist/examples.js.map +0 -1
- package/dist/follows.d.ts +0 -38
- package/dist/follows.d.ts.map +0 -1
- package/dist/follows.js +0 -212
- package/dist/follows.js.map +0 -1
- package/dist/harness-cli.d.ts +0 -21
- package/dist/harness-cli.d.ts.map +0 -1
- package/dist/harness-cli.js +0 -200
- package/dist/harness-cli.js.map +0 -1
- package/dist/harness-mcp.d.ts +0 -21
- package/dist/harness-mcp.d.ts.map +0 -1
- package/dist/harness-mcp.js +0 -53
- package/dist/harness-mcp.js.map +0 -1
- package/dist/index.d.ts +0 -16
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -16
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts +0 -15
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -87
- package/dist/logger.js.map +0 -1
- package/dist/trail.d.ts +0 -20
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -80
- package/dist/trail.js.map +0 -1
- package/dist/types.d.ts +0 -80
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/src/__tests__/all.test.ts +0 -135
- package/src/__tests__/context.test.ts +0 -150
- package/src/__tests__/contracts.test.ts +0 -185
- package/src/__tests__/crosses.test.ts +0 -587
- package/src/__tests__/detours.test.ts +0 -55
- package/src/__tests__/examples.test.ts +0 -534
- package/src/__tests__/harness-cli.test.ts +0 -66
- package/src/__tests__/logger.test.ts +0 -136
- package/src/__tests__/trail.test.ts +0 -99
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,587 +0,0 @@
|
|
|
1
|
-
import { describe } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import type { AnyTrail, TrailContext } from '@ontrails/core';
|
|
4
|
-
import { InternalError, Result, provision, trail } from '@ontrails/core';
|
|
5
|
-
import { z } from 'zod';
|
|
6
|
-
|
|
7
|
-
import { testCrosses } from '../crosses.js';
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Test trails (trails with crossings)
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
const addTrail = trail('entity.add', {
|
|
14
|
-
blaze: (input: { name: string }) => Result.ok({ id: '1', name: input.name }),
|
|
15
|
-
description: 'Add an entity',
|
|
16
|
-
examples: [
|
|
17
|
-
{ input: { name: 'Alpha' }, name: 'success' },
|
|
18
|
-
{
|
|
19
|
-
description: 'duplicate name',
|
|
20
|
-
error: 'AlreadyExistsError',
|
|
21
|
-
input: { name: '' },
|
|
22
|
-
name: 'duplicate',
|
|
23
|
-
},
|
|
24
|
-
],
|
|
25
|
-
input: z.object({ name: z.string() }),
|
|
26
|
-
output: z.object({ id: z.string(), name: z.string() }),
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const relateTrail = trail('entity.relate', {
|
|
30
|
-
blaze: (input: { from: string; to: string }) =>
|
|
31
|
-
Result.ok({ from: input.from, to: input.to }),
|
|
32
|
-
description: 'Relate two entities',
|
|
33
|
-
input: z.object({ from: z.string(), to: z.string() }),
|
|
34
|
-
output: z.object({ from: z.string(), to: z.string() }),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
// Composition trail
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
const onboardTrail = trail('entity.onboard', {
|
|
42
|
-
blaze: async (
|
|
43
|
-
input: { name: string; relatedTo: string },
|
|
44
|
-
ctx: TrailContext
|
|
45
|
-
) => {
|
|
46
|
-
if (!ctx.cross) {
|
|
47
|
-
return Result.err(new Error('cross not available'));
|
|
48
|
-
}
|
|
49
|
-
const addResult = await ctx.cross<{ id: string; name: string }>(
|
|
50
|
-
'entity.add',
|
|
51
|
-
{ name: input.name }
|
|
52
|
-
);
|
|
53
|
-
if (addResult.isErr()) {
|
|
54
|
-
return Result.err(addResult.error);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const relateResult = await ctx.cross<{ from: string; to: string }>(
|
|
58
|
-
'entity.relate',
|
|
59
|
-
{ from: addResult.value.name, to: input.relatedTo }
|
|
60
|
-
);
|
|
61
|
-
if (relateResult.isErr()) {
|
|
62
|
-
return Result.err(relateResult.error);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return Result.ok({
|
|
66
|
-
name: addResult.value.name,
|
|
67
|
-
relatedTo: relateResult.value.to,
|
|
68
|
-
});
|
|
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
|
-
});
|
|
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('testCrosses: expectOk', () => {
|
|
87
|
-
// eslint-disable-next-line jest/require-hook
|
|
88
|
-
testCrosses(
|
|
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('testCrosses: expectCrossed', () => {
|
|
102
|
-
// eslint-disable-next-line jest/require-hook
|
|
103
|
-
testCrosses(
|
|
104
|
-
onboardTrail,
|
|
105
|
-
[
|
|
106
|
-
{
|
|
107
|
-
description: 'crosses add then relate in order',
|
|
108
|
-
expectCrossed: ['entity.add', 'entity.relate'],
|
|
109
|
-
expectOk: true,
|
|
110
|
-
input: { name: 'Alpha', relatedTo: 'Beta' },
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
opts
|
|
114
|
-
);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
describe('testCrosses: expectCrossedCount', () => {
|
|
118
|
-
// eslint-disable-next-line jest/require-hook
|
|
119
|
-
testCrosses(
|
|
120
|
-
onboardTrail,
|
|
121
|
-
[
|
|
122
|
-
{
|
|
123
|
-
description: 'each trail crossed exactly once',
|
|
124
|
-
expectCrossedCount: { 'entity.add': 1, 'entity.relate': 1 },
|
|
125
|
-
expectOk: true,
|
|
126
|
-
input: { name: 'Alpha', relatedTo: 'Beta' },
|
|
127
|
-
},
|
|
128
|
-
],
|
|
129
|
-
opts
|
|
130
|
-
);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe('testCrosses: injectFromExample', () => {
|
|
134
|
-
// eslint-disable-next-line jest/require-hook
|
|
135
|
-
testCrosses(
|
|
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('testCrosses: expectValue', () => {
|
|
151
|
-
// eslint-disable-next-line jest/require-hook
|
|
152
|
-
testCrosses(
|
|
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
|
-
});
|
|
164
|
-
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
// Nested cross chain: A → B → C
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
|
|
169
|
-
const leafTrail = trail('step.leaf', {
|
|
170
|
-
blaze: (input: { value: string }) => Result.ok({ leaf: input.value }),
|
|
171
|
-
description: 'Leaf trail in a nested chain',
|
|
172
|
-
input: z.object({ value: z.string() }),
|
|
173
|
-
output: z.object({ leaf: z.string() }),
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
const middleTrail = trail('step.middle', {
|
|
177
|
-
blaze: async (input: { value: string }, ctx: TrailContext) => {
|
|
178
|
-
if (!ctx.cross) {
|
|
179
|
-
return Result.err(new Error('cross not available'));
|
|
180
|
-
}
|
|
181
|
-
const leafResult = await ctx.cross<{ leaf: string }>('step.leaf', input);
|
|
182
|
-
if (leafResult.isErr()) {
|
|
183
|
-
return leafResult;
|
|
184
|
-
}
|
|
185
|
-
return Result.ok({ middle: leafResult.value.leaf });
|
|
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
|
-
});
|
|
192
|
-
|
|
193
|
-
const nestedRootTrail = trail('step.root', {
|
|
194
|
-
blaze: async (input: { value: string }, ctx: TrailContext) => {
|
|
195
|
-
if (!ctx.cross) {
|
|
196
|
-
return Result.err(new Error('cross not available'));
|
|
197
|
-
}
|
|
198
|
-
const midResult = await ctx.cross<{ middle: string }>('step.middle', input);
|
|
199
|
-
if (midResult.isErr()) {
|
|
200
|
-
return midResult;
|
|
201
|
-
}
|
|
202
|
-
return Result.ok({ root: midResult.value.middle });
|
|
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() }),
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
const nestedTrailsMap = new Map<string, AnyTrail>([
|
|
211
|
-
['step.leaf', leafTrail],
|
|
212
|
-
['step.middle', middleTrail],
|
|
213
|
-
]);
|
|
214
|
-
|
|
215
|
-
describe('testCrosses: nested cross chain (A → B → C)', () => {
|
|
216
|
-
// eslint-disable-next-line jest/require-hook
|
|
217
|
-
testCrosses(
|
|
218
|
-
nestedRootTrail,
|
|
219
|
-
[
|
|
220
|
-
{
|
|
221
|
-
description: 'nested ctx.cross works through A → B → C',
|
|
222
|
-
expectOk: true,
|
|
223
|
-
expectValue: { root: 'hello' },
|
|
224
|
-
input: { value: 'hello' },
|
|
225
|
-
},
|
|
226
|
-
],
|
|
227
|
-
{ trails: nestedTrailsMap }
|
|
228
|
-
);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
const mockDbProvision = provision('db.mock.crosses', {
|
|
232
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
233
|
-
mock: () => ({ source: 'mock' }),
|
|
234
|
-
});
|
|
235
|
-
|
|
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',
|
|
240
|
-
input: z.object({}),
|
|
241
|
-
output: z.object({ childSource: z.string() }),
|
|
242
|
-
provisions: [mockDbProvision],
|
|
243
|
-
});
|
|
244
|
-
|
|
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'));
|
|
249
|
-
}
|
|
250
|
-
const childResult = await ctx.cross<{ childSource: string }>(
|
|
251
|
-
'provision.leaf',
|
|
252
|
-
{}
|
|
253
|
-
);
|
|
254
|
-
if (childResult.isErr()) {
|
|
255
|
-
return childResult;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return Result.ok({
|
|
259
|
-
childSource: childResult.value.childSource,
|
|
260
|
-
rootSource: mockDbProvision.from(ctx).source,
|
|
261
|
-
});
|
|
262
|
-
},
|
|
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],
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
const provisionTrailsMap = new Map<string, AnyTrail>([
|
|
272
|
-
['provision.leaf', provisionLeafTrail],
|
|
273
|
-
]);
|
|
274
|
-
|
|
275
|
-
const statefulMockDbProvision = provision('db.mock.crosses.stateful', {
|
|
276
|
-
create: () => Result.ok({ count: 0 }),
|
|
277
|
-
mock: () => ({ count: 0 }),
|
|
278
|
-
});
|
|
279
|
-
|
|
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
|
-
input: z.object({}),
|
|
285
|
-
output: z.object({ childCount: z.number() }),
|
|
286
|
-
provisions: [statefulMockDbProvision],
|
|
287
|
-
});
|
|
288
|
-
|
|
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'));
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const statefulProvision = statefulMockDbProvision.from(ctx);
|
|
296
|
-
statefulProvision.count += 1;
|
|
297
|
-
|
|
298
|
-
const childResult = await ctx.cross<{ childCount: number }>(
|
|
299
|
-
'provision.stateful.leaf',
|
|
300
|
-
{}
|
|
301
|
-
);
|
|
302
|
-
if (childResult.isErr()) {
|
|
303
|
-
return childResult;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return Result.ok({
|
|
307
|
-
childCount: childResult.value.childCount,
|
|
308
|
-
rootCount: statefulProvision.count,
|
|
309
|
-
});
|
|
310
|
-
},
|
|
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],
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
const statefulProvisionTrailsMap = new Map<string, AnyTrail>([
|
|
320
|
-
['provision.stateful.leaf', statefulProvisionLeafTrail],
|
|
321
|
-
]);
|
|
322
|
-
|
|
323
|
-
const scenarioStateProvision = provision('db.mock.scenarios', {
|
|
324
|
-
create: () => Result.ok({ count: 0 }),
|
|
325
|
-
mock: () => ({ count: 0 }),
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
const scenarioLeafTrail = trail('provision.scenario.leaf', {
|
|
329
|
-
blaze: (_input, ctx) =>
|
|
330
|
-
Result.ok({ count: scenarioStateProvision.from(ctx).count }),
|
|
331
|
-
description: 'Leaf trail that reads mutable scenario state',
|
|
332
|
-
input: z.object({}),
|
|
333
|
-
output: z.object({ count: z.number() }),
|
|
334
|
-
provisions: [scenarioStateProvision],
|
|
335
|
-
});
|
|
336
|
-
|
|
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'));
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const state = scenarioStateProvision.from(ctx);
|
|
344
|
-
state.count += 1;
|
|
345
|
-
|
|
346
|
-
const leafResult = await ctx.cross<{ count: number }>(
|
|
347
|
-
'provision.scenario.leaf',
|
|
348
|
-
{}
|
|
349
|
-
);
|
|
350
|
-
if (leafResult.isErr()) {
|
|
351
|
-
return leafResult;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return Result.ok({ count: leafResult.value.count });
|
|
355
|
-
},
|
|
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],
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
const scenarioProvisionTrailsMap = new Map<string, AnyTrail>([
|
|
364
|
-
['provision.scenario.leaf', scenarioLeafTrail],
|
|
365
|
-
]);
|
|
366
|
-
|
|
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',
|
|
370
|
-
input: z.object({ value: z.number() }),
|
|
371
|
-
output: z.object({ value: z.number() }),
|
|
372
|
-
});
|
|
373
|
-
|
|
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'));
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const leafResult = await ctx.cross<{ value: number }>(
|
|
381
|
-
'cross.transformed.leaf',
|
|
382
|
-
{ value: input.value }
|
|
383
|
-
);
|
|
384
|
-
if (leafResult.isErr()) {
|
|
385
|
-
return leafResult;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return Result.ok({ root: leafResult.value.value });
|
|
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() }),
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
const transformedCrossTrailsMap = new Map<string, AnyTrail>([
|
|
399
|
-
['cross.transformed.leaf', transformedCrossLeafTrail],
|
|
400
|
-
]);
|
|
401
|
-
|
|
402
|
-
const undeclaredCrossProvision = provision('db.undeclared.crosses', {
|
|
403
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
404
|
-
mock: () => ({ source: 'mock' }),
|
|
405
|
-
});
|
|
406
|
-
|
|
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
|
-
input: z.object({}),
|
|
412
|
-
output: z.object({ childSource: z.string() }),
|
|
413
|
-
provisions: [undeclaredCrossProvision],
|
|
414
|
-
});
|
|
415
|
-
|
|
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'));
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const childResult = await ctx.cross<{ childSource: string }>(
|
|
423
|
-
'provision.undeclared.leaf',
|
|
424
|
-
{}
|
|
425
|
-
);
|
|
426
|
-
if (childResult.isErr()) {
|
|
427
|
-
return childResult;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return Result.ok({
|
|
431
|
-
childSource: childResult.value.childSource,
|
|
432
|
-
rootSource: undeclaredCrossProvision.from(ctx).source,
|
|
433
|
-
});
|
|
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() }),
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
const undeclaredProvisionTrailsMap = new Map<string, AnyTrail>([
|
|
442
|
-
['provision.undeclared.leaf', undeclaredProvisionLeafTrail],
|
|
443
|
-
]);
|
|
444
|
-
|
|
445
|
-
const unrelatedCrossProvision = provision('db.unrelated.crosses', {
|
|
446
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
447
|
-
mock: () => {
|
|
448
|
-
throw new Error('unrelated mock should not be resolved');
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
const unrelatedProvisionTrail = trail('provision.unrelated', {
|
|
453
|
-
blaze: (_input, ctx) =>
|
|
454
|
-
Result.ok({ source: unrelatedCrossProvision.from(ctx).source }),
|
|
455
|
-
description: 'Trail that should not be traversed or mocked',
|
|
456
|
-
input: z.object({}),
|
|
457
|
-
output: z.object({ source: z.string() }),
|
|
458
|
-
provisions: [unrelatedCrossProvision],
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
const unrelatedProvisionTrailsMap = new Map<string, AnyTrail>([
|
|
462
|
-
['provision.unrelated', unrelatedProvisionTrail],
|
|
463
|
-
]);
|
|
464
|
-
|
|
465
|
-
describe('testCrosses provision mocks', () => {
|
|
466
|
-
// eslint-disable-next-line jest/require-hook
|
|
467
|
-
testCrosses(
|
|
468
|
-
provisionRootTrail,
|
|
469
|
-
[
|
|
470
|
-
{
|
|
471
|
-
description: 'propagates auto-resolved provision mocks through cross',
|
|
472
|
-
expectValue: { childSource: 'mock', rootSource: 'mock' },
|
|
473
|
-
input: {},
|
|
474
|
-
},
|
|
475
|
-
],
|
|
476
|
-
{ trails: provisionTrailsMap }
|
|
477
|
-
);
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
describe('testCrosses provision mocks are fresh per scenario', () => {
|
|
481
|
-
// eslint-disable-next-line jest/require-hook
|
|
482
|
-
testCrosses(
|
|
483
|
-
scenarioRootTrail,
|
|
484
|
-
[
|
|
485
|
-
{
|
|
486
|
-
description: 'first scenario sees a fresh mutable provision',
|
|
487
|
-
expectValue: { count: 1 },
|
|
488
|
-
input: {},
|
|
489
|
-
},
|
|
490
|
-
{
|
|
491
|
-
description: 'second scenario also sees a fresh mutable provision',
|
|
492
|
-
expectValue: { count: 1 },
|
|
493
|
-
input: {},
|
|
494
|
-
},
|
|
495
|
-
],
|
|
496
|
-
{ trails: scenarioProvisionTrailsMap }
|
|
497
|
-
);
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
describe('testCrosses explicit provision overrides', () => {
|
|
501
|
-
// eslint-disable-next-line jest/require-hook
|
|
502
|
-
testCrosses(
|
|
503
|
-
provisionRootTrail,
|
|
504
|
-
[
|
|
505
|
-
{
|
|
506
|
-
description: 'propagates explicit provision overrides through cross',
|
|
507
|
-
expectValue: { childSource: 'override', rootSource: 'override' },
|
|
508
|
-
input: {},
|
|
509
|
-
},
|
|
510
|
-
],
|
|
511
|
-
{
|
|
512
|
-
provisions: { 'db.mock.crosses': { source: 'override' } },
|
|
513
|
-
trails: provisionTrailsMap,
|
|
514
|
-
}
|
|
515
|
-
);
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
describe('testCrosses raw transformed input', () => {
|
|
519
|
-
// eslint-disable-next-line jest/require-hook
|
|
520
|
-
testCrosses(
|
|
521
|
-
transformedCrossRootTrail,
|
|
522
|
-
[
|
|
523
|
-
{
|
|
524
|
-
description: 'raw scenario input is only transformed once',
|
|
525
|
-
expectValue: { root: 2 },
|
|
526
|
-
input: { value: '1' },
|
|
527
|
-
},
|
|
528
|
-
],
|
|
529
|
-
{ trails: transformedCrossTrailsMap }
|
|
530
|
-
);
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
describe('testCrosses provision declarations', () => {
|
|
534
|
-
// eslint-disable-next-line jest/require-hook
|
|
535
|
-
testCrosses(
|
|
536
|
-
undeclaredProvisionRootTrail,
|
|
537
|
-
[
|
|
538
|
-
{
|
|
539
|
-
description: 'fails when the root trail omits a required provision',
|
|
540
|
-
expectErr: InternalError,
|
|
541
|
-
expectErrMessage: undeclaredCrossProvision.id,
|
|
542
|
-
input: {},
|
|
543
|
-
},
|
|
544
|
-
],
|
|
545
|
-
{ trails: undeclaredProvisionTrailsMap }
|
|
546
|
-
);
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
describe('testCrosses only resolves mocks for trails under test', () => {
|
|
550
|
-
// eslint-disable-next-line jest/require-hook
|
|
551
|
-
testCrosses(
|
|
552
|
-
provisionRootTrail,
|
|
553
|
-
[
|
|
554
|
-
{
|
|
555
|
-
description: 'unrelated provision mocks are not resolved',
|
|
556
|
-
expectValue: { childSource: 'mock', rootSource: 'mock' },
|
|
557
|
-
input: {},
|
|
558
|
-
},
|
|
559
|
-
],
|
|
560
|
-
{
|
|
561
|
-
trails: new Map<string, AnyTrail>([
|
|
562
|
-
...provisionTrailsMap.entries(),
|
|
563
|
-
...unrelatedProvisionTrailsMap.entries(),
|
|
564
|
-
]),
|
|
565
|
-
}
|
|
566
|
-
);
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
describe('testCrosses fresh provision mocks per scenario', () => {
|
|
570
|
-
// eslint-disable-next-line jest/require-hook
|
|
571
|
-
testCrosses(
|
|
572
|
-
statefulProvisionRootTrail,
|
|
573
|
-
[
|
|
574
|
-
{
|
|
575
|
-
description: 'first scenario gets a fresh provision mock',
|
|
576
|
-
expectValue: { childCount: 1, rootCount: 1 },
|
|
577
|
-
input: {},
|
|
578
|
-
},
|
|
579
|
-
{
|
|
580
|
-
description: 'second scenario also gets a fresh provision mock',
|
|
581
|
-
expectValue: { childCount: 1, rootCount: 1 },
|
|
582
|
-
input: {},
|
|
583
|
-
},
|
|
584
|
-
],
|
|
585
|
-
{ trails: statefulProvisionTrailsMap }
|
|
586
|
-
);
|
|
587
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { Result, trail, topo } from '@ontrails/core';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
import { testDetours } from '../detours.js';
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Test trails
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
const showTrail = trail('entity.show', {
|
|
13
|
-
blaze: (input: { id: string }) => Result.ok({ id: input.id }),
|
|
14
|
-
detours: {
|
|
15
|
-
related: ['entity.list'],
|
|
16
|
-
},
|
|
17
|
-
input: z.object({ id: z.string() }),
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const listTrail = trail('entity.list', {
|
|
21
|
-
blaze: () => Result.ok([]),
|
|
22
|
-
input: z.object({}),
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const noDetoursTrail = trail('entity.plain', {
|
|
26
|
-
blaze: () => Result.ok('ok'),
|
|
27
|
-
input: z.object({}),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Tests
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
describe('testDetours: all targets exist', () => {
|
|
35
|
-
// eslint-disable-next-line jest/require-hook
|
|
36
|
-
testDetours(
|
|
37
|
-
topo('test-app', {
|
|
38
|
-
listTrail,
|
|
39
|
-
showTrail,
|
|
40
|
-
} as Record<string, unknown>)
|
|
41
|
-
);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('testDetours: skips trails without detours', () => {
|
|
45
|
-
// eslint-disable-next-line jest/require-hook
|
|
46
|
-
testDetours(
|
|
47
|
-
topo('test-app', {
|
|
48
|
-
noDetoursTrail,
|
|
49
|
-
} as Record<string, unknown>)
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
test('no-op marker', () => {
|
|
53
|
-
// Trail without detours is skipped -- no detour tests generated
|
|
54
|
-
});
|
|
55
|
-
});
|