@ontrails/testing 1.0.0-beta.3 → 1.0.0-beta.32

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 +507 -8
  2. package/README.md +86 -22
  3. package/package.json +34 -5
  4. package/src/all-established.ts +168 -0
  5. package/src/all.ts +51 -12
  6. package/src/assertions.ts +253 -0
  7. package/src/cli.ts +6 -0
  8. package/src/composes.ts +433 -0
  9. package/src/context.ts +200 -14
  10. package/src/contracts.ts +46 -35
  11. package/src/detours.ts +155 -18
  12. package/src/effective-examples.ts +414 -0
  13. package/src/errors.ts +47 -0
  14. package/src/examples.ts +302 -177
  15. package/src/harness-cli.ts +83 -58
  16. package/src/harness-http.ts +341 -0
  17. package/src/harness-mcp.ts +46 -16
  18. package/src/http.ts +10 -0
  19. package/src/index.ts +22 -14
  20. package/src/logger.ts +3 -1
  21. package/src/mcp.ts +6 -0
  22. package/src/scenario.ts +375 -0
  23. package/src/signals.ts +221 -0
  24. package/src/surface-parity.ts +389 -0
  25. package/src/trail.ts +1 -1
  26. package/src/types.ts +24 -52
  27. package/.turbo/turbo-build.log +0 -1
  28. package/.turbo/turbo-lint.log +0 -3
  29. package/.turbo/turbo-typecheck.log +0 -1
  30. package/dist/all.d.ts +0 -30
  31. package/dist/all.d.ts.map +0 -1
  32. package/dist/all.js +0 -47
  33. package/dist/all.js.map +0 -1
  34. package/dist/assertions.d.ts +0 -49
  35. package/dist/assertions.d.ts.map +0 -1
  36. package/dist/assertions.js +0 -84
  37. package/dist/assertions.js.map +0 -1
  38. package/dist/context.d.ts +0 -19
  39. package/dist/context.d.ts.map +0 -1
  40. package/dist/context.js +0 -33
  41. package/dist/context.js.map +0 -1
  42. package/dist/contracts.d.ts +0 -16
  43. package/dist/contracts.d.ts.map +0 -1
  44. package/dist/contracts.js +0 -66
  45. package/dist/contracts.js.map +0 -1
  46. package/dist/detours.d.ts +0 -12
  47. package/dist/detours.d.ts.map +0 -1
  48. package/dist/detours.js +0 -30
  49. package/dist/detours.js.map +0 -1
  50. package/dist/examples.d.ts +0 -22
  51. package/dist/examples.d.ts.map +0 -1
  52. package/dist/examples.js +0 -187
  53. package/dist/examples.js.map +0 -1
  54. package/dist/harness-cli.d.ts +0 -21
  55. package/dist/harness-cli.d.ts.map +0 -1
  56. package/dist/harness-cli.js +0 -213
  57. package/dist/harness-cli.js.map +0 -1
  58. package/dist/harness-mcp.d.ts +0 -21
  59. package/dist/harness-mcp.d.ts.map +0 -1
  60. package/dist/harness-mcp.js +0 -50
  61. package/dist/harness-mcp.js.map +0 -1
  62. package/dist/hike.d.ts +0 -32
  63. package/dist/hike.d.ts.map +0 -1
  64. package/dist/hike.js +0 -169
  65. package/dist/hike.js.map +0 -1
  66. package/dist/index.d.ts +0 -14
  67. package/dist/index.d.ts.map +0 -1
  68. package/dist/index.js +0 -16
  69. package/dist/index.js.map +0 -1
  70. package/dist/logger.d.ts +0 -15
  71. package/dist/logger.d.ts.map +0 -1
  72. package/dist/logger.js +0 -87
  73. package/dist/logger.js.map +0 -1
  74. package/dist/trail.d.ts +0 -20
  75. package/dist/trail.d.ts.map +0 -1
  76. package/dist/trail.js +0 -80
  77. package/dist/trail.js.map +0 -1
  78. package/dist/types.d.ts +0 -80
  79. package/dist/types.d.ts.map +0 -1
  80. package/dist/types.js +0 -5
  81. package/dist/types.js.map +0 -1
  82. package/src/__tests__/context.test.ts +0 -60
  83. package/src/__tests__/contracts.test.ts +0 -93
  84. package/src/__tests__/detours.test.ts +0 -55
  85. package/src/__tests__/examples.test.ts +0 -176
  86. package/src/__tests__/hike.test.ts +0 -164
  87. package/src/__tests__/logger.test.ts +0 -136
  88. package/src/__tests__/trail.test.ts +0 -99
  89. package/src/hike.ts +0 -283
  90. package/tsconfig.json +0 -9
  91. package/tsconfig.tsbuildinfo +0 -1
