@ontrails/testing 0.2.0
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 +807 -0
- package/README.md +157 -0
- package/package.json +57 -0
- package/src/all-established.ts +168 -0
- package/src/all.ts +94 -0
- package/src/assertions.ts +361 -0
- package/src/cli.ts +6 -0
- package/src/composes.ts +433 -0
- package/src/context.ts +228 -0
- package/src/contracts.ts +109 -0
- package/src/detours.ts +181 -0
- package/src/effective-examples.ts +408 -0
- package/src/errors.ts +47 -0
- package/src/examples.ts +439 -0
- package/src/harness-cli.ts +335 -0
- package/src/harness-http.ts +341 -0
- package/src/harness-mcp.ts +98 -0
- package/src/http.ts +10 -0
- package/src/index.ts +48 -0
- package/src/logger.ts +127 -0
- 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 +116 -0
- package/src/types.ts +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# @ontrails/testing
|
|
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 contract coverage.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { testAll } from '@ontrails/testing';
|
|
9
|
+
import { graph } from '../app';
|
|
10
|
+
|
|
11
|
+
testAll(graph);
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
That single call covers example execution, contract validation, detour checks, and topo validation. For most apps, this is all you need.
|
|
15
|
+
|
|
16
|
+
If you want finer control:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { testExamples, testContracts, testDetours } from '@ontrails/testing';
|
|
20
|
+
|
|
21
|
+
testExamples(graph); // Run every trail's examples as tests
|
|
22
|
+
testContracts(graph); // Validate outputs against declared schemas
|
|
23
|
+
testDetours(graph); // Validate detour constructor, recover, and ordering semantics
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## API
|
|
27
|
+
|
|
28
|
+
| Export | What it does |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `testAll(topo, ctx?)` | Single-line contract 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 composition chains |
|
|
33
|
+
| `testContracts(topo, ctx?)` | Validate output against declared schemas |
|
|
34
|
+
| `testDetours(topo)` | Validate detour constructor, recover, and shadowing semantics |
|
|
35
|
+
| `createComposeContext(options?)` | Mock `ComposeFn` 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
|
+
|
|
39
|
+
See the [API Reference](../../docs/api-reference.md) for the full list.
|
|
40
|
+
|
|
41
|
+
## testTrail
|
|
42
|
+
|
|
43
|
+
For edge cases that do not belong in agent-facing examples:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { testTrail } from '@ontrails/testing';
|
|
47
|
+
import { ValidationError, NotFoundError } from '@ontrails/core';
|
|
48
|
+
|
|
49
|
+
testTrail(showTrail, [
|
|
50
|
+
{ description: 'empty name', input: { name: '' }, expectOk: true },
|
|
51
|
+
{ description: 'missing name', input: {}, expectErr: ValidationError },
|
|
52
|
+
{ description: 'not found', input: { name: 'missing' }, expectErr: NotFoundError },
|
|
53
|
+
]);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Testing composition (trails with composes)
|
|
57
|
+
|
|
58
|
+
`testTrail` works the same for trails with `composes` -- it exercises the composition graph:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { testTrail } from '@ontrails/testing';
|
|
62
|
+
|
|
63
|
+
testTrail(onboardTrail, [
|
|
64
|
+
{ description: 'happy path', input: { name: 'Delta', type: 'tool' }, expectOk: true },
|
|
65
|
+
{ description: 'add fails', input: { name: 'Alpha' }, expectErr: AlreadyExistsError },
|
|
66
|
+
]);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
When you need to isolate a composite trail and stub out its dependencies, use `createComposeContext`:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { createComposeContext, testTrail } from '@ontrails/testing';
|
|
73
|
+
import { Result } from '@ontrails/core';
|
|
74
|
+
|
|
75
|
+
const compose = createComposeContext({
|
|
76
|
+
responses: {
|
|
77
|
+
'entity.add': Result.ok({ id: '1', name: 'Delta', type: 'tool' }),
|
|
78
|
+
'search': Result.ok({ results: [] }),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
testTrail(onboardTrail, [
|
|
82
|
+
{
|
|
83
|
+
description: 'uses mocked composes',
|
|
84
|
+
input: { name: 'Delta', type: 'tool' },
|
|
85
|
+
expectOk: true,
|
|
86
|
+
},
|
|
87
|
+
], { compose });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Calls to unregistered trail IDs return `Result.err` with a descriptive message, so missing stubs fail loudly.
|
|
91
|
+
|
|
92
|
+
## Surface Harnesses
|
|
93
|
+
|
|
94
|
+
Surface harnesses live on explicit subpaths so renders that only import contract helpers from `@ontrails/testing` do not need CLI, MCP, or HTTP peers. Install the peer package for the subpath you use:
|
|
95
|
+
|
|
96
|
+
- `@ontrails/testing/cli` requires `@ontrails/cli`
|
|
97
|
+
- `@ontrails/testing/mcp` requires `@ontrails/mcp`
|
|
98
|
+
- `@ontrails/testing/http` requires `@ontrails/http`
|
|
99
|
+
- `@ontrails/testing/established` and `@ontrails/testing/surface-parity`
|
|
100
|
+
require all three shipped surface peers
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { createCliHarness } from '@ontrails/testing/cli';
|
|
104
|
+
import { createHttpHarness } from '@ontrails/testing/http';
|
|
105
|
+
import { createMcpHarness } from '@ontrails/testing/mcp';
|
|
106
|
+
|
|
107
|
+
// CLI
|
|
108
|
+
const cli = createCliHarness({ graph });
|
|
109
|
+
const result = await cli.run('entity show --name Alpha --output json');
|
|
110
|
+
expect(result.exitCode).toBe(0);
|
|
111
|
+
|
|
112
|
+
// MCP
|
|
113
|
+
const mcp = createMcpHarness({ graph });
|
|
114
|
+
const tool = await mcp.callTool('myapp_entity_show', { name: 'Alpha' });
|
|
115
|
+
expect(tool.isError).toBe(false);
|
|
116
|
+
|
|
117
|
+
// HTTP
|
|
118
|
+
const http = createHttpHarness({ graph });
|
|
119
|
+
const response = await http.get('/entity/show', { name: 'Alpha' });
|
|
120
|
+
expect(response.status).toBe(200);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Use `testAllEstablished()` from `@ontrails/testing/established` when an established app should run the root contract suite and validate CLI, MCP, and HTTP renderings in one call.
|
|
124
|
+
|
|
125
|
+
## Surface Parity
|
|
126
|
+
|
|
127
|
+
`testSurfaceParity()` is a focused gate for established apps that want to prove their examples behave the same through every shipped surface.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { testSurfaceParity } from '@ontrails/testing/surface-parity';
|
|
131
|
+
import { graph } from '../app';
|
|
132
|
+
|
|
133
|
+
const createDeterministicTestDb = () => ({});
|
|
134
|
+
|
|
135
|
+
testSurfaceParity(graph, {
|
|
136
|
+
createResources: () => ({
|
|
137
|
+
'db.main': createDeterministicTestDb(),
|
|
138
|
+
}),
|
|
139
|
+
exclusions: [
|
|
140
|
+
{
|
|
141
|
+
example: 'Creates generated data',
|
|
142
|
+
reason: 'output includes generated IDs that intentionally differ per surface run',
|
|
143
|
+
trailId: 'entity.add',
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The helper compares normalized success payloads and normalized TrailsError category/code pairs. CLI command names, MCP tool names, HTTP method/path values, transport envelopes, activation consumers, internal trails, and planned WebSocket work stay outside the equality check. Use `createResources` when examples need deterministic fixtures for each surface invocation, and use exclusions for intentional semantic differences that should not be silently skipped.
|
|
150
|
+
|
|
151
|
+
## Installation
|
|
152
|
+
|
|
153
|
+
These commands target stable `0.2.0`. Run them after that version is published to npm.
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
bun add --exact -d @ontrails/testing@0.2.0
|
|
157
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ontrails/testing",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/outfitter-dev/trails.git",
|
|
7
|
+
"directory": "packages/testing"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src/**/*.ts",
|
|
11
|
+
"!src/**/__tests__/**",
|
|
12
|
+
"!src/**/*.test.ts",
|
|
13
|
+
"!src/**/*.test-d.ts",
|
|
14
|
+
"README.md",
|
|
15
|
+
"CHANGELOG.md"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./src/index.ts",
|
|
20
|
+
"./cli": "./src/cli.ts",
|
|
21
|
+
"./established": "./src/all-established.ts",
|
|
22
|
+
"./http": "./src/http.ts",
|
|
23
|
+
"./mcp": "./src/mcp.ts",
|
|
24
|
+
"./surface-parity": "./src/surface-parity.ts",
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -b",
|
|
29
|
+
"test": "bun test",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"lint": "oxlint ./src",
|
|
32
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@ontrails/drizzle": "^0.2.0",
|
|
36
|
+
"@ontrails/store": "^0.2.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@ontrails/cli": "^0.2.0",
|
|
40
|
+
"@ontrails/core": "^0.2.0",
|
|
41
|
+
"@ontrails/http": "^0.2.0",
|
|
42
|
+
"@ontrails/mcp": "^0.2.0",
|
|
43
|
+
"@ontrails/observability": "^0.2.0",
|
|
44
|
+
"zod": "^4.3.5"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"@ontrails/cli": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"@ontrails/http": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@ontrails/mcp": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* testAllEstablished - contract suite plus shipped surface rendering checks.
|
|
3
|
+
*
|
|
4
|
+
* This helper intentionally lives behind a surface subpath so root
|
|
5
|
+
* `@ontrails/testing` imports do not pull CLI, MCP, or HTTP peers into
|
|
6
|
+
* contract-only consumers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from 'bun:test';
|
|
10
|
+
|
|
11
|
+
import type { Topo, TrailContext } from '@ontrails/core';
|
|
12
|
+
import { validateEstablishedTopo } from '@ontrails/core';
|
|
13
|
+
|
|
14
|
+
import { registerContractSuite } from './all.js';
|
|
15
|
+
import type { TestExecutionOptions } from './context.js';
|
|
16
|
+
import { createCliHarness } from './harness-cli.js';
|
|
17
|
+
import type { CliHarnessOptions } from './harness-cli.js';
|
|
18
|
+
import { createHttpHarness } from './harness-http.js';
|
|
19
|
+
import type { HttpHarnessOptions } from './harness-http.js';
|
|
20
|
+
import { createMcpHarness } from './harness-mcp.js';
|
|
21
|
+
import type { McpHarnessOptions } from './harness-mcp.js';
|
|
22
|
+
|
|
23
|
+
export interface TestAllEstablishedOptions {
|
|
24
|
+
readonly cli?: Omit<CliHarnessOptions, 'graph'> | undefined;
|
|
25
|
+
readonly createPermit?:
|
|
26
|
+
| ((trail: {
|
|
27
|
+
readonly permit?:
|
|
28
|
+
| { readonly scopes: readonly string[] }
|
|
29
|
+
| 'public'
|
|
30
|
+
| undefined;
|
|
31
|
+
}) =>
|
|
32
|
+
| {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly scopes: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
| undefined)
|
|
37
|
+
| undefined;
|
|
38
|
+
readonly ctx?: Partial<TrailContext> | undefined;
|
|
39
|
+
readonly http?: Omit<HttpHarnessOptions, 'graph'> | undefined;
|
|
40
|
+
readonly mcp?: Omit<McpHarnessOptions, 'graph'> | undefined;
|
|
41
|
+
readonly resources?: Record<string, unknown> | undefined;
|
|
42
|
+
readonly strictPermits?: boolean | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type EstablishedInput =
|
|
46
|
+
| Partial<TrailContext>
|
|
47
|
+
| TestAllEstablishedOptions
|
|
48
|
+
| (() => Partial<TrailContext> | TestAllEstablishedOptions);
|
|
49
|
+
|
|
50
|
+
const isEstablishedOptions = (
|
|
51
|
+
input: Partial<TrailContext> | TestAllEstablishedOptions | undefined
|
|
52
|
+
): input is TestAllEstablishedOptions =>
|
|
53
|
+
input !== undefined &&
|
|
54
|
+
(Object.hasOwn(input, 'cli') ||
|
|
55
|
+
Object.hasOwn(input, 'createPermit') ||
|
|
56
|
+
Object.hasOwn(input, 'ctx') ||
|
|
57
|
+
Object.hasOwn(input, 'http') ||
|
|
58
|
+
Object.hasOwn(input, 'mcp') ||
|
|
59
|
+
Object.hasOwn(input, 'resources') ||
|
|
60
|
+
Object.hasOwn(input, 'strictPermits'));
|
|
61
|
+
|
|
62
|
+
const normalizeEstablishedOptions = (
|
|
63
|
+
input?: Partial<TrailContext> | TestAllEstablishedOptions
|
|
64
|
+
): TestAllEstablishedOptions =>
|
|
65
|
+
isEstablishedOptions(input) ? input : { ctx: input };
|
|
66
|
+
|
|
67
|
+
const toExecutionOptions = (
|
|
68
|
+
options: TestAllEstablishedOptions
|
|
69
|
+
): TestExecutionOptions => ({
|
|
70
|
+
...(options.createPermit === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: { createPermit: options.createPermit }),
|
|
73
|
+
...(options.ctx === undefined ? {} : { ctx: options.ctx }),
|
|
74
|
+
...(options.resources === undefined ? {} : { resources: options.resources }),
|
|
75
|
+
...(options.strictPermits === undefined
|
|
76
|
+
? {}
|
|
77
|
+
: { strictPermits: options.strictPermits }),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const toCliHarnessOptions = (
|
|
81
|
+
topo: Topo,
|
|
82
|
+
options: TestAllEstablishedOptions
|
|
83
|
+
) => {
|
|
84
|
+
const cliOptions = {
|
|
85
|
+
graph: topo,
|
|
86
|
+
...options.cli,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (options.ctx !== undefined) {
|
|
90
|
+
cliOptions.ctx = options.ctx;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return cliOptions;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const toMcpHarnessOptions = (
|
|
97
|
+
topo: Topo,
|
|
98
|
+
options: TestAllEstablishedOptions
|
|
99
|
+
) => ({
|
|
100
|
+
graph: topo,
|
|
101
|
+
...options.mcp,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const toHttpHarnessOptions = (
|
|
105
|
+
topo: Topo,
|
|
106
|
+
options: TestAllEstablishedOptions
|
|
107
|
+
) => {
|
|
108
|
+
const httpOptions = {
|
|
109
|
+
graph: topo,
|
|
110
|
+
...options.http,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (options.ctx !== undefined) {
|
|
114
|
+
httpOptions.ctx = options.ctx;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return httpOptions;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const registerEstablishedSurfaceSuite = (
|
|
121
|
+
topo: Topo,
|
|
122
|
+
resolveInput: () =>
|
|
123
|
+
| Partial<TrailContext>
|
|
124
|
+
| TestAllEstablishedOptions
|
|
125
|
+
| undefined
|
|
126
|
+
): void => {
|
|
127
|
+
describe('surfaces', () => {
|
|
128
|
+
test('CLI rendering validates established topo', () => {
|
|
129
|
+
const options = normalizeEstablishedOptions(resolveInput());
|
|
130
|
+
expect(() =>
|
|
131
|
+
createCliHarness(toCliHarnessOptions(topo, options))
|
|
132
|
+
).not.toThrow();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('MCP rendering validates established topo', () => {
|
|
136
|
+
const options = normalizeEstablishedOptions(resolveInput());
|
|
137
|
+
expect(() =>
|
|
138
|
+
createMcpHarness(toMcpHarnessOptions(topo, options))
|
|
139
|
+
).not.toThrow();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('HTTP rendering validates established topo', () => {
|
|
143
|
+
const options = normalizeEstablishedOptions(resolveInput());
|
|
144
|
+
expect(() =>
|
|
145
|
+
createHttpHarness(toHttpHarnessOptions(topo, options))
|
|
146
|
+
).not.toThrow();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const testAllEstablished = (
|
|
152
|
+
topo: Topo,
|
|
153
|
+
optionsOrFactory?: EstablishedInput
|
|
154
|
+
): void => {
|
|
155
|
+
const resolveInput =
|
|
156
|
+
typeof optionsOrFactory === 'function'
|
|
157
|
+
? optionsOrFactory
|
|
158
|
+
: () => optionsOrFactory;
|
|
159
|
+
|
|
160
|
+
registerContractSuite(
|
|
161
|
+
topo,
|
|
162
|
+
() => toExecutionOptions(normalizeEstablishedOptions(resolveInput())),
|
|
163
|
+
validateEstablishedTopo
|
|
164
|
+
);
|
|
165
|
+
registerEstablishedSurfaceSuite(topo, resolveInput);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export type { TestAllInput } from './all.js';
|
package/src/all.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* testAll — single-line contract suite for any Topo.
|
|
3
|
+
*
|
|
4
|
+
* Wraps topo validation, example execution, contract checks, and detour
|
|
5
|
+
* contract validation into one describe block.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from 'bun:test';
|
|
9
|
+
|
|
10
|
+
import type { Topo, TrailContext } from '@ontrails/core';
|
|
11
|
+
import { validateTopo } from '@ontrails/core';
|
|
12
|
+
|
|
13
|
+
import { testContracts } from './contracts.js';
|
|
14
|
+
import type { TestExecutionOptions } from './context.js';
|
|
15
|
+
import { testDetours } from './detours.js';
|
|
16
|
+
import { testExamples } from './examples.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Run the full contract test suite for a Topo.
|
|
20
|
+
*
|
|
21
|
+
* Generates a `contract` describe block containing:
|
|
22
|
+
* - Structural validation via `validateTopo`
|
|
23
|
+
* - Example execution via `testExamples`
|
|
24
|
+
* - Output contract checks via `testContracts`
|
|
25
|
+
* - Detour contract validation via `testDetours`
|
|
26
|
+
*
|
|
27
|
+
* Accepts either a static context or a factory function that produces a
|
|
28
|
+
* fresh context per test (useful when the context contains mutable state
|
|
29
|
+
* like an in-memory store).
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { testAll } from '@ontrails/testing';
|
|
34
|
+
* import { graph } from '../src/app.js';
|
|
35
|
+
*
|
|
36
|
+
* testAll(graph);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export type TestAllInput =
|
|
40
|
+
| Partial<TrailContext>
|
|
41
|
+
| TestExecutionOptions
|
|
42
|
+
| (() => Partial<TrailContext> | TestExecutionOptions);
|
|
43
|
+
|
|
44
|
+
const formatValidationFailure = (error: Error): string => {
|
|
45
|
+
const issues = (
|
|
46
|
+
error as {
|
|
47
|
+
context?: { issues?: readonly Record<string, unknown>[] };
|
|
48
|
+
}
|
|
49
|
+
).context?.issues;
|
|
50
|
+
|
|
51
|
+
if (issues === undefined || issues.length === 0) {
|
|
52
|
+
return error.message;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const details = issues.map((issue) => {
|
|
56
|
+
const id = typeof issue['id'] === 'string' ? issue['id'] : undefined;
|
|
57
|
+
const message =
|
|
58
|
+
typeof issue['message'] === 'string' ? issue['message'] : undefined;
|
|
59
|
+
const rule = typeof issue['rule'] === 'string' ? issue['rule'] : undefined;
|
|
60
|
+
|
|
61
|
+
return [rule, id, message].filter(Boolean).join(': ');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return [error.message, ...details].join('\n');
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const assertValidTopo = (result: ReturnType<typeof validateTopo>): void => {
|
|
68
|
+
if (result.isErr()) {
|
|
69
|
+
throw new Error(formatValidationFailure(result.error));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const registerContractSuite = (
|
|
74
|
+
topo: Topo,
|
|
75
|
+
ctxOrFactory: TestAllInput | undefined,
|
|
76
|
+
validate: (topo: Topo) => ReturnType<typeof validateTopo>
|
|
77
|
+
): void => {
|
|
78
|
+
describe('contract', () => {
|
|
79
|
+
test('topo validates', () => {
|
|
80
|
+
expect(() => assertValidTopo(validate(topo))).not.toThrow();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// oxlint-disable-next-line jest/require-hook -- these generate describe/test blocks, not setup code
|
|
84
|
+
testExamples(topo, ctxOrFactory);
|
|
85
|
+
// oxlint-disable-next-line jest/require-hook -- these generate describe/test blocks, not setup code
|
|
86
|
+
testContracts(topo, ctxOrFactory);
|
|
87
|
+
// oxlint-disable-next-line jest/require-hook -- these generate describe/test blocks, not setup code
|
|
88
|
+
testDetours(topo);
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const testAll = (topo: Topo, ctxOrFactory?: TestAllInput): void => {
|
|
93
|
+
registerContractSuite(topo, ctxOrFactory, validateTopo);
|
|
94
|
+
};
|