@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.
Files changed (88) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +12 -12
  3. package/package.json +15 -6
  4. package/src/all.ts +153 -16
  5. package/src/assertions.ts +253 -0
  6. package/src/context.ts +67 -38
  7. package/src/contracts.ts +15 -24
  8. package/src/crosses.ts +93 -51
  9. package/src/detours.ts +155 -18
  10. package/src/effective-examples.ts +350 -0
  11. package/src/examples.ts +139 -63
  12. package/src/harness-cli.ts +19 -11
  13. package/src/harness-mcp.ts +9 -8
  14. package/src/index.ts +14 -4
  15. package/src/logger.ts +3 -1
  16. package/src/scenario.ts +368 -0
  17. package/src/signals.ts +221 -0
  18. package/src/types.ts +64 -6
  19. package/.turbo/turbo-build.log +0 -1
  20. package/.turbo/turbo-lint.log +0 -3
  21. package/.turbo/turbo-typecheck.log +0 -1
  22. package/dist/all.d.ts +0 -31
  23. package/dist/all.d.ts.map +0 -1
  24. package/dist/all.js +0 -47
  25. package/dist/all.js.map +0 -1
  26. package/dist/assertions.d.ts +0 -49
  27. package/dist/assertions.d.ts.map +0 -1
  28. package/dist/assertions.js +0 -84
  29. package/dist/assertions.js.map +0 -1
  30. package/dist/context.d.ts +0 -75
  31. package/dist/context.d.ts.map +0 -1
  32. package/dist/context.js +0 -107
  33. package/dist/context.js.map +0 -1
  34. package/dist/contracts.d.ts +0 -17
  35. package/dist/contracts.d.ts.map +0 -1
  36. package/dist/contracts.js +0 -71
  37. package/dist/contracts.js.map +0 -1
  38. package/dist/crosses.d.ts +0 -38
  39. package/dist/crosses.d.ts.map +0 -1
  40. package/dist/crosses.js +0 -213
  41. package/dist/crosses.js.map +0 -1
  42. package/dist/detours.d.ts +0 -12
  43. package/dist/detours.d.ts.map +0 -1
  44. package/dist/detours.js +0 -30
  45. package/dist/detours.js.map +0 -1
  46. package/dist/examples.d.ts +0 -23
  47. package/dist/examples.d.ts.map +0 -1
  48. package/dist/examples.js +0 -202
  49. package/dist/examples.js.map +0 -1
  50. package/dist/follows.d.ts +0 -38
  51. package/dist/follows.d.ts.map +0 -1
  52. package/dist/follows.js +0 -212
  53. package/dist/follows.js.map +0 -1
  54. package/dist/harness-cli.d.ts +0 -21
  55. package/dist/harness-cli.d.ts.map +0 -1
  56. package/dist/harness-cli.js +0 -200
  57. package/dist/harness-cli.js.map +0 -1
  58. package/dist/harness-mcp.d.ts +0 -21
  59. package/dist/harness-mcp.d.ts.map +0 -1
  60. package/dist/harness-mcp.js +0 -53
  61. package/dist/harness-mcp.js.map +0 -1
  62. package/dist/index.d.ts +0 -16
  63. package/dist/index.d.ts.map +0 -1
  64. package/dist/index.js +0 -16
  65. package/dist/index.js.map +0 -1
  66. package/dist/logger.d.ts +0 -15
  67. package/dist/logger.d.ts.map +0 -1
  68. package/dist/logger.js +0 -87
  69. package/dist/logger.js.map +0 -1
  70. package/dist/trail.d.ts +0 -20
  71. package/dist/trail.d.ts.map +0 -1
  72. package/dist/trail.js +0 -80
  73. package/dist/trail.js.map +0 -1
  74. package/dist/types.d.ts +0 -80
  75. package/dist/types.d.ts.map +0 -1
  76. package/dist/types.js +0 -5
  77. package/dist/types.js.map +0 -1
  78. package/src/__tests__/all.test.ts +0 -135
  79. package/src/__tests__/context.test.ts +0 -150
  80. package/src/__tests__/contracts.test.ts +0 -185
  81. package/src/__tests__/crosses.test.ts +0 -587
  82. package/src/__tests__/detours.test.ts +0 -55
  83. package/src/__tests__/examples.test.ts +0 -534
  84. package/src/__tests__/harness-cli.test.ts +0 -66
  85. package/src/__tests__/logger.test.ts +0 -136
  86. package/src/__tests__/trail.test.ts +0 -99
  87. package/tsconfig.json +0 -9
  88. 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
