@ontrails/testing 1.0.0-beta.13 → 1.0.0-beta.15
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 +28 -0
- package/README.md +11 -11
- package/dist/all.d.ts +10 -5
- package/dist/all.d.ts.map +1 -1
- package/dist/all.js +78 -26
- package/dist/all.js.map +1 -1
- package/dist/assertions.d.ts +23 -0
- package/dist/assertions.d.ts.map +1 -1
- package/dist/assertions.js +154 -0
- package/dist/assertions.js.map +1 -1
- package/dist/context.d.ts +17 -16
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +31 -20
- package/dist/context.js.map +1 -1
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +9 -5
- package/dist/contracts.js.map +1 -1
- package/dist/crosses.d.ts +4 -4
- package/dist/crosses.d.ts.map +1 -1
- package/dist/crosses.js +49 -39
- package/dist/crosses.js.map +1 -1
- package/dist/detours.d.ts +5 -4
- package/dist/detours.d.ts.map +1 -1
- package/dist/detours.js +93 -14
- package/dist/detours.js.map +1 -1
- package/dist/effective-examples.d.ts +30 -0
- package/dist/effective-examples.d.ts.map +1 -0
- package/dist/effective-examples.js +227 -0
- package/dist/effective-examples.js.map +1 -0
- package/dist/examples.d.ts +1 -1
- package/dist/examples.d.ts.map +1 -1
- package/dist/examples.js +79 -41
- package/dist/examples.js.map +1 -1
- package/dist/harness-cli.d.ts +3 -3
- package/dist/harness-cli.d.ts.map +1 -1
- package/dist/harness-cli.js +25 -33
- package/dist/harness-cli.js.map +1 -1
- package/dist/harness-mcp.d.ts +3 -3
- package/dist/harness-mcp.d.ts.map +1 -1
- package/dist/harness-mcp.js +9 -8
- package/dist/harness-mcp.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/scenario.d.ts +37 -0
- package/dist/scenario.d.ts.map +1 -0
- package/dist/scenario.js +235 -0
- package/dist/scenario.js.map +1 -0
- package/dist/types.d.ts +38 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -5
- package/src/__tests__/all.test.ts +217 -29
- package/src/__tests__/context.test.ts +32 -12
- package/src/__tests__/contracts.test.ts +72 -18
- package/src/__tests__/crosses.test.ts +78 -78
- package/src/__tests__/detours.test.ts +176 -19
- package/src/__tests__/effective-examples.test.ts +203 -0
- package/src/__tests__/examples.test.ts +152 -50
- package/src/__tests__/harness-cli.test.ts +90 -0
- package/src/__tests__/harness-mcp.test.ts +37 -0
- package/src/__tests__/partial-match.test.ts +126 -0
- package/src/__tests__/scenario.test.ts +381 -0
- package/src/all.ts +149 -12
- package/src/assertions.ts +253 -0
- package/src/context.ts +64 -38
- package/src/contracts.ts +14 -8
- package/src/crosses.ts +93 -51
- package/src/detours.ts +155 -18
- package/src/effective-examples.ts +350 -0
- package/src/examples.ts +127 -59
- package/src/harness-cli.ts +33 -49
- package/src/harness-mcp.ts +9 -8
- package/src/index.ts +13 -3
- package/src/scenario.ts +370 -0
- package/src/types.ts +63 -5
- package/tsconfig.tests.json +10 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/follows.d.ts +0 -38
- package/dist/follows.d.ts.map +0 -1
- package/dist/follows.js +0 -212
- package/dist/follows.js.map +0 -1
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared types for @ontrails/testing.
|
|
3
3
|
*/
|
|
4
|
-
import type {
|
|
4
|
+
import type { DeriveCliCommandsOptions } from '@ontrails/cli';
|
|
5
|
+
import type { McpExtra, DeriveMcpToolsOptions } from '@ontrails/mcp';
|
|
6
|
+
import type { AnyTrail, Logger, Topo, TraceFn, TrailContext } from '@ontrails/core';
|
|
5
7
|
import type { LogLevel, LogRecord } from '@ontrails/logging';
|
|
6
8
|
/** A custom test scenario for a single trail. */
|
|
7
9
|
export interface TestScenario {
|
|
@@ -45,10 +47,12 @@ export interface TestTrailContextOptions {
|
|
|
45
47
|
readonly logger?: Logger | undefined;
|
|
46
48
|
readonly requestId?: string | undefined;
|
|
47
49
|
readonly abortSignal?: AbortSignal | undefined;
|
|
50
|
+
readonly trace?: TraceFn | undefined;
|
|
48
51
|
}
|
|
49
52
|
/** Options for creating a CLI harness. */
|
|
50
|
-
export interface CliHarnessOptions {
|
|
51
|
-
readonly
|
|
53
|
+
export interface CliHarnessOptions extends Omit<DeriveCliCommandsOptions, 'onResult' | 'presets' | 'resolveInput'> {
|
|
54
|
+
readonly ctx?: Partial<TrailContext> | undefined;
|
|
55
|
+
readonly graph: Topo;
|
|
52
56
|
}
|
|
53
57
|
/** A test harness for CLI commands. */
|
|
54
58
|
export interface CliHarness {
|
|
@@ -64,8 +68,9 @@ export interface CliHarnessResult {
|
|
|
64
68
|
readonly stdout: string;
|
|
65
69
|
}
|
|
66
70
|
/** Options for creating an MCP harness. */
|
|
67
|
-
export interface McpHarnessOptions {
|
|
68
|
-
readonly
|
|
71
|
+
export interface McpHarnessOptions extends DeriveMcpToolsOptions {
|
|
72
|
+
readonly extra?: Partial<McpExtra> | undefined;
|
|
73
|
+
readonly graph: Topo;
|
|
69
74
|
}
|
|
70
75
|
/** A test harness for MCP tools. */
|
|
71
76
|
export interface McpHarness {
|
|
@@ -77,4 +82,32 @@ export interface McpHarnessResult {
|
|
|
77
82
|
readonly content: unknown;
|
|
78
83
|
readonly isError: boolean;
|
|
79
84
|
}
|
|
85
|
+
export interface TestAllEstablishedOptions {
|
|
86
|
+
readonly cli?: Omit<CliHarnessOptions, 'graph'> | undefined;
|
|
87
|
+
readonly createPermit?: ((trail: {
|
|
88
|
+
readonly permit?: {
|
|
89
|
+
readonly scopes: readonly string[];
|
|
90
|
+
} | 'public' | undefined;
|
|
91
|
+
}) => {
|
|
92
|
+
readonly id: string;
|
|
93
|
+
readonly scopes: readonly string[];
|
|
94
|
+
} | undefined) | undefined;
|
|
95
|
+
readonly ctx?: Partial<TrailContext> | undefined;
|
|
96
|
+
readonly mcp?: Omit<McpHarnessOptions, 'graph'> | undefined;
|
|
97
|
+
readonly resources?: Record<string, unknown> | undefined;
|
|
98
|
+
readonly strictPermits?: boolean | undefined;
|
|
99
|
+
}
|
|
100
|
+
/** Marker for cross-step references in scenario inputs. */
|
|
101
|
+
export interface RefToken {
|
|
102
|
+
readonly __ref: true;
|
|
103
|
+
readonly path: string;
|
|
104
|
+
}
|
|
105
|
+
/** A single step in a scenario. */
|
|
106
|
+
export interface ScenarioStep {
|
|
107
|
+
readonly cross: AnyTrail;
|
|
108
|
+
readonly input: Record<string, unknown>;
|
|
109
|
+
readonly as?: string | undefined;
|
|
110
|
+
readonly expected?: unknown | undefined;
|
|
111
|
+
readonly expectedMatch?: unknown | undefined;
|
|
112
|
+
}
|
|
80
113
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ontrails/testing",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts",
|
|
@@ -13,11 +13,15 @@
|
|
|
13
13
|
"lint": "oxlint ./src",
|
|
14
14
|
"clean": "rm -rf dist *.tsbuildinfo"
|
|
15
15
|
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@ontrails/drizzle": "^1.0.0-beta.14",
|
|
18
|
+
"@ontrails/store": "^1.0.0-beta.14"
|
|
19
|
+
},
|
|
16
20
|
"peerDependencies": {
|
|
17
|
-
"@ontrails/cli": "^1.0.0-beta.
|
|
18
|
-
"@ontrails/core": "^1.0.0-beta.
|
|
19
|
-
"@ontrails/logging": "^1.0.0-beta.
|
|
20
|
-
"@ontrails/mcp": "^1.0.0-beta.
|
|
21
|
+
"@ontrails/cli": "^1.0.0-beta.14",
|
|
22
|
+
"@ontrails/core": "^1.0.0-beta.14",
|
|
23
|
+
"@ontrails/logging": "^1.0.0-beta.14",
|
|
24
|
+
"@ontrails/mcp": "^1.0.0-beta.14",
|
|
21
25
|
"zod": "^4.3.5"
|
|
22
26
|
}
|
|
23
27
|
}
|
|
@@ -1,66 +1,254 @@
|
|
|
1
|
-
import { describe } from 'bun:test';
|
|
1
|
+
import { afterAll, describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
2
4
|
|
|
3
|
-
import {
|
|
5
|
+
import { contour, Result, trail, topo } from '@ontrails/core';
|
|
6
|
+
import { connectDrizzle } from '@ontrails/drizzle';
|
|
4
7
|
import { z } from 'zod';
|
|
5
8
|
|
|
6
9
|
import { testAll } from '../all.js';
|
|
10
|
+
import { store as defineStore } from '@ontrails/store';
|
|
7
11
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
const requireContourExample = (
|
|
13
|
+
contourDef: { examples?: readonly Record<string, unknown>[] },
|
|
14
|
+
index: number
|
|
15
|
+
) => {
|
|
16
|
+
const example = contourDef.examples?.[index];
|
|
17
|
+
expect(example).toBeDefined();
|
|
18
|
+
if (!example) {
|
|
19
|
+
throw new Error(`Expected contour example at index ${index}`);
|
|
20
|
+
}
|
|
21
|
+
return example;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const dbDefinition = defineStore({
|
|
25
|
+
entities: {
|
|
26
|
+
fixtures: [
|
|
27
|
+
{
|
|
28
|
+
id: 'seed-1',
|
|
29
|
+
name: 'Alpha',
|
|
30
|
+
source: 'mock',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
generated: ['id'],
|
|
34
|
+
primaryKey: 'id',
|
|
35
|
+
schema: z.object({
|
|
36
|
+
id: z.string(),
|
|
37
|
+
name: z.string(),
|
|
38
|
+
source: z.string(),
|
|
39
|
+
}),
|
|
40
|
+
},
|
|
11
41
|
});
|
|
12
42
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
43
|
+
const createDbResource = (
|
|
44
|
+
seed?: readonly {
|
|
45
|
+
readonly id: string;
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly source: string;
|
|
48
|
+
}[]
|
|
49
|
+
) =>
|
|
50
|
+
connectDrizzle(dbDefinition, {
|
|
51
|
+
id: 'db.mock.all',
|
|
52
|
+
...(seed === undefined ? {} : { mockSeed: { entities: seed } }),
|
|
53
|
+
url: ':memory:',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const createOverrideStore = () => {
|
|
57
|
+
const { mock: mockFactory } = createDbResource([
|
|
58
|
+
{
|
|
59
|
+
id: 'seed-1',
|
|
60
|
+
name: 'Override',
|
|
61
|
+
source: 'override',
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
if (mockFactory === undefined) {
|
|
66
|
+
throw new Error('Expected drizzle test store to expose a mock factory');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const created = mockFactory();
|
|
70
|
+
if (created instanceof Promise) {
|
|
71
|
+
throw new TypeError(
|
|
72
|
+
'Expected drizzle test store mock to resolve synchronously'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return created;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const mockDbResource = createDbResource();
|
|
80
|
+
|
|
81
|
+
const mockedTrail = trail('resource.mocked.all', {
|
|
82
|
+
blaze: async (_input, ctx) => {
|
|
83
|
+
const entity = await mockDbResource.from(ctx).entities.get('seed-1');
|
|
84
|
+
if (entity === null) {
|
|
85
|
+
return Result.err(new Error('expected seeded entity to exist'));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Result.ok({ name: entity.name, source: entity.source });
|
|
89
|
+
},
|
|
90
|
+
description:
|
|
91
|
+
'Trail that uses a mocked connector-bound resource through testAll',
|
|
17
92
|
examples: [
|
|
18
93
|
{
|
|
19
|
-
expected: { source: 'mock' },
|
|
94
|
+
expected: { name: 'Alpha', source: 'mock' },
|
|
20
95
|
input: {},
|
|
21
|
-
name: 'Uses auto-resolved
|
|
96
|
+
name: 'Uses auto-resolved resource mock',
|
|
22
97
|
},
|
|
23
98
|
],
|
|
24
99
|
input: z.object({}),
|
|
25
|
-
output: z.object({ source: z.string() }),
|
|
26
|
-
|
|
100
|
+
output: z.object({ name: z.string(), source: z.string() }),
|
|
101
|
+
resources: [mockDbResource],
|
|
27
102
|
});
|
|
28
103
|
|
|
29
|
-
const overrideTrail = trail('
|
|
30
|
-
blaze: (_input, ctx) =>
|
|
31
|
-
|
|
104
|
+
const overrideTrail = trail('resource.override.all', {
|
|
105
|
+
blaze: async (_input, ctx) => {
|
|
106
|
+
const entity = await mockDbResource.from(ctx).entities.get('seed-1');
|
|
107
|
+
if (entity === null) {
|
|
108
|
+
return Result.err(new Error('expected overridden entity to exist'));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return Result.ok({ name: entity.name, source: entity.source });
|
|
112
|
+
},
|
|
32
113
|
description: 'Trail that prefers explicit overrides over mock factories',
|
|
33
114
|
examples: [
|
|
34
115
|
{
|
|
35
|
-
expected: { source: 'override' },
|
|
116
|
+
expected: { name: 'Override', source: 'override' },
|
|
36
117
|
input: {},
|
|
37
|
-
name: 'Explicit
|
|
118
|
+
name: 'Explicit resource override wins',
|
|
38
119
|
},
|
|
39
120
|
],
|
|
40
121
|
input: z.object({}),
|
|
41
|
-
output: z.object({ source: z.string() }),
|
|
42
|
-
|
|
122
|
+
output: z.object({ name: z.string(), source: z.string() }),
|
|
123
|
+
resources: [mockDbResource],
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const contourFixture = contour(
|
|
127
|
+
'allFixture',
|
|
128
|
+
{
|
|
129
|
+
id: z.string().uuid(),
|
|
130
|
+
name: z.string(),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
examples: [
|
|
134
|
+
{
|
|
135
|
+
id: 'f46b837e-6c8d-42ec-8539-536f4e6daf0e',
|
|
136
|
+
name: 'Contour-derived governance fixture',
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
identity: 'id',
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const contourDerivedBlaze = mock(() =>
|
|
144
|
+
Result.ok(requireContourExample(contourFixture, 0))
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const contourDerivedTrail = trail('contour.derived.all', {
|
|
148
|
+
blaze: () => contourDerivedBlaze(),
|
|
149
|
+
contours: [contourFixture],
|
|
150
|
+
description: 'Trail that relies on contour-derived fixtures inside testAll',
|
|
151
|
+
input: z.object({ id: contourFixture.shape.id }),
|
|
152
|
+
output: contourFixture,
|
|
43
153
|
});
|
|
44
154
|
|
|
45
|
-
|
|
155
|
+
const repoTempDir = (): string =>
|
|
156
|
+
join(
|
|
157
|
+
resolve(import.meta.dir, '../..'),
|
|
158
|
+
'.tmp-tests',
|
|
159
|
+
`test-all-established-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const runGeneratedGovernanceSuite = (
|
|
163
|
+
helperName: 'testAll' | 'testAllEstablished'
|
|
164
|
+
): { readonly exitCode: number; readonly output: string } => {
|
|
165
|
+
const dir = repoTempDir();
|
|
166
|
+
const testFile = join(dir, `${helperName}.test.ts`);
|
|
167
|
+
|
|
168
|
+
mkdirSync(dir, { recursive: true });
|
|
169
|
+
writeFileSync(
|
|
170
|
+
testFile,
|
|
171
|
+
`import { Result, trail, topo } from '@ontrails/core';
|
|
172
|
+
import { ${helperName} } from '../../src/index.ts';
|
|
173
|
+
import { z } from 'zod';
|
|
174
|
+
|
|
175
|
+
const draftTrail = trail('_draft.entity.prepare', {
|
|
176
|
+
blaze: async () => Result.ok({ ok: true }),
|
|
177
|
+
input: z.object({}),
|
|
178
|
+
output: z.object({ ok: z.boolean() }),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
${helperName}(topo('draft-topo', { draftTrail }));
|
|
182
|
+
`
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const proc = Bun.spawnSync({
|
|
187
|
+
cmd: ['bun', 'test', testFile, '--bail'],
|
|
188
|
+
cwd: resolve(import.meta.dir, '..', '..', '..'),
|
|
189
|
+
stderr: 'pipe',
|
|
190
|
+
stdout: 'pipe',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
exitCode: proc.exitCode,
|
|
195
|
+
output: `${proc.stdout.toString()}\n${proc.stderr.toString()}`,
|
|
196
|
+
};
|
|
197
|
+
} finally {
|
|
198
|
+
rmSync(dir, { force: true, recursive: true });
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
describe('testAll resource mocks', () => {
|
|
46
203
|
// eslint-disable-next-line jest/require-hook
|
|
47
204
|
testAll(
|
|
48
|
-
topo('test-all-
|
|
49
|
-
|
|
205
|
+
topo('test-all-resource-mock-app', {
|
|
206
|
+
mockDbResource,
|
|
50
207
|
mockedTrail,
|
|
51
208
|
} as Record<string, unknown>)
|
|
52
209
|
);
|
|
53
210
|
});
|
|
54
211
|
|
|
55
|
-
describe('testAll explicit
|
|
212
|
+
describe('testAll explicit resource overrides', () => {
|
|
56
213
|
// eslint-disable-next-line jest/require-hook
|
|
57
214
|
testAll(
|
|
58
|
-
topo('test-all-
|
|
59
|
-
|
|
215
|
+
topo('test-all-resource-override-app', {
|
|
216
|
+
mockDbResource,
|
|
60
217
|
overrideTrail,
|
|
61
218
|
} as Record<string, unknown>),
|
|
62
|
-
{
|
|
63
|
-
|
|
64
|
-
|
|
219
|
+
() => ({
|
|
220
|
+
resources: {
|
|
221
|
+
'db.mock.all': createOverrideStore(),
|
|
222
|
+
},
|
|
223
|
+
})
|
|
65
224
|
);
|
|
66
225
|
});
|
|
226
|
+
|
|
227
|
+
describe('testAll contour-derived fixtures', () => {
|
|
228
|
+
// eslint-disable-next-line jest/require-hook
|
|
229
|
+
testAll(
|
|
230
|
+
topo('test-all-contour-derived-app', {
|
|
231
|
+
contourDerivedTrail,
|
|
232
|
+
contourFixture,
|
|
233
|
+
} as Record<string, unknown>)
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
afterAll(() => {
|
|
237
|
+
expect(contourDerivedBlaze).toHaveBeenCalledTimes(2);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('testAllEstablished draft hygiene', () => {
|
|
242
|
+
test('keeps testAll draft-friendly for in-progress graphs', () => {
|
|
243
|
+
const result = runGeneratedGovernanceSuite('testAll');
|
|
244
|
+
|
|
245
|
+
expect(result.exitCode).toBe(0);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('fails draft-contaminated graphs under testAllEstablished', () => {
|
|
249
|
+
const result = runGeneratedGovernanceSuite('testAllEstablished');
|
|
250
|
+
|
|
251
|
+
expect(result.exitCode).toBe(1);
|
|
252
|
+
expect(result.output).toContain('_draft.entity.prepare');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import { Result,
|
|
3
|
+
import { Result, resource, topo } from '@ontrails/core';
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
createCrossContext,
|
|
7
7
|
createTestContext,
|
|
8
8
|
mergeTestContext,
|
|
9
|
-
|
|
9
|
+
createMockResources,
|
|
10
10
|
} from '../context.js';
|
|
11
11
|
import type { TestLogger } from '../types.js';
|
|
12
12
|
|
|
@@ -22,6 +22,7 @@ describe('createTestContext', () => {
|
|
|
22
22
|
expect(ctx.abortSignal).toBeDefined();
|
|
23
23
|
expect(ctx.abortSignal.aborted).toBe(false);
|
|
24
24
|
expect(ctx.logger).toBeDefined();
|
|
25
|
+
expect(ctx.trace).toBeDefined();
|
|
25
26
|
expect(ctx.workspaceRoot).toBe(process.cwd());
|
|
26
27
|
});
|
|
27
28
|
|
|
@@ -66,6 +67,25 @@ describe('createTestContext', () => {
|
|
|
66
67
|
const env = ctx['env'] as Record<string, string>;
|
|
67
68
|
expect(env).toEqual({ CUSTOM: '1', NODE_ENV: 'test' });
|
|
68
69
|
});
|
|
70
|
+
|
|
71
|
+
test('default trace passthrough resolves the callback result', async () => {
|
|
72
|
+
const ctx = createTestContext();
|
|
73
|
+
const result = await ctx.trace('test-span', () => Promise.resolve('ok'));
|
|
74
|
+
expect(result).toBe('ok');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('overrides trace', async () => {
|
|
78
|
+
const calls: string[] = [];
|
|
79
|
+
const ctx = createTestContext({
|
|
80
|
+
trace: async (label, fn) => {
|
|
81
|
+
calls.push(label);
|
|
82
|
+
return await fn();
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
await ctx.trace('custom-span', () => Promise.resolve('done'));
|
|
87
|
+
expect(calls).toEqual(['custom-span']);
|
|
88
|
+
});
|
|
69
89
|
});
|
|
70
90
|
|
|
71
91
|
describe('createCrossContext', () => {
|
|
@@ -102,7 +122,7 @@ describe('createCrossContext', () => {
|
|
|
102
122
|
});
|
|
103
123
|
|
|
104
124
|
describe('mergeTestContext', () => {
|
|
105
|
-
test('merges
|
|
125
|
+
test('merges resource overrides into extensions and the resource accessor', () => {
|
|
106
126
|
const ctx = mergeTestContext(
|
|
107
127
|
{
|
|
108
128
|
extensions: { existing: true },
|
|
@@ -114,14 +134,14 @@ describe('mergeTestContext', () => {
|
|
|
114
134
|
'db.main': { source: 'mock' },
|
|
115
135
|
existing: true,
|
|
116
136
|
});
|
|
117
|
-
expect(ctx.
|
|
137
|
+
expect(ctx.resource<{ source: string }>('db.main').source).toBe('mock');
|
|
118
138
|
});
|
|
119
139
|
});
|
|
120
140
|
|
|
121
|
-
describe('
|
|
122
|
-
test('creates fresh mock
|
|
141
|
+
describe('createMockResources', () => {
|
|
142
|
+
test('creates fresh mock resources for each invocation', async () => {
|
|
123
143
|
let mockCalls = 0;
|
|
124
|
-
const mockable =
|
|
144
|
+
const mockable = resource(`resource.mock.${Bun.randomUUIDv7()}`, {
|
|
125
145
|
create: () => Result.ok({ source: 'factory' }),
|
|
126
146
|
mock: () => {
|
|
127
147
|
mockCalls += 1;
|
|
@@ -130,8 +150,8 @@ describe('resolveMockProvisions', () => {
|
|
|
130
150
|
});
|
|
131
151
|
const app = topo('mock-app', { mockable } as Record<string, unknown>);
|
|
132
152
|
|
|
133
|
-
const first = await
|
|
134
|
-
const second = await
|
|
153
|
+
const first = await createMockResources(app);
|
|
154
|
+
const second = await createMockResources(app);
|
|
135
155
|
|
|
136
156
|
expect(first).toEqual({ [mockable.id]: { source: 'mock' } });
|
|
137
157
|
expect(second).toEqual(first);
|
|
@@ -139,12 +159,12 @@ describe('resolveMockProvisions', () => {
|
|
|
139
159
|
expect(mockCalls).toBe(2);
|
|
140
160
|
});
|
|
141
161
|
|
|
142
|
-
test('skips
|
|
143
|
-
const plain =
|
|
162
|
+
test('skips resources without mock factories', async () => {
|
|
163
|
+
const plain = resource(`resource.plain.${Bun.randomUUIDv7()}`, {
|
|
144
164
|
create: () => Result.ok({ source: 'factory' }),
|
|
145
165
|
});
|
|
146
166
|
const app = topo('plain-app', { plain } as Record<string, unknown>);
|
|
147
167
|
|
|
148
|
-
expect(await
|
|
168
|
+
expect(await createMockResources(app)).toEqual({});
|
|
149
169
|
});
|
|
150
170
|
});
|
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import { describe, test } from 'bun:test';
|
|
1
|
+
import { afterAll, describe, expect, mock, test } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import { Result,
|
|
3
|
+
import { contour, Result, resource, trail, topo } from '@ontrails/core';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
|
|
6
6
|
import { testContracts } from '../contracts.js';
|
|
7
7
|
|
|
8
|
+
const requireContourExample = (
|
|
9
|
+
contourDef: { examples?: readonly Record<string, unknown>[] },
|
|
10
|
+
index: number
|
|
11
|
+
) => {
|
|
12
|
+
const example = contourDef.examples?.[index];
|
|
13
|
+
expect(example).toBeDefined();
|
|
14
|
+
if (!example) {
|
|
15
|
+
throw new Error(`Expected contour example at index ${index}`);
|
|
16
|
+
}
|
|
17
|
+
return example;
|
|
18
|
+
};
|
|
19
|
+
|
|
8
20
|
// ---------------------------------------------------------------------------
|
|
9
21
|
// Test trails
|
|
10
22
|
// ---------------------------------------------------------------------------
|
|
@@ -72,46 +84,74 @@ const transformedInputTrail = trail('contract.transformed', {
|
|
|
72
84
|
output: z.object({ value: z.number() }),
|
|
73
85
|
});
|
|
74
86
|
|
|
75
|
-
const
|
|
87
|
+
const undeclaredContractDbResource = resource('db.undeclared.contracts', {
|
|
76
88
|
create: () => Result.ok({ source: 'factory' }),
|
|
77
89
|
mock: () => ({ source: 'mock' }),
|
|
78
90
|
});
|
|
79
91
|
|
|
80
|
-
const undeclaredContractTrail = trail('
|
|
92
|
+
const undeclaredContractTrail = trail('resource.undeclared.contracts', {
|
|
81
93
|
blaze: (_input, ctx) =>
|
|
82
94
|
Result.ok({
|
|
83
|
-
|
|
84
|
-
ctx.extensions?.[
|
|
95
|
+
hasInjectedResource:
|
|
96
|
+
ctx.extensions?.[undeclaredContractDbResource.id] !== undefined,
|
|
85
97
|
}),
|
|
86
98
|
examples: [
|
|
87
99
|
{
|
|
88
|
-
expected: {
|
|
100
|
+
expected: { hasInjectedResource: false },
|
|
89
101
|
input: {},
|
|
90
|
-
name: 'Undeclared
|
|
102
|
+
name: 'Undeclared resources are not preloaded into contract contexts',
|
|
91
103
|
},
|
|
92
104
|
],
|
|
93
105
|
input: z.object({}),
|
|
94
|
-
output: z.object({
|
|
106
|
+
output: z.object({ hasInjectedResource: z.literal(false) }),
|
|
95
107
|
});
|
|
96
108
|
|
|
97
|
-
const
|
|
109
|
+
const ctxOverrideContractResource = resource('db.mock.contracts', {
|
|
98
110
|
create: () => Result.ok({ source: 'factory' }),
|
|
99
111
|
mock: () => ({ source: 'mock' }),
|
|
100
112
|
});
|
|
101
113
|
|
|
102
|
-
const ctxOverrideContractTrail = trail('
|
|
114
|
+
const ctxOverrideContractTrail = trail('resource.ctx.contracts', {
|
|
103
115
|
blaze: (_input, ctx) =>
|
|
104
|
-
Result.ok({ source:
|
|
116
|
+
Result.ok({ source: ctxOverrideContractResource.from(ctx).source }),
|
|
105
117
|
examples: [
|
|
106
118
|
{
|
|
107
119
|
expected: { source: 'ctx' },
|
|
108
120
|
input: {},
|
|
109
|
-
name: 'Context extensions beat auto-resolved contract
|
|
121
|
+
name: 'Context extensions beat auto-resolved contract resource mocks',
|
|
110
122
|
},
|
|
111
123
|
],
|
|
112
124
|
input: z.object({}),
|
|
113
125
|
output: z.object({ source: z.literal('ctx') }),
|
|
114
|
-
|
|
126
|
+
resources: [ctxOverrideContractResource],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const derivedContractContour = contour(
|
|
130
|
+
'contractFixture',
|
|
131
|
+
{
|
|
132
|
+
id: z.string().uuid(),
|
|
133
|
+
name: z.string(),
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
examples: [
|
|
137
|
+
{
|
|
138
|
+
id: '03a5873c-0ca6-43c4-9201-3cb3c07ca6bf',
|
|
139
|
+
name: 'Contour contract fixture',
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
identity: 'id',
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const derivedContractBlaze = mock(() =>
|
|
147
|
+
Result.ok(requireContourExample(derivedContractContour, 0))
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const derivedContractTrail = trail('contract.derived', {
|
|
151
|
+
blaze: () => derivedContractBlaze(),
|
|
152
|
+
contours: [derivedContractContour],
|
|
153
|
+
input: z.object({ id: derivedContractContour.shape.id }),
|
|
154
|
+
output: derivedContractContour,
|
|
115
155
|
});
|
|
116
156
|
|
|
117
157
|
// ---------------------------------------------------------------------------
|
|
@@ -163,7 +203,7 @@ describe('testContracts: context extension overrides', () => {
|
|
|
163
203
|
// eslint-disable-next-line jest/require-hook
|
|
164
204
|
testContracts(
|
|
165
205
|
topo('ctx-contract-app', {
|
|
166
|
-
|
|
206
|
+
ctxOverrideContractResource,
|
|
167
207
|
ctxOverrideContractTrail,
|
|
168
208
|
} as Record<string, unknown>),
|
|
169
209
|
{
|
|
@@ -174,12 +214,26 @@ describe('testContracts: context extension overrides', () => {
|
|
|
174
214
|
);
|
|
175
215
|
});
|
|
176
216
|
|
|
177
|
-
describe('testContracts
|
|
217
|
+
describe('testContracts resource declarations', () => {
|
|
178
218
|
// eslint-disable-next-line jest/require-hook
|
|
179
219
|
testContracts(
|
|
180
|
-
topo('undeclared-contract-
|
|
181
|
-
|
|
220
|
+
topo('undeclared-contract-resource-app', {
|
|
221
|
+
undeclaredContractDbResource,
|
|
182
222
|
undeclaredContractTrail,
|
|
183
223
|
} as Record<string, unknown>)
|
|
184
224
|
);
|
|
185
225
|
});
|
|
226
|
+
|
|
227
|
+
describe('testContracts derives contour examples when trail examples are absent', () => {
|
|
228
|
+
// eslint-disable-next-line jest/require-hook
|
|
229
|
+
testContracts(
|
|
230
|
+
topo('derived-contract-app', {
|
|
231
|
+
derivedContractContour,
|
|
232
|
+
derivedContractTrail,
|
|
233
|
+
} as Record<string, unknown>)
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
afterAll(() => {
|
|
237
|
+
expect(derivedContractBlaze).toHaveBeenCalledTimes(1);
|
|
238
|
+
});
|
|
239
|
+
});
|