@ontrails/testing 1.0.0-beta.15 → 1.0.0-beta.16

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.
Files changed (91) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/README.md +2 -2
  3. package/package.json +15 -7
  4. package/src/all.ts +7 -7
  5. package/src/context.ts +3 -0
  6. package/src/contracts.ts +1 -16
  7. package/src/examples.ts +19 -11
  8. package/src/index.ts +1 -1
  9. package/src/logger.ts +3 -1
  10. package/src/scenario.ts +3 -5
  11. package/src/signals.ts +221 -0
  12. package/src/types.ts +1 -1
  13. package/.turbo/turbo-build.log +0 -1
  14. package/.turbo/turbo-lint.log +0 -3
  15. package/.turbo/turbo-typecheck.log +0 -1
  16. package/dist/all.d.ts +0 -36
  17. package/dist/all.d.ts.map +0 -1
  18. package/dist/all.js +0 -99
  19. package/dist/all.js.map +0 -1
  20. package/dist/assertions.d.ts +0 -72
  21. package/dist/assertions.d.ts.map +0 -1
  22. package/dist/assertions.js +0 -238
  23. package/dist/assertions.js.map +0 -1
  24. package/dist/context.d.ts +0 -76
  25. package/dist/context.d.ts.map +0 -1
  26. package/dist/context.js +0 -118
  27. package/dist/context.js.map +0 -1
  28. package/dist/contracts.d.ts +0 -17
  29. package/dist/contracts.d.ts.map +0 -1
  30. package/dist/contracts.js +0 -75
  31. package/dist/contracts.js.map +0 -1
  32. package/dist/crosses.d.ts +0 -38
  33. package/dist/crosses.d.ts.map +0 -1
  34. package/dist/crosses.js +0 -223
  35. package/dist/crosses.js.map +0 -1
  36. package/dist/detours.d.ts +0 -13
  37. package/dist/detours.d.ts.map +0 -1
  38. package/dist/detours.js +0 -109
  39. package/dist/detours.js.map +0 -1
  40. package/dist/effective-examples.d.ts +0 -30
  41. package/dist/effective-examples.d.ts.map +0 -1
  42. package/dist/effective-examples.js +0 -227
  43. package/dist/effective-examples.js.map +0 -1
  44. package/dist/examples.d.ts +0 -23
  45. package/dist/examples.d.ts.map +0 -1
  46. package/dist/examples.js +0 -240
  47. package/dist/examples.js.map +0 -1
  48. package/dist/harness-cli.d.ts +0 -21
  49. package/dist/harness-cli.d.ts.map +0 -1
  50. package/dist/harness-cli.js +0 -205
  51. package/dist/harness-cli.js.map +0 -1
  52. package/dist/harness-mcp.d.ts +0 -21
  53. package/dist/harness-mcp.d.ts.map +0 -1
  54. package/dist/harness-mcp.js +0 -54
  55. package/dist/harness-mcp.js.map +0 -1
  56. package/dist/index.d.ts +0 -18
  57. package/dist/index.d.ts.map +0 -1
  58. package/dist/index.js +0 -19
  59. package/dist/index.js.map +0 -1
  60. package/dist/logger.d.ts +0 -15
  61. package/dist/logger.d.ts.map +0 -1
  62. package/dist/logger.js +0 -87
  63. package/dist/logger.js.map +0 -1
  64. package/dist/scenario.d.ts +0 -37
  65. package/dist/scenario.d.ts.map +0 -1
  66. package/dist/scenario.js +0 -235
  67. package/dist/scenario.js.map +0 -1
  68. package/dist/trail.d.ts +0 -20
  69. package/dist/trail.d.ts.map +0 -1
  70. package/dist/trail.js +0 -80
  71. package/dist/trail.js.map +0 -1
  72. package/dist/types.d.ts +0 -113
  73. package/dist/types.d.ts.map +0 -1
  74. package/dist/types.js +0 -5
  75. package/dist/types.js.map +0 -1
  76. package/src/__tests__/all.test.ts +0 -254
  77. package/src/__tests__/context.test.ts +0 -170
  78. package/src/__tests__/contracts.test.ts +0 -239
  79. package/src/__tests__/crosses.test.ts +0 -587
  80. package/src/__tests__/detours.test.ts +0 -212
  81. package/src/__tests__/effective-examples.test.ts +0 -203
  82. package/src/__tests__/examples.test.ts +0 -636
  83. package/src/__tests__/harness-cli.test.ts +0 -90
  84. package/src/__tests__/harness-mcp.test.ts +0 -37
  85. package/src/__tests__/logger.test.ts +0 -136
  86. package/src/__tests__/partial-match.test.ts +0 -126
  87. package/src/__tests__/scenario.test.ts +0 -381
  88. package/src/__tests__/trail.test.ts +0 -99
  89. package/tsconfig.json +0 -9
  90. package/tsconfig.tests.json +0 -10
  91. package/tsconfig.tsbuildinfo +0 -1