- ProvisionOverrideMap,
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
- defaultMintPermit,
49
- mergeProvisionOverrides,
53
+ defaultCreatePermit,
54
+ mergeResourceOverrides,
50
55
  mergeTestContext,
51
56
  normalizeTestExecutionOptions,
52
- resolveMockProvisions,
57
+ createMockResources,
53
58
  } from './context.js';
54
- import type { MintableTrail, TestExecutionOptions } from './context.js';
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): (new (...args: never[]) => Error) =>
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
- return;
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
- const errorClass = resolveErrorClass(example.error);
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-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.
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 applyAutoMint = (
145
+ const applyAutoPermit = (
136
146
  ctx: TrailContext,
137
- trailDef: MintableTrail,
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 mint = opts.mintPermit ?? defaultMintPermit;
147
- const permit = mint(trailDef);
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
- provisions?: ProvisionOverrideMap,
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
- provisions: provisions ?? opts?.provisions,
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
- provisions?: ProvisionOverrideMap
209
+ resources?: ResourceOverrideMap
198
210
  ): CrossFn => {
199
- const cross = (id: string, input: unknown) => {
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
- provisions,
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 Promise.resolve(Result.ok());
232
+ return Result.ok();
215
233
  };
216
- return cross as CrossFn;
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
- provisions?: ProvisionOverrideMap,
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
- mintedCtx.cross,
288
+ signals.ctx.cross,
242
289
  topo,
243
- mintedCtx,
244
- provisions
290
+ signals.ctx,
291
+ resources
245
292
  );
246
- const testCtx: TrailContext = { ...mintedCtx, cross };
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
- provisions: provisions ?? opts?.provisions,
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(app);
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 allTrails = app.list() as Trail<unknown, unknown>[];
280
-
281
- const withExamples = allTrails.filter(
282
- (t) => t.examples !== undefined && t.examples.length > 0
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 provisions = mergeProvisionOverrides(
300
- await resolveMockProvisions(app),
350
+ const resources = mergeResourceOverrides(
351
+ await createMockResources(app),
301
352
  resolved.ctx,
302
- resolved.provisions
353
+ resolved.resources
303
354
  );
304
355
  const testCtx = mergeTestContext(resolved.ctx);
305
- await runExample(t, example, output, testCtx, provisions, resolved);
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 called = new Set<string>();
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 provisions = mergeProvisionOverrides(
326
- await resolveMockProvisions(app),
397
+ const resources = mergeResourceOverrides(
398
+ await createMockResources(app),
327
399
  resolved.ctx,
328
- resolved.provisions
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
- called,
408
+ pickCoverageSink(example),
337
409
  app,
338
- provisions,
410
+ resources,
339
411
  resolved
340
412
  );
341
413
  }
342
414
  );
343
415
 
344
- test('crossing coverage', () => {
345
- const uncovered = t.crosses.filter((id) => !called.has(id));
346
- expect(uncovered).toEqual([]);
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
  };
@@ -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,
@@ -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 = createTestContext();
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 app's topo and provides a `run()` method
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({ app });
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 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;
282
290
 
283
291
  return {
284
- run: (commandString: string) => runCommand(commands, commandString),
292
+ run: (commandString: string) => runCommand(commands, commandString, ctx),
285
293
  };
286
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,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
- defaultMintPermit,
29
+ defaultCreatePermit,
23
30
  } from './context.js';
24
31
  export { createTestLogger } from './logger.js';
25
32
 
26
- // Trailhead harnesses
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
- 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,
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, LogMetadata, LogRecord } from '@ontrails/logging';
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
  // ---------------------------------------------------------------------------