@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
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Example-driven parity checks across shipped surfaces.
3
+ */
4
+
5
+ import { describe, expect, test } from 'bun:test';
6
+
7
+ import { deriveCliPath, filterSurfaceTrails } from '@ontrails/core';
8
+ import type { Topo, Trail, TrailContext, TrailExample } from '@ontrails/core';
9
+ import { deriveHttpInputSource, deriveHttpMethod } from '@ontrails/http';
10
+ import type { HttpMethod } from '@ontrails/http';
11
+ import { MCP_TOOL_ERROR_META_KEY, deriveToolName } from '@ontrails/mcp';
12
+
13
+ import {
14
+ createMockResources,
15
+ defaultCreatePermit,
16
+ mergeResourceOverrides,
17
+ mergeTestContext,
18
+ } from './context.js';
19
+ import { deriveTrailExamples } from './effective-examples.js';
20
+ import { createCliHarness } from './harness-cli.js';
21
+ import { createHttpHarness } from './harness-http.js';
22
+ import { createMcpHarness } from './harness-mcp.js';
23
+ import type {
24
+ CliHarnessResult,
25
+ HttpHarnessResult,
26
+ McpHarnessResult,
27
+ NormalizedSurfaceParityResult,
28
+ SurfaceParityExclusion,
29
+ SurfaceParityOptions,
30
+ } from './types.js';
31
+
32
+ type ParityTrail = Trail<unknown, unknown, unknown>;
33
+
34
+ export interface SurfaceParityComparison {
35
+ readonly cli: NormalizedSurfaceParityResult;
36
+ readonly http: NormalizedSurfaceParityResult;
37
+ readonly mcp: NormalizedSurfaceParityResult;
38
+ }
39
+
40
+ const httpPathForTrail = (trailId: string): string =>
41
+ `/${trailId.replaceAll('.', '/')}`;
42
+
43
+ const escapeWhitespaceForCliToken = (value: string): string =>
44
+ value.replaceAll(/\s/gu, (char) => {
45
+ let escaped = '';
46
+
47
+ for (let index = 0; index < char.length; index += 1) {
48
+ const codePoint = char.codePointAt(index);
49
+
50
+ if (codePoint !== undefined) {
51
+ escaped += `\\u${codePoint.toString(16).padStart(4, '0')}`;
52
+ }
53
+ }
54
+
55
+ return escaped;
56
+ });
57
+
58
+ const cliCommandForExample = (
59
+ trail: ParityTrail,
60
+ example: TrailExample<unknown, unknown>
61
+ ): string => {
62
+ const path = deriveCliPath(trail.id).join(' ');
63
+ const inputJson = escapeWhitespaceForCliToken(
64
+ JSON.stringify(example.input ?? {})
65
+ );
66
+ return `${path} --input-json ${inputJson} --output json`;
67
+ };
68
+
69
+ const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
70
+ value !== null && typeof value === 'object' && !Array.isArray(value);
71
+
72
+ const readTextContent = (content: unknown): string | undefined => {
73
+ if (!Array.isArray(content)) {
74
+ return undefined;
75
+ }
76
+ const firstText = content.find(
77
+ (item): item is { readonly text: string; readonly type: string } =>
78
+ isObjectRecord(item) &&
79
+ item['type'] === 'text' &&
80
+ typeof item['text'] === 'string'
81
+ );
82
+ return firstText?.text;
83
+ };
84
+
85
+ const parseJsonText = (text: string | undefined): unknown => {
86
+ if (text === undefined) {
87
+ return undefined;
88
+ }
89
+ try {
90
+ return JSON.parse(text);
91
+ } catch {
92
+ return text;
93
+ }
94
+ };
95
+
96
+ const structuredContentValue = (
97
+ structuredContent: Record<string, unknown> | undefined
98
+ ): unknown => {
99
+ if (structuredContent === undefined) {
100
+ return undefined;
101
+ }
102
+ const keys = Object.keys(structuredContent);
103
+ return keys.length === 1 && keys[0] === 'data'
104
+ ? structuredContent['data']
105
+ : structuredContent;
106
+ };
107
+
108
+ const normalizeCliResult = (
109
+ result: CliHarnessResult
110
+ ): NormalizedSurfaceParityResult =>
111
+ result.exitCode === 0
112
+ ? { ok: true, value: result.json }
113
+ : {
114
+ error: {
115
+ category: result.error?.category ?? 'internal',
116
+ code: result.error?.code ?? 'InternalError',
117
+ },
118
+ ok: false,
119
+ };
120
+
121
+ const normalizeHttpResult = (
122
+ result: HttpHarnessResult
123
+ ): NormalizedSurfaceParityResult =>
124
+ result.ok
125
+ ? { ok: true, value: result.data }
126
+ : {
127
+ error: {
128
+ category: result.error?.category ?? 'internal',
129
+ code: result.error?.code ?? 'InternalError',
130
+ },
131
+ ok: false,
132
+ };
133
+
134
+ const readMcpError = (
135
+ result: McpHarnessResult
136
+ ): { readonly category: string; readonly code: string } => {
137
+ const errorMeta = result.meta?.[MCP_TOOL_ERROR_META_KEY];
138
+ if (isObjectRecord(errorMeta)) {
139
+ return {
140
+ category:
141
+ typeof errorMeta['category'] === 'string'
142
+ ? errorMeta['category']
143
+ : 'internal',
144
+ code:
145
+ typeof errorMeta['name'] === 'string'
146
+ ? errorMeta['name']
147
+ : 'InternalError',
148
+ };
149
+ }
150
+ return { category: 'internal', code: 'InternalError' };
151
+ };
152
+
153
+ const normalizeMcpResult = (
154
+ result: McpHarnessResult
155
+ ): NormalizedSurfaceParityResult =>
156
+ result.isError
157
+ ? { error: readMcpError(result), ok: false }
158
+ : {
159
+ ok: true,
160
+ value:
161
+ result.structuredContent === undefined
162
+ ? parseJsonText(readTextContent(result.content))
163
+ : structuredContentValue(result.structuredContent),
164
+ };
165
+
166
+ const applyAutoPermit = (
167
+ ctx: TrailContext,
168
+ trail: ParityTrail,
169
+ options: SurfaceParityOptions
170
+ ): TrailContext => {
171
+ if (options.strictPermits || ctx.permit !== undefined) {
172
+ return ctx;
173
+ }
174
+ const permit = (options.createPermit ?? defaultCreatePermit)(trail);
175
+ return permit === undefined ? ctx : { ...ctx, permit };
176
+ };
177
+
178
+ const createInvocationContext = async (
179
+ app: Topo,
180
+ trail: ParityTrail,
181
+ options: SurfaceParityOptions
182
+ ) => {
183
+ const autoResources =
184
+ options.createResources === undefined
185
+ ? await createMockResources(app)
186
+ : await options.createResources();
187
+ const resources = mergeResourceOverrides(
188
+ autoResources,
189
+ options.ctx,
190
+ options.resources
191
+ );
192
+ const ctx = applyAutoPermit(mergeTestContext(options.ctx), trail, options);
193
+ return { ctx, resources };
194
+ };
195
+
196
+ const mergeSurfaceContext = (
197
+ ctx: TrailContext,
198
+ surfaceCtx: Partial<TrailContext> | undefined
199
+ ): TrailContext =>
200
+ surfaceCtx === undefined ? ctx : mergeTestContext({ ...ctx, ...surfaceCtx });
201
+
202
+ const runCliExample = async (
203
+ app: Topo,
204
+ trail: ParityTrail,
205
+ example: TrailExample<unknown, unknown>,
206
+ options: SurfaceParityOptions
207
+ ): Promise<NormalizedSurfaceParityResult> => {
208
+ const { ctx, resources } = await createInvocationContext(app, trail, options);
209
+ const cliOptions = options.cli;
210
+ const harness = createCliHarness({
211
+ graph: app,
212
+ ...cliOptions,
213
+ ctx: mergeSurfaceContext(ctx, cliOptions?.ctx),
214
+ resources,
215
+ });
216
+ return normalizeCliResult(
217
+ await harness.run(cliCommandForExample(trail, example))
218
+ );
219
+ };
220
+
221
+ const runMcpExample = async (
222
+ app: Topo,
223
+ trail: ParityTrail,
224
+ example: TrailExample<unknown, unknown>,
225
+ options: SurfaceParityOptions
226
+ ): Promise<NormalizedSurfaceParityResult> => {
227
+ const { ctx, resources } = await createInvocationContext(app, trail, options);
228
+ const mcpOptions = options.mcp;
229
+ const harness = createMcpHarness({
230
+ graph: app,
231
+ ...mcpOptions,
232
+ createContext: async () =>
233
+ mergeSurfaceContext(ctx, await mcpOptions?.createContext?.()),
234
+ resources,
235
+ });
236
+ const toolName = deriveToolName(app.name, trail.id);
237
+ return normalizeMcpResult(
238
+ await harness.callTool(
239
+ toolName,
240
+ isObjectRecord(example.input) ? example.input : {}
241
+ )
242
+ );
243
+ };
244
+
245
+ const httpRequestForExample = (
246
+ method: HttpMethod,
247
+ trailId: string,
248
+ example: TrailExample<unknown, unknown>
249
+ ) => {
250
+ const input = isObjectRecord(example.input) ? example.input : {};
251
+ return deriveHttpInputSource(method) === 'query'
252
+ ? { method, path: httpPathForTrail(trailId), query: input }
253
+ : { body: input, method, path: httpPathForTrail(trailId) };
254
+ };
255
+
256
+ const runHttpExample = async (
257
+ app: Topo,
258
+ trail: ParityTrail,
259
+ example: TrailExample<unknown, unknown>,
260
+ options: SurfaceParityOptions
261
+ ): Promise<NormalizedSurfaceParityResult> => {
262
+ const { ctx, resources } = await createInvocationContext(app, trail, options);
263
+ const httpOptions = options.http;
264
+ const harness = createHttpHarness({
265
+ graph: app,
266
+ ...httpOptions,
267
+ ctx: mergeSurfaceContext(ctx, httpOptions?.ctx),
268
+ resources,
269
+ });
270
+ const method = deriveHttpMethod(trail.intent);
271
+ return normalizeHttpResult(
272
+ await harness.request(httpRequestForExample(method, trail.id, example))
273
+ );
274
+ };
275
+
276
+ export const runSurfaceParityExample = async (
277
+ app: Topo,
278
+ trail: ParityTrail,
279
+ example: TrailExample<unknown, unknown>,
280
+ options: SurfaceParityOptions = {}
281
+ ): Promise<SurfaceParityComparison> => {
282
+ // CLI harness output capture is process-scoped, so keep surface execution
283
+ // ordered even though MCP and HTTP do not share that constraint.
284
+ const cli = await runCliExample(app, trail, example, options);
285
+ const mcp = await runMcpExample(app, trail, example, options);
286
+ const http = await runHttpExample(app, trail, example, options);
287
+ return { cli, http, mcp };
288
+ };
289
+
290
+ const findExclusion = (
291
+ exclusions: readonly SurfaceParityExclusion[] | undefined,
292
+ trail: ParityTrail,
293
+ example: TrailExample<unknown, unknown>
294
+ ): SurfaceParityExclusion | undefined =>
295
+ exclusions?.find(
296
+ (exclusion) =>
297
+ exclusion.trailId === trail.id &&
298
+ (exclusion.example === undefined || exclusion.example === example.name)
299
+ );
300
+
301
+ const parityTrails = (app: Topo): readonly ParityTrail[] =>
302
+ filterSurfaceTrails(app.list()).filter(
303
+ (trail) => deriveTrailExamples(trail).length > 0
304
+ );
305
+
306
+ /**
307
+ * Register example-driven parity tests for CLI, MCP, and HTTP.
308
+ *
309
+ * @example
310
+ * ```ts
311
+ * import { testSurfaceParity } from '@ontrails/testing';
312
+ * import { graph } from '../src/app.js';
313
+ *
314
+ * testSurfaceParity(graph);
315
+ * ```
316
+ */
317
+ export const testSurfaceParity = (
318
+ app: Topo,
319
+ optionsOrFactory?:
320
+ | SurfaceParityOptions
321
+ | (() => SurfaceParityOptions | undefined)
322
+ ): void => {
323
+ const resolveOptions =
324
+ typeof optionsOrFactory === 'function'
325
+ ? optionsOrFactory
326
+ : () => optionsOrFactory;
327
+
328
+ describe('surface parity', () => {
329
+ for (const trail of parityTrails(app)) {
330
+ describe(trail.id, () => {
331
+ for (const example of deriveTrailExamples(trail)) {
332
+ const options = resolveOptions() ?? {};
333
+ const exclusion = findExclusion(options.exclusions, trail, example);
334
+ const testName = `example: ${example.name}`;
335
+ if (exclusion !== undefined) {
336
+ test.skip(`${testName} (excluded: ${exclusion.reason})`, () => {
337
+ throw new Error('Skipped parity exclusion should not execute');
338
+ });
339
+ continue;
340
+ }
341
+
342
+ test(testName, async () => {
343
+ const comparison = await runSurfaceParityExample(
344
+ app,
345
+ trail,
346
+ example,
347
+ resolveOptions() ?? {}
348
+ );
349
+ expect(comparison.mcp).toEqual(comparison.cli);
350
+ expect(comparison.http).toEqual(comparison.cli);
351
+ });
352
+ }
353
+ });
354
+ }
355
+ });
356
+ };
package/src/types.ts CHANGED
@@ -3,15 +3,21 @@
3
3
  */
