@ontrails/testing 1.0.0-beta.7 → 1.0.0-beta.9

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.
@@ -1,3 +1,3 @@
1
1
  $ oxlint ./src
2
2
  Found 0 warnings and 0 errors.
3
- Finished in 28ms on 20 files with 93 rules using 24 threads.
3
+ Finished in 72ms on 20 files with 93 rules using 24 threads.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # @ontrails/testing
2
2
 
3
+ ## 1.0.0-beta.9
4
+
5
+ ### Minor Changes
6
+
7
+ - Consolidated improvements across all surface packages.
8
+
9
+ **core**: Add `TrailResult<T>` utility type, `topo.ids()` and `topo.count` accessors, `dispatch()` for headless trail execution, and extract shared `executeTrail` pipeline used by CLI/MCP/HTTP.
10
+
11
+ **http**: Detect route path collisions and return `Result` from `buildHttpRoutes()`, wire request `AbortSignal` through to trail context, and make write → POST mapping explicit in intent-to-method lookup.
12
+
13
+ **mcp**: Return `Result` from `buildMcpTools()` on collision instead of throwing.
14
+
15
+ **cli**: Verify exception catching via centralized `executeTrail`.
16
+
17
+ **testing**: Follow context awareness improvements.
18
+
19
+ **warden**: Refactor rules as composable trails with examples.
20
+
21
+ **schema**: Error code and empty body fixes.
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies
26
+ - @ontrails/core@1.0.0-beta.9
27
+ - @ontrails/cli@1.0.0-beta.9
28
+ - @ontrails/mcp@1.0.0-beta.9
29
+ - @ontrails/logging@1.0.0-beta.9
30
+
31
+ ## 1.0.0-beta.8
32
+
33
+ ### Patch Changes
34
+
35
+ - @ontrails/cli@1.0.0-beta.8
36
+ - @ontrails/core@1.0.0-beta.8
37
+ - @ontrails/logging@1.0.0-beta.8
38
+ - @ontrails/mcp@1.0.0-beta.8
39
+
3
40
  ## 1.0.0-beta.7
4
41
 
5
42
  ### Patch Changes
package/README.md CHANGED
@@ -32,6 +32,7 @@ testDetours(app); // Verify detour targets exist
32
32
  | `testTrail(trail, scenarios)` | Custom scenarios for edge cases, error paths, and follow chains |
33
33
  | `testContracts(topo, ctx?)` | Validate output against declared schemas |
34
34
  | `testDetours(topo)` | Verify every detour target exists in the topo |
35
+ | `createFollowContext(options?)` | Mock `FollowFn` for testing composite trails; returns preconfigured `Result` values keyed by trail ID |
35
36
  | `createTestContext(options?)` | `TrailContext` with sensible test defaults |
36
37
  | `createTestLogger()` | Logger that captures entries in memory for assertions |
37
38
  | `createCliHarness(options)` | Execute CLI commands in-process, capture stdout/stderr |
@@ -67,6 +68,24 @@ testTrail(onboardTrail, [
67
68
  ]);
68
69
  ```
69
70
 
71
+ When you need to isolate a composite trail and stub out its dependencies, use `createFollowContext`:
72
+
73
+ ```typescript
74
+ import { createFollowContext, createTestContext } from '@ontrails/testing';
75
+ import { Result } from '@ontrails/core';
76
+
77
+ const follow = createFollowContext({
78
+ responses: {
79
+ 'entity.add': Result.ok({ id: '1', name: 'Delta', type: 'tool' }),
80
+ 'search': Result.ok({ results: [] }),
81
+ },
82
+ });
83
+ const ctx = { ...createTestContext(), follow };
84
+ const result = await onboardTrail.run({ name: 'Delta', type: 'tool' }, ctx);
85
+ ```
86
+
87
+ Calls to unregistered trail IDs return `Result.err` with a descriptive message, so missing stubs fail loudly.
88
+
70
89
  ## Surface harnesses
71
90
 
72
91
  ```typescript
package/dist/context.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Test context factory for creating TrailContext instances suitable for testing.
3
3
  */
4
- import type { TrailContext } from '@ontrails/core';
4
+ import type { FollowFn, TrailContext } from '@ontrails/core';
5
+ import { Result } from '@ontrails/core';
5
6
  import type { TestTrailContextOptions } from './types.js';
