@ontrails/testing 1.0.0-beta.15 → 1.0.0-beta.17
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 +70 -1
- package/README.md +47 -3
- package/package.json +16 -7
- package/src/all.ts +32 -7
- package/src/context.ts +3 -0
- package/src/contracts.ts +1 -16
- package/src/examples.ts +19 -11
- package/src/harness-cli.ts +16 -3
- package/src/harness-http.ts +263 -0
- package/src/harness-mcp.ts +2 -0
- package/src/index.ts +18 -1
- package/src/logger.ts +3 -1
- package/src/scenario.ts +3 -5
- package/src/signals.ts +221 -0
- package/src/surface-parity.ts +356 -0
- package/src/types.ts +136 -1
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/all.d.ts +0 -36
- package/dist/all.d.ts.map +0 -1
- package/dist/all.js +0 -99
- package/dist/all.js.map +0 -1
- package/dist/assertions.d.ts +0 -72
- package/dist/assertions.d.ts.map +0 -1
- package/dist/assertions.js +0 -238
- package/dist/assertions.js.map +0 -1
- package/dist/context.d.ts +0 -76
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -118
- package/dist/context.js.map +0 -1
- package/dist/contracts.d.ts +0 -17
- package/dist/contracts.d.ts.map +0 -1
- package/dist/contracts.js +0 -75
- package/dist/contracts.js.map +0 -1
- package/dist/crosses.d.ts +0 -38
- package/dist/crosses.d.ts.map +0 -1
- package/dist/crosses.js +0 -223
- package/dist/crosses.js.map +0 -1
- package/dist/detours.d.ts +0 -13
- package/dist/detours.d.ts.map +0 -1
- package/dist/detours.js +0 -109
- package/dist/detours.js.map +0 -1
- package/dist/effective-examples.d.ts +0 -30
- package/dist/effective-examples.d.ts.map +0 -1
- package/dist/effective-examples.js +0 -227
- package/dist/effective-examples.js.map +0 -1
- package/dist/examples.d.ts +0 -23
- package/dist/examples.d.ts.map +0 -1
- package/dist/examples.js +0 -240
- package/dist/examples.js.map +0 -1
- package/dist/harness-cli.d.ts +0 -21
- package/dist/harness-cli.d.ts.map +0 -1
- package/dist/harness-cli.js +0 -205
- package/dist/harness-cli.js.map +0 -1
- package/dist/harness-mcp.d.ts +0 -21
- package/dist/harness-mcp.d.ts.map +0 -1
- package/dist/harness-mcp.js +0 -54
- package/dist/harness-mcp.js.map +0 -1
- package/dist/index.d.ts +0 -18
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -19
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts +0 -15
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -87
- package/dist/logger.js.map +0 -1
- package/dist/scenario.d.ts +0 -37
- package/dist/scenario.d.ts.map +0 -1
- package/dist/scenario.js +0 -235
- package/dist/scenario.js.map +0 -1
- package/dist/trail.d.ts +0 -20
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -80
- package/dist/trail.js.map +0 -1
- package/dist/types.d.ts +0 -113
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/src/__tests__/all.test.ts +0 -254
- package/src/__tests__/context.test.ts +0 -170
- package/src/__tests__/contracts.test.ts +0 -239
- package/src/__tests__/crosses.test.ts +0 -587
- package/src/__tests__/detours.test.ts +0 -212
- package/src/__tests__/effective-examples.test.ts +0 -203
- package/src/__tests__/examples.test.ts +0 -636
- package/src/__tests__/harness-cli.test.ts +0 -90
- package/src/__tests__/harness-mcp.test.ts +0 -37
- package/src/__tests__/logger.test.ts +0 -136
- package/src/__tests__/partial-match.test.ts +0 -126
- package/src/__tests__/scenario.test.ts +0 -381
- package/src/__tests__/trail.test.ts +0 -99
- package/tsconfig.json +0 -9
- package/tsconfig.tests.json +0 -10
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
4
|
-
|
|
5
|
-
import { ConflictError, Result, trail, topo } from '@ontrails/core';
|
|
6
|
-
import { z } from 'zod';
|
|
7
|
-
|
|
8
|
-
import { testDetours } from '../detours.js';
|
|
9
|
-
|
|
10
|
-
const showTrail = trail('entity.show', {
|
|
11
|
-
blaze: (input: { id: string }) => Result.ok({ id: input.id }),
|
|
12
|
-
detours: [
|
|
13
|
-
{
|
|
14
|
-
on: ConflictError,
|
|
15
|
-
/* oxlint-disable-next-line require-await -- test stub */
|
|
16
|
-
recover: async () => Result.ok({ id: 'recovered' }),
|
|
17
|
-
},
|
|
18
|
-
],
|
|
19
|
-
input: z.object({ id: z.string() }),
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const noDetoursTrail = trail('entity.plain', {
|
|
23
|
-
blaze: () => Result.ok('ok'),
|
|
24
|
-
input: z.object({}),
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const createBlankNamedDetourTrail = () => {
|
|
28
|
-
class BlankNamedConflictError extends ConflictError {}
|
|
29
|
-
|
|
30
|
-
Object.defineProperty(BlankNamedConflictError, 'name', { value: '' });
|
|
31
|
-
|
|
32
|
-
return trail('entity.blank-name', {
|
|
33
|
-
blaze: (input: { id: string }) => Result.ok({ id: input.id }),
|
|
34
|
-
detours: [
|
|
35
|
-
{
|
|
36
|
-
on: BlankNamedConflictError,
|
|
37
|
-
/* oxlint-disable-next-line require-await -- test stub */
|
|
38
|
-
recover: async () => Result.ok({ id: 'recovered' }),
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
input: z.object({ id: z.string() }),
|
|
42
|
-
});
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const repoTempDir = (): string =>
|
|
46
|
-
join(
|
|
47
|
-
resolve(import.meta.dir, '../..'),
|
|
48
|
-
'.tmp-tests',
|
|
49
|
-
`test-detours-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const generatedSuiteSource = (
|
|
53
|
-
variant: 'invalidOn' | 'invalidRecover' | 'shadowed'
|
|
54
|
-
): string => {
|
|
55
|
-
const shared = `import { ConflictError, Result, TrailsError, trail, topo } from '@ontrails/core';
|
|
56
|
-
import { testDetours } from '../../src/index.ts';
|
|
57
|
-
import { z } from 'zod';
|
|
58
|
-
|
|
59
|
-
const baseTrail = trail('entity.save', {
|
|
60
|
-
blaze: () => Result.err(new ConflictError('conflict')),
|
|
61
|
-
detours: [
|
|
62
|
-
{
|
|
63
|
-
on: ConflictError,
|
|
64
|
-
recover: async () => Result.ok({ recovered: true }),
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
input: z.object({}),
|
|
68
|
-
output: z.object({ recovered: z.boolean() }),
|
|
69
|
-
});
|
|
70
|
-
`;
|
|
71
|
-
|
|
72
|
-
if (variant === 'invalidOn') {
|
|
73
|
-
return `${shared}
|
|
74
|
-
const candidate = {
|
|
75
|
-
...baseTrail,
|
|
76
|
-
detours: [
|
|
77
|
-
{
|
|
78
|
-
on: 'ConflictError',
|
|
79
|
-
recover: async () => Result.ok({ recovered: true }),
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
} as typeof baseTrail;
|
|
83
|
-
|
|
84
|
-
testDetours(topo('detour-invalid-on', { candidate } as Record<string, unknown>));
|
|
85
|
-
`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (variant === 'invalidRecover') {
|
|
89
|
-
return `${shared}
|
|
90
|
-
const candidate = {
|
|
91
|
-
...baseTrail,
|
|
92
|
-
detours: [
|
|
93
|
-
{
|
|
94
|
-
on: ConflictError,
|
|
95
|
-
recover: 'not callable',
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
} as typeof baseTrail;
|
|
99
|
-
|
|
100
|
-
testDetours(topo('detour-invalid-recover', { candidate } as Record<string, unknown>));
|
|
101
|
-
`;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return `${shared}
|
|
105
|
-
const candidate = trail('entity.shadowed', {
|
|
106
|
-
blaze: () => Result.err(new ConflictError('conflict')),
|
|
107
|
-
detours: [
|
|
108
|
-
{
|
|
109
|
-
on: TrailsError,
|
|
110
|
-
recover: async () => Result.ok({ recovered: 'broad' }),
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
on: ConflictError,
|
|
114
|
-
recover: async () => Result.ok({ recovered: 'specific' }),
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
input: z.object({}),
|
|
118
|
-
output: z.object({ recovered: z.string() }),
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
testDetours(topo('detour-shadowed', { candidate }));
|
|
122
|
-
`;
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const runGeneratedDetourSuite = (
|
|
126
|
-
variant: 'invalidOn' | 'invalidRecover' | 'shadowed'
|
|
127
|
-
): { readonly exitCode: number; readonly output: string } => {
|
|
128
|
-
const dir = repoTempDir();
|
|
129
|
-
const testFile = join(dir, `${variant}.test.ts`);
|
|
130
|
-
|
|
131
|
-
mkdirSync(dir, { recursive: true });
|
|
132
|
-
writeFileSync(testFile, generatedSuiteSource(variant));
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
const proc = Bun.spawnSync({
|
|
136
|
-
cmd: ['bun', 'test', testFile, '--bail'],
|
|
137
|
-
cwd: resolve(import.meta.dir, '..', '..', '..'),
|
|
138
|
-
stderr: 'pipe',
|
|
139
|
-
stdout: 'pipe',
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
exitCode: proc.exitCode,
|
|
144
|
-
output: `${proc.stdout.toString()}\n${proc.stderr.toString()}`,
|
|
145
|
-
};
|
|
146
|
-
} finally {
|
|
147
|
-
rmSync(dir, { force: true, recursive: true });
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
describe('testDetours valid contracts', () => {
|
|
152
|
-
// eslint-disable-next-line jest/require-hook
|
|
153
|
-
testDetours(
|
|
154
|
-
topo('test-app', {
|
|
155
|
-
showTrail,
|
|
156
|
-
} as Record<string, unknown>)
|
|
157
|
-
);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe('testDetours skips trails without detours', () => {
|
|
161
|
-
// eslint-disable-next-line jest/require-hook
|
|
162
|
-
testDetours(
|
|
163
|
-
topo('test-app', {
|
|
164
|
-
noDetoursTrail,
|
|
165
|
-
} as Record<string, unknown>)
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
test('no-op marker', () => {
|
|
169
|
-
// Trail without detours is skipped -- no detour tests generated
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe('testDetours constructor handling', () => {
|
|
174
|
-
// eslint-disable-next-line jest/require-hook
|
|
175
|
-
testDetours(
|
|
176
|
-
topo('test-app', {
|
|
177
|
-
blankNamedDetourTrail: createBlankNamedDetourTrail(),
|
|
178
|
-
} as Record<string, unknown>)
|
|
179
|
-
);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe('testDetours invalid contract detection', () => {
|
|
183
|
-
test('fails when on is not an error constructor', () => {
|
|
184
|
-
const result = runGeneratedDetourSuite('invalidOn');
|
|
185
|
-
|
|
186
|
-
expect(result.exitCode).toBe(1);
|
|
187
|
-
expect(result.output).toContain(
|
|
188
|
-
'entity.save detour[0] must declare a real error constructor in on:'
|
|
189
|
-
);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test('fails when recover is not callable', () => {
|
|
193
|
-
const result = runGeneratedDetourSuite('invalidRecover');
|
|
194
|
-
|
|
195
|
-
expect(result.exitCode).toBe(1);
|
|
196
|
-
expect(result.output).toContain(
|
|
197
|
-
'entity.save detour[0] on ConflictError must declare a callable recover function'
|
|
198
|
-
);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test('fails when a later detour is shadowed by an earlier broader detour', () => {
|
|
202
|
-
const result = runGeneratedDetourSuite('shadowed');
|
|
203
|
-
|
|
204
|
-
expect(result.exitCode).toBe(1);
|
|
205
|
-
expect(result.output).toContain(
|
|
206
|
-
'entity.shadowed detour[1] on ConflictError'
|
|
207
|
-
);
|
|
208
|
-
expect(result.output).toContain(
|
|
209
|
-
'shadowed by earlier detour[0] on TrailsError'
|
|
210
|
-
);
|
|
211
|
-
});
|
|
212
|
-
});
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { contour, Result, trail } from '@ontrails/core';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
import { deriveTrailExamples } from '../effective-examples.js';
|
|
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
|
-
|
|
20
|
-
const userContour = contour(
|
|
21
|
-
'user',
|
|
22
|
-
{
|
|
23
|
-
email: z.string().email(),
|
|
24
|
-
id: z.string().uuid(),
|
|
25
|
-
name: z.string(),
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
examples: [
|
|
29
|
-
{
|
|
30
|
-
email: 'ada@example.com',
|
|
31
|
-
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
32
|
-
name: 'Ada',
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
email: 'grace@example.com',
|
|
36
|
-
id: '0f31f6ba-6ff0-41ce-9f6b-8d132b6c4b81',
|
|
37
|
-
name: 'Grace',
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
identity: 'id',
|
|
41
|
-
}
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
const gistContour = contour(
|
|
45
|
-
'gist',
|
|
46
|
-
{
|
|
47
|
-
id: z.string().uuid(),
|
|
48
|
-
ownerId: userContour.id(),
|
|
49
|
-
title: z.string(),
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
examples: [
|
|
53
|
-
{
|
|
54
|
-
id: '8f7ef40d-8234-4f73-8de8-4bb8366cf5c0',
|
|
55
|
-
ownerId: '550e8400-e29b-41d4-a716-446655440000',
|
|
56
|
-
title: 'Ada Gist',
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
id: 'f104f457-b3fd-4643-87b9-d872c54b8a79',
|
|
60
|
-
ownerId: '0f31f6ba-6ff0-41ce-9f6b-8d132b6c4b81',
|
|
61
|
-
title: 'Grace Gist',
|
|
62
|
-
},
|
|
63
|
-
],
|
|
64
|
-
identity: 'id',
|
|
65
|
-
}
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
describe('deriveTrailExamples', () => {
|
|
69
|
-
test('prefers authored trail examples over contour-derived fixtures', () => {
|
|
70
|
-
const authoredExample = {
|
|
71
|
-
expected: { email: 'manual@example.com', name: 'Manual' },
|
|
72
|
-
input: { email: 'manual@example.com', name: 'Manual' },
|
|
73
|
-
name: 'Manual example',
|
|
74
|
-
} as const;
|
|
75
|
-
|
|
76
|
-
const trailDef = trail('user.manual', {
|
|
77
|
-
blaze: (input: { email: string; name: string }) => Result.ok(input),
|
|
78
|
-
contours: [userContour],
|
|
79
|
-
examples: [authoredExample],
|
|
80
|
-
input: z.object({ email: z.string().email(), name: z.string() }),
|
|
81
|
-
output: z.object({ email: z.string().email(), name: z.string() }),
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
expect(deriveTrailExamples(trailDef)).toEqual([authoredExample]);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test('derives single-contour fixtures and preserves full contour output', () => {
|
|
88
|
-
const firstUserExample = requireContourExample(userContour, 0);
|
|
89
|
-
|
|
90
|
-
const trailDef = trail('user.create', {
|
|
91
|
-
blaze: () => Result.ok(firstUserExample),
|
|
92
|
-
contours: [userContour],
|
|
93
|
-
input: userContour.pick({ email: true, name: true }),
|
|
94
|
-
output: userContour,
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const examples = deriveTrailExamples(trailDef);
|
|
98
|
-
expect(examples).toHaveLength(2);
|
|
99
|
-
const firstRecord = firstUserExample as Record<string, unknown>;
|
|
100
|
-
expect(examples[0]).toEqual(
|
|
101
|
-
expect.objectContaining({
|
|
102
|
-
expected: firstUserExample,
|
|
103
|
-
// Input is projected down to the keys `trail.input` declares, so
|
|
104
|
-
// only `email` and `name` (from `.pick`) survive.
|
|
105
|
-
input: {
|
|
106
|
-
email: firstRecord.email,
|
|
107
|
-
name: firstRecord.name,
|
|
108
|
-
},
|
|
109
|
-
})
|
|
110
|
-
);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('filters contour fixtures that do not satisfy the trail input schema', () => {
|
|
114
|
-
const trailDef = trail('user.slug-only', {
|
|
115
|
-
blaze: () => Result.ok({ slug: 'unused' }),
|
|
116
|
-
contours: [userContour],
|
|
117
|
-
input: z.object({ slug: z.string() }),
|
|
118
|
-
output: z.object({ slug: z.string() }),
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
expect(deriveTrailExamples(trailDef)).toEqual([]);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('matches cross-contour references and exposes contour-prefixed aliases', () => {
|
|
125
|
-
const trailDef = trail('gist.star', {
|
|
126
|
-
blaze: (input: { gistId: string; userId: string }) => Result.ok(input),
|
|
127
|
-
contours: [userContour, gistContour],
|
|
128
|
-
input: z.object({
|
|
129
|
-
gistId: gistContour.id(),
|
|
130
|
-
userId: userContour.id(),
|
|
131
|
-
}),
|
|
132
|
-
output: z.object({
|
|
133
|
-
gistId: gistContour.shape.id,
|
|
134
|
-
userId: userContour.shape.id,
|
|
135
|
-
}),
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const examples = deriveTrailExamples(trailDef);
|
|
139
|
-
expect(examples).toHaveLength(2);
|
|
140
|
-
// No contour fixture parses as the output schema on its own (the
|
|
141
|
-
// output expects `gistId` and `userId`, but the fixtures expose
|
|
142
|
-
// `id`, `ownerId`, etc.), so derived examples are left without an
|
|
143
|
-
// `expected` and fall back to schema-only validation at runtime.
|
|
144
|
-
expect(examples).toEqual([
|
|
145
|
-
{
|
|
146
|
-
input: {
|
|
147
|
-
gistId: '8f7ef40d-8234-4f73-8de8-4bb8366cf5c0',
|
|
148
|
-
userId: '550e8400-e29b-41d4-a716-446655440000',
|
|
149
|
-
},
|
|
150
|
-
name: expect.stringContaining('Derived fixture 1'),
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
input: {
|
|
154
|
-
gistId: 'f104f457-b3fd-4643-87b9-d872c54b8a79',
|
|
155
|
-
userId: '0f31f6ba-6ff0-41ce-9f6b-8d132b6c4b81',
|
|
156
|
-
},
|
|
157
|
-
name: expect.stringContaining('Derived fixture 2'),
|
|
158
|
-
},
|
|
159
|
-
]);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test('derives fixtures for strict input schemas by projecting to known keys', () => {
|
|
163
|
-
const firstUserExample = requireContourExample(userContour, 0);
|
|
164
|
-
|
|
165
|
-
const trailDef = trail('user.strict-create', {
|
|
166
|
-
blaze: (input: { email: string; name: string }) => Result.ok(input),
|
|
167
|
-
contours: [userContour],
|
|
168
|
-
input: z.object({ email: z.string().email(), name: z.string() }).strict(),
|
|
169
|
-
output: z.object({ email: z.string().email(), name: z.string() }),
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const examples = deriveTrailExamples(trailDef);
|
|
173
|
-
expect(examples).toHaveLength(2);
|
|
174
|
-
expect(examples[0]?.input).toEqual({
|
|
175
|
-
email: (firstUserExample as { email: string }).email,
|
|
176
|
-
name: (firstUserExample as { name: string }).name,
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test('does not infer expected from merged input when no contour fixture matches the output', () => {
|
|
181
|
-
// The output schema is a subset of the input shape, so the merged
|
|
182
|
-
// derived input would parse as the output if we tried to infer from
|
|
183
|
-
// it — but that inference is semantically wrong because input and
|
|
184
|
-
// output have distinct meanings. Since no contour fixture matches
|
|
185
|
-
// the output schema (userContour fixtures carry id+email+name, and
|
|
186
|
-
// the strict output only accepts email+name), `expected` must be
|
|
187
|
-
// omitted entirely.
|
|
188
|
-
const trailDef = trail('user.create-strict-output', {
|
|
189
|
-
blaze: (input: { email: string; name: string }) => Result.ok(input),
|
|
190
|
-
contours: [userContour],
|
|
191
|
-
input: z.object({ email: z.string().email(), name: z.string() }),
|
|
192
|
-
output: z
|
|
193
|
-
.object({ email: z.string().email(), name: z.string() })
|
|
194
|
-
.strict(),
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
const examples = deriveTrailExamples(trailDef);
|
|
198
|
-
expect(examples.length).toBeGreaterThan(0);
|
|
199
|
-
for (const example of examples) {
|
|
200
|
-
expect(example.expected).toBeUndefined();
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
});
|