@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,534 +0,0 @@
|
|
|
1
|
-
import { describe, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { NotFoundError, Result, provision, trail, topo } from '@ontrails/core';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
import { testExamples } from '../examples.js';
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Test trails
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
const greetTrail = trail('greet', {
|
|
13
|
-
blaze: (input: { name: string }) =>
|
|
14
|
-
Result.ok({ greeting: `Hello, ${input.name}` }),
|
|
15
|
-
description: 'Greet someone',
|
|
16
|
-
examples: [
|
|
17
|
-
{
|
|
18
|
-
expected: { greeting: 'Hello, Alice' },
|
|
19
|
-
input: { name: 'Alice' },
|
|
20
|
-
name: 'Greet Alice',
|
|
21
|
-
},
|
|
22
|
-
],
|
|
23
|
-
input: z.object({ name: z.string() }),
|
|
24
|
-
output: z.object({ greeting: z.string() }),
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const searchTrail = trail('search', {
|
|
28
|
-
blaze: (input: { query: string }) =>
|
|
29
|
-
Result.ok({ results: [`result for ${input.query}`] }),
|
|
30
|
-
description: 'Search for things',
|
|
31
|
-
examples: [
|
|
32
|
-
{
|
|
33
|
-
input: { query: 'test' },
|
|
34
|
-
name: 'Schema-only search',
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
input: z.object({ query: z.string() }),
|
|
38
|
-
output: z.object({ results: z.array(z.string()) }),
|
|
39
|
-
});
|
|
40
|
-
|
|
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
|
-
},
|
|
48
|
-
description: 'Show entity',
|
|
49
|
-
examples: [
|
|
50
|
-
{
|
|
51
|
-
expected: { id: 1, name: 'Alpha' },
|
|
52
|
-
input: { name: 'Alpha' },
|
|
53
|
-
name: 'Show entity by name',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
error: 'NotFoundError',
|
|
57
|
-
input: { name: 'missing' },
|
|
58
|
-
name: 'Entity not found returns NotFoundError',
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
input: z.object({ name: z.string() }),
|
|
62
|
-
output: z.object({ id: z.number(), name: z.string() }),
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const noExamplesTrail = trail('noexamples', {
|
|
66
|
-
blaze: (input: { x: number }) => Result.ok(input.x * 2),
|
|
67
|
-
input: z.object({ x: z.number() }),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const mockDbProvision = provision('db.mock.examples', {
|
|
71
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
72
|
-
mock: () => ({ source: 'mock' }),
|
|
73
|
-
});
|
|
74
|
-
|
|
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',
|
|
79
|
-
examples: [
|
|
80
|
-
{
|
|
81
|
-
expected: { source: 'mock' },
|
|
82
|
-
input: {},
|
|
83
|
-
name: 'Uses auto-resolved provision mock',
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
input: z.object({}),
|
|
87
|
-
output: z.object({ source: z.string() }),
|
|
88
|
-
provisions: [mockDbProvision],
|
|
89
|
-
});
|
|
90
|
-
|
|
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',
|
|
95
|
-
examples: [
|
|
96
|
-
{
|
|
97
|
-
expected: { source: 'override' },
|
|
98
|
-
input: {},
|
|
99
|
-
name: 'Explicit provision override wins over mock factory',
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
input: z.object({}),
|
|
103
|
-
output: z.object({ source: z.string() }),
|
|
104
|
-
provisions: [mockDbProvision],
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const transformedInputTrail = trail('example.transformed', {
|
|
108
|
-
blaze: (input: { value: number }) => Result.ok({ value: input.value }),
|
|
109
|
-
description: 'Trail whose input schema transforms once',
|
|
110
|
-
examples: [
|
|
111
|
-
{
|
|
112
|
-
expected: { value: 2 },
|
|
113
|
-
input: { value: '1' },
|
|
114
|
-
name: 'Raw example input is only transformed once',
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
input: z
|
|
118
|
-
.object({ value: z.string() })
|
|
119
|
-
.transform(({ value }) => ({ value: Number(value) + 1 })),
|
|
120
|
-
output: z.object({ value: z.number() }),
|
|
121
|
-
});
|
|
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',
|
|
127
|
-
examples: [
|
|
128
|
-
{
|
|
129
|
-
expected: { source: 'ctx' },
|
|
130
|
-
input: {},
|
|
131
|
-
name: 'Context extensions beat auto-resolved mock provisions',
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
input: z.object({}),
|
|
135
|
-
output: z.object({ source: z.string() }),
|
|
136
|
-
provisions: [mockDbProvision],
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const undeclaredDbProvision = provision('db.undeclared.examples', {
|
|
140
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
141
|
-
mock: () => ({ source: 'mock' }),
|
|
142
|
-
});
|
|
143
|
-
|
|
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',
|
|
148
|
-
examples: [
|
|
149
|
-
{
|
|
150
|
-
error: 'InternalError',
|
|
151
|
-
input: {},
|
|
152
|
-
name: 'Undeclared provisions stay unavailable during example execution',
|
|
153
|
-
},
|
|
154
|
-
],
|
|
155
|
-
input: z.object({}),
|
|
156
|
-
output: z.object({ source: z.string() }),
|
|
157
|
-
});
|
|
158
|
-
const crossDbProvision = provision('db.mock.examples.crosses', {
|
|
159
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
160
|
-
mock: () => ({ source: 'mock' }),
|
|
161
|
-
});
|
|
162
|
-
|
|
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',
|
|
167
|
-
input: z.object({}),
|
|
168
|
-
output: z.object({ childSource: z.string() }),
|
|
169
|
-
provisions: [crossDbProvision],
|
|
170
|
-
});
|
|
171
|
-
|
|
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'));
|
|
176
|
-
}
|
|
177
|
-
const childResult = await ctx.cross<{ childSource: string }>(
|
|
178
|
-
'provision.crosses.leaf',
|
|
179
|
-
{}
|
|
180
|
-
);
|
|
181
|
-
if (childResult.isErr()) {
|
|
182
|
-
return childResult;
|
|
183
|
-
}
|
|
184
|
-
return Result.ok({
|
|
185
|
-
childSource: childResult.value.childSource,
|
|
186
|
-
rootSource: crossDbProvision.from(ctx).source,
|
|
187
|
-
});
|
|
188
|
-
},
|
|
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],
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
// ---------------------------------------------------------------------------
|
|
204
|
-
// Composition trails (for cross coverage)
|
|
205
|
-
// ---------------------------------------------------------------------------
|
|
206
|
-
|
|
207
|
-
const addTrail = trail('entity.add', {
|
|
208
|
-
blaze: (input: { name: string }) => Result.ok({ id: '1', name: input.name }),
|
|
209
|
-
description: 'Add an entity',
|
|
210
|
-
input: z.object({ name: z.string() }),
|
|
211
|
-
output: z.object({ id: z.string(), name: z.string() }),
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const relateTrail = trail('entity.relate', {
|
|
215
|
-
blaze: (input: { from: string; to: string }) =>
|
|
216
|
-
Result.ok({ from: input.from, to: input.to }),
|
|
217
|
-
description: 'Relate entities',
|
|
218
|
-
input: z.object({ from: z.string(), to: z.string() }),
|
|
219
|
-
output: z.object({ from: z.string(), to: z.string() }),
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
const onboardTrail = trail('entity.onboard', {
|
|
223
|
-
blaze: async (input: { name: string }, ctx) => {
|
|
224
|
-
if (!ctx.cross) {
|
|
225
|
-
return Result.err(new Error('cross not available'));
|
|
226
|
-
}
|
|
227
|
-
const addResult = await ctx.cross('entity.add', input);
|
|
228
|
-
if (addResult.isErr()) {
|
|
229
|
-
return addResult;
|
|
230
|
-
}
|
|
231
|
-
const relateResult = await ctx.cross('entity.relate', {
|
|
232
|
-
from: 'root',
|
|
233
|
-
to: (addResult.value as { id: string }).id,
|
|
234
|
-
});
|
|
235
|
-
if (relateResult.isErr()) {
|
|
236
|
-
return relateResult;
|
|
237
|
-
}
|
|
238
|
-
return Result.ok({ id: '1', name: input.name });
|
|
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() }),
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// ---------------------------------------------------------------------------
|
|
254
|
-
// Tests
|
|
255
|
-
// ---------------------------------------------------------------------------
|
|
256
|
-
|
|
257
|
-
describe('testExamples', () => {
|
|
258
|
-
// eslint-disable-next-line jest/require-hook
|
|
259
|
-
testExamples(
|
|
260
|
-
topo('test-app', {
|
|
261
|
-
entityTrail,
|
|
262
|
-
greetTrail,
|
|
263
|
-
noExamplesTrail,
|
|
264
|
-
searchTrail,
|
|
265
|
-
} as Record<string, unknown>)
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
// Also test with custom context (static form)
|
|
269
|
-
describe('with custom context', () => {
|
|
270
|
-
// eslint-disable-next-line jest/require-hook
|
|
271
|
-
testExamples(topo('ctx-app', { greetTrail } as Record<string, unknown>), {
|
|
272
|
-
requestId: 'custom-request',
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
// Test with factory form (each test gets a fresh context)
|
|
277
|
-
describe('with context factory', () => {
|
|
278
|
-
// eslint-disable-next-line jest/require-hook
|
|
279
|
-
testExamples(
|
|
280
|
-
topo('factory-app', { greetTrail } as Record<string, unknown>),
|
|
281
|
-
() => ({ requestId: `factory-${crypto.randomUUID()}` })
|
|
282
|
-
);
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
describe('testExamples skips trails with no examples', () => {
|
|
287
|
-
// eslint-disable-next-line jest/require-hook
|
|
288
|
-
testExamples(
|
|
289
|
-
topo('skip-app', {
|
|
290
|
-
noExamplesTrail,
|
|
291
|
-
} as Record<string, unknown>)
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
test('no-op marker', () => {
|
|
295
|
-
// This test exists so the describe block is not empty
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
describe('testExamples provision mocks', () => {
|
|
300
|
-
// eslint-disable-next-line jest/require-hook
|
|
301
|
-
testExamples(
|
|
302
|
-
topo('provision-mock-app', {
|
|
303
|
-
mockDbProvision,
|
|
304
|
-
mockProvisionTrail,
|
|
305
|
-
} as Record<string, unknown>)
|
|
306
|
-
);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
describe('testExamples explicit provision overrides', () => {
|
|
310
|
-
// eslint-disable-next-line jest/require-hook
|
|
311
|
-
testExamples(
|
|
312
|
-
topo('provision-override-app', {
|
|
313
|
-
explicitOverrideTrail,
|
|
314
|
-
mockDbProvision,
|
|
315
|
-
} as Record<string, unknown>),
|
|
316
|
-
{
|
|
317
|
-
provisions: { 'db.mock.examples': { source: 'override' } },
|
|
318
|
-
}
|
|
319
|
-
);
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
describe('testExamples raw transformed input', () => {
|
|
323
|
-
// eslint-disable-next-line jest/require-hook
|
|
324
|
-
testExamples(
|
|
325
|
-
topo('transformed-input-app', {
|
|
326
|
-
transformedInputTrail,
|
|
327
|
-
} as Record<string, unknown>)
|
|
328
|
-
);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
describe('testExamples context extension overrides', () => {
|
|
332
|
-
// eslint-disable-next-line jest/require-hook
|
|
333
|
-
testExamples(
|
|
334
|
-
topo('ctx-override-app', {
|
|
335
|
-
ctxOverrideTrail,
|
|
336
|
-
mockDbProvision,
|
|
337
|
-
} as Record<string, unknown>),
|
|
338
|
-
{
|
|
339
|
-
ctx: {
|
|
340
|
-
extensions: { 'db.mock.examples': { source: 'ctx' } },
|
|
341
|
-
},
|
|
342
|
-
}
|
|
343
|
-
);
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
describe('testExamples provision declarations', () => {
|
|
347
|
-
// eslint-disable-next-line jest/require-hook
|
|
348
|
-
testExamples(
|
|
349
|
-
topo('undeclared-provision-app', {
|
|
350
|
-
undeclaredDbProvision,
|
|
351
|
-
undeclaredProvisionTrail,
|
|
352
|
-
} as Record<string, unknown>)
|
|
353
|
-
);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
describe('testExamples crossing coverage for trails with crossings', () => {
|
|
357
|
-
// eslint-disable-next-line jest/require-hook
|
|
358
|
-
testExamples(
|
|
359
|
-
topo('composition-app', {
|
|
360
|
-
addTrail,
|
|
361
|
-
onboardTrail,
|
|
362
|
-
relateTrail,
|
|
363
|
-
} as Record<string, unknown>)
|
|
364
|
-
);
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
describe('testExamples provision mocks through cross', () => {
|
|
368
|
-
// eslint-disable-next-line jest/require-hook
|
|
369
|
-
testExamples(
|
|
370
|
-
topo('provision-cross-app', {
|
|
371
|
-
crossDbProvision,
|
|
372
|
-
crossLeafTrail,
|
|
373
|
-
crossRootTrail,
|
|
374
|
-
} as Record<string, unknown>)
|
|
375
|
-
);
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
// ---------------------------------------------------------------------------
|
|
379
|
-
// Nested cross chain: A → B → C
|
|
380
|
-
// ---------------------------------------------------------------------------
|
|
381
|
-
|
|
382
|
-
const leafTrail = trail('step.leaf', {
|
|
383
|
-
blaze: (input: { value: string }) => Result.ok({ leaf: input.value }),
|
|
384
|
-
description: 'Leaf trail in a nested chain',
|
|
385
|
-
input: z.object({ value: z.string() }),
|
|
386
|
-
output: z.object({ leaf: z.string() }),
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
const middleTrail = trail('step.middle', {
|
|
390
|
-
blaze: async (input: { value: string }, ctx) => {
|
|
391
|
-
if (!ctx.cross) {
|
|
392
|
-
return Result.err(new Error('cross not available'));
|
|
393
|
-
}
|
|
394
|
-
const leafResult = await ctx.cross<{ leaf: string }>('step.leaf', input);
|
|
395
|
-
if (leafResult.isErr()) {
|
|
396
|
-
return leafResult;
|
|
397
|
-
}
|
|
398
|
-
return Result.ok({ middle: leafResult.value.leaf });
|
|
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() }),
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
const rootTrail = trail('step.root', {
|
|
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',
|
|
419
|
-
examples: [
|
|
420
|
-
{
|
|
421
|
-
expected: { root: 'hello' },
|
|
422
|
-
input: { value: 'hello' },
|
|
423
|
-
name: 'Nested cross chain A→B→C',
|
|
424
|
-
},
|
|
425
|
-
],
|
|
426
|
-
input: z.object({ value: z.string() }),
|
|
427
|
-
output: z.object({ root: z.string() }),
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
describe('testExamples nested cross 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
|
-
});
|
|
440
|
-
|
|
441
|
-
// ---------------------------------------------------------------------------
|
|
442
|
-
// Auto-minting permit tests (B3)
|
|
443
|
-
// ---------------------------------------------------------------------------
|
|
444
|
-
|
|
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
|
-
},
|
|
456
|
-
description: 'Trail requiring admin scope',
|
|
457
|
-
examples: [
|
|
458
|
-
{
|
|
459
|
-
expected: { ok: true },
|
|
460
|
-
input: {},
|
|
461
|
-
name: 'Runs with auto-minted permit',
|
|
462
|
-
},
|
|
463
|
-
],
|
|
464
|
-
input: z.object({}),
|
|
465
|
-
output: z.object({ ok: z.boolean() }),
|
|
466
|
-
permit: { scopes: ['admin'] },
|
|
467
|
-
});
|
|
468
|
-
|
|
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
|
-
},
|
|
477
|
-
description: 'Public trail — no permit needed',
|
|
478
|
-
examples: [
|
|
479
|
-
{
|
|
480
|
-
expected: { ok: true },
|
|
481
|
-
input: {},
|
|
482
|
-
name: 'Runs without a permit',
|
|
483
|
-
},
|
|
484
|
-
],
|
|
485
|
-
input: z.object({}),
|
|
486
|
-
output: z.object({ ok: z.boolean() }),
|
|
487
|
-
permit: 'public',
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
describe('testExamples auto-minting permits', () => {
|
|
491
|
-
describe('scoped trail gets auto-minted permit', () => {
|
|
492
|
-
// eslint-disable-next-line jest/require-hook
|
|
493
|
-
testExamples(
|
|
494
|
-
topo('mint-scoped-app', {
|
|
495
|
-
scopedTrail,
|
|
496
|
-
} as Record<string, unknown>)
|
|
497
|
-
);
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
describe('public trail does NOT get a permit', () => {
|
|
501
|
-
// eslint-disable-next-line jest/require-hook
|
|
502
|
-
testExamples(
|
|
503
|
-
topo('mint-public-app', {
|
|
504
|
-
publicTrail,
|
|
505
|
-
} as Record<string, unknown>)
|
|
506
|
-
);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
describe('strictPermits skips auto-minting', () => {
|
|
510
|
-
const strictScopedTrail = trail('strict.scoped', {
|
|
511
|
-
blaze: (_input, ctx) =>
|
|
512
|
-
Result.ok({ hasPermit: ctx.permit !== undefined }),
|
|
513
|
-
description: 'Trail that expects no permit under strictPermits',
|
|
514
|
-
examples: [
|
|
515
|
-
{
|
|
516
|
-
expected: { hasPermit: false },
|
|
517
|
-
input: {},
|
|
518
|
-
name: 'No auto-minted permit when strictPermits is true',
|
|
519
|
-
},
|
|
520
|
-
],
|
|
521
|
-
input: z.object({}),
|
|
522
|
-
output: z.object({ hasPermit: z.boolean() }),
|
|
523
|
-
permit: { scopes: ['admin'] },
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
// eslint-disable-next-line jest/require-hook
|
|
527
|
-
testExamples(
|
|
528
|
-
topo('strict-app', {
|
|
529
|
-
strictScopedTrail,
|
|
530
|
-
} as Record<string, unknown>),
|
|
531
|
-
{ strictPermits: true }
|
|
532
|
-
);
|
|
533
|
-
});
|
|
534
|
-
});
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { Result, topo, trail } from '@ontrails/core';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
import { createCliHarness } from '../harness-cli.js';
|
|
7
|
-
|
|
8
|
-
describe('createCliHarness', () => {
|
|
9
|
-
test('runs top-level commands', async () => {
|
|
10
|
-
const greet = trail('greet', {
|
|
11
|
-
blaze: (input: { name: string }) => Result.ok(`Hello, ${input.name}!`),
|
|
12
|
-
input: z.object({ name: z.string() }),
|
|
13
|
-
});
|
|
14
|
-
const harness = createCliHarness({
|
|
15
|
-
app: topo('test-app', { greet }),
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const result = await harness.run('greet --name Trails');
|
|
19
|
-
|
|
20
|
-
expect(result.exitCode).toBe(0);
|
|
21
|
-
expect(result.stdout).toContain('Hello, Trails!');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('runs nested commands using the full ordered path', async () => {
|
|
25
|
-
const pin = trail('topo.pin', {
|
|
26
|
-
blaze: (input: { name: string }) => Result.ok(`Pinned ${input.name}`),
|
|
27
|
-
input: z.object({ name: z.string() }),
|
|
28
|
-
});
|
|
29
|
-
const harness = createCliHarness({
|
|
30
|
-
app: topo('test-app', { pin }),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const result = await harness.run('topo pin --name before-auth');
|
|
34
|
-
|
|
35
|
-
expect(result.exitCode).toBe(0);
|
|
36
|
-
expect(result.stdout).toContain('Pinned before-auth');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('prefers the deepest matching executable path', async () => {
|
|
40
|
-
const calls: string[] = [];
|
|
41
|
-
const topoShow = trail('topo', {
|
|
42
|
-
blaze: () => {
|
|
43
|
-
calls.push('topo');
|
|
44
|
-
return Result.ok('topo');
|
|
45
|
-
},
|
|
46
|
-
input: z.object({}),
|
|
47
|
-
});
|
|
48
|
-
const topoPin = trail('topo.pin', {
|
|
49
|
-
blaze: () => {
|
|
50
|
-
calls.push('topo.pin');
|
|
51
|
-
return Result.ok('topo.pin');
|
|
52
|
-
},
|
|
53
|
-
input: z.object({}),
|
|
54
|
-
});
|
|
55
|
-
const harness = createCliHarness({
|
|
56
|
-
app: topo('test-app', { topoPin, topoShow }),
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const child = await harness.run('topo pin');
|
|
60
|
-
const parent = await harness.run('topo');
|
|
61
|
-
|
|
62
|
-
expect(child.exitCode).toBe(0);
|
|
63
|
-
expect(parent.exitCode).toBe(0);
|
|
64
|
-
expect(calls).toEqual(['topo.pin', 'topo']);
|
|
65
|
-
});
|
|
66
|
-
});
|