@ontrails/testing 1.0.0-beta.2 → 1.0.0-beta.21
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 +396 -5
- package/README.md +86 -22
- package/package.json +34 -5
- package/src/all-established.ts +168 -0
- package/src/all.ts +51 -12
- package/src/assertions.ts +253 -0
- package/src/cli.ts +6 -0
- package/src/composes.ts +433 -0
- package/src/context.ts +200 -14
- package/src/contracts.ts +45 -21
- package/src/detours.ts +155 -18
- package/src/effective-examples.ts +414 -0
- package/src/errors.ts +47 -0
- package/src/examples.ts +302 -177
- package/src/harness-cli.ts +83 -58
- package/src/harness-http.ts +341 -0
- package/src/harness-mcp.ts +46 -16
- package/src/http.ts +10 -0
- package/src/index.ts +22 -14
- package/src/logger.ts +3 -1
- package/src/mcp.ts +6 -0
- package/src/scenario.ts +375 -0
- package/src/signals.ts +221 -0
- package/src/surface-parity.ts +389 -0
- package/src/trail.ts +1 -1
- package/src/types.ts +24 -52
- 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 -30
- package/dist/all.d.ts.map +0 -1
- package/dist/all.js +0 -47
- package/dist/all.js.map +0 -1
- package/dist/assertions.d.ts +0 -49
- package/dist/assertions.d.ts.map +0 -1
- package/dist/assertions.js +0 -84
- package/dist/assertions.js.map +0 -1
- package/dist/context.d.ts +0 -19
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -33
- package/dist/context.js.map +0 -1
- package/dist/contracts.d.ts +0 -16
- package/dist/contracts.d.ts.map +0 -1
- package/dist/contracts.js +0 -56
- package/dist/contracts.js.map +0 -1
- package/dist/detours.d.ts +0 -12
- package/dist/detours.d.ts.map +0 -1
- package/dist/detours.js +0 -30
- package/dist/detours.js.map +0 -1
- package/dist/examples.d.ts +0 -22
- package/dist/examples.d.ts.map +0 -1
- package/dist/examples.js +0 -187
- 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 -213
- 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 -50
- package/dist/harness-mcp.js.map +0 -1
- package/dist/hike.d.ts +0 -32
- package/dist/hike.d.ts.map +0 -1
- package/dist/hike.js +0 -169
- package/dist/hike.js.map +0 -1
- package/dist/index.d.ts +0 -14
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -16
- 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/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 -80
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/src/__tests__/context.test.ts +0 -60
- package/src/__tests__/contracts.test.ts +0 -68
- package/src/__tests__/detours.test.ts +0 -55
- package/src/__tests__/examples.test.ts +0 -176
- package/src/__tests__/hike.test.ts +0 -164
- package/src/__tests__/logger.test.ts +0 -136
- package/src/__tests__/trail.test.ts +0 -99
- package/src/hike.ts +0 -283
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
package/src/contracts.ts
CHANGED
|
@@ -2,18 +2,26 @@
|
|
|
2
2
|
* testContracts — output schema verification.
|
|
3
3
|
*
|
|
4
4
|
* For every trail that has both examples and an output schema,
|
|
5
|
-
* run each example and validate the
|
|
5
|
+
* run each example and validate the Result.ok value against
|
|
6
6
|
* the declared schema.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, test } from 'bun:test';
|
|
10
10
|
|
|
11
11
|
import type { Topo, TrailExample, Trail, TrailContext } from '@ontrails/core';
|
|
12
|
-
import { formatZodIssues, validateInput } from '@ontrails/core';
|
|
12
|
+
import { executeTrail, formatZodIssues, validateInput } from '@ontrails/core';
|
|
13
13
|
import type { z } from 'zod';
|
|
14
14
|
|
|
15
15
|
import { expectOk } from './assertions.js';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
mergeResourceOverrides,
|
|
18
|
+
mergeTestContext,
|
|
19
|
+
normalizeTestExecutionOptions,
|
|
20
|
+
createMockResources,
|
|
21
|
+
} from './context.js';
|
|
22
|
+
import type { TestExecutionOptions } from './context.js';
|
|
23
|
+
import type { TrailExampleTarget } from './effective-examples.js';
|
|
24
|
+
import { deriveTrailExampleTargets } from './effective-examples.js';
|
|
17
25
|
|
|
18
26
|
// ---------------------------------------------------------------------------
|
|
19
27
|
// Helpers
|
|
@@ -34,47 +42,63 @@ const validateOutputSchema = (
|
|
|
34
42
|
}
|
|
35
43
|
};
|
|
36
44
|
|
|
45
|
+
type ContractExampleTarget = TrailExampleTarget & {
|
|
46
|
+
readonly output: z.ZodType;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const hasContractExamples = (
|
|
50
|
+
target: TrailExampleTarget
|
|
51
|
+
): target is ContractExampleTarget =>
|
|
52
|
+
target.output !== undefined && target.examples.length > 0;
|
|
53
|
+
|
|
37
54
|
// ---------------------------------------------------------------------------
|
|
38
55
|
// testContracts
|
|
39
56
|
// ---------------------------------------------------------------------------
|
|
40
57
|
|
|
41
58
|
/**
|
|
42
|
-
* Verify that every trail
|
|
43
|
-
* output schema. Catches
|
|
59
|
+
* Verify that every successful trail result matches its declared
|
|
60
|
+
* output schema. Catches output-schema drift.
|
|
44
61
|
*
|
|
45
62
|
* Trails without output schemas or examples are skipped.
|
|
46
63
|
*/
|
|
47
64
|
export const testContracts = (
|
|
48
65
|
app: Topo,
|
|
49
|
-
ctxOrFactory?:
|
|
66
|
+
ctxOrFactory?:
|
|
67
|
+
| Partial<TrailContext>
|
|
68
|
+
| TestExecutionOptions
|
|
69
|
+
| (() => Partial<TrailContext> | TestExecutionOptions)
|
|
50
70
|
): void => {
|
|
51
|
-
const
|
|
71
|
+
const resolveInput =
|
|
52
72
|
typeof ctxOrFactory === 'function' ? ctxOrFactory : () => ctxOrFactory;
|
|
53
|
-
const
|
|
73
|
+
const allEntries = (app.list() as Trail<unknown, unknown, unknown>[])
|
|
74
|
+
.flatMap(deriveTrailExampleTargets)
|
|
75
|
+
.filter(hasContractExamples);
|
|
54
76
|
|
|
55
77
|
describe('contracts', () => {
|
|
56
|
-
describe.each(
|
|
57
|
-
const t = trailDef as Trail<unknown, unknown>;
|
|
58
|
-
|
|
59
|
-
if (t.output === undefined) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (t.examples === undefined || t.examples.length === 0) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
78
|
+
describe.each(allEntries)('$id', (t) => {
|
|
66
79
|
const { examples, output: outputSchema } = t;
|
|
67
80
|
const successExamples = examples.filter((e) => e.error === undefined);
|
|
68
81
|
|
|
69
82
|
test.each(successExamples)(
|
|
70
83
|
'contract: $name',
|
|
71
84
|
async (example: TrailExample<unknown, unknown>) => {
|
|
72
|
-
const
|
|
85
|
+
const resolved = normalizeTestExecutionOptions(resolveInput());
|
|
86
|
+
const resources = mergeResourceOverrides(
|
|
87
|
+
await createMockResources(app),
|
|
88
|
+
resolved.ctx,
|
|
89
|
+
resolved.resources
|
|
90
|
+
);
|
|
91
|
+
const testCtx = mergeTestContext(resolved.ctx);
|
|
73
92
|
|
|
74
93
|
const validated = validateInput(t.input, example.input);
|
|
75
|
-
|
|
94
|
+
expectOk(validated);
|
|
76
95
|
|
|
77
|
-
const result = await t.
|
|
96
|
+
const result = await executeTrail(t.trail, example.input, {
|
|
97
|
+
ctx: testCtx,
|
|
98
|
+
resources,
|
|
99
|
+
topo: app,
|
|
100
|
+
...(t.version === undefined ? {} : { version: t.version }),
|
|
101
|
+
});
|
|
78
102
|
const resultValue = expectOk(result);
|
|
79
103
|
|
|
80
104
|
validateOutputSchema(outputSchema, resultValue, t.id, example.name);
|
package/src/detours.ts
CHANGED
|
@@ -1,42 +1,179 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* testDetours —
|
|
2
|
+
* testDetours — validate the live detour contract for every trail.
|
|
3
3
|
*
|
|
4
|
-
* Pure structural validation. No
|
|
4
|
+
* Pure structural validation. No blaze or detour recovery execution needed.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { describe,
|
|
7
|
+
import { describe, test } from 'bun:test';
|
|
8
8
|
|
|
9
|
+
import { TrailsError } from '@ontrails/core';
|
|
9
10
|
import type { Topo, Trail } from '@ontrails/core';
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
interface RuntimeDetour {
|
|
13
|
+
readonly on?: unknown;
|
|
14
|
+
readonly recover?: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const isErrorConstructor = (
|
|
18
|
+
value: unknown
|
|
19
|
+
): value is abstract new (...args: never[]) => Error => {
|
|
20
|
+
if (typeof value !== 'function') {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { prototype } = value as { prototype?: unknown };
|
|
25
|
+
return prototype instanceof Error;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const detourLabel = (trailId: string, index: number, detour: RuntimeDetour) => {
|
|
29
|
+
if (typeof detour.on === 'function') {
|
|
30
|
+
const { name } = detour.on as { name?: unknown };
|
|
31
|
+
if (typeof name === 'string' && name.length > 0) {
|
|
32
|
+
return `${trailId} detour[${index}] on ${name}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return `${trailId} detour[${index}]`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const assertValidOn = (
|
|
40
|
+
trailId: string,
|
|
41
|
+
index: number,
|
|
42
|
+
detour: RuntimeDetour
|
|
43
|
+
): void => {
|
|
44
|
+
if (isErrorConstructor(detour.on)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error(
|
|
49
|
+
`${detourLabel(trailId, index, detour)} must declare a real error constructor in on:`
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const assertCallableRecover = (
|
|
54
|
+
trailId: string,
|
|
55
|
+
index: number,
|
|
56
|
+
detour: RuntimeDetour
|
|
57
|
+
): void => {
|
|
58
|
+
if (typeof detour.recover === 'function') {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw new Error(
|
|
63
|
+
`${detourLabel(trailId, index, detour)} must declare a callable recover function`
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const sameOrSubtype = (
|
|
68
|
+
candidate: abstract new (...args: never[]) => Error,
|
|
69
|
+
ancestor: abstract new (...args: never[]) => Error
|
|
70
|
+
): boolean => {
|
|
71
|
+
if (candidate === ancestor) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let current = Object.getPrototypeOf(candidate.prototype);
|
|
76
|
+
while (current && typeof current === 'object') {
|
|
77
|
+
const ctor = (current as { constructor?: unknown }).constructor;
|
|
78
|
+
if (ctor === ancestor) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
current = Object.getPrototypeOf(current);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return false;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const getShadowingDetour = (
|
|
88
|
+
detours: readonly RuntimeDetour[],
|
|
89
|
+
index: number
|
|
90
|
+
):
|
|
91
|
+
| {
|
|
92
|
+
readonly index: number;
|
|
93
|
+
readonly on: abstract new (...args: never[]) => Error;
|
|
94
|
+
}
|
|
95
|
+
| undefined => {
|
|
96
|
+
const detour = detours[index];
|
|
97
|
+
if (!detour || !isErrorConstructor(detour.on)) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (let previousIndex = 0; previousIndex < index; previousIndex += 1) {
|
|
102
|
+
const previous = detours[previousIndex];
|
|
103
|
+
if (!previous || !isErrorConstructor(previous.on)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (sameOrSubtype(detour.on, previous.on)) {
|
|
108
|
+
return { index: previousIndex, on: previous.on };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return undefined;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const assertNotShadowed = (
|
|
116
|
+
trailId: string,
|
|
117
|
+
detours: readonly RuntimeDetour[],
|
|
118
|
+
index: number
|
|
119
|
+
): void => {
|
|
120
|
+
const detour = detours[index];
|
|
121
|
+
if (!detour || !isErrorConstructor(detour.on)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const shadowing = getShadowingDetour(detours, index);
|
|
126
|
+
if (!shadowing) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const previousName = shadowing.on.name || TrailsError.name;
|
|
131
|
+
const currentName = detour.on.name || TrailsError.name;
|
|
132
|
+
throw new Error(
|
|
133
|
+
`${trailId} detour[${index}] on ${currentName} is shadowed by earlier detour[${shadowing.index}] on ${previousName}`
|
|
134
|
+
);
|
|
135
|
+
};
|
|
14
136
|
|
|
15
137
|
/**
|
|
16
|
-
* Verify that every trail's
|
|
17
|
-
*
|
|
138
|
+
* Verify that every trail's detours match the live runtime contract:
|
|
139
|
+
* `on` must be an error constructor, `recover` must be callable, and
|
|
140
|
+
* later detours must not be shadowed by earlier broader `on` types.
|
|
18
141
|
*/
|
|
19
142
|
export const testDetours = (app: Topo): void => {
|
|
20
143
|
const trailEntries = [...app.trails];
|
|
21
144
|
|
|
22
145
|
describe('detours', () => {
|
|
23
146
|
describe.each(trailEntries)('%s', (_id, trailDef) => {
|
|
24
|
-
const
|
|
147
|
+
const trail = trailDef as Trail<unknown, unknown, unknown>;
|
|
25
148
|
|
|
26
|
-
if (
|
|
149
|
+
if (trail.detours.length === 0) {
|
|
27
150
|
return;
|
|
28
151
|
}
|
|
29
152
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
153
|
+
const detourCases = trail.detours.map((detour, index) => ({
|
|
154
|
+
detour,
|
|
155
|
+
index,
|
|
156
|
+
trailId: trail.id,
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
test.each(detourCases)(
|
|
160
|
+
'$trailId detour[$index] uses an error constructor',
|
|
161
|
+
({ detour, index, trailId }) => {
|
|
162
|
+
assertValidOn(trailId, index, detour);
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
test.each(detourCases)(
|
|
167
|
+
'$trailId detour[$index] provides a callable recover',
|
|
168
|
+
({ detour, index, trailId }) => {
|
|
169
|
+
assertCallableRecover(trailId, index, detour);
|
|
170
|
+
}
|
|
34
171
|
);
|
|
35
172
|
|
|
36
|
-
test.each(
|
|
37
|
-
'detour
|
|
38
|
-
({
|
|
39
|
-
|
|
173
|
+
test.each(detourCases)(
|
|
174
|
+
'$trailId detour[$index] is not shadowed by an earlier detour',
|
|
175
|
+
({ index, trailId }) => {
|
|
176
|
+
assertNotShadowed(trailId, trail.detours, index);
|
|
40
177
|
}
|
|
41
178
|
);
|
|
42
179
|
});
|