trxa-tool 0.1.1 → 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/MANUAL.md +376 -368
- package/README.md +75 -51
- package/SETUP.md +401 -0
- package/STUDIO.md +417 -309
- package/dist/fixture-schema.d.ts +45 -5
- package/dist/fixture-schema.js +97 -13
- package/dist/fixture-store.d.ts +37 -0
- package/dist/fixture-store.js +75 -0
- package/dist/index.js +76 -40
- package/dist/mapping.js +4 -1
- package/dist/mcp-bridge.d.ts +52 -0
- package/dist/mcp-bridge.js +290 -0
- package/dist/planrun-schema.d.ts +14 -6
- package/dist/planrun-schema.js +31 -7
- package/dist/planrun.d.ts +1 -1
- package/dist/planrun.js +2 -2
- package/dist/report-schema.d.ts +10 -1
- package/dist/report-schema.js +31 -5
- package/dist/report.d.ts +4 -1
- package/dist/report.js +9 -2
- package/dist/suite-runner.js +8 -2
- package/media/studio/fixtures-test-cases.png +0 -0
- package/media/studio/plans-editor.png +0 -0
- package/media/studio/recordings-create-test-case.png +0 -0
- package/media/studio/run-dialog.png +0 -0
- package/media/studio/settings-agent-runs.png +0 -0
- package/media/studio/settings-working-folder.png +0 -0
- package/media/studio/studio-recordings.png +0 -0
- package/media/studio/suites-canvas.png +0 -0
- package/media/studio/suites-inspector.png +0 -0
- package/media/studio/testcases-expectations.png +0 -0
- package/media/studio/testcases-outputs.png +0 -0
- package/media/studio/testrun-details.png +0 -0
- package/media/studio/testruns-list.png +0 -0
- package/package.json +6 -2
- package/studio/client-dist/assets/CreatePanel-C5bb5OIs.js +6 -0
- package/studio/client-dist/assets/FactTile-G1pURLys.js +1 -0
- package/studio/client-dist/assets/FieldLabel-DPmIEQ08.js +1 -0
- package/studio/client-dist/assets/FixtureNamePicker-137AAgwZ.js +1 -0
- package/studio/client-dist/assets/Fixtures-D0t-soph.js +1 -0
- package/studio/client-dist/assets/GeneralSection-CWkTeSX2.js +1 -0
- package/studio/client-dist/assets/ListPaneHeader-DXKqX36N.js +1 -0
- package/studio/client-dist/assets/Plans-C70evkGc.js +1 -0
- package/studio/client-dist/assets/Recordings-BT0dyVLs.js +1 -0
- package/studio/client-dist/assets/Settings-k-J3u5lH.js +1 -0
- package/studio/client-dist/assets/Suites-DLioOiRN.css +1 -0
- package/studio/client-dist/assets/Suites-DQhhMxjT.js +7 -0
- package/studio/client-dist/assets/TestCases-BdiopVGI.js +1 -0
- package/studio/client-dist/assets/TestRuns-CylAe-3t.js +9 -0
- package/studio/client-dist/assets/api-DJZ703_F.js +24 -0
- package/studio/client-dist/assets/bucket-18-viuwGcaj.js +1 -0
- package/studio/client-dist/assets/fixtureName-CTZT_jKu.js +1 -0
- package/studio/client-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-300-normal-LoaTnByX.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-300-normal-RhT0Ayi3.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-400-italic-Bf1n82Tb.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-400-italic-D9J6q4_O.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-300-normal-Bbd5v1xK.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-300-normal-DqmgDkIN.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-400-italic-BELOEI8R.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-400-italic-CfSXsIbO.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-greek-300-normal-J-CBqZoJ.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-greek-300-normal-STc8XiDr.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-greek-400-italic-BEZwmy7D.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-greek-400-italic-CTYLy0lX.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-300-normal-C9Of02-5.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-300-normal-b4i7YzLG.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-400-italic-CZTNEAuW.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-400-italic-CsGl1sm0.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-ext-300-normal-B8Zgwjh3.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-ext-300-normal-BLFuR0P1.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-ext-400-italic-Du9zzoL2.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-ext-400-italic-V_9i1P_m.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-vietnamese-300-normal-CkYRZD2a.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-vietnamese-300-normal-SDfX6ktj.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-vietnamese-400-italic-78jqtfZH.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-vietnamese-400-italic-BbRnwouv.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
- package/studio/client-dist/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
- package/studio/client-dist/assets/index-cxBc2iRv.js +10 -0
- package/studio/client-dist/assets/index-wjWuVHXN.css +1 -0
- package/studio/client-dist/assets/status-DUGxC7G7.js +1 -0
- package/studio/client-dist/assets/testCaseTree-Cl1-bc5O.js +1 -0
- package/studio/client-dist/axtr-icon.svg +2 -5
- package/studio/client-dist/favicon.svg +2 -5
- package/studio/client-dist/index.html +5 -4
- package/studio/server/dist/index.js +1530 -292
- package/studio/client-dist/assets/index-CgWhGkEn.js +0 -24
- package/studio/client-dist/assets/index-CwAUMVCL.css +0 -1
package/dist/fixture-schema.d.ts
CHANGED
|
@@ -1,12 +1,52 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* case
|
|
1
|
+
/** A named set of input values, e.g. "USMF" or "EU company", holding one slice of values per test
|
|
2
|
+
* case it covers. Kept separate from test cases so the same test case can run against different
|
|
3
|
+
* data without editing it, and so one data set reads as one thing even when it spans many test
|
|
4
|
+
* cases. One fixture is one `testdata/<name>.json` file.
|
|
5
|
+
*
|
|
6
|
+
* Suites bind to a fixture by `name` and a param (see suite-schema.ts); at run time the slice for
|
|
7
|
+
* the test case being run is used (suite-runner.ts's `resolveInputs`). */
|
|
3
8
|
export interface Fixture {
|
|
9
|
+
/** Identifier suites refer to, and the file name (`testdata/<name>.json`). Filename safe, see
|
|
10
|
+
* `validateFixtureName`. */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Short free-text description shown alongside `name` in Studio. Purely descriptive. */
|
|
13
|
+
description: string;
|
|
14
|
+
/** testCaseId -> (param name -> value). */
|
|
15
|
+
testCases: Record<string, Record<string, string>>;
|
|
16
|
+
}
|
|
17
|
+
/** The format before 2026-11: one file per (test case, fixture name) pair, each repeating the
|
|
18
|
+
* fixture's name and description. Still read by the CLI (merged by name, see `mergeFixtures`) so
|
|
19
|
+
* an existing library keeps working; `trxa migrate-fixtures` rewrites them in the current format. */
|
|
20
|
+
export interface LegacyFixture {
|
|
4
21
|
testCaseId: string;
|
|
5
22
|
name: string;
|
|
6
|
-
/** Short free-text description shown alongside `name` in Studio's Fixtures list (e.g. "EU
|
|
7
|
-
* vendor, small item") -- distinct from `name`, which stays a filename-safe identifier
|
|
8
|
-
* referenced by suites' `inputBindings`. Purely descriptive; nothing resolves against it. */
|
|
9
23
|
description: string;
|
|
10
24
|
values: Record<string, string>;
|
|
11
25
|
}
|
|
26
|
+
/** Longest fixture name accepted. */
|
|
27
|
+
export declare const FIXTURE_NAME_MAX_LENGTH = 64;
|
|
28
|
+
/** Returns why `name` can't be a fixture name, or null when it can. A fixture's name is also its
|
|
29
|
+
* file name, so it is limited to letters, digits, spaces, dots, underscores and hyphens, starting
|
|
30
|
+
* with a letter or digit and not ending with a dot or space (Windows drops those silently). */
|
|
31
|
+
export declare function fixtureNameProblem(name: string): string | null;
|
|
32
|
+
/** True when `value` looks like the pre-2026-11 one-test-case-per-file format. */
|
|
33
|
+
export declare function isLegacyFixture(value: unknown): boolean;
|
|
12
34
|
export declare function validateFixture(value: unknown): Fixture;
|
|
35
|
+
export declare function validateLegacyFixture(value: unknown): LegacyFixture;
|
|
36
|
+
/** Reads either format. A legacy file becomes a Fixture covering its one test case. Its name is
|
|
37
|
+
* not checked against `fixtureNameProblem`, since legacy names were never limited and still have
|
|
38
|
+
* to load; `trxa migrate-fixtures` reports a name that can't become a file name. */
|
|
39
|
+
export declare function validateFixtureFile(value: unknown): Fixture;
|
|
40
|
+
/** Merges fixtures that share a name (case-insensitively, since file names on Windows are) into
|
|
41
|
+
* one each, in input order. The first non-empty description wins. Two sources holding values for
|
|
42
|
+
* the same test case under the same name is ambiguous, so it throws, naming both sources. */
|
|
43
|
+
export declare function mergeFixtures(sources: {
|
|
44
|
+
source: string;
|
|
45
|
+
fixture: Fixture;
|
|
46
|
+
}[]): {
|
|
47
|
+
fixture: Fixture;
|
|
48
|
+
sources: string[];
|
|
49
|
+
}[];
|
|
50
|
+
/** Writes a fixture the way every tool writes one, with test cases in a stable order so a saved
|
|
51
|
+
* file diffs cleanly. */
|
|
52
|
+
export declare function serializeFixture(fixture: Fixture): string;
|
package/dist/fixture-schema.js
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/** Longest fixture name accepted. */
|
|
2
|
+
export const FIXTURE_NAME_MAX_LENGTH = 64;
|
|
3
|
+
const FIXTURE_NAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9 ._-]*[A-Za-z0-9_-])?$/;
|
|
4
|
+
/** Returns why `name` can't be a fixture name, or null when it can. A fixture's name is also its
|
|
5
|
+
* file name, so it is limited to letters, digits, spaces, dots, underscores and hyphens, starting
|
|
6
|
+
* with a letter or digit and not ending with a dot or space (Windows drops those silently). */
|
|
7
|
+
export function fixtureNameProblem(name) {
|
|
8
|
+
if (name.length === 0)
|
|
9
|
+
return "must not be empty";
|
|
10
|
+
if (name.length > FIXTURE_NAME_MAX_LENGTH)
|
|
11
|
+
return `must be at most ${FIXTURE_NAME_MAX_LENGTH} characters`;
|
|
12
|
+
if (!FIXTURE_NAME_PATTERN.test(name)) {
|
|
13
|
+
return "may only use letters, digits, spaces, dots, underscores and hyphens, starting with a letter or digit and not ending with a dot or space";
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
1
17
|
function fail(field, reason) {
|
|
2
18
|
throw new Error(`Invalid Fixture: ${field} ${reason}`);
|
|
3
19
|
}
|
|
@@ -5,24 +21,92 @@ function assertString(value, field) {
|
|
|
5
21
|
if (typeof value !== "string" || value.length === 0)
|
|
6
22
|
fail(field, "must be a non-empty string");
|
|
7
23
|
}
|
|
24
|
+
function assertStringMap(value, field) {
|
|
25
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
26
|
+
fail(field, "must be an object mapping param names to string values");
|
|
27
|
+
}
|
|
28
|
+
for (const [key, val] of Object.entries(value)) {
|
|
29
|
+
if (typeof val !== "string")
|
|
30
|
+
fail(`${field}.${key}`, "must be a string");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** True when `value` looks like the pre-2026-11 one-test-case-per-file format. */
|
|
34
|
+
export function isLegacyFixture(value) {
|
|
35
|
+
return typeof value === "object" && value !== null && "testCaseId" in value && !("testCases" in value);
|
|
36
|
+
}
|
|
8
37
|
export function validateFixture(value) {
|
|
9
38
|
if (typeof value !== "object" || value === null)
|
|
10
39
|
fail("fixture", "must be an object");
|
|
11
40
|
const v = value;
|
|
12
|
-
assertString(v.testCaseId, "testCaseId");
|
|
13
41
|
assertString(v.name, "name");
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
fail("
|
|
42
|
+
const problem = fixtureNameProblem(v.name);
|
|
43
|
+
if (problem)
|
|
44
|
+
fail("name", problem);
|
|
45
|
+
if (typeof v.description !== "string")
|
|
46
|
+
fail("description", "must be a string");
|
|
47
|
+
if (typeof v.testCases !== "object" || v.testCases === null || Array.isArray(v.testCases)) {
|
|
48
|
+
fail("testCases", "must be an object mapping test case ids to their values");
|
|
17
49
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
50
|
+
const testCases = {};
|
|
51
|
+
for (const [testCaseId, values] of Object.entries(v.testCases)) {
|
|
52
|
+
if (testCaseId.length === 0)
|
|
53
|
+
fail("testCases", "must not contain an empty test case id");
|
|
54
|
+
assertStringMap(values, `testCases.${testCaseId}`);
|
|
55
|
+
testCases[testCaseId] = { ...values };
|
|
21
56
|
}
|
|
22
|
-
return {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
57
|
+
return { name: v.name, description: v.description, testCases };
|
|
58
|
+
}
|
|
59
|
+
export function validateLegacyFixture(value) {
|
|
60
|
+
if (typeof value !== "object" || value === null)
|
|
61
|
+
fail("fixture", "must be an object");
|
|
62
|
+
const v = value;
|
|
63
|
+
assertString(v.testCaseId, "testCaseId");
|
|
64
|
+
assertString(v.name, "name");
|
|
65
|
+
if (typeof v.description !== "string")
|
|
66
|
+
fail("description", "must be a string");
|
|
67
|
+
assertStringMap(v.values, "values");
|
|
68
|
+
return { testCaseId: v.testCaseId, name: v.name, description: v.description, values: { ...v.values } };
|
|
69
|
+
}
|
|
70
|
+
/** Reads either format. A legacy file becomes a Fixture covering its one test case. Its name is
|
|
71
|
+
* not checked against `fixtureNameProblem`, since legacy names were never limited and still have
|
|
72
|
+
* to load; `trxa migrate-fixtures` reports a name that can't become a file name. */
|
|
73
|
+
export function validateFixtureFile(value) {
|
|
74
|
+
if (!isLegacyFixture(value))
|
|
75
|
+
return validateFixture(value);
|
|
76
|
+
const legacy = validateLegacyFixture(value);
|
|
77
|
+
return { name: legacy.name, description: legacy.description, testCases: { [legacy.testCaseId]: legacy.values } };
|
|
78
|
+
}
|
|
79
|
+
/** Merges fixtures that share a name (case-insensitively, since file names on Windows are) into
|
|
80
|
+
* one each, in input order. The first non-empty description wins. Two sources holding values for
|
|
81
|
+
* the same test case under the same name is ambiguous, so it throws, naming both sources. */
|
|
82
|
+
export function mergeFixtures(sources) {
|
|
83
|
+
const merged = new Map();
|
|
84
|
+
for (const { source, fixture } of sources) {
|
|
85
|
+
const key = fixture.name.toLowerCase();
|
|
86
|
+
let entry = merged.get(key);
|
|
87
|
+
if (!entry) {
|
|
88
|
+
entry = { fixture: { name: fixture.name, description: fixture.description, testCases: {} }, sources: [], slices: new Map() };
|
|
89
|
+
merged.set(key, entry);
|
|
90
|
+
}
|
|
91
|
+
if (!entry.fixture.description && fixture.description)
|
|
92
|
+
entry.fixture.description = fixture.description;
|
|
93
|
+
entry.sources.push(source);
|
|
94
|
+
for (const [testCaseId, values] of Object.entries(fixture.testCases)) {
|
|
95
|
+
const previous = entry.slices.get(testCaseId);
|
|
96
|
+
if (previous) {
|
|
97
|
+
throw new Error(`Fixture "${fixture.name}" has values for test case "${testCaseId}" in both ${previous} and ${source}. Keep one of them.`);
|
|
98
|
+
}
|
|
99
|
+
entry.slices.set(testCaseId, source);
|
|
100
|
+
entry.fixture.testCases[testCaseId] = { ...values };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return [...merged.values()].map(({ fixture, sources }) => ({ fixture, sources }));
|
|
104
|
+
}
|
|
105
|
+
/** Writes a fixture the way every tool writes one, with test cases in a stable order so a saved
|
|
106
|
+
* file diffs cleanly. */
|
|
107
|
+
export function serializeFixture(fixture) {
|
|
108
|
+
const testCases = {};
|
|
109
|
+
for (const id of Object.keys(fixture.testCases).sort((a, b) => a.localeCompare(b)))
|
|
110
|
+
testCases[id] = fixture.testCases[id];
|
|
111
|
+
return JSON.stringify({ name: fixture.name, description: fixture.description, testCases }, null, 2) + "\n";
|
|
28
112
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type Fixture } from "./fixture-schema.js";
|
|
2
|
+
export interface FixtureFolder {
|
|
3
|
+
/** Every fixture in the folder, with legacy files merged into the fixture they name. */
|
|
4
|
+
fixtures: {
|
|
5
|
+
fixture: Fixture;
|
|
6
|
+
files: string[];
|
|
7
|
+
}[];
|
|
8
|
+
/** File names (with extension) still in the pre-2026-11 one-test-case format. */
|
|
9
|
+
legacyFiles: string[];
|
|
10
|
+
/** Files that could not be read as a fixture at all. */
|
|
11
|
+
invalid: {
|
|
12
|
+
file: string;
|
|
13
|
+
error: string;
|
|
14
|
+
}[];
|
|
15
|
+
}
|
|
16
|
+
/** A missing folder reads as empty: a suite that binds no fixture doesn't need one to exist. */
|
|
17
|
+
export declare function readFixtureFolder(dir: string): Promise<FixtureFolder>;
|
|
18
|
+
/** A fixture is in its final shape when it is exactly one current format file named after it.
|
|
19
|
+
* Anything else (legacy files, or a file named differently from the fixture inside it) is what
|
|
20
|
+
* `migrateFixtureFolder` rewrites. */
|
|
21
|
+
export declare function needsMigration(fixture: Fixture, files: string[], legacyFiles: string[]): boolean;
|
|
22
|
+
export interface MigrationResult {
|
|
23
|
+
/** File written (`<name>.json`) and the files merged into it. */
|
|
24
|
+
written: {
|
|
25
|
+
file: string;
|
|
26
|
+
fixture: string;
|
|
27
|
+
from: string[];
|
|
28
|
+
}[];
|
|
29
|
+
/** Legacy files removed after their values were written. */
|
|
30
|
+
removed: string[];
|
|
31
|
+
}
|
|
32
|
+
/** Rewrites every legacy file in `dir` into one `<name>.json` per fixture, merging into a current
|
|
33
|
+
* format file of that name when one exists, then removes the legacy files. Everything is checked
|
|
34
|
+
* before anything is written, so a conflict or an unusable name leaves the folder untouched. */
|
|
35
|
+
export declare function migrateFixtureFolder(dir: string, options?: {
|
|
36
|
+
dryRun?: boolean;
|
|
37
|
+
}): Promise<MigrationResult>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Reading and migrating a fixtures folder (`testdata/`), shared by the CLI and Studio's server so
|
|
2
|
+
* both see the same fixtures and the same migration. */
|
|
3
|
+
import { readdir, readFile, writeFile, unlink } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fixtureNameProblem, isLegacyFixture, mergeFixtures, serializeFixture, validateFixtureFile, } from "./fixture-schema.js";
|
|
6
|
+
/** A missing folder reads as empty: a suite that binds no fixture doesn't need one to exist. */
|
|
7
|
+
export async function readFixtureFolder(dir) {
|
|
8
|
+
let entries;
|
|
9
|
+
try {
|
|
10
|
+
entries = (await readdir(dir)).filter((f) => f.endsWith(".json")).sort((a, b) => a.localeCompare(b));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return { fixtures: [], legacyFiles: [], invalid: [] };
|
|
14
|
+
}
|
|
15
|
+
const sources = [];
|
|
16
|
+
const legacyFiles = [];
|
|
17
|
+
const invalid = [];
|
|
18
|
+
for (const file of entries) {
|
|
19
|
+
try {
|
|
20
|
+
const raw = JSON.parse(await readFile(path.join(dir, file), "utf-8"));
|
|
21
|
+
if (isLegacyFixture(raw))
|
|
22
|
+
legacyFiles.push(file);
|
|
23
|
+
sources.push({ source: file, fixture: validateFixtureFile(raw) });
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
invalid.push({ file, error: err instanceof Error ? err.message : String(err) });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Files in the current format come first, so a fixture's own description wins over a legacy
|
|
30
|
+
// file's when both exist.
|
|
31
|
+
sources.sort((a, b) => Number(legacyFiles.includes(a.source)) - Number(legacyFiles.includes(b.source)));
|
|
32
|
+
const fixtures = mergeFixtures(sources).map(({ fixture, sources }) => ({ fixture, files: sources }));
|
|
33
|
+
return { fixtures, legacyFiles, invalid };
|
|
34
|
+
}
|
|
35
|
+
/** A fixture is in its final shape when it is exactly one current format file named after it.
|
|
36
|
+
* Anything else (legacy files, or a file named differently from the fixture inside it) is what
|
|
37
|
+
* `migrateFixtureFolder` rewrites. */
|
|
38
|
+
export function needsMigration(fixture, files, legacyFiles) {
|
|
39
|
+
if (files.some((f) => legacyFiles.includes(f)))
|
|
40
|
+
return true;
|
|
41
|
+
return files.length !== 1 || files[0].toLowerCase() !== `${fixture.name}.json`.toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
/** Rewrites every legacy file in `dir` into one `<name>.json` per fixture, merging into a current
|
|
44
|
+
* format file of that name when one exists, then removes the legacy files. Everything is checked
|
|
45
|
+
* before anything is written, so a conflict or an unusable name leaves the folder untouched. */
|
|
46
|
+
export async function migrateFixtureFolder(dir, options = {}) {
|
|
47
|
+
const folder = await readFixtureFolder(dir);
|
|
48
|
+
if (folder.invalid.length > 0) {
|
|
49
|
+
throw new Error(`Fix or remove these files first: ${folder.invalid.map((i) => `${i.file} (${i.error})`).join("; ")}`);
|
|
50
|
+
}
|
|
51
|
+
const plan = [];
|
|
52
|
+
for (const { fixture, files } of folder.fixtures) {
|
|
53
|
+
if (!needsMigration(fixture, files, folder.legacyFiles))
|
|
54
|
+
continue;
|
|
55
|
+
const problem = fixtureNameProblem(fixture.name);
|
|
56
|
+
if (problem) {
|
|
57
|
+
throw new Error(`Fixture name "${fixture.name}" (in ${files.join(", ")}) can't be used as a file name: it ${problem}. Rename it first.`);
|
|
58
|
+
}
|
|
59
|
+
const file = `${fixture.name}.json`;
|
|
60
|
+
plan.push({ file, fixture, from: files });
|
|
61
|
+
}
|
|
62
|
+
// Every source merged into <name>.json is removed afterwards, except <name>.json itself, which
|
|
63
|
+
// is overwritten in place (compared case-insensitively, as Windows does).
|
|
64
|
+
const result = {
|
|
65
|
+
written: plan.map(({ file, fixture, from }) => ({ file, fixture: fixture.name, from })),
|
|
66
|
+
removed: plan.flatMap(({ file, from }) => from.filter((f) => f.toLowerCase() !== file.toLowerCase())),
|
|
67
|
+
};
|
|
68
|
+
if (options.dryRun)
|
|
69
|
+
return result;
|
|
70
|
+
for (const { file, fixture } of plan)
|
|
71
|
+
await writeFile(path.join(dir, file), serializeFixture(fixture), "utf-8");
|
|
72
|
+
for (const file of result.removed)
|
|
73
|
+
await unlink(path.join(dir, file));
|
|
74
|
+
return result;
|
|
75
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFile, writeFile,
|
|
2
|
+
import { readFile, writeFile, mkdir, unlink } from "node:fs/promises";
|
|
3
|
+
import { runMcpBridge } from "./mcp-bridge.js";
|
|
3
4
|
import { existsSync } from "node:fs";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
5
6
|
import path from "node:path";
|
|
@@ -10,7 +11,8 @@ import { analyzeLibrary } from "./analyze.js";
|
|
|
10
11
|
import { toCsv } from "./csv.js";
|
|
11
12
|
import { suggestInputs } from "./suggest-inputs.js";
|
|
12
13
|
import { validateTestCase } from "./testcase-schema.js";
|
|
13
|
-
import {
|
|
14
|
+
import { isLegacyFixture, validateFixtureFile } from "./fixture-schema.js";
|
|
15
|
+
import { migrateFixtureFolder, readFixtureFolder } from "./fixture-store.js";
|
|
14
16
|
import { validateTestSuite } from "./suite-schema.js";
|
|
15
17
|
import { validateTestPlan } from "./plan-schema.js";
|
|
16
18
|
import { topologicalOrder, resolveInputs, nextStep } from "./suite-runner.js";
|
|
@@ -70,28 +72,19 @@ async function loadTestCaseFile(testcasesDir, testCaseId) {
|
|
|
70
72
|
return validateTestCase(raw);
|
|
71
73
|
}
|
|
72
74
|
/** `--fixtures-dir` is optional at the CLI level (see `resolve-next`'s usage) -- if omitted, this
|
|
73
|
-
* function is never called at all and `fixtures` is just `[]`.
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
75
|
+
* function is never called at all and `fixtures` is just `[]`. A folder that doesn't exist reads
|
|
76
|
+
* as empty rather than a hard error (see fixture-store.ts): a suite that binds nothing via
|
|
77
|
+
* `fixture` kind doesn't need one to exist. The tradeoff is a mistyped `--fixtures-dir` path
|
|
78
|
+
* silently produces zero fixtures instead of an error naming the bad path -- the first symptom a
|
|
79
|
+
* reviewer sees is `suite-runner.ts`'s later, less specific "no fixture named X found". Files in
|
|
80
|
+
* the pre-2026-11 one-test-case format are merged into the fixture they name, so an unmigrated
|
|
81
|
+
* library still runs. */
|
|
79
82
|
async function loadFixtures(fixturesDir) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
return [];
|
|
86
|
-
}
|
|
87
|
-
const fixtures = [];
|
|
88
|
-
for (const entry of entries) {
|
|
89
|
-
if (!entry.endsWith(".json"))
|
|
90
|
-
continue;
|
|
91
|
-
const raw = JSON.parse(await readFile(path.join(fixturesDir, entry), "utf-8"));
|
|
92
|
-
fixtures.push(validateFixture(raw));
|
|
83
|
+
const folder = await readFixtureFolder(fixturesDir);
|
|
84
|
+
if (folder.invalid.length > 0) {
|
|
85
|
+
throw new Error(`Invalid fixture file ${folder.invalid[0].file}: ${folder.invalid[0].error}`);
|
|
93
86
|
}
|
|
94
|
-
return fixtures;
|
|
87
|
+
return folder.fixtures.map((f) => f.fixture);
|
|
95
88
|
}
|
|
96
89
|
async function loadSuite(suitePath) {
|
|
97
90
|
const raw = JSON.parse(await readFile(suitePath, "utf-8"));
|
|
@@ -134,8 +127,26 @@ async function cmdValidateFixture(args) {
|
|
|
134
127
|
if (!filePath)
|
|
135
128
|
throw new Error("Usage: trxa validate-fixture <fixture.json>");
|
|
136
129
|
const raw = JSON.parse(await readFile(filePath, "utf-8"));
|
|
137
|
-
const fixture =
|
|
138
|
-
|
|
130
|
+
const fixture = validateFixtureFile(raw);
|
|
131
|
+
const ids = Object.keys(fixture.testCases);
|
|
132
|
+
const legacy = isLegacyFixture(raw) ? " (old one test case per file format; run `trxa migrate-fixtures` to update it)" : "";
|
|
133
|
+
console.error(`OK: "${fixture.name}" is a valid fixture with values for ${ids.length} test case(s): ${ids.join(", ")}${legacy}`);
|
|
134
|
+
}
|
|
135
|
+
async function cmdMigrateFixtures(args) {
|
|
136
|
+
const [dir, ...rest] = args;
|
|
137
|
+
if (!dir)
|
|
138
|
+
throw new Error("Usage: trxa migrate-fixtures <testdata-dir> [--dry-run]");
|
|
139
|
+
const dryRun = rest.includes("--dry-run");
|
|
140
|
+
const result = await migrateFixtureFolder(dir, { dryRun });
|
|
141
|
+
if (result.written.length === 0) {
|
|
142
|
+
console.error("Nothing to migrate: every fixture is already one file named after it.");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const verb = dryRun ? "Would write" : "Wrote";
|
|
146
|
+
for (const w of result.written)
|
|
147
|
+
console.error(`${verb} ${w.file} (fixture "${w.fixture}") from ${w.from.join(", ")}`);
|
|
148
|
+
if (result.removed.length > 0)
|
|
149
|
+
console.error(`${dryRun ? "Would remove" : "Removed"}: ${result.removed.join(", ")}`);
|
|
139
150
|
}
|
|
140
151
|
async function cmdValidateSuite(args) {
|
|
141
152
|
const [filePath] = args;
|
|
@@ -189,21 +200,33 @@ async function cmdInitSkill(args) {
|
|
|
189
200
|
`(${SKILL_INSTALL_TARGETS.map((t) => t.relativePath).join(", ")}) -- each tool will pick it up ` +
|
|
190
201
|
`automatically the next time it runs in this project.`);
|
|
191
202
|
}
|
|
203
|
+
/** Started by an agent CLI as its `d365-erp-mcp` server; settings come from environment variables. */
|
|
204
|
+
async function cmdMcpBridge() {
|
|
205
|
+
try {
|
|
206
|
+
await runMcpBridge(process.env);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
// The agent shows this in its MCP server log; a stack trace would bury the setting to fix.
|
|
210
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
211
|
+
process.exitCode = 1;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
192
214
|
async function cmdInitRun(args) {
|
|
193
215
|
const [suitePath, ...rest] = args;
|
|
194
216
|
const outPath = flagValue(rest, "-o");
|
|
195
217
|
if (!suitePath || !outPath) {
|
|
196
|
-
throw new Error(
|
|
218
|
+
throw new Error('Usage: trxa init-run <suite.json> -o testruns/<suiteId>-<runId>.json [--name "<run name>"]');
|
|
197
219
|
}
|
|
198
220
|
const suite = await loadSuite(suitePath);
|
|
199
|
-
const
|
|
221
|
+
const name = flagValue(rest, "--name") ?? undefined;
|
|
222
|
+
const testRun = await initTestRun(suite, outPath, { name });
|
|
200
223
|
console.error(`Initialized test run for suite "${suite.id}" (${Object.keys(testRun.results).length} test cases, all pending) at ${outPath}`);
|
|
201
224
|
}
|
|
202
225
|
async function cmdInitPlanRun(args) {
|
|
203
226
|
const [planPath, ...rest] = args;
|
|
204
227
|
const outDir = flagValue(rest, "-o");
|
|
205
228
|
if (!planPath || !outDir) {
|
|
206
|
-
throw new Error(
|
|
229
|
+
throw new Error('Usage: trxa init-plan-run <plan.json> -o runs/ [--name "<run name>"]');
|
|
207
230
|
}
|
|
208
231
|
const plan = await loadPlan(planPath);
|
|
209
232
|
const planDir = path.dirname(planPath);
|
|
@@ -211,7 +234,8 @@ async function cmdInitPlanRun(args) {
|
|
|
211
234
|
const suite = await loadSuite(path.resolve(planDir, entry.suitePath));
|
|
212
235
|
return { id: suite.id, name: suite.name };
|
|
213
236
|
}));
|
|
214
|
-
const
|
|
237
|
+
const name = flagValue(rest, "--name") ?? undefined;
|
|
238
|
+
const { planRun, filePath } = await initPlanRun(plan.id, suites, outDir, name);
|
|
215
239
|
console.error(`Initialized plan run "${planRun.id}" for plan "${plan.id}" at ${filePath}`);
|
|
216
240
|
console.log(JSON.stringify(planRun, null, 2));
|
|
217
241
|
}
|
|
@@ -502,7 +526,7 @@ async function launchDetachedScript(scriptPath, cwd) {
|
|
|
502
526
|
const child = spawn("/bin/sh", [scriptPath], { cwd, detached: true, stdio: "ignore" });
|
|
503
527
|
child.unref();
|
|
504
528
|
if (!child.pid)
|
|
505
|
-
throw new Error("Failed to start
|
|
529
|
+
throw new Error("Failed to start TRXA Studio (no PID returned)");
|
|
506
530
|
return child.pid;
|
|
507
531
|
}
|
|
508
532
|
const psCommand = `$p = Start-Process -FilePath 'cmd.exe' -ArgumentList '/c','${scriptPath}' ` +
|
|
@@ -518,7 +542,7 @@ async function launchDetachedScript(scriptPath, cwd) {
|
|
|
518
542
|
ps.on("exit", (code) => {
|
|
519
543
|
const pid = Number(out.trim());
|
|
520
544
|
if (code !== 0 || !Number.isInteger(pid)) {
|
|
521
|
-
reject(new Error(`Failed to launch
|
|
545
|
+
reject(new Error(`Failed to launch TRXA Studio via PowerShell (exit ${code}): ${out.trim()}`));
|
|
522
546
|
}
|
|
523
547
|
else {
|
|
524
548
|
resolve(pid);
|
|
@@ -558,7 +582,7 @@ async function cmdStudioStart(repoRoot) {
|
|
|
558
582
|
const studioUrl = packagedServer ? `http://127.0.0.1:${process.env.STUDIO_SERVER_PORT ?? 4321}/` : STUDIO_URL;
|
|
559
583
|
const existingPid = await readStudioPid(pidFile);
|
|
560
584
|
if (existingPid !== null && isProcessAlive(existingPid)) {
|
|
561
|
-
console.error(`
|
|
585
|
+
console.error(`TRXA Studio is already running (PID ${existingPid}). Opening ${studioUrl} ...`);
|
|
562
586
|
openBrowser(studioUrl);
|
|
563
587
|
return;
|
|
564
588
|
}
|
|
@@ -589,13 +613,13 @@ async function cmdStudioStart(repoRoot) {
|
|
|
589
613
|
await writeFile(pidFile, String(pid), "utf-8");
|
|
590
614
|
const ready = await waitForServer(studioUrl, 20_000);
|
|
591
615
|
if (ready) {
|
|
592
|
-
console.error(`
|
|
616
|
+
console.error(`TRXA Studio is running in the background (PID ${pid}).\n` +
|
|
593
617
|
` UI: ${studioUrl}\n` +
|
|
594
618
|
` Logs: ${logFile}\n` +
|
|
595
619
|
` Stop: trxa studio stop`);
|
|
596
620
|
}
|
|
597
621
|
else {
|
|
598
|
-
console.error(`
|
|
622
|
+
console.error(`TRXA Studio (PID ${pid}) is taking longer than expected to come up -- check ${logFile}.\n` +
|
|
599
623
|
` Opening ${studioUrl} anyway; refresh if it's not ready yet.\n` +
|
|
600
624
|
` Stop: trxa studio stop`);
|
|
601
625
|
}
|
|
@@ -605,11 +629,11 @@ async function cmdStudioStop() {
|
|
|
605
629
|
const pidFile = path.join(STUDIO_RUN_DIR, "pid");
|
|
606
630
|
const pid = await readStudioPid(pidFile);
|
|
607
631
|
if (pid === null) {
|
|
608
|
-
console.error("
|
|
632
|
+
console.error("TRXA Studio doesn't appear to be running (no PID file found).");
|
|
609
633
|
return;
|
|
610
634
|
}
|
|
611
635
|
if (!isProcessAlive(pid)) {
|
|
612
|
-
console.error(`
|
|
636
|
+
console.error(`TRXA Studio isn't running (stale PID ${pid}) -- cleaning up.`);
|
|
613
637
|
await unlink(pidFile).catch(() => { });
|
|
614
638
|
return;
|
|
615
639
|
}
|
|
@@ -631,7 +655,7 @@ async function cmdStudioStop() {
|
|
|
631
655
|
}
|
|
632
656
|
}
|
|
633
657
|
await unlink(pidFile).catch(() => { });
|
|
634
|
-
console.error(`Stopped
|
|
658
|
+
console.error(`Stopped TRXA Studio (was PID ${pid}).`);
|
|
635
659
|
}
|
|
636
660
|
/** Launches (or stops) the Studio server+client, detached and backgrounded so `trxa studio`
|
|
637
661
|
* returns control immediately and auto-opens the browser. Delegates to the root `npm run studio`
|
|
@@ -665,6 +689,12 @@ const COMMAND_GROUPS = [
|
|
|
665
689
|
commands: [
|
|
666
690
|
{ name: "validate-testcase", usage: "<testcase.json>", summary: "Validate a Test Case file", handler: cmdValidateTestCase },
|
|
667
691
|
{ name: "validate-fixture", usage: "<fixture.json>", summary: "Validate a Fixture file", handler: cmdValidateFixture },
|
|
692
|
+
{
|
|
693
|
+
name: "migrate-fixtures",
|
|
694
|
+
usage: "<testdata-dir> [--dry-run]",
|
|
695
|
+
summary: "Merge one-test-case fixture files into one file per fixture",
|
|
696
|
+
handler: cmdMigrateFixtures,
|
|
697
|
+
},
|
|
668
698
|
{
|
|
669
699
|
name: "validate-suite",
|
|
670
700
|
usage: "<suite.json>",
|
|
@@ -689,6 +719,12 @@ const COMMAND_GROUPS = [
|
|
|
689
719
|
summary: "Copy the replay-trxa-testplan skill into this project for Claude Code and Codex to pick up",
|
|
690
720
|
handler: cmdInitSkill,
|
|
691
721
|
},
|
|
722
|
+
{
|
|
723
|
+
name: "mcp-bridge",
|
|
724
|
+
usage: "",
|
|
725
|
+
summary: "Connect an agent to a D365 environment's MCP server (started by the agent; see SETUP.md)",
|
|
726
|
+
handler: cmdMcpBridge,
|
|
727
|
+
},
|
|
692
728
|
],
|
|
693
729
|
},
|
|
694
730
|
{
|
|
@@ -696,14 +732,14 @@ const COMMAND_GROUPS = [
|
|
|
696
732
|
commands: [
|
|
697
733
|
{
|
|
698
734
|
name: "init-plan-run",
|
|
699
|
-
usage:
|
|
700
|
-
summary: "Create a new PlanRun record for a plan, auto-generating its id",
|
|
735
|
+
usage: '<plan.json> -o runs/ [--name "<run name>"]',
|
|
736
|
+
summary: "Create a new PlanRun record for a plan, auto-generating its id; --name labels the run",
|
|
701
737
|
handler: cmdInitPlanRun,
|
|
702
738
|
},
|
|
703
739
|
{
|
|
704
740
|
name: "init-run",
|
|
705
|
-
usage:
|
|
706
|
-
summary: "Create a new Test Run file for a suite, all test cases pending",
|
|
741
|
+
usage: '<suite.json> -o testruns/<suiteId>-<runId>.json [--name "<run name>"]',
|
|
742
|
+
summary: "Create a new Test Run file for a suite, all test cases pending; --name labels the run",
|
|
707
743
|
handler: cmdInitRun,
|
|
708
744
|
},
|
|
709
745
|
{
|
package/dist/mapping.js
CHANGED
|
@@ -371,10 +371,13 @@ function propertyHint(step) {
|
|
|
371
371
|
note: `Value was entered on a grid-bound control (UserActionType="${UserActionType}"); may be a filter rather than a cell edit.`,
|
|
372
372
|
};
|
|
373
373
|
}
|
|
374
|
+
// The recorder captures the invoice number's grid-cell name; the live Invoice number field
|
|
375
|
+
// on the vendor invoice dialog is PurchParmTable_Num (confirmed live, 2026-10-28/29 agent runs).
|
|
376
|
+
const liveControlName = ControlName === "PurchParmTable_gridParmTableNum" ? "PurchParmTable_Num" : ControlName;
|
|
374
377
|
return {
|
|
375
378
|
tool: "form_set_control_values",
|
|
376
379
|
confidence: "high",
|
|
377
|
-
params: { setControlValues: [{ controlName:
|
|
380
|
+
params: { setControlValues: [{ controlName: liveControlName, value: formattedValue }] },
|
|
378
381
|
followUp: [
|
|
379
382
|
{
|
|
380
383
|
tool: "form_save_form",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface BridgeConfig {
|
|
2
|
+
/** The environment's base URL, e.g. https://contoso.operations.dynamics.com */
|
|
3
|
+
environmentUrl: string;
|
|
4
|
+
tenantId: string;
|
|
5
|
+
clientId: string;
|
|
6
|
+
clientSecret: string;
|
|
7
|
+
requestTimeoutMs: number;
|
|
8
|
+
}
|
|
9
|
+
export interface HttpResponse {
|
|
10
|
+
status: number;
|
|
11
|
+
body: string;
|
|
12
|
+
sessionId: string | null;
|
|
13
|
+
}
|
|
14
|
+
/** What the bridge needs from the outside world; tests replace each one. */
|
|
15
|
+
export interface BridgeDeps {
|
|
16
|
+
post(body: string, sessionId: string | null, token: string, timeoutMs: number): Promise<HttpResponse>;
|
|
17
|
+
getToken(forceRefresh: boolean): Promise<string>;
|
|
18
|
+
write(line: string): void;
|
|
19
|
+
log(text: string): void;
|
|
20
|
+
sleep(ms: number): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export interface BridgeOptions {
|
|
23
|
+
requestTimeoutMs?: number;
|
|
24
|
+
/** `initialize` must answer within the client's own connect timeout (30 s in Claude Code). */
|
|
25
|
+
initializeTimeoutMs?: number;
|
|
26
|
+
/** Waits between attempts while the environment restarts (about 1 minute in total). */
|
|
27
|
+
retryDelaysMs?: number[];
|
|
28
|
+
}
|
|
29
|
+
interface JsonRpcMessage {
|
|
30
|
+
jsonrpc?: string;
|
|
31
|
+
id?: string | number | null;
|
|
32
|
+
method?: string;
|
|
33
|
+
params?: {
|
|
34
|
+
name?: string;
|
|
35
|
+
};
|
|
36
|
+
error?: {
|
|
37
|
+
message?: string;
|
|
38
|
+
};
|
|
39
|
+
result?: unknown;
|
|
40
|
+
}
|
|
41
|
+
/** The JSON-RPC messages in a JSON or server-sent-events body; anything else (HTML) is skipped. */
|
|
42
|
+
export declare function parseMessages(body: string): JsonRpcMessage[];
|
|
43
|
+
export declare function isReplaySafe(message: JsonRpcMessage | null): boolean;
|
|
44
|
+
/** The message-handling core, independent of stdio, HTTPS and MSAL. */
|
|
45
|
+
export declare function createBridge(deps: BridgeDeps, options?: BridgeOptions): {
|
|
46
|
+
handle: (line: string) => Promise<void>;
|
|
47
|
+
};
|
|
48
|
+
/** Reads the bridge settings from the environment variables SETUP.md documents. */
|
|
49
|
+
export declare function configFromEnv(env: NodeJS.ProcessEnv): BridgeConfig;
|
|
50
|
+
/** `trxa mcp-bridge`: runs until stdin closes. */
|
|
51
|
+
export declare function runMcpBridge(env?: NodeJS.ProcessEnv): Promise<void>;
|
|
52
|
+
export {};
|