@ontrails/testing 1.0.0-beta.11 → 1.0.0-beta.13
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 +72 -22
- package/README.md +9 -9
- package/dist/context.d.ts +41 -11
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +36 -21
- package/dist/context.js.map +1 -1
- package/dist/contracts.js +8 -8
- package/dist/contracts.js.map +1 -1
- package/dist/crosses.d.ts +38 -0
- package/dist/crosses.d.ts.map +1 -0
- package/dist/crosses.js +213 -0
- package/dist/crosses.js.map +1 -0
- package/dist/examples.d.ts +4 -4
- package/dist/examples.d.ts.map +1 -1
- package/dist/examples.js +52 -32
- package/dist/examples.js.map +1 -1
- package/dist/harness-mcp.d.ts +1 -1
- package/dist/harness-mcp.js +2 -2
- package/dist/harness-mcp.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- 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 +8 -8
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/all.test.ts +20 -18
- package/src/__tests__/context.test.ts +27 -27
- package/src/__tests__/contracts.test.ts +29 -29
- package/src/__tests__/{follows.test.ts → crosses.test.ts} +193 -195
- package/src/__tests__/detours.test.ts +3 -3
- package/src/__tests__/examples.test.ts +221 -126
- package/src/__tests__/trail.test.ts +4 -4
- package/src/context.ts +80 -32
- package/src/contracts.ts +12 -12
- package/src/{follows.ts → crosses.ts} +97 -92
- package/src/examples.ts +76 -46
- package/src/harness-mcp.ts +2 -2
- package/src/index.ts +15 -7
- package/src/trail.ts +1 -1
- package/src/types.ts +9 -9
- package/tsconfig.tsbuildinfo +1 -1
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.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts",
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
"clean": "rm -rf dist *.tsbuildinfo"
|
|
15
15
|
},
|
|
16
16
|
"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.
|
|
17
|
+
"@ontrails/cli": "^1.0.0-beta.12",
|
|
18
|
+
"@ontrails/core": "^1.0.0-beta.12",
|
|
19
|
+
"@ontrails/logging": "^1.0.0-beta.12",
|
|
20
|
+
"@ontrails/mcp": "^1.0.0-beta.12",
|
|
21
21
|
"zod": "^4.3.5"
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -1,64 +1,66 @@
|
|
|
1
1
|
import { describe } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import { Result,
|
|
3
|
+
import { Result, provision, trail, topo } from '@ontrails/core';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
|
|
6
6
|
import { testAll } from '../all.js';
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const mockDbProvision = provision('db.mock.all', {
|
|
9
9
|
create: () => Result.ok({ source: 'factory' }),
|
|
10
10
|
mock: () => ({ source: 'mock' }),
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
const mockedTrail = trail('
|
|
14
|
-
|
|
13
|
+
const mockedTrail = trail('provision.mocked.all', {
|
|
14
|
+
blaze: (_input, ctx) =>
|
|
15
|
+
Result.ok({ source: mockDbProvision.from(ctx).source }),
|
|
16
|
+
description: 'Trail that uses a mocked provision through testAll',
|
|
15
17
|
examples: [
|
|
16
18
|
{
|
|
17
19
|
expected: { source: 'mock' },
|
|
18
20
|
input: {},
|
|
19
|
-
name: 'Uses auto-resolved
|
|
21
|
+
name: 'Uses auto-resolved provision mock',
|
|
20
22
|
},
|
|
21
23
|
],
|
|
22
24
|
input: z.object({}),
|
|
23
25
|
output: z.object({ source: z.string() }),
|
|
24
|
-
|
|
25
|
-
services: [mockDbService],
|
|
26
|
+
provisions: [mockDbProvision],
|
|
26
27
|
});
|
|
27
28
|
|
|
28
|
-
const overrideTrail = trail('
|
|
29
|
+
const overrideTrail = trail('provision.override.all', {
|
|
30
|
+
blaze: (_input, ctx) =>
|
|
31
|
+
Result.ok({ source: mockDbProvision.from(ctx).source }),
|
|
29
32
|
description: 'Trail that prefers explicit overrides over mock factories',
|
|
30
33
|
examples: [
|
|
31
34
|
{
|
|
32
35
|
expected: { source: 'override' },
|
|
33
36
|
input: {},
|
|
34
|
-
name: 'Explicit
|
|
37
|
+
name: 'Explicit provision override wins',
|
|
35
38
|
},
|
|
36
39
|
],
|
|
37
40
|
input: z.object({}),
|
|
38
41
|
output: z.object({ source: z.string() }),
|
|
39
|
-
|
|
40
|
-
services: [mockDbService],
|
|
42
|
+
provisions: [mockDbProvision],
|
|
41
43
|
});
|
|
42
44
|
|
|
43
|
-
describe('testAll
|
|
45
|
+
describe('testAll provision mocks', () => {
|
|
44
46
|
// eslint-disable-next-line jest/require-hook
|
|
45
47
|
testAll(
|
|
46
|
-
topo('test-all-
|
|
47
|
-
|
|
48
|
+
topo('test-all-provision-mock-app', {
|
|
49
|
+
mockDbProvision,
|
|
48
50
|
mockedTrail,
|
|
49
51
|
} as Record<string, unknown>)
|
|
50
52
|
);
|
|
51
53
|
});
|
|
52
54
|
|
|
53
|
-
describe('testAll explicit
|
|
55
|
+
describe('testAll explicit provision overrides', () => {
|
|
54
56
|
// eslint-disable-next-line jest/require-hook
|
|
55
57
|
testAll(
|
|
56
|
-
topo('test-all-
|
|
57
|
-
|
|
58
|
+
topo('test-all-provision-override-app', {
|
|
59
|
+
mockDbProvision,
|
|
58
60
|
overrideTrail,
|
|
59
61
|
} as Record<string, unknown>),
|
|
60
62
|
{
|
|
61
|
-
|
|
63
|
+
provisions: { 'db.mock.all': { source: 'override' } },
|
|
62
64
|
}
|
|
63
65
|
);
|
|
64
66
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import { Result,
|
|
3
|
+
import { Result, provision, topo } from '@ontrails/core';
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
|
|
6
|
+
createCrossContext,
|
|
7
7
|
createTestContext,
|
|
8
8
|
mergeTestContext,
|
|
9
|
-
|
|
9
|
+
resolveMockProvisions,
|
|
10
10
|
} from '../context.js';
|
|
11
11
|
import type { TestLogger } from '../types.js';
|
|
12
12
|
|
|
@@ -19,8 +19,8 @@ describe('createTestContext', () => {
|
|
|
19
19
|
const ctx = createTestContext();
|
|
20
20
|
expect(ctx.cwd).toBe(process.cwd());
|
|
21
21
|
expect(ctx.requestId).toBe('test-request-001');
|
|
22
|
-
expect(ctx.
|
|
23
|
-
expect(ctx.
|
|
22
|
+
expect(ctx.abortSignal).toBeDefined();
|
|
23
|
+
expect(ctx.abortSignal.aborted).toBe(false);
|
|
24
24
|
expect(ctx.logger).toBeDefined();
|
|
25
25
|
expect(ctx.workspaceRoot).toBe(process.cwd());
|
|
26
26
|
});
|
|
@@ -46,11 +46,11 @@ describe('createTestContext', () => {
|
|
|
46
46
|
expect(ctx.requestId).toBe('custom-id');
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
test('overrides
|
|
49
|
+
test('overrides abortSignal', () => {
|
|
50
50
|
const controller = new AbortController();
|
|
51
51
|
controller.abort();
|
|
52
|
-
const ctx = createTestContext({
|
|
53
|
-
expect(ctx.
|
|
52
|
+
const ctx = createTestContext({ abortSignal: controller.signal });
|
|
53
|
+
expect(ctx.abortSignal.aborted).toBe(true);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
test('overrides cwd', () => {
|
|
@@ -68,41 +68,41 @@ describe('createTestContext', () => {
|
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
describe('
|
|
71
|
+
describe('createCrossContext', () => {
|
|
72
72
|
test('returns configured ok response for registered id', async () => {
|
|
73
|
-
const
|
|
73
|
+
const cross = createCrossContext({
|
|
74
74
|
responses: { 'entity.add': Result.ok({ id: '1', name: 'Alpha' }) },
|
|
75
75
|
});
|
|
76
|
-
const result = await
|
|
76
|
+
const result = await cross('entity.add', { name: 'Alpha' });
|
|
77
77
|
expect(result.unwrap()).toEqual({ id: '1', name: 'Alpha' });
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
test('returns configured err response for registered id', async () => {
|
|
81
81
|
const err = new Error('conflict');
|
|
82
|
-
const
|
|
82
|
+
const cross = createCrossContext({
|
|
83
83
|
responses: { 'entity.add': Result.err(err) },
|
|
84
84
|
});
|
|
85
|
-
const result = await
|
|
85
|
+
const result = await cross('entity.add', {});
|
|
86
86
|
expect(result.isErr()).toBe(true);
|
|
87
87
|
expect(result.error?.message).toBe('conflict');
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
test('returns err with descriptive message for unregistered id', async () => {
|
|
91
|
-
const
|
|
92
|
-
const result = await
|
|
91
|
+
const cross = createCrossContext();
|
|
92
|
+
const result = await cross('unknown.trail', {});
|
|
93
93
|
expect(result.isErr()).toBe(true);
|
|
94
94
|
expect(result.error?.message).toContain('unknown.trail');
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
test('no options defaults to empty responses', async () => {
|
|
98
|
-
const
|
|
99
|
-
const result = await
|
|
98
|
+
const cross = createCrossContext();
|
|
99
|
+
const result = await cross('any.id', {});
|
|
100
100
|
expect(result.isErr()).toBe(true);
|
|
101
101
|
});
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
describe('mergeTestContext', () => {
|
|
105
|
-
test('merges
|
|
105
|
+
test('merges provision overrides into extensions and the provision accessor', () => {
|
|
106
106
|
const ctx = mergeTestContext(
|
|
107
107
|
{
|
|
108
108
|
extensions: { existing: true },
|
|
@@ -114,14 +114,14 @@ describe('mergeTestContext', () => {
|
|
|
114
114
|
'db.main': { source: 'mock' },
|
|
115
115
|
existing: true,
|
|
116
116
|
});
|
|
117
|
-
expect(ctx.
|
|
117
|
+
expect(ctx.provision<{ source: string }>('db.main').source).toBe('mock');
|
|
118
118
|
});
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
describe('
|
|
122
|
-
test('creates fresh mock
|
|
121
|
+
describe('resolveMockProvisions', () => {
|
|
122
|
+
test('creates fresh mock provisions for each invocation', async () => {
|
|
123
123
|
let mockCalls = 0;
|
|
124
|
-
const mockable =
|
|
124
|
+
const mockable = provision(`provision.mock.${Bun.randomUUIDv7()}`, {
|
|
125
125
|
create: () => Result.ok({ source: 'factory' }),
|
|
126
126
|
mock: () => {
|
|
127
127
|
mockCalls += 1;
|
|
@@ -130,8 +130,8 @@ describe('resolveMockServices', () => {
|
|
|
130
130
|
});
|
|
131
131
|
const app = topo('mock-app', { mockable } as Record<string, unknown>);
|
|
132
132
|
|
|
133
|
-
const first = await
|
|
134
|
-
const second = await
|
|
133
|
+
const first = await resolveMockProvisions(app);
|
|
134
|
+
const second = await resolveMockProvisions(app);
|
|
135
135
|
|
|
136
136
|
expect(first).toEqual({ [mockable.id]: { source: 'mock' } });
|
|
137
137
|
expect(second).toEqual(first);
|
|
@@ -139,12 +139,12 @@ describe('resolveMockServices', () => {
|
|
|
139
139
|
expect(mockCalls).toBe(2);
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
-
test('skips
|
|
143
|
-
const plain =
|
|
142
|
+
test('skips provisions without mock factories', async () => {
|
|
143
|
+
const plain = provision(`provision.plain.${Bun.randomUUIDv7()}`, {
|
|
144
144
|
create: () => Result.ok({ source: 'factory' }),
|
|
145
145
|
});
|
|
146
146
|
const app = topo('plain-app', { plain } as Record<string, unknown>);
|
|
147
147
|
|
|
148
|
-
expect(await
|
|
148
|
+
expect(await resolveMockProvisions(app)).toEqual({});
|
|
149
149
|
});
|
|
150
150
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test } from 'bun:test';
|
|
2
2
|
|
|
3
|
-
import { Result,
|
|
3
|
+
import { Result, provision, trail, topo } from '@ontrails/core';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
|
|
6
6
|
import { testContracts } from '../contracts.js';
|
|
@@ -11,6 +11,7 @@ import { testContracts } from '../contracts.js';
|
|
|
11
11
|
|
|
12
12
|
/** Trail whose implementation matches the output schema. */
|
|
13
13
|
const validTrail = trail('valid', {
|
|
14
|
+
blaze: (input: { name: string }) => Result.ok({ id: 1, name: input.name }),
|
|
14
15
|
examples: [
|
|
15
16
|
{
|
|
16
17
|
expected: { id: 1, name: 'Alpha' },
|
|
@@ -20,21 +21,20 @@ const validTrail = trail('valid', {
|
|
|
20
21
|
],
|
|
21
22
|
input: z.object({ name: z.string() }),
|
|
22
23
|
output: z.object({ id: z.number(), name: z.string() }),
|
|
23
|
-
run: (input: { name: string }) => Result.ok({ id: 1, name: input.name }),
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
/** Trail without output schema -- should be skipped. */
|
|
27
27
|
const noSchemaTrail = trail('noschema', {
|
|
28
|
+
blaze: (input: { x: number }) => Result.ok(input.x * 2),
|
|
28
29
|
examples: [{ expected: 10, input: { x: 5 }, name: 'No schema' }],
|
|
29
30
|
input: z.object({ x: z.number() }),
|
|
30
|
-
run: (input: { x: number }) => Result.ok(input.x * 2),
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
/** Trail without examples -- should be skipped. */
|
|
34
34
|
const noExamplesTrail = trail('noexamples', {
|
|
35
|
+
blaze: (input: { x: number }) => Result.ok({ value: input.x }),
|
|
35
36
|
input: z.object({ x: z.number() }),
|
|
36
37
|
output: z.object({ value: z.number() }),
|
|
37
|
-
run: (input: { x: number }) => Result.ok({ value: input.x }),
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
// ---------------------------------------------------------------------------
|
|
@@ -43,6 +43,9 @@ const noExamplesTrail = trail('noexamples', {
|
|
|
43
43
|
|
|
44
44
|
/** Composition trail whose implementation matches the output schema. */
|
|
45
45
|
const compositionTrail = trail('composition.valid', {
|
|
46
|
+
blaze: (input: { a: number; b: number }) =>
|
|
47
|
+
Result.ok({ total: input.a + input.b }),
|
|
48
|
+
crosses: ['valid'],
|
|
46
49
|
examples: [
|
|
47
50
|
{
|
|
48
51
|
expected: { total: 3 },
|
|
@@ -50,14 +53,12 @@ const compositionTrail = trail('composition.valid', {
|
|
|
50
53
|
name: 'Valid composition output',
|
|
51
54
|
},
|
|
52
55
|
],
|
|
53
|
-
follow: ['valid'],
|
|
54
56
|
input: z.object({ a: z.number(), b: z.number() }),
|
|
55
57
|
output: z.object({ total: z.number() }),
|
|
56
|
-
run: (input: { a: number; b: number }) =>
|
|
57
|
-
Result.ok({ total: input.a + input.b }),
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
const transformedInputTrail = trail('contract.transformed', {
|
|
61
|
+
blaze: (input: { value: number }) => Result.ok({ value: input.value }),
|
|
61
62
|
examples: [
|
|
62
63
|
{
|
|
63
64
|
expected: { value: 2 },
|
|
@@ -69,49 +70,48 @@ const transformedInputTrail = trail('contract.transformed', {
|
|
|
69
70
|
.object({ value: z.string() })
|
|
70
71
|
.transform(({ value }) => ({ value: Number(value) + 1 })),
|
|
71
72
|
output: z.object({ value: z.number() }),
|
|
72
|
-
run: (input: { value: number }) => Result.ok({ value: input.value }),
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
const
|
|
75
|
+
const undeclaredContractDbProvision = provision('db.undeclared.contracts', {
|
|
76
76
|
create: () => Result.ok({ source: 'factory' }),
|
|
77
77
|
mock: () => ({ source: 'mock' }),
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
-
const undeclaredContractTrail = trail('
|
|
80
|
+
const undeclaredContractTrail = trail('provision.undeclared.contracts', {
|
|
81
|
+
blaze: (_input, ctx) =>
|
|
82
|
+
Result.ok({
|
|
83
|
+
hasInjectedProvision:
|
|
84
|
+
ctx.extensions?.[undeclaredContractDbProvision.id] !== undefined,
|
|
85
|
+
}),
|
|
81
86
|
examples: [
|
|
82
87
|
{
|
|
83
|
-
expected: {
|
|
88
|
+
expected: { hasInjectedProvision: false },
|
|
84
89
|
input: {},
|
|
85
|
-
name: 'Undeclared
|
|
90
|
+
name: 'Undeclared provisions are not preloaded into contract contexts',
|
|
86
91
|
},
|
|
87
92
|
],
|
|
88
93
|
input: z.object({}),
|
|
89
|
-
output: z.object({
|
|
90
|
-
run: (_input, ctx) =>
|
|
91
|
-
Result.ok({
|
|
92
|
-
hasInjectedService:
|
|
93
|
-
ctx.extensions?.[undeclaredContractDbService.id] !== undefined,
|
|
94
|
-
}),
|
|
94
|
+
output: z.object({ hasInjectedProvision: z.literal(false) }),
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
const
|
|
97
|
+
const ctxOverrideContractProvision = provision('db.mock.contracts', {
|
|
98
98
|
create: () => Result.ok({ source: 'factory' }),
|
|
99
99
|
mock: () => ({ source: 'mock' }),
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
const ctxOverrideContractTrail = trail('
|
|
102
|
+
const ctxOverrideContractTrail = trail('provision.ctx.contracts', {
|
|
103
|
+
blaze: (_input, ctx) =>
|
|
104
|
+
Result.ok({ source: ctxOverrideContractProvision.from(ctx).source }),
|
|
103
105
|
examples: [
|
|
104
106
|
{
|
|
105
107
|
expected: { source: 'ctx' },
|
|
106
108
|
input: {},
|
|
107
|
-
name: 'Context extensions beat auto-resolved contract mocks',
|
|
109
|
+
name: 'Context extensions beat auto-resolved contract provision mocks',
|
|
108
110
|
},
|
|
109
111
|
],
|
|
110
112
|
input: z.object({}),
|
|
111
113
|
output: z.object({ source: z.literal('ctx') }),
|
|
112
|
-
|
|
113
|
-
Result.ok({ source: ctxOverrideContractService.from(ctx).source }),
|
|
114
|
-
services: [ctxOverrideContractService],
|
|
114
|
+
provisions: [ctxOverrideContractProvision],
|
|
115
115
|
});
|
|
116
116
|
|
|
117
117
|
// ---------------------------------------------------------------------------
|
|
@@ -143,7 +143,7 @@ describe('testContracts: skips trails without examples', () => {
|
|
|
143
143
|
});
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
-
describe('testContracts: validates
|
|
146
|
+
describe('testContracts: validates output schemas for trails with crossings', () => {
|
|
147
147
|
// eslint-disable-next-line jest/require-hook
|
|
148
148
|
testContracts(
|
|
149
149
|
topo('test-app', { compositionTrail } as Record<string, unknown>)
|
|
@@ -163,7 +163,7 @@ describe('testContracts: context extension overrides', () => {
|
|
|
163
163
|
// eslint-disable-next-line jest/require-hook
|
|
164
164
|
testContracts(
|
|
165
165
|
topo('ctx-contract-app', {
|
|
166
|
-
|
|
166
|
+
ctxOverrideContractProvision,
|
|
167
167
|
ctxOverrideContractTrail,
|
|
168
168
|
} as Record<string, unknown>),
|
|
169
169
|
{
|
|
@@ -174,11 +174,11 @@ describe('testContracts: context extension overrides', () => {
|
|
|
174
174
|
);
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
describe('testContracts
|
|
177
|
+
describe('testContracts provision declarations', () => {
|
|
178
178
|
// eslint-disable-next-line jest/require-hook
|
|
179
179
|
testContracts(
|
|
180
|
-
topo('undeclared-contract-
|
|
181
|
-
|
|
180
|
+
topo('undeclared-contract-provision-app', {
|
|
181
|
+
undeclaredContractDbProvision,
|
|
182
182
|
undeclaredContractTrail,
|
|
183
183
|
} as Record<string, unknown>)
|
|
184
184
|
);
|