@ontrails/testing 1.0.0-beta.15 → 1.0.0-beta.17

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 (95) hide show
  1. package/CHANGELOG.md +70 -1
  2. package/README.md +47 -3
  3. package/package.json +16 -7
  4. package/src/all.ts +32 -7
  5. package/src/context.ts +3 -0
  6. package/src/contracts.ts +1 -16
  7. package/src/examples.ts +19 -11
  8. package/src/harness-cli.ts +16 -3
  9. package/src/harness-http.ts +263 -0
  10. package/src/harness-mcp.ts +2 -0
  11. package/src/index.ts +18 -1
  12. package/src/logger.ts +3 -1
  13. package/src/scenario.ts +3 -5
  14. package/src/signals.ts +221 -0
  15. package/src/surface-parity.ts +356 -0
  16. package/src/types.ts +136 -1
  17. package/.turbo/turbo-build.log +0 -1
  18. package/.turbo/turbo-lint.log +0 -3
  19. package/.turbo/turbo-typecheck.log +0 -1
  20. package/dist/all.d.ts +0 -36
  21. package/dist/all.d.ts.map +0 -1
  22. package/dist/all.js +0 -99
  23. package/dist/all.js.map +0 -1
  24. package/dist/assertions.d.ts +0 -72
  25. package/dist/assertions.d.ts.map +0 -1
  26. package/dist/assertions.js +0 -238
  27. package/dist/assertions.js.map +0 -1
  28. package/dist/context.d.ts +0 -76
  29. package/dist/context.d.ts.map +0 -1
  30. package/dist/context.js +0 -118
  31. package/dist/context.js.map +0 -1
  32. package/dist/contracts.d.ts +0 -17
  33. package/dist/contracts.d.ts.map +0 -1
  34. package/dist/contracts.js +0 -75
  35. package/dist/contracts.js.map +0 -1
  36. package/dist/crosses.d.ts +0 -38
  37. package/dist/crosses.d.ts.map +0 -1
  38. package/dist/crosses.js +0 -223
  39. package/dist/crosses.js.map +0 -1
  40. package/dist/detours.d.ts +0 -13
  41. package/dist/detours.d.ts.map +0 -1
  42. package/dist/detours.js +0 -109
  43. package/dist/detours.js.map +0 -1
  44. package/dist/effective-examples.d.ts +0 -30
  45. package/dist/effective-examples.d.ts.map +0 -1
  46. package/dist/effective-examples.js +0 -227
  47. package/dist/effective-examples.js.map +0 -1
  48. package/dist/examples.d.ts +0 -23
  49. package/dist/examples.d.ts.map +0 -1
  50. package/dist/examples.js +0 -240
  51. package/dist/examples.js.map +0 -1
  52. package/dist/harness-cli.d.ts +0 -21
  53. package/dist/harness-cli.d.ts.map +0 -1
  54. package/dist/harness-cli.js +0 -205
  55. package/dist/harness-cli.js.map +0 -1
  56. package/dist/harness-mcp.d.ts +0 -21
  57. package/dist/harness-mcp.d.ts.map +0 -1
  58. package/dist/harness-mcp.js +0 -54
  59. package/dist/harness-mcp.js.map +0 -1
  60. package/dist/index.d.ts +0 -18
  61. package/dist/index.d.ts.map +0 -1
  62. package/dist/index.js +0 -19
  63. package/dist/index.js.map +0 -1
  64. package/dist/logger.d.ts +0 -15
  65. package/dist/logger.d.ts.map +0 -1
  66. package/dist/logger.js +0 -87
  67. package/dist/logger.js.map +0 -1
  68. package/dist/scenario.d.ts +0 -37
  69. package/dist/scenario.d.ts.map +0 -1
  70. package/dist/scenario.js +0 -235
  71. package/dist/scenario.js.map +0 -1
  72. package/dist/trail.d.ts +0 -20
  73. package/dist/trail.d.ts.map +0 -1
  74. package/dist/trail.js +0 -80
  75. package/dist/trail.js.map +0 -1
  76. package/dist/types.d.ts +0 -113
  77. package/dist/types.d.ts.map +0 -1
  78. package/dist/types.js +0 -5
  79. package/dist/types.js.map +0 -1
  80. package/src/__tests__/all.test.ts +0 -254
  81. package/src/__tests__/context.test.ts +0 -170
  82. package/src/__tests__/contracts.test.ts +0 -239
  83. package/src/__tests__/crosses.test.ts +0 -587
  84. package/src/__tests__/detours.test.ts +0 -212
  85. package/src/__tests__/effective-examples.test.ts +0 -203
  86. package/src/__tests__/examples.test.ts +0 -636
  87. package/src/__tests__/harness-cli.test.ts +0 -90
  88. package/src/__tests__/harness-mcp.test.ts +0 -37
  89. package/src/__tests__/logger.test.ts +0 -136
  90. package/src/__tests__/partial-match.test.ts +0 -126
  91. package/src/__tests__/scenario.test.ts +0 -381
  92. package/src/__tests__/trail.test.ts +0 -99
  93. package/tsconfig.json +0 -9
  94. package/tsconfig.tests.json +0 -10
  95. package/tsconfig.tsbuildinfo +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,74 @@