6
7
  /**
7
8
  * Create a TrailContext with deterministic, test-friendly defaults.
@@ -11,6 +12,24 @@ import type { TestTrailContextOptions } from './types.js';
11
12
  * - `signal`: a non-aborted AbortController signal
12
13
  */
13
14
  export declare const createTestContext: (overrides?: TestTrailContextOptions) => TrailContext;
15
+ export interface CreateFollowContextOptions {
16
+ readonly responses?: Record<string, Result<unknown, Error>> | undefined;
17
+ }
18
+ /**
19
+ * Create a mock `FollowFn` for testing composite trails.
20
+ *
21
+ * Returns preconfigured `Result` values keyed by trail ID. Calls to
22
+ * unregistered IDs return `Result.err` with a descriptive message.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const follow = createFollowContext({
27
+ * responses: { 'entity.add': Result.ok({ id: '1', name: 'Alpha' }) },
28
+ * });
29
+ * const ctx = { ...createTestContext(), follow };
30
+ * ```
31
+ */
32
+ export declare const createFollowContext: (options?: CreateFollowContextOptions) => FollowFn;
14
33
  /**
15
34
  * Merge a Partial<TrailContext> into a test context.
16
35
  * Used internally when the public API accepts Partial<TrailContext>.
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAGnD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAM1D;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAC5B,YAAY,uBAAuB,KAClC,YAMD,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,OAAO,CAAC,YAAY,CAAC,KAAG,YAO9D,CAAC"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAGxC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAM1D;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAC5B,YAAY,uBAAuB,KAClC,YAMD,CAAC;AAMH,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC;CACzE;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,GAC9B,UAAU,0BAA0B,KACnC,QAcF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,OAAO,CAAC,YAAY,CAAC,KAAG,YAO9D,CAAC"}
package/dist/context.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Test context factory for creating TrailContext instances suitable for testing.
3
3
  */
4
+ import { Result } from '@ontrails/core';
4
5
  import { createTestLogger } from './logger.js';
5
6
  // ---------------------------------------------------------------------------
6
7
  // createTestContext
@@ -19,6 +20,30 @@ export const createTestContext = (overrides) => ({
19
20
  signal: overrides?.signal ?? new AbortController().signal,
20
21
  workspaceRoot: overrides?.cwd ?? process.cwd(),
21
22
  });
23
+ /**
24
+ * Create a mock `FollowFn` for testing composite trails.
25
+ *
26
+ * Returns preconfigured `Result` values keyed by trail ID. Calls to
27
+ * unregistered IDs return `Result.err` with a descriptive message.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const follow = createFollowContext({
32
+ * responses: { 'entity.add': Result.ok({ id: '1', name: 'Alpha' }) },
33
+ * });
34
+ * const ctx = { ...createTestContext(), follow };
35
+ * ```
36
+ */
37
+ export const createFollowContext = (options) => {
38
+ const responses = options?.responses ?? {};
39
+ return (id, _input) => {
40
+ const response = responses[id];
41
+ if (response === undefined) {
42
+ return Promise.resolve(Result.err(new Error(`No mock response for follow("${id}")`)));
43
+ }
44
+ return Promise.resolve(response);
45
+ };
46
+ };
22
47
  /**
23
48
  * Merge a Partial<TrailContext> into a test context.
24
49
  * Used internally when the public API accepts Partial<TrailContext>.
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG/C,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,SAAmC,EACrB,EAAE,CAAC,CAAC;IAClB,GAAG,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE;IAC7C,MAAM,EAAE,SAAS,EAAE,MAAM,IAAI,gBAAgB,EAAE;IAC/C,SAAS,EAAE,SAAS,EAAE,SAAS,IAAI,kBAAkB;IACrD,MAAM,EAAE,SAAS,EAAE,MAAM,IAAI,IAAI,eAAe,EAAE,CAAC,MAAM;IACzD,aAAa,EAAE,SAAS,EAAE,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE;CAC/C,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAA2B,EAAgB,EAAE;IAC5E,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,OAAO,iBAAiB,EAAE,CAAC;IAC7B,CAAC;IAED,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAC;IACjC,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,GAAG,EAAE,CAAC;AAC7B,CAAC,CAAC"}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAExC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG/C,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,SAAmC,EACrB,EAAE,CAAC,CAAC;IAClB,GAAG,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE;IAC7C,MAAM,EAAE,SAAS,EAAE,MAAM,IAAI,gBAAgB,EAAE;IAC/C,SAAS,EAAE,SAAS,EAAE,SAAS,IAAI,kBAAkB;IACrD,MAAM,EAAE,SAAS,EAAE,MAAM,IAAI,IAAI,eAAe,EAAE,CAAC,MAAM;IACzD,aAAa,EAAE,SAAS,EAAE,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE;CAC/C,CAAC,CAAC;AAUH;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,OAAoC,EAC1B,EAAE;IACZ,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,EAAE,CAAC;IAC3C,OAAO,CAAI,EAAU,EAAE,MAAe,EAA6B,EAAE;QACnE,MAAM,QAAQ,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC;QAC/B,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,OAAO,OAAO,CAAC,OAAO,CACpB,MAAM,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,gCAAgC,EAAE,IAAI,CAAC,CAG3D,CACF,CAAC;QACJ,CAAC;QACD,OAAO,OAAO,CAAC,OAAO,CAAC,QAA4B,CAAC,CAAC;IACvD,CAAC,CAAC;AACJ,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAA2B,EAAgB,EAAE;IAC5E,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,OAAO,iBAAiB,EAAE,CAAC;IAC7B,CAAC;IAED,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAC;IACjC,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,GAAG,EAAE,CAAC;AAC7B,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"harness-mcp.d.ts","sourceRoot":"","sources":["../src/harness-mcp.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAMpB;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,GAAI,SAAS,iBAAiB,KAAG,UAgC7D,CAAC"}
1
+ {"version":3,"file":"harness-mcp.d.ts","sourceRoot":"","sources":["../src/harness-mcp.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAMpB;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,GAAI,SAAS,iBAAiB,KAAG,UAmC7D,CAAC"}
@@ -21,9 +21,12 @@ import { buildMcpTools } from '@ontrails/mcp';
21
21
  * ```
22
22
  */
