@ontrails/testing 1.0.0-beta.15 → 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 (91) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/README.md +2 -2
  3. package/package.json +15 -7
  4. package/src/all.ts +7 -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/index.ts +1 -1
  9. package/src/logger.ts +3 -1
  10. package/src/scenario.ts +3 -5
  11. package/src/signals.ts +221 -0
  12. package/src/types.ts +1 -1
  13. package/.turbo/turbo-build.log +0 -1
  14. package/.turbo/turbo-lint.log +0 -3
  15. package/.turbo/turbo-typecheck.log +0 -1
  16. package/dist/all.d.ts +0 -36
  17. package/dist/all.d.ts.map +0 -1
  18. package/dist/all.js +0 -99
  19. package/dist/all.js.map +0 -1
  20. package/dist/assertions.d.ts +0 -72
  21. package/dist/assertions.d.ts.map +0 -1
  22. package/dist/assertions.js +0 -238
  23. package/dist/assertions.js.map +0 -1
  24. package/dist/context.d.ts +0 -76
  25. package/dist/context.d.ts.map +0 -1
  26. package/dist/context.js +0 -118
  27. package/dist/context.js.map +0 -1
  28. package/dist/contracts.d.ts +0 -17
  29. package/dist/contracts.d.ts.map +0 -1
  30. package/dist/contracts.js +0 -75
  31. package/dist/contracts.js.map +0 -1
  32. package/dist/crosses.d.ts +0 -38
  33. package/dist/crosses.d.ts.map +0 -1
  34. package/dist/crosses.js +0 -223
  35. package/dist/crosses.js.map +0 -1
  36. package/dist/detours.d.ts +0 -13
  37. package/dist/detours.d.ts.map +0 -1
  38. package/dist/detours.js +0 -109
  39. package/dist/detours.js.map +0 -1
  40. package/dist/effective-examples.d.ts +0 -30
  41. package/dist/effective-examples.d.ts.map +0 -1
  42. package/dist/effective-examples.js +0 -227
  43. package/dist/effective-examples.js.map +0 -1
  44. package/dist/examples.d.ts +0 -23
  45. package/dist/examples.d.ts.map +0 -1
  46. package/dist/examples.js +0 -240
  47. package/dist/examples.js.map +0 -1
  48. package/dist/harness-cli.d.ts +0 -21
  49. package/dist/harness-cli.d.ts.map +0 -1
  50. package/dist/harness-cli.js +0 -205
  51. package/dist/harness-cli.js.map +0 -1
  52. package/dist/harness-mcp.d.ts +0 -21
  53. package/dist/harness-mcp.d.ts.map +0 -1
  54. package/dist/harness-mcp.js +0 -54
  55. package/dist/harness-mcp.js.map +0 -1
  56. package/dist/index.d.ts +0 -18
  57. package/dist/index.d.ts.map +0 -1
  58. package/dist/index.js +0 -19
  59. package/dist/index.js.map +0 -1
  60. package/dist/logger.d.ts +0 -15
  61. package/dist/logger.d.ts.map +0 -1
  62. package/dist/logger.js +0 -87
  63. package/dist/logger.js.map +0 -1
  64. package/dist/scenario.d.ts +0 -37
  65. package/dist/scenario.d.ts.map +0 -1
  66. package/dist/scenario.js +0 -235
  67. package/dist/scenario.js.map +0 -1
  68. package/dist/trail.d.ts +0 -20
  69. package/dist/trail.d.ts.map +0 -1
  70. package/dist/trail.js +0 -80
  71. package/dist/trail.js.map +0 -1
  72. package/dist/types.d.ts +0 -113
  73. package/dist/types.d.ts.map +0 -1
  74. package/dist/types.js +0 -5
  75. package/dist/types.js.map +0 -1
  76. package/src/__tests__/all.test.ts +0 -254
  77. package/src/__tests__/context.test.ts +0 -170
  78. package/src/__tests__/contracts.test.ts +0 -239
  79. package/src/__tests__/crosses.test.ts +0 -587
  80. package/src/__tests__/detours.test.ts +0 -212
  81. package/src/__tests__/effective-examples.test.ts +0 -203
  82. package/src/__tests__/examples.test.ts +0 -636
  83. package/src/__tests__/harness-cli.test.ts +0 -90
  84. package/src/__tests__/harness-mcp.test.ts +0 -37
  85. package/src/__tests__/logger.test.ts +0 -136
  86. package/src/__tests__/partial-match.test.ts +0 -126
  87. package/src/__tests__/scenario.test.ts +0 -381
  88. package/src/__tests__/trail.test.ts +0 -99
  89. package/tsconfig.json +0 -9
  90. package/tsconfig.tests.json +0 -10
  91. package/tsconfig.tsbuildinfo +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # @ontrails/testing
