@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.
- package/CHANGELOG.md +56 -1
- package/README.md +2 -2
- package/package.json +15 -7
- package/src/all.ts +7 -7
- package/src/context.ts +3 -0
- package/src/contracts.ts +1 -16
- package/src/examples.ts +19 -11
- package/src/index.ts +1 -1
- package/src/logger.ts +3 -1
- package/src/scenario.ts +3 -5
- package/src/signals.ts +221 -0
- package/src/types.ts +1 -1
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/all.d.ts +0 -36
- package/dist/all.d.ts.map +0 -1
- package/dist/all.js +0 -99
- package/dist/all.js.map +0 -1
- package/dist/assertions.d.ts +0 -72
- package/dist/assertions.d.ts.map +0 -1
- package/dist/assertions.js +0 -238
- package/dist/assertions.js.map +0 -1
- package/dist/context.d.ts +0 -76
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -118
- package/dist/context.js.map +0 -1
- package/dist/contracts.d.ts +0 -17
- package/dist/contracts.d.ts.map +0 -1
- package/dist/contracts.js +0 -75
- package/dist/contracts.js.map +0 -1
- package/dist/crosses.d.ts +0 -38
- package/dist/crosses.d.ts.map +0 -1
- package/dist/crosses.js +0 -223
- package/dist/crosses.js.map +0 -1
- package/dist/detours.d.ts +0 -13
- package/dist/detours.d.ts.map +0 -1
- package/dist/detours.js +0 -109
- package/dist/detours.js.map +0 -1
- package/dist/effective-examples.d.ts +0 -30
- package/dist/effective-examples.d.ts.map +0 -1
- package/dist/effective-examples.js +0 -227
- package/dist/effective-examples.js.map +0 -1
- package/dist/examples.d.ts +0 -23
- package/dist/examples.d.ts.map +0 -1
- package/dist/examples.js +0 -240
- package/dist/examples.js.map +0 -1
- package/dist/harness-cli.d.ts +0 -21
- package/dist/harness-cli.d.ts.map +0 -1
- package/dist/harness-cli.js +0 -205
- package/dist/harness-cli.js.map +0 -1
- package/dist/harness-mcp.d.ts +0 -21
- package/dist/harness-mcp.d.ts.map +0 -1
- package/dist/harness-mcp.js +0 -54
- package/dist/harness-mcp.js.map +0 -1
- package/dist/index.d.ts +0 -18
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -19
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts +0 -15
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -87
- package/dist/logger.js.map +0 -1
- package/dist/scenario.d.ts +0 -37
- package/dist/scenario.d.ts.map +0 -1
- package/dist/scenario.js +0 -235
- package/dist/scenario.js.map +0 -1
- package/dist/trail.d.ts +0 -20
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -80
- package/dist/trail.js.map +0 -1
- package/dist/types.d.ts +0 -113
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/src/__tests__/all.test.ts +0 -254
- package/src/__tests__/context.test.ts +0 -170
- package/src/__tests__/contracts.test.ts +0 -239
- package/src/__tests__/crosses.test.ts +0 -587
- package/src/__tests__/detours.test.ts +0 -212
- package/src/__tests__/effective-examples.test.ts +0 -203
- package/src/__tests__/examples.test.ts +0 -636
- package/src/__tests__/harness-cli.test.ts +0 -90
- package/src/__tests__/harness-mcp.test.ts +0 -37
- package/src/__tests__/logger.test.ts +0 -136
- package/src/__tests__/partial-match.test.ts +0 -126
- package/src/__tests__/scenario.test.ts +0 -381
- package/src/__tests__/trail.test.ts +0 -99
- package/tsconfig.json +0 -9
- package/tsconfig.tests.json +0 -10
- 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/
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
18
|
-
"@ontrails/store": "^1.0.0-beta.
|
|
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.
|
|
22
|
-
"@ontrails/core": "^1.0.0-beta.
|
|
23
|
-
"@ontrails/
|
|
24
|
-
"@ontrails/
|
|
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
|
|
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
|
|
22
|
+
* Run the full contract test suite for a Topo.
|
|
23
23
|
*
|
|
24
|
-
* Generates a `
|
|
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
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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):
|
|
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
|
-
|
|
288
|
+
signals.ctx.cross,
|
|
282
289
|
topo,
|
|
283
|
-
|
|
290
|
+
signals.ctx,
|
|
284
291
|
resources
|
|
285
292
|
);
|
|
286
|
-
const testCtx: TrailContext = { ...
|
|
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
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,
|
|
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/
|
|
14
|
+
import type { LogLevel, LogRecord } from '@ontrails/observe';
|
|
15
15
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
// Test Scenario (for testTrail)
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
$ tsc -b
|
package/.turbo/turbo-lint.log
DELETED
|
@@ -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"}
|