@ontrails/testing 1.0.0-beta.18 → 1.0.0-beta.19
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 +51 -0
- package/README.md +31 -29
- package/package.json +24 -8
- package/src/all-established.ts +168 -0
- package/src/all.ts +3 -130
- package/src/assertions.ts +2 -2
- package/src/cli.ts +6 -0
- package/src/{crosses.ts → composes.ts} +87 -79
- package/src/context.ts +22 -21
- package/src/contracts.ts +19 -17
- package/src/effective-examples.ts +67 -3
- package/src/errors.ts +47 -0
- package/src/examples.ts +131 -118
- package/src/harness-cli.ts +35 -7
- package/src/harness-http.ts +87 -9
- package/src/harness-mcp.ts +30 -6
- package/src/http.ts +10 -0
- package/src/index.ts +5 -34
- package/src/mcp.ts +6 -0
- package/src/scenario.ts +67 -60
- package/src/surface-parity.ts +43 -10
- package/src/types.ts +11 -232
package/src/harness-mcp.ts
CHANGED
|
@@ -6,13 +6,35 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { deriveMcpTools } from '@ontrails/mcp';
|
|
9
|
-
import type { McpToolDefinition } from '@ontrails/mcp';
|
|
10
|
-
|
|
11
9
|
import type {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from '
|
|
10
|
+
DeriveMcpToolsOptions,
|
|
11
|
+
McpExtra,
|
|
12
|
+
McpToolDefinition,
|
|
13
|
+
} from '@ontrails/mcp';
|
|
14
|
+
import type { Topo } from '@ontrails/core';
|
|
15
|
+
|
|
16
|
+
/** Options for creating an MCP harness. */
|
|
17
|
+
export interface McpHarnessOptions extends DeriveMcpToolsOptions {
|
|
18
|
+
readonly extra?: Partial<McpExtra> | undefined;
|
|
19
|
+
readonly graph: Topo;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A test harness for MCP tools. */
|
|
23
|
+
export interface McpHarness {
|
|
24
|
+
/** Call an MCP tool by name with arguments. */
|
|
25
|
+
callTool(
|
|
26
|
+
name: string,
|
|
27
|
+
args: Record<string, unknown>
|
|
28
|
+
): Promise<McpHarnessResult>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The result of an MCP harness tool invocation. */
|
|
32
|
+
export interface McpHarnessResult {
|
|
33
|
+
readonly content: unknown;
|
|
34
|
+
readonly isError: boolean;
|
|
35
|
+
readonly meta?: Record<string, unknown> | undefined;
|
|
36
|
+
readonly structuredContent?: Record<string, unknown> | undefined;
|
|
37
|
+
}
|
|
16
38
|
|
|
17
39
|
// ---------------------------------------------------------------------------
|
|
18
40
|
// createMcpHarness
|
|
@@ -25,6 +47,8 @@ import type {
|
|
|
25
47
|
* that invokes tools directly without any transport boundary.
|
|
26
48
|
*
|
|
27
49
|
* ```ts
|
|
50
|
+
* import { createMcpHarness } from '@ontrails/testing/mcp';
|
|
51
|
+
*
|
|
28
52
|
* const harness = createMcpHarness({ graph });
|
|
29
53
|
* const result = await harness.callTool("myapp_entity_show", { name: "Alpha" });
|
|
30
54
|
* expect(result.isError).toBe(false);
|
package/src/http.ts
ADDED
package/src/index.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
// Contract-driven testing
|
|
2
2
|
export { testAll } from './all.js';
|
|
3
|
-
export { testAllEstablished } from './all.js';
|
|
4
3
|
export { testExamples } from './examples.js';
|
|
5
|
-
export {
|
|
4
|
+
export { testComposes } from './composes.js';
|
|
6
5
|
export { testTrail } from './trail.js';
|
|
7
6
|
export { testContracts } from './contracts.js';
|
|
8
7
|
export { testDetours } from './detours.js';
|
|
9
|
-
export {
|
|
10
|
-
runSurfaceParityExample,
|
|
11
|
-
testSurfaceParity,
|
|
12
|
-
} from './surface-parity.js';
|
|
13
8
|
|
|
14
9
|
// Assertions
|
|
15
10
|
export {
|
|
@@ -28,50 +23,26 @@ export { executeScenarioSteps, ref, scenario } from './scenario.js';
|
|
|
28
23
|
|
|
29
24
|
// Mock factories
|
|
30
25
|
export {
|
|
31
|
-
|
|
26
|
+
createComposeContext,
|
|
32
27
|
createTestContext,
|
|
33
28
|
defaultCreatePermit,
|
|
34
29
|
} from './context.js';
|
|
35
30
|
export { createTestLogger } from './logger.js';
|
|
36
31
|
|
|
37
|
-
// Surface harnesses
|
|
38
|
-
export { createCliHarness } from './harness-cli.js';
|
|
39
|
-
export { createHttpHarness } from './harness-http.js';
|
|
40
|
-
export { createMcpHarness } from './harness-mcp.js';
|
|
41
|
-
|
|
42
32
|
// Types
|
|
43
|
-
export type {
|
|
33
|
+
export type { CreateComposeContextOptions } from './context.js';
|
|
44
34
|
export type {
|
|
45
35
|
PermittedTrail,
|
|
46
36
|
MinimalPermit,
|
|
47
37
|
TestExecutionOptions,
|
|
48
38
|
} from './context.js';
|
|
49
|
-
export type {
|
|
39
|
+
export type { TestComposeOptions } from './composes.js';
|
|
50
40
|
|
|
51
41
|
export type {
|
|
52
|
-
|
|
42
|
+
ComposeScenario,
|
|
53
43
|
RefToken,
|
|
54
44
|
ScenarioStep,
|
|
55
|
-
TestAllEstablishedOptions,
|
|
56
45
|
TestScenario,
|
|
57
46
|
TestLogger,
|
|
58
47
|
TestTrailContextOptions,
|
|
59
|
-
CliHarness,
|
|
60
|
-
CliHarnessOptions,
|
|
61
|
-
CliHarnessResult,
|
|
62
|
-
HttpHarness,
|
|
63
|
-
HttpHarnessErrorBody,
|
|
64
|
-
HttpHarnessOptions,
|
|
65
|
-
HttpHarnessRequest,
|
|
66
|
-
HttpHarnessRequestOptions,
|
|
67
|
-
HttpHarnessResult,
|
|
68
|
-
HttpHarnessSuccessBody,
|
|
69
|
-
McpHarness,
|
|
70
|
-
McpHarnessOptions,
|
|
71
|
-
McpHarnessResult,
|
|
72
|
-
NormalizedSurfaceParityResult,
|
|
73
|
-
SurfaceParityExclusion,
|
|
74
|
-
SurfaceParityOptions,
|
|
75
|
-
SurfaceParitySurface,
|
|
76
48
|
} from './types.js';
|
|
77
|
-
export type { SurfaceParityComparison } from './surface-parity.js';
|
package/src/mcp.ts
ADDED
package/src/scenario.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Multi-step scenario runner for composition testing.
|
|
3
3
|
*
|
|
4
4
|
* Scenarios express multi-trail flows as structured data — arrays of steps
|
|
5
|
-
* with
|
|
5
|
+
* with compose-step references via `ref()`. Each step invokes a trail through
|
|
6
6
|
* the normal execution pipeline (validation, layers, blaze, Result).
|
|
7
7
|
*/
|
|
8
8
|
|
|
@@ -10,19 +10,20 @@ import { describe, test } from 'bun:test';
|
|
|
10
10
|
|
|
11
11
|
import type {
|
|
12
12
|
AnyTrail,
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
ComposeBatchOptions,
|
|
14
|
+
ComposeFn,
|
|
15
|
+
ExecuteTrailOptions,
|
|
15
16
|
ResourceOverrideMap,
|
|
16
17
|
Result,
|
|
17
18
|
Topo,
|
|
18
19
|
} from '@ontrails/core';
|
|
19
20
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
buildComposeValidationSchema,
|
|
22
|
+
claimNextComposeBatchIndex,
|
|
23
|
+
createComposeBatchValidationResults,
|
|
23
24
|
executeTrail,
|
|
24
25
|
InternalError,
|
|
25
|
-
|
|
26
|
+
normalizeComposeBatchConcurrency,
|
|
26
27
|
Result as R,
|
|
27
28
|
} from '@ontrails/core';
|
|
28
29
|
|
|
@@ -30,15 +31,18 @@ import { assertPartialMatch, expectOk } from './assertions.js';
|
|
|
30
31
|
import { createTestContext, createMockResources } from './context.js';
|
|
31
32
|
import type { RefToken, ScenarioStep } from './types.js';
|
|
32
33
|
|
|
33
|
-
type
|
|
34
|
-
type
|
|
34
|
+
type ScenarioComposeTarget = string | { readonly id: string };
|
|
35
|
+
type ScenarioComposeCall = readonly [ScenarioComposeTarget, unknown];
|
|
36
|
+
type TestingExecuteTrailOptions = ExecuteTrailOptions & {
|
|
37
|
+
readonly validationSchema?: ReturnType<typeof buildComposeValidationSchema>;
|
|
38
|
+
};
|
|
35
39
|
|
|
36
40
|
// ---------------------------------------------------------------------------
|
|
37
|
-
// ref() —
|
|
41
|
+
// ref() — compose-step reference marker
|
|
38
42
|
// ---------------------------------------------------------------------------
|
|
39
43
|
|
|
40
44
|
/**
|
|
41
|
-
* Create a reference marker for
|
|
45
|
+
* Create a reference marker for compose-step data in scenario inputs.
|
|
42
46
|
*
|
|
43
47
|
* `ref('create.id')` resolves to the `id` field of the step aliased as
|
|
44
48
|
* `create`. Dot-paths are supported for nested access.
|
|
@@ -46,8 +50,8 @@ type ScenarioCrossCall = readonly [ScenarioCrossTarget, unknown];
|
|
|
46
50
|
* @example
|
|
47
51
|
* ```typescript
|
|
48
52
|
* scenario('Fork flow', app, [
|
|
49
|
-
* {
|
|
50
|
-
* {
|
|
53
|
+
* { compose: createGist, input: { name: 'Hello' }, as: 'original' },
|
|
54
|
+
* { compose: forkGist, input: { id: ref('original.id') } },
|
|
51
55
|
* ]);
|
|
52
56
|
* ```
|
|
53
57
|
*/
|
|
@@ -152,8 +156,8 @@ export const deriveRefs = (
|
|
|
152
156
|
* @example
|
|
153
157
|
* ```typescript
|
|
154
158
|
* scenario('Create and show', app, [
|
|
155
|
-
* {
|
|
156
|
-
* {
|
|
159
|
+
* { compose: createItem, input: { name: 'Test' }, as: 'created' },
|
|
160
|
+
* { compose: showItem, input: { id: ref('created.id') },
|
|
157
161
|
* expectedMatch: { found: true } },
|
|
158
162
|
* ]);
|
|
159
163
|
* ```
|
|
@@ -176,10 +180,10 @@ const assertStepExpectations = async (
|
|
|
176
180
|
}
|
|
177
181
|
};
|
|
178
182
|
|
|
179
|
-
const
|
|
180
|
-
calls: readonly
|
|
183
|
+
const executeUnlimitedComposeBatch = async (
|
|
184
|
+
calls: readonly ScenarioComposeCall[],
|
|
181
185
|
runCall: (
|
|
182
|
-
call:
|
|
186
|
+
call: ScenarioComposeCall,
|
|
183
187
|
branchIndex: number
|
|
184
188
|
) => Promise<Result<unknown, Error>>
|
|
185
189
|
): Promise<Result<unknown, Error>[]> =>
|
|
@@ -187,10 +191,10 @@ const executeUnlimitedCrossBatch = async (
|
|
|
187
191
|
calls.map((call, branchIndex) => runCall(call, branchIndex))
|
|
188
192
|
);
|
|
189
193
|
|
|
190
|
-
const
|
|
191
|
-
calls: readonly
|
|
194
|
+
const executeLimitedComposeBatch = async (
|
|
195
|
+
calls: readonly ScenarioComposeCall[],
|
|
192
196
|
runCall: (
|
|
193
|
-
call:
|
|
197
|
+
call: ScenarioComposeCall,
|
|
194
198
|
branchIndex: number
|
|
195
199
|
) => Promise<Result<unknown, Error>>,
|
|
196
200
|
limit: number
|
|
@@ -200,20 +204,20 @@ const executeLimitedCrossBatch = async (
|
|
|
200
204
|
|
|
201
205
|
const runWorker = async () => {
|
|
202
206
|
while (true) {
|
|
203
|
-
const branchIndex =
|
|
207
|
+
const branchIndex = claimNextComposeBatchIndex(nextIndex, calls);
|
|
204
208
|
if (branchIndex === undefined) {
|
|
205
209
|
return;
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
const call = calls[branchIndex];
|
|
209
213
|
if (call === undefined) {
|
|
210
|
-
// Defensive: `
|
|
214
|
+
// Defensive: `claimNextComposeBatchIndex` only returns indices within
|
|
211
215
|
// bounds, so this slot should always be populated. If it ever isn't,
|
|
212
216
|
// surface a clear InternalError in place of the missing slot and keep
|
|
213
217
|
// the worker loop running so sibling branches still get processed.
|
|
214
218
|
results[branchIndex] = R.err(
|
|
215
219
|
new InternalError(
|
|
216
|
-
`unreachable: concurrent
|
|
220
|
+
`unreachable: concurrent compose batch call missing at index ${branchIndex}`
|
|
217
221
|
)
|
|
218
222
|
);
|
|
219
223
|
continue;
|
|
@@ -228,78 +232,81 @@ const executeLimitedCrossBatch = async (
|
|
|
228
232
|
};
|
|
229
233
|
|
|
230
234
|
/**
|
|
231
|
-
* Build a
|
|
232
|
-
* them through the standard pipeline. Mirrors the pattern in
|
|
235
|
+
* Build a compose function that resolves trails from the topo and executes
|
|
236
|
+
* them through the standard pipeline. Mirrors the pattern in composes.ts
|
|
233
237
|
* `executeFromMap` but without recording or injection.
|
|
234
238
|
*/
|
|
235
|
-
const
|
|
239
|
+
const createScenarioCompose = (
|
|
236
240
|
app: Topo,
|
|
237
241
|
resources?: ResourceOverrideMap
|
|
238
|
-
):
|
|
239
|
-
const
|
|
240
|
-
idOrTrail:
|
|
242
|
+
): ComposeFn => {
|
|
243
|
+
const invokeCompose = async (
|
|
244
|
+
idOrTrail: ScenarioComposeTarget,
|
|
241
245
|
input: unknown,
|
|
242
|
-
self:
|
|
246
|
+
self: ComposeFn
|
|
243
247
|
) => {
|
|
244
248
|
const id = typeof idOrTrail === 'string' ? idOrTrail : idOrTrail.id;
|
|
245
249
|
const trailDef: AnyTrail | undefined = app.get(id);
|
|
246
250
|
if (trailDef === undefined) {
|
|
247
|
-
return R.err(
|
|
251
|
+
return R.err(
|
|
252
|
+
new InternalError(`compose: trail "${id}" not found in topo`)
|
|
253
|
+
);
|
|
248
254
|
}
|
|
249
255
|
const baseCtx = createTestContext();
|
|
250
|
-
|
|
251
|
-
ctx: { ...baseCtx,
|
|
256
|
+
const options: TestingExecuteTrailOptions = {
|
|
257
|
+
ctx: { ...baseCtx, compose: self },
|
|
252
258
|
resources,
|
|
253
259
|
topo: app,
|
|
254
|
-
validationSchema:
|
|
255
|
-
}
|
|
260
|
+
validationSchema: buildComposeValidationSchema(trailDef),
|
|
261
|
+
};
|
|
262
|
+
return await executeTrail(trailDef, input, options);
|
|
256
263
|
};
|
|
257
264
|
|
|
258
|
-
const
|
|
259
|
-
calls: readonly
|
|
260
|
-
self:
|
|
261
|
-
options?:
|
|
265
|
+
const executeComposeBatch = async (
|
|
266
|
+
calls: readonly ScenarioComposeCall[],
|
|
267
|
+
self: ComposeFn,
|
|
268
|
+
options?: ComposeBatchOptions
|
|
262
269
|
): Promise<Result<unknown, Error>[]> => {
|
|
263
270
|
if (calls.length === 0) {
|
|
264
271
|
return [];
|
|
265
272
|
}
|
|
266
273
|
|
|
267
|
-
const concurrency =
|
|
274
|
+
const concurrency = normalizeComposeBatchConcurrency(options);
|
|
268
275
|
if (concurrency.isErr()) {
|
|
269
|
-
return
|
|
276
|
+
return createComposeBatchValidationResults(calls, concurrency.error);
|
|
270
277
|
}
|
|
271
278
|
|
|
272
279
|
const runCall = async (
|
|
273
|
-
[target, batchInput]:
|
|
280
|
+
[target, batchInput]: ScenarioComposeCall,
|
|
274
281
|
_branchIndex: number
|
|
275
|
-
) => await
|
|
282
|
+
) => await invokeCompose(target, batchInput, self);
|
|
276
283
|
|
|
277
284
|
const limit = concurrency.value ?? calls.length;
|
|
278
285
|
return limit >= calls.length
|
|
279
|
-
? await
|
|
280
|
-
: await
|
|
286
|
+
? await executeUnlimitedComposeBatch(calls, runCall)
|
|
287
|
+
: await executeLimitedComposeBatch(calls, runCall, limit);
|
|
281
288
|
};
|
|
282
289
|
|
|
283
|
-
const
|
|
284
|
-
idOrTrail:
|
|
290
|
+
const compose = async function compose(
|
|
291
|
+
idOrTrail: ScenarioComposeTarget | readonly ScenarioComposeCall[],
|
|
285
292
|
inputOrOptions?: unknown
|
|
286
293
|
) {
|
|
287
294
|
if (Array.isArray(idOrTrail)) {
|
|
288
|
-
return await
|
|
295
|
+
return await executeComposeBatch(
|
|
289
296
|
idOrTrail,
|
|
290
|
-
|
|
291
|
-
inputOrOptions as
|
|
297
|
+
compose as ComposeFn,
|
|
298
|
+
inputOrOptions as ComposeBatchOptions | undefined
|
|
292
299
|
);
|
|
293
300
|
}
|
|
294
301
|
|
|
295
|
-
return await
|
|
296
|
-
idOrTrail as
|
|
302
|
+
return await invokeCompose(
|
|
303
|
+
idOrTrail as ScenarioComposeTarget,
|
|
297
304
|
inputOrOptions,
|
|
298
|
-
|
|
305
|
+
compose as ComposeFn
|
|
299
306
|
);
|
|
300
|
-
} as
|
|
307
|
+
} as ComposeFn;
|
|
301
308
|
|
|
302
|
-
return
|
|
309
|
+
return compose;
|
|
303
310
|
};
|
|
304
311
|
|
|
305
312
|
/**
|
|
@@ -319,18 +326,18 @@ const executeStep = async (
|
|
|
319
326
|
);
|
|
320
327
|
}
|
|
321
328
|
|
|
322
|
-
const
|
|
329
|
+
const scenarioCompose = createScenarioCompose(app, resources);
|
|
323
330
|
const baseCtx = createTestContext();
|
|
324
331
|
const resolvedInput = deriveRefs(step.input, outputs);
|
|
325
|
-
const result = await executeTrail(step.
|
|
326
|
-
ctx: { ...baseCtx,
|
|
332
|
+
const result = await executeTrail(step.compose, resolvedInput, {
|
|
333
|
+
ctx: { ...baseCtx, compose: scenarioCompose },
|
|
327
334
|
resources,
|
|
328
335
|
topo: app,
|
|
329
336
|
});
|
|
330
337
|
|
|
331
338
|
if (result.isErr()) {
|
|
332
339
|
throw new Error(
|
|
333
|
-
`Step ${String(index + 1)} ("${step.as ?? step.
|
|
340
|
+
`Step ${String(index + 1)} ("${step.as ?? step.compose.id}") failed: ${result.error.message}`
|
|
334
341
|
);
|
|
335
342
|
}
|
|
336
343
|
|
package/src/surface-parity.ts
CHANGED
|
@@ -5,11 +5,18 @@
|
|
|
5
5
|
import { describe, expect, test } from 'bun:test';
|
|
6
6
|
|
|
7
7
|
import { deriveCliPath, filterSurfaceTrails } from '@ontrails/core';
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
ResourceOverrideMap,
|
|
10
|
+
Topo,
|
|
11
|
+
Trail,
|
|
12
|
+
TrailContext,
|
|
13
|
+
TrailExample,
|
|
14
|
+
} from '@ontrails/core';
|
|
9
15
|
import { deriveHttpInputSource, deriveHttpMethod } from '@ontrails/http';
|
|
10
16
|
import type { HttpMethod } from '@ontrails/http';
|
|
11
17
|
import { MCP_TOOL_ERROR_META_KEY, deriveToolName } from '@ontrails/mcp';
|
|
12
18
|
|
|
19
|
+
import type { TestAllEstablishedOptions } from './all-established.js';
|
|
13
20
|
import {
|
|
14
21
|
createMockResources,
|
|
15
22
|
defaultCreatePermit,
|
|
@@ -18,19 +25,45 @@ import {
|
|
|
18
25
|
} from './context.js';
|
|
19
26
|
import { deriveTrailExamples } from './effective-examples.js';
|
|
20
27
|
import { createCliHarness } from './harness-cli.js';
|
|
28
|
+
import type { CliHarnessResult } from './harness-cli.js';
|
|
21
29
|
import { createHttpHarness } from './harness-http.js';
|
|
30
|
+
import type { HttpHarnessResult } from './harness-http.js';
|
|
22
31
|
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';
|
|
32
|
+
import type { McpHarnessResult } from './harness-mcp.js';
|
|
31
33
|
|
|
32
34
|
type ParityTrail = Trail<unknown, unknown, unknown>;
|
|
33
35
|
|
|
36
|
+
export type SurfaceParitySurface = 'cli' | 'mcp' | 'http';
|
|
37
|
+
|
|
38
|
+
export interface SurfaceParityExclusion {
|
|
39
|
+
/** Optional example name. Omit to exclude every example for the trail. */
|
|
40
|
+
readonly example?: string | undefined;
|
|
41
|
+
/** Human-readable reason shown in the skipped test name. */
|
|
42
|
+
readonly reason: string;
|
|
43
|
+
/** Trail ID to exclude. */
|
|
44
|
+
readonly trailId: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SurfaceParityOptions extends TestAllEstablishedOptions {
|
|
48
|
+
readonly createResources?:
|
|
49
|
+
| (() => ResourceOverrideMap | Promise<ResourceOverrideMap>)
|
|
50
|
+
| undefined;
|
|
51
|
+
readonly exclusions?: readonly SurfaceParityExclusion[] | undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type NormalizedSurfaceParityResult =
|
|
55
|
+
| {
|
|
56
|
+
readonly ok: true;
|
|
57
|
+
readonly value: unknown;
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
readonly error: {
|
|
61
|
+
readonly category: string;
|
|
62
|
+
readonly code: string;
|
|
63
|
+
};
|
|
64
|
+
readonly ok: false;
|
|
65
|
+
};
|
|
66
|
+
|
|
34
67
|
export interface SurfaceParityComparison {
|
|
35
68
|
readonly cli: NormalizedSurfaceParityResult;
|
|
36
69
|
readonly http: NormalizedSurfaceParityResult;
|
|
@@ -308,7 +341,7 @@ const parityTrails = (app: Topo): readonly ParityTrail[] =>
|
|
|
308
341
|
*
|
|
309
342
|
* @example
|
|
310
343
|
* ```ts
|
|
311
|
-
* import { testSurfaceParity } from '@ontrails/testing';
|
|
344
|
+
* import { testSurfaceParity } from '@ontrails/testing/surface-parity';
|
|
312
345
|
* import { graph } from '../src/app.js';
|
|
313
346
|
*
|
|
314
347
|
* testSurfaceParity(graph);
|