2
2
 
3
+ ## 1.0.0-beta.16
4
+
5
+ ### Minor Changes
6
+
7
+ - bcdc484: Add an explicit `unmockable: { reason }` resource marker and have testing auto-mock resolution skip intentionally unmockable resources.
8
+ - bb1cadf: Rename the generated `testAll` suite from `governance` to `contract`.
9
+
10
+ ### Patch Changes
11
+
12
+ - 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.
13
+ - 193ae78: Migrate testing logger types from the legacy `@ontrails/logging` package to `@ontrails/observe`.
14
+ - Updated dependencies [73622ae]
15
+ - Updated dependencies [e991a5b]
16
+ - Updated dependencies [25f3c5c]
17
+ - Updated dependencies [6300f70]
18
+ - Updated dependencies [d172013]
19
+ - Updated dependencies [c3fc5c3]
20
+ - Updated dependencies [20d7a5c]
21
+ - Updated dependencies [be5fb46]
22
+ - Updated dependencies [e898cc4]
23
+ - Updated dependencies [a8997ed]
24
+ - Updated dependencies [fe03945]
25
+ - Updated dependencies [3395234]
26
+ - Updated dependencies [d40430d]
27
+ - Updated dependencies [bcdc484]
28
+ - Updated dependencies [ed171d5]
29
+ - Updated dependencies [49c2e7d]
30
+ - Updated dependencies [331e3a9]
31
+ - Updated dependencies [4399fdb]
32
+ - Updated dependencies [4b8d13b]
33
+ - Updated dependencies [fbd42fc]
34
+ - Updated dependencies [63d1aef]
35
+ - Updated dependencies [112b9f2]
36
+ - Updated dependencies [893025e]
37
+ - Updated dependencies [ed888e2]
38
+ - Updated dependencies [2e05e27]
39
+ - Updated dependencies [9cdb0f2]
40
+ - Updated dependencies [c8caa5e]
41
+ - Updated dependencies [f4b90c9]
42
+ - Updated dependencies [eec5e9d]
43
+ - Updated dependencies [4e75129]
44
+ - Updated dependencies [47505fe]
45
+ - Updated dependencies [ebd4434]
46
+ - Updated dependencies [863d473]
47
+ - Updated dependencies [344f2f7]
48
+ - Updated dependencies [26f9ffd]
49
+ - Updated dependencies [66056ac]
50
+ - Updated dependencies [0bad534]
51
+ - Updated dependencies [10eae9a]
52
+ - Updated dependencies [22c6c06]
53
+ - @ontrails/core@1.0.0-beta.16
54
+ - @ontrails/cli@1.0.0-beta.16
55
+ - @ontrails/mcp@1.0.0-beta.16
56
+ - @ontrails/observe@1.0.0-beta.16
57
+
3
58
  ## 1.0.0-beta.15
4
59
 
5
60
  ### Minor Changes
@@ -11,7 +66,7 @@
11
66
  - Updated dependencies [4ad6b25]
12
67
  - @ontrails/core@1.0.0-beta.15
13
68
  - @ontrails/cli@1.0.0-beta.15
