@ontrails/testing 1.0.0-beta.0 → 1.0.0-beta.10
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/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +151 -0
- package/README.md +56 -168
- package/dist/context.d.ts +20 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +25 -0
- package/dist/context.js.map +1 -1
- package/dist/contracts.d.ts +1 -1
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +15 -5
- package/dist/contracts.js.map +1 -1
- package/dist/examples.d.ts +2 -2
- package/dist/examples.d.ts.map +1 -1
- package/dist/examples.js +43 -55
- package/dist/examples.js.map +1 -1
- package/dist/follows.d.ts +32 -0
- package/dist/follows.d.ts.map +1 -0
- package/dist/{hike.js → follows.js} +15 -15
- package/dist/follows.js.map +1 -0
- package/dist/harness-mcp.d.ts.map +1 -1
- package/dist/harness-mcp.js +5 -2
- package/dist/harness-mcp.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/trail.js +1 -1
- package/dist/trail.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/context.test.ts +36 -1
- package/src/__tests__/contracts.test.ts +30 -4
- package/src/__tests__/detours.test.ts +3 -3
- package/src/__tests__/examples.test.ts +21 -22
- package/src/__tests__/{hike.test.ts → follows.test.ts} +27 -28
- package/src/__tests__/trail.test.ts +4 -4
- package/src/context.ts +42 -1
- package/src/contracts.ts +19 -6
- package/src/examples.ts +55 -91
- package/src/{hike.ts → follows.ts} +30 -30
- package/src/harness-mcp.ts +5 -2
- package/src/index.ts +5 -4
- package/src/trail.ts +1 -1
- package/src/types.ts +3 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/hike.d.ts +0 -32
- package/dist/hike.d.ts.map +0 -1
- package/dist/hike.js.map +0 -1
package/.turbo/turbo-lint.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,156 @@
|
|
|
1
1
|
# @ontrails/testing
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
- @ontrails/core@1.0.0-beta.10
|
|
9
|
+
- @ontrails/cli@1.0.0-beta.10
|
|
10
|
+
- @ontrails/mcp@1.0.0-beta.10
|
|
11
|
+
- @ontrails/logging@1.0.0-beta.10
|
|
12
|
+
|
|
13
|
+
## 1.0.0-beta.9
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- Consolidated improvements across all surface packages.
|
|
18
|
+
|
|
19
|
+
**core**: Add `TrailResult<T>` utility type, `topo.ids()` and `topo.count` accessors, `dispatch()` for headless trail execution, and extract shared `executeTrail` pipeline used by CLI/MCP/HTTP.
|
|
20
|
+
|
|
21
|
+
**http**: Detect route path collisions and return `Result` from `buildHttpRoutes()`, wire request `AbortSignal` through to trail context, and make write → POST mapping explicit in intent-to-method lookup.
|
|
22
|
+
|
|
23
|
+
**mcp**: Return `Result` from `buildMcpTools()` on collision instead of throwing.
|
|
24
|
+
|
|
25
|
+
**cli**: Verify exception catching via centralized `executeTrail`.
|
|
26
|
+
|
|
27
|
+
**testing**: Follow context awareness improvements.
|
|
28
|
+
|
|
29
|
+
**warden**: Refactor rules as composable trails with examples.
|
|
30
|
+
|
|
31
|
+
**schema**: Error code and empty body fixes.
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- Updated dependencies
|
|
36
|
+
- @ontrails/core@1.0.0-beta.9
|
|
37
|
+
- @ontrails/cli@1.0.0-beta.9
|
|
38
|
+
- @ontrails/mcp@1.0.0-beta.9
|
|
39
|
+
- @ontrails/logging@1.0.0-beta.9
|
|
40
|
+
|
|
41
|
+
## 1.0.0-beta.8
|
|
42
|
+
|
|
43
|
+
### Patch Changes
|
|
44
|
+
|
|
45
|
+
- @ontrails/cli@1.0.0-beta.8
|
|
46
|
+
- @ontrails/core@1.0.0-beta.8
|
|
47
|
+
- @ontrails/logging@1.0.0-beta.8
|
|
48
|
+
- @ontrails/mcp@1.0.0-beta.8
|
|
49
|
+
|
|
50
|
+
## 1.0.0-beta.7
|
|
51
|
+
|
|
52
|
+
### Patch Changes
|
|
53
|
+
|
|
54
|
+
- @ontrails/cli@1.0.0-beta.7
|
|
55
|
+
- @ontrails/core@1.0.0-beta.7
|
|
56
|
+
- @ontrails/logging@1.0.0-beta.7
|
|
57
|
+
- @ontrails/mcp@1.0.0-beta.7
|
|
58
|
+
|
|
59
|
+
## 1.0.0-beta.6
|
|
60
|
+
|
|
61
|
+
### Patch Changes
|
|
62
|
+
|
|
63
|
+
- Updated dependencies
|
|
64
|
+
- @ontrails/core@1.0.0-beta.6
|
|
65
|
+
- @ontrails/cli@1.0.0-beta.6
|
|
66
|
+
- @ontrails/logging@1.0.0-beta.6
|
|
67
|
+
- @ontrails/mcp@1.0.0-beta.6
|
|
68
|
+
|
|
69
|
+
## 1.0.0-beta.5
|
|
70
|
+
|
|
71
|
+
### Patch Changes
|
|
72
|
+
|
|
73
|
+
- Updated dependencies
|
|
74
|
+
- @ontrails/core@1.0.0-beta.5
|
|
75
|
+
- @ontrails/logging@1.0.0-beta.5
|
|
76
|
+
- @ontrails/cli@1.0.0-beta.5
|
|
77
|
+
- @ontrails/mcp@1.0.0-beta.5
|
|
78
|
+
|
|
79
|
+
## 1.0.0-beta.4
|
|
80
|
+
|
|
81
|
+
### Major Changes
|
|
82
|
+
|
|
83
|
+
- API simplification: unified trail model, intent enum, run, metadata.
|
|
84
|
+
|
|
85
|
+
**BREAKING CHANGES:**
|
|
86
|
+
|
|
87
|
+
- `hike()` removed — use `trail()` with optional `follow: [...]` field
|
|
88
|
+
- `follows` renamed to `follow` (singular, matching `ctx.follow()`)
|
|
89
|
+
- `topo.hikes` removed — single `topo.trails` map
|
|
90
|
+
- `kind: 'hike'` removed — everything is `kind: 'trail'`
|
|
91
|
+
- `readOnly`/`destructive` booleans replaced by `intent: 'read' | 'write' | 'destroy'`
|
|
92
|
+
- `implementation` field renamed to `run`
|
|
93
|
+
- `markers` field renamed to `metadata`
|
|
94
|
+
- `testHike` renamed to `testFollows`, `HikeScenario` to `FollowScenario`
|
|
95
|
+
- `blaze()` now returns the surface handle (`Command` for CLI, `Server` for MCP)
|
|
96
|
+
|
|
97
|
+
### Patch Changes
|
|
98
|
+
|
|
99
|
+
- Updated dependencies
|
|
100
|
+
- @ontrails/core@1.0.0-beta.4
|
|
101
|
+
- @ontrails/cli@1.0.0-beta.4
|
|
102
|
+
- @ontrails/mcp@1.0.0-beta.4
|
|
103
|
+
- @ontrails/logging@1.0.0-beta.4
|
|
104
|
+
|
|
105
|
+
## 1.0.0-beta.3
|
|
106
|
+
|
|
107
|
+
### Minor Changes
|
|
108
|
+
|
|
109
|
+
- Bug fixes across all surface packages found via parallel Codex review.
|
|
110
|
+
|
|
111
|
+
**core**: Fix Result.toJson false circular detection on DAGs, deserializeError subclass round-trip, topo cross-kind ID collisions, validateTopo multi-node cycle detection, error example input validation bypass, and deriveFields array type collapse.
|
|
112
|
+
|
|
113
|
+
**cli**: Switch blaze to parseAsync for proper async error handling, add boolean flag negation (--no-flag), and strict number parsing that rejects partial input.
|
|
114
|
+
|
|
115
|
+
**mcp**: Align BlobRef with core (including ReadableStream support) and detect tool-name collisions after normalization.
|
|
116
|
+
|
|
117
|
+
**testing**: Include hikes in testContracts validation, with follow-context awareness.
|
|
118
|
+
|
|
119
|
+
**warden**: Collect hike detour targets, validate detour refs in hike specs, and stop implementation-returns-result from walking into nested function bodies.
|
|
120
|
+
|
|
121
|
+
### Patch Changes
|
|
122
|
+
|
|
123
|
+
- Updated dependencies
|
|
124
|
+
- @ontrails/core@1.0.0-beta.3
|
|
125
|
+
- @ontrails/cli@1.0.0-beta.3
|
|
126
|
+
- @ontrails/mcp@1.0.0-beta.3
|
|
127
|
+
- @ontrails/logging@1.0.0-beta.3
|
|
128
|
+
|
|
129
|
+
## 1.0.0-beta.2
|
|
130
|
+
|
|
131
|
+
### Patch Changes
|
|
132
|
+
|
|
133
|
+
- Fix workspace dependency resolution in published packages. Now using bun publish
|
|
134
|
+
which correctly replaces workspace:^ with actual version numbers.
|
|
135
|
+
- Updated dependencies
|
|
136
|
+
- @ontrails/core@1.0.0-beta.2
|
|
137
|
+
- @ontrails/cli@1.0.0-beta.2
|
|
138
|
+
- @ontrails/mcp@1.0.0-beta.2
|
|
139
|
+
- @ontrails/logging@1.0.0-beta.2
|
|
140
|
+
|
|
141
|
+
## 1.0.0-beta.1
|
|
142
|
+
|
|
143
|
+
### Patch Changes
|
|
144
|
+
|
|
145
|
+
- Fix two blocking bugs from real-world migration:
|
|
146
|
+
- Published packages now resolve correctly (workspace:^ instead of workspace:\*)
|
|
147
|
+
- Error forwarding works across different success types (Err no longer carries phantom T)
|
|
148
|
+
- Updated dependencies
|
|
149
|
+
- @ontrails/core@1.0.0-beta.1
|
|
150
|
+
- @ontrails/cli@1.0.0-beta.1
|
|
151
|
+
- @ontrails/mcp@1.0.0-beta.1
|
|
152
|
+
- @ontrails/logging@1.0.0-beta.1
|
|
153
|
+
|
|
3
154
|
## 1.0.0-beta.0
|
|
4
155
|
|
|
5
156
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,40 +1,8 @@
|
|
|
1
1
|
# @ontrails/testing
|
|
2
2
|
|
|
3
|
-
Contract-driven testing
|
|
3
|
+
Contract-driven testing for Trails. Add examples to your trails, then `testAll(app)` runs them as assertions, validates output schemas, checks composition graphs, and verifies structural integrity. One line of test code, full governance.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
bun add -d @ontrails/testing
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Peer dependencies: `@ontrails/core`, `@ontrails/cli`, `@ontrails/mcp`, `@ontrails/logging`, `zod`.
|
|
12
|
-
|
|
13
|
-
## Quick Start
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
import { testExamples } from '@ontrails/testing';
|
|
17
|
-
import { app } from '../app';
|
|
18
|
-
|
|
19
|
-
testExamples(app);
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
One line. The entire topo is tested. Every trail, every example: input validation, implementation execution, output verification.
|
|
23
|
-
|
|
24
|
-
```text
|
|
25
|
-
PASS src/__tests__/app.test.ts
|
|
26
|
-
greet
|
|
27
|
-
example: Basic greeting
|
|
28
|
-
example: Loud greeting
|
|
29
|
-
entity.show
|
|
30
|
-
example: Show entity by name
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## API Overview
|
|
34
|
-
|
|
35
|
-
### `testAll(topo, ctx?)`
|
|
36
|
-
|
|
37
|
-
Single-line governance suite. Wraps topo validation, example execution, contract checks, and detour verification into one `governance` describe block:
|
|
5
|
+
## Usage
|
|
38
6
|
|
|
39
7
|
```typescript
|
|
40
8
|
import { testAll } from '@ontrails/testing';
|
|
@@ -43,26 +11,38 @@ import { app } from '../app';
|
|
|
43
11
|
testAll(app);
|
|
44
12
|
```
|
|
45
13
|
|
|
46
|
-
For most apps,
|
|
14
|
+
That single call covers example execution, contract validation, detour checks, and topo validation. For most apps, this is all you need.
|
|
47
15
|
|
|
48
|
-
|
|
16
|
+
If you want finer control:
|
|
49
17
|
|
|
50
|
-
|
|
18
|
+
```typescript
|
|
19
|
+
import { testExamples, testContracts, testDetours } from '@ontrails/testing';
|
|
51
20
|
|
|
52
|
-
|
|
21
|
+
testExamples(app); // Run every trail's examples as tests
|
|
22
|
+
testContracts(app); // Validate outputs against declared schemas
|
|
23
|
+
testDetours(app); // Verify detour targets exist
|
|
24
|
+
```
|
|
53
25
|
|
|
54
|
-
|
|
55
|
-
2. Calls the implementation with validated input
|
|
56
|
-
3. Applies progressive assertion (see below)
|
|
57
|
-
4. Validates output against the trail's output schema (if present)
|
|
26
|
+
## API
|
|
58
27
|
|
|
59
|
-
|
|
28
|
+
| Export | What it does |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `testAll(topo, ctx?)` | Single-line governance suite: validation + examples + contracts + detours |
|
|
31
|
+
| `testExamples(topo, ctx?)` | Run trail examples as `describe`/`test` blocks |
|
|
32
|
+
| `testTrail(trail, scenarios)` | Custom scenarios for edge cases, error paths, and follow chains |
|
|
33
|
+
| `testContracts(topo, ctx?)` | Validate output against declared schemas |
|
|
34
|
+
| `testDetours(topo)` | Verify every detour target exists in the topo |
|
|
35
|
+
| `createFollowContext(options?)` | Mock `FollowFn` for testing composite trails; returns preconfigured `Result` values keyed by trail ID |
|
|
36
|
+
| `createTestContext(options?)` | `TrailContext` with sensible test defaults |
|
|
37
|
+
| `createTestLogger()` | Logger that captures entries in memory for assertions |
|
|
38
|
+
| `createCliHarness(options)` | Execute CLI commands in-process, capture stdout/stderr |
|
|
39
|
+
| `createMcpHarness(options)` | Invoke MCP tools directly without transport |
|
|
60
40
|
|
|
61
|
-
|
|
41
|
+
See the [API Reference](../../docs/api-reference.md) for the full list.
|
|
62
42
|
|
|
63
|
-
|
|
43
|
+
## testTrail
|
|
64
44
|
|
|
65
|
-
|
|
45
|
+
For edge cases that do not belong in agent-facing examples:
|
|
66
46
|
|
|
67
47
|
```typescript
|
|
68
48
|
import { testTrail } from '@ontrails/testing';
|
|
@@ -71,151 +51,59 @@ import { ValidationError, NotFoundError } from '@ontrails/core';
|
|
|
71
51
|
testTrail(showTrail, [
|
|
72
52
|
{ description: 'empty name', input: { name: '' }, expectOk: true },
|
|
73
53
|
{ description: 'missing name', input: {}, expectErr: ValidationError },
|
|
74
|
-
{
|
|
75
|
-
description: 'exact match',
|
|
76
|
-
input: { name: 'Alpha' },
|
|
77
|
-
expectValue: { name: 'Alpha', type: 'concept' },
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
description: 'not found',
|
|
81
|
-
input: { name: 'missing' },
|
|
82
|
-
expectErr: NotFoundError,
|
|
83
|
-
expectErrMessage: 'not found',
|
|
84
|
-
},
|
|
54
|
+
{ description: 'not found', input: { name: 'missing' }, expectErr: NotFoundError },
|
|
85
55
|
]);
|
|
86
56
|
```
|
|
87
57
|
|
|
88
|
-
|
|
58
|
+
## Testing composition (trails with follow)
|
|
89
59
|
|
|
90
|
-
|
|
60
|
+
`testTrail` works the same for trails with `follow` -- it exercises the follow graph:
|
|
91
61
|
|
|
92
62
|
```typescript
|
|
93
|
-
import {
|
|
63
|
+
import { testTrail } from '@ontrails/testing';
|
|
94
64
|
|
|
95
|
-
|
|
96
|
-
{ description: '
|
|
97
|
-
{ description: '
|
|
65
|
+
testTrail(onboardTrail, [
|
|
66
|
+
{ description: 'happy path', input: { name: 'Delta', type: 'tool' }, expectOk: true },
|
|
67
|
+
{ description: 'add fails', input: { name: 'Alpha' }, expectErr: AlreadyExistsError },
|
|
98
68
|
]);
|
|
99
69
|
```
|
|
100
70
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
### `testContracts(app, ctx?)`
|
|
104
|
-
|
|
105
|
-
Catches implementation-schema drift. Runs every example through the implementation, then validates the result against the trail's `output` schema. Reports detailed Zod errors on mismatch.
|
|
71
|
+
When you need to isolate a composite trail and stub out its dependencies, use `createFollowContext`:
|
|
106
72
|
|
|
107
73
|
```typescript
|
|
108
|
-
import {
|
|
109
|
-
|
|
110
|
-
testContracts(app);
|
|
111
|
-
// Fails if any implementation returns data that doesn't match its declared output schema
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### `testDetours(app)`
|
|
115
|
-
|
|
116
|
-
Structural validation of detour declarations. Verifies every detour target trail exists in the topo. No implementation execution needed.
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
import { testDetours } from '@ontrails/testing';
|
|
120
|
-
|
|
121
|
-
testDetours(app);
|
|
122
|
-
// Fails: Trail "entity.show" has detour target "entity.search" which does not exist in the topo
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### Progressive Assertion
|
|
74
|
+
import { createFollowContext, createTestContext } from '@ontrails/testing';
|
|
75
|
+
import { Result } from '@ontrails/core';
|
|
126
76
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
{
|
|
133
|
-
name: 'Found',
|
|
134
|
-
input: { name: 'Alpha' },
|
|
135
|
-
expected: { name: 'Alpha', type: 'concept' },
|
|
136
|
-
}
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
Asserts `result.isOk()` and `result.value` deep-equals `expected`.
|
|
140
|
-
|
|
141
|
-
**Schema-only match** -- example has no `expected` and no `error`:
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
{ name: 'Returns something valid', input: { name: 'Alpha' } }
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
Asserts `result.isOk()` and validates against the trail's output schema.
|
|
148
|
-
|
|
149
|
-
**Error match** -- example has `error`:
|
|
150
|
-
|
|
151
|
-
```typescript
|
|
152
|
-
{ name: 'Not found', input: { name: 'missing' }, error: 'NotFoundError' }
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
Asserts `result.isErr()` and `instanceof` check.
|
|
156
|
-
|
|
157
|
-
### Test Context and Mocks
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
import { createTestContext, createTestLogger } from '@ontrails/testing';
|
|
161
|
-
|
|
162
|
-
// TrailContext with sensible test defaults
|
|
163
|
-
const ctx = createTestContext({
|
|
164
|
-
requestId: 'test-001',
|
|
165
|
-
env: { TRAILS_ENV: 'test' },
|
|
77
|
+
const follow = createFollowContext({
|
|
78
|
+
responses: {
|
|
79
|
+
'entity.add': Result.ok({ id: '1', name: 'Delta', type: 'tool' }),
|
|
80
|
+
'search': Result.ok({ results: [] }),
|
|
81
|
+
},
|
|
166
82
|
});
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const logger = createTestLogger();
|
|
170
|
-
logger.info('hello');
|
|
171
|
-
logger.entries; // All captured records
|
|
172
|
-
logger.assertLogged('info', 'hello'); // Passes if any entry matches
|
|
173
|
-
logger.clear(); // Reset
|
|
83
|
+
const ctx = { ...createTestContext(), follow };
|
|
84
|
+
const result = await onboardTrail.run({ name: 'Delta', type: 'tool' }, ctx);
|
|
174
85
|
```
|
|
175
86
|
|
|
176
|
-
|
|
87
|
+
Calls to unregistered trail IDs return `Result.err` with a descriptive message, so missing stubs fail loudly.
|
|
177
88
|
|
|
178
|
-
|
|
89
|
+
## Surface harnesses
|
|
179
90
|
|
|
180
91
|
```typescript
|
|
181
|
-
import { createCliHarness } from '@ontrails/testing';
|
|
182
|
-
|
|
183
|
-
const harness = createCliHarness({ app });
|
|
184
|
-
const result = await harness.run('entity show --name Alpha --output json');
|
|
92
|
+
import { createCliHarness, createMcpHarness } from '@ontrails/testing';
|
|
185
93
|
|
|
94
|
+
// CLI
|
|
95
|
+
const cli = createCliHarness({ app });
|
|
96
|
+
const result = await cli.run('entity show --name Alpha --output json');
|
|
186
97
|
expect(result.exitCode).toBe(0);
|
|
187
|
-
expect(result.json).toMatchObject({ name: 'Alpha' });
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
**MCP harness** -- invoke tools directly without transport:
|
|
191
|
-
|
|
192
|
-
```typescript
|
|
193
|
-
import { createMcpHarness } from '@ontrails/testing';
|
|
194
|
-
|
|
195
|
-
const harness = createMcpHarness({ app });
|
|
196
|
-
const result = await harness.callTool('myapp_entity_show', { name: 'Alpha' });
|
|
197
98
|
|
|
198
|
-
|
|
99
|
+
// MCP
|
|
100
|
+
const mcp = createMcpHarness({ app });
|
|
101
|
+
const tool = await mcp.callTool('myapp_entity_show', { name: 'Alpha' });
|
|
102
|
+
expect(tool.isError).toBe(false);
|
|
199
103
|
```
|
|
200
104
|
|
|
201
|
-
##
|
|
105
|
+
## Installation
|
|
202
106
|
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
testAll,
|
|
206
|
-
testExamples,
|
|
207
|
-
testTrail,
|
|
208
|
-
testHike,
|
|
209
|
-
testContracts,
|
|
210
|
-
testDetours,
|
|
211
|
-
createTestContext,
|
|
212
|
-
createTestLogger,
|
|
213
|
-
createCliHarness,
|
|
214
|
-
createMcpHarness,
|
|
215
|
-
} from '@ontrails/testing';
|
|
107
|
+
```bash
|
|
108
|
+
bun add -d @ontrails/testing
|
|
216
109
|
```
|
|
217
|
-
|
|
218
|
-
## Further Reading
|
|
219
|
-
|
|
220
|
-
- [Testing Guide](../../docs/testing.md)
|
|
221
|
-
- [Getting Started](../../docs/getting-started.md)
|
package/dist/context.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Test context factory for creating TrailContext instances suitable for testing.
|
|
3
3
|
*/
|
|
4
|
-
import type { TrailContext } from '@ontrails/core';
|
|
4
|
+
import type { FollowFn, TrailContext } from '@ontrails/core';
|
|
5
|
+
import { Result } from '@ontrails/core';
|
|
5
6
|
import type { TestTrailContextOptions } from './types.js';
|
|
6
7
|
/**
|
|
7
8
|
* Create a TrailContext with deterministic, test-friendly defaults.
|
|
@@ -11,6 +12,24 @@ import type { TestTrailContextOptions } from './types.js';
|
|
|
11
12
|
* - `signal`: a non-aborted AbortController signal
|
|
12
13
|
*/
|
|
13
14
|
export declare const createTestContext: (overrides?: TestTrailContextOptions) => TrailContext;
|
|
15
|
+
export interface CreateFollowContextOptions {
|
|
16
|
+
readonly responses?: Record<string, Result<unknown, Error>> | undefined;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create a mock `FollowFn` for testing composite trails.
|
|
20
|
+
*
|
|
21
|
+
* Returns preconfigured `Result` values keyed by trail ID. Calls to
|
|
22
|
+
* unregistered IDs return `Result.err` with a descriptive message.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const follow = createFollowContext({
|
|
27
|
+
* responses: { 'entity.add': Result.ok({ id: '1', name: 'Alpha' }) },
|
|
28
|
+
* });
|
|
29
|
+
* const ctx = { ...createTestContext(), follow };
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare const createFollowContext: (options?: CreateFollowContextOptions) => FollowFn;
|
|
14
33
|
/**
|
|
15
34
|
* Merge a Partial<TrailContext> into a test context.
|
|
16
35
|
* Used internally when the public API accepts Partial<TrailContext>.
|
package/dist/context.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAGxC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAM1D;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAC5B,YAAY,uBAAuB,KAClC,YAMD,CAAC;AAMH,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC;CACzE;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,GAC9B,UAAU,0BAA0B,KACnC,QAcF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,OAAO,CAAC,YAAY,CAAC,KAAG,YAO9D,CAAC"}
|
package/dist/context.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Test context factory for creating TrailContext instances suitable for testing.
|
|
3
3
|
*/
|
|
4
|
+
import { Result } from '@ontrails/core';
|
|
4
5
|
import { createTestLogger } from './logger.js';
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// createTestContext
|
|
@@ -19,6 +20,30 @@ export const createTestContext = (overrides) => ({
|
|
|
19
20
|
signal: overrides?.signal ?? new AbortController().signal,
|
|
20
21
|
workspaceRoot: overrides?.cwd ?? process.cwd(),
|
|
21
22
|
});
|
|
23
|
+
/**
|
|
24
|
+
* Create a mock `FollowFn` for testing composite trails.
|
|
25
|
+
*
|
|
26
|
+
* Returns preconfigured `Result` values keyed by trail ID. Calls to
|
|
27
|
+
* unregistered IDs return `Result.err` with a descriptive message.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const follow = createFollowContext({
|
|
32
|
+
* responses: { 'entity.add': Result.ok({ id: '1', name: 'Alpha' }) },
|
|
33
|
+
* });
|
|
34
|
+
* const ctx = { ...createTestContext(), follow };
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export const createFollowContext = (options) => {
|
|
38
|
+
const responses = options?.responses ?? {};
|
|
39
|
+
return (id, _input) => {
|
|
40
|
+
const response = responses[id];
|
|
41
|
+
if (response === undefined) {
|
|
42
|
+
return Promise.resolve(Result.err(new Error(`No mock response for follow("${id}")`)));
|
|
43
|
+
}
|
|
44
|
+
return Promise.resolve(response);
|
|
45
|
+
};
|
|
46
|
+
};
|
|
22
47
|
/**
|
|
23
48
|
* Merge a Partial<TrailContext> into a test context.
|
|
24
49
|
* Used internally when the public API accepts Partial<TrailContext>.
|
package/dist/context.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAExC,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG/C,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,SAAmC,EACrB,EAAE,CAAC,CAAC;IAClB,GAAG,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE;IAC7C,MAAM,EAAE,SAAS,EAAE,MAAM,IAAI,gBAAgB,EAAE;IAC/C,SAAS,EAAE,SAAS,EAAE,SAAS,IAAI,kBAAkB;IACrD,MAAM,EAAE,SAAS,EAAE,MAAM,IAAI,IAAI,eAAe,EAAE,CAAC,MAAM;IACzD,aAAa,EAAE,SAAS,EAAE,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE;CAC/C,CAAC,CAAC;AAUH;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,OAAoC,EAC1B,EAAE;IACZ,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,EAAE,CAAC;IAC3C,OAAO,CAAI,EAAU,EAAE,MAAe,EAA6B,EAAE;QACnE,MAAM,QAAQ,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC;QAC/B,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,OAAO,OAAO,CAAC,OAAO,CACpB,MAAM,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,gCAAgC,EAAE,IAAI,CAAC,CAG3D,CACF,CAAC;QACJ,CAAC;QACD,OAAO,OAAO,CAAC,OAAO,CAAC,QAA4B,CAAC,CAAC;IACvD,CAAC,CAAC;AACJ,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAA2B,EAAgB,EAAE;IAC5E,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,OAAO,iBAAiB,EAAE,CAAC;IAC7B,CAAC;IAED,MAAM,IAAI,GAAG,iBAAiB,EAAE,CAAC;IACjC,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,GAAG,EAAE,CAAC;AAC7B,CAAC,CAAC"}
|
package/dist/contracts.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { Topo, TrailContext } from '@ontrails/core';
|
|
9
9
|
/**
|
|
10
|
-
* Verify that every trail
|
|
10
|
+
* Verify that every trail implementation output matches its declared
|
|
11
11
|
* output schema. Catches implementation-schema drift.
|
|
12
12
|
*
|
|
13
13
|
* Trails without output schemas or examples are skipped.
|
package/dist/contracts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAuB,YAAY,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAuB,YAAY,EAAE,MAAM,gBAAgB,CAAC;AA0C9E;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GACxB,KAAK,IAAI,EACT,eAAe,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC,KACnE,IAoCF,CAAC"}
|
package/dist/contracts.js
CHANGED
|
@@ -12,6 +12,14 @@ import { mergeTestContext } from './context.js';
|
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
// Helpers
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
|
+
/** Check if a trail requires follow() but the context doesn't provide it. */
|
|
16
|
+
const needsFollowContext = (t, resolveCtx) => {
|
|
17
|
+
const spec = t;
|
|
18
|
+
if (!spec.follow || spec.follow.length === 0) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return !resolveCtx()?.follow;
|
|
22
|
+
};
|
|
15
23
|
const validateOutputSchema = (outputSchema, value, trailId, exampleName) => {
|
|
16
24
|
const parsed = outputSchema.safeParse(value);
|
|
17
25
|
if (!parsed.success) {
|
|
@@ -23,30 +31,32 @@ const validateOutputSchema = (outputSchema, value, trailId, exampleName) => {
|
|
|
23
31
|
// testContracts
|
|
24
32
|
// ---------------------------------------------------------------------------
|
|
25
33
|
/**
|
|
26
|
-
* Verify that every trail
|
|
34
|
+
* Verify that every trail implementation output matches its declared
|
|
27
35
|
* output schema. Catches implementation-schema drift.
|
|
28
36
|
*
|
|
29
37
|
* Trails without output schemas or examples are skipped.
|
|
30
38
|
*/
|
|
31
39
|
export const testContracts = (app, ctxOrFactory) => {
|
|
32
40
|
const resolveCtx = typeof ctxOrFactory === 'function' ? ctxOrFactory : () => ctxOrFactory;
|
|
33
|
-
const
|
|
41
|
+
const allEntries = app.list();
|
|
34
42
|
describe('contracts', () => {
|
|
35
|
-
describe.each(
|
|
36
|
-
const t = trailDef;
|
|
43
|
+
describe.each(allEntries)('$id', (t) => {
|
|
37
44
|
if (t.output === undefined) {
|
|
38
45
|
return;
|
|
39
46
|
}
|
|
40
47
|
if (t.examples === undefined || t.examples.length === 0) {
|
|
41
48
|
return;
|
|
42
49
|
}
|
|
50
|
+
if (needsFollowContext(t, resolveCtx)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
43
53
|
const { examples, output: outputSchema } = t;
|
|
44
54
|
const successExamples = examples.filter((e) => e.error === undefined);
|
|
45
55
|
test.each(successExamples)('contract: $name', async (example) => {
|
|
46
56
|
const testCtx = mergeTestContext(resolveCtx());
|
|
47
57
|
const validated = validateInput(t.input, example.input);
|
|
48
58
|
const validatedInput = expectOk(validated);
|
|
49
|
-
const result = await t.
|
|
59
|
+
const result = await t.run(validatedInput, testCtx);
|
|
50
60
|
const resultValue = expectOk(result);
|
|
51
61
|
validateOutputSchema(outputSchema, resultValue, t.id, example.name);
|
|
52
62
|
});
|
package/dist/contracts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contracts.js","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAG1C,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGhE,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEhD,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,MAAM,oBAAoB,GAAG,CAC3B,YAAuB,EACvB,KAAc,EACd,OAAe,EACf,WAAmB,EACb,EAAE;IACR,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC7C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACpD,MAAM,IAAI,KAAK,CACb,sCAAsC,OAAO,eAAe,WAAW,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,sBAAsB,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAC7K,CAAC;IACJ,CAAC;AACH,CAAC,CAAC;AAEF,8EAA8E;AAC9E,gBAAgB;AAChB,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,GAAS,EACT,YAAoE,EAC9D,EAAE;IACR,MAAM,UAAU,GACd,OAAO,YAAY,KAAK,UAAU,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC;IACzE,MAAM,
|
|
1
|
+
{"version":3,"file":"contracts.js","sourceRoot":"","sources":["../src/contracts.ts"],"names":[],"mappings":"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"}
|
package/dist/examples.d.ts
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Iterates every trail in the app's topo. For each trail with examples,
|
|
5
5
|
* generates describe/test blocks using bun:test. Progressive assertion
|
|
6
|
-
* determines which check to run per example. For
|
|
6
|
+
* determines which check to run per example. For trails with `follow`
|
|
7
7
|
* declarations, checks that every declared follow was called at least once.
|
|
8
8
|
*/
|
|
9
9
|
import type { Topo, TrailContext } from '@ontrails/core';
|
|
10
10
|
/**
|
|
11
11
|
* Generate describe/test blocks for every trail example in the app.
|
|
12
12
|
*
|
|
13
|
-
* For
|
|
13
|
+
* For trails with `follow` declarations and examples, also verifies that
|
|
14
14
|
* every declared follow ID was called at least once across all examples.
|
|
15
15
|
*
|
|
16
16
|
* One line in your test file:
|
package/dist/examples.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"examples.d.ts","sourceRoot":"","sources":["../src/examples.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"examples.d.ts","sourceRoot":"","sources":["../src/examples.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAEV,IAAI,EAGJ,YAAY,EACb,MAAM,gBAAgB,CAAC;AAiMxB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,GACvB,KAAK,IAAI,EACT,eAAe,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC,KACnE,IAqDF,CAAC"}
|