@ontrails/testing 1.0.0-beta.13 → 1.0.0-beta.15
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/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +28 -0
- package/README.md +11 -11
- package/dist/all.d.ts +10 -5
- package/dist/all.d.ts.map +1 -1
- package/dist/all.js +78 -26
- package/dist/all.js.map +1 -1
- package/dist/assertions.d.ts +23 -0
- package/dist/assertions.d.ts.map +1 -1
- package/dist/assertions.js +154 -0
- package/dist/assertions.js.map +1 -1
- package/dist/context.d.ts +17 -16
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +31 -20
- package/dist/context.js.map +1 -1
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +9 -5
- package/dist/contracts.js.map +1 -1
- package/dist/crosses.d.ts +4 -4
- package/dist/crosses.d.ts.map +1 -1
- package/dist/crosses.js +49 -39
- package/dist/crosses.js.map +1 -1
- package/dist/detours.d.ts +5 -4
- package/dist/detours.d.ts.map +1 -1
- package/dist/detours.js +93 -14
- package/dist/detours.js.map +1 -1
- package/dist/effective-examples.d.ts +30 -0
- package/dist/effective-examples.d.ts.map +1 -0
- package/dist/effective-examples.js +227 -0
- package/dist/effective-examples.js.map +1 -0
- package/dist/examples.d.ts +1 -1
- package/dist/examples.d.ts.map +1 -1
- package/dist/examples.js +79 -41
- package/dist/examples.js.map +1 -1
- package/dist/harness-cli.d.ts +3 -3
- package/dist/harness-cli.d.ts.map +1 -1
- package/dist/harness-cli.js +25 -33
- package/dist/harness-cli.js.map +1 -1
- package/dist/harness-mcp.d.ts +3 -3
- package/dist/harness-mcp.d.ts.map +1 -1
- package/dist/harness-mcp.js +9 -8
- package/dist/harness-mcp.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/scenario.d.ts +37 -0
- package/dist/scenario.d.ts.map +1 -0
- package/dist/scenario.js +235 -0
- package/dist/scenario.js.map +1 -0
- package/dist/types.d.ts +38 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -5
- package/src/__tests__/all.test.ts +217 -29
- package/src/__tests__/context.test.ts +32 -12
- package/src/__tests__/contracts.test.ts +72 -18
- package/src/__tests__/crosses.test.ts +78 -78
- package/src/__tests__/detours.test.ts +176 -19
- package/src/__tests__/effective-examples.test.ts +203 -0
- package/src/__tests__/examples.test.ts +152 -50
- package/src/__tests__/harness-cli.test.ts +90 -0
- package/src/__tests__/harness-mcp.test.ts +37 -0
- package/src/__tests__/partial-match.test.ts +126 -0
- package/src/__tests__/scenario.test.ts +381 -0
- package/src/all.ts +149 -12
- package/src/assertions.ts +253 -0
- package/src/context.ts +64 -38
- package/src/contracts.ts +14 -8
- package/src/crosses.ts +93 -51
- package/src/detours.ts +155 -18
- package/src/effective-examples.ts +350 -0
- package/src/examples.ts +127 -59
- package/src/harness-cli.ts +33 -49
- package/src/harness-mcp.ts +9 -8
- package/src/index.ts +13 -3
- package/src/scenario.ts +370 -0
- package/src/types.ts +63 -5
- package/tsconfig.tests.json +10 -0
- package/tsconfig.tsbuildinfo +1 -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/src/scenario.ts
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-step scenario runner for composition testing.
|
|
3
|
+
*
|
|
4
|
+
* Scenarios express multi-trail flows as structured data — arrays of steps
|
|
5
|
+
* with cross-step references via `ref()`. Each step invokes a trail through
|
|
6
|
+
* the normal execution pipeline (validation, layers, blaze, Result).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, test } from 'bun:test';
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
AnyTrail,
|
|
13
|
+
CrossBatchOptions,
|
|
14
|
+
CrossFn,
|
|
15
|
+
ResourceOverrideMap,
|
|
16
|
+
Result,
|
|
17
|
+
Topo,
|
|
18
|
+
} from '@ontrails/core';
|
|
19
|
+
import {
|
|
20
|
+
buildCrossValidationSchema,
|
|
21
|
+
executeTrail,
|
|
22
|
+
InternalError,
|
|
23
|
+
Result as R,
|
|
24
|
+
} from '@ontrails/core';
|
|
25
|
+
import {
|
|
26
|
+
claimNextCrossBatchIndex,
|
|
27
|
+
createCrossBatchValidationResults,
|
|
28
|
+
normalizeCrossBatchConcurrency,
|
|
29
|
+
} from '@ontrails/core/internal/cross-batch';
|
|
30
|
+
|
|
31
|
+
import { assertPartialMatch, expectOk } from './assertions.js';
|
|
32
|
+
import { createTestContext, createMockResources } from './context.js';
|
|
33
|
+
import type { RefToken, ScenarioStep } from './types.js';
|
|
34
|
+
|
|
35
|
+
type ScenarioCrossTarget = string | { readonly id: string };
|
|
36
|
+
type ScenarioCrossCall = readonly [ScenarioCrossTarget, unknown];
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// ref() — cross-step reference marker
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a reference marker for cross-step data in scenario inputs.
|
|
44
|
+
*
|
|
45
|
+
* `ref('create.id')` resolves to the `id` field of the step aliased as
|
|
46
|
+
* `create`. Dot-paths are supported for nested access.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* scenario('Fork flow', app, [
|
|
51
|
+
* { cross: createGist, input: { name: 'Hello' }, as: 'original' },
|
|
52
|
+
* { cross: forkGist, input: { id: ref('original.id') } },
|
|
53
|
+
* ]);
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export const ref = (path: string): RefToken => ({ __ref: true, path });
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Internals
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/** Type guard for RefToken. */
|
|
63
|
+
const isRef = (value: unknown): value is RefToken =>
|
|
64
|
+
typeof value === 'object' &&
|
|
65
|
+
value !== null &&
|
|
66
|
+
'__ref' in value &&
|
|
67
|
+
(value as Record<string, unknown>)['__ref'] === true &&
|
|
68
|
+
'path' in value;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a dot-path against the outputs map.
|
|
72
|
+
*
|
|
73
|
+
* `ref('create.id')` splits into step name `create` and field path `id`.
|
|
74
|
+
* The first segment is the step alias; remaining segments are property lookups.
|
|
75
|
+
*/
|
|
76
|
+
/**
|
|
77
|
+
* Walk remaining segments of a dot-path, drilling into the step output.
|
|
78
|
+
*/
|
|
79
|
+
const drillPath = (
|
|
80
|
+
path: string,
|
|
81
|
+
segments: readonly string[],
|
|
82
|
+
start: unknown
|
|
83
|
+
): unknown => {
|
|
84
|
+
let current: unknown = start;
|
|
85
|
+
for (let i = 1; i < segments.length; i += 1) {
|
|
86
|
+
const segment = segments[i];
|
|
87
|
+
if (segment === undefined) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
if (typeof current !== 'object' || current === null) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`ref('${path}'): cannot access '${segment}' on ${typeof current}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
current = (current as Record<string, unknown>)[segment];
|
|
96
|
+
}
|
|
97
|
+
return current;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const resolvePath = (path: string, outputs: Map<string, unknown>): unknown => {
|
|
101
|
+
const segments = path.split('.');
|
|
102
|
+
const [stepName] = segments;
|
|
103
|
+
if (stepName === undefined) {
|
|
104
|
+
throw new Error(`ref(): empty path`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const stepOutput = outputs.get(stepName);
|
|
108
|
+
if (stepOutput === undefined) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`ref('${path}'): no step output found for alias '${stepName}'`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return drillPath(path, segments, stepOutput);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Recursively walk a value, replacing RefToken instances with resolved values.
|
|
119
|
+
*/
|
|
120
|
+
export const deriveRefs = (
|
|
121
|
+
value: unknown,
|
|
122
|
+
outputs: Map<string, unknown>
|
|
123
|
+
): unknown => {
|
|
124
|
+
if (isRef(value)) {
|
|
125
|
+
return resolvePath(value.path, outputs);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (Array.isArray(value)) {
|
|
129
|
+
return value.map((item) => deriveRefs(item, outputs));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (typeof value === 'object' && value !== null) {
|
|
133
|
+
const result: Record<string, unknown> = {};
|
|
134
|
+
for (const [key, val] of Object.entries(value)) {
|
|
135
|
+
result[key] = deriveRefs(val, outputs);
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return value;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// scenario() — the public API
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Define a multi-step scenario test for composition flows.
|
|
149
|
+
*
|
|
150
|
+
* Each step invokes a trail through the normal execution pipeline.
|
|
151
|
+
* Steps can reference prior step outputs via `ref()`. If any step
|
|
152
|
+
* fails, the scenario stops and reports which step failed.
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```typescript
|
|
156
|
+
* scenario('Create and show', app, [
|
|
157
|
+
* { cross: createItem, input: { name: 'Test' }, as: 'created' },
|
|
158
|
+
* { cross: showItem, input: { id: ref('created.id') },
|
|
159
|
+
* expectedMatch: { found: true } },
|
|
160
|
+
* ]);
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
/** Assert the result of a step against its expectations and record output. */
|
|
164
|
+
const assertStepExpectations = async (
|
|
165
|
+
step: ScenarioStep,
|
|
166
|
+
result: Result<unknown, Error>,
|
|
167
|
+
outputs: Map<string, unknown>
|
|
168
|
+
): Promise<void> => {
|
|
169
|
+
const value = expectOk(result);
|
|
170
|
+
if (step.expected !== undefined) {
|
|
171
|
+
const { expect } = await import('bun:test');
|
|
172
|
+
expect(value).toEqual(deriveRefs(step.expected, outputs));
|
|
173
|
+
} else if (step.expectedMatch !== undefined) {
|
|
174
|
+
assertPartialMatch(result, deriveRefs(step.expectedMatch, outputs));
|
|
175
|
+
}
|
|
176
|
+
if (step.as !== undefined) {
|
|
177
|
+
outputs.set(step.as, value);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const executeUnlimitedCrossBatch = async (
|
|
182
|
+
calls: readonly ScenarioCrossCall[],
|
|
183
|
+
runCall: (
|
|
184
|
+
call: ScenarioCrossCall,
|
|
185
|
+
branchIndex: number
|
|
186
|
+
) => Promise<Result<unknown, Error>>
|
|
187
|
+
): Promise<Result<unknown, Error>[]> =>
|
|
188
|
+
await Promise.all(
|
|
189
|
+
calls.map((call, branchIndex) => runCall(call, branchIndex))
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const executeLimitedCrossBatch = async (
|
|
193
|
+
calls: readonly ScenarioCrossCall[],
|
|
194
|
+
runCall: (
|
|
195
|
+
call: ScenarioCrossCall,
|
|
196
|
+
branchIndex: number
|
|
197
|
+
) => Promise<Result<unknown, Error>>,
|
|
198
|
+
limit: number
|
|
199
|
+
): Promise<Result<unknown, Error>[]> => {
|
|
200
|
+
const results = Array.from<Result<unknown, Error>>({ length: calls.length });
|
|
201
|
+
const nextIndex = { value: 0 };
|
|
202
|
+
|
|
203
|
+
const runWorker = async () => {
|
|
204
|
+
while (true) {
|
|
205
|
+
const branchIndex = claimNextCrossBatchIndex(nextIndex, calls);
|
|
206
|
+
if (branchIndex === undefined) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const call = calls[branchIndex];
|
|
211
|
+
if (call === undefined) {
|
|
212
|
+
// Defensive: `claimNextCrossBatchIndex` only returns indices within
|
|
213
|
+
// bounds, so this slot should always be populated. If it ever isn't,
|
|
214
|
+
// surface a clear InternalError in place of the missing slot and keep
|
|
215
|
+
// the worker loop running so sibling branches still get processed.
|
|
216
|
+
results[branchIndex] = R.err(
|
|
217
|
+
new InternalError(
|
|
218
|
+
`unreachable: concurrent cross batch call missing at index ${branchIndex}`
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
results[branchIndex] = await runCall(call, branchIndex);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
await Promise.all(Array.from({ length: limit }, runWorker));
|
|
229
|
+
return results;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Build a cross function that resolves trails from the topo and executes
|
|
234
|
+
* them through the standard pipeline. Mirrors the pattern in crosses.ts
|
|
235
|
+
* `executeFromMap` but without recording or injection.
|
|
236
|
+
*/
|
|
237
|
+
const createScenarioCross = (
|
|
238
|
+
app: Topo,
|
|
239
|
+
resources?: ResourceOverrideMap
|
|
240
|
+
): CrossFn => {
|
|
241
|
+
const invokeCross = async (
|
|
242
|
+
idOrTrail: ScenarioCrossTarget,
|
|
243
|
+
input: unknown,
|
|
244
|
+
self: CrossFn
|
|
245
|
+
) => {
|
|
246
|
+
const id = typeof idOrTrail === 'string' ? idOrTrail : idOrTrail.id;
|
|
247
|
+
const trailDef: AnyTrail | undefined = app.get(id);
|
|
248
|
+
if (trailDef === undefined) {
|
|
249
|
+
return R.err(new InternalError(`cross: trail "${id}" not found in topo`));
|
|
250
|
+
}
|
|
251
|
+
const baseCtx = createTestContext();
|
|
252
|
+
return await executeTrail(trailDef, input, {
|
|
253
|
+
ctx: { ...baseCtx, cross: self },
|
|
254
|
+
resources,
|
|
255
|
+
topo: app,
|
|
256
|
+
validationSchema: buildCrossValidationSchema(trailDef),
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const executeCrossBatch = async (
|
|
261
|
+
calls: readonly ScenarioCrossCall[],
|
|
262
|
+
self: CrossFn,
|
|
263
|
+
options?: CrossBatchOptions
|
|
264
|
+
): Promise<Result<unknown, Error>[]> => {
|
|
265
|
+
if (calls.length === 0) {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const concurrency = normalizeCrossBatchConcurrency(options);
|
|
270
|
+
if (concurrency.isErr()) {
|
|
271
|
+
return createCrossBatchValidationResults(calls, concurrency.error);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const runCall = async (
|
|
275
|
+
[target, batchInput]: ScenarioCrossCall,
|
|
276
|
+
_branchIndex: number
|
|
277
|
+
) => await invokeCross(target, batchInput, self);
|
|
278
|
+
|
|
279
|
+
const limit = concurrency.value ?? calls.length;
|
|
280
|
+
return limit >= calls.length
|
|
281
|
+
? await executeUnlimitedCrossBatch(calls, runCall)
|
|
282
|
+
: await executeLimitedCrossBatch(calls, runCall, limit);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const cross = async function cross(
|
|
286
|
+
idOrTrail: ScenarioCrossTarget | readonly ScenarioCrossCall[],
|
|
287
|
+
inputOrOptions?: unknown
|
|
288
|
+
) {
|
|
289
|
+
if (Array.isArray(idOrTrail)) {
|
|
290
|
+
return await executeCrossBatch(
|
|
291
|
+
idOrTrail,
|
|
292
|
+
cross as CrossFn,
|
|
293
|
+
inputOrOptions as CrossBatchOptions | undefined
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return await invokeCross(
|
|
298
|
+
idOrTrail as ScenarioCrossTarget,
|
|
299
|
+
inputOrOptions,
|
|
300
|
+
cross as CrossFn
|
|
301
|
+
);
|
|
302
|
+
} as CrossFn;
|
|
303
|
+
|
|
304
|
+
return cross;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Execute a single scenario step: run the trail, assert expectations,
|
|
309
|
+
* and record outputs.
|
|
310
|
+
*/
|
|
311
|
+
const executeStep = async (
|
|
312
|
+
step: ScenarioStep,
|
|
313
|
+
index: number,
|
|
314
|
+
app: Topo,
|
|
315
|
+
outputs: Map<string, unknown>,
|
|
316
|
+
resources?: ResourceOverrideMap
|
|
317
|
+
): Promise<void> => {
|
|
318
|
+
if (step.as !== undefined && outputs.has(step.as)) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`scenario: duplicate step alias "${step.as}" — each alias must be unique`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const scenarioCross = createScenarioCross(app, resources);
|
|
325
|
+
const baseCtx = createTestContext();
|
|
326
|
+
const resolvedInput = deriveRefs(step.input, outputs);
|
|
327
|
+
const result = await executeTrail(step.cross, resolvedInput, {
|
|
328
|
+
ctx: { ...baseCtx, cross: scenarioCross },
|
|
329
|
+
resources,
|
|
330
|
+
topo: app,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (result.isErr()) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`Step ${String(index + 1)} ("${step.as ?? step.cross.id}") failed: ${result.error.message}`
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
await assertStepExpectations(step, result, outputs);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Execute scenario steps sequentially, resolving mock resources once upfront.
|
|
344
|
+
*
|
|
345
|
+
* Exported for direct use in tests that need to assert on step execution
|
|
346
|
+
* without the describe/test wrapper that `scenario()` provides.
|
|
347
|
+
*/
|
|
348
|
+
export const executeScenarioSteps = async (
|
|
349
|
+
app: Topo,
|
|
350
|
+
steps: readonly ScenarioStep[]
|
|
351
|
+
): Promise<void> => {
|
|
352
|
+
const outputs = new Map<string, unknown>();
|
|
353
|
+
const resources = await createMockResources(app);
|
|
354
|
+
|
|
355
|
+
for (const [index, step] of steps.entries()) {
|
|
356
|
+
await executeStep(step, index, app, outputs, resources);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
export const scenario = (
|
|
361
|
+
name: string,
|
|
362
|
+
app: Topo,
|
|
363
|
+
steps: readonly ScenarioStep[]
|
|
364
|
+
): void => {
|
|
365
|
+
describe(name, () => {
|
|
366
|
+
test('executes all steps', async () => {
|
|
367
|
+
await executeScenarioSteps(app, steps);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
* Shared types for @ontrails/testing.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { DeriveCliCommandsOptions } from '@ontrails/cli';
|
|
6
|
+
import type { McpExtra, DeriveMcpToolsOptions } from '@ontrails/mcp';
|
|
7
|
+
import type {
|
|
8
|
+
AnyTrail,
|
|
9
|
+
Logger,
|
|
10
|
+
Topo,
|
|
11
|
+
TraceFn,
|
|
12
|
+
TrailContext,
|
|
13
|
+
} from '@ontrails/core';
|
|
6
14
|
import type { LogLevel, LogRecord } from '@ontrails/logging';
|
|
7
15
|
|
|
8
16
|
// ---------------------------------------------------------------------------
|
|
@@ -66,6 +74,7 @@ export interface TestTrailContextOptions {
|
|
|
66
74
|
readonly logger?: Logger | undefined;
|
|
67
75
|
readonly requestId?: string | undefined;
|
|
68
76
|
readonly abortSignal?: AbortSignal | undefined;
|
|
77
|
+
readonly trace?: TraceFn | undefined;
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
// ---------------------------------------------------------------------------
|
|
@@ -73,8 +82,12 @@ export interface TestTrailContextOptions {
|
|
|
73
82
|
// ---------------------------------------------------------------------------
|
|
74
83
|
|
|
75
84
|
/** Options for creating a CLI harness. */
|
|
76
|
-
export interface CliHarnessOptions
|
|
77
|
-
|
|
85
|
+
export interface CliHarnessOptions extends Omit<
|
|
86
|
+
DeriveCliCommandsOptions,
|
|
87
|
+
'onResult' | 'presets' | 'resolveInput'
|
|
88
|
+
> {
|
|
89
|
+
readonly ctx?: Partial<TrailContext> | undefined;
|
|
90
|
+
readonly graph: Topo;
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
/** A test harness for CLI commands. */
|
|
@@ -97,8 +110,9 @@ export interface CliHarnessResult {
|
|
|
97
110
|
// ---------------------------------------------------------------------------
|
|
98
111
|
|
|
99
112
|
/** Options for creating an MCP harness. */
|
|
100
|
-
export interface McpHarnessOptions {
|
|
101
|
-
readonly
|
|
113
|
+
export interface McpHarnessOptions extends DeriveMcpToolsOptions {
|
|
114
|
+
readonly extra?: Partial<McpExtra> | undefined;
|
|
115
|
+
readonly graph: Topo;
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
/** A test harness for MCP tools. */
|
|
@@ -115,3 +129,47 @@ export interface McpHarnessResult {
|
|
|
115
129
|
readonly content: unknown;
|
|
116
130
|
readonly isError: boolean;
|
|
117
131
|
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Established verification
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
export interface TestAllEstablishedOptions {
|
|
138
|
+
readonly cli?: Omit<CliHarnessOptions, 'graph'> | undefined;
|
|
139
|
+
readonly createPermit?:
|
|
140
|
+
| ((trail: {
|
|
141
|
+
readonly permit?:
|
|
142
|
+
| { readonly scopes: readonly string[] }
|
|
143
|
+
| 'public'
|
|
144
|
+
| undefined;
|
|
145
|
+
}) =>
|
|
146
|
+
| {
|
|
147
|
+
readonly id: string;
|
|
148
|
+
readonly scopes: readonly string[];
|
|
149
|
+
}
|
|
150
|
+
| undefined)
|
|
151
|
+
| undefined;
|
|
152
|
+
readonly ctx?: Partial<TrailContext> | undefined;
|
|
153
|
+
readonly mcp?: Omit<McpHarnessOptions, 'graph'> | undefined;
|
|
154
|
+
readonly resources?: Record<string, unknown> | undefined;
|
|
155
|
+
readonly strictPermits?: boolean | undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Scenario (for composition testing)
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
/** Marker for cross-step references in scenario inputs. */
|
|
163
|
+
export interface RefToken {
|
|
164
|
+
readonly __ref: true;
|
|
165
|
+
readonly path: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** A single step in a scenario. */
|
|
169
|
+
export interface ScenarioStep {
|
|
170
|
+
readonly cross: AnyTrail;
|
|
171
|
+
readonly input: Record<string, unknown>;
|
|
172
|
+
readonly as?: string | undefined;
|
|
173
|
+
readonly expected?: unknown | undefined;
|
|
174
|
+
readonly expectedMatch?: unknown | undefined;
|
|
175
|
+
}
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/all.ts","./src/assertions.ts","./src/context.ts","./src/contracts.ts","./src/crosses.ts","./src/detours.ts","./src/examples.ts","./src/harness-cli.ts","./src/harness-mcp.ts","./src/index.ts","./src/logger.ts","./src/trail.ts","./src/types.ts"],"version":"5.9.3"}
|
|
1
|
+
{"root":["./src/all.ts","./src/assertions.ts","./src/context.ts","./src/contracts.ts","./src/crosses.ts","./src/detours.ts","./src/effective-examples.ts","./src/examples.ts","./src/harness-cli.ts","./src/harness-mcp.ts","./src/index.ts","./src/logger.ts","./src/scenario.ts","./src/trail.ts","./src/types.ts"],"version":"5.9.3"}
|
package/dist/follows.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* testFollows — composition-aware scenario testing for trails with follow.
|
|
3
|
-
*
|
|
4
|
-
* Tests the follow graph: which trails were followed, in what order,
|
|
5
|
-
* and supports failure injection from followed trail examples.
|
|
6
|
-
*/
|
|
7
|
-
import type { AnyTrail, ServiceOverrideMap, TrailContext } from '@ontrails/core';
|
|
8
|
-
import type { FollowScenario } from './types.js';
|
|
9
|
-
/** Options for testFollows that provide trail definitions for injection. */
|
|
10
|
-
export interface TestFollowOptions {
|
|
11
|
-
/** Partial context overrides. */
|
|
12
|
-
readonly ctx?: Partial<TrailContext> | undefined;
|
|
13
|
-
/**
|
|
14
|
-
* Explicit service overrides merged on top of auto-resolved mocks for every
|
|
15
|
-
* scenario. Values are passed by reference — provide immutable objects, or
|
|
16
|
-
* use `mock()` on the service definition to get a fresh instance per run.
|
|
17
|
-
*/
|
|
18
|
-
readonly services?: ServiceOverrideMap | undefined;
|
|
19
|
-
/** Map of trail ID to trail definition, used for injectFromExample. */
|
|
20
|
-
readonly trails?: ReadonlyMap<string, AnyTrail> | undefined;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Generate a describe block for a composition trail with one test per scenario.
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* ```ts
|
|
27
|
-
* testFollows(onboardTrail, [
|
|
28
|
-
* {
|
|
29
|
-
* description: "follows add then relate",
|
|
30
|
-
* input: { name: "Alpha" },
|
|
31
|
-
* expectOk: true,
|
|
32
|
-
* expectFollowed: ["entity.add", "entity.relate"],
|
|
33
|
-
* },
|
|
34
|
-
* ]);
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
export declare const testFollows: (trailDef: AnyTrail, scenarios: readonly FollowScenario[], options?: TestFollowOptions) => void;
|
|
38
|
-
//# sourceMappingURL=follows.d.ts.map
|
package/dist/follows.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"follows.d.ts","sourceRoot":"","sources":["../src/follows.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EACV,QAAQ,EAER,kBAAkB,EAClB,YAAY,EACb,MAAM,gBAAgB,CAAC;AAexB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAuSjD,4EAA4E;AAC5E,MAAM,WAAW,iBAAiB;IAChC,iCAAiC;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,SAAS,CAAC;IACjD;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,kBAAkB,GAAG,SAAS,CAAC;IACnD,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,SAAS,CAAC;CAC7D;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,WAAW,GACtB,UAAU,QAAQ,EAClB,WAAW,SAAS,cAAc,EAAE,EACpC,UAAU,iBAAiB,KAC1B,IAoBF,CAAC"}
|