package/src/hike.ts DELETED
@@ -1,283 +0,0 @@
1
- /**
2
- * testHike — composition-aware scenario testing for hikes.
3
- *
4
- * Tests the composition graph: which trails were followed, in what order,
5
- * and supports failure injection from followed trail examples.
6
- */
7
-
8
- import { describe, expect, test } from 'bun:test';
9
-
10
- import type { AnyHike, AnyTrail, FollowFn, TrailContext } from '@ontrails/core';
11
- import {
12
- InternalError,
13
- Result,
14
- ValidationError,
15
- validateInput,
16
- } from '@ontrails/core';
17
-
18
- import {
19
- assertErrorMatch,
20
- assertFullMatch,
21
- assertSchemaMatch,
22
- expectOk,
23
- } from './assertions.js';
24
- import { mergeTestContext } from './context.js';
25
- import type { HikeScenario } from './types.js';
26
-
27
- // ---------------------------------------------------------------------------
28
- // Follow trace
29
- // ---------------------------------------------------------------------------
30
-
31
- interface FollowRecord {
32
- readonly id: string;
33
- readonly input: unknown;
34
- }
35
-
36
- // ---------------------------------------------------------------------------
37
- // Injection helpers
38
- // ---------------------------------------------------------------------------
39
-
40
- /**
41
- * Find an error example on a trail by name or description substring.
42
- */
43
- const findErrorExample = (
44
- trailDef: AnyTrail,
45
- description: string
46
- ): string | undefined => {
47
- const example = trailDef.examples?.find(
48
- (ex) =>
49
- ex.error !== undefined &&
50
- (ex.description?.includes(description) || ex.name.includes(description))
51
- );
52
- return example?.error;
53
- };
54
-
55
- /**
56
- * Try to inject an error from a followed trail's example.
57
- * Returns undefined when no injection is configured for this trail ID.
58
- */
59
- const tryInjectError = (
60
- id: string,
61
- scenario: HikeScenario,
62
- trailsMap: ReadonlyMap<string, AnyTrail> | undefined
63
- ): Result<unknown, Error> | undefined => {
64
- const injection = scenario.injectFromExample?.[id];
65
- if (injection === undefined) {
66
- return undefined;
67
- }
68
-
69
- const trailDef = trailsMap?.get(id);
70
- if (trailDef === undefined) {
71
- return Result.err(
72
- new InternalError(`Cannot inject: trail "${id}" not in topo`)
73
- );
74
- }
75
- const errorName = findErrorExample(trailDef, injection);
76
- if (errorName === undefined) {
77
- return Result.err(
78
- new InternalError(
79
- `No error example matching "${injection}" on trail "${id}"`
80
- )
81
- );
82
- }
83
- return Result.err(new Error(errorName));
84
- };
85
-
86
- /**
87
- * Execute a trail from the map, validating input first.
88
- */
89
- const executeFromMap = (
90
- id: string,
91
- input: unknown,
92
- trailsMap: ReadonlyMap<string, AnyTrail> | undefined,
93
- ctx: TrailContext
94
- ): Result<unknown, Error> | Promise<Result<unknown, Error>> | undefined => {
95
- const trailDef = trailsMap?.get(id);
96
- if (trailDef === undefined) {
97
- return undefined;
98
- }
99
-
100
- const validated = validateInput(trailDef.input, input);
101
- if (validated.isErr()) {
102
- return validated;
103
- }
104
- return trailDef.implementation(validated.value, ctx);
105
- };
106
-
107
- // ---------------------------------------------------------------------------
108
- // Follow factory
109
- // ---------------------------------------------------------------------------
110
-
111
- /**
112
- * Build a recording follow function that optionally injects errors.
113
- */
114
- const createRecordingFollow = (
115
- trace: FollowRecord[],
116
- scenario: HikeScenario,
117
- trailsMap: ReadonlyMap<string, AnyTrail> | undefined,
118
- baseFollow: FollowFn | undefined,
119
- ctx: TrailContext
120
- ): FollowFn => {
121
- // The generic O on FollowFn is erased at runtime; the cast is safe
122
- // because callers narrow via isOk/isErr before accessing the value.
123
- const follow = (id: string, input: unknown) => {
124
- trace.push({ id, input });
125
-
126
- const injected = tryInjectError(id, scenario, trailsMap);
127
- if (injected !== undefined) {
128
- return Promise.resolve(injected);
129
- }
130
-
131
- if (baseFollow !== undefined) {
132
- return baseFollow(id, input);
133
- }
134
-
135
- const executed = executeFromMap(id, input, trailsMap, ctx);
136
- if (executed !== undefined) {
137
- return Promise.resolve(executed);
138
- }
139
-
140
- return Promise.resolve(Result.ok());
141
- };
142
- return follow as FollowFn;
143
- };
144
-
145
- // ---------------------------------------------------------------------------
146
- // Scenario assertions
147
- // ---------------------------------------------------------------------------
148
-
149
- const assertScenarioResult = (
150
- result: Result<unknown, Error>,
151
- scenario: HikeScenario,
152
- hikeDef: AnyHike
153
- ): void => {
154
- if (scenario.expectValue !== undefined) {
155
- assertFullMatch(result, scenario.expectValue);
156
- } else if (scenario.expectErr !== undefined) {
157
- assertErrorMatch(result, scenario.expectErr, scenario.expectErrMessage);
158
- } else if (scenario.expectErrMessage !== undefined) {
159
- expect(result.isErr()).toBe(true);
160
- if (result.isErr()) {
161
- expect(result.error.message).toContain(scenario.expectErrMessage);
162
- }
163
- } else if (scenario.expectOk === true) {
164
- expect(result.isOk()).toBe(true);
165
- assertSchemaMatch(result, hikeDef.output);
166
- }
167
- };
168
-
169
- const assertFollowTrace = (
170
- trace: readonly FollowRecord[],
171
- scenario: HikeScenario
172
- ): void => {
173
- if (scenario.expectFollowed !== undefined) {
174
- const followedIds = trace.map((r) => r.id);
175
- expect(followedIds).toEqual([...scenario.expectFollowed]);
176
- }
177
- if (scenario.expectFollowedCount !== undefined) {
178
- const counts: Record<string, number> = {};
179
- for (const record of trace) {
180
- counts[record.id] = (counts[record.id] ?? 0) + 1;
181
- }
182
- expect(counts).toEqual({ ...scenario.expectFollowedCount });
183
- }
184
- };
185
-
186
- const handleValidationError = (
187
- validated: Result<unknown, Error>,
188
- scenario: HikeScenario
189
- ): boolean => {
190
- if (!validated.isErr()) {
191
- return false;
192
- }
193
- if (scenario.expectErr === ValidationError) {
194
- expect(validated.error).toBeInstanceOf(ValidationError);
195
- if (scenario.expectErrMessage !== undefined) {
196
- expect(validated.error.message).toContain(scenario.expectErrMessage);
197
- }
198
- return true;
199
- }
200
- throw new Error(
201
- `Input validation failed unexpectedly: ${validated.error.message}`
202
- );
203
- };
204
-
205
- // ---------------------------------------------------------------------------
206
- // Scenario runner
207
- // ---------------------------------------------------------------------------
208
-
209
- const buildTestContext = (
210
- scenario: HikeScenario,
211
- ctx: Partial<TrailContext> | undefined,
212
- trailsMap: ReadonlyMap<string, AnyTrail> | undefined
213
- ): { trace: FollowRecord[]; testCtx: TrailContext } => {
214
- const trace: FollowRecord[] = [];
215
- const baseCtx = mergeTestContext(ctx);
216
- const follow = createRecordingFollow(
217
- trace,
218
- scenario,
219
- trailsMap,
220
- baseCtx.follow,
221
- baseCtx
222
- );
223
- return { testCtx: { ...baseCtx, follow }, trace };
224
- };
225
-
226
- const runScenario = async (
227
- hikeDef: AnyHike,
228
- scenario: HikeScenario,
229
- ctx: Partial<TrailContext> | undefined,
230
- trailsMap: ReadonlyMap<string, AnyTrail> | undefined
231
- ): Promise<void> => {
232
- const validated = validateInput(hikeDef.input, scenario.input);
233
- if (handleValidationError(validated, scenario)) {
234
- return;
235
- }
236
-
237
- const { trace, testCtx } = buildTestContext(scenario, ctx, trailsMap);
238
- const result = await hikeDef.implementation(expectOk(validated), testCtx);
239
- assertFollowTrace(trace, scenario);
240
- assertScenarioResult(result, scenario, hikeDef);
241
- };
242
-
243
- // ---------------------------------------------------------------------------
244
- // testHike
245
- // ---------------------------------------------------------------------------
246
-
247
- /** Options for testHike that provide trail definitions for injection. */
248
- export interface TestHikeOptions {
249
- /** Partial context overrides. */
250
- readonly ctx?: Partial<TrailContext> | undefined;
251
- /** Map of trail ID to trail definition, used for injectFromExample. */
252
- readonly trails?: ReadonlyMap<string, AnyTrail> | undefined;
253
- }
254
-
255
- /**
256
- * Generate a describe block for a hike with one test per scenario.
257
- *
258
- * @example
259
- * ```ts
260
- * testHike(onboardHike, [
261
- * {
262
- * description: "follows add then relate",
263
- * input: { name: "Alpha" },
264
- * expectOk: true,
265
- * expectFollowed: ["entity.add", "entity.relate"],
266
- * },
267
- * ]);
268
- * ```
269
- */
270
- export const testHike = (
271
- hikeDef: AnyHike,
272
- scenarios: readonly HikeScenario[],
273
- options?: TestHikeOptions
274
- ): void => {
275
- describe(hikeDef.id, () => {
276
- test.each([...scenarios])(
277
- '$description',
278
- async (scenario: HikeScenario) => {
279
- await runScenario(hikeDef, scenario, options?.ctx, options?.trails);
280
- }
281
- );
282
- });
283
- };
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 +0,0 @@
1
- {"root":["./src/all.ts","./src/assertions.ts","./src/context.ts","./src/contracts.ts","./src/detours.ts","./src/examples.ts","./src/harness-cli.ts","./src/harness-mcp.ts","./src/hike.ts","./src/index.ts","./src/logger.ts","./src/trail.ts","./src/types.ts"],"version":"5.9.3"}