@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
package/src/examples.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { describe, expect, test } from 'bun:test';
|
|
|
11
11
|
|
|
12
12
|
import type {
|
|
13
13
|
CrossFn,
|
|
14
|
-
|
|
14
|
+
ResourceOverrideMap,
|
|
15
15
|
Topo,
|
|
16
16
|
TrailExample,
|
|
17
17
|
Trail,
|
|
@@ -23,13 +23,17 @@ import {
|
|
|
23
23
|
AmbiguousError,
|
|
24
24
|
AssertionError,
|
|
25
25
|
AuthError,
|
|
26
|
+
buildCrossValidationSchema,
|
|
26
27
|
CancelledError,
|
|
27
28
|
ConflictError,
|
|
29
|
+
DerivationError,
|
|
28
30
|
InternalError,
|
|
29
31
|
NetworkError,
|
|
30
32
|
NotFoundError,
|
|
31
33
|
PermissionError,
|
|
34
|
+
PermitError,
|
|
32
35
|
RateLimitError,
|
|
36
|
+
RetryExhaustedError,
|
|
33
37
|
executeTrail,
|
|
34
38
|
Result,
|
|
35
39
|
TimeoutError,
|
|
@@ -42,16 +46,19 @@ import type { z } from 'zod';
|
|
|
42
46
|
import {
|
|
43
47
|
assertErrorMatch,
|
|
44
48
|
assertFullMatch,
|
|
49
|
+
assertPartialMatch,
|
|
45
50
|
assertSchemaMatch,
|
|
46
51
|
} from './assertions.js';
|
|
47
52
|
import {
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
defaultCreatePermit,
|
|
54
|
+
mergeResourceOverrides,
|
|
50
55
|
mergeTestContext,
|
|
51
56
|
normalizeTestExecutionOptions,
|
|
52
|
-
|
|
57
|
+
createMockResources,
|
|
53
58
|
} from './context.js';
|
|
54
|
-
import type {
|
|
59
|
+
import type { PermittedTrail, TestExecutionOptions } from './context.js';
|
|
60
|
+
import { isDerivedExample, deriveTrailExamples } from './effective-examples.js';
|
|
61
|
+
import { withSignalAssertions } from './signals.js';
|
|
55
62
|
|
|
56
63
|
// ---------------------------------------------------------------------------
|
|
57
64
|
// Error class name -> constructor map
|
|
@@ -64,11 +71,16 @@ const ERROR_MAP: Record<string, new (...args: never[]) => Error> = {
|
|
|
64
71
|
AuthError: AuthError as new (...args: never[]) => Error,
|
|
65
72
|
CancelledError: CancelledError as new (...args: never[]) => Error,
|
|
66
73
|
ConflictError: ConflictError as new (...args: never[]) => Error,
|
|
74
|
+
DerivationError: DerivationError as new (...args: never[]) => Error,
|
|
67
75
|
InternalError: InternalError as new (...args: never[]) => Error,
|
|
68
76
|
NetworkError: NetworkError as new (...args: never[]) => Error,
|
|
69
77
|
NotFoundError: NotFoundError as new (...args: never[]) => Error,
|
|
70
78
|
PermissionError: PermissionError as new (...args: never[]) => Error,
|
|
79
|
+
PermitError: PermitError as new (...args: never[]) => Error,
|
|
71
80
|
RateLimitError: RateLimitError as new (...args: never[]) => Error,
|
|
81
|
+
RetryExhaustedError: RetryExhaustedError as unknown as new (
|
|
82
|
+
...args: never[]
|
|
83
|
+
) => Error,
|
|
72
84
|
TimeoutError: TimeoutError as new (...args: never[]) => Error,
|
|
73
85
|
TrailsError: TrailsError as unknown as new (...args: never[]) => Error,
|
|
74
86
|
ValidationError: ValidationError as new (...args: never[]) => Error,
|
|
@@ -78,7 +90,7 @@ const ERROR_MAP: Record<string, new (...args: never[]) => Error> = {
|
|
|
78
90
|
* Resolve an error class name string to the actual constructor.
|
|
79
91
|
* Falls back to generic Error if the name is not in the core taxonomy.
|
|
80
92
|
*/
|
|
81
|
-
const resolveErrorClass = (name: string):
|
|
93
|
+
const resolveErrorClass = (name: string): new (...args: never[]) => Error =>
|
|
82
94
|
ERROR_MAP[name] ?? (Error as new (...args: never[]) => Error);
|
|
83
95
|
|
|
84
96
|
// ---------------------------------------------------------------------------
|
|
@@ -91,16 +103,14 @@ const assertProgressiveMatch = (
|
|
|
91
103
|
output: z.ZodType | undefined
|
|
92
104
|
): void => {
|
|
93
105
|
if (example.expected !== undefined) {
|
|
94
|
-
assertFullMatch(result, example.expected);
|
|
95
|
-
|
|
106
|
+
return assertFullMatch(result, example.expected);
|
|
107
|
+
}
|
|
108
|
+
if (example.expectedMatch !== undefined) {
|
|
109
|
+
return assertPartialMatch(result, example.expectedMatch);
|
|
96
110
|
}
|
|
97
|
-
|
|
98
111
|
if (example.error !== undefined) {
|
|
99
|
-
|
|
100
|
-
assertErrorMatch(result, errorClass);
|
|
101
|
-
return;
|
|
112
|
+
return assertErrorMatch(result, resolveErrorClass(example.error));
|
|
102
113
|
}
|
|
103
|
-
|
|
104
114
|
assertSchemaMatch(result, output);
|
|
105
115
|
};
|
|
106
116
|
|
|
@@ -129,12 +139,12 @@ const handleValidationError = (
|
|
|
129
139
|
};
|
|
130
140
|
|
|
131
141
|
/**
|
|
132
|
-
* Apply auto-
|
|
133
|
-
* doesn't already have a permit,
|
|
142
|
+
* Apply auto-permit: if the trail declares scoped permits and the context
|
|
143
|
+
* doesn't already have a permit, create one and merge it into the context.
|
|
134
144
|
*/
|
|
135
|
-
const
|
|
145
|
+
const applyAutoPermit = (
|
|
136
146
|
ctx: TrailContext,
|
|
137
|
-
trailDef:
|
|
147
|
+
trailDef: PermittedTrail,
|
|
138
148
|
opts: TestExecutionOptions
|
|
139
149
|
): TrailContext => {
|
|
140
150
|
if (opts.strictPermits) {
|
|
@@ -143,8 +153,8 @@ const applyAutoMint = (
|
|
|
143
153
|
if (ctx.permit !== undefined) {
|
|
144
154
|
return ctx;
|
|
145
155
|
}
|
|
146
|
-
const
|
|
147
|
-
const permit =
|
|
156
|
+
const create = opts.createPermit ?? defaultCreatePermit;
|
|
157
|
+
const permit = create(trailDef);
|
|
148
158
|
if (!permit) {
|
|
149
159
|
return ctx;
|
|
150
160
|
}
|
|
@@ -155,27 +165,29 @@ const applyAutoMint = (
|
|
|
155
165
|
* Run a single example against a trail.
|
|
156
166
|
* Handles validation, execution, and assertions.
|
|
157
167
|
*/
|
|
158
|
-
const runExample = async (
|
|
159
|
-
t: Trail<unknown, unknown>,
|
|
168
|
+
export const runExample = async (
|
|
169
|
+
t: Trail<unknown, unknown, unknown>,
|
|
160
170
|
example: TrailExample<unknown, unknown>,
|
|
161
171
|
output: z.ZodType | undefined,
|
|
162
172
|
testCtx: TrailContext,
|
|
163
|
-
|
|
173
|
+
resources?: ResourceOverrideMap,
|
|
164
174
|
opts?: TestExecutionOptions
|
|
165
175
|
): Promise<void> => {
|
|
176
|
+
const ctx = opts ? applyAutoPermit(testCtx, t, opts) : testCtx;
|
|
177
|
+
const signals = withSignalAssertions(ctx, example);
|
|
166
178
|
const validated = validateInput(t.input, example.input);
|
|
167
179
|
|
|
168
180
|
if (handleValidationError(validated, example)) {
|
|
181
|
+
signals.assert();
|
|
169
182
|
return;
|
|
170
183
|
}
|
|
171
184
|
|
|
172
|
-
const ctx = opts ? applyAutoMint(testCtx, t, opts) : testCtx;
|
|
173
|
-
|
|
174
185
|
const result = await executeTrail(t, example.input, {
|
|
175
|
-
ctx,
|
|
176
|
-
|
|
186
|
+
ctx: signals.ctx,
|
|
187
|
+
resources: resources ?? opts?.resources,
|
|
177
188
|
});
|
|
178
189
|
assertProgressiveMatch(result, example, output);
|
|
190
|
+
signals.assert();
|
|
179
191
|
};
|
|
180
192
|
|
|
181
193
|
// ---------------------------------------------------------------------------
|
|
@@ -194,62 +206,100 @@ const createCoverageCross = (
|
|
|
194
206
|
baseCross: CrossFn | undefined,
|
|
195
207
|
topo: Topo,
|
|
196
208
|
ctx: TrailContext,
|
|
197
|
-
|
|
209
|
+
resources?: ResourceOverrideMap
|
|
198
210
|
): CrossFn => {
|
|
199
|
-
const
|
|
211
|
+
const invokeCross = async (
|
|
212
|
+
idOrTrail: string | { readonly id: string },
|
|
213
|
+
input: unknown,
|
|
214
|
+
self: CrossFn
|
|
215
|
+
) => {
|
|
216
|
+
const id = typeof idOrTrail === 'string' ? idOrTrail : idOrTrail.id;
|
|
200
217
|
called.add(id);
|
|
201
218
|
|
|
202
219
|
if (baseCross !== undefined) {
|
|
203
|
-
return baseCross(id, input);
|
|
220
|
+
return await baseCross(id, input);
|
|
204
221
|
}
|
|
205
222
|
|
|
206
223
|
const trailDef = topo.get(id);
|
|
207
224
|
if (trailDef !== undefined) {
|
|
208
|
-
return executeTrail(trailDef, input, {
|
|
209
|
-
ctx: { ...ctx, cross },
|
|
210
|
-
|
|
225
|
+
return await executeTrail(trailDef, input, {
|
|
226
|
+
ctx: { ...ctx, cross: self },
|
|
227
|
+
resources,
|
|
228
|
+
validationSchema: buildCrossValidationSchema(trailDef),
|
|
211
229
|
});
|
|
212
230
|
}
|
|
213
231
|
|
|
214
|
-
return
|
|
232
|
+
return Result.ok();
|
|
215
233
|
};
|
|
216
|
-
|
|
234
|
+
|
|
235
|
+
// Accepts either a trail object (typed cross), a string id (untyped),
|
|
236
|
+
// or a batch of `[target, input]` tuples.
|
|
237
|
+
const cross = async function cross(
|
|
238
|
+
idOrTrail:
|
|
239
|
+
| string
|
|
240
|
+
| { readonly id: string }
|
|
241
|
+
| readonly (readonly [string | { readonly id: string }, unknown])[],
|
|
242
|
+
input?: unknown
|
|
243
|
+
) {
|
|
244
|
+
if (Array.isArray(idOrTrail)) {
|
|
245
|
+
return await Promise.all(
|
|
246
|
+
idOrTrail.map(([target, batchInput]) =>
|
|
247
|
+
invokeCross(target, batchInput, cross as CrossFn)
|
|
248
|
+
)
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return await invokeCross(
|
|
253
|
+
idOrTrail as string | { readonly id: string },
|
|
254
|
+
input,
|
|
255
|
+
cross as CrossFn
|
|
256
|
+
);
|
|
257
|
+
} as CrossFn;
|
|
258
|
+
|
|
259
|
+
return cross;
|
|
217
260
|
};
|
|
218
261
|
|
|
219
262
|
/**
|
|
220
263
|
* Run a single example against a trail with crossings, recording cross calls.
|
|
221
264
|
*/
|
|
222
265
|
const runCompositionExample = async (
|
|
223
|
-
trailDef: Trail<unknown, unknown>,
|
|
266
|
+
trailDef: Trail<unknown, unknown, unknown>,
|
|
224
267
|
example: TrailExample<unknown, unknown>,
|
|
225
268
|
output: z.ZodType | undefined,
|
|
226
269
|
baseCtx: TrailContext,
|
|
227
270
|
called: Set<string>,
|
|
228
271
|
topo: Topo,
|
|
229
|
-
|
|
272
|
+
resources?: ResourceOverrideMap,
|
|
230
273
|
opts?: TestExecutionOptions
|
|
231
274
|
): Promise<void> => {
|
|
275
|
+
const permittedCtx = opts
|
|
276
|
+
? applyAutoPermit(baseCtx, trailDef, opts)
|
|
277
|
+
: baseCtx;
|
|
278
|
+
const signals = withSignalAssertions(permittedCtx, example);
|
|
232
279
|
const validated = validateInput(trailDef.input, example.input);
|
|
233
280
|
|
|
234
281
|
if (handleValidationError(validated, example)) {
|
|
282
|
+
signals.assert();
|
|
235
283
|
return;
|
|
236
284
|
}
|
|
237
285
|
|
|
238
|
-
const mintedCtx = opts ? applyAutoMint(baseCtx, trailDef, opts) : baseCtx;
|
|
239
286
|
const cross = createCoverageCross(
|
|
240
287
|
called,
|
|
241
|
-
|
|
288
|
+
signals.ctx.cross,
|
|
242
289
|
topo,
|
|
243
|
-
|
|
244
|
-
|
|
290
|
+
signals.ctx,
|
|
291
|
+
resources
|
|
245
292
|
);
|
|
246
|
-
const testCtx: TrailContext = { ...
|
|
293
|
+
const testCtx: TrailContext = { ...signals.ctx, cross };
|
|
247
294
|
|
|
295
|
+
// Top-level trail validates against trail.input (not merged crossInput).
|
|
296
|
+
// Merged validation only applies to cross targets in executeFromMap/createCoverageCross.
|
|
248
297
|
const result = await executeTrail(trailDef, example.input, {
|
|
249
298
|
ctx: testCtx,
|
|
250
|
-
|
|
299
|
+
resources: resources ?? opts?.resources,
|
|
251
300
|
});
|
|
252
301
|
assertProgressiveMatch(result, example, output);
|
|
302
|
+
signals.assert();
|
|
253
303
|
};
|
|
254
304
|
|
|
255
305
|
// ---------------------------------------------------------------------------
|
|
@@ -264,7 +314,7 @@ const runCompositionExample = async (
|
|
|
264
314
|
*
|
|
265
315
|
* One line in your test file:
|
|
266
316
|
* ```ts
|
|
267
|
-
* testExamples(
|
|
317
|
+
* testExamples(graph);
|
|
268
318
|
* ```
|
|
269
319
|
*/
|
|
270
320
|
export const testExamples = (
|
|
@@ -276,11 +326,12 @@ export const testExamples = (
|
|
|
276
326
|
): void => {
|
|
277
327
|
const resolveInput =
|
|
278
328
|
typeof ctxOrFactory === 'function' ? ctxOrFactory : () => ctxOrFactory;
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
329
|
+
const withExamples = (app.list() as Trail<unknown, unknown, unknown>[])
|
|
330
|
+
.map((trailDef) => ({
|
|
331
|
+
...trailDef,
|
|
332
|
+
examples: deriveTrailExamples(trailDef),
|
|
333
|
+
}))
|
|
334
|
+
.filter((trailDef) => trailDef.examples.length > 0);
|
|
284
335
|
const simpleTrails = withExamples.filter((t) => t.crosses.length === 0);
|
|
285
336
|
const compositionTrails = withExamples.filter((t) => t.crosses.length > 0);
|
|
286
337
|
|
|
@@ -296,19 +347,27 @@ export const testExamples = (
|
|
|
296
347
|
'example: $name',
|
|
297
348
|
async (example: TrailExample<unknown, unknown>) => {
|
|
298
349
|
const resolved = normalizeTestExecutionOptions(resolveInput());
|
|
299
|
-
const
|
|
300
|
-
await
|
|
350
|
+
const resources = mergeResourceOverrides(
|
|
351
|
+
await createMockResources(app),
|
|
301
352
|
resolved.ctx,
|
|
302
|
-
resolved.
|
|
353
|
+
resolved.resources
|
|
303
354
|
);
|
|
304
355
|
const testCtx = mergeTestContext(resolved.ctx);
|
|
305
|
-
await runExample(t, example, output, testCtx,
|
|
356
|
+
await runExample(t, example, output, testCtx, resources, resolved);
|
|
306
357
|
}
|
|
307
358
|
);
|
|
308
359
|
});
|
|
309
360
|
}
|
|
310
361
|
|
|
311
|
-
// Composition trails: use recording cross and check coverage
|
|
362
|
+
// Composition trails: use recording cross and check coverage.
|
|
363
|
+
//
|
|
364
|
+
// Crossing coverage only runs against AUTHORED examples. Contour-derived
|
|
365
|
+
// fixtures are opportunistic coverage that may not exercise every
|
|
366
|
+
// `ctx.cross()` branch in the trail, so asserting coverage against them
|
|
367
|
+
// would produce false failures for trails whose authored intent was a
|
|
368
|
+
// single path. When a trail has zero authored examples the coverage
|
|
369
|
+
// assertion is skipped entirely — the derived-example runs still
|
|
370
|
+
// execute, but they are not required to cover declared crossings.
|
|
312
371
|
if (compositionTrails.length > 0) {
|
|
313
372
|
describe.each(compositionTrails)('$id', (t) => {
|
|
314
373
|
const { examples, output } = t;
|
|
@@ -316,16 +375,29 @@ export const testExamples = (
|
|
|
316
375
|
return;
|
|
317
376
|
}
|
|
318
377
|
|
|
319
|
-
const
|
|
378
|
+
const calledFromAuthored = new Set<string>();
|
|
379
|
+
const hasAuthoredExamples = examples.some(
|
|
380
|
+
(example) => !isDerivedExample(example)
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// Only record cross calls from authored examples. Derived fixtures
|
|
384
|
+
// execute normally but do not contribute to coverage — the sink map
|
|
385
|
+
// routes each example to the right bucket without an inline
|
|
386
|
+
// conditional inside the test body.
|
|
387
|
+
const discardSink = new Set<string>();
|
|
388
|
+
const pickCoverageSink = (
|
|
389
|
+
example: TrailExample<unknown, unknown>
|
|
390
|
+
): Set<string> =>
|
|
391
|
+
isDerivedExample(example) ? discardSink : calledFromAuthored;
|
|
320
392
|
|
|
321
393
|
test.each([...examples])(
|
|
322
394
|
'example: $name',
|
|
323
395
|
async (example: TrailExample<unknown, unknown>) => {
|
|
324
396
|
const resolved = normalizeTestExecutionOptions(resolveInput());
|
|
325
|
-
const
|
|
326
|
-
await
|
|
397
|
+
const resources = mergeResourceOverrides(
|
|
398
|
+
await createMockResources(app),
|
|
327
399
|
resolved.ctx,
|
|
328
|
-
resolved.
|
|
400
|
+
resolved.resources
|
|
329
401
|
);
|
|
330
402
|
const baseCtx = mergeTestContext(resolved.ctx);
|
|
331
403
|
await runCompositionExample(
|
|
@@ -333,18 +405,22 @@ export const testExamples = (
|
|
|
333
405
|
example,
|
|
334
406
|
output,
|
|
335
407
|
baseCtx,
|
|
336
|
-
|
|
408
|
+
pickCoverageSink(example),
|
|
337
409
|
app,
|
|
338
|
-
|
|
410
|
+
resources,
|
|
339
411
|
resolved
|
|
340
412
|
);
|
|
341
413
|
}
|
|
342
414
|
);
|
|
343
415
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
416
|
+
if (hasAuthoredExamples) {
|
|
417
|
+
test('crossing coverage', () => {
|
|
418
|
+
const uncovered = t.crosses.filter(
|
|
419
|
+
(id) => !calledFromAuthored.has(id)
|
|
420
|
+
);
|
|
421
|
+
expect(uncovered).toEqual([]);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
348
424
|
});
|
|
349
425
|
}
|
|
350
426
|
};
|
package/src/harness-cli.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI integration test harness.
|
|
3
3
|
*
|
|
4
|
-
* Builds CLI commands from
|
|
4
|
+
* Builds CLI commands from a graph, executes them in-process,
|
|
5
5
|
* and captures stdout/stderr.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { deriveCliCommands } from '@ontrails/cli';
|
|
9
9
|
import type { CliCommand } from '@ontrails/cli';
|
|
10
|
+
import type { TrailContext } from '@ontrails/core';
|
|
10
11
|
|
|
11
|
-
import {
|
|
12
|
+
import { mergeTestContext } from './context.js';
|
|
12
13
|
import type {
|
|
13
14
|
CliHarness,
|
|
14
15
|
CliHarnessOptions,
|
|
@@ -218,9 +219,10 @@ const buildErrorResult = (
|
|
|
218
219
|
const executeCommand = async (
|
|
219
220
|
command: CliCommand,
|
|
220
221
|
flags: Record<string, unknown>,
|
|
221
|
-
streams: CapturedStreams
|
|
222
|
+
streams: CapturedStreams,
|
|
223
|
+
ctxOverrides?: Partial<TrailContext>
|
|
222
224
|
): Promise<CliHarnessResult> => {
|
|
223
|
-
const ctx =
|
|
225
|
+
const ctx = mergeTestContext(ctxOverrides);
|
|
224
226
|
const result = await command.execute({}, flags, ctx);
|
|
225
227
|
streams.restore();
|
|
226
228
|
|
|
@@ -238,7 +240,8 @@ const executeCommand = async (
|
|
|
238
240
|
/** Run the full command pipeline: resolve, parse, execute. */
|
|
239
241
|
const runCommand = async (
|
|
240
242
|
commands: CliCommand[],
|
|
241
|
-
commandString: string
|
|
243
|
+
commandString: string,
|
|
244
|
+
ctxOverrides?: Partial<TrailContext>
|
|
242
245
|
): Promise<CliHarnessResult> => {
|
|
243
246
|
const parts = parseCommandString(commandString);
|
|
244
247
|
const resolved = resolveCommand(commands, parts);
|
|
@@ -255,7 +258,7 @@ const runCommand = async (
|
|
|
255
258
|
const streams = captureStreams();
|
|
256
259
|
|
|
257
260
|
try {
|
|
258
|
-
return await executeCommand(command, flags, streams);
|
|
261
|
+
return await executeCommand(command, flags, streams, ctxOverrides);
|
|
259
262
|
} catch (error: unknown) {
|
|
260
263
|
return buildErrorResult(error, streams);
|
|
261
264
|
}
|
|
@@ -268,19 +271,24 @@ const runCommand = async (
|
|
|
268
271
|
/**
|
|
269
272
|
* Create a CLI harness for integration testing.
|
|
270
273
|
*
|
|
271
|
-
* Builds commands from the
|
|
274
|
+
* Builds commands from the graph's topo and provides a `run()` method
|
|
272
275
|
* that parses command strings and executes them in-process.
|
|
273
276
|
*
|
|
274
277
|
* ```ts
|
|
275
|
-
* const harness = createCliHarness({
|
|
278
|
+
* const harness = createCliHarness({ graph });
|
|
276
279
|
* const result = await harness.run("entity show --name Alpha --output json");
|
|
277
280
|
* expect(result.exitCode).toBe(0);
|
|
278
281
|
* ```
|
|
279
282
|
*/
|
|
280
283
|
export const createCliHarness = (options: CliHarnessOptions): CliHarness => {
|
|
281
|
-
const
|
|
284
|
+
const { ctx, graph, ...deriveOptions } = options;
|
|
285
|
+
const commandsResult = deriveCliCommands(graph, deriveOptions);
|
|
286
|
+
if (commandsResult.isErr()) {
|
|
287
|
+
throw commandsResult.error;
|
|
288
|
+
}
|
|
289
|
+
const commands = commandsResult.value;
|
|
282
290
|
|
|
283
291
|
return {
|
|
284
|
-
run: (commandString: string) => runCommand(commands, commandString),
|
|
292
|
+
run: (commandString: string) => runCommand(commands, commandString, ctx),
|
|
285
293
|
};
|
|
286
294
|
};
|
package/src/harness-mcp.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP integration test harness.
|
|
3
3
|
*
|
|
4
|
-
* Builds MCP tools from
|
|
4
|
+
* Builds MCP tools from a graph, invokes them directly (no transport),
|
|
5
5
|
* and returns the MCP tool response.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { deriveMcpTools } from '@ontrails/mcp';
|
|
9
9
|
import type { McpToolDefinition } from '@ontrails/mcp';
|
|
10
10
|
|
|
11
11
|
import type {
|
|
@@ -21,17 +21,18 @@ import type {
|
|
|
21
21
|
/**
|
|
22
22
|
* Create an MCP harness for integration testing.
|
|
23
23
|
*
|
|
24
|
-
* Builds MCP tools from the
|
|
24
|
+
* Builds MCP tools from the graph's topo and provides a `callTool()` method
|
|
25
25
|
* that invokes tools directly without any transport boundary.
|
|
26
26
|
*
|
|
27
27
|
* ```ts
|
|
28
|
-
* const harness = createMcpHarness({
|
|
28
|
+
* const harness = createMcpHarness({ graph });
|
|
29
29
|
* const result = await harness.callTool("myapp_entity_show", { name: "Alpha" });
|
|
30
30
|
* expect(result.isError).toBe(false);
|
|
31
31
|
* ```
|
|
32
32
|
*/
|
|
33
33
|
export const createMcpHarness = (options: McpHarnessOptions): McpHarness => {
|
|
34
|
-
const
|
|
34
|
+
const { extra, graph, ...deriveOptions } = options;
|
|
35
|
+
const toolsResult = deriveMcpTools(graph, deriveOptions);
|
|
35
36
|
if (toolsResult.isErr()) {
|
|
36
37
|
throw toolsResult.error;
|
|
37
38
|
}
|
|
@@ -54,9 +55,9 @@ export const createMcpHarness = (options: McpHarnessOptions): McpHarness => {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
const result = await tool.handler(args, {
|
|
57
|
-
abortSignal:
|
|
58
|
-
progressToken:
|
|
59
|
-
sendProgress:
|
|
58
|
+
abortSignal: extra?.abortSignal,
|
|
59
|
+
progressToken: extra?.progressToken,
|
|
60
|
+
sendProgress: extra?.sendProgress,
|
|
60
61
|
});
|
|
61
62
|
|
|
62
63
|
return {
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Contract-driven testing
|
|
2
2
|
export { testAll } from './all.js';
|
|
3
|
+
export { testAllEstablished } from './all.js';
|
|
3
4
|
export { testExamples } from './examples.js';
|
|
4
5
|
export { testCrosses } from './crosses.js';
|
|
5
6
|
export { testTrail } from './trail.js';
|
|
@@ -10,34 +11,43 @@ export { testDetours } from './detours.js';
|
|
|
10
11
|
export {
|
|
11
12
|
assertErrorMatch,
|
|
12
13
|
assertFullMatch,
|
|
14
|
+
assertPartialMatch,
|
|
13
15
|
assertSchemaMatch,
|
|
16
|
+
errResultMatch,
|
|
14
17
|
expectErr,
|
|
15
18
|
expectOk,
|
|
19
|
+
okResultMatch,
|
|
16
20
|
} from './assertions.js';
|
|
17
21
|
|
|
22
|
+
// Scenario testing
|
|
23
|
+
export { executeScenarioSteps, ref, scenario } from './scenario.js';
|
|
24
|
+
|
|
18
25
|
// Mock factories
|
|
19
26
|
export {
|
|
20
27
|
createCrossContext,
|
|
21
28
|
createTestContext,
|
|
22
|
-
|
|
29
|
+
defaultCreatePermit,
|
|
23
30
|
} from './context.js';
|
|
24
31
|
export { createTestLogger } from './logger.js';
|
|
25
32
|
|
|
26
|
-
//
|
|
33
|
+
// Surface harnesses
|
|
27
34
|
export { createCliHarness } from './harness-cli.js';
|
|
28
35
|
export { createMcpHarness } from './harness-mcp.js';
|
|
29
36
|
|
|
30
37
|
// Types
|
|
31
38
|
export type { CreateCrossContextOptions } from './context.js';
|
|
32
39
|
export type {
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
PermittedTrail,
|
|
41
|
+
MinimalPermit,
|
|
35
42
|
TestExecutionOptions,
|
|
36
43
|
} from './context.js';
|
|
37
44
|
export type { TestCrossOptions } from './crosses.js';
|
|
38
45
|
|
|
39
46
|
export type {
|
|
40
47
|
CrossScenario,
|
|
48
|
+
RefToken,
|
|
49
|
+
ScenarioStep,
|
|
50
|
+
TestAllEstablishedOptions,
|
|
41
51
|
TestScenario,
|
|
42
52
|
TestLogger,
|
|
43
53
|
TestTrailContextOptions,
|
package/src/logger.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Test logger that captures log records for assertion.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { LogLevel,
|
|
5
|
+
import type { LogLevel, LogRecord } from '@ontrails/observe';
|
|
6
6
|
|
|
7
7
|
import type { TestLogger } from './types.js';
|
|
8
8
|
|
|
9
|
+
type LogMetadata = Readonly<Record<string, unknown>>;
|
|
10
|
+
|
|
9
11
|
// ---------------------------------------------------------------------------
|
|
10
12
|
// Level ordering for filtering
|
|
11
13
|
// ---------------------------------------------------------------------------
|