@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.
- package/.turbo/turbo-build.log +1 -0
- package/.turbo/turbo-lint.log +3 -0
- package/.turbo/turbo-typecheck.log +1 -0
- package/CHANGELOG.md +23 -0
- package/README.md +221 -0
- package/dist/all.d.ts +30 -0
- package/dist/all.d.ts.map +1 -0
- package/dist/all.js +47 -0
- package/dist/all.js.map +1 -0
- package/dist/assertions.d.ts +49 -0
- package/dist/assertions.d.ts.map +1 -0
- package/dist/assertions.js +84 -0
- package/dist/assertions.js.map +1 -0
- package/dist/context.d.ts +19 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +33 -0
- package/dist/context.js.map +1 -0
- package/dist/contracts.d.ts +16 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +56 -0
- package/dist/contracts.js.map +1 -0
- package/dist/detours.d.ts +12 -0
- package/dist/detours.d.ts.map +1 -0
- package/dist/detours.js +30 -0
- package/dist/detours.js.map +1 -0
- package/dist/examples.d.ts +22 -0
- package/dist/examples.d.ts.map +1 -0
- package/dist/examples.js +187 -0
- package/dist/examples.js.map +1 -0
- package/dist/harness-cli.d.ts +21 -0
- package/dist/harness-cli.d.ts.map +1 -0
- package/dist/harness-cli.js +213 -0
- package/dist/harness-cli.js.map +1 -0
- package/dist/harness-mcp.d.ts +21 -0
- package/dist/harness-mcp.d.ts.map +1 -0
- package/dist/harness-mcp.js +50 -0
- package/dist/harness-mcp.js.map +1 -0
- package/dist/hike.d.ts +32 -0
- package/dist/hike.d.ts.map +1 -0
- package/dist/hike.js +169 -0
- package/dist/hike.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +15 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +87 -0
- package/dist/logger.js.map +1 -0
- package/dist/trail.d.ts +20 -0
- package/dist/trail.d.ts.map +1 -0
- package/dist/trail.js +80 -0
- package/dist/trail.js.map +1 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +23 -0
- package/src/__tests__/context.test.ts +60 -0
- package/src/__tests__/contracts.test.ts +68 -0
- package/src/__tests__/detours.test.ts +55 -0
- package/src/__tests__/examples.test.ts +176 -0
- package/src/__tests__/hike.test.ts +164 -0
- package/src/__tests__/logger.test.ts +136 -0
- package/src/__tests__/trail.test.ts +99 -0
- package/src/all.ts +55 -0
- package/src/assertions.ts +108 -0
- package/src/context.ts +42 -0
- package/src/contracts.ts +85 -0
- package/src/detours.ts +44 -0
- package/src/examples.ts +314 -0
- package/src/harness-cli.ts +310 -0
- package/src/harness-mcp.ts +65 -0
- package/src/hike.ts +283 -0
- package/src/index.ts +40 -0
- package/src/logger.ts +125 -0
- package/src/trail.ts +116 -0
- package/src/types.ts +117 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
package/dist/detours.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* testDetours — verify that all detour targets exist in the topo.
|
|
3
|
+
*
|
|
4
|
+
* Pure structural validation. No implementation execution needed.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, test } from 'bun:test';
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// testDetours
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
/**
|
|
11
|
+
* Verify that every trail's detour targets reference trails that
|
|
12
|
+
* actually exist in the app's topo.
|
|
13
|
+
*/
|
|
14
|
+
export const testDetours = (app) => {
|
|
15
|
+
const trailEntries = [...app.trails];
|
|
16
|
+
describe('detours', () => {
|
|
17
|
+
describe.each(trailEntries)('%s', (_id, trailDef) => {
|
|
18
|
+
const t = trailDef;
|
|
19
|
+
if (t.detours === undefined) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const { detours } = t;
|
|
23
|
+
const testCases = Object.entries(detours).flatMap(([detourName, targets]) => targets.map((targetId) => ({ detourName, targetId })));
|
|
24
|
+
test.each(testCases)('detour "$detourName" -> "$targetId" exists', ({ targetId }) => {
|
|
25
|
+
expect(app.has(targetId)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=detours.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detours.js","sourceRoot":"","sources":["../src/detours.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAIlD,8EAA8E;AAC9E,cAAc;AACd,8EAA8E;AAE9E;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,GAAS,EAAQ,EAAE;IAC7C,MAAM,YAAY,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IAErC,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE;QACvB,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE;YAClD,MAAM,CAAC,GAAG,QAAmC,CAAC;YAE9C,IAAI,CAAC,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC5B,OAAO;YACT,CAAC;YAED,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;YACtB,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,CAC/C,CAAC,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE,CACxB,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC,CACxD,CAAC;YAEF,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAClB,4CAA4C,EAC5C,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE;gBACf,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvC,CAAC,CACF,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* testExamples — the headline one-liner.
|
|
3
|
+
*
|
|
4
|
+
* Iterates every trail in the app's topo. For each trail with examples,
|
|
5
|
+
* generates describe/test blocks using bun:test. Progressive assertion
|
|
6
|
+
* determines which check to run per example. For hikes with `follows`
|
|
7
|
+
* declarations, checks that every declared follow was called at least once.
|
|
8
|
+
*/
|
|
9
|
+
import type { Topo, TrailContext } from '@ontrails/core';
|
|
10
|
+
/**
|
|
11
|
+
* Generate describe/test blocks for every trail example in the app.
|
|
12
|
+
*
|
|
13
|
+
* For hikes with `follows` declarations and examples, also verifies that
|
|
14
|
+
* every declared follow ID was called at least once across all examples.
|
|
15
|
+
*
|
|
16
|
+
* One line in your test file:
|
|
17
|
+
* ```ts
|
|
18
|
+
* testExamples(app);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare const testExamples: (app: Topo, ctxOrFactory?: Partial<TrailContext> | (() => Partial<TrailContext>)) => void;
|
|
22
|
+
//# sourceMappingURL=examples.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"examples.d.ts","sourceRoot":"","sources":["../src/examples.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAGV,IAAI,EAGJ,YAAY,EACb,MAAM,gBAAgB,CAAC;AAkQxB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,GACvB,KAAK,IAAI,EACT,eAAe,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,YAAY,CAAC,CAAC,KACnE,IAuBF,CAAC"}
|
package/dist/examples.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* testExamples — the headline one-liner.
|
|
3
|
+
*
|
|
4
|
+
* Iterates every trail in the app's topo. For each trail with examples,
|
|
5
|
+
* generates describe/test blocks using bun:test. Progressive assertion
|
|
6
|
+
* determines which check to run per example. For hikes with `follows`
|
|
7
|
+
* declarations, checks that every declared follow was called at least once.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, test } from 'bun:test';
|
|
10
|
+
import { AlreadyExistsError, AmbiguousError, AssertionError, AuthError, CancelledError, ConflictError, InternalError, NetworkError, NotFoundError, PermissionError, RateLimitError, Result, TimeoutError, TrailsError, ValidationError, validateInput, } from '@ontrails/core';
|
|
11
|
+
import { assertErrorMatch, assertFullMatch, assertSchemaMatch, expectOk, } from './assertions.js';
|
|
12
|
+
import { mergeTestContext } from './context.js';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Error class name -> constructor map
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const ERROR_MAP = {
|
|
17
|
+
AlreadyExistsError: AlreadyExistsError,
|
|
18
|
+
AmbiguousError: AmbiguousError,
|
|
19
|
+
AssertionError: AssertionError,
|
|
20
|
+
AuthError: AuthError,
|
|
21
|
+
CancelledError: CancelledError,
|
|
22
|
+
ConflictError: ConflictError,
|
|
23
|
+
InternalError: InternalError,
|
|
24
|
+
NetworkError: NetworkError,
|
|
25
|
+
NotFoundError: NotFoundError,
|
|
26
|
+
PermissionError: PermissionError,
|
|
27
|
+
RateLimitError: RateLimitError,
|
|
28
|
+
TimeoutError: TimeoutError,
|
|
29
|
+
TrailsError: TrailsError,
|
|
30
|
+
ValidationError: ValidationError,
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Resolve an error class name string to the actual constructor.
|
|
34
|
+
* Falls back to generic Error if the name is not in the core taxonomy.
|
|
35
|
+
*/
|
|
36
|
+
const resolveErrorClass = (name) => ERROR_MAP[name] ?? Error;
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Helpers
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
const assertProgressiveMatch = (result, example, output) => {
|
|
41
|
+
if (example.expected !== undefined) {
|
|
42
|
+
assertFullMatch(result, example.expected);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (example.error !== undefined) {
|
|
46
|
+
const errorClass = resolveErrorClass(example.error);
|
|
47
|
+
assertErrorMatch(result, errorClass);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
assertSchemaMatch(result, output);
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Handle input validation failure for an example.
|
|
54
|
+
* Returns true if the validation error was expected (and assertions passed).
|
|
55
|
+
* Throws if the validation error was unexpected.
|
|
56
|
+
*/
|
|
57
|
+
const handleValidationError = (validated, example) => {
|
|
58
|
+
if (!validated.isErr()) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (example.error !== undefined) {
|
|
62
|
+
const errorClass = resolveErrorClass(example.error);
|
|
63
|
+
expect(validated.error).toBeInstanceOf(errorClass);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
throw new Error(`Example "${example.name}" has invalid input: ${validated.error.message}`);
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Run a single example against a trail.
|
|
70
|
+
* Handles validation, execution, and assertions.
|
|
71
|
+
*/
|
|
72
|
+
const runExample = async (t, example, output, testCtx) => {
|
|
73
|
+
const validated = validateInput(t.input, example.input);
|
|
74
|
+
if (handleValidationError(validated, example)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const validatedInput = expectOk(validated);
|
|
78
|
+
const result = await t.implementation(validatedInput, testCtx);
|
|
79
|
+
assertProgressiveMatch(result, example, output);
|
|
80
|
+
};
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Follows coverage for hikes
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
/**
|
|
85
|
+
* Build a recording follow function that tracks which trail IDs are called.
|
|
86
|
+
*
|
|
87
|
+
* Delegates to `baseFollow` when available, otherwise looks up the trail
|
|
88
|
+
* in the topo and executes it with validated input. Falls back to
|
|
89
|
+
* `Result.ok()` when neither is available.
|
|
90
|
+
*/
|
|
91
|
+
const createCoverageFollow = (called, baseFollow, topo, ctx) => {
|
|
92
|
+
const follow = (id, input) => {
|
|
93
|
+
called.add(id);
|
|
94
|
+
if (baseFollow !== undefined) {
|
|
95
|
+
return baseFollow(id, input);
|
|
96
|
+
}
|
|
97
|
+
const trailDef = topo.get(id);
|
|
98
|
+
if (trailDef !== undefined) {
|
|
99
|
+
const validated = validateInput(trailDef.input, input);
|
|
100
|
+
if (validated.isErr()) {
|
|
101
|
+
return Promise.resolve(validated);
|
|
102
|
+
}
|
|
103
|
+
return Promise.resolve(trailDef.implementation(validated.value, ctx));
|
|
104
|
+
}
|
|
105
|
+
return Promise.resolve(Result.ok());
|
|
106
|
+
};
|
|
107
|
+
return follow;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Run a single example against a hike, recording follow calls.
|
|
111
|
+
*/
|
|
112
|
+
const runHikeExample = async (hikeDef, example, output, baseCtx, called, topo) => {
|
|
113
|
+
const validated = validateInput(hikeDef.input, example.input);
|
|
114
|
+
if (handleValidationError(validated, example)) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const validatedInput = expectOk(validated);
|
|
118
|
+
const follow = createCoverageFollow(called, baseCtx.follow, topo, baseCtx);
|
|
119
|
+
const testCtx = { ...baseCtx, follow };
|
|
120
|
+
const result = await hikeDef.implementation(validatedInput, testCtx);
|
|
121
|
+
assertProgressiveMatch(result, example, output);
|
|
122
|
+
};
|
|
123
|
+
const collectHikesWithExamples = (app) => [...app.hikes]
|
|
124
|
+
.filter(([, h]) => h.examples !== undefined && h.examples.length > 0)
|
|
125
|
+
.map(([hikeId, hikeDef]) => ({
|
|
126
|
+
examples: hikeDef.examples,
|
|
127
|
+
hikeDef,
|
|
128
|
+
hikeId,
|
|
129
|
+
}));
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Hike example describe blocks
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
/**
|
|
134
|
+
* Generate describe/test blocks for hikes with follows coverage.
|
|
135
|
+
*
|
|
136
|
+
* Always uses a recording follow so that follows coverage can be checked.
|
|
137
|
+
* Hikes without `follows` still run their examples but skip the coverage test.
|
|
138
|
+
*/
|
|
139
|
+
const describeHikeExamples = (hikesWithExamples, resolveCtx, topo) => {
|
|
140
|
+
if (hikesWithExamples.length === 0) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
describe.each([...hikesWithExamples])('$hikeId', ({ hikeDef, examples }) => {
|
|
144
|
+
const called = new Set();
|
|
145
|
+
test.each([...examples])('example: $name', async (example) => {
|
|
146
|
+
const baseCtx = mergeTestContext(resolveCtx());
|
|
147
|
+
await runHikeExample(hikeDef, example, hikeDef.output, baseCtx, called, topo);
|
|
148
|
+
});
|
|
149
|
+
if (hikeDef.follows.length > 0) {
|
|
150
|
+
test('follows coverage', () => {
|
|
151
|
+
const uncovered = hikeDef.follows.filter((id) => !called.has(id));
|
|
152
|
+
expect(uncovered).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// testExamples
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
/**
|
|
161
|
+
* Generate describe/test blocks for every trail example in the app.
|
|
162
|
+
*
|
|
163
|
+
* For hikes with `follows` declarations and examples, also verifies that
|
|
164
|
+
* every declared follow ID was called at least once across all examples.
|
|
165
|
+
*
|
|
166
|
+
* One line in your test file:
|
|
167
|
+
* ```ts
|
|
168
|
+
* testExamples(app);
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
export const testExamples = (app, ctxOrFactory) => {
|
|
172
|
+
const resolveCtx = typeof ctxOrFactory === 'function' ? ctxOrFactory : () => ctxOrFactory;
|
|
173
|
+
const trailEntries = [...app.trails];
|
|
174
|
+
describe.each(trailEntries)('%s', (_id, trailDef) => {
|
|
175
|
+
const t = trailDef;
|
|
176
|
+
if (t.examples === undefined || t.examples.length === 0) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const { examples, output } = t;
|
|
180
|
+
test.each([...examples])('example: $name', async (example) => {
|
|
181
|
+
const testCtx = mergeTestContext(resolveCtx());
|
|
182
|
+
await runExample(t, example, output, testCtx);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describeHikeExamples(collectHikesWithExamples(app), resolveCtx, app);
|
|
186
|
+
};
|
|
187
|
+
//# sourceMappingURL=examples.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"examples.js","sourceRoot":"","sources":["../src/examples.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI integration test harness.
|
|
3
|
+
*
|
|
4
|
+
* Builds CLI commands from an App, executes them in-process,
|
|
5
|
+
* and captures stdout/stderr.
|
|
6
|
+
*/
|
|
7
|
+
import type { CliHarness, CliHarnessOptions } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Create a CLI harness for integration testing.
|
|
10
|
+
*
|
|
11
|
+
* Builds commands from the app's topo and provides a `run()` method
|
|
12
|
+
* that parses command strings and executes them in-process.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* const harness = createCliHarness({ app });
|
|
16
|
+
* const result = await harness.run("entity show --name Alpha --output json");
|
|
17
|
+
* expect(result.exitCode).toBe(0);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare const createCliHarness: (options: CliHarnessOptions) => CliHarness;
|
|
21
|
+
//# sourceMappingURL=harness-cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness-cli.d.ts","sourceRoot":"","sources":["../src/harness-cli.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAoRpB;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,GAAI,SAAS,iBAAiB,KAAG,UAM7D,CAAC"}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI integration test harness.
|
|
3
|
+
*
|
|
4
|
+
* Builds CLI commands from an App, executes them in-process,
|
|
5
|
+
* and captures stdout/stderr.
|
|
6
|
+
*/
|
|
7
|
+
import { buildCliCommands } from '@ontrails/cli';
|
|
8
|
+
import { createTestContext } from './context.js';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Tokenizer
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/** Parse a command string into tokens (simple split, no quoting support). */
|
|
13
|
+
const parseCommandString = (input) => input
|
|
14
|
+
.trim()
|
|
15
|
+
.split(/\s+/)
|
|
16
|
+
.filter((s) => s.length > 0);
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Command resolution
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
/** Try to match group + name from tokens. */
|
|
21
|
+
const tryGroupMatch = (commands, firstToken, secondToken, tokens) => {
|
|
22
|
+
if (secondToken === undefined || secondToken.startsWith('-')) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const match = commands.find((c) => c.group === firstToken && c.name === secondToken);
|
|
26
|
+
if (match === undefined) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
return { command: match, flagTokens: tokens.slice(2) };
|
|
30
|
+
};
|
|
31
|
+
/** Try to match a direct name from the first token. */
|
|
32
|
+
const tryDirectMatch = (commands, firstToken, tokens) => {
|
|
33
|
+
const match = commands.find((c) => c.name === firstToken && c.group === undefined);
|
|
34
|
+
if (match === undefined) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return { command: match, flagTokens: tokens.slice(1) };
|
|
38
|
+
};
|
|
39
|
+
/** Resolve a command from tokens, handling group.name patterns. */
|
|
40
|
+
const resolveCommand = (commands, tokens) => {
|
|
41
|
+
const [firstToken, secondToken] = tokens;
|
|
42
|
+
if (firstToken === undefined) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
return (tryGroupMatch(commands, firstToken, secondToken, tokens) ??
|
|
46
|
+
tryDirectMatch(commands, firstToken, tokens));
|
|
47
|
+
};
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Flag parsing
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
/** Parse a value flag (--key value) and return new index. */
|
|
52
|
+
const parseValueFlag = (key, next, flags) => {
|
|
53
|
+
const num = Number(next);
|
|
54
|
+
flags[key] = Number.isNaN(num) ? next : num;
|
|
55
|
+
};
|
|
56
|
+
/** Parse a single flag token and advance the index. */
|
|
57
|
+
const parseSingleFlag = (tokens, i, flags) => {
|
|
58
|
+
const token = tokens[i];
|
|
59
|
+
if (token === undefined || !token.startsWith('--')) {
|
|
60
|
+
return i + 1;
|
|
61
|
+
}
|
|
62
|
+
const key = token.slice(2);
|
|
63
|
+
const next = tokens[i + 1];
|
|
64
|
+
if (next !== undefined && !next.startsWith('-')) {
|
|
65
|
+
parseValueFlag(key, next, flags);
|
|
66
|
+
return i + 2;
|
|
67
|
+
}
|
|
68
|
+
flags[key] = true;
|
|
69
|
+
return i + 1;
|
|
70
|
+
};
|
|
71
|
+
/** Parse flag tokens into a record. */
|
|
72
|
+
const parseFlagTokens = (tokens) => {
|
|
73
|
+
const flags = {};
|
|
74
|
+
let i = 0;
|
|
75
|
+
while (i < tokens.length) {
|
|
76
|
+
i = parseSingleFlag(tokens, i, flags);
|
|
77
|
+
}
|
|
78
|
+
return flags;
|
|
79
|
+
};
|
|
80
|
+
/** Create interceptors for stdout/stderr capture. */
|
|
81
|
+
const captureStreams = () => {
|
|
82
|
+
let stdout = '';
|
|
83
|
+
let stderr = '';
|
|
84
|
+
const origStdoutWrite = process.stdout.write;
|
|
85
|
+
const origStderrWrite = process.stderr.write;
|
|
86
|
+
process.stdout.write = ((chunk) => {
|
|
87
|
+
stdout +=
|
|
88
|
+
typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);
|
|
89
|
+
return true;
|
|
90
|
+
});
|
|
91
|
+
process.stderr.write = ((chunk) => {
|
|
92
|
+
stderr +=
|
|
93
|
+
typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);
|
|
94
|
+
return true;
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
getStderr: () => stderr,
|
|
98
|
+
getStdout: () => stdout,
|
|
99
|
+
restore: () => {
|
|
100
|
+
process.stdout.write = origStdoutWrite;
|
|
101
|
+
process.stderr.write = origStderrWrite;
|
|
102
|
+
},
|
|
103
|
+
writeStdout: (text) => {
|
|
104
|
+
stdout += text;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Output formatting
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
/** Try to parse a string as JSON, returning undefined on failure. */
|
|
112
|
+
const tryParseJson = (text) => {
|
|
113
|
+
try {
|
|
114
|
+
return JSON.parse(text);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
/** Format result value into stdout and build the CLI result. */
|
|
121
|
+
const formatSuccessResult = (value, flags, streams) => {
|
|
122
|
+
const outputMode = flags['output'] ?? (flags['json'] === true ? 'json' : 'text');
|
|
123
|
+
if (outputMode === 'json') {
|
|
124
|
+
const jsonStr = `${JSON.stringify(value, null, 2)}\n`;
|
|
125
|
+
streams.writeStdout(jsonStr);
|
|
126
|
+
return {
|
|
127
|
+
exitCode: 0,
|
|
128
|
+
json: tryParseJson(jsonStr),
|
|
129
|
+
stderr: streams.getStderr(),
|
|
130
|
+
stdout: streams.getStdout(),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const formatted = typeof value === 'string'
|
|
134
|
+
? `${value}\n`
|
|
135
|
+
: `${JSON.stringify(value, null, 2)}\n`;
|
|
136
|
+
streams.writeStdout(formatted);
|
|
137
|
+
return {
|
|
138
|
+
exitCode: 0,
|
|
139
|
+
json: tryParseJson(streams.getStdout().trim()),
|
|
140
|
+
stderr: streams.getStderr(),
|
|
141
|
+
stdout: streams.getStdout(),
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Execute command
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
/** Build an error result from a caught exception. */
|
|
148
|
+
const buildErrorResult = (error, streams) => {
|
|
149
|
+
streams.restore();
|
|
150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
+
return {
|
|
152
|
+
exitCode: 1,
|
|
153
|
+
stderr: streams.getStderr() || message,
|
|
154
|
+
stdout: streams.getStdout(),
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
/** Execute a resolved command and return the result. */
|
|
158
|
+
const executeCommand = async (command, flags, streams) => {
|
|
159
|
+
const ctx = createTestContext();
|
|
160
|
+
const result = await command.execute({}, flags, ctx);
|
|
161
|
+
streams.restore();
|
|
162
|
+
if (result.isErr()) {
|
|
163
|
+
return {
|
|
164
|
+
exitCode: 1,
|
|
165
|
+
stderr: streams.getStderr() || result.error.message,
|
|
166
|
+
stdout: streams.getStdout(),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return formatSuccessResult(result.value, flags, streams);
|
|
170
|
+
};
|
|
171
|
+
/** Run the full command pipeline: resolve, parse, execute. */
|
|
172
|
+
const runCommand = async (commands, commandString) => {
|
|
173
|
+
const parts = parseCommandString(commandString);
|
|
174
|
+
const resolved = resolveCommand(commands, parts);
|
|
175
|
+
if (resolved === undefined) {
|
|
176
|
+
return {
|
|
177
|
+
exitCode: 1,
|
|
178
|
+
stderr: `Unknown command: ${commandString}`,
|
|
179
|
+
stdout: '',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const { command, flagTokens } = resolved;
|
|
183
|
+
const flags = parseFlagTokens(flagTokens);
|
|
184
|
+
const streams = captureStreams();
|
|
185
|
+
try {
|
|
186
|
+
return await executeCommand(command, flags, streams);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
return buildErrorResult(error, streams);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// createCliHarness
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
/**
|
|
196
|
+
* Create a CLI harness for integration testing.
|
|
197
|
+
*
|
|
198
|
+
* Builds commands from the app's topo and provides a `run()` method
|
|
199
|
+
* that parses command strings and executes them in-process.
|
|
200
|
+
*
|
|
201
|
+
* ```ts
|
|
202
|
+
* const harness = createCliHarness({ app });
|
|
203
|
+
* const result = await harness.run("entity show --name Alpha --output json");
|
|
204
|
+
* expect(result.exitCode).toBe(0);
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
export const createCliHarness = (options) => {
|
|
208
|
+
const commands = buildCliCommands(options.app);
|
|
209
|
+
return {
|
|
210
|
+
run: (commandString) => runCommand(commands, commandString),
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
//# sourceMappingURL=harness-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness-cli.js","sourceRoot":"","sources":["../src/harness-cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP integration test harness.
|
|
3
|
+
*
|
|
4
|
+
* Builds MCP tools from an App, invokes them directly (no transport),
|
|
5
|
+
* and returns the MCP tool response.
|
|
6
|
+
*/
|
|
7
|
+
import type { McpHarness, McpHarnessOptions } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Create an MCP harness for integration testing.
|
|
10
|
+
*
|
|
11
|
+
* Builds MCP tools from the app's topo and provides a `callTool()` method
|
|
12
|
+
* that invokes tools directly without any transport layer.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* const harness = createMcpHarness({ app });
|
|
16
|
+
* const result = await harness.callTool("myapp_entity_show", { name: "Alpha" });
|
|
17
|
+
* expect(result.isError).toBe(false);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare const createMcpHarness: (options: McpHarnessOptions) => McpHarness;
|
|
21
|
+
//# sourceMappingURL=harness-mcp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness-mcp.d.ts","sourceRoot":"","sources":["../src/harness-mcp.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EAElB,MAAM,YAAY,CAAC;AAMpB;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,GAAI,SAAS,iBAAiB,KAAG,UAgC7D,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP integration test harness.
|
|
3
|
+
*
|
|
4
|
+
* Builds MCP tools from an App, invokes them directly (no transport),
|
|
5
|
+
* and returns the MCP tool response.
|
|
6
|
+
*/
|
|
7
|
+
import { buildMcpTools } from '@ontrails/mcp';
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// createMcpHarness
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/**
|
|
12
|
+
* Create an MCP harness for integration testing.
|
|
13
|
+
*
|
|
14
|
+
* Builds MCP tools from the app's topo and provides a `callTool()` method
|
|
15
|
+
* that invokes tools directly without any transport layer.
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* const harness = createMcpHarness({ app });
|
|
19
|
+
* const result = await harness.callTool("myapp_entity_show", { name: "Alpha" });
|
|
20
|
+
* expect(result.isError).toBe(false);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export const createMcpHarness = (options) => {
|
|
24
|
+
const tools = buildMcpTools(options.app);
|
|
25
|
+
const toolMap = new Map();
|
|
26
|
+
for (const tool of tools) {
|
|
27
|
+
toolMap.set(tool.name, tool);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
async callTool(name, args) {
|
|
31
|
+
const tool = toolMap.get(name);
|
|
32
|
+
if (tool === undefined) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ text: `Unknown tool: ${name}`, type: 'text' }],
|
|
35
|
+
isError: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const result = await tool.handler(args, {
|
|
39
|
+
progressToken: undefined,
|
|
40
|
+
sendProgress: undefined,
|
|
41
|
+
signal: undefined,
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
content: result.content,
|
|
45
|
+
isError: result.isError ?? false,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=harness-mcp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness-mcp.js","sourceRoot":"","sources":["../src/harness-mcp.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAS9C,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,OAA0B,EAAc,EAAE;IACzE,MAAM,KAAK,GAAG,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACzC,MAAM,OAAO,GAAG,IAAI,GAAG,EAA6B,CAAC;IACrD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,OAAO;QACL,KAAK,CAAC,QAAQ,CACZ,IAAY,EACZ,IAA6B;YAE7B,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC/B,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;gBACvB,OAAO;oBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,iBAAiB,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;oBAC1D,OAAO,EAAE,IAAI;iBACd,CAAC;YACJ,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE;gBACtC,aAAa,EAAE,SAAS;gBACxB,YAAY,EAAE,SAAS;gBACvB,MAAM,EAAE,SAAS;aAClB,CAAC,CAAC;YAEH,OAAO;gBACL,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,KAAK;aACjC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC,CAAC"}
|
package/dist/hike.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
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 type { AnyHike, AnyTrail, TrailContext } from '@ontrails/core';
|
|
8
|
+
import type { HikeScenario } from './types.js';
|
|
9
|
+
/** Options for testHike that provide trail definitions for injection. */
|
|
10
|
+
export interface TestHikeOptions {
|
|
11
|
+
/** Partial context overrides. */
|
|
12
|
+
readonly ctx?: Partial<TrailContext> | undefined;
|
|
13
|
+
/** Map of trail ID to trail definition, used for injectFromExample. */
|
|
14
|
+
readonly trails?: ReadonlyMap<string, AnyTrail> | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generate a describe block for a hike with one test per scenario.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* testHike(onboardHike, [
|
|
22
|
+
* {
|
|
23
|
+
* description: "follows add then relate",
|
|
24
|
+
* input: { name: "Alpha" },
|
|
25
|
+
* expectOk: true,
|
|
26
|
+
* expectFollowed: ["entity.add", "entity.relate"],
|
|
27
|
+
* },
|
|
28
|
+
* ]);
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare const testHike: (hikeDef: AnyHike, scenarios: readonly HikeScenario[], options?: TestHikeOptions) => void;
|
|
32
|
+
//# sourceMappingURL=hike.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hike.d.ts","sourceRoot":"","sources":["../src/hike.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAY,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAehF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA8N/C,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,iCAAiC;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,SAAS,CAAC;IACjD,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,SAAS,CAAC;CAC7D;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,QAAQ,GACnB,SAAS,OAAO,EAChB,WAAW,SAAS,YAAY,EAAE,EAClC,UAAU,eAAe,KACxB,IASF,CAAC"}
|