23
23
  export const createMcpHarness = (options) => {
24
- const tools = buildMcpTools(options.app);
24
+ const toolsResult = buildMcpTools(options.app);
25
+ if (toolsResult.isErr()) {
26
+ throw toolsResult.error;
27
+ }
25
28
  const toolMap = new Map();
26
- for (const tool of tools) {
29
+ for (const tool of toolsResult.value) {
27
30
  toolMap.set(tool.name, tool);
28
31
  }
29
32
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"harness-mcp.js","sourceRoot":"","sources":["../src/harness-mcp.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAS9C,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAA0B,EAAc,EAAE;IACzE,MAAM,KAAK,GAAG,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACzC,MAAM,OAAO,GAAG,IAAI,GAAG,EAA6B,CAAC;IACrD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,OAAO;QACL,KAAK,CAAC,QAAQ,CACZ,IAAY,EACZ,IAA6B;YAE7B,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC/B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;gBACvB,OAAO;oBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,iBAAiB,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;oBAC1D,OAAO,EAAE,IAAI;iBACd,CAAC;YACJ,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE;gBACtC,aAAa,EAAE,SAAS;gBACxB,YAAY,EAAE,SAAS;gBACvB,MAAM,EAAE,SAAS;aAClB,CAAC,CAAC;YAEH,OAAO;gBACL,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,KAAK;aACjC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC,CAAC"}
1
+ {"version":3,"file":"harness-mcp.js","sourceRoot":"","sources":["../src/harness-mcp.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAS9C,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAA0B,EAAc,EAAE;IACzE,MAAM,WAAW,GAAG,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC/C,IAAI,WAAW,CAAC,KAAK,EAAE,EAAE,CAAC;QACxB,MAAM,WAAW,CAAC,KAAK,CAAC;IAC1B,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,GAAG,EAA6B,CAAC;IACrD,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,KAAK,EAAE,CAAC;QACrC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,OAAO;QACL,KAAK,CAAC,QAAQ,CACZ,IAAY,EACZ,IAA6B;YAE7B,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC/B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;gBACvB,OAAO;oBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,iBAAiB,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;oBAC1D,OAAO,EAAE,IAAI;iBACd,CAAC;YACJ,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE;gBACtC,aAAa,EAAE,SAAS;gBACxB,YAAY,EAAE,SAAS;gBACvB,MAAM,EAAE,SAAS;aAClB,CAAC,CAAC;YAEH,OAAO;gBACL,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,KAAK;aACjC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC,CAAC"}
package/dist/index.d.ts CHANGED
@@ -5,10 +5,11 @@ export { testTrail } from './trail.js';
5
5
  export { testContracts } from './contracts.js';
6
6
  export { testDetours } from './detours.js';
7
7
  export { assertErrorMatch, assertFullMatch, assertSchemaMatch, expectErr, expectOk, } from './assertions.js';
8
- export { createTestContext } from './context.js';
8
+ export { createFollowContext, createTestContext } from './context.js';
9
9
  export { createTestLogger } from './logger.js';
10
10
  export { createCliHarness } from './harness-cli.js';
11
11
  export { createMcpHarness } from './harness-mcp.js';
12
+ export type { CreateFollowContextOptions } from './context.js';
12
13
  export type { TestFollowOptions } from './follows.js';
13
14
  export type { FollowScenario, TestScenario, TestLogger, TestTrailContextOptions, CliHarness, CliHarnessOptions, CliHarnessResult, McpHarness, McpHarnessOptions, McpHarnessResult, } from './types.js';
14
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,SAAS,EACT,QAAQ,GACT,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGpD,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,YAAY,EACV,cAAc,EACd,YAAY,EACZ,UAAU,EACV,uBAAuB,EACvB,UAAU,EACV,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,SAAS,EACT,QAAQ,GACT,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGpD,YAAY,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAC;AAC/D,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,YAAY,EACV,cAAc,EACd,YAAY,EACZ,UAAU,EACV,uBAAuB,EACvB,UAAU,EACV,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ export { testDetours } from './detours.js';
8
8
  // Assertions
9
9
  export { assertErrorMatch, assertFullMatch, assertSchemaMatch, expectErr, expectOk, } from './assertions.js';
10
10
  // Mock factories
11
- export { createTestContext } from './context.js';
11
+ export { createFollowContext, createTestContext } from './context.js';
12
12
  export { createTestLogger } from './logger.js';
13
13
  // Surface harnesses
14
14
  export { createCliHarness } from './harness-cli.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0BAA0B;AAC1B,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,aAAa;AACb,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,SAAS,EACT,QAAQ,GACT,MAAM,iBAAiB,CAAC;AAEzB,iBAAiB;AACjB,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,oBAAoB;AACpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0BAA0B;AAC1B,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,aAAa;AACb,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,SAAS,EACT,QAAQ,GACT,MAAM,iBAAiB,CAAC;AAEzB,iBAAiB;AACjB,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,oBAAoB;AACpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/testing",
3
- "version": "1.0.0-beta.7",
3
+ "version": "1.0.0-beta.9",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts",
@@ -14,10 +14,10 @@
14
14
  "clean": "rm -rf dist *.tsbuildinfo"
15
15
  },
16
16
  "peerDependencies": {
17
- "@ontrails/cli": "^1.0.0-beta.6",
18
- "@ontrails/core": "^1.0.0-beta.6",
19
- "@ontrails/logging": "^1.0.0-beta.6",
20
- "@ontrails/mcp": "^1.0.0-beta.6",
17
+ "@ontrails/cli": "^1.0.0-beta.8",
18
+ "@ontrails/core": "^1.0.0-beta.8",
19
+ "@ontrails/logging": "^1.0.0-beta.8",
20
+ "@ontrails/mcp": "^1.0.0-beta.8",
21
21
  "zod": "^4.3.5"
22
22
  }
23
23
  }
@@ -1,6 +1,8 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
2
 
3
- import { createTestContext } from '../context.js';
3
+ import { Result } from '@ontrails/core';
4
+
5
+ import { createFollowContext, createTestContext } from '../context.js';
4
6
  import type { TestLogger } from '../types.js';
5
7
 
6
8
  // ---------------------------------------------------------------------------
@@ -58,3 +60,36 @@ describe('createTestContext', () => {
58
60
  expect(env).toEqual({ CUSTOM: '1', NODE_ENV: 'test' });
59
61
  });
60
62
  });