14
- - @ontrails/logging@1.0.0-beta.15
69
+ - @ontrails/observe@1.0.0-beta.15
15
70
  - @ontrails/mcp@1.0.0-beta.15
16
71
 
17
72
  ## 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,7 +27,7 @@ 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 |
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.16",
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,14 @@
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.15",
26
+ "@ontrails/store": "^1.0.0-beta.15"
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.15",
30
+ "@ontrails/core": "^1.0.0-beta.15",
31
+ "@ontrails/mcp": "^1.0.0-beta.15",
32
+ "@ontrails/observe": "^1.0.0-beta.15",
25
33
  "zod": "^4.3.5"
26
34
  }
27
35
  }
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.
@@ -19,9 +19,9 @@ import { testExamples } from './examples.js';
19
19
  import type { TestAllEstablishedOptions } from './types.js';
20
20
 
21
21
  /**
22
- * Run the full governance test suite for a Topo.
22
+ * Run the full contract test suite for a Topo.
23
23
  *
24
- * Generates a `governance` describe block containing:
24
+ * Generates a `contract` describe block containing:
25
25
  * - Structural validation via `validateTopo`
26
26
  * - Example execution via `testExamples`
27
27
  * - Output contract checks via `testContracts`
@@ -73,12 +73,12 @@ const assertValidTopo = (result: ReturnType<typeof validateTopo>): void => {
73
73
  }
74
74
  };
75
75
 
76
- const registerGovernanceSuite = (
76
+ const registerContractSuite = (
77
77
  topo: Topo,
78
78
  ctxOrFactory: TestAllInput | undefined,
79
79
  validate: (topo: Topo) => ReturnType<typeof validateTopo>
80
80
  ): void => {
81
- describe('governance', () => {
81
+ describe('contract', () => {
82
82
  test('topo validates', () => {
83
83
  expect(() => assertValidTopo(validate(topo))).not.toThrow();
84
84
  });
@@ -93,7 +93,7 @@ const registerGovernanceSuite = (
93
93
  };
94
94
 
95
95
  export const testAll = (topo: Topo, ctxOrFactory?: TestAllInput): void => {
96
- registerGovernanceSuite(topo, ctxOrFactory, validateTopo);
96
+ registerContractSuite(topo, ctxOrFactory, validateTopo);
97
97
  };
98
98
 
99
99
  type EstablishedInput =
@@ -187,7 +187,7 @@ export const testAllEstablished = (
187
187
  ? optionsOrFactory
188
188
  : () => optionsOrFactory;
189
189
 
190
- registerGovernanceSuite(
190
+ registerContractSuite(
191
191
  topo,
192
192
  () => toExecutionOptions(normalizeEstablishedOptions(resolveInput())),
193
193
  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
  // ---------------------------------------------------------------------------
package/src/index.ts CHANGED
@@ -30,7 +30,7 @@ export {
30
30
  } from './context.js';
31
31
  export { createTestLogger } from './logger.js';
32
32
 
33
- // Trailhead harnesses
33
+ // Surface harnesses
34
34
  export { createCliHarness } from './harness-cli.js';
35
35
  export { createMcpHarness } from './harness-mcp.js';
36
36
 
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
  // ---------------------------------------------------------------------------
package/src/scenario.ts CHANGED
@@ -18,15 +18,13 @@ import type {
18
18
  } from '@ontrails/core';
19
19
  import {
20
20
  buildCrossValidationSchema,
21
+ claimNextCrossBatchIndex,
22
+ createCrossBatchValidationResults,
21
23
  executeTrail,
22
24
  InternalError,
25
+ normalizeCrossBatchConcurrency,
23
26
  Result as R,
24
27
  } from '@ontrails/core';
25
- import {
26
- claimNextCrossBatchIndex,
27
- createCrossBatchValidationResults,
28
- normalizeCrossBatchConcurrency,
29
- } from '@ontrails/core/internal/cross-batch';
30
28
 
31
29
  import { assertPartialMatch, expectOk } from './assertions.js';
32
30
  import { createTestContext, createMockResources } from './context.js';
package/src/signals.ts ADDED
@@ -0,0 +1,221 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+
3
+ import type {
4
+ FireFn,
5
+ TrailContext,
6
+ TrailExample,
7
+ TrailExampleSignalAssertion,
8
+ } from '@ontrails/core';
9
+ import { summarizeSignalPayload } from '@ontrails/core';
10
+
11
+ export interface RecordedSignal {
12
+ readonly payload: unknown;
13
+ readonly signalId: string;
14
+ }
15
+
16
+ export interface SignalAssertionHarness {
17
+ readonly assert: () => void;
18
+ readonly ctx: TrailContext;
19
+ }
20
+
21
+ const noop = (): void => undefined;
22
+
23
+ const resolveSignalId = (signal: unknown): string => {
24
+ if (typeof signal === 'string') {
25
+ return signal;
26
+ }
27
+ if (
28
+ typeof signal === 'object' &&
29
+ signal !== null &&
30
+ 'id' in signal &&
31
+ typeof (signal as { readonly id: unknown }).id === 'string'
32
+ ) {
33
+ return (signal as { readonly id: string }).id;
34
+ }
35
+ return '<unknown signal>';
36
+ };
37
+
38
+ const assertionSignalId = (assertion: TrailExampleSignalAssertion): string =>
39
+ resolveSignalId(assertion.signal);
40
+
41
+ const formatPayloadSummary = (payload: unknown): string => {
42
+ const summary = summarizeSignalPayload(payload);
43
+ const parts = [
44
+ `redacted=${summary.redacted}`,
45
+ `shape=${summary.shape}`,
46
+ `digest=${summary.digest}`,
47
+ ];
48
+ if (summary.topLevelEntryCount !== undefined) {
49
+ parts.push(`topLevelEntryCount=${summary.topLevelEntryCount}`);
50
+ }
51
+ return `{${parts.join(' ')}}`;
52
+ };
53
+
54
+ const formatAssertion = (assertion: TrailExampleSignalAssertion): string => {
55
+ const parts = [`signal=${assertionSignalId(assertion)}`];
56
+ if (assertion.payload !== undefined) {
57
+ parts.push(`payloadSummary=${formatPayloadSummary(assertion.payload)}`);
58
+ }
59
+ if (assertion.payloadMatch !== undefined) {
60
+ parts.push(
61
+ `payloadMatchSummary=${formatPayloadSummary(assertion.payloadMatch)}`
62
+ );
63
+ }
64
+ if (assertion.times !== undefined) {
65
+ parts.push(`times=${assertion.times}`);
66
+ }
67
+ return parts.join(' ');
68
+ };
69
+
70
+ const formatObserved = (observed: readonly RecordedSignal[]): string => {
71
+ if (observed.length === 0) {
72
+ return '<none>';
73
+ }
74
+ return observed
75
+ .map(
76
+ (record) =>
77
+ `${record.signalId} payloadSummary=${formatPayloadSummary(record.payload)}`
78
+ )
79
+ .join('; ');
80
+ };
81
+
82
+ const subsetArrayMatches = (
83
+ actual: readonly unknown[],
84
+ expected: readonly unknown[]
85
+ ): boolean => {
86
+ const consumed = new Set<number>();
87
+ for (const expectedItem of expected) {
88
+ const matchIndex = actual.findIndex(
89
+ (actualItem, index) =>
90
+ !consumed.has(index) &&
91
+ // oxlint-disable-next-line no-use-before-define -- mutual recursion with subsetMatches
92
+ subsetMatches(actualItem, expectedItem)
93
+ );
94
+ if (matchIndex === -1) {
95
+ return false;
96
+ }
97
+ consumed.add(matchIndex);
98
+ }
99
+ return true;
100
+ };
101
+
102
+ const subsetObjectMatches = (
103
+ actual: Record<string, unknown>,
104
+ expected: Record<string, unknown>
105
+ ): boolean =>
106
+ Object.keys(expected).every(
107
+ (key) =>
108
+ key in actual &&
109
+ // oxlint-disable-next-line no-use-before-define -- mutual recursion with subsetMatches
110
+ subsetMatches(actual[key], expected[key])
111
+ );
112
+
113
+ const subsetMatches = (actual: unknown, expected: unknown): boolean => {
114
+ if (Array.isArray(expected)) {
115
+ return Array.isArray(actual) && subsetArrayMatches(actual, expected);
116
+ }
117
+ if (expected !== null && typeof expected === 'object') {
118
+ return (
119
+ actual !== null &&
120
+ typeof actual === 'object' &&
121
+ !Array.isArray(actual) &&
122
+ subsetObjectMatches(
123
+ actual as Record<string, unknown>,
124
+ expected as Record<string, unknown>
125
+ )
126
+ );
127
+ }
128
+ return isDeepStrictEqual(actual, expected);
129
+ };
130
+
131
+ const payloadMatches = (
132
+ payload: unknown,
133
+ assertion: TrailExampleSignalAssertion
134
+ ): boolean => {
135
+ if (
136
+ assertion.payload !== undefined &&
137
+ !isDeepStrictEqual(payload, assertion.payload)
138
+ ) {
139
+ return false;
140
+ }
141
+ if (
142
+ assertion.payloadMatch !== undefined &&
143
+ !subsetMatches(payload, assertion.payloadMatch)
144
+ ) {
145
+ return false;
146
+ }
147
+ return true;
148
+ };
149
+
150
+ const signalMatches = (
151
+ record: RecordedSignal,
152
+ assertion: TrailExampleSignalAssertion
153
+ ): boolean =>
154
+ record.signalId === assertionSignalId(assertion) &&
155
+ payloadMatches(record.payload, assertion);
156
+
157
+ const assertValidTimes = (assertion: TrailExampleSignalAssertion): number => {
158
+ const times = assertion.times ?? 1;
159
+ if (!Number.isInteger(times) || times < 1) {
160
+ throw new Error(
161
+ `Signal assertion has invalid times value: ${formatAssertion(assertion)}`
162
+ );
163
+ }
164
+ return times;
165
+ };
166
+
167
+ const assertSignalAssertion = (
168
+ example: TrailExample<unknown, unknown>,
169
+ assertion: TrailExampleSignalAssertion,
170
+ observed: readonly RecordedSignal[],
171
+ consumed: Set<number>
172
+ ): void => {
173
+ const times = assertValidTimes(assertion);
174
+ for (let count = 0; count < times; count += 1) {
175
+ const matchIndex = observed.findIndex(
176
+ (record, index) =>
177
+ !consumed.has(index) && signalMatches(record, assertion)
178
+ );
179
+ if (matchIndex === -1) {
180
+ throw new Error(
181
+ `Example "${example.name}" expected signal ${formatAssertion(
182
+ assertion
183
+ )}; observed ${formatObserved(observed)}`
184
+ );
185
+ }
186
+ consumed.add(matchIndex);
187
+ }
188
+ };
189
+
190
+ export const assertSignalAssertions = (
191
+ example: TrailExample<unknown, unknown>,
192
+ observed: readonly RecordedSignal[]
193
+ ): void => {
194
+ const consumed = new Set<number>();
195
+ for (const assertion of example.signals ?? []) {
196
+ assertSignalAssertion(example, assertion, observed, consumed);
197
+ }
198
+ };
199
+
200
+ export const withSignalAssertions = (
201
+ ctx: TrailContext,
202
+ example: TrailExample<unknown, unknown>
203
+ ): SignalAssertionHarness => {
204
+ if (example.signals === undefined || example.signals.length === 0) {
205
+ return { assert: noop, ctx };
206
+ }
207
+
208
+ const observed: RecordedSignal[] = [];
209
+ const baseFire = ctx.fire as
210
+ | ((signal: unknown, payload: unknown) => Promise<void>)
211
+ | undefined;
212
+ const fire = (async (signal: unknown, payload: unknown): Promise<void> => {
213
+ observed.push({ payload, signalId: resolveSignalId(signal) });
214
+ await baseFire?.(signal, payload);
215
+ }) as FireFn;
216
+
217
+ return {
218
+ assert: () => assertSignalAssertions(example, observed),
219
+ ctx: { ...ctx, fire },
220
+ };
221
+ };
package/src/types.ts CHANGED
@@ -11,7 +11,7 @@ import type {
11
11
  TraceFn,
12
12
  TrailContext,
13
13
  } from '@ontrails/core';
14
- import type { LogLevel, LogRecord } from '@ontrails/logging';
14
+ import type { LogLevel, LogRecord } from '@ontrails/observe';
15
15
 
16
16
  // ---------------------------------------------------------------------------
17
17
  // Test Scenario (for testTrail)
@@ -1 +0,0 @@
1
- $ tsc -b
@@ -1,3 +0,0 @@
1
- $ oxlint ./src
2
- Found 0 warnings and 0 errors.
3
- Finished in 81ms on 28 files with 93 rules using 24 threads.
@@ -1 +0,0 @@
1
- $ tsc --noEmit
package/dist/all.d.ts DELETED
@@ -1,36 +0,0 @@
1
- /**
2
- * testAll — single-line governance suite for any Topo.
3
- *
4
- * Wraps topo validation, example execution, contract checks, and detour
5
- * contract validation into one describe block.
6
- */
7
- import type { Topo, TrailContext } from '@ontrails/core';
8
- import type { TestExecutionOptions } from './context.js';
9
- import type { TestAllEstablishedOptions } from './types.js';
10
- /**
11
- * Run the full governance test suite for a Topo.
12
- *
13
- * Generates a `governance` describe block containing:
14
- * - Structural validation via `validateTopo`
15
- * - Example execution via `testExamples`
16
- * - Output contract checks via `testContracts`
17
- * - Detour contract validation via `testDetours`
18
- *
19
- * Accepts either a static context or a factory function that produces a
20
- * fresh context per test (useful when the context contains mutable state
21
- * like an in-memory store).
22
- *
23
- * @example
24
- * ```ts
25
- * import { testAll } from '@ontrails/testing';
26
- * import { graph } from '../src/app.js';
27
- *
28
- * testAll(graph);
29
- * ```
30
- */
31
- type TestAllInput = Partial<TrailContext> | TestExecutionOptions | (() => Partial<TrailContext> | TestExecutionOptions);
32
- export declare const testAll: (topo: Topo, ctxOrFactory?: TestAllInput) => void;
33
- type EstablishedInput = Partial<TrailContext> | TestAllEstablishedOptions | (() => Partial<TrailContext> | TestAllEstablishedOptions);
34
- export declare const testAllEstablished: (topo: Topo, optionsOrFactory?: EstablishedInput) => void;
35
- export {};
36
- //# sourceMappingURL=all.d.ts.map
package/dist/all.d.ts.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"all.d.ts","sourceRoot":"","sources":["../src/all.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAMzD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGzD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,KAAK,YAAY,GACb,OAAO,CAAC,YAAY,CAAC,GACrB,oBAAoB,GACpB,CAAC,MAAM,OAAO,CAAC,YAAY,CAAC,GAAG,oBAAoB,CAAC,CAAC;AAkDzD,eAAO,MAAM,OAAO,GAAI,MAAM,IAAI,EAAE,eAAe,YAAY,KAAG,IAEjE,CAAC;AAEF,KAAK,gBAAgB,GACjB,OAAO,CAAC,YAAY,CAAC,GACrB,yBAAyB,GACzB,CAAC,MAAM,OAAO,CAAC,YAAY,CAAC,GAAG,yBAAyB,CAAC,CAAC;AA+E9D,eAAO,MAAM,kBAAkB,GAC7B,MAAM,IAAI,EACV,mBAAmB,gBAAgB,KAClC,IAYF,CAAC"}