@ontrails/testing 1.0.0-beta.15 → 1.0.0-beta.17
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 +70 -1
- package/README.md +47 -3
- package/package.json +16 -7
- package/src/all.ts +32 -7
- package/src/context.ts +3 -0
- package/src/contracts.ts +1 -16
- package/src/examples.ts +19 -11
- package/src/harness-cli.ts +16 -3
- package/src/harness-http.ts +263 -0
- package/src/harness-mcp.ts +2 -0
- package/src/index.ts +18 -1
- package/src/logger.ts +3 -1
- package/src/scenario.ts +3 -5
- package/src/signals.ts +221 -0
- package/src/surface-parity.ts +356 -0
- package/src/types.ts +136 -1
- 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 -36
- package/dist/all.d.ts.map +0 -1
- package/dist/all.js +0 -99
- package/dist/all.js.map +0 -1
- package/dist/assertions.d.ts +0 -72
- package/dist/assertions.d.ts.map +0 -1
- package/dist/assertions.js +0 -238
- package/dist/assertions.js.map +0 -1
- package/dist/context.d.ts +0 -76
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -118
- 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 -75
- 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 -223
- package/dist/crosses.js.map +0 -1
- package/dist/detours.d.ts +0 -13
- package/dist/detours.d.ts.map +0 -1
- package/dist/detours.js +0 -109
- package/dist/detours.js.map +0 -1
- package/dist/effective-examples.d.ts +0 -30
- package/dist/effective-examples.d.ts.map +0 -1
- package/dist/effective-examples.js +0 -227
- package/dist/effective-examples.js.map +0 -1
- package/dist/examples.d.ts +0 -23
- package/dist/examples.d.ts.map +0 -1
- package/dist/examples.js +0 -240
- package/dist/examples.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 -205
- 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 -54
- package/dist/harness-mcp.js.map +0 -1
- package/dist/index.d.ts +0 -18
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -19
- 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/scenario.d.ts +0 -37
- package/dist/scenario.d.ts.map +0 -1
- package/dist/scenario.js +0 -235
- package/dist/scenario.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 -113
- 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 -254
- package/src/__tests__/context.test.ts +0 -170
- package/src/__tests__/contracts.test.ts +0 -239
- package/src/__tests__/crosses.test.ts +0 -587
- package/src/__tests__/detours.test.ts +0 -212
- package/src/__tests__/effective-examples.test.ts +0 -203
- package/src/__tests__/examples.test.ts +0 -636
- package/src/__tests__/harness-cli.test.ts +0 -90
- package/src/__tests__/harness-mcp.test.ts +0 -37
- package/src/__tests__/logger.test.ts +0 -136
- package/src/__tests__/partial-match.test.ts +0 -126
- package/src/__tests__/scenario.test.ts +0 -381
- package/src/__tests__/trail.test.ts +0 -99
- package/tsconfig.json +0 -9
- package/tsconfig.tests.json +0 -10
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,636 +0,0 @@
|
|
|
1
|
-
import { describe, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
ConflictError,
|
|
5
|
-
contour,
|
|
6
|
-
DerivationError,
|
|
7
|
-
NotFoundError,
|
|
8
|
-
Result,
|
|
9
|
-
RetryExhaustedError,
|
|
10
|
-
resource,
|
|
11
|
-
trail,
|
|
12
|
-
topo,
|
|
13
|
-
} from '@ontrails/core';
|
|
14
|
-
import { z } from 'zod';
|
|
15
|
-
|
|
16
|
-
import { testExamples } from '../examples.js';
|
|
17
|
-
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
// Test trails
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
const greetTrail = trail('greet', {
|
|
23
|
-
blaze: (input: { name: string }) =>
|
|
24
|
-
Result.ok({ greeting: `Hello, ${input.name}` }),
|
|
25
|
-
description: 'Greet someone',
|
|
26
|
-
examples: [
|
|
27
|
-
{
|
|
28
|
-
expected: { greeting: 'Hello, Alice' },
|
|
29
|
-
input: { name: 'Alice' },
|
|
30
|
-
name: 'Greet Alice',
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
input: z.object({ name: z.string() }),
|
|
34
|
-
output: z.object({ greeting: z.string() }),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const searchTrail = trail('search', {
|
|
38
|
-
blaze: (input: { query: string }) =>
|
|
39
|
-
Result.ok({ results: [`result for ${input.query}`] }),
|
|
40
|
-
description: 'Search for things',
|
|
41
|
-
examples: [
|
|
42
|
-
{
|
|
43
|
-
input: { query: 'test' },
|
|
44
|
-
name: 'Schema-only search',
|
|
45
|
-
},
|
|
46
|
-
],
|
|
47
|
-
input: z.object({ query: z.string() }),
|
|
48
|
-
output: z.object({ results: z.array(z.string()) }),
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const entityTrail = trail('entity.show', {
|
|
52
|
-
blaze: (input: { name: string }) => {
|
|
53
|
-
if (input.name === 'missing') {
|
|
54
|
-
return Result.err(new NotFoundError('Entity not found'));
|
|
55
|
-
}
|
|
56
|
-
return Result.ok({ id: 1, name: input.name });
|
|
57
|
-
},
|
|
58
|
-
description: 'Show entity',
|
|
59
|
-
examples: [
|
|
60
|
-
{
|
|
61
|
-
expected: { id: 1, name: 'Alpha' },
|
|
62
|
-
input: { name: 'Alpha' },
|
|
63
|
-
name: 'Show entity by name',
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
error: 'NotFoundError',
|
|
67
|
-
input: { name: 'missing' },
|
|
68
|
-
name: 'Entity not found returns NotFoundError',
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
input: z.object({ name: z.string() }),
|
|
72
|
-
output: z.object({ id: z.number(), name: z.string() }),
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
const noExamplesTrail = trail('noexamples', {
|
|
76
|
-
blaze: (input: { x: number }) => Result.ok(input.x * 2),
|
|
77
|
-
input: z.object({ x: z.number() }),
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
const taxonomyErrorTrail = trail('taxonomy.errors', {
|
|
81
|
-
blaze: (input: { type: 'derivation' | 'retry' }) => {
|
|
82
|
-
if (input.type === 'derivation') {
|
|
83
|
-
return Result.err(new DerivationError('could not derive projection'));
|
|
84
|
-
}
|
|
85
|
-
return Result.err(
|
|
86
|
-
new RetryExhaustedError(new ConflictError('version mismatch'), {
|
|
87
|
-
attempts: 3,
|
|
88
|
-
detour: 'ConflictError',
|
|
89
|
-
})
|
|
90
|
-
);
|
|
91
|
-
},
|
|
92
|
-
examples: [
|
|
93
|
-
{
|
|
94
|
-
error: 'DerivationError',
|
|
95
|
-
input: { type: 'derivation' },
|
|
96
|
-
name: 'Derivation failure returns DerivationError',
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
error: 'RetryExhaustedError',
|
|
100
|
-
input: { type: 'retry' },
|
|
101
|
-
name: 'Exhausted detour returns RetryExhaustedError',
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
input: z.object({ type: z.enum(['derivation', 'retry']) }),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const mockDbResource = resource('db.mock.examples', {
|
|
108
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
109
|
-
mock: () => ({ source: 'mock' }),
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
const mockResourceTrail = trail('resource.mocked', {
|
|
113
|
-
blaze: (_input, ctx) =>
|
|
114
|
-
Result.ok({ source: mockDbResource.from(ctx).source }),
|
|
115
|
-
description: 'Trail that reads from a mocked resource',
|
|
116
|
-
examples: [
|
|
117
|
-
{
|
|
118
|
-
expected: { source: 'mock' },
|
|
119
|
-
input: {},
|
|
120
|
-
name: 'Uses auto-resolved resource mock',
|
|
121
|
-
},
|
|
122
|
-
],
|
|
123
|
-
input: z.object({}),
|
|
124
|
-
output: z.object({ source: z.string() }),
|
|
125
|
-
resources: [mockDbResource],
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const explicitOverrideTrail = trail('resource.override', {
|
|
129
|
-
blaze: (_input, ctx) =>
|
|
130
|
-
Result.ok({ source: mockDbResource.from(ctx).source }),
|
|
131
|
-
description: 'Trail whose resource mock can be overridden explicitly',
|
|
132
|
-
examples: [
|
|
133
|
-
{
|
|
134
|
-
expected: { source: 'override' },
|
|
135
|
-
input: {},
|
|
136
|
-
name: 'Explicit resource override wins over mock factory',
|
|
137
|
-
},
|
|
138
|
-
],
|
|
139
|
-
input: z.object({}),
|
|
140
|
-
output: z.object({ source: z.string() }),
|
|
141
|
-
resources: [mockDbResource],
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
const transformedInputTrail = trail('example.transformed', {
|
|
145
|
-
blaze: (input: { value: number }) => Result.ok({ value: input.value }),
|
|
146
|
-
description: 'Trail whose input schema transforms once',
|
|
147
|
-
examples: [
|
|
148
|
-
{
|
|
149
|
-
expected: { value: 2 },
|
|
150
|
-
input: { value: '1' },
|
|
151
|
-
name: 'Raw example input is only transformed once',
|
|
152
|
-
},
|
|
153
|
-
],
|
|
154
|
-
input: z
|
|
155
|
-
.object({ value: z.string() })
|
|
156
|
-
.transform(({ value }) => ({ value: Number(value) + 1 })),
|
|
157
|
-
output: z.object({ value: z.number() }),
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const ctxOverrideTrail = trail('resource.ctx-override', {
|
|
161
|
-
blaze: (_input, ctx) =>
|
|
162
|
-
Result.ok({ source: mockDbResource.from(ctx).source }),
|
|
163
|
-
description: 'Trail whose resource mock can be overridden by ctx.extensions',
|
|
164
|
-
examples: [
|
|
165
|
-
{
|
|
166
|
-
expected: { source: 'ctx' },
|
|
167
|
-
input: {},
|
|
168
|
-
name: 'Context extensions beat auto-resolved mock resources',
|
|
169
|
-
},
|
|
170
|
-
],
|
|
171
|
-
input: z.object({}),
|
|
172
|
-
output: z.object({ source: z.string() }),
|
|
173
|
-
resources: [mockDbResource],
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
const undeclaredDbResource = resource('db.undeclared.examples', {
|
|
177
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
178
|
-
mock: () => ({ source: 'mock' }),
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
const undeclaredResourceTrail = trail('resource.undeclared.examples', {
|
|
182
|
-
blaze: (_input, ctx) =>
|
|
183
|
-
Result.ok({ source: undeclaredDbResource.from(ctx).source }),
|
|
184
|
-
description: 'Trail that uses a resource without declaring it',
|
|
185
|
-
examples: [
|
|
186
|
-
{
|
|
187
|
-
error: 'InternalError',
|
|
188
|
-
input: {},
|
|
189
|
-
name: 'Undeclared resources stay unavailable during example execution',
|
|
190
|
-
},
|
|
191
|
-
],
|
|
192
|
-
input: z.object({}),
|
|
193
|
-
output: z.object({ source: z.string() }),
|
|
194
|
-
});
|
|
195
|
-
const crossDbResource = resource('db.mock.examples.crosses', {
|
|
196
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
197
|
-
mock: () => ({ source: 'mock' }),
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
const crossLeafTrail = trail('resource.crosses.leaf', {
|
|
201
|
-
blaze: (_input, ctx) =>
|
|
202
|
-
Result.ok({ childSource: crossDbResource.from(ctx).source }),
|
|
203
|
-
description: 'Leaf trail that resolves a resource inside a cross chain',
|
|
204
|
-
input: z.object({}),
|
|
205
|
-
output: z.object({ childSource: z.string() }),
|
|
206
|
-
resources: [crossDbResource],
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const crossRootTrail = trail('resource.crosses.root', {
|
|
210
|
-
blaze: async (_input, ctx) => {
|
|
211
|
-
if (!ctx.cross) {
|
|
212
|
-
return Result.err(new Error('cross not available'));
|
|
213
|
-
}
|
|
214
|
-
const childResult = await ctx.cross<{ childSource: string }>(
|
|
215
|
-
'resource.crosses.leaf',
|
|
216
|
-
{}
|
|
217
|
-
);
|
|
218
|
-
if (childResult.isErr()) {
|
|
219
|
-
return childResult;
|
|
220
|
-
}
|
|
221
|
-
return Result.ok({
|
|
222
|
-
childSource: childResult.value.childSource,
|
|
223
|
-
rootSource: crossDbResource.from(ctx).source,
|
|
224
|
-
});
|
|
225
|
-
},
|
|
226
|
-
crosses: ['resource.crosses.leaf'],
|
|
227
|
-
description: 'Root trail that crosses a child trail using resources',
|
|
228
|
-
examples: [
|
|
229
|
-
{
|
|
230
|
-
expected: { childSource: 'mock', rootSource: 'mock' },
|
|
231
|
-
input: {},
|
|
232
|
-
name: 'Propagates resource mocks through cross execution',
|
|
233
|
-
},
|
|
234
|
-
],
|
|
235
|
-
input: z.object({}),
|
|
236
|
-
output: z.object({ childSource: z.string(), rootSource: z.string() }),
|
|
237
|
-
resources: [crossDbResource],
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// ---------------------------------------------------------------------------
|
|
241
|
-
// Composition trails (for cross coverage)
|
|
242
|
-
// ---------------------------------------------------------------------------
|
|
243
|
-
|
|
244
|
-
const addTrail = trail('entity.add', {
|
|
245
|
-
blaze: (input: { name: string }) => Result.ok({ id: '1', name: input.name }),
|
|
246
|
-
description: 'Add an entity',
|
|
247
|
-
input: z.object({ name: z.string() }),
|
|
248
|
-
output: z.object({ id: z.string(), name: z.string() }),
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
const relateTrail = trail('entity.relate', {
|
|
252
|
-
blaze: (input: { from: string; to: string }) =>
|
|
253
|
-
Result.ok({ from: input.from, to: input.to }),
|
|
254
|
-
description: 'Relate entities',
|
|
255
|
-
input: z.object({ from: z.string(), to: z.string() }),
|
|
256
|
-
output: z.object({ from: z.string(), to: z.string() }),
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
const onboardTrail = trail('entity.onboard', {
|
|
260
|
-
blaze: async (input: { name: string }, ctx) => {
|
|
261
|
-
if (!ctx.cross) {
|
|
262
|
-
return Result.err(new Error('cross not available'));
|
|
263
|
-
}
|
|
264
|
-
const addResult = await ctx.cross('entity.add', input);
|
|
265
|
-
if (addResult.isErr()) {
|
|
266
|
-
return addResult;
|
|
267
|
-
}
|
|
268
|
-
const relateResult = await ctx.cross('entity.relate', {
|
|
269
|
-
from: 'root',
|
|
270
|
-
to: (addResult.value as { id: string }).id,
|
|
271
|
-
});
|
|
272
|
-
if (relateResult.isErr()) {
|
|
273
|
-
return relateResult;
|
|
274
|
-
}
|
|
275
|
-
return Result.ok({ id: '1', name: input.name });
|
|
276
|
-
},
|
|
277
|
-
crosses: ['entity.add', 'entity.relate'],
|
|
278
|
-
description: 'Onboard a new entity',
|
|
279
|
-
examples: [
|
|
280
|
-
{
|
|
281
|
-
expected: { id: '1', name: 'Alpha' },
|
|
282
|
-
input: { name: 'Alpha' },
|
|
283
|
-
name: 'Onboard Alpha',
|
|
284
|
-
},
|
|
285
|
-
],
|
|
286
|
-
input: z.object({ name: z.string() }),
|
|
287
|
-
output: z.object({ id: z.string(), name: z.string() }),
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
// ---------------------------------------------------------------------------
|
|
291
|
-
// Tests
|
|
292
|
-
// ---------------------------------------------------------------------------
|
|
293
|
-
|
|
294
|
-
describe('testExamples', () => {
|
|
295
|
-
// eslint-disable-next-line jest/require-hook
|
|
296
|
-
testExamples(
|
|
297
|
-
topo('test-app', {
|
|
298
|
-
entityTrail,
|
|
299
|
-
greetTrail,
|
|
300
|
-
noExamplesTrail,
|
|
301
|
-
searchTrail,
|
|
302
|
-
taxonomyErrorTrail,
|
|
303
|
-
} as Record<string, unknown>)
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
// Also test with custom context (static form)
|
|
307
|
-
describe('with custom context', () => {
|
|
308
|
-
// eslint-disable-next-line jest/require-hook
|
|
309
|
-
testExamples(topo('ctx-app', { greetTrail } as Record<string, unknown>), {
|
|
310
|
-
requestId: 'custom-request',
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
// Test with factory form (each test gets a fresh context)
|
|
315
|
-
describe('with context factory', () => {
|
|
316
|
-
// eslint-disable-next-line jest/require-hook
|
|
317
|
-
testExamples(
|
|
318
|
-
topo('factory-app', { greetTrail } as Record<string, unknown>),
|
|
319
|
-
() => ({ requestId: `factory-${crypto.randomUUID()}` })
|
|
320
|
-
);
|
|
321
|
-
});
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
describe('testExamples skips trails with no examples', () => {
|
|
325
|
-
// eslint-disable-next-line jest/require-hook
|
|
326
|
-
testExamples(
|
|
327
|
-
topo('skip-app', {
|
|
328
|
-
noExamplesTrail,
|
|
329
|
-
} as Record<string, unknown>)
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
test('no-op marker', () => {
|
|
333
|
-
// This test exists so the describe block is not empty
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
describe('testExamples resource mocks', () => {
|
|
338
|
-
// eslint-disable-next-line jest/require-hook
|
|
339
|
-
testExamples(
|
|
340
|
-
topo('resource-mock-app', {
|
|
341
|
-
mockDbResource,
|
|
342
|
-
mockResourceTrail,
|
|
343
|
-
} as Record<string, unknown>)
|
|
344
|
-
);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
describe('testExamples explicit resource overrides', () => {
|
|
348
|
-
// eslint-disable-next-line jest/require-hook
|
|
349
|
-
testExamples(
|
|
350
|
-
topo('resource-override-app', {
|
|
351
|
-
explicitOverrideTrail,
|
|
352
|
-
mockDbResource,
|
|
353
|
-
} as Record<string, unknown>),
|
|
354
|
-
{
|
|
355
|
-
resources: { 'db.mock.examples': { source: 'override' } },
|
|
356
|
-
}
|
|
357
|
-
);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
describe('testExamples raw transformed input', () => {
|
|
361
|
-
// eslint-disable-next-line jest/require-hook
|
|
362
|
-
testExamples(
|
|
363
|
-
topo('transformed-input-app', {
|
|
364
|
-
transformedInputTrail,
|
|
365
|
-
} as Record<string, unknown>)
|
|
366
|
-
);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
describe('testExamples context extension overrides', () => {
|
|
370
|
-
// eslint-disable-next-line jest/require-hook
|
|
371
|
-
testExamples(
|
|
372
|
-
topo('ctx-override-app', {
|
|
373
|
-
ctxOverrideTrail,
|
|
374
|
-
mockDbResource,
|
|
375
|
-
} as Record<string, unknown>),
|
|
376
|
-
{
|
|
377
|
-
ctx: {
|
|
378
|
-
extensions: { 'db.mock.examples': { source: 'ctx' } },
|
|
379
|
-
},
|
|
380
|
-
}
|
|
381
|
-
);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
describe('testExamples resource declarations', () => {
|
|
385
|
-
// eslint-disable-next-line jest/require-hook
|
|
386
|
-
testExamples(
|
|
387
|
-
topo('undeclared-resource-app', {
|
|
388
|
-
undeclaredDbResource,
|
|
389
|
-
undeclaredResourceTrail,
|
|
390
|
-
} as Record<string, unknown>)
|
|
391
|
-
);
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
describe('testExamples crossing coverage for trails with crossings', () => {
|
|
395
|
-
// eslint-disable-next-line jest/require-hook
|
|
396
|
-
testExamples(
|
|
397
|
-
topo('composition-app', {
|
|
398
|
-
addTrail,
|
|
399
|
-
onboardTrail,
|
|
400
|
-
relateTrail,
|
|
401
|
-
} as Record<string, unknown>)
|
|
402
|
-
);
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
describe('testExamples resource mocks through cross', () => {
|
|
406
|
-
// eslint-disable-next-line jest/require-hook
|
|
407
|
-
testExamples(
|
|
408
|
-
topo('resource-cross-app', {
|
|
409
|
-
crossDbResource,
|
|
410
|
-
crossLeafTrail,
|
|
411
|
-
crossRootTrail,
|
|
412
|
-
} as Record<string, unknown>)
|
|
413
|
-
);
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
// ---------------------------------------------------------------------------
|
|
417
|
-
// Nested cross chain: A → B → C
|
|
418
|
-
// ---------------------------------------------------------------------------
|
|
419
|
-
|
|
420
|
-
const leafTrail = trail('step.leaf', {
|
|
421
|
-
blaze: (input: { value: string }) => Result.ok({ leaf: input.value }),
|
|
422
|
-
description: 'Leaf trail in a nested chain',
|
|
423
|
-
input: z.object({ value: z.string() }),
|
|
424
|
-
output: z.object({ leaf: z.string() }),
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
const middleTrail = trail('step.middle', {
|
|
428
|
-
blaze: async (input: { value: string }, ctx) => {
|
|
429
|
-
if (!ctx.cross) {
|
|
430
|
-
return Result.err(new Error('cross not available'));
|
|
431
|
-
}
|
|
432
|
-
const leafResult = await ctx.cross<{ leaf: string }>('step.leaf', input);
|
|
433
|
-
if (leafResult.isErr()) {
|
|
434
|
-
return leafResult;
|
|
435
|
-
}
|
|
436
|
-
return Result.ok({ middle: leafResult.value.leaf });
|
|
437
|
-
},
|
|
438
|
-
crosses: ['step.leaf'],
|
|
439
|
-
description: 'Middle trail that crosses the leaf',
|
|
440
|
-
input: z.object({ value: z.string() }),
|
|
441
|
-
output: z.object({ middle: z.string() }),
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
const rootTrail = trail('step.root', {
|
|
445
|
-
blaze: async (input: { value: string }, ctx) => {
|
|
446
|
-
if (!ctx.cross) {
|
|
447
|
-
return Result.err(new Error('cross not available'));
|
|
448
|
-
}
|
|
449
|
-
const midResult = await ctx.cross<{ middle: string }>('step.middle', input);
|
|
450
|
-
if (midResult.isErr()) {
|
|
451
|
-
return midResult;
|
|
452
|
-
}
|
|
453
|
-
return Result.ok({ root: midResult.value.middle });
|
|
454
|
-
},
|
|
455
|
-
crosses: ['step.middle'],
|
|
456
|
-
description: 'Root trail that crosses the middle trail',
|
|
457
|
-
examples: [
|
|
458
|
-
{
|
|
459
|
-
expected: { root: 'hello' },
|
|
460
|
-
input: { value: 'hello' },
|
|
461
|
-
name: 'Nested cross chain A→B→C',
|
|
462
|
-
},
|
|
463
|
-
],
|
|
464
|
-
input: z.object({ value: z.string() }),
|
|
465
|
-
output: z.object({ root: z.string() }),
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
describe('testExamples nested cross chain (A → B → C)', () => {
|
|
469
|
-
// eslint-disable-next-line jest/require-hook
|
|
470
|
-
testExamples(
|
|
471
|
-
topo('nested-chain-app', {
|
|
472
|
-
leafTrail,
|
|
473
|
-
middleTrail,
|
|
474
|
-
rootTrail,
|
|
475
|
-
} as Record<string, unknown>)
|
|
476
|
-
);
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
// ---------------------------------------------------------------------------
|
|
480
|
-
// Auto-minting permit tests (B3)
|
|
481
|
-
// ---------------------------------------------------------------------------
|
|
482
|
-
|
|
483
|
-
const scopedTrail = trail('scoped.trail', {
|
|
484
|
-
blaze: (_input, ctx) => {
|
|
485
|
-
// Verify the permit was auto-minted with declared scopes
|
|
486
|
-
const permit = ctx.permit as
|
|
487
|
-
| { id: string; scopes: readonly string[] }
|
|
488
|
-
| undefined;
|
|
489
|
-
if (!permit || !permit.scopes.includes('admin')) {
|
|
490
|
-
return Result.err(new Error('Missing permit or scopes'));
|
|
491
|
-
}
|
|
492
|
-
return Result.ok({ ok: true });
|
|
493
|
-
},
|
|
494
|
-
description: 'Trail requiring admin scope',
|
|
495
|
-
examples: [
|
|
496
|
-
{
|
|
497
|
-
expected: { ok: true },
|
|
498
|
-
input: {},
|
|
499
|
-
name: 'Runs with auto-minted permit',
|
|
500
|
-
},
|
|
501
|
-
],
|
|
502
|
-
input: z.object({}),
|
|
503
|
-
output: z.object({ ok: z.boolean() }),
|
|
504
|
-
permit: { scopes: ['admin'] },
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
const publicTrail = trail('public.trail', {
|
|
508
|
-
blaze: (_input, ctx) => {
|
|
509
|
-
// Public trail should NOT get a permit
|
|
510
|
-
if (ctx.permit !== undefined) {
|
|
511
|
-
return Result.err(new Error('Unexpected permit on public trail'));
|
|
512
|
-
}
|
|
513
|
-
return Result.ok({ ok: true });
|
|
514
|
-
},
|
|
515
|
-
description: 'Public trail — no permit needed',
|
|
516
|
-
examples: [
|
|
517
|
-
{
|
|
518
|
-
expected: { ok: true },
|
|
519
|
-
input: {},
|
|
520
|
-
name: 'Runs without a permit',
|
|
521
|
-
},
|
|
522
|
-
],
|
|
523
|
-
input: z.object({}),
|
|
524
|
-
output: z.object({ ok: z.boolean() }),
|
|
525
|
-
permit: 'public',
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
describe('testExamples auto-minting permits', () => {
|
|
529
|
-
describe('scoped trail gets auto-minted permit', () => {
|
|
530
|
-
// eslint-disable-next-line jest/require-hook
|
|
531
|
-
testExamples(
|
|
532
|
-
topo('mint-scoped-app', {
|
|
533
|
-
scopedTrail,
|
|
534
|
-
} as Record<string, unknown>)
|
|
535
|
-
);
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
describe('public trail does NOT get a permit', () => {
|
|
539
|
-
// eslint-disable-next-line jest/require-hook
|
|
540
|
-
testExamples(
|
|
541
|
-
topo('mint-public-app', {
|
|
542
|
-
publicTrail,
|
|
543
|
-
} as Record<string, unknown>)
|
|
544
|
-
);
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
describe('strictPermits skips auto-minting', () => {
|
|
548
|
-
const strictScopedTrail = trail('strict.scoped', {
|
|
549
|
-
blaze: (_input, ctx) =>
|
|
550
|
-
Result.ok({ hasPermit: ctx.permit !== undefined }),
|
|
551
|
-
description: 'Trail that expects no permit under strictPermits',
|
|
552
|
-
examples: [
|
|
553
|
-
{
|
|
554
|
-
expected: { hasPermit: false },
|
|
555
|
-
input: {},
|
|
556
|
-
name: 'No auto-minted permit when strictPermits is true',
|
|
557
|
-
},
|
|
558
|
-
],
|
|
559
|
-
input: z.object({}),
|
|
560
|
-
output: z.object({ hasPermit: z.boolean() }),
|
|
561
|
-
permit: { scopes: ['admin'] },
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
// eslint-disable-next-line jest/require-hook
|
|
565
|
-
testExamples(
|
|
566
|
-
topo('strict-app', {
|
|
567
|
-
strictScopedTrail,
|
|
568
|
-
} as Record<string, unknown>),
|
|
569
|
-
{ strictPermits: true }
|
|
570
|
-
);
|
|
571
|
-
});
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
// ---------------------------------------------------------------------------
|
|
575
|
-
// Derived-fixture crossing coverage regression
|
|
576
|
-
// ---------------------------------------------------------------------------
|
|
577
|
-
//
|
|
578
|
-
// A composition trail whose only examples come from contour-derived
|
|
579
|
-
// fixtures must not fail crossing-coverage — derived inputs are not
|
|
580
|
-
// guaranteed to exercise every declared cross.
|
|
581
|
-
|
|
582
|
-
const itemContour = contour(
|
|
583
|
-
'item',
|
|
584
|
-
{
|
|
585
|
-
id: z.string(),
|
|
586
|
-
name: z.string(),
|
|
587
|
-
},
|
|
588
|
-
{
|
|
589
|
-
examples: [{ id: 'abc', name: 'Widget' }],
|
|
590
|
-
identity: 'id',
|
|
591
|
-
}
|
|
592
|
-
);
|
|
593
|
-
|
|
594
|
-
const helperTrail = trail('derived.helper', {
|
|
595
|
-
blaze: (input: { id: string }) => Result.ok({ id: input.id, ok: true }),
|
|
596
|
-
description: 'Helper referenced by a conditional cross',
|
|
597
|
-
input: z.object({ id: z.string() }),
|
|
598
|
-
output: z.object({ id: z.string(), ok: z.boolean() }),
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
const conditionalCrossTrail = trail('derived.conditional', {
|
|
602
|
-
blaze: async (input: { id: string; name: string }, ctx) => {
|
|
603
|
-
// The conditional cross is never taken for derived fixtures because
|
|
604
|
-
// `shouldCross` is always false in the synthesized input. This is
|
|
605
|
-
// exactly the case the provenance gate exists to protect: if we
|
|
606
|
-
// asserted crossing coverage against derived examples, this trail
|
|
607
|
-
// would fail even though its declaration is accurate for authored
|
|
608
|
-
// use.
|
|
609
|
-
const { shouldCross } = input as { shouldCross?: boolean };
|
|
610
|
-
if (shouldCross && ctx.cross) {
|
|
611
|
-
const result = await ctx.cross<{ id: string; ok: boolean }>(
|
|
612
|
-
'derived.helper',
|
|
613
|
-
{ id: input.id }
|
|
614
|
-
);
|
|
615
|
-
if (result.isErr()) {
|
|
616
|
-
return result;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
return Result.ok({ id: input.id, name: input.name });
|
|
620
|
-
},
|
|
621
|
-
contours: [itemContour],
|
|
622
|
-
crosses: ['derived.helper'],
|
|
623
|
-
description: 'Composition trail with a cross that derived fixtures skip',
|
|
624
|
-
input: z.object({ id: z.string(), name: z.string() }),
|
|
625
|
-
output: z.object({ id: z.string(), name: z.string() }),
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
describe('testExamples derived-fixture crossing coverage is gated', () => {
|
|
629
|
-
// eslint-disable-next-line jest/require-hook
|
|
630
|
-
testExamples(
|
|
631
|
-
topo('derived-coverage-app', {
|
|
632
|
-
conditionalCrossTrail,
|
|
633
|
-
helperTrail,
|
|
634
|
-
} as Record<string, unknown>)
|
|
635
|
-
);
|
|
636
|
-
});
|