63
+
64
+ describe('createFollowContext', () => {
65
+ test('returns configured ok response for registered id', async () => {
66
+ const follow = createFollowContext({
67
+ responses: { 'entity.add': Result.ok({ id: '1', name: 'Alpha' }) },
68
+ });
69
+ const result = await follow('entity.add', { name: 'Alpha' });
70
+ expect(result.unwrap()).toEqual({ id: '1', name: 'Alpha' });
71
+ });
72
+
73
+ test('returns configured err response for registered id', async () => {
74
+ const err = new Error('conflict');
75
+ const follow = createFollowContext({
76
+ responses: { 'entity.add': Result.err(err) },
77
+ });
78
+ const result = await follow('entity.add', {});
79
+ expect(result.isErr()).toBe(true);
80
+ expect(result.error?.message).toBe('conflict');
81
+ });
82
+
83
+ test('returns err with descriptive message for unregistered id', async () => {
84
+ const follow = createFollowContext();
85
+ const result = await follow('unknown.trail', {});
86
+ expect(result.isErr()).toBe(true);
87
+ expect(result.error?.message).toContain('unknown.trail');
88
+ });
89
+
90
+ test('no options defaults to empty responses', async () => {
91
+ const follow = createFollowContext();
92
+ const result = await follow('any.id', {});
93
+ expect(result.isErr()).toBe(true);
94
+ });
95
+ });
package/src/context.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  * Test context factory for creating TrailContext instances suitable for testing.
3
3
  */
