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