@ontrails/testing 1.0.0-beta.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.
Files changed (80) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/.turbo/turbo-lint.log +3 -0
  3. package/.turbo/turbo-typecheck.log +1 -0
  4. package/CHANGELOG.md +23 -0
  5. package/README.md +221 -0
  6. package/dist/all.d.ts +30 -0
  7. package/dist/all.d.ts.map +1 -0
  8. package/dist/all.js +47 -0
  9. package/dist/all.js.map +1 -0
  10. package/dist/assertions.d.ts +49 -0
  11. package/dist/assertions.d.ts.map +1 -0
  12. package/dist/assertions.js +84 -0
  13. package/dist/assertions.js.map +1 -0
  14. package/dist/context.d.ts +19 -0
  15. package/dist/context.d.ts.map +1 -0
  16. package/dist/context.js +33 -0
  17. package/dist/context.js.map +1 -0
  18. package/dist/contracts.d.ts +16 -0
  19. package/dist/contracts.d.ts.map +1 -0
  20. package/dist/contracts.js +56 -0
  21. package/dist/contracts.js.map +1 -0
  22. package/dist/detours.d.ts +12 -0
  23. package/dist/detours.d.ts.map +1 -0
  24. package/dist/detours.js +30 -0
  25. package/dist/detours.js.map +1 -0
  26. package/dist/examples.d.ts +22 -0
  27. package/dist/examples.d.ts.map +1 -0
  28. package/dist/examples.js +187 -0
  29. package/dist/examples.js.map +1 -0
  30. package/dist/harness-cli.d.ts +21 -0
  31. package/dist/harness-cli.d.ts.map +1 -0
  32. package/dist/harness-cli.js +213 -0
  33. package/dist/harness-cli.js.map +1 -0
  34. package/dist/harness-mcp.d.ts +21 -0
  35. package/dist/harness-mcp.d.ts.map +1 -0
  36. package/dist/harness-mcp.js +50 -0
  37. package/dist/harness-mcp.js.map +1 -0
  38. package/dist/hike.d.ts +32 -0
  39. package/dist/hike.d.ts.map +1 -0
  40. package/dist/hike.js +169 -0
  41. package/dist/hike.js.map +1 -0
  42. package/dist/index.d.ts +14 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +16 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/logger.d.ts +15 -0
  47. package/dist/logger.d.ts.map +1 -0
  48. package/dist/logger.js +87 -0
  49. package/dist/logger.js.map +1 -0
  50. package/dist/trail.d.ts +20 -0
  51. package/dist/trail.d.ts.map +1 -0
  52. package/dist/trail.js +80 -0
  53. package/dist/trail.js.map +1 -0
  54. package/dist/types.d.ts +80 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +5 -0
  57. package/dist/types.js.map +1 -0
  58. package/package.json +23 -0
  59. package/src/__tests__/context.test.ts +60 -0
  60. package/src/__tests__/contracts.test.ts +68 -0
  61. package/src/__tests__/detours.test.ts +55 -0
  62. package/src/__tests__/examples.test.ts +176 -0
  63. package/src/__tests__/hike.test.ts +164 -0
  64. package/src/__tests__/logger.test.ts +136 -0
  65. package/src/__tests__/trail.test.ts +99 -0
  66. package/src/all.ts +55 -0
  67. package/src/assertions.ts +108 -0
  68. package/src/context.ts +42 -0
  69. package/src/contracts.ts +85 -0
  70. package/src/detours.ts +44 -0
  71. package/src/examples.ts +314 -0
  72. package/src/harness-cli.ts +310 -0
  73. package/src/harness-mcp.ts +65 -0
  74. package/src/hike.ts +283 -0
  75. package/src/index.ts +40 -0
  76. package/src/logger.ts +125 -0
  77. package/src/trail.ts +116 -0
  78. package/src/types.ts +117 -0
  79. package/tsconfig.json +9 -0
  80. package/tsconfig.tsbuildinfo +1 -0