4
4
 
5
- import type { TrailContext } from '@ontrails/core';
5
+ import type { FollowFn, TrailContext } from '@ontrails/core';
6
+ import { Result } from '@ontrails/core';
6
7
 
7
8
  import { createTestLogger } from './logger.js';
8
9
  import type { TestTrailContextOptions } from './types.js';
@@ -28,6 +29,46 @@ export const createTestContext = (
28
29
  workspaceRoot: overrides?.cwd ?? process.cwd(),
29
30
  });
30
31
 
32
+ // ---------------------------------------------------------------------------
33
+ // createFollowContext
34
+ // ---------------------------------------------------------------------------
35
+
36
+ export interface CreateFollowContextOptions {
37
+ readonly responses?: Record<string, Result<unknown, Error>> | undefined;
38
+ }
39
+
40
+ /**
41
+ * Create a mock `FollowFn` for testing composite trails.
42
+ *
43
+ * Returns preconfigured `Result` values keyed by trail ID. Calls to
44
+ * unregistered IDs return `Result.err` with a descriptive message.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const follow = createFollowContext({
49
+ * responses: { 'entity.add': Result.ok({ id: '1', name: 'Alpha' }) },
50
+ * });
51
+ * const ctx = { ...createTestContext(), follow };
52
+ * ```
53
+ */
54
+ export const createFollowContext = (
55
+ options?: CreateFollowContextOptions
56
+ ): FollowFn => {
57
+ const responses = options?.responses ?? {};
58
+ return <O>(id: string, _input: unknown): Promise<Result<O, Error>> => {
59
+ const response = responses[id];
60
+ if (response === undefined) {
61
+ return Promise.resolve(
62
+ Result.err(new Error(`No mock response for follow("${id}")`)) as Result<
63
+ O,
64
+ Error
65
+ >
66
+ );
67
+ }
68
+ return Promise.resolve(response as Result<O, Error>);
69
+ };
70
+ };
71
+
31
72
  /**
32
73
  * Merge a Partial<TrailContext> into a test context.
33
74
  * Used internally when the public API accepts Partial<TrailContext>.
@@ -31,9 +31,12 @@ import type {
31
31
  * ```
32
32
  */
33
33
  export const createMcpHarness = (options: McpHarnessOptions): McpHarness => {
34
- const tools = buildMcpTools(options.app);
34
+ const toolsResult = buildMcpTools(options.app);
35
+ if (toolsResult.isErr()) {
36
+ throw toolsResult.error;
37
+ }
35
38
  const toolMap = new Map<string, McpToolDefinition>();
36
- for (const tool of tools) {
39
+ for (const tool of toolsResult.value) {
37
40
  toolMap.set(tool.name, tool);
38
41
  }
39
42
 
package/src/index.ts CHANGED
@@ -16,7 +16,7 @@ export {
16
16
  } from './assertions.js';
17
17
 
18
18
  // Mock factories
19
- export { createTestContext } from './context.js';
19
+ export { createFollowContext, createTestContext } from './context.js';
20
20
  export { createTestLogger } from './logger.js';
21
21
 
22
22
  // Surface harnesses
@@ -24,6 +24,7 @@ export { createCliHarness } from './harness-cli.js';
24
24
  export { createMcpHarness } from './harness-mcp.js';
25
25
 
26
26
  // Types
27
+ export type { CreateFollowContextOptions } from './context.js';
27
28
  export type { TestFollowOptions } from './follows.js';
28
29
 
29
30
  export type {