@ontrails/testing 0.2.0
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 +807 -0
- package/README.md +157 -0
- package/package.json +57 -0
- package/src/all-established.ts +168 -0
- package/src/all.ts +94 -0
- package/src/assertions.ts +361 -0
- package/src/cli.ts +6 -0
- package/src/composes.ts +433 -0
- package/src/context.ts +228 -0
- package/src/contracts.ts +109 -0
- package/src/detours.ts +181 -0
- package/src/effective-examples.ts +408 -0
- package/src/errors.ts +47 -0
- package/src/examples.ts +439 -0
- package/src/harness-cli.ts +335 -0
- package/src/harness-http.ts +341 -0
- package/src/harness-mcp.ts +98 -0
- package/src/http.ts +10 -0
- package/src/index.ts +48 -0
- package/src/logger.ts +127 -0
- package/src/mcp.ts +6 -0
- package/src/scenario.ts +375 -0
- package/src/signals.ts +221 -0
- package/src/surface-parity.ts +389 -0
- package/src/trail.ts +116 -0
- package/src/types.ts +89 -0
package/src/trail.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* testTrail — custom scenario testing for individual trails.
|
|
3
|
+
*
|
|
4
|
+
* Use this for edge cases, boundary values, and regression tests
|
|
5
|
+
* that don't belong in `examples` (which are agent-facing documentation).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from 'bun:test';
|
|
9
|
+
|
|
10
|
+
import type { AnyTrail, Result, TrailContext } from '@ontrails/core';
|
|
11
|
+
import { ValidationError, validateInput } from '@ontrails/core';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
assertErrorMatch,
|
|
15
|
+
assertFullMatch,
|
|
16
|
+
assertSchemaMatch,
|
|
17
|
+
expectOk,
|
|
18
|
+
} from './assertions.js';
|
|
19
|
+
import { mergeTestContext } from './context.js';
|
|
20
|
+
import type { TestScenario } from './types.js';
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Helpers
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const assertScenarioResult = (
|
|
27
|
+
result: Result<unknown, Error>,
|
|
28
|
+
scenario: TestScenario,
|
|
29
|
+
trailDef: AnyTrail
|
|
30
|
+
): void => {
|
|
31
|
+
if (scenario.expectValue !== undefined) {
|
|
32
|
+
assertFullMatch(result, scenario.expectValue);
|
|
33
|
+
} else if (scenario.expectErr !== undefined) {
|
|
34
|
+
assertErrorMatch(result, scenario.expectErr, scenario.expectErrMessage);
|
|
35
|
+
} else if (scenario.expectErrMessage !== undefined) {
|
|
36
|
+
expect(result.isErr()).toBe(true);
|
|
37
|
+
if (result.isErr()) {
|
|
38
|
+
expect(result.error.message).toContain(scenario.expectErrMessage);
|
|
39
|
+
}
|
|
40
|
+
} else if (scenario.expectOk === true) {
|
|
41
|
+
expect(result.isOk()).toBe(true);
|
|
42
|
+
assertSchemaMatch(result, trailDef.output);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Handle input validation failure for a scenario.
|
|
48
|
+
* Returns true if the error was expected and handled.
|
|
49
|
+
* Throws if the error was unexpected.
|
|
50
|
+
*/
|
|
51
|
+
const handleValidationError = (
|
|
52
|
+
validated: Result<unknown, Error>,
|
|
53
|
+
scenario: TestScenario
|
|
54
|
+
): boolean => {
|
|
55
|
+
if (!validated.isErr()) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (scenario.expectErr === ValidationError) {
|
|
60
|
+
expect(validated.error).toBeInstanceOf(ValidationError);
|
|
61
|
+
if (scenario.expectErrMessage !== undefined) {
|
|
62
|
+
expect(validated.error.message).toContain(scenario.expectErrMessage);
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Input validation failed unexpectedly: ${validated.error.message}`
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const runScenario = async (
|
|
73
|
+
trailDef: AnyTrail,
|
|
74
|
+
scenario: TestScenario,
|
|
75
|
+
ctx: Partial<TrailContext> | undefined
|
|
76
|
+
): Promise<void> => {
|
|
77
|
+
const testCtx = mergeTestContext(ctx);
|
|
78
|
+
const validated = validateInput(trailDef.input, scenario.input);
|
|
79
|
+
|
|
80
|
+
if (handleValidationError(validated, scenario)) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const validatedInput = expectOk(validated);
|
|
84
|
+
|
|
85
|
+
const result = await trailDef.implementation(validatedInput, testCtx);
|
|
86
|
+
assertScenarioResult(result, scenario, trailDef);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// testTrail
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate a describe block for a trail with one test per scenario.
|
|
95
|
+
*
|
|
96
|
+
* ```ts
|
|
97
|
+
* testTrail(myTrail, [
|
|
98
|
+
* { description: "valid input", input: { name: "Alpha" }, expectOk: true },
|
|
99
|
+
* { description: "missing name", input: {}, expectErr: ValidationError },
|
|
100
|
+
* ]);
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export const testTrail = (
|
|
104
|
+
trailDef: AnyTrail,
|
|
105
|
+
scenarios: readonly TestScenario[],
|
|
106
|
+
ctx?: Partial<TrailContext>
|
|
107
|
+
): void => {
|
|
108
|
+
describe(trailDef.id, () => {
|
|
109
|
+
test.each([...scenarios])(
|
|
110
|
+
'$description',
|
|
111
|
+
async (scenario: TestScenario) => {
|
|
112
|
+
await runScenario(trailDef, scenario, ctx);
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for @ontrails/testing.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AnyTrail, Logger, TraceFn } from '@ontrails/core';
|
|
6
|
+
import type { LogLevel, LogRecord } from '@ontrails/observability';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Test Scenario (for testTrail)
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/** A custom test scenario for a single trail. */
|
|
13
|
+
export interface TestScenario {
|
|
14
|
+
/** Description shown in test output. */
|
|
15
|
+
readonly description?: string | undefined;
|
|
16
|
+
/** Assert the result error has this message (substring match). */
|
|
17
|
+
readonly expectErrMessage?: string | undefined;
|
|
18
|
+
/** Assert the result is an error of this type. */
|
|
19
|
+
readonly expectErr?: (new (...args: never[]) => Error) | undefined;
|
|
20
|
+
/** Assert the result is ok. */
|
|
21
|
+
readonly expectOk?: boolean | undefined;
|
|
22
|
+
/** Assert the result value equals this. */
|
|
23
|
+
readonly expectValue?: unknown | undefined;
|
|
24
|
+
/** Input to pass to the implementation. */
|
|
25
|
+
readonly input: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Compose Scenario (for testComposes)
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/** A test scenario for a trail's composing graph. */
|
|
33
|
+
export interface ComposeScenario extends TestScenario {
|
|
34
|
+
/** Assert these trail IDs were composed, in order. */
|
|
35
|
+
readonly expectComposed?: readonly string[] | undefined;
|
|
36
|
+
/** Assert composing counts per trail ID. */
|
|
37
|
+
readonly expectComposedCount?: Readonly<Record<string, number>> | undefined;
|
|
38
|
+
/** Inject failure from a composed trail's example by description. */
|
|
39
|
+
readonly injectFromExample?: Readonly<Record<string, string>> | undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Test Logger
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/** A logger that captures entries for assertion in tests. */
|
|
47
|
+
export interface TestLogger extends Logger {
|
|
48
|
+
/** All log records captured during the test. */
|
|
49
|
+
readonly entries: readonly LogRecord[];
|
|
50
|
+
/** Clear captured entries. */
|
|
51
|
+
clear(): void;
|
|
52
|
+
/** Find entries matching a predicate. */
|
|
53
|
+
find(predicate: (record: LogRecord) => boolean): readonly LogRecord[];
|
|
54
|
+
/** Assert that at least one entry matches. */
|
|
55
|
+
assertLogged(level: LogLevel, messageSubstring: string): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Test Trail Context Options
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
/** Options for creating a test trail context. */
|
|
63
|
+
export interface TestTrailContextOptions {
|
|
64
|
+
readonly cwd?: string | undefined;
|
|
65
|
+
readonly env?: Record<string, string> | undefined;
|
|
66
|
+
readonly logger?: Logger | undefined;
|
|
67
|
+
readonly requestId?: string | undefined;
|
|
68
|
+
readonly abortSignal?: AbortSignal | undefined;
|
|
69
|
+
readonly trace?: TraceFn | undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Scenario (for composition testing)
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/** Marker for compose-step references in scenario inputs. */
|
|
77
|
+
export interface RefToken {
|
|
78
|
+
readonly __ref: true;
|
|
79
|
+
readonly path: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A single step in a scenario. */
|
|
83
|
+
export interface ScenarioStep {
|
|
84
|
+
readonly compose: AnyTrail;
|
|
85
|
+
readonly input: Record<string, unknown>;
|
|
86
|
+
readonly as?: string | undefined;
|
|
87
|
+
readonly expected?: unknown | undefined;
|
|
88
|
+
readonly expectedMatch?: unknown | undefined;
|
|
89
|
+
}
|