4
4
 
5
5
  import type { DeriveCliCommandsOptions } from '@ontrails/cli';
6
+ import type {
7
+ DeriveHttpRoutesOptions,
8
+ HttpHeaderSource,
9
+ HttpMethod,
10
+ } from '@ontrails/http';
6
11
  import type { McpExtra, DeriveMcpToolsOptions } from '@ontrails/mcp';
7
12
  import type {
8
13
  AnyTrail,
9
14
  Logger,
15
+ ResourceOverrideMap,
10
16
  Topo,
11
17
  TraceFn,
12
18
  TrailContext,
13
19
  } from '@ontrails/core';
14
- import type { LogLevel, LogRecord } from '@ontrails/logging';
20
+ import type { LogLevel, LogRecord } from '@ontrails/observe';
15
21
 
16
22
  // ---------------------------------------------------------------------------
17
23
  // Test Scenario (for testTrail)
@@ -98,6 +104,13 @@ export interface CliHarness {
98
104
 
99
105
  /** The result of a CLI harness command execution. */
100
106
  export interface CliHarnessResult {
107
+ readonly error?:
108
+ | {
109
+ readonly category: string;
110
+ readonly code: string;
111
+ readonly message: string;
112
+ }
113
+ | undefined;
101
114
  readonly exitCode: number;
102
115
  /** Parsed JSON output if --output json was used. */
103
116
  readonly json?: unknown | undefined;
@@ -128,8 +141,129 @@ export interface McpHarness {
128
141
  export interface McpHarnessResult {
129
142
  readonly content: unknown;
130
143
  readonly isError: boolean;
144
+ readonly meta?: Record<string, unknown> | undefined;
145
+ readonly structuredContent?: Record<string, unknown> | undefined;
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // HTTP Harness
150
+ // ---------------------------------------------------------------------------
151
+
152
+ /** Options for creating an HTTP harness. */
153
+ export interface HttpHarnessOptions extends DeriveHttpRoutesOptions {
154
+ readonly ctx?: Partial<TrailContext> | undefined;
155
+ readonly graph: Topo;
131
156
  }
132
157
 
158
+ export interface HttpHarnessRequest {
159
+ readonly abortSignal?: AbortSignal | undefined;
160
+ readonly body?: unknown | undefined;
161
+ readonly headers?: HttpHeaderSource | undefined;
162
+ readonly method: HttpMethod;
163
+ readonly path: string;
164
+ readonly query?: Record<string, unknown> | undefined;
165
+ readonly requestId?: string | undefined;
166
+ }
167
+
168
+ export interface HttpHarnessRequestOptions extends Omit<
169
+ HttpHarnessRequest,
170
+ 'body' | 'method' | 'path' | 'query'
171
+ > {
172
+ readonly query?: Record<string, unknown> | undefined;
173
+ }
174
+
175
+ /** A test harness for HTTP route projections. */
176
+ export interface HttpHarness {
177
+ /** Execute a raw HTTP-style harness request. */
178
+ request(request: HttpHarnessRequest): Promise<HttpHarnessResult>;
179
+ /** Execute a GET request, reading input from query params. */
180
+ get(
181
+ path: string,
182
+ query?: Record<string, unknown>,
183
+ options?: HttpHarnessRequestOptions
184
+ ): Promise<HttpHarnessResult>;
185
+ /** Execute a POST request, reading input from the JSON-like body value. */
186
+ post(
187
+ path: string,
188
+ body?: unknown,
189
+ options?: HttpHarnessRequestOptions
190
+ ): Promise<HttpHarnessResult>;
191
+ /** Execute a PUT request. */
192
+ put(
193
+ path: string,
194
+ body?: unknown,
195
+ options?: HttpHarnessRequestOptions
196
+ ): Promise<HttpHarnessResult>;
197
+ /** Execute a PATCH request. */
198
+ patch(
199
+ path: string,
200
+ body?: unknown,
201
+ options?: HttpHarnessRequestOptions
202
+ ): Promise<HttpHarnessResult>;
203
+ /** Execute a DELETE request. */
204
+ delete(
205
+ path: string,
206
+ body?: unknown,
207
+ options?: HttpHarnessRequestOptions
208
+ ): Promise<HttpHarnessResult>;
209
+ }
210
+
211
+ export interface HttpHarnessErrorBody {
212
+ readonly error: {
213
+ readonly category: string;
214
+ readonly code: string;
215
+ readonly message: string;
216
+ };
217
+ }
218
+
219
+ export interface HttpHarnessSuccessBody {
220
+ readonly data: unknown;
221
+ }
222
+
223
+ /** The result of an HTTP harness request. */
224
+ export interface HttpHarnessResult {
225
+ readonly body: HttpHarnessErrorBody | HttpHarnessSuccessBody;
226
+ readonly data?: unknown | undefined;
227
+ readonly error?: HttpHarnessErrorBody['error'] | undefined;
228
+ readonly ok: boolean;
229
+ readonly status: number;
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // Surface parity
234
+ // ---------------------------------------------------------------------------
235
+
236
+ export type SurfaceParitySurface = 'cli' | 'mcp' | 'http';
237
+
238
+ export interface SurfaceParityExclusion {
239
+ /** Optional example name. Omit to exclude every example for the trail. */
240
+ readonly example?: string | undefined;
241
+ /** Human-readable reason shown in the skipped test name. */
242
+ readonly reason: string;
243
+ /** Trail ID to exclude. */
244
+ readonly trailId: string;
245
+ }
246
+
247
+ export interface SurfaceParityOptions extends TestAllEstablishedOptions {
248
+ readonly createResources?:
249
+ | (() => ResourceOverrideMap | Promise<ResourceOverrideMap>)
250
+ | undefined;
251
+ readonly exclusions?: readonly SurfaceParityExclusion[] | undefined;
252
+ }
253
+
254
+ export type NormalizedSurfaceParityResult =
255
+ | {
256
+ readonly ok: true;
257
+ readonly value: unknown;
258
+ }
259
+ | {
260
+ readonly error: {
261
+ readonly category: string;
262
+ readonly code: string;
263
+ };
264
+ readonly ok: false;
265
+ };
266
+
133
267
  // ---------------------------------------------------------------------------
134
268
  // Established verification
135
269
  // ---------------------------------------------------------------------------
@@ -150,6 +284,7 @@ export interface TestAllEstablishedOptions {
150
284
  | undefined)
151
285
  | undefined;
152
286
  readonly ctx?: Partial<TrailContext> | undefined;
287
+ readonly http?: Omit<HttpHarnessOptions, 'graph'> | undefined;
153
288
  readonly mcp?: Omit<McpHarnessOptions, 'graph'> | undefined;
154
289
  readonly resources?: Record<string, unknown> | undefined;
155
290
  readonly strictPermits?: boolean | undefined;
@@ -1 +0,0 @@
1
- $ tsc -b
@@ -1,3 +0,0 @@
1
- $ oxlint ./src
2
- Found 0 warnings and 0 errors.
3
- Finished in 81ms on 28 files with 93 rules using 24 threads.
@@ -1 +0,0 @@
1
- $ tsc --noEmit
package/dist/all.d.ts DELETED
@@ -1,36 +0,0 @@
1
- /**
2
- * testAll — single-line governance suite for any Topo.
3
- *
4
- * Wraps topo validation, example execution, contract checks, and detour
5
- * contract validation into one describe block.
6
- */
7
- import type { Topo, TrailContext } from '@ontrails/core';
8
- import type { TestExecutionOptions } from './context.js';
9
- import type { TestAllEstablishedOptions } from './types.js';
10
- /**
11
- * Run the full governance test suite for a Topo.
12
- *
13
- * Generates a `governance` describe block containing:
14
- * - Structural validation via `validateTopo`
15
- * - Example execution via `testExamples`
16
- * - Output contract checks via `testContracts`
17
- * - Detour contract validation via `testDetours`
18
- *
19
- * Accepts either a static context or a factory function that produces a
20
- * fresh context per test (useful when the context contains mutable state
21
- * like an in-memory store).
22
- *
23
- * @example
24
- * ```ts
25
- * import { testAll } from '@ontrails/testing';
26
- * import { graph } from '../src/app.js';
27
- *
28
- * testAll(graph);
29
- * ```
30
- */
31
- type TestAllInput = Partial<TrailContext> | TestExecutionOptions | (() => Partial<TrailContext> | TestExecutionOptions);
32
- export declare const testAll: (topo: Topo, ctxOrFactory?: TestAllInput) => void;
33
- type EstablishedInput = Partial<TrailContext> | TestAllEstablishedOptions | (() => Partial<TrailContext> | TestAllEstablishedOptions);
34
- export declare const testAllEstablished: (topo: Topo, optionsOrFactory?: EstablishedInput) => void;
35
- export {};
36
- //# sourceMappingURL=all.d.ts.map
package/dist/all.d.ts.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"all.d.ts","sourceRoot":"","sources":["../src/all.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAMzD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGzD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,KAAK,YAAY,GACb,OAAO,CAAC,YAAY,CAAC,GACrB,oBAAoB,GACpB,CAAC,MAAM,OAAO,CAAC,YAAY,CAAC,GAAG,oBAAoB,CAAC,CAAC;AAkDzD,eAAO,MAAM,OAAO,GAAI,MAAM,IAAI,EAAE,eAAe,YAAY,KAAG,IAEjE,CAAC;AAEF,KAAK,gBAAgB,GACjB,OAAO,CAAC,YAAY,CAAC,GACrB,yBAAyB,GACzB,CAAC,MAAM,OAAO,CAAC,YAAY,CAAC,GAAG,yBAAyB,CAAC,CAAC;AA+E9D,eAAO,MAAM,kBAAkB,GAC7B,MAAM,IAAI,EACV,mBAAmB,gBAAgB,KAClC,IAYF,CAAC"}
package/dist/all.js DELETED
@@ -1,99 +0,0 @@
1
- /**
2
- * testAll — single-line governance suite for any Topo.
3
- *
4
- * Wraps topo validation, example execution, contract checks, and detour
5
- * contract validation into one describe block.
6
- */
7
- import { describe, expect, test } from 'bun:test';
8
- import { validateEstablishedTopo, validateTopo } from '@ontrails/core';
9
- import { createCliHarness } from './harness-cli.js';
10
- import { createMcpHarness } from './harness-mcp.js';
11
- import { testContracts } from './contracts.js';
12
- import { testDetours } from './detours.js';
13
- import { testExamples } from './examples.js';
14
- const formatValidationFailure = (error) => {
15
- const issues = error.context?.issues;
16
- if (issues === undefined || issues.length === 0) {
17
- return error.message;
18
- }
19
- const details = issues.map((issue) => {
20
- const id = typeof issue['id'] === 'string' ? issue['id'] : undefined;
21
- const message = typeof issue['message'] === 'string' ? issue['message'] : undefined;
22
- const rule = typeof issue['rule'] === 'string' ? issue['rule'] : undefined;
23
- return [rule, id, message].filter(Boolean).join(': ');
24
- });
25
- return [error.message, ...details].join('\n');
26
- };
27
- const assertValidTopo = (result) => {
28
- if (result.isErr()) {
29
- throw new Error(formatValidationFailure(result.error));
30
- }
31
- };
32
- const registerGovernanceSuite = (topo, ctxOrFactory, validate) => {
33
- describe('governance', () => {
34
- test('topo validates', () => {
35
- expect(() => assertValidTopo(validate(topo))).not.toThrow();
36
- });
37
- // oxlint-disable-next-line jest/require-hook -- these generate describe/test blocks, not setup code
38
- testExamples(topo, ctxOrFactory);
39
- // oxlint-disable-next-line jest/require-hook -- these generate describe/test blocks, not setup code
40
- testContracts(topo, ctxOrFactory);
41
- // oxlint-disable-next-line jest/require-hook -- these generate describe/test blocks, not setup code
42
- testDetours(topo);
43
- });
44
- };
45
- export const testAll = (topo, ctxOrFactory) => {
46
- registerGovernanceSuite(topo, ctxOrFactory, validateTopo);
47
- };
48
- const isEstablishedOptions = (input) => input !== undefined &&
49
- (Object.hasOwn(input, 'cli') ||
50
- Object.hasOwn(input, 'createPermit') ||
51
- Object.hasOwn(input, 'ctx') ||
52
- Object.hasOwn(input, 'mcp') ||
53
- Object.hasOwn(input, 'resources') ||
54
- Object.hasOwn(input, 'strictPermits'));
55
- const normalizeEstablishedOptions = (input) => isEstablishedOptions(input) ? input : { ctx: input };
56
- const toExecutionOptions = (options) => ({
57
- ...(options.createPermit === undefined
58
- ? {}
59
- : { createPermit: options.createPermit }),
60
- ...(options.ctx === undefined ? {} : { ctx: options.ctx }),
61
- ...(options.resources === undefined ? {} : { resources: options.resources }),
62
- ...(options.strictPermits === undefined
63
- ? {}
64
- : { strictPermits: options.strictPermits }),
65
- });
66
- const toCliHarnessOptions = (topo, options) => {
67
- const cliOptions = {
68
- graph: topo,
69
- ...options.cli,
70
- };
71
- if (options.ctx !== undefined) {
72
- cliOptions.ctx = options.ctx;
73
- }
74
- return cliOptions;
75
- };
76
- const toMcpHarnessOptions = (topo, options) => ({
77
- graph: topo,
78
- ...options.mcp,
79
- });
80
- const registerEstablishedSurfaceSuite = (topo, resolveInput) => {
81
- describe('surfaces', () => {
82
- test('CLI projection validates established topo', () => {
83
- const options = normalizeEstablishedOptions(resolveInput());
84
- expect(() => createCliHarness(toCliHarnessOptions(topo, options))).not.toThrow();
85
- });
86
- test('MCP projection validates established topo', () => {
87
- const options = normalizeEstablishedOptions(resolveInput());
88
- expect(() => createMcpHarness(toMcpHarnessOptions(topo, options))).not.toThrow();
89
- });
90
- });
91
- };
92
- export const testAllEstablished = (topo, optionsOrFactory) => {
93
- const resolveInput = typeof optionsOrFactory === 'function'
94
- ? optionsOrFactory
95
- : () => optionsOrFactory;
96
- registerGovernanceSuite(topo, () => toExecutionOptions(normalizeEstablishedOptions(resolveInput())), validateEstablishedTopo);
97
- registerEstablishedSurfaceSuite(topo, resolveInput);
98
- };
99
- //# sourceMappingURL=all.js.map
package/dist/all.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"all.js","sourceRoot":"","sources":["../src/all.ts"],"names":[],"mappings":"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"}