@@ -1,381 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
-
3
- import type { TrailContext } from '@ontrails/core';
4
- import { resource, Result, trail, topo } from '@ontrails/core';
5
- import { z } from 'zod';
6
-
7
- import { errResultMatch, okResultMatch } from '../assertions.js';
8
- import { executeScenarioSteps, ref, scenario } from '../scenario.js';
9
- import type { ScenarioStep } from '../types.js';
10
-
11
- // ---------------------------------------------------------------------------
12
- // Test trails
13
- // ---------------------------------------------------------------------------
14
-
15
- const createTrail = trail('item.create', {
16
- blaze: (input: { name: string }) => Result.ok({ id: 'g1', name: input.name }),
17
- description: 'Create an item',
18
- input: z.object({ name: z.string() }),
19
- output: z.object({ id: z.string(), name: z.string() }),
20
- });
21
-
22
- const showTrail = trail('item.show', {
23
- blaze: (input: { id: string }) =>
24
- Result.ok({ found: true, id: input.id, name: 'Test' }),
25
- description: 'Show an item',
26
- input: z.object({ id: z.string() }),
27
- output: z.object({ found: z.boolean(), id: z.string(), name: z.string() }),
28
- });
29
-
30
- const failTrail = trail('item.fail', {
31
- blaze: () => Result.err(new Error('intentional failure')),
32
- description: 'Always fails',
33
- input: z.object({}),
34
- output: z.object({}),
35
- });
36
-
37
- /** A trail that uses ctx.cross() to delegate to item.create. */
38
- const createViaProxy = trail('item.create-via-proxy', {
39
- blaze: (input: { name: string }, ctx: TrailContext) => {
40
- const crossFn = ctx.cross;
41
- if (!crossFn) {
42
- return Promise.resolve(Result.err(new Error('ctx.cross is undefined')));
43
- }
44
- return crossFn(createTrail, input);
45
- },
46
- crosses: [createTrail],
47
- description: 'Delegates to item.create via ctx.cross()',
48
- input: z.object({ name: z.string() }),
49
- output: z.object({ id: z.string(), name: z.string() }),
50
- });
51
-
52
- /** A resource with a mock factory. */
53
- const db = resource<{ query: (sql: string) => string }>('db', {
54
- create: () => Result.err(new Error('not wired in tests')),
55
- mock: () => ({ query: (sql: string) => `mock:${sql}` }),
56
- });
57
-
58
- /** A trail that uses a resource via ctx.resource(). */
59
- const queryTrail = trail('item.query', {
60
- blaze: (_input: { sql: string }, ctx: TrailContext) => {
61
- const instance = db.from(ctx);
62
- return Result.ok({ result: instance.query(_input.sql) });
63
- },
64
- description: 'Run a query via the db resource',
65
- input: z.object({ sql: z.string() }),
66
- output: z.object({ result: z.string() }),
67
- resources: [db],
68
- });
69
-
70
- const app = topo('scenario-test-app', {
71
- createTrail,
72
- createViaProxy,
73
- db,
74
- failTrail,
75
- queryTrail,
76
- showTrail,
77
- } as Record<string, unknown>);
78
-
79
- const readySignal = 'ready';
80
- type ReadySignal = typeof readySignal;
81
-
82
- const concurrentBatchOutput = z.object({ results: z.array(z.unknown()) });
83
-
84
- const createReadyController = () => Promise.withResolvers<ReadySignal>();
85
-
86
- const requireCrossFn = (
87
- ctx: TrailContext
88
- ): NonNullable<TrailContext['cross']> => {
89
- expect(ctx.cross).toBeDefined();
90
- return ctx.cross as NonNullable<TrailContext['cross']>;
91
- };
92
-
93
- const waitForReadyPair = async (
94
- first: Promise<ReadySignal>,
95
- second: Promise<ReadySignal>
96
- ): Promise<void> => {
97
- await first;
98
- await second;
99
- };
100
-
101
- // ---------------------------------------------------------------------------
102
- // Tests
103
- // ---------------------------------------------------------------------------
104
-
105
- describe('ref()', () => {
106
- test('creates a RefToken with the given path', () => {
107
- const token = ref('create.id');
108
- expect(token).toEqual({ __ref: true, path: 'create.id' });
109
- });
110
- });
111
-
112
- describe('scenario()', () => {
113
- // oxlint-disable-next-line jest/require-hook -- scenario() registers describe/test blocks, not setup code
114
- scenario('basic two-step flow', app, [
115
- {
116
- as: 'created',
117
- cross: createTrail,
118
- input: { name: 'Hello' },
119
- },
120
- {
121
- cross: showTrail,
122
- expectedMatch: { found: true, id: 'g1' },
123
- input: { id: ref('created.id') },
124
- },
125
- ]);
126
-
127
- // oxlint-disable-next-line jest/require-hook -- scenario() registers describe/test blocks, not setup code
128
- scenario('ref resolves dot-path from prior step', app, [
129
- {
130
- as: 'original',
131
- cross: createTrail,
132
- expected: { id: 'g1', name: 'Test' },
133
- input: { name: 'Test' },
134
- },
135
- {
136
- cross: showTrail,
137
- input: { id: ref('original.id') },
138
- },
139
- ]);
140
-
141
- // oxlint-disable-next-line jest/require-hook -- scenario() registers describe/test blocks, not setup code
142
- scenario('expectedMatch on a step works', app, [
143
- {
144
- cross: createTrail,
145
- expectedMatch: { name: 'Partial' },
146
- input: { name: 'Partial' },
147
- },
148
- ]);
149
-
150
- // oxlint-disable-next-line jest/require-hook -- scenario() registers describe/test blocks, not setup code
151
- scenario('step that uses ctx.cross() receives a bound cross function', app, [
152
- {
153
- as: 'proxied',
154
- cross: createViaProxy,
155
- expectedMatch: { id: 'g1', name: 'CrossTest' },
156
- input: { name: 'CrossTest' },
157
- },
158
- ]);
159
-
160
- // Resource mock forwarding
161
- // oxlint-disable-next-line jest/require-hook -- scenario() registers describe/test blocks, not setup code
162
- scenario('step with resource receives mock from topo', app, [
163
- {
164
- cross: queryTrail,
165
- expected: { result: 'mock:SELECT 1' },
166
- input: { sql: 'SELECT 1' },
167
- },
168
- ]);
169
-
170
- // Step failure reporting
171
- describe('step failure reporting', () => {
172
- test('reports which step failed', async () => {
173
- // We can't use scenario() directly here because it registers
174
- // describe/test blocks. Instead, test the error message shape
175
- // by importing the internals or checking that the scenario
176
- // properly reports failures.
177
- // For now, verify that a failing trail in a scenario produces
178
- // an informative error.
179
- const { executeTrail } = await import('@ontrails/core');
180
- const result = await executeTrail(failTrail, {}, { topo: app });
181
- expect(result.isErr()).toBe(true);
182
- });
183
- });
184
-
185
- // Duplicate alias guard
186
- describe('duplicate alias guard', () => {
187
- test('throws on duplicate step alias', async () => {
188
- const steps: ScenarioStep[] = [
189
- { as: 'dup', cross: createTrail, input: { name: 'A' } },
190
- { as: 'dup', cross: createTrail, input: { name: 'B' } },
191
- ];
192
-
193
- await expect(executeScenarioSteps(app, steps)).rejects.toThrow(
194
- 'duplicate step alias "dup"'
195
- );
196
- });
197
- });
198
- });
199
-
200
- describe('executeScenarioSteps concurrent crossing support', () => {
201
- test('matches concurrent fan-out arrays with ok result helpers', async () => {
202
- const alphaTrail = trail('scenario.batch.alpha', {
203
- blaze: () => Result.ok({ label: 'alpha' }),
204
- input: z.object({}),
205
- output: z.object({ label: z.string() }),
206
- visibility: 'internal',
207
- });
208
- const betaTrail = trail('scenario.batch.beta', {
209
- blaze: () => Result.ok({ label: 'beta' }),
210
- input: z.object({}),
211
- output: z.object({ label: z.string() }),
212
- visibility: 'internal',
213
- });
214
- const fanoutTrail = trail('scenario.batch.fanout', {
215
- blaze: async (_input, ctx) => {
216
- const results = await requireCrossFn(ctx)([
217
- [alphaTrail, {}],
218
- [betaTrail, {}],
219
- ] as const);
220
- return Result.ok({ results });
221
- },
222
- crosses: [alphaTrail, betaTrail],
223
- input: z.object({}),
224
- output: concurrentBatchOutput,
225
- });
226
- const fanoutApp = topo('scenario-concurrent-fanout-app', {
227
- alphaTrail,
228
- betaTrail,
229
- fanoutTrail,
230
- } as Record<string, unknown>);
231
-
232
- await executeScenarioSteps(fanoutApp, [
233
- {
234
- cross: fanoutTrail,
235
- expectedMatch: {
236
- results: [
237
- okResultMatch({ label: 'alpha' }),
238
- okResultMatch({ label: 'beta' }),
239
- ],
240
- },
241
- input: {},
242
- },
243
- ]);
244
- });
245
-
246
- test('matches mixed ok/err batch results for partial failure scenarios', async () => {
247
- const successTrail = trail('scenario.batch.partial.success', {
248
- blaze: () => Result.ok({ label: 'success' }),
249
- input: z.object({}),
250
- output: z.object({ label: z.string() }),
251
- visibility: 'internal',
252
- });
253
- const failureTrail = trail('scenario.batch.partial.failure', {
254
- blaze: () => Result.err(new Error('branch failure')),
255
- input: z.object({}),
256
- output: z.object({}),
257
- visibility: 'internal',
258
- });
259
- const partialTrail = trail('scenario.batch.partial.root', {
260
- blaze: async (_input, ctx) => {
261
- const results = await requireCrossFn(ctx)([
262
- [successTrail, {}],
263
- [failureTrail, {}],
264
- ] as const);
265
- return Result.ok({ results });
266
- },
267
- crosses: [successTrail, failureTrail],
268
- input: z.object({}),
269
- output: concurrentBatchOutput,
270
- });
271
- const partialApp = topo('scenario-partial-failure-app', {
272
- failureTrail,
273
- partialTrail,
274
- successTrail,
275
- } as Record<string, unknown>);
276
-
277
- await executeScenarioSteps(partialApp, [
278
- {
279
- cross: partialTrail,
280
- expectedMatch: {
281
- results: [
282
- okResultMatch({ label: 'success' }),
283
- errResultMatch({ message: 'branch failure' }),
284
- ],
285
- },
286
- input: {},
287
- },
288
- ]);
289
- });
290
-
291
- test('respects concurrency limits for scenario ctx.cross batch flows', async () => {
292
- const slowStarted = createReadyController();
293
- const fastStarted = createReadyController();
294
- const releaseFirstBatch = createReadyController();
295
- const startedIds: string[] = [];
296
- const slowTrail = trail('scenario.batch.limited.slow', {
297
- blaze: async () => {
298
- startedIds.push('slow');
299
- slowStarted.resolve(readySignal);
300
- await releaseFirstBatch.promise;
301
- await Bun.sleep(20);
302
- return Result.ok({ id: 'slow' });
303
- },
304
- input: z.object({}),
305
- output: z.object({ id: z.string() }),
306
- visibility: 'internal',
307
- });
308
- const fastTrail = trail('scenario.batch.limited.fast', {
309
- blaze: async () => {
310
- startedIds.push('fast');
311
- fastStarted.resolve(readySignal);
312
- await releaseFirstBatch.promise;
313
- await Bun.sleep(1);
314
- return Result.ok({ id: 'fast' });
315
- },
316
- input: z.object({}),
317
- output: z.object({ id: z.string() }),
318
- visibility: 'internal',
319
- });
320
- const queuedTrail = trail('scenario.batch.limited.queued', {
321
- blaze: () => {
322
- startedIds.push('queued');
323
- return Result.ok({ id: 'queued' });
324
- },
325
- input: z.object({}),
326
- output: z.object({ id: z.string() }),
327
- visibility: 'internal',
328
- });
329
- const limitedTrail = trail('scenario.batch.limited.root', {
330
- blaze: async (_input, ctx) => {
331
- const run = requireCrossFn(ctx)(
332
- [
333
- [slowTrail, {}],
334
- [fastTrail, {}],
335
- [queuedTrail, {}],
336
- ] as const,
337
- { concurrency: 2 }
338
- );
339
- await waitForReadyPair(slowStarted.promise, fastStarted.promise);
340
- const startedBeforeRelease = [...startedIds];
341
- releaseFirstBatch.resolve(readySignal);
342
- const results = await run;
343
- return Result.ok({
344
- startedBeforeRelease,
345
- startedOverall: [...startedIds],
346
- successIds: results.flatMap((result) =>
347
- result.match({
348
- err: () => [] as string[],
349
- ok: (value) => [value.id],
350
- })
351
- ),
352
- });
353
- },
354
- crosses: [slowTrail, fastTrail, queuedTrail],
355
- input: z.object({}),
356
- output: z.object({
357
- startedBeforeRelease: z.array(z.string()),
358
- startedOverall: z.array(z.string()),
359
- successIds: z.array(z.string()),
360
- }),
361
- });
362
- const limitedApp = topo('scenario-concurrency-limited-app', {
363
- fastTrail,
364
- limitedTrail,
365
- queuedTrail,
366
- slowTrail,
367
- } as Record<string, unknown>);
368
-
369
- await executeScenarioSteps(limitedApp, [
370
- {
371
- cross: limitedTrail,
372
- expected: {
373
- startedBeforeRelease: ['slow', 'fast'],
374
- startedOverall: ['slow', 'fast', 'queued'],
375
- successIds: ['slow', 'fast', 'queued'],
376
- },
377
- input: {},
378
- },
379
- ]);
380
- });
381
- });
@@ -1,99 +0,0 @@
1
- import { describe } from 'bun:test';
2
-
3
- import { NotFoundError, Result, ValidationError, trail } from '@ontrails/core';
4
- import { z } from 'zod';
5
-
6
- import { testTrail } from '../trail.js';
7
-
8
- // ---------------------------------------------------------------------------
9
- // Test trails
10
- // ---------------------------------------------------------------------------
11
-
12
- const greetTrail = trail('greet', {
13
- blaze: (input: { name: string }) =>
14
- Result.ok({ greeting: `Hello, ${input.name}` }),
15
- input: z.object({ name: z.string() }),
16
- output: z.object({ greeting: z.string() }),
17
- });
18
-
19
- const failTrail = trail('fail', {
20
- blaze: (input: { id: string }) => {
21
- if (input.id === 'missing') {
22
- return Result.err(new NotFoundError('Not found: missing'));
23
- }
24
- return Result.ok({ id: input.id });
25
- },
26
- input: z.object({ id: z.string() }),
27
- });
28
-
29
- // ---------------------------------------------------------------------------
30
- // Tests
31
- //
32
- // Each call to testTrail registers describe/test blocks.
33
- // We call them at the describe scope so bun:test can discover them.
34
- // ---------------------------------------------------------------------------
35
-
36
- describe('testTrail: expectOk', () => {
37
- // eslint-disable-next-line jest/require-hook
38
- testTrail(greetTrail, [
39
- { description: 'valid greeting', expectOk: true, input: { name: 'Bob' } },
40
- ]);
41
- });
42
-
43
- describe('testTrail: expectValue', () => {
44
- // eslint-disable-next-line jest/require-hook
45
- testTrail(greetTrail, [
46
- {
47
- description: 'exact match',
48
- expectValue: { greeting: 'Hello, Charlie' },
49
- input: { name: 'Charlie' },
50
- },
51
- ]);
52
- });
53
-
54
- describe('testTrail: expectErr', () => {
55
- // eslint-disable-next-line jest/require-hook
56
- testTrail(failTrail, [
57
- {
58
- description: 'not found error',
59
- expectErr: NotFoundError,
60
- input: { id: 'missing' },
61
- },
62
- ]);
63
- });
64
-
65
- describe('testTrail: expectErrMessage', () => {
66
- // eslint-disable-next-line jest/require-hook
67
- testTrail(failTrail, [
68
- {
69
- description: 'error message contains substring',
70
- expectErr: NotFoundError,
71
- expectErrMessage: 'Not found',
72
- input: { id: 'missing' },
73
- },
74
- ]);
75
- });
76
-
77
- describe('testTrail: invalid input with ValidationError', () => {
78
- // eslint-disable-next-line jest/require-hook
79
- testTrail(greetTrail, [
80
- {
81
- description: 'invalid input caught by validation',
82
- expectErr: ValidationError,
83
- input: { name: 123 },
84
- },
85
- ]);
86
- });
87
-
88
- describe('testTrail: multiple scenarios', () => {
89
- // eslint-disable-next-line jest/require-hook
90
- testTrail(greetTrail, [
91
- { description: 'scenario 1', expectOk: true, input: { name: 'A' } },
92
- { description: 'scenario 2', expectOk: true, input: { name: 'B' } },
93
- {
94
- description: 'scenario 3',
95
- expectValue: { greeting: 'Hello, C' },
96
- input: { name: 'C' },
97
- },
98
- ]);
99
- });
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src"
6
- },
7
- "include": ["src"],
8
- "exclude": ["**/__tests__/**", "**/*.test.ts", "dist"]
9
- }
@@ -1,10 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "noEmit": true,
5
- "rootDir": "./src",
6
- "types": ["bun"]
7
- },
8
- "include": ["src/**/*.test.ts", "src/__tests__/**/*.ts"],
9
- "exclude": []
10
- }
@@ -1 +0,0 @@
1
- {"root":["./src/all.ts","./src/assertions.ts","./src/context.ts","./src/contracts.ts","./src/crosses.ts","./src/detours.ts","./src/effective-examples.ts","./src/examples.ts","./src/harness-cli.ts","./src/harness-mcp.ts","./src/index.ts","./src/logger.ts","./src/scenario.ts","./src/trail.ts","./src/types.ts"],"version":"5.9.3"}