@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.
Files changed (95) hide show
  1. package/CHANGELOG.md +70 -1
  2. package/README.md +47 -3
  3. package/package.json +16 -7
  4. package/src/all.ts +32 -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/harness-cli.ts +16 -3
  9. package/src/harness-http.ts +263 -0
  10. package/src/harness-mcp.ts +2 -0
  11. package/src/index.ts +18 -1
  12. package/src/logger.ts +3 -1
  13. package/src/scenario.ts +3 -5
  14. package/src/signals.ts +221 -0
  15. package/src/surface-parity.ts +356 -0
  16. package/src/types.ts +136 -1
  17. package/.turbo/turbo-build.log +0 -1
  18. package/.turbo/turbo-lint.log +0 -3
  19. package/.turbo/turbo-typecheck.log +0 -1
  20. package/dist/all.d.ts +0 -36
  21. package/dist/all.d.ts.map +0 -1
  22. package/dist/all.js +0 -99
  23. package/dist/all.js.map +0 -1
  24. package/dist/assertions.d.ts +0 -72
  25. package/dist/assertions.d.ts.map +0 -1
  26. package/dist/assertions.js +0 -238
  27. package/dist/assertions.js.map +0 -1
  28. package/dist/context.d.ts +0 -76
  29. package/dist/context.d.ts.map +0 -1
  30. package/dist/context.js +0 -118
  31. package/dist/context.js.map +0 -1
  32. package/dist/contracts.d.ts +0 -17
  33. package/dist/contracts.d.ts.map +0 -1
  34. package/dist/contracts.js +0 -75
  35. package/dist/contracts.js.map +0 -1
  36. package/dist/crosses.d.ts +0 -38
  37. package/dist/crosses.d.ts.map +0 -1
  38. package/dist/crosses.js +0 -223
  39. package/dist/crosses.js.map +0 -1
  40. package/dist/detours.d.ts +0 -13
  41. package/dist/detours.d.ts.map +0 -1
  42. package/dist/detours.js +0 -109
  43. package/dist/detours.js.map +0 -1
  44. package/dist/effective-examples.d.ts +0 -30
  45. package/dist/effective-examples.d.ts.map +0 -1
  46. package/dist/effective-examples.js +0 -227
  47. package/dist/effective-examples.js.map +0 -1
  48. package/dist/examples.d.ts +0 -23
  49. package/dist/examples.d.ts.map +0 -1
  50. package/dist/examples.js +0 -240
  51. package/dist/examples.js.map +0 -1
  52. package/dist/harness-cli.d.ts +0 -21
  53. package/dist/harness-cli.d.ts.map +0 -1
  54. package/dist/harness-cli.js +0 -205
  55. package/dist/harness-cli.js.map +0 -1
  56. package/dist/harness-mcp.d.ts +0 -21
  57. package/dist/harness-mcp.d.ts.map +0 -1
  58. package/dist/harness-mcp.js +0 -54
  59. package/dist/harness-mcp.js.map +0 -1
  60. package/dist/index.d.ts +0 -18
  61. package/dist/index.d.ts.map +0 -1
  62. package/dist/index.js +0 -19
  63. package/dist/index.js.map +0 -1
  64. package/dist/logger.d.ts +0 -15
  65. package/dist/logger.d.ts.map +0 -1
  66. package/dist/logger.js +0 -87
  67. package/dist/logger.js.map +0 -1
  68. package/dist/scenario.d.ts +0 -37
  69. package/dist/scenario.d.ts.map +0 -1
  70. package/dist/scenario.js +0 -235
  71. package/dist/scenario.js.map +0 -1
  72. package/dist/trail.d.ts +0 -20
  73. package/dist/trail.d.ts.map +0 -1
  74. package/dist/trail.js +0 -80
  75. package/dist/trail.js.map +0 -1
  76. package/dist/types.d.ts +0 -113
  77. package/dist/types.d.ts.map +0 -1
  78. package/dist/types.js +0 -5
  79. package/dist/types.js.map +0 -1
  80. package/src/__tests__/all.test.ts +0 -254
  81. package/src/__tests__/context.test.ts +0 -170
  82. package/src/__tests__/contracts.test.ts +0 -239
  83. package/src/__tests__/crosses.test.ts +0 -587
  84. package/src/__tests__/detours.test.ts +0 -212
  85. package/src/__tests__/effective-examples.test.ts +0 -203
  86. package/src/__tests__/examples.test.ts +0 -636
  87. package/src/__tests__/harness-cli.test.ts +0 -90
  88. package/src/__tests__/harness-mcp.test.ts +0 -37
  89. package/src/__tests__/logger.test.ts +0 -136
  90. package/src/__tests__/partial-match.test.ts +0 -126
  91. package/src/__tests__/scenario.test.ts +0 -381
  92. package/src/__tests__/trail.test.ts +0 -99
  93. package/tsconfig.json +0 -9
  94. package/tsconfig.tests.json +0 -10
  95. 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
- });