package/dist/hike.js ADDED
@@ -0,0 +1,169 @@
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
+ import { describe, expect, test } from 'bun:test';
8
+ import { InternalError, Result, ValidationError, validateInput, } from '@ontrails/core';
9
+ import { assertErrorMatch, assertFullMatch, assertSchemaMatch, expectOk, } from './assertions.js';
10
+ import { mergeTestContext } from './context.js';
11
+ // ---------------------------------------------------------------------------
12
+ // Injection helpers
13
+ // ---------------------------------------------------------------------------
14
+ /**
15
+ * Find an error example on a trail by name or description substring.
16
+ */
17
+ const findErrorExample = (trailDef, description) => {
18
+ const example = trailDef.examples?.find((ex) => ex.error !== undefined &&
19
+ (ex.description?.includes(description) || ex.name.includes(description)));
20
+ return example?.error;
21
+ };
22
+ /**
23
+ * Try to inject an error from a followed trail's example.
24
+ * Returns undefined when no injection is configured for this trail ID.
25
+ */
26
+ const tryInjectError = (id, scenario, trailsMap) => {
27
+ const injection = scenario.injectFromExample?.[id];
28
+ if (injection === undefined) {
29
+ return undefined;
30
+ }
31
+ const trailDef = trailsMap?.get(id);
32
+ if (trailDef === undefined) {
33
+ return Result.err(new InternalError(`Cannot inject: trail "${id}" not in topo`));
34
+ }
35
+ const errorName = findErrorExample(trailDef, injection);
36
+ if (errorName === undefined) {
37
+ return Result.err(new InternalError(`No error example matching "${injection}" on trail "${id}"`));
38
+ }
39
+ return Result.err(new Error(errorName));
40
+ };
41
+ /**
42
+ * Execute a trail from the map, validating input first.
43
+ */
44
+ const executeFromMap = (id, input, trailsMap, ctx) => {
45
+ const trailDef = trailsMap?.get(id);
46
+ if (trailDef === undefined) {
47
+ return undefined;
48
+ }
49
+ const validated = validateInput(trailDef.input, input);
50
+ if (validated.isErr()) {
51
+ return validated;
52
+ }
53
+ return trailDef.implementation(validated.value, ctx);
54
+ };
55
+ // ---------------------------------------------------------------------------
56
+ // Follow factory
57
+ // ---------------------------------------------------------------------------
58
+ /**
59
+ * Build a recording follow function that optionally injects errors.
60
+ */
61
+ const createRecordingFollow = (trace, scenario, trailsMap, baseFollow, ctx) => {
62
+ // The generic O on FollowFn is erased at runtime; the cast is safe
63
+ // because callers narrow via isOk/isErr before accessing the value.
64
+ const follow = (id, input) => {
65
+ trace.push({ id, input });
66
+ const injected = tryInjectError(id, scenario, trailsMap);
67
+ if (injected !== undefined) {
68
+ return Promise.resolve(injected);
69
+ }
70
+ if (baseFollow !== undefined) {
71
+ return baseFollow(id, input);
72
+ }
73
+ const executed = executeFromMap(id, input, trailsMap, ctx);
74
+ if (executed !== undefined) {
75
+ return Promise.resolve(executed);
76
+ }
77
+ return Promise.resolve(Result.ok());
78
+ };
79
+ return follow;
80
+ };
81
+ // ---------------------------------------------------------------------------
82
+ // Scenario assertions
83
+ // ---------------------------------------------------------------------------
84
+ const assertScenarioResult = (result, scenario, hikeDef) => {
85
+ if (scenario.expectValue !== undefined) {
86
+ assertFullMatch(result, scenario.expectValue);
87
+ }
88
+ else if (scenario.expectErr !== undefined) {
89
+ assertErrorMatch(result, scenario.expectErr, scenario.expectErrMessage);
90
+ }
91
+ else if (scenario.expectErrMessage !== undefined) {
92
+ expect(result.isErr()).toBe(true);
93
+ if (result.isErr()) {
94
+ expect(result.error.message).toContain(scenario.expectErrMessage);
95
+ }
96
+ }
97
+ else if (scenario.expectOk === true) {
98
+ expect(result.isOk()).toBe(true);
99
+ assertSchemaMatch(result, hikeDef.output);
100
+ }
101
+ };
102
+ const assertFollowTrace = (trace, scenario) => {
103
+ if (scenario.expectFollowed !== undefined) {
104
+ const followedIds = trace.map((r) => r.id);
105
+ expect(followedIds).toEqual([...scenario.expectFollowed]);
106
+ }
107
+ if (scenario.expectFollowedCount !== undefined) {
108
+ const counts = {};
109
+ for (const record of trace) {
110
+ counts[record.id] = (counts[record.id] ?? 0) + 1;
111
+ }
112
+ expect(counts).toEqual({ ...scenario.expectFollowedCount });
113
+ }
114
+ };
115
+ const handleValidationError = (validated, scenario) => {
116
+ if (!validated.isErr()) {
117
+ return false;
118
+ }
119
+ if (scenario.expectErr === ValidationError) {
120
+ expect(validated.error).toBeInstanceOf(ValidationError);
121
+ if (scenario.expectErrMessage !== undefined) {
122
+ expect(validated.error.message).toContain(scenario.expectErrMessage);
123
+ }
124
+ return true;
125
+ }
126
+ throw new Error(`Input validation failed unexpectedly: ${validated.error.message}`);
127
+ };
128
+ // ---------------------------------------------------------------------------
129
+ // Scenario runner
130
+ // ---------------------------------------------------------------------------
131
+ const buildTestContext = (scenario, ctx, trailsMap) => {
132
+ const trace = [];
133
+ const baseCtx = mergeTestContext(ctx);
134
+ const follow = createRecordingFollow(trace, scenario, trailsMap, baseCtx.follow, baseCtx);
135
+ return { testCtx: { ...baseCtx, follow }, trace };
136
+ };
137
+ const runScenario = async (hikeDef, scenario, ctx, trailsMap) => {
138
+ const validated = validateInput(hikeDef.input, scenario.input);
139
+ if (handleValidationError(validated, scenario)) {
140
+ return;
141
+ }
142
+ const { trace, testCtx } = buildTestContext(scenario, ctx, trailsMap);
143
+ const result = await hikeDef.implementation(expectOk(validated), testCtx);
144
+ assertFollowTrace(trace, scenario);
145
+ assertScenarioResult(result, scenario, hikeDef);
146
+ };
147
+ /**
148
+ * Generate a describe block for a hike with one test per scenario.
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * testHike(onboardHike, [
153
+ * {
154
+ * description: "follows add then relate",
155
+ * input: { name: "Alpha" },
156
+ * expectOk: true,
157
+ * expectFollowed: ["entity.add", "entity.relate"],
158
+ * },
159
+ * ]);
160
+ * ```
161
+ */
162
+ export const testHike = (hikeDef, scenarios, options) => {
163
+ describe(hikeDef.id, () => {
164
+ test.each([...scenarios])('$description', async (scenario) => {
165
+ await runScenario(hikeDef, scenario, options?.ctx, options?.trails);
166
+ });
167
+ });
168
+ };
169
+ //# sourceMappingURL=hike.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hike.js","sourceRoot":"","sources":["../src/hike.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ export { testAll } from './all.js';
2
+ export { testExamples } from './examples.js';
3
+ export { testHike } from './hike.js';
4
+ export { testTrail } from './trail.js';
5
+ export { testContracts } from './contracts.js';
6
+ export { testDetours } from './detours.js';
7
+ export { assertErrorMatch, assertFullMatch, assertSchemaMatch, expectErr, expectOk, } from './assertions.js';
8
+ export { createTestContext } from './context.js';
9
+ export { createTestLogger } from './logger.js';
10
+ export { createCliHarness } from './harness-cli.js';
11
+ export { createMcpHarness } from './harness-mcp.js';
12
+ export type { TestHikeOptions } from './hike.js';
13
+ export type { HikeScenario, TestScenario, TestLogger, TestTrailContextOptions, CliHarness, CliHarnessOptions, CliHarnessResult, McpHarness, McpHarnessOptions, McpHarnessResult, } from './types.js';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,SAAS,EACT,QAAQ,GACT,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAG/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGpD,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,uBAAuB,EACvB,UAAU,EACV,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ // Contract-driven testing
2
+ export { testAll } from './all.js';
3
+ export { testExamples } from './examples.js';
4
+ export { testHike } from './hike.js';
5
+ export { testTrail } from './trail.js';
6
+ export { testContracts } from './contracts.js';
7
+ export { testDetours } from './detours.js';
8
+ // Assertions
9
+ export { assertErrorMatch, assertFullMatch, assertSchemaMatch, expectErr, expectOk, } from './assertions.js';
10
+ // Mock factories
11
+ export { createTestContext } from './context.js';
12
+ export { createTestLogger } from './logger.js';
13
+ // Surface harnesses
14
+ export { createCliHarness } from './harness-cli.js';
15
+ export { createMcpHarness } from './harness-mcp.js';
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0BAA0B;AAC1B,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,aAAa;AACb,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,SAAS,EACT,QAAQ,GACT,MAAM,iBAAiB,CAAC;AAEzB,iBAAiB;AACjB,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,oBAAoB;AACpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Test logger that captures log records for assertion.
3
+ */
4
+ import type { LogLevel } from '@ontrails/logging';
5
+ import type { TestLogger } from './types.js';
6
+ /**
7
+ * Create a test logger that captures all log records in an array.
8
+ *
9
+ * Records are not printed. Use `entries`, `find()`, and `assertLogged()`
10
+ * to inspect what was logged during a test.
11
+ */
12
+ export declare const createTestLogger: (options?: {
13
+ level?: LogLevel;
14
+ }) => TestLogger;
15
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAA0B,MAAM,mBAAmB,CAAC;AAE1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA+G7C;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,UAAU;IAAE,KAAK,CAAC,EAAE,QAAQ,CAAA;CAAE,KAAG,UACG,CAAC"}
package/dist/logger.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Test logger that captures log records for assertion.
3
+ */
4
+ // ---------------------------------------------------------------------------
5
+ // Level ordering for filtering
6
+ // ---------------------------------------------------------------------------
7
+ const LEVEL_ORDER = {
8
+ debug: 1,
9
+ error: 4,
10
+ fatal: 5,
11
+ info: 2,
12
+ silent: 6,
13
+ trace: 0,
14
+ warn: 3,
15
+ };
16
+ // ---------------------------------------------------------------------------
17
+ // Internal factory (shared between root and child loggers)
18
+ // ---------------------------------------------------------------------------
19
+ const createTestLoggerInternal = (name, minLevel, sharedEntries, baseMetadata) => {
20
+ const minOrder = LEVEL_ORDER[minLevel] ?? 0;
21
+ const shouldLog = (level) => (LEVEL_ORDER[level] ?? 0) >= minOrder;
22
+ const log = (level, message, metadata) => {
23
+ if (!shouldLog(level)) {
24
+ return;
25
+ }
26
+ const record = {
27
+ category: name,
28
+ level,
29
+ message,
30
+ metadata: { ...baseMetadata, ...metadata },
31
+ timestamp: new Date(),
32
+ };
33
+ sharedEntries.push(record);
34
+ };
35
+ return {
36
+ assertLogged(level, messageSubstring) {
37
+ const match = sharedEntries.find((r) => r.level === level && r.message.includes(messageSubstring));
38
+ if (match === undefined) {
39
+ throw new Error(`Expected a log entry with level="${level}" containing "${messageSubstring}", but none was found. ` +
40
+ `Entries: ${JSON.stringify(sharedEntries.map((r) => ({ level: r.level, message: r.message })))}`);
41
+ }
42
+ },
43
+ child(metadata) {
44
+ const merged = { ...baseMetadata, ...metadata };
45
+ return createTestLoggerInternal(name, minLevel, sharedEntries, merged);
46
+ },
47
+ clear() {
48
+ sharedEntries.length = 0;
49
+ },
50
+ debug(message, metadata) {
51
+ log('debug', message, metadata);
52
+ },
53
+ get entries() {
54
+ return sharedEntries;
55
+ },
56
+ error(message, metadata) {
57
+ log('error', message, metadata);
58
+ },
59
+ fatal(message, metadata) {
60
+ log('fatal', message, metadata);
61
+ },
62
+ find(predicate) {
63
+ return sharedEntries.filter(predicate);
64
+ },
65
+ info(message, metadata) {
66
+ log('info', message, metadata);
67
+ },
68
+ name,
69
+ trace(message, metadata) {
70
+ log('trace', message, metadata);
71
+ },
72
+ warn(message, metadata) {
73
+ log('warn', message, metadata);
74
+ },
75
+ };
76
+ };
77
+ // ---------------------------------------------------------------------------
78
+ // createTestLogger
79
+ // ---------------------------------------------------------------------------
80
+ /**
81
+ * Create a test logger that captures all log records in an array.
82
+ *
83
+ * Records are not printed. Use `entries`, `find()`, and `assertLogged()`
84
+ * to inspect what was logged during a test.
85
+ */
86
+ export const createTestLogger = (options) => createTestLoggerInternal('test', options?.level ?? 'trace', [], {});
87
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
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
+ import type { AnyTrail, TrailContext } from '@ontrails/core';
8
+ import type { TestScenario } from './types.js';
9
+ /**
10
+ * Generate a describe block for a trail with one test per scenario.
11
+ *
12
+ * ```ts
13
+ * testTrail(myTrail, [
14
+ * { description: "valid input", input: { name: "Alpha" }, expectOk: true },
15
+ * { description: "missing name", input: {}, expectErr: ValidationError },
16
+ * ]);
17
+ * ```
18
+ */
19
+ export declare const testTrail: (trailDef: AnyTrail, scenarios: readonly TestScenario[], ctx?: Partial<TrailContext>) => void;
20
+ //# sourceMappingURL=trail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trail.d.ts","sourceRoot":"","sources":["../src/trail.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAU,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAUrE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAyE/C;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS,GACpB,UAAU,QAAQ,EAClB,WAAW,SAAS,YAAY,EAAE,EAClC,MAAM,OAAO,CAAC,YAAY,CAAC,KAC1B,IASF,CAAC"}
package/dist/trail.js ADDED
@@ -0,0 +1,80 @@
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
+ import { describe, expect, test } from 'bun:test';
8
+ import { ValidationError, validateInput } from '@ontrails/core';
9
+ import { assertErrorMatch, assertFullMatch, assertSchemaMatch, expectOk, } from './assertions.js';
10
+ import { mergeTestContext } from './context.js';
11
+ // ---------------------------------------------------------------------------
12
+ // Helpers
13
+ // ---------------------------------------------------------------------------
14
+ const assertScenarioResult = (result, scenario, trailDef) => {
15
+ if (scenario.expectValue !== undefined) {
16
+ assertFullMatch(result, scenario.expectValue);
17
+ }
18
+ else if (scenario.expectErr !== undefined) {
19
+ assertErrorMatch(result, scenario.expectErr, scenario.expectErrMessage);
20
+ }
21
+ else if (scenario.expectErrMessage !== undefined) {
22
+ expect(result.isErr()).toBe(true);
23
+ if (result.isErr()) {
24
+ expect(result.error.message).toContain(scenario.expectErrMessage);
25
+ }
26
+ }
27
+ else if (scenario.expectOk === true) {
28
+ expect(result.isOk()).toBe(true);
29
+ assertSchemaMatch(result, trailDef.output);
30
+ }
31
+ };
32
+ /**
33
+ * Handle input validation failure for a scenario.
34
+ * Returns true if the error was expected and handled.
35
+ * Throws if the error was unexpected.
36
+ */
37
+ const handleValidationError = (validated, scenario) => {
38
+ if (!validated.isErr()) {
39
+ return false;
40
+ }
41
+ if (scenario.expectErr === ValidationError) {
42
+ expect(validated.error).toBeInstanceOf(ValidationError);
43
+ if (scenario.expectErrMessage !== undefined) {
44
+ expect(validated.error.message).toContain(scenario.expectErrMessage);
45
+ }
46
+ return true;
47
+ }
48
+ throw new Error(`Input validation failed unexpectedly: ${validated.error.message}`);
49
+ };
50
+ const runScenario = async (trailDef, scenario, ctx) => {
51
+ const testCtx = mergeTestContext(ctx);
52
+ const validated = validateInput(trailDef.input, scenario.input);
53
+ if (handleValidationError(validated, scenario)) {
54
+ return;
55
+ }
56
+ const validatedInput = expectOk(validated);
57
+ const result = await trailDef.implementation(validatedInput, testCtx);
58
+ assertScenarioResult(result, scenario, trailDef);
59
+ };
60
+ // ---------------------------------------------------------------------------
61
+ // testTrail
62
+ // ---------------------------------------------------------------------------
63
+ /**
64
+ * Generate a describe block for a trail with one test per scenario.
65
+ *
66
+ * ```ts
67
+ * testTrail(myTrail, [
68
+ * { description: "valid input", input: { name: "Alpha" }, expectOk: true },
69
+ * { description: "missing name", input: {}, expectErr: ValidationError },
70
+ * ]);
71
+ * ```
72
+ */
73
+ export const testTrail = (trailDef, scenarios, ctx) => {
74
+ describe(trailDef.id, () => {
75
+ test.each([...scenarios])('$description', async (scenario) => {
76
+ await runScenario(trailDef, scenario, ctx);
77
+ });
78
+ });
79
+ };
80
+ //# sourceMappingURL=trail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trail.js","sourceRoot":"","sources":["../src/trail.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Shared types for @ontrails/testing.
3
+ */
4
+ import type { Logger, Topo } from '@ontrails/core';
5
+ import type { LogLevel, LogRecord } from '@ontrails/logging';
6
+ /** A custom test scenario for a single trail. */
7
+ export interface TestScenario {
8
+ /** Description shown in test output. */
9
+ readonly description?: string | undefined;
10
+ /** Assert the result error has this message (substring match). */
11
+ readonly expectErrMessage?: string | undefined;
12
+ /** Assert the result is an error of this type. */
13
+ readonly expectErr?: (new (...args: never[]) => Error) | undefined;
14
+ /** Assert the result is ok. */
15
+ readonly expectOk?: boolean | undefined;
16
+ /** Assert the result value equals this. */
17
+ readonly expectValue?: unknown | undefined;
18
+ /** Input to pass to the implementation. */
19
+ readonly input: unknown;
20
+ }
21
+ /** A test scenario for a hike's composition graph. */
22
+ export interface HikeScenario extends TestScenario {
23
+ /** Assert these trail IDs were followed, in order. */
24
+ readonly expectFollowed?: readonly string[] | undefined;
25
+ /** Assert follow counts per trail ID. */
26
+ readonly expectFollowedCount?: Readonly<Record<string, number>> | undefined;
27
+ /** Inject failure from a followed trail's example by description. */
28
+ readonly injectFromExample?: Readonly<Record<string, string>> | undefined;
29
+ }
30
+ /** A logger that captures entries for assertion in tests. */
31
+ export interface TestLogger extends Logger {
32
+ /** All log records captured during the test. */
33
+ readonly entries: readonly LogRecord[];
34
+ /** Clear captured entries. */
35
+ clear(): void;
36
+ /** Find entries matching a predicate. */
37
+ find(predicate: (record: LogRecord) => boolean): readonly LogRecord[];
38
+ /** Assert that at least one entry matches. */
39
+ assertLogged(level: LogLevel, messageSubstring: string): void;
40
+ }
41
+ /** Options for creating a test trail context. */
42
+ export interface TestTrailContextOptions {
43
+ readonly cwd?: string | undefined;
44
+ readonly env?: Record<string, string> | undefined;
45
+ readonly logger?: Logger | undefined;
46
+ readonly requestId?: string | undefined;
47
+ readonly signal?: AbortSignal | undefined;
48
+ }
49
+ /** Options for creating a CLI harness. */
50
+ export interface CliHarnessOptions {
51
+ readonly app: Topo;
52
+ }
53
+ /** A test harness for CLI commands. */
54
+ export interface CliHarness {
55
+ /** Execute a CLI command string and capture output. */
56
+ run(command: string): Promise<CliHarnessResult>;
57
+ }
58
+ /** The result of a CLI harness command execution. */
59
+ export interface CliHarnessResult {
60
+ readonly exitCode: number;
61
+ /** Parsed JSON output if --output json was used. */
62
+ readonly json?: unknown | undefined;
63
+ readonly stderr: string;
64
+ readonly stdout: string;
65
+ }
66
+ /** Options for creating an MCP harness. */
67
+ export interface McpHarnessOptions {
68
+ readonly app: Topo;
69
+ }
70
+ /** A test harness for MCP tools. */
71
+ export interface McpHarness {
72
+ /** Call an MCP tool by name with arguments. */
73
+ callTool(name: string, args: Record<string, unknown>): Promise<McpHarnessResult>;
74
+ }
75
+ /** The result of an MCP harness tool invocation. */
76
+ export interface McpHarnessResult {
77
+ readonly content: unknown;
78
+ readonly isError: boolean;
79
+ }
80
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Shared types for @ontrails/testing.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@ontrails/testing",
3
+ "version": "1.0.0-beta.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.ts",
7
+ "./package.json": "./package.json"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc -b",
11
+ "test": "bun test",
12
+ "typecheck": "tsc --noEmit",
13
+ "lint": "oxlint ./src",
14
+ "clean": "rm -rf dist *.tsbuildinfo"
15
+ },
16
+ "peerDependencies": {
17
+ "@ontrails/cli": "workspace:*",
18
+ "@ontrails/core": "workspace:*",
19
+ "@ontrails/logging": "workspace:*",
20
+ "@ontrails/mcp": "workspace:*",
21
+ "zod": "catalog:"
22
+ }
23
+ }
@@ -0,0 +1,60 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { createTestContext } from '../context.js';
4
+ import type { TestLogger } from '../types.js';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Tests
8
+ // ---------------------------------------------------------------------------
9
+
10
+ describe('createTestContext', () => {
11
+ test('produces a valid TrailContext with no args', () => {
12
+ const ctx = createTestContext();
13
+ expect(ctx.requestId).toBe('test-request-001');
14
+ expect(ctx.signal).toBeDefined();
15
+ expect(ctx.signal.aborted).toBe(false);
16
+ expect(ctx.logger).toBeDefined();
17
+ expect(ctx.workspaceRoot).toBeDefined();
18
+ });
19
+
20
+ test('default logger is a TestLogger', () => {
21
+ const ctx = createTestContext();
22
+ const logger = ctx.logger as TestLogger;
23
+ expect(typeof logger.info).toBe('function');
24
+ expect(typeof logger.clear).toBe('function');
25
+ expect(typeof logger.find).toBe('function');
26
+ expect(typeof logger.assertLogged).toBe('function');
27
+ expect(logger.entries).toBeDefined();
28
+ });
29
+
30
+ test('default env is set to test', () => {
31
+ const ctx = createTestContext();
32
+ const env = ctx['env'] as Record<string, string>;
33
+ expect(env).toEqual({ TRAILS_ENV: 'test' });
34
+ });
35
+
36
+ test('overrides requestId', () => {
37
+ const ctx = createTestContext({ requestId: 'custom-id' });
38
+ expect(ctx.requestId).toBe('custom-id');
39
+ });
40
+
41
+ test('overrides signal', () => {
42
+ const controller = new AbortController();
43
+ controller.abort();
44
+ const ctx = createTestContext({ signal: controller.signal });
45
+ expect(ctx.signal.aborted).toBe(true);
46
+ });
47
+
48
+ test('overrides cwd', () => {
49
+ const ctx = createTestContext({ cwd: '/tmp/test' });
50
+ expect(ctx.workspaceRoot).toBe('/tmp/test');
51
+ });
52
+
53
+ test('overrides env', () => {
54
+ const ctx = createTestContext({
55
+ env: { CUSTOM: '1', NODE_ENV: 'test' },
56
+ });
57
+ const env = ctx['env'] as Record<string, string>;
58
+ expect(env).toEqual({ CUSTOM: '1', NODE_ENV: 'test' });
59
+ });
60
+ });