@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,254 +0,0 @@
|
|
|
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';
|
|
4
|
-
|
|
5
|
-
import { contour, Result, trail, topo } from '@ontrails/core';
|
|
6
|
-
import { connectDrizzle } from '@ontrails/drizzle';
|
|
7
|
-
import { z } from 'zod';
|
|
8
|
-
|
|
9
|
-
import { testAll } from '../all.js';
|
|
10
|
-
import { store as defineStore } from '@ontrails/store';
|
|
11
|
-
|
|
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
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
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',
|
|
92
|
-
examples: [
|
|
93
|
-
{
|
|
94
|
-
expected: { name: 'Alpha', source: 'mock' },
|
|
95
|
-
input: {},
|
|
96
|
-
name: 'Uses auto-resolved resource mock',
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
input: z.object({}),
|
|
100
|
-
output: z.object({ name: z.string(), source: z.string() }),
|
|
101
|
-
resources: [mockDbResource],
|
|
102
|
-
});
|
|
103
|
-
|
|
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
|
-
},
|
|
113
|
-
description: 'Trail that prefers explicit overrides over mock factories',
|
|
114
|
-
examples: [
|
|
115
|
-
{
|
|
116
|
-
expected: { name: 'Override', source: 'override' },
|
|
117
|
-
input: {},
|
|
118
|
-
name: 'Explicit resource override wins',
|
|
119
|
-
},
|
|
120
|
-
],
|
|
121
|
-
input: z.object({}),
|
|
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,
|
|
153
|
-
});
|
|
154
|
-
|
|
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', () => {
|
|
203
|
-
// eslint-disable-next-line jest/require-hook
|
|
204
|
-
testAll(
|
|
205
|
-
topo('test-all-resource-mock-app', {
|
|
206
|
-
mockDbResource,
|
|
207
|
-
mockedTrail,
|
|
208
|
-
} as Record<string, unknown>)
|
|
209
|
-
);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
describe('testAll explicit resource overrides', () => {
|
|
213
|
-
// eslint-disable-next-line jest/require-hook
|
|
214
|
-
testAll(
|
|
215
|
-
topo('test-all-resource-override-app', {
|
|
216
|
-
mockDbResource,
|
|
217
|
-
overrideTrail,
|
|
218
|
-
} as Record<string, unknown>),
|
|
219
|
-
() => ({
|
|
220
|
-
resources: {
|
|
221
|
-
'db.mock.all': createOverrideStore(),
|
|
222
|
-
},
|
|
223
|
-
})
|
|
224
|
-
);
|
|
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,170 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { Result, resource, topo } from '@ontrails/core';
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
createCrossContext,
|
|
7
|
-
createTestContext,
|
|
8
|
-
mergeTestContext,
|
|
9
|
-
createMockResources,
|
|
10
|
-
} from '../context.js';
|
|
11
|
-
import type { TestLogger } from '../types.js';
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// Tests
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
describe('createTestContext', () => {
|
|
18
|
-
test('produces a valid TrailContext with no args', () => {
|
|
19
|
-
const ctx = createTestContext();
|
|
20
|
-
expect(ctx.cwd).toBe(process.cwd());
|
|
21
|
-
expect(ctx.requestId).toBe('test-request-001');
|
|
22
|
-
expect(ctx.abortSignal).toBeDefined();
|
|
23
|
-
expect(ctx.abortSignal.aborted).toBe(false);
|
|
24
|
-
expect(ctx.logger).toBeDefined();
|
|
25
|
-
expect(ctx.trace).toBeDefined();
|
|
26
|
-
expect(ctx.workspaceRoot).toBe(process.cwd());
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('default logger is a TestLogger', () => {
|
|
30
|
-
const ctx = createTestContext();
|
|
31
|
-
const logger = ctx.logger as TestLogger;
|
|
32
|
-
expect(typeof logger.info).toBe('function');
|
|
33
|
-
expect(typeof logger.clear).toBe('function');
|
|
34
|
-
expect(typeof logger.find).toBe('function');
|
|
35
|
-
expect(typeof logger.assertLogged).toBe('function');
|
|
36
|
-
expect(logger.entries).toBeDefined();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test('default env is set to test', () => {
|
|
40
|
-
const ctx = createTestContext();
|
|
41
|
-
const env = ctx['env'] as Record<string, string>;
|
|
42
|
-
expect(env).toEqual({ TRAILS_ENV: 'test' });
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('overrides requestId', () => {
|
|
46
|
-
const ctx = createTestContext({ requestId: 'custom-id' });
|
|
47
|
-
expect(ctx.requestId).toBe('custom-id');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('overrides abortSignal', () => {
|
|
51
|
-
const controller = new AbortController();
|
|
52
|
-
controller.abort();
|
|
53
|
-
const ctx = createTestContext({ abortSignal: controller.signal });
|
|
54
|
-
expect(ctx.abortSignal.aborted).toBe(true);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('overrides cwd', () => {
|
|
58
|
-
const ctx = createTestContext({ cwd: '/tmp/test' });
|
|
59
|
-
expect(ctx.cwd).toBe('/tmp/test');
|
|
60
|
-
expect(ctx.workspaceRoot).toBe('/tmp/test');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('overrides env', () => {
|
|
64
|
-
const ctx = createTestContext({
|
|
65
|
-
env: { CUSTOM: '1', NODE_ENV: 'test' },
|
|
66
|
-
});
|
|
67
|
-
const env = ctx['env'] as Record<string, string>;
|
|
68
|
-
expect(env).toEqual({ CUSTOM: '1', NODE_ENV: 'test' });
|
|
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
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('createCrossContext', () => {
|
|
92
|
-
test('returns configured ok response for registered id', async () => {
|
|
93
|
-
const cross = createCrossContext({
|
|
94
|
-
responses: { 'entity.add': Result.ok({ id: '1', name: 'Alpha' }) },
|
|
95
|
-
});
|
|
96
|
-
const result = await cross('entity.add', { name: 'Alpha' });
|
|
97
|
-
expect(result.unwrap()).toEqual({ id: '1', name: 'Alpha' });
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('returns configured err response for registered id', async () => {
|
|
101
|
-
const err = new Error('conflict');
|
|
102
|
-
const cross = createCrossContext({
|
|
103
|
-
responses: { 'entity.add': Result.err(err) },
|
|
104
|
-
});
|
|
105
|
-
const result = await cross('entity.add', {});
|
|
106
|
-
expect(result.isErr()).toBe(true);
|
|
107
|
-
expect(result.error?.message).toBe('conflict');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test('returns err with descriptive message for unregistered id', async () => {
|
|
111
|
-
const cross = createCrossContext();
|
|
112
|
-
const result = await cross('unknown.trail', {});
|
|
113
|
-
expect(result.isErr()).toBe(true);
|
|
114
|
-
expect(result.error?.message).toContain('unknown.trail');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('no options defaults to empty responses', async () => {
|
|
118
|
-
const cross = createCrossContext();
|
|
119
|
-
const result = await cross('any.id', {});
|
|
120
|
-
expect(result.isErr()).toBe(true);
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe('mergeTestContext', () => {
|
|
125
|
-
test('merges resource overrides into extensions and the resource accessor', () => {
|
|
126
|
-
const ctx = mergeTestContext(
|
|
127
|
-
{
|
|
128
|
-
extensions: { existing: true },
|
|
129
|
-
},
|
|
130
|
-
{ 'db.main': { source: 'mock' } }
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
expect(ctx.extensions).toEqual({
|
|
134
|
-
'db.main': { source: 'mock' },
|
|
135
|
-
existing: true,
|
|
136
|
-
});
|
|
137
|
-
expect(ctx.resource<{ source: string }>('db.main').source).toBe('mock');
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
describe('createMockResources', () => {
|
|
142
|
-
test('creates fresh mock resources for each invocation', async () => {
|
|
143
|
-
let mockCalls = 0;
|
|
144
|
-
const mockable = resource(`resource.mock.${Bun.randomUUIDv7()}`, {
|
|
145
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
146
|
-
mock: () => {
|
|
147
|
-
mockCalls += 1;
|
|
148
|
-
return Promise.resolve({ source: 'mock' });
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
const app = topo('mock-app', { mockable } as Record<string, unknown>);
|
|
152
|
-
|
|
153
|
-
const first = await createMockResources(app);
|
|
154
|
-
const second = await createMockResources(app);
|
|
155
|
-
|
|
156
|
-
expect(first).toEqual({ [mockable.id]: { source: 'mock' } });
|
|
157
|
-
expect(second).toEqual(first);
|
|
158
|
-
expect(second[mockable.id]).not.toBe(first[mockable.id]);
|
|
159
|
-
expect(mockCalls).toBe(2);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test('skips resources without mock factories', async () => {
|
|
163
|
-
const plain = resource(`resource.plain.${Bun.randomUUIDv7()}`, {
|
|
164
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
165
|
-
});
|
|
166
|
-
const app = topo('plain-app', { plain } as Record<string, unknown>);
|
|
167
|
-
|
|
168
|
-
expect(await createMockResources(app)).toEqual({});
|
|
169
|
-
});
|
|
170
|
-
});
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { afterAll, describe, expect, mock, test } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { contour, Result, resource, trail, topo } from '@ontrails/core';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
|
|
6
|
-
import { testContracts } from '../contracts.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
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Test trails
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
/** Trail whose implementation matches the output schema. */
|
|
25
|
-
const validTrail = trail('valid', {
|
|
26
|
-
blaze: (input: { name: string }) => Result.ok({ id: 1, name: input.name }),
|
|
27
|
-
examples: [
|
|
28
|
-
{
|
|
29
|
-
expected: { id: 1, name: 'Alpha' },
|
|
30
|
-
input: { name: 'Alpha' },
|
|
31
|
-
name: 'Valid output',
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
input: z.object({ name: z.string() }),
|
|
35
|
-
output: z.object({ id: z.number(), name: z.string() }),
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
/** Trail without output schema -- should be skipped. */
|
|
39
|
-
const noSchemaTrail = trail('noschema', {
|
|
40
|
-
blaze: (input: { x: number }) => Result.ok(input.x * 2),
|
|
41
|
-
examples: [{ expected: 10, input: { x: 5 }, name: 'No schema' }],
|
|
42
|
-
input: z.object({ x: z.number() }),
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
/** Trail without examples -- should be skipped. */
|
|
46
|
-
const noExamplesTrail = trail('noexamples', {
|
|
47
|
-
blaze: (input: { x: number }) => Result.ok({ value: input.x }),
|
|
48
|
-
input: z.object({ x: z.number() }),
|
|
49
|
-
output: z.object({ value: z.number() }),
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// Composition trail
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
/** Composition trail whose implementation matches the output schema. */
|
|
57
|
-
const compositionTrail = trail('composition.valid', {
|
|
58
|
-
blaze: (input: { a: number; b: number }) =>
|
|
59
|
-
Result.ok({ total: input.a + input.b }),
|
|
60
|
-
crosses: ['valid'],
|
|
61
|
-
examples: [
|
|
62
|
-
{
|
|
63
|
-
expected: { total: 3 },
|
|
64
|
-
input: { a: 1, b: 2 },
|
|
65
|
-
name: 'Valid composition output',
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
input: z.object({ a: z.number(), b: z.number() }),
|
|
69
|
-
output: z.object({ total: z.number() }),
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
const transformedInputTrail = trail('contract.transformed', {
|
|
73
|
-
blaze: (input: { value: number }) => Result.ok({ value: input.value }),
|
|
74
|
-
examples: [
|
|
75
|
-
{
|
|
76
|
-
expected: { value: 2 },
|
|
77
|
-
input: { value: '1' },
|
|
78
|
-
name: 'Raw contract input is only transformed once',
|
|
79
|
-
},
|
|
80
|
-
],
|
|
81
|
-
input: z
|
|
82
|
-
.object({ value: z.string() })
|
|
83
|
-
.transform(({ value }) => ({ value: Number(value) + 1 })),
|
|
84
|
-
output: z.object({ value: z.number() }),
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const undeclaredContractDbResource = resource('db.undeclared.contracts', {
|
|
88
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
89
|
-
mock: () => ({ source: 'mock' }),
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const undeclaredContractTrail = trail('resource.undeclared.contracts', {
|
|
93
|
-
blaze: (_input, ctx) =>
|
|
94
|
-
Result.ok({
|
|
95
|
-
hasInjectedResource:
|
|
96
|
-
ctx.extensions?.[undeclaredContractDbResource.id] !== undefined,
|
|
97
|
-
}),
|
|
98
|
-
examples: [
|
|
99
|
-
{
|
|
100
|
-
expected: { hasInjectedResource: false },
|
|
101
|
-
input: {},
|
|
102
|
-
name: 'Undeclared resources are not preloaded into contract contexts',
|
|
103
|
-
},
|
|
104
|
-
],
|
|
105
|
-
input: z.object({}),
|
|
106
|
-
output: z.object({ hasInjectedResource: z.literal(false) }),
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const ctxOverrideContractResource = resource('db.mock.contracts', {
|
|
110
|
-
create: () => Result.ok({ source: 'factory' }),
|
|
111
|
-
mock: () => ({ source: 'mock' }),
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const ctxOverrideContractTrail = trail('resource.ctx.contracts', {
|
|
115
|
-
blaze: (_input, ctx) =>
|
|
116
|
-
Result.ok({ source: ctxOverrideContractResource.from(ctx).source }),
|
|
117
|
-
examples: [
|
|
118
|
-
{
|
|
119
|
-
expected: { source: 'ctx' },
|
|
120
|
-
input: {},
|
|
121
|
-
name: 'Context extensions beat auto-resolved contract resource mocks',
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
input: z.object({}),
|
|
125
|
-
output: z.object({ source: z.literal('ctx') }),
|
|
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,
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// ---------------------------------------------------------------------------
|
|
158
|
-
// Tests
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
|
|
161
|
-
describe('testContracts: valid output matches schema', () => {
|
|
162
|
-
// eslint-disable-next-line jest/require-hook
|
|
163
|
-
testContracts(topo('test-app', { validTrail } as Record<string, unknown>));
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe('testContracts: skips trails without output schemas', () => {
|
|
167
|
-
// eslint-disable-next-line jest/require-hook
|
|
168
|
-
testContracts(topo('test-app', { noSchemaTrail } as Record<string, unknown>));
|
|
169
|
-
|
|
170
|
-
test('no-op marker', () => {
|
|
171
|
-
// Trail without output schema is skipped -- no contract tests generated
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
describe('testContracts: skips trails without examples', () => {
|
|
176
|
-
// eslint-disable-next-line jest/require-hook
|
|
177
|
-
testContracts(
|
|
178
|
-
topo('test-app', { noExamplesTrail } as Record<string, unknown>)
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
test('no-op marker', () => {
|
|
182
|
-
// Trail without examples is skipped -- no contract tests generated
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
describe('testContracts: validates output schemas for trails with crossings', () => {
|
|
187
|
-
// eslint-disable-next-line jest/require-hook
|
|
188
|
-
testContracts(
|
|
189
|
-
topo('test-app', { compositionTrail } as Record<string, unknown>)
|
|
190
|
-
);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
describe('testContracts: raw transformed input', () => {
|
|
194
|
-
// eslint-disable-next-line jest/require-hook
|
|
195
|
-
testContracts(
|
|
196
|
-
topo('transformed-contract-app', {
|
|
197
|
-
transformedInputTrail,
|
|
198
|
-
} as Record<string, unknown>)
|
|
199
|
-
);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
describe('testContracts: context extension overrides', () => {
|
|
203
|
-
// eslint-disable-next-line jest/require-hook
|
|
204
|
-
testContracts(
|
|
205
|
-
topo('ctx-contract-app', {
|
|
206
|
-
ctxOverrideContractResource,
|
|
207
|
-
ctxOverrideContractTrail,
|
|
208
|
-
} as Record<string, unknown>),
|
|
209
|
-
{
|
|
210
|
-
ctx: {
|
|
211
|
-
extensions: { 'db.mock.contracts': { source: 'ctx' } },
|
|
212
|
-
},
|
|
213
|
-
}
|
|
214
|
-
);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
describe('testContracts resource declarations', () => {
|
|
218
|
-
// eslint-disable-next-line jest/require-hook
|
|
219
|
-
testContracts(
|
|
220
|
-
topo('undeclared-contract-resource-app', {
|
|
221
|
-
undeclaredContractDbResource,
|
|
222
|
-
undeclaredContractTrail,
|
|
223
|
-
} as Record<string, unknown>)
|
|
224
|
-
);
|
|
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
|
-
});
|