1
1
  # @ontrails/testing
2
2
 
3
+ ## 1.0.0-beta.17
4
+
5
+ ### Patch Changes
6
+
7
+ - ce42573: Add an example-driven CLI/MCP/HTTP surface parity helper.
8
+ - e1eb4ee: Add an HTTP surface harness and include HTTP projection validation in `testAllEstablished`.
9
+ - Updated dependencies [3dc8254]
10
+ - Updated dependencies [61497c5]
11
+ - @ontrails/core@1.0.0-beta.17
12
+ - @ontrails/cli@1.0.0-beta.17
13
+ - @ontrails/http@1.0.0-beta.17
14
+ - @ontrails/mcp@1.0.0-beta.17
15
+ - @ontrails/observe@1.0.0-beta.17
16
+
17
+ ## 1.0.0-beta.16
18
+
19
+ ### Minor Changes
20
+
21
+ - bcdc484: Add an explicit `unmockable: { reason }` resource marker and have testing auto-mock resolution skip intentionally unmockable resources.
22
+ - bb1cadf: Rename the generated `testAll` suite from `governance` to `contract`.
23
+
24
+ ### Patch Changes
25
+
26
+ - d40430d: Remove the retired `@ontrails/logging` workspace from the prerelease package set. Use `@ontrails/observe` for log and trace sink contracts and `@ontrails/logtape` for LogTape forwarding.
27
+ - 193ae78: Migrate testing logger types from the legacy `@ontrails/logging` package to `@ontrails/observe`.
28
+ - Updated dependencies [73622ae]
29
+ - Updated dependencies [e991a5b]
30
+ - Updated dependencies [25f3c5c]
31
+ - Updated dependencies [6300f70]
32
+ - Updated dependencies [d172013]
33
+ - Updated dependencies [c3fc5c3]
34
+ - Updated dependencies [20d7a5c]
35
+ - Updated dependencies [be5fb46]
36
+ - Updated dependencies [e898cc4]
37
+ - Updated dependencies [a8997ed]
38
+ - Updated dependencies [fe03945]
39
+ - Updated dependencies [3395234]
40
+ - Updated dependencies [d40430d]
41
+ - Updated dependencies [bcdc484]
42
+ - Updated dependencies [ed171d5]
43
+ - Updated dependencies [49c2e7d]
44
+ - Updated dependencies [331e3a9]
45
+ - Updated dependencies [4399fdb]
46
+ - Updated dependencies [4b8d13b]
47
+ - Updated dependencies [fbd42fc]
48
+ - Updated dependencies [63d1aef]
49
+ - Updated dependencies [112b9f2]
50
+ - Updated dependencies [893025e]
51
+ - Updated dependencies [ed888e2]
52
+ - Updated dependencies [2e05e27]
53
+ - Updated dependencies [9cdb0f2]
54
+ - Updated dependencies [c8caa5e]
55
+ - Updated dependencies [f4b90c9]
56
+ - Updated dependencies [eec5e9d]
57
+ - Updated dependencies [4e75129]
58
+ - Updated dependencies [47505fe]
59
+ - Updated dependencies [ebd4434]
60
+ - Updated dependencies [863d473]
61
+ - Updated dependencies [344f2f7]
62
+ - Updated dependencies [26f9ffd]
63
+ - Updated dependencies [66056ac]
64
+ - Updated dependencies [0bad534]
65
+ - Updated dependencies [10eae9a]
66
+ - Updated dependencies [22c6c06]
67
+ - @ontrails/core@1.0.0-beta.16
68
+ - @ontrails/cli@1.0.0-beta.16
69
+ - @ontrails/mcp@1.0.0-beta.16
70
+ - @ontrails/observe@1.0.0-beta.16
71
+
3
72
  ## 1.0.0-beta.15
4
73
 
5
74
  ### Minor Changes
@@ -11,7 +80,7 @@
11
80
  - Updated dependencies [4ad6b25]
12
81
  - @ontrails/core@1.0.0-beta.15
13
82
  - @ontrails/cli@1.0.0-beta.15
14
- - @ontrails/logging@1.0.0-beta.15
83
+ - @ontrails/observe@1.0.0-beta.15
15
84
  - @ontrails/mcp@1.0.0-beta.15
16
85
 
17
86
  ## 1.0.0-beta.14
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ontrails/testing
2
2
 
3
- Contract-driven testing for Trails. Add examples to your trails, then `testAll(graph)` runs them as assertions, validates output schemas, checks composition graphs, and verifies structural integrity. One line of test code, full governance.
3
+ Contract-driven testing for Trails. Add examples to your trails, then `testAll(graph)` runs them as assertions, validates output schemas, checks composition graphs, and verifies structural integrity. One line of test code, full contract coverage.
4
4
 
5
5
  ## Usage
6
6
 
@@ -27,16 +27,18 @@ testDetours(graph); // Validate detour constructor, recover, and ordering sem
27
27
 
28
28
  | Export | What it does |
29
29
  | --- | --- |
30
- | `testAll(topo, ctx?)` | Single-line governance suite: validation + examples + contracts + detours |
30
+ | `testAll(topo, ctx?)` | Single-line contract suite: validation + examples + contracts + detours |
31
31
  | `testExamples(topo, ctx?)` | Run trail examples as `describe`/`test` blocks |
32
32
  | `testTrail(trail, scenarios)` | Custom scenarios for edge cases, error paths, and cross chains |
33
33
  | `testContracts(topo, ctx?)` | Validate output against declared schemas |
34
34
  | `testDetours(topo)` | Validate detour constructor, recover, and shadowing semantics |
35
+ | `testSurfaceParity(topo, options?)` | Run trail examples through CLI, MCP, and HTTP and compare normalized semantics |
35
36
  | `createCrossContext(options?)` | Mock `CrossFn` for testing composite trails; returns preconfigured `Result` values keyed by trail ID |
36
37
  | `createTestContext(options?)` | `TrailContext` with sensible test defaults |
37
38
  | `createTestLogger()` | Logger that captures entries in memory for assertions |
38
39
  | `createCliHarness(options)` | Execute CLI commands in-process, capture stdout/stderr |
39
40
  | `createMcpHarness(options)` | Invoke MCP tools directly without transport |
41
+ | `createHttpHarness(options)` | Execute HTTP route projections in-process without a server |
40
42
 
41
43
  See the [API Reference](../../docs/api-reference.md) for the full list.
42
44
 
@@ -89,7 +91,11 @@ Calls to unregistered trail IDs return `Result.err` with a descriptive message,
89
91
  ## Surface Harnesses
90
92
 
91
93
  ```typescript
92
- import { createCliHarness, createMcpHarness } from '@ontrails/testing';
94
+ import {
95
+ createCliHarness,
96
+ createHttpHarness,
97
+ createMcpHarness,
98
+ } from '@ontrails/testing';
93
99
 
94
100
  // CLI
95
101
  const cli = createCliHarness({ graph });
@@ -100,8 +106,46 @@ expect(result.exitCode).toBe(0);
100
106
  const mcp = createMcpHarness({ graph });
101
107
  const tool = await mcp.callTool('myapp_entity_show', { name: 'Alpha' });
102
108
  expect(tool.isError).toBe(false);
109
+
110
+ // HTTP
111
+ const http = createHttpHarness({ graph });
112
+ const response = await http.get('/entity/show', { name: 'Alpha' });
113
+ expect(response.status).toBe(200);
114
+ ```
115
+
116
+ ## Surface Parity
117
+
118
+ `testSurfaceParity()` is a focused gate for established apps that want to prove
119
+ their examples behave the same through every shipped surface.
120
+
121
+ ```typescript
122
+ import { testSurfaceParity } from '@ontrails/testing';
123
+ import { graph } from '../app';
124
+
125
+ const createDeterministicTestDb = () => ({});
126
+
127
+ testSurfaceParity(graph, {
128
+ createResources: () => ({
129
+ 'db.main': createDeterministicTestDb(),
130
+ }),
131
+ exclusions: [
132
+ {
133
+ example: 'Creates generated data',
134
+ reason: 'output includes generated IDs that intentionally differ per surface run',
135
+ trailId: 'entity.add',
136
+ },
137
+ ],
138
+ });
103
139
  ```
104
140
 
141
+ The helper compares normalized success payloads and normalized TrailsError
142
+ category/code pairs. CLI command names, MCP tool names, HTTP method/path values,
143
+ transport envelopes, activation consumers, internal trails, and planned
144
+ WebSocket work stay outside the equality check. Use `createResources` when
145
+ examples need deterministic fixtures for each surface invocation, and use
146
+ exclusions for intentional semantic differences that should not be silently
147
+ skipped.
148
+
105
149
  ## Installation
106
150
 
107
151
  ```bash
package/package.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "name": "@ontrails/testing",
3
- "version": "1.0.0-beta.15",
3
+ "version": "1.0.0-beta.17",
4
+ "files": [
5
+ "src/**/*.ts",
6
+ "!src/**/__tests__/**",
7
+ "!src/**/*.test.ts",
8
+ "!src/**/*.test-d.ts",
9
+ "README.md",
10
+ "CHANGELOG.md"
11
+ ],
4
12
  "type": "module",
5
13
  "exports": {
6
14
  ".": "./src/index.ts",
@@ -14,14 +22,15 @@
14
22
  "clean": "rm -rf dist *.tsbuildinfo"
15
23
  },
16
24
  "devDependencies": {
17
- "@ontrails/drizzle": "^1.0.0-beta.14",
18
- "@ontrails/store": "^1.0.0-beta.14"
25
+ "@ontrails/drizzle": "^1.0.0-beta.16",
26
+ "@ontrails/store": "^1.0.0-beta.16"
19
27
  },
20
28
  "peerDependencies": {
21
- "@ontrails/cli": "^1.0.0-beta.14",
22
- "@ontrails/core": "^1.0.0-beta.14",
23
- "@ontrails/logging": "^1.0.0-beta.14",
24
- "@ontrails/mcp": "^1.0.0-beta.14",
29
+ "@ontrails/cli": "^1.0.0-beta.16",
30
+ "@ontrails/core": "^1.0.0-beta.16",
31
+ "@ontrails/http": "^1.0.0-beta.16",
32
+ "@ontrails/mcp": "^1.0.0-beta.16",
33
+ "@ontrails/observe": "^1.0.0-beta.16",
25
34
  "zod": "^4.3.5"
26
35
  }
27
36
  }
package/src/all.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * testAll — single-line governance suite for any Topo.
2
+ * testAll — single-line contract suite for any Topo.
3
3
  *
4
4
  * Wraps topo validation, example execution, contract checks, and detour
5
5
  * contract validation into one describe block.
@@ -11,6 +11,7 @@ import type { Topo, TrailContext } from '@ontrails/core';
11
11
  import { validateEstablishedTopo, validateTopo } from '@ontrails/core';
12
12
 
13
13
  import { createCliHarness } from './harness-cli.js';
14
+ import { createHttpHarness } from './harness-http.js';
14
15
  import { createMcpHarness } from './harness-mcp.js';
15
16
  import { testContracts } from './contracts.js';
16
17
  import type { TestExecutionOptions } from './context.js';
@@ -19,9 +20,9 @@ import { testExamples } from './examples.js';
19
20
  import type { TestAllEstablishedOptions } from './types.js';
20
21
 
21
22
  /**
22
- * Run the full governance test suite for a Topo.
23
+ * Run the full contract test suite for a Topo.
23
24
  *
24
- * Generates a `governance` describe block containing:
25
+ * Generates a `contract` describe block containing:
25
26
  * - Structural validation via `validateTopo`
26
27
  * - Example execution via `testExamples`
27
28
  * - Output contract checks via `testContracts`
@@ -73,12 +74,12 @@ const assertValidTopo = (result: ReturnType<typeof validateTopo>): void => {
73
74
  }
74
75
  };
75
76
 
76
- const registerGovernanceSuite = (
77
+ const registerContractSuite = (
77
78
  topo: Topo,
78
79
  ctxOrFactory: TestAllInput | undefined,
79
80
  validate: (topo: Topo) => ReturnType<typeof validateTopo>
80
81
  ): void => {
81
- describe('governance', () => {
82
+ describe('contract', () => {
82
83
  test('topo validates', () => {
83
84
  expect(() => assertValidTopo(validate(topo))).not.toThrow();
84
85
  });
@@ -93,7 +94,7 @@ const registerGovernanceSuite = (
93
94
  };
94
95
 
95
96
  export const testAll = (topo: Topo, ctxOrFactory?: TestAllInput): void => {
96
- registerGovernanceSuite(topo, ctxOrFactory, validateTopo);
97
+ registerContractSuite(topo, ctxOrFactory, validateTopo);
97
98
  };
98
99
 
99
100
  type EstablishedInput =
@@ -108,6 +109,7 @@ const isEstablishedOptions = (
108
109
  (Object.hasOwn(input, 'cli') ||
109
110
  Object.hasOwn(input, 'createPermit') ||
110
111
  Object.hasOwn(input, 'ctx') ||
112
+ Object.hasOwn(input, 'http') ||
111
113
  Object.hasOwn(input, 'mcp') ||
112
114
  Object.hasOwn(input, 'resources') ||
113
115
  Object.hasOwn(input, 'strictPermits'));
@@ -154,6 +156,22 @@ const toMcpHarnessOptions = (
154
156
  ...options.mcp,
155
157
  });
156
158
 
159
+ const toHttpHarnessOptions = (
160
+ topo: Topo,
161
+ options: TestAllEstablishedOptions
162
+ ) => {
163
+ const httpOptions = {
164
+ graph: topo,
165
+ ...options.http,
166
+ };
167
+
168
+ if (options.ctx !== undefined) {
169
+ httpOptions.ctx = options.ctx;
170
+ }
171
+
172
+ return httpOptions;
173
+ };
174
+
157
175
  const registerEstablishedSurfaceSuite = (
158
176
  topo: Topo,
159
177
  resolveInput: () =>
@@ -175,6 +193,13 @@ const registerEstablishedSurfaceSuite = (
175
193
  createMcpHarness(toMcpHarnessOptions(topo, options))
176
194
  ).not.toThrow();
177
195
  });
196
+
197
+ test('HTTP projection validates established topo', () => {
198
+ const options = normalizeEstablishedOptions(resolveInput());
199
+ expect(() =>
200
+ createHttpHarness(toHttpHarnessOptions(topo, options))
201
+ ).not.toThrow();
202
+ });
178
203
  });
179
204
  };
180
205
 
@@ -187,7 +212,7 @@ export const testAllEstablished = (
187
212
  ? optionsOrFactory
188
213
  : () => optionsOrFactory;
189
214
 
190
- registerGovernanceSuite(
215
+ registerContractSuite(
191
216
  topo,
192
217
  () => toExecutionOptions(normalizeEstablishedOptions(resolveInput())),
193
218
  validateEstablishedTopo
package/src/context.ts CHANGED
@@ -184,6 +184,9 @@ export const mergeResourceOverrides = (
184
184
  const buildMockResources = async (app: Topo): Promise<ResourceOverrideMap> => {
185
185
  const resources: Record<string, unknown> = {};
186
186
  for (const declaredResource of app.listResources()) {
187
+ if (declaredResource.unmockable !== undefined) {
188
+ continue;
189
+ }
187
190
  if (!declaredResource.mock) {
188
191
  continue;
189
192
  }
package/src/contracts.ts CHANGED
@@ -26,18 +26,6 @@ import { deriveTrailExamples } from './effective-examples.js';
26
26
  // Helpers
27
27
  // ---------------------------------------------------------------------------
28
28
 
29
- /** Check if a trail requires cross() but the context doesn't provide it. */
30
- const needsCrossContext = (
31
- t: unknown,
32
- resolveCtx: () => Partial<TrailContext> | TestExecutionOptions | undefined
33
- ): boolean => {
34
- const spec = t as { crosses?: readonly string[] };
35
- if (!spec.crosses || spec.crosses.length === 0) {
36
- return false;
37
- }
38
- return !normalizeTestExecutionOptions(resolveCtx()).ctx?.cross;
39
- };
40
-
41
29
  const validateOutputSchema = (
42
30
  outputSchema: z.ZodType,
43
31
  value: unknown,
@@ -87,10 +75,6 @@ export const testContracts = (
87
75
  if (t.examples.length === 0) {
88
76
  return;
89
77
  }
90
- if (needsCrossContext(t, resolveInput)) {
91
- return;
92
- }
93
-
94
78
  const { examples, output: outputSchema } = t;
95
79
  const successExamples = examples.filter((e) => e.error === undefined);
96
80
 
@@ -111,6 +95,7 @@ export const testContracts = (
111
95
  const result = await executeTrail(t, example.input, {
112
96
  ctx: testCtx,
113
97
  resources,
98
+ topo: app,
114
99
  });
115
100
  const resultValue = expectOk(result);
116
101
 
package/src/examples.ts CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  NetworkError,
32
32
  NotFoundError,
33
33
  PermissionError,
34
+ PermitError,
34
35
  RateLimitError,
35
36
  RetryExhaustedError,
36
37
  executeTrail,
@@ -57,6 +58,7 @@ import {
57
58
  } from './context.js';
58
59
  import type { PermittedTrail, TestExecutionOptions } from './context.js';
59
60
  import { isDerivedExample, deriveTrailExamples } from './effective-examples.js';
61
+ import { withSignalAssertions } from './signals.js';
60
62
 
61
63
  // ---------------------------------------------------------------------------
62
64
  // Error class name -> constructor map
@@ -74,6 +76,7 @@ const ERROR_MAP: Record<string, new (...args: never[]) => Error> = {
74
76
  NetworkError: NetworkError as new (...args: never[]) => Error,
75
77
  NotFoundError: NotFoundError as new (...args: never[]) => Error,
76
78
  PermissionError: PermissionError as new (...args: never[]) => Error,
79
+ PermitError: PermitError as new (...args: never[]) => Error,
77
80
  RateLimitError: RateLimitError as new (...args: never[]) => Error,
78
81
  RetryExhaustedError: RetryExhaustedError as unknown as new (
79
82
  ...args: never[]
@@ -87,7 +90,7 @@ const ERROR_MAP: Record<string, new (...args: never[]) => Error> = {
87
90
  * Resolve an error class name string to the actual constructor.
88
91
  * Falls back to generic Error if the name is not in the core taxonomy.
89
92
  */
90
- const resolveErrorClass = (name: string): (new (...args: never[]) => Error) =>
93
+ const resolveErrorClass = (name: string): new (...args: never[]) => Error =>
91
94
  ERROR_MAP[name] ?? (Error as new (...args: never[]) => Error);
92
95
 
93
96
  // ---------------------------------------------------------------------------
@@ -162,7 +165,7 @@ const applyAutoPermit = (
162
165
  * Run a single example against a trail.
163
166
  * Handles validation, execution, and assertions.
164
167
  */
165
- const runExample = async (
168
+ export const runExample = async (
166
169
  t: Trail<unknown, unknown, unknown>,
167
170
  example: TrailExample<unknown, unknown>,
168
171
  output: z.ZodType | undefined,
@@ -170,19 +173,21 @@ const runExample = async (
170
173
  resources?: ResourceOverrideMap,
171
174
  opts?: TestExecutionOptions
172
175
  ): Promise<void> => {
176
+ const ctx = opts ? applyAutoPermit(testCtx, t, opts) : testCtx;
177
+ const signals = withSignalAssertions(ctx, example);
173
178
  const validated = validateInput(t.input, example.input);
174
179
 
175
180
  if (handleValidationError(validated, example)) {
181
+ signals.assert();
176
182
  return;
177
183
  }
178
184
 
179
- const ctx = opts ? applyAutoPermit(testCtx, t, opts) : testCtx;
180
-
181
185
  const result = await executeTrail(t, example.input, {
182
- ctx,
186
+ ctx: signals.ctx,
183
187
  resources: resources ?? opts?.resources,
184
188
  });
185
189
  assertProgressiveMatch(result, example, output);
190
+ signals.assert();
186
191
  };
187
192
 
188
193
  // ---------------------------------------------------------------------------
@@ -267,23 +272,25 @@ const runCompositionExample = async (
267
272
  resources?: ResourceOverrideMap,
268
273
  opts?: TestExecutionOptions
269
274
  ): Promise<void> => {
275
+ const permittedCtx = opts
276
+ ? applyAutoPermit(baseCtx, trailDef, opts)
277
+ : baseCtx;
278
+ const signals = withSignalAssertions(permittedCtx, example);
270
279
  const validated = validateInput(trailDef.input, example.input);
271
280
 
272
281
  if (handleValidationError(validated, example)) {
282
+ signals.assert();
273
283
  return;
274
284
  }
275
285
 
276
- const permittedCtx = opts
277
- ? applyAutoPermit(baseCtx, trailDef, opts)
278
- : baseCtx;
279
286
  const cross = createCoverageCross(
280
287
  called,
281
- permittedCtx.cross,
288
+ signals.ctx.cross,
282
289
  topo,
283
- permittedCtx,
290
+ signals.ctx,
284
291
  resources
285
292
  );
286
- const testCtx: TrailContext = { ...permittedCtx, cross };
293
+ const testCtx: TrailContext = { ...signals.ctx, cross };
287
294
 
288
295
  // Top-level trail validates against trail.input (not merged crossInput).
289
296
  // Merged validation only applies to cross targets in executeFromMap/createCoverageCross.
@@ -292,6 +299,7 @@ const runCompositionExample = async (
292
299
  resources: resources ?? opts?.resources,
293
300
  });
294
301
  assertProgressiveMatch(result, example, output);
302
+ signals.assert();
295
303
  };
296
304
 
297
305
  // ---------------------------------------------------------------------------
@@ -8,6 +8,7 @@
8
8
  import { deriveCliCommands } from '@ontrails/cli';
9
9
  import type { CliCommand } from '@ontrails/cli';
10
10
  import type { TrailContext } from '@ontrails/core';
11
+ import { projectPublicSurfaceError } from '@ontrails/core';
11
12
 
12
13
  import { mergeTestContext } from './context.js';
13
14
  import type {
@@ -207,10 +208,16 @@ const buildErrorResult = (
207
208
  streams: CapturedStreams
208
209
  ): CliHarnessResult => {
209
210
  streams.restore();
210
- const message = error instanceof Error ? error.message : String(error);
211
+ const actualError = error instanceof Error ? error : new Error(String(error));
212
+ const projection = projectPublicSurfaceError('cli', actualError);
211
213
  return {
214
+ error: {
215
+ category: projection.category,
216
+ code: projection.name,
217
+ message: projection.message,
218
+ },
212
219
  exitCode: 1,
213
- stderr: streams.getStderr() || message,
220
+ stderr: streams.getStderr() || projection.message,
214
221
  stdout: streams.getStdout(),
215
222
  };
216
223
  };
@@ -227,9 +234,15 @@ const executeCommand = async (
227
234
  streams.restore();
228
235
 
229
236
  if (result.isErr()) {
237
+ const projection = projectPublicSurfaceError('cli', result.error);
230
238
  return {
239
+ error: {
240
+ category: projection.category,
241
+ code: projection.name,
242
+ message: projection.message,
243
+ },
231
244
  exitCode: 1,
232
- stderr: streams.getStderr() || result.error.message,
245
+ stderr: streams.getStderr() || projection.message,
233
246
  stdout: streams.getStdout(),
234
247
  };
235
248
  }