trxa-tool 0.0.0-stage → 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/.claude/skills/replay-trxa-testplan/SKILL.md +627 -0
- package/LICENSE +120 -0
- package/MANUAL.md +556 -0
- package/README.md +91 -3
- package/SETUP.md +401 -0
- package/STUDIO.md +427 -0
- package/dist/analyze.d.ts +58 -0
- package/dist/analyze.js +122 -0
- package/dist/bpm.d.ts +40 -0
- package/dist/bpm.js +60 -0
- package/dist/build-test-script.d.ts +24 -0
- package/dist/build-test-script.js +40 -0
- package/dist/csv.d.ts +1 -0
- package/dist/csv.js +11 -0
- package/dist/extract.d.ts +14 -0
- package/dist/extract.js +34 -0
- package/dist/fixture-schema.d.ts +52 -0
- package/dist/fixture-schema.js +112 -0
- package/dist/fixture-store.d.ts +37 -0
- package/dist/fixture-store.js +75 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +850 -0
- package/dist/library-archive.d.ts +37 -0
- package/dist/library-archive.js +106 -0
- package/dist/mapping.d.ts +25 -0
- package/dist/mapping.js +393 -0
- package/dist/mcp-bridge.d.ts +52 -0
- package/dist/mcp-bridge.js +290 -0
- package/dist/normalize.d.ts +21 -0
- package/dist/normalize.js +43 -0
- package/dist/parse.d.ts +3 -0
- package/dist/parse.js +233 -0
- package/dist/plan-runner.d.ts +27 -0
- package/dist/plan-runner.js +36 -0
- package/dist/plan-schema.d.ts +27 -0
- package/dist/plan-schema.js +45 -0
- package/dist/planrun-schema.d.ts +53 -0
- package/dist/planrun-schema.js +111 -0
- package/dist/planrun.d.ts +20 -0
- package/dist/planrun.js +46 -0
- package/dist/report-schema.d.ts +92 -0
- package/dist/report-schema.js +115 -0
- package/dist/report.d.ts +45 -0
- package/dist/report.js +77 -0
- package/dist/schema.d.ts +96 -0
- package/dist/schema.js +10 -0
- package/dist/scrub.d.ts +44 -0
- package/dist/scrub.js +166 -0
- package/dist/suggest-inputs.d.ts +19 -0
- package/dist/suggest-inputs.js +92 -0
- package/dist/suite-runner.d.ts +59 -0
- package/dist/suite-runner.js +204 -0
- package/dist/suite-schema.d.ts +59 -0
- package/dist/suite-schema.js +95 -0
- package/dist/testcase-schema.d.ts +102 -0
- package/dist/testcase-schema.js +132 -0
- package/dist/token-usage.d.ts +38 -0
- package/dist/token-usage.js +95 -0
- 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 +62 -6
- 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 -0
- package/studio/client-dist/favicon.svg +2 -0
- package/studio/client-dist/index.html +15 -0
- package/studio/server/dist/index.js +3176 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: replay-trxa-testplan
|
|
3
|
+
description: Replay a D365 Finance & Operations Task Recorder test script (testscript.json, produced by TRXA-TOOL from a .axtr file) against a live/sandbox environment via the d365-erp-mcp form tools. Resolves low-confidence step hints at runtime, saves edits, handles confirmation/async-processing dialogs, and reports pass/fail from what actually happened in the session rather than from the recording. Use when the user asks to "replay a task recording", "run this test script", "dry-run this axtr file", "run this test suite/plan", or "test this business process against D365".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Replay an axtr Test Script against D365FO
|
|
7
|
+
|
|
8
|
+
## Input
|
|
9
|
+
|
|
10
|
+
A Test Script (`testscript.json`) produced by `trxa-tool`
|
|
11
|
+
(`trxa parse <file.axtr> -o testscript.json`). If the user only has a raw `.axtr` file, run
|
|
12
|
+
that first. Each entry in `steps[]` has: `sequence`, `description` (human text from the
|
|
13
|
+
recording), `formName`, `nodeType`, the full recorded `node`, and a `hint` (`tool`, `confidence`,
|
|
14
|
+
`params`, optional `note` and `followUp`) generated from the tool's own recording-to-MCP-call
|
|
15
|
+
mapping logic.
|
|
16
|
+
|
|
17
|
+
This skill folds in everything learned from live replays about the gaps between what a recording
|
|
18
|
+
captures and what the MCP tools actually need, so it is self-contained. For the Test Case, Fixture,
|
|
19
|
+
Suite and Plan file formats, `MANUAL.md` ships inside the installed package (with `README.md` and
|
|
20
|
+
`STUDIO.md`): locate the install (`npm root -g` for a global install, `npm root` for a local
|
|
21
|
+
one) and look under `trxa-tool/`.
|
|
22
|
+
|
|
23
|
+
### A note on where this skill runs
|
|
24
|
+
|
|
25
|
+
Every command below is written as `trxa <command> ...`, assuming a real install (`npm install
|
|
26
|
+
-g trxa-tool` or equivalent) where that's the correct, always-available invocation. If instead
|
|
27
|
+
you're working inside a clone of the tool's own source repo without having run `npm link`, use
|
|
28
|
+
`node dist/index.js <command> ...` in its place (see that repo's own `DEVELOPING.md`). `src/`,
|
|
29
|
+
`test/` and the design notes in `ARCHITECTURE.md` are source-checkout-only, never shipped in the
|
|
30
|
+
installed package — don't go looking for them unless you know you're in a source checkout.
|
|
31
|
+
|
|
32
|
+
## Before executing anything
|
|
33
|
+
|
|
34
|
+
1. **Confirm the target environment and scope with the user.** A test script can contain
|
|
35
|
+
irreversible actions (posting, invoicing, deleting). Ask how far to run — e.g. "navigation
|
|
36
|
+
only" vs "full run including posting" — before starting, the same way you would for any
|
|
37
|
+
hard-to-reverse action. Never assume a connected `d365-erp-mcp` session points at a safe
|
|
38
|
+
sandbox; check the company/session info from the first tool response and say what you see.
|
|
39
|
+
2. **Security preflight (optional but cheap).** If the Test Script's
|
|
40
|
+
`security.requiredPrivileges` is present, check it against the target user's access via
|
|
41
|
+
`get_security_coverage_for_object` / the `security-hierarchy-trace` skill, so a permissions
|
|
42
|
+
error doesn't get misreported as a business-logic failure.
|
|
43
|
+
3. **`trxa-tool`'s own tooling is read-only while replaying — never edit it to work around an
|
|
44
|
+
error you hit mid-run**, whether that means source (`src/`, only present if you have the tool's
|
|
45
|
+
own repo checkout) or the installed package's own compiled `dist/`. A CLI command failing is one
|
|
46
|
+
of two things, and they call for opposite responses: (a) a mismatch between your shell and how
|
|
47
|
+
you quoted the command (see the PowerShell note under `record-result`, step 4 of "Running as
|
|
48
|
+
part of a Test Suite" below, for a documented example) — fix your own invocation, not the tool;
|
|
49
|
+
or (b) an actual bug in the tool. For (b), stop and report it to the user with what you saw
|
|
50
|
+
instead of patching it yourself — whether/how to fix the tool mid-test-run isn't a call to make
|
|
51
|
+
unilaterally while real D365FO data may be involved, and if you're running from an installed
|
|
52
|
+
package there's usually no source to fix anyway (see "Reporting" below for what to do with a
|
|
53
|
+
mapping-gap finding in that case). If you do have a source checkout, `mapping.ts` is the one
|
|
54
|
+
deliberate exception, and only as an explicit, separate follow-up once a run is done — not a
|
|
55
|
+
reflex during the run itself. Rebuilding (`npm run build`) to pick up a source change someone
|
|
56
|
+
else already made is fine; writing the change is not yours to do here.
|
|
57
|
+
|
|
58
|
+
## Executing each step, in `sequence` order
|
|
59
|
+
|
|
60
|
+
1. **`hint.tool === "skip"`**: don't call any MCP tool — `"skip"` isn't a real `d365-erp-mcp`
|
|
61
|
+
tool, it's `mapping.ts`'s way of saying this step's `SystemGenerated=true` in the recording
|
|
62
|
+
(an automatic side effect, like a form part lazy-loading — not something the user did). Move
|
|
63
|
+
to the next step; note it in the final report as "no action needed" rather than a pass/fail.
|
|
64
|
+
2. **High confidence hints** (any other `tool`): call `hint.tool` with `hint.params` directly.
|
|
65
|
+
Date-typed `PropertyUserAction` values are already reformatted from the recording's ISO
|
|
66
|
+
(`yyyy-MM-dd`) into `M/d/yyyy` by `mapping.ts` -- confirmed live that some Date controls reject
|
|
67
|
+
ISO outright ("Dates must be in the format M/d/yyyy") even though the recording always captures
|
|
68
|
+
ISO, so this is handled for you; no extra reformatting needed on your end.
|
|
69
|
+
3. **Medium/low confidence hints**: do not guess blindly.
|
|
70
|
+
- For grid row selection (`form_select_grid_row`/`form_filter_grid` hints with `confidence:
|
|
71
|
+
"low"`): the recording only captured a row *position*, which doesn't carry across
|
|
72
|
+
environments or data. Derive the real target from the step's `description` and neighboring
|
|
73
|
+
steps (e.g. a customer/item/order mentioned nearby), or ask the user which record to use —
|
|
74
|
+
exactly as was done in the reference dry run when a recording said "find and select the
|
|
75
|
+
desired record" with no captured business key. Then: `form_filter_grid` (if there's a
|
|
76
|
+
usable filter value) → read the returned grid rows → `form_select_grid_row` with the
|
|
77
|
+
resolved `rowNumber` and the `marking` value the grid's own response guidance asks for
|
|
78
|
+
(check `FormState...Grid.Guidance` — most grids require `"Marked"` or `"Unmarked"`
|
|
79
|
+
explicitly).
|
|
80
|
+
- For tree navigation (`form_click_control` hints on a `Tree`-typed control, `confidence:
|
|
81
|
+
"medium"`, e.g. a category picker): same non-portability problem as grid rows, but the
|
|
82
|
+
resolution mechanics are different. `hint.params` only has `controlName` — the actual click
|
|
83
|
+
needs an `actionId` you read off the *live* tree, not the recording. Call
|
|
84
|
+
`form_click_control` with just `controlName` (no `actionId`) or inspect the current
|
|
85
|
+
`FormState.Tree[name].Nodes` first; each node has `Actions.Select`/`Actions.Expand`
|
|
86
|
+
formatted `"Select:<n>"`/`"Expand:<n>"` where `<n>` is a live-session index. Walk the tree
|
|
87
|
+
matching each node's `Text` against the business value you need (expanding parent nodes with
|
|
88
|
+
their `Expand:<n>` action first if the target isn't visible yet), then click with that
|
|
89
|
+
node's own `Select:<n>`.
|
|
90
|
+
- For the filter/sort command family (`GetFilters`, `ApplyFilterForTaskRecorder`,
|
|
91
|
+
`ApplyFiltersForTaskRecorder`, `SetSortsForTaskRecorder`, `ApplyFilters`): `mapping.ts` now
|
|
92
|
+
computes `gridColumnName` (as `DataSourceName_FieldName`) and `sortDirection`/
|
|
93
|
+
`gridColumnValue` directly from the step's embedded JSON, at medium confidence. The one
|
|
94
|
+
piece still missing is `gridName` — it's never recorded on these steps and isn't always
|
|
95
|
+
`"Grid"` (standard lookup popups commonly expose one grid literally named `"Grid"`, but
|
|
96
|
+
list-page forms don't) — identify it from the current `FormState`'s `Grid` section before
|
|
97
|
+
calling `form_filter_grid`/`form_sort_grid_column`. `GetFilters` itself maps to `skip`
|
|
98
|
+
(confirmed read-only in every occurrence across the library).
|
|
99
|
+
- For any other low-confidence hint: call `form_find_controls`/`form_find_menu_item` with the
|
|
100
|
+
recorded `ControlName`/`MenuItemName` as the search term first, and act on what comes back.
|
|
101
|
+
- **Grid row selection immediately after creating a new record may already be selected.**
|
|
102
|
+
Confirmed live on a general journal entry: the recording captured `MarkActiveRow` right
|
|
103
|
+
after clicking New, but the newly created row was already active — a lookup opened on the
|
|
104
|
+
header's Name field worked immediately, with no `form_select_grid_row` call needed first.
|
|
105
|
+
Before spending a call resolving a `MarkActiveRow` hint, check whether the action you're
|
|
106
|
+
about to take even needs an explicit selection.
|
|
107
|
+
- **A lookup's `form_select_grid_row` call can submit and close the lookup by itself.**
|
|
108
|
+
Confirmed on the same journal's Name-field lookup: calling it with `marking: "Marked"`
|
|
109
|
+
closed the popup and applied the value directly — the recording's separate "click the link
|
|
110
|
+
in the selected row" step (a second `NavigationAction`) wasn't needed, even though the
|
|
111
|
+
lookup's own `Guidance` text says to click the first column afterward. Check `FormState`
|
|
112
|
+
after marking before assuming the extra click is required.
|
|
113
|
+
- **A lookup selection can cascade defaults into other fields.** Selecting `GenJrn` as the
|
|
114
|
+
journal Name auto-filled the header's Description field ("General Journal") — not every
|
|
115
|
+
`PropertyUserAction` the recording captured is still necessary; check `FormState` after each
|
|
116
|
+
lookup selection for fields it already populated before setting them again.
|
|
117
|
+
- **`SegmentedEntry` controls (ledger account/dimension combinations, e.g.
|
|
118
|
+
`LedgerJournalTrans_AccountNum`) are not lookups** — `form_open_lookup` fails with "Control
|
|
119
|
+
is not a lookup control". Use `form_set_control_values` directly; a bare main account number
|
|
120
|
+
is enough (D365FO fills in default financial dimension segments itself, confirmed live:
|
|
121
|
+
`"110110"` resolved to `"110110--"` and looked up the account name), so there's no need to
|
|
122
|
+
reproduce the recording's full dimension string. If the recorded literal account doesn't
|
|
123
|
+
exist in the target environment, query a valid one first (e.g. `data_find_entities` against
|
|
124
|
+
`MainAccountLegalEntities` filtered to the target `LegalEntityId` and
|
|
125
|
+
`DoNotAllowManualEntry eq 'No'`) rather than guessing.
|
|
126
|
+
- **`SegmentedEntry` values can cascade defaults too, not just lookup selections.** Confirmed
|
|
127
|
+
on a fixed asset acquisition journal: setting `LedgerJournalTrans_AccountNum` to a fixed
|
|
128
|
+
asset number auto-populated the account name, the depreciation book, and the offset account
|
|
129
|
+
from that asset's configured Fixed Asset Posting Profile — none of those needed a separate
|
|
130
|
+
`form_set_control_values` call the recording implied.
|
|
131
|
+
- **A recorded lookup step immediately followed by a close, with no row selection in between,
|
|
132
|
+
usually means the user typed the value manually instead of picking one.** If a
|
|
133
|
+
`RequestPopup` step is directly followed by `RequestClose` (no `NavigationAction`/grid
|
|
134
|
+
selection between them), skip both and go straight to the next `PropertyUserAction`'s
|
|
135
|
+
`form_set_control_values` call rather than opening the lookup at all.
|
|
136
|
+
- **Not every journal type's "Validate" click triggers the async `SysOperationSandboxForm`
|
|
137
|
+
pattern.** Confirmed on a fixed asset journal: `Validate` returned the updated `FormState`
|
|
138
|
+
synchronously with no polling needed, unlike the general journal scenario above where it did.
|
|
139
|
+
Don't assume polling is required after every validate/post-type click — check whether the
|
|
140
|
+
response is already a normal `FormState` or a `SysOperationSandboxForm` first.
|
|
141
|
+
- **Posting can insert lines you never entered.** Confirmed on the same fixed asset scenario:
|
|
142
|
+
posting a single acquisition line auto-generated a second, system-inserted "Depreciation
|
|
143
|
+
adjustment" voucher line (a catch-up entry for depreciation missed between the asset's
|
|
144
|
+
configured service date and the acquisition date) that wasn't part of the recording or
|
|
145
|
+
anything set during replay. After posting, re-read the grid rather than assuming it still
|
|
146
|
+
matches only what was entered — extra system-generated lines are a real outcome to report,
|
|
147
|
+
not a bug in the replay.
|
|
148
|
+
- **An Action Pane tab click the recording captured may be skippable if the button is already
|
|
149
|
+
reachable without it.** Confirmed on a purchase order confirmation: the recording clicked the
|
|
150
|
+
"Purchase" tab before "Confirmation", but `buttonUpdatePurchaseOrder` ("Confirmation") was
|
|
151
|
+
already clickable directly from the list page's top-level `Button` set with no tab switch —
|
|
152
|
+
check the current `FormState`'s `Button`/`MenuButton` sections for the target control before
|
|
153
|
+
replaying an `AppBarTab` click that precedes it.
|
|
154
|
+
- **Async job duration varies a lot by operation — don't assume a fixed number of polls, but do
|
|
155
|
+
cap it.** Journal posting resolved in 2-4 `__TimerForAsyncTaskPolling` calls; the same pattern
|
|
156
|
+
on a purchase order confirmation (which also generates a printed confirmation document) took
|
|
157
|
+
8. Keep polling until the form changes away from `SysOperationSandboxForm`, up to a hard cap of
|
|
158
|
+
**20 polls** for a single async operation. If still on `SysOperationSandboxForm` after 20,
|
|
159
|
+
stop — treat it as a genuine failure (record it as `failed`/`blocked` per the "Handling a
|
|
160
|
+
genuine failure mid-run" section below, with whatever `FormState`/`Messages` the last poll
|
|
161
|
+
returned as the `actual` evidence) rather than polling indefinitely.
|
|
162
|
+
- **When a recording's `ChangeSelectedIndexInCache`/`NavigationAction` grid-selection step gives
|
|
163
|
+
no usable filter value** (e.g. "find and select the desired record" with nothing else to go
|
|
164
|
+
on), query the underlying business data first (`data_find_entities` against the relevant
|
|
165
|
+
entity, filtered to the status/condition the scenario needs -- e.g. an unconfirmed purchase
|
|
166
|
+
order via `PurchaseOrderHeadersV2`'s `DocumentApprovalStatus`) to pick a real, valid target
|
|
167
|
+
before touching the UI grid at all, then select it directly by the row it appears at once the
|
|
168
|
+
list is open.
|
|
169
|
+
- **A menu item or button the recording expects may not exist in the target environment at
|
|
170
|
+
all.** Confirmed on a vendor invoice matching scenario: the recorded flow submitted the
|
|
171
|
+
invoice through a Workflow drop-dialog, but that environment's `VendEditInvoice` form had no
|
|
172
|
+
Workflow button in its `Button`/`MenuButton` set whatsoever -- completion was a direct
|
|
173
|
+
`OK`/"Post" click instead. Check what's actually available in the current `FormState` rather
|
|
174
|
+
than assuming every recorded control exists; a missing control isn't necessarily a mapping
|
|
175
|
+
failure, it can be a genuine environment/configuration difference.
|
|
176
|
+
- **Invoice/quantity/price matching status lives in `FormState` combobox and grid columns, not
|
|
177
|
+
just Messages.** Confirmed on the same scenario: clicking "Update match status"
|
|
178
|
+
(`UpdateMatchStatus`) flipped the header `MatchVariance` combobox from `"Not performed"` to
|
|
179
|
+
`"Passed"`, and each line already carried its own `QuantityMatch`/`PriceMatch` values
|
|
180
|
+
(`"Passed"`) from matching against the linked product receipt. For any matching-status
|
|
181
|
+
scenario, read these fields back to confirm the business outcome, the same way `Messages` is
|
|
182
|
+
read for other steps.
|
|
183
|
+
- **A product's dimension requirements (Site/Size/Color/...) can cascade defaults the same way
|
|
184
|
+
`SegmentedEntry` does, but through an ordinary combobox/lookup control, not a segmented one.**
|
|
185
|
+
Confirmed on an item requiring `Site` and `Size` as product dimensions: setting `Site` on a
|
|
186
|
+
line auto-populated the `Warehouse` and `Variant` defaults configured for that site, without a
|
|
187
|
+
separate `form_set_control_values` call for them. Check `FormState` after setting any
|
|
188
|
+
dimension-typed field for fields it already filled in, the same way you would after a
|
|
189
|
+
`SegmentedEntry` value or lookup selection.
|
|
190
|
+
- **A ribbon button that looks disabled/missing can have more than one cause — check the
|
|
191
|
+
specific one before treating it as a mapping gap.** `buttonUpdateInvoice`/`buttonJournalInvoice`
|
|
192
|
+
availability was inconsistent across replay sessions; in one case the real cause was an
|
|
193
|
+
intentional vendor-level "blocked for invoicing" setting (a genuine business-rule block, not a
|
|
194
|
+
recording/mapping issue) rather than a missing control or wrong `FormState` path. Before
|
|
195
|
+
concluding a button is unreachable due to a mapping problem, check the record's own
|
|
196
|
+
configuration (vendor/customer/item hold or block flags) as a possible cause, not just the
|
|
197
|
+
form's `Button`/`MenuButton` set.
|
|
198
|
+
4. **Run any `hint.followUp` entries** after the main call — currently this only fires for
|
|
199
|
+
`form_set_control_values`, which requires a trailing `form_save_form` or the edit is lost.
|
|
200
|
+
`form_set_control_values`'s `setControlValues` also accepts multiple entries in one call
|
|
201
|
+
(confirmed live setting a line's Description and Credit amount together) — batch fields on the
|
|
202
|
+
same form together instead of one call per field when the recording has them as separate
|
|
203
|
+
consecutive `PropertyUserAction` steps.
|
|
204
|
+
5. **Inspect the tool response, not the recording, for what actually happened.** Every
|
|
205
|
+
`d365-erp-mcp` form tool response includes `FormState` with a `Messages` array (`Info` /
|
|
206
|
+
`Warning` / `Error`) and sometimes a `Summary`/`Guidance`. This — not the recording's
|
|
207
|
+
`description` text — is the source of truth for whether a step succeeded. A recording never
|
|
208
|
+
carries an "expected outcome"; treat any `Error` message or an unexpected form appearing as a
|
|
209
|
+
failure worth surfacing in the report even if the click itself "succeeded".
|
|
210
|
+
- **`FormState.Company` can change as a side effect of any click, not just a recognized
|
|
211
|
+
"intercompany" jump button.** Confirmed live: clicking `ButtonInterCompanyPurchTable`/
|
|
212
|
+
`ButtonInterCompanySalesTable` returns the new company directly in `FormState.Company`, no
|
|
213
|
+
extra lookup needed — but treat this as a general rule, not special-cased to those two
|
|
214
|
+
buttons. Re-read `FormState.Company` after *any* click and trust it over assuming the company
|
|
215
|
+
stays fixed for the rest of a test case; `companyId` is only a parameter on the navigation
|
|
216
|
+
tools (`form_open_menu_item`/`form_find_menu_item`/`api_invoke_action`) — every other form
|
|
217
|
+
tool call has no `companyId` at all and simply inherits whatever company the currently open
|
|
218
|
+
form is in.
|
|
219
|
+
6. **Handle forms the recording couldn't predict:**
|
|
220
|
+
- A `SysBoxForm` with `Summary: "A modal has appeared."` is a confirmation dialog — read its
|
|
221
|
+
`Label` text (it's real, runtime-only content) and click through if it matches the expected
|
|
222
|
+
flow.
|
|
223
|
+
- **A "Match found" duplicate-party dialog on a create-new-vendor/customer replay is expected
|
|
224
|
+
and by design, not a mapping problem or something needing fixture cleanup.** Suite authors
|
|
225
|
+
may deliberately reuse the same `Org_Name`/address fixture across runs (varying only the
|
|
226
|
+
account number, e.g. via an `autogenerated` binding) precisely because this pipeline's goal is
|
|
227
|
+
regression-testing the *process* — does creating a vendor still work end-to-end without error
|
|
228
|
+
— not generating unique, realistic data every time. Confirmed live: a fixture that always
|
|
229
|
+
supplies the same name surfaced a picker listing prior runs' own leftover records once the
|
|
230
|
+
environment already had some. Decline the match (don't select an existing party) to preserve
|
|
231
|
+
the test case's actual "create a new vendor" semantics — accepting it would silently turn a
|
|
232
|
+
create-scenario into an update against someone else's record — and continue normally. This
|
|
233
|
+
isn't a finding to flag or a fixture to "fix" on your own initiative; a suite that wanted
|
|
234
|
+
unique names every run would already bind `Org_Name` as `autogenerated` the same way it does
|
|
235
|
+
for the account number.
|
|
236
|
+
- A `SysOperationSandboxForm` with guidance to poll means an async job (commonly triggered by
|
|
237
|
+
posting actions) has started. Repeatedly call `form_click_control` on
|
|
238
|
+
`__TimerForAsyncTaskPolling` until the form changes away from
|
|
239
|
+
`SysOperationSandboxForm` — that response carries the final `Messages`, which is where a
|
|
240
|
+
genuine business failure (e.g. insufficient inventory) will show up. Confirmed on a general
|
|
241
|
+
journal post: the same pattern fires for **both** "Simulate posting" and the actual "Post"
|
|
242
|
+
click — a recording that captures a simulate step means the orchestrator polls twice, not
|
|
243
|
+
once. A clean post's `Messages` includes an explicit count, e.g. `"Number of vouchers posted
|
|
244
|
+
to the journal: 1"` — a good example of the kind of runtime-only success signal this skill's
|
|
245
|
+
"inspect the tool response" step above is about.
|
|
246
|
+
- **A tool call that times out client-side may still have succeeded server-side, silently
|
|
247
|
+
invalidating the whole session.** Confirmed live: a `form_click_control` call that opened a
|
|
248
|
+
vendor invoice edit dialog timed out; the next call failed with `"No form is available"`
|
|
249
|
+
(not just a stale-form error), and re-navigating from a menu item returned a brand-new
|
|
250
|
+
`SessionId`. Recovery is to re-navigate from a menu item rather than retrying the same call.
|
|
251
|
+
Separately, that first click *had* actually gone through server-side and left a genuinely
|
|
252
|
+
orphaned in-progress record: the next attempt to invoice the same purchase order surfaced a
|
|
253
|
+
"One or more invoices are in use" dialog listing it. Recover it (the dialog's own recovery
|
|
254
|
+
button) rather than abandoning it and creating a second orphan.
|
|
255
|
+
- **A Part-embedded button (e.g. `NewAddress` on a Vendor's Addresses tab) can fail immediately
|
|
256
|
+
after a fresh `New` + `form_save_form` on the parent record, even though the parent record now
|
|
257
|
+
genuinely exists.** Confirmed live on `VendTable`: clicking `NewAddress` right after creating
|
|
258
|
+
and saving a new vendor failed twice in a row with `"Must be called with Vendors table buffer
|
|
259
|
+
passed."` — not a real data problem, a stale/uninitialized table-buffer reference left over
|
|
260
|
+
from the New+Save sequence. Recovery is the same shape as the session-recovery case above but
|
|
261
|
+
a different root cause: re-navigate to the record via the list page (open the menu item,
|
|
262
|
+
filter/select the row, click the record link) rather than continuing to work off the
|
|
263
|
+
just-created form's own context — the button works normally once you're on a freshly-opened
|
|
264
|
+
record.
|
|
265
|
+
|
|
266
|
+
## Reporting
|
|
267
|
+
|
|
268
|
+
Produce a per-step table: `sequence`, `description`, tool actually called (may differ from the
|
|
269
|
+
hint), params used, pass/fail, and any captured messages. Finish with a plain-language summary:
|
|
270
|
+
what broke, and whether it was a mapping/tool logic problem (addressed below) or a genuine business
|
|
271
|
+
process failure (report to the user as the actual test result — that's the point of this whole
|
|
272
|
+
pipeline). If a hint's `params` shape turns out to be wrong against the live tool schema, **and you
|
|
273
|
+
have access to the tool's own source repo** (`src/mapping.ts`, `test/normalize.test.ts` — a
|
|
274
|
+
separate clone, not part of what an installed package ships), fix it there and add a regression
|
|
275
|
+
test, the same way the `form_select_grid_row`/`form_filter_grid`/`form_set_control_values` param
|
|
276
|
+
shapes were corrected after the first live run. **If you're running from an installed package with
|
|
277
|
+
no source access**, you can't make that fix yourself — report the specific mapping gap (which
|
|
278
|
+
tool, what params shape was expected vs. actually needed) to the user as part of your summary
|
|
279
|
+
instead, so they can pass it upstream, rather than searching for a `src/` directory that isn't
|
|
280
|
+
there and concluding the tool itself is broken.
|
|
281
|
+
|
|
282
|
+
## Running as part of a Test Suite (tracked test cases, real data, sequencing)
|
|
283
|
+
|
|
284
|
+
Everything above covers replaying one Test Script ad hoc. When a Test Case (`testcases/*.json`),
|
|
285
|
+
Test Suite (`suites/*.json`), and Test Run exist, use this check-in loop instead — **and stay
|
|
286
|
+
silent in chat while it runs.** Don't narrate per-step or per-test-case progress ("now running
|
|
287
|
+
create-purchase-order…", "confirm-purchase-order passed…") — nobody is reading it mid-run, it burns
|
|
288
|
+
chat tokens for no benefit, and it duplicates what's already being captured properly. The Test Run
|
|
289
|
+
file (`outputs`, `assertionResults` with real `actual` evidence) is the durable, comprehensive
|
|
290
|
+
record; chat only needs the one final summary in step 5 below (or, for a whole plan, the
|
|
291
|
+
plan-level rollup). This holds through failures too — per "Handling a genuine failure mid-run"
|
|
292
|
+
below, a failed/blocked entry gets recorded and the loop continues without pausing to tell the
|
|
293
|
+
user about it; it isn't a reason to break silence either. It also holds for parallel-eligible
|
|
294
|
+
batches ("Running parallel-eligible branches concurrently" below) — don't narrate spawning
|
|
295
|
+
subagents or their individual progress either, just record each result as it comes back and
|
|
296
|
+
keep going.
|
|
297
|
+
|
|
298
|
+
1. `trxa resolve-next <suite.json> <testrun.json> --testcases-dir testcases --fixtures-dir testdata`
|
|
299
|
+
- `{"status":"blocked","blockedIds":[...]}` — a dependency failed, or (per the note below)
|
|
300
|
+
evaluated a downstream entry's `runIf` unreachable; `resolve-next` has already recorded each
|
|
301
|
+
of `blockedIds` as `"blocked"` itself (no `record-result` call needed from you) before
|
|
302
|
+
printing this. Just call `resolve-next` again — in a linear chain this surfaces one dependent
|
|
303
|
+
at a time, so repeat rather than stopping after the first one. Keep looping silently until
|
|
304
|
+
`resolve-next` itself reports `"complete"`, then move to the final summary (step 5).
|
|
305
|
+
- `{"status":"skipped","skippedIds":[...]}` — one or more pending, otherwise-runnable entries
|
|
306
|
+
declared a `runIf` (`suites/*.json`'s `TestSuiteEntry.runIf`) that evaluated false against an
|
|
307
|
+
upstream test case's captured output. Same handling as `blocked` above: `resolve-next` has
|
|
308
|
+
already recorded each id as `"skipped"` itself -- not a failure, just call `resolve-next`
|
|
309
|
+
again. A `dependsOn` on a skipped entry surfaces as `blocked` on the *next* call (a skipped
|
|
310
|
+
dependency can never satisfy "every dependency passed"), not as a further `skipped`.
|
|
311
|
+
- `{"status":"waiting"}` / `{"status":"complete"}` — nothing to do / suite finished. Skip to
|
|
312
|
+
step 5 for a final summary once complete.
|
|
313
|
+
- `{"status":"runnable","runnable":[{"testCaseId","testScriptPath","legalEntity","resolvedInputs","autogeneratedParams","outputs","expectations"}, ...]}`
|
|
314
|
+
— `runnable` is every entry that's currently pending, has every dependency passed, and (if it
|
|
315
|
+
has one) passes its `runIf`, not just one — a suite whose DAG has two or more entries that
|
|
316
|
+
share a satisfied dependency without depending on each other (e.g. two entries that both only
|
|
317
|
+
depend on a common upstream entry) surfaces all of them here at once. The command has already
|
|
318
|
+
marked every entry in `runnable` `"running"` before returning, atomically as a batch — either
|
|
319
|
+
all of them resolved cleanly and got marked, or (if resolving any one of them failed, e.g. a
|
|
320
|
+
missing `inputBindings` entry) none of them did, so a failed call is always safely retryable
|
|
321
|
+
without checking what partially landed.
|
|
322
|
+
- **`runnable.length === 1`** (by far the common case): proceed with steps 2-4 below directly,
|
|
323
|
+
exactly as if this were the only thing resolve-next could ever return.
|
|
324
|
+
- **`runnable.length > 1`**: see "Running parallel-eligible branches concurrently" below before
|
|
325
|
+
continuing — don't just work through the array one entry at a time with steps 2-4 in a
|
|
326
|
+
simple loop, that leaves real concurrency (and thus wall-clock time) on the table for exactly
|
|
327
|
+
the case this array shape exists to expose.
|
|
328
|
+
Each entry's `resolvedInputs` are already resolved concrete values (from a fixture or an
|
|
329
|
+
upstream test case's output) — **use them as-is, do not substitute your own guessed business
|
|
330
|
+
data.**
|
|
331
|
+
- `autogeneratedParams` (an array of `{name, description, example}`) is the one deliberate
|
|
332
|
+
exception to that rule: these are params whose suite binding is `{"kind": "autogenerated"}`
|
|
333
|
+
— no fixture or upstream output to pull from, by design. **Before replaying, invent a value
|
|
334
|
+
for each one yourself** using its `description`/`example` as guidance (this kind exists
|
|
335
|
+
specifically for values that just need to be unique per run, e.g. append a short
|
|
336
|
+
timestamp/random suffix to a recorded-example-shaped base value), then treat it exactly as if
|
|
337
|
+
it were part of `resolvedInputs` for every remaining step below — form input substitution
|
|
338
|
+
(step 2) and `{{paramName}}` expectation substitution (step 3) both need it, and the CLI has
|
|
339
|
+
no way to know what you generated, so you must carry it yourself.
|
|
340
|
+
- **A text field can silently truncate an over-length autogenerated value, with no error.**
|
|
341
|
+
Confirmed live on `PurchParmTable_Num` ("Product receipt"): a 22-char generated value was
|
|
342
|
+
silently cut to 21 chars on save, no `Messages` entry raised. Don't assume the value you
|
|
343
|
+
set is the value stored, especially for a generated value whose length you don't fully
|
|
344
|
+
control — after setting an autogenerated value, re-read the field back off `FormState`
|
|
345
|
+
(the same "inspect the tool response" discipline from the per-step procedure above) and
|
|
346
|
+
carry the *stored* value forward for any later step/expectation that references it, not
|
|
347
|
+
the one you originally generated.
|
|
348
|
+
- `legalEntity`, when present, is the company this entry's suite author intended the test case
|
|
349
|
+
to run in — pass it as `companyId` on the *first* navigation call of the replay in step 2
|
|
350
|
+
below (the `form_open_menu_item`/`form_find_menu_item`/`api_invoke_action` that opens the
|
|
351
|
+
test case's starting form) and omit it from every call after that. When `legalEntity` is
|
|
352
|
+
absent, don't pass `companyId` at all — the test case runs in whatever company the current
|
|
353
|
+
session/form is already in. This is a single fixed starting company, not a substitute for the
|
|
354
|
+
"Executing each step" section's `FormState.Company`-tracking rule above — a multi-company
|
|
355
|
+
round trip within one test case (e.g. an intercompany order pair) is still just one atomic
|
|
356
|
+
pass/fail unit, and any company change mid-flow is picked up live from `FormState.Company`
|
|
357
|
+
after the click that causes it, never declared up front.
|
|
358
|
+
2. Replay `testScriptPath` using the existing per-step procedure above, but plug each step whose
|
|
359
|
+
`bindTo.controlName` matches a key in `resolvedInputs` (plus whatever you invented for
|
|
360
|
+
`autogeneratedParams`) with that resolved value instead of the recorded one. For any
|
|
361
|
+
`bindTo.kind: "grid-row-selector"` input, that resolved value **is** the business key to
|
|
362
|
+
filter/search the grid by (this is exactly the ambiguity flagged earlier in this skill — a
|
|
363
|
+
suite's `resolvedInputs` is what resolves it, so you should no longer need to guess from context
|
|
364
|
+
when a test case supplies one).
|
|
365
|
+
3. After the step(s) each `expectations[]` entry references (`checkAfterStepSequence`), evaluate
|
|
366
|
+
it:
|
|
367
|
+
- `kind: "odata"` (preferred) — `filter`/`expectedValue` may contain `{{paramName}}`
|
|
368
|
+
placeholders referencing `resolvedInputs` from step 1 (or a value you invented for an
|
|
369
|
+
`autogeneratedParams` entry — same substitution, just from your own memory instead of the
|
|
370
|
+
CLI's JSON) (e.g. `SalesOrder eq '{{salesOrderId}}'`); substitute those first, then call
|
|
371
|
+
`data_find_entities` on
|
|
372
|
+
`entitySet` with the resolved `filter`, read `expectedField`, compare to `expectedValue`
|
|
373
|
+
with `comparator`.
|
|
374
|
+
- `kind: "uiMessage"` (fallback) — check `FormState.Messages` for an entry of `severity` whose
|
|
375
|
+
text contains `pattern` as a **plain substring, not a regex** (empty `pattern` means "any
|
|
376
|
+
message of this severity, regardless of text"). `shouldExist: true` means a matching message
|
|
377
|
+
must be present; `false` means it must be absent — e.g. `{severity: "Error", pattern: "",
|
|
378
|
+
shouldExist: false}` asserts no error occurred at all, `{severity: "Info", pattern: "created",
|
|
379
|
+
shouldExist: true}` asserts a creation-confirmation message appeared.
|
|
380
|
+
Also capture each `outputs[]` entry the same way:
|
|
381
|
+
- `kind: "odata"` — query as above.
|
|
382
|
+
- `kind: "uiField"` with `controlName` set — read that exact control's value off `FormState` at
|
|
383
|
+
`afterStepSequence`. No extra tool call needed: the value is already in the response of a
|
|
384
|
+
step you're replaying anyway, so this is free and exactly reproducible.
|
|
385
|
+
- `kind: "uiField"` with only `hint` set (no `controlName`) — the control name wasn't known at
|
|
386
|
+
authoring time. This happens for values Task Recorder never captured because nobody clicked
|
|
387
|
+
them (a commonly-seen example: an auto-assigned ID that only appears on screen after Save).
|
|
388
|
+
Read the full `FormState` at `afterStepSequence` and use the hint (plain English, e.g. "the
|
|
389
|
+
newly assigned purchase order number") to identify the right field yourself. **After a
|
|
390
|
+
successful resolution, edit the test case file to add the resolved `controlName`** (keep
|
|
391
|
+
`hint` too) so future replays read it directly instead of re-interpreting `FormState` every
|
|
392
|
+
time — confirmed live on a purchase order creation: `PurchTable_PurchId` appeared cleanly in
|
|
393
|
+
the "Create purchase order" dialog's own `FormState` right after clicking New, well before
|
|
394
|
+
Save, with a clean scalar value (`"00000178"`) distinct from the header's later combined
|
|
395
|
+
`HeaderTitle` string (`"00000178 : 1001 - Acme Office Supplies"`) — pick the cleanest matching
|
|
396
|
+
field, not just the first one that looks plausible.
|
|
397
|
+
These become available to *later* test cases in the suite via `resolvedInputs`.
|
|
398
|
+
4. Record the outcome:
|
|
399
|
+
`trxa record-result <testrun.json> <testCaseId> --status passed|failed|blocked --outputs '{"name":"value"}' --assertions '[{"description":"...","passed":true,"actual":"..."}]'`
|
|
400
|
+
`sequence`/`startedAt`/`durationMs` are computed automatically by this command — nothing to
|
|
401
|
+
pass. If this entry was replayed by a subagent (see "Running parallel-eligible branches
|
|
402
|
+
concurrently" above), **you** — the parent — are the one who runs this command, using the
|
|
403
|
+
subagent's reported result as the source for every flag below; the subagent never calls
|
|
404
|
+
`record-result` itself, and multiple entries from the same chunk are still recorded one at a
|
|
405
|
+
time, never concurrently.
|
|
406
|
+
- **On Windows PowerShell, prefer the `--outputs-file`/`--assertions-file`/`--inputs-file`/
|
|
407
|
+
`--messages-file` flags over the inline `'{...}'` form.** PowerShell 5.1's argument-passing
|
|
408
|
+
layer to a native executable (`node.exe` isn't a cmdlet) can silently corrupt inline JSON
|
|
409
|
+
before Node ever sees it — confirmed live in two distinct, reproducible ways: quote-less-of-
|
|
410
|
+
spaces JSON gets its `"` characters stripped outright, and JSON containing a space (e.g. a
|
|
411
|
+
`description`/`actual` string) gets split across multiple `argv` entries mid-string, so
|
|
412
|
+
`flagValue` only reads the truncated first piece. Neither is a bug in the tool (see `MANUAL.md`'s "Windows
|
|
413
|
+
PowerShell" section). The `-file` flags exist specifically to avoid this: write the JSON to a
|
|
414
|
+
scratch file, pass its path, done — this works identically in every shell. Write the file
|
|
415
|
+
**without a byte-order mark** (your own file-writing tool, or `[IO.File]::WriteAllText`, not
|
|
416
|
+
PowerShell 5.1's `Set-Content -Encoding utf8`/`Out-File`, which add one and make the JSON fail
|
|
417
|
+
to parse). Two more flags:
|
|
418
|
+
- `--messages '["...", "..."]'` — **always pass this, pass or fail, whenever any step you
|
|
419
|
+
replayed for this test case had a non-empty `FormState.Messages`.** Collect one entry per
|
|
420
|
+
message across *every* step of this test case, not just the last one (e.g. an early "Info:
|
|
421
|
+
Purchase order 00000226 has been created." plus a later validation message both belong here) —
|
|
422
|
+
format each as `"<Severity>: <text>"`. This is the Studio Test Runs page's "Infolog" column;
|
|
423
|
+
it's only as complete as what you actually pass here, since the CLI has no independent way to
|
|
424
|
+
observe it. Only omit `--messages` entirely if every step's `FormState.Messages` was genuinely
|
|
425
|
+
empty for this test case.
|
|
426
|
+
- **A message's `Text` can itself contain tab characters (`\t`) encoding a hierarchy** —
|
|
427
|
+
confirmed live: posting a sales order blocked by credit management returned
|
|
428
|
+
`"Posting\tPosting\tSales order: 000963\tOrder is in credit management."` as one `Warning`
|
|
429
|
+
entry. This is the exact same content the D365 web client's "Message details" flyout renders
|
|
430
|
+
as an indented tree (`Posting` → `Posting` → `Sales order: 000963` → the actual detail) —
|
|
431
|
+
there's no separate tool call or hidden data source behind that flyout, it's this string,
|
|
432
|
+
client-side-indented. **Don't strip or collapse the tabs** — replace them with `" > "` when
|
|
433
|
+
building the `--messages` entry (e.g. `"Warning: Posting > Posting > Sales order: 000963 >
|
|
434
|
+
Order is in credit management."`) so the hierarchy survives into the Infolog column instead
|
|
435
|
+
of getting silently flattened into a run-on sentence or truncated at the first tab.
|
|
436
|
+
- `--tokens`/`--cost` — nothing to do here. `record-result` auto-measures `tokensUsed` itself
|
|
437
|
+
(reading Claude Code's own session transcript for the window between this test case's
|
|
438
|
+
`"running"` transition and now) whenever you don't pass `--tokens` explicitly, so just leave
|
|
439
|
+
both flags out. Be aware the auto-measured number is only a rough signal, not a trustworthy
|
|
440
|
+
one, whenever another Claude Code session might be active against this project at the same
|
|
441
|
+
time — live-tested inflation of 24%-98% from cross-session misattribution (more detail in
|
|
442
|
+
`src/report-schema.ts`'s `tokensUsed` comment, if you have a source checkout to read it from)
|
|
443
|
+
— but there's nothing different to do about it here; it's still the best available default.
|
|
444
|
+
Only pass `--tokens` yourself if you have a better
|
|
445
|
+
number from somewhere else; leave `--cost` unset entirely — there's no reliable per-model,
|
|
446
|
+
cache-aware pricing table this
|
|
447
|
+
skill should be maintaining, so don't compute one from `tokensUsed` with a guessed price.
|
|
448
|
+
- `--inputs '{"name":"value"}'` — only needed if you invented values for `autogeneratedParams`
|
|
449
|
+
(step 1) and want the full input set on record, since `resolve-next`'s own automatic capture
|
|
450
|
+
only knows the deterministic bindings.
|
|
451
|
+
Then go back to step 1 for the next test case.
|
|
452
|
+
5. Once `resolve-next` reports `"complete"`, read the final Test Run file and summarize: per
|
|
453
|
+
test case pass/fail, which assertions failed and why (the captured `actual` value is the
|
|
454
|
+
evidence), and which — if any — got `"blocked"` by an upstream failure.
|
|
455
|
+
|
|
456
|
+
### Running parallel-eligible branches concurrently
|
|
457
|
+
|
|
458
|
+
When step 1's `resolve-next` returns `runnable.length > 1`, the suite's author has deliberately
|
|
459
|
+
shaped the DAG so these entries share a satisfied dependency without depending on each other (e.g.
|
|
460
|
+
two entries that both only depend on one common upstream entry, like `purchase-order-lifecycle.json`'s
|
|
461
|
+
receive/confirm pair) — that's an explicit signal to run them at the same time, not an incidental
|
|
462
|
+
list to work through sequentially.
|
|
463
|
+
|
|
464
|
+
1. **Determine the concurrency cap.** Each subagent needs its own live D365FO session/connection —
|
|
465
|
+
two agents sharing one `d365-erp-mcp` connection will corrupt each other's `FormState` mid-flight,
|
|
466
|
+
since navigation and form state are per-connection. Count the distinct `d365-erp-mcp`-style MCP
|
|
467
|
+
servers actually available this session (check the tool list; this repo typically exposes two:
|
|
468
|
+
`d365-erp-mcp` and `d365-erp-mcp-2`) and treat that count as the hard cap on simultaneous
|
|
469
|
+
subagents — never spawn more than you have connections for, even if `runnable` has more entries
|
|
470
|
+
than that.
|
|
471
|
+
2. **Chunk `runnable` into groups of that size.** Process one chunk fully (spawn, wait for all,
|
|
472
|
+
record all) before starting the next chunk — don't pipeline across chunks.
|
|
473
|
+
3. **Spawn one subagent per entry in the chunk, all in a single message** containing multiple `Agent`
|
|
474
|
+
tool_use blocks (this is what actually parallelizes them — spawning one at a time in separate
|
|
475
|
+
messages serializes them instead). Assign each subagent a distinct MCP connection explicitly (e.g.
|
|
476
|
+
"use only `d365-erp-mcp-2` tools for every form/data/API call, never `d365-erp-mcp`") so the two
|
|
477
|
+
replays can't collide. Each subagent's prompt must be fully self-contained — it has no memory of
|
|
478
|
+
this conversation — and should include:
|
|
479
|
+
- The `replay-trxa-testplan` skill's per-step replay procedure (steps 2-3 above: substitute
|
|
480
|
+
`resolvedInputs`/`autogeneratedParams` into the script, evaluate `expectations[]`, capture
|
|
481
|
+
`outputs[]`), for exactly this one entry's `testScriptPath`/`legalEntity`/`resolvedInputs`/
|
|
482
|
+
`autogeneratedParams`/`outputs`/`expectations` bundle — paste the actual values, don't make the
|
|
483
|
+
subagent re-derive them.
|
|
484
|
+
- The two numeric hard caps restated directly, not just referenced — a subagent that only reads
|
|
485
|
+
"the skill has caps" tends to loop past them: **cap async-operation polling at 20 polls** (per
|
|
486
|
+
the `SysOperationSandboxForm` guidance above) and **cap alternate-path attempts on a rejected
|
|
487
|
+
action at 3** (per "Handling a genuine failure mid-run" below).
|
|
488
|
+
- An explicit instruction **not** to call `resolve-next` or `record-result` itself — those mutate
|
|
489
|
+
the shared Test Run file, and concurrent read-modify-write from multiple subagents will clobber
|
|
490
|
+
each other's writes. The subagent's job ends at reporting back.
|
|
491
|
+
- The exact structured result to report back: `status` (`passed`/`failed`/`blocked`), `outputs`
|
|
492
|
+
(`{name: value}`), `assertionResults` (`[{description, passed, actual}]`), `messages` (the full
|
|
493
|
+
`FormState.Messages` stack, formatted per step 4's rules below, including the tab-hierarchy
|
|
494
|
+
`" > "` substitution), and whatever values it invented for `autogeneratedParams` (so the parent
|
|
495
|
+
can pass `--inputs` if needed).
|
|
496
|
+
4. **Wait for every subagent in the chunk to finish**, then call `record-result` yourself, once per
|
|
497
|
+
entry, **sequentially — never concurrently** — using each subagent's reported result exactly as
|
|
498
|
+
step 4 above describes. You (the parent orchestrating session) are the only writer to the Test Run
|
|
499
|
+
file in this whole flow; the subagents only replay and report.
|
|
500
|
+
5. Go back to step 1 (`resolve-next`) for the next batch — no need to call it once per entry you just
|
|
501
|
+
recorded, one call after the whole chunk is committed is enough, and it'll either surface the next
|
|
502
|
+
chunk or report `blocked`/`skipped`/`complete` as usual.
|
|
503
|
+
|
|
504
|
+
This procedure is a direct extension of steps 1-4 above, not a separate system: a subagent replaying
|
|
505
|
+
its one entry follows exactly the same "Executing each step"/step 2/step 3 guidance as a
|
|
506
|
+
sequential solo replay would, it just does so in an isolated subagent with a pinned connection so
|
|
507
|
+
several branches can run at once. It has not yet been exercised end-to-end against a live
|
|
508
|
+
environment as of this writing — treat the mechanics above as the documented design, and note here
|
|
509
|
+
if a live run surfaces something the steps above don't cover (a new failure mode, a caps value that
|
|
510
|
+
turns out wrong, etc.), the same way the rest of this skill accumulates confirmed-live notes.
|
|
511
|
+
|
|
512
|
+
**Handling a genuine failure mid-run — do not get creative, and do not ask.** A resolved input can
|
|
513
|
+
itself turn out to be invalid (a fixture's vendor/item/account rejected by D365FO), or a business
|
|
514
|
+
rule can block the action outright (a stopped vendor, a vendor blocked for invoicing, etc.). This
|
|
515
|
+
is a real test result, not a reason to pause: an automated suite run has no one to ask mid-flight,
|
|
516
|
+
so never stop the check-in loop to request a replacement value or confirmation from the user.
|
|
517
|
+
- Try at most **3** alternate paths the recording or the current `FormState` itself suggests (e.g.
|
|
518
|
+
a ribbon tab click before the button it reveals, or a list-level equivalent of a per-record
|
|
519
|
+
action) — a hard cap, not a target. If those are all consistently rejected or disabled, that's
|
|
520
|
+
confirmation enough — don't keep searching for workarounds. Once the cause is a business rule
|
|
521
|
+
rather than a mapping gap, more attempts won't change the outcome; they only burn tool calls.
|
|
522
|
+
- Record the failure with `record-result ... --status failed`, using the real error/message text
|
|
523
|
+
as the `actual` evidence — never invent a plausible-sounding one, and never substitute a
|
|
524
|
+
different value than what `resolvedInputs` gave you just to make the step succeed. This is
|
|
525
|
+
exactly the case step 4's `--messages` flag matters most for — pass the full `FormState.Messages`
|
|
526
|
+
stack you observed, not just the one line you happened to quote in `actual`.
|
|
527
|
+
- Go back to step 1. `resolve-next` reports the directly-dependent entry as `blocked` and has
|
|
528
|
+
already recorded it as such itself (no `record-result` call needed) — just call `resolve-next`
|
|
529
|
+
again; in a linear chain this surfaces one dependent at a time, so repeat until it reports
|
|
530
|
+
`"complete"`.
|
|
531
|
+
- Confirmed live: an invalid fixture item number, a stopped vendor, and a vendor deliberately
|
|
532
|
+
blocked for invoicing all surfaced this way in the same session — each correctly recorded as
|
|
533
|
+
`failed`/`blocked` with no manual intervention, and the run still completed as a durable,
|
|
534
|
+
accurate record of what actually happened.
|
|
535
|
+
|
|
536
|
+
**If a `d365-erp-mcp` connection appears unresponsive, don't keep retrying the same one
|
|
537
|
+
indefinitely — and don't try to "restart" it, that's not something you can actually do.** MCP
|
|
538
|
+
servers are processes the host application launches and manages; you have no tool for killing or
|
|
539
|
+
relaunching one mid-session, and a tool call is synchronous from your side — you can't unilaterally
|
|
540
|
+
abandon a call that hasn't returned yet, only decide what to do once one comes back as an error (or
|
|
541
|
+
the user interrupts). Confirmed live: `d365-erp-mcp` node processes hung entirely partway through a
|
|
542
|
+
run and had to be killed/restarted by the user, outside the replay session itself.
|
|
543
|
+
- If a call to one connection errors out in a way that looks connection-level (not a business-logic
|
|
544
|
+
rejection — a timeout, "no response," or similar, distinct from the "times out client-side but
|
|
545
|
+
succeeded server-side" pattern above, which is about one slow call, not a dead connection) more
|
|
546
|
+
than **2** times in a row, stop retrying that same connection.
|
|
547
|
+
- If another `d365-erp-mcp*` connection is available and healthy (this project commonly has more
|
|
548
|
+
than one configured — see "Running parallel-eligible branches concurrently" below for how suites
|
|
549
|
+
already use multiple connections), switch the remaining work to it rather than continuing to
|
|
550
|
+
target the dead one. This is genuinely something you can do, unlike restarting a server.
|
|
551
|
+
- If no healthy connection is available, stop the replay loop entirely rather than hang
|
|
552
|
+
indefinitely — go to "Stopping before a test case reaches a real outcome" immediately below, and
|
|
553
|
+
tell the user directly (breaking silence is warranted here) that the `d365-erp-mcp` connection
|
|
554
|
+
appears unresponsive and needs to be restarted on their end; that part genuinely isn't something
|
|
555
|
+
you can fix from inside the conversation.
|
|
556
|
+
|
|
557
|
+
**Stopping before a test case reaches a real outcome — never leave an entry stuck at `"running"`.**
|
|
558
|
+
If you need to end the replay loop before a currently in-flight test case has reached a definitive
|
|
559
|
+
`passed`/`failed`/`blocked` outcome — the user asks you to stop, you're running low on
|
|
560
|
+
context/budget, or anything else cuts the session short while you're still able to make one more
|
|
561
|
+
tool call — record its outcome as `failed` before you stop. Don't just end the conversation with it
|
|
562
|
+
left at `"running"`.
|
|
563
|
+
|
|
564
|
+
This isn't just tidiness. `resolve-next`'s own logic (`nextStep` in `suite-runner.ts`) treats
|
|
565
|
+
`"running"` as a normal mid-flight state, not an error, and reports `"waiting"` for as long as
|
|
566
|
+
anything is stuck there — a suite with an abandoned `"running"` entry can never reach `"complete"`
|
|
567
|
+
again, and a future session calling `resolve-next` against the same Test Run gets stuck reporting
|
|
568
|
+
`"waiting"` forever instead of surfacing the next real step (confirmed by reading `nextStep`'s
|
|
569
|
+
actual return logic, not assumed). There's no separate "stopped"/"cancelled" status to reach for
|
|
570
|
+
either — the schema has `pending`/`running`/`passed`/`failed`/`blocked`/`skipped`, nothing else --
|
|
571
|
+
`failed` is the honest, conservative choice when you don't actually know the outcome: better to
|
|
572
|
+
under-claim success than leave a false still-in-progress marker behind.
|
|
573
|
+
|
|
574
|
+
- Call `record-result ... --status failed`, with an assertion or `--messages` entry whose `actual`
|
|
575
|
+
clearly states this was an interruption, not a genuine business-process failure — e.g.
|
|
576
|
+
`"actual": "Replay stopped by user request before this step's outcome could be determined"` — so
|
|
577
|
+
a human reading the Test Run later can tell the difference from a real D365FO rejection.
|
|
578
|
+
- If you're mid-batch in the parallel-branch procedure above and the *orchestrating* session itself
|
|
579
|
+
is what's stopping (not one individual subagent finishing normally), every entry `resolve-next`
|
|
580
|
+
marked `"running"` for that batch needs this treatment, not just whichever one you happened to be
|
|
581
|
+
looking at — check the Test Run file for every `"running"` entry before ending, not only the most
|
|
582
|
+
recent one.
|
|
583
|
+
- This only applies when you're still active enough to make one more tool call before stopping — a
|
|
584
|
+
genuine hard crash with no chance to run anything is outside what any skill instruction can fix.
|
|
585
|
+
For that case: if a *future* replay session opens a Test Run and finds an entry already marked
|
|
586
|
+
`"running"` that clearly isn't actually in progress (no other session is active against it),
|
|
587
|
+
recover it the same way — record it `failed` with a message explaining it was found abandoned —
|
|
588
|
+
before proceeding, rather than treating a stale `"running"` marker as a currently-active run.
|
|
589
|
+
|
|
590
|
+
If you don't yet have a Test Run file for a suite, create one first:
|
|
591
|
+
`trxa init-run <suite.json> -o testruns/<suiteId>-<runId>.json`.
|
|
592
|
+
|
|
593
|
+
## Running a whole Test Plan (multiple related suites)
|
|
594
|
+
|
|
595
|
+
A Test Plan (`plans/*.json`) groups several independent Test Suites for combined reporting — no
|
|
596
|
+
dependency graph between suites, unlike within one suite. To run one:
|
|
597
|
+
|
|
598
|
+
1. Call `trxa init-plan-run <plan.json> -o runs/` once, before running any suite.
|
|
599
|
+
This creates a PlanRun record and prints its generated `id` — use that exact id as the
|
|
600
|
+
`<runId>` for every suite below, instead of inventing one yourself; it's what lets
|
|
601
|
+
`plan-status` correlate them afterward, and the file it's written to (`runs/<id>.json`) is
|
|
602
|
+
what you pass to `plan-status` in step 3.
|
|
603
|
+
2. For each suite listed in the plan (`suites[].suitePath`, resolved relative to the plan file),
|
|
604
|
+
repeat the full per-suite procedure from "Running as part of a Test Suite" above unchanged,
|
|
605
|
+
using `testruns/<suiteId>-<runId>.json` as that suite's Test Run path.
|
|
606
|
+
3. Once every suite in the plan has reached `"complete"` (or you want an in-progress snapshot),
|
|
607
|
+
call `trxa plan-status <plan.json> runs/<runId>.json --testruns-dir testruns`
|
|
608
|
+
and report its rollup (`all-passed` / `has-failures` / `incomplete` / `missing-runs`, plus the
|
|
609
|
+
per-suite breakdown) to the user — this is the plan-level summary, on top of each suite's own
|
|
610
|
+
per-test-case report from step 5 above.
|
|
611
|
+
|
|
612
|
+
This does not touch the per-scenario replay guidance above. `mapping.ts`'s coverage has since been
|
|
613
|
+
broadened and live-validated against several of the ~100 real business scenarios in `recordings/`
|
|
614
|
+
(legal entity addresses, free text invoices, purchase order lines, product categories), which is why this skill now
|
|
615
|
+
documents `skip`, tree navigation, and the filter/sort family above instead of only the one
|
|
616
|
+
packing-slip scenario. Four more scenarios — a general journal entry, a fixed asset acquisition
|
|
617
|
+
journal, a purchase order confirmation, and a vendor invoice matched/posted against a purchase
|
|
618
|
+
order, all run end-to-end (see the grid-selection, lookup-defaults, `SegmentedEntry`,
|
|
619
|
+
double-async-poll, skip-redundant-lookup, system-inserted-lines, skippable-tab-click,
|
|
620
|
+
variable-async-duration, missing-workflow-button, match-status, and session-recovery notes above,
|
|
621
|
+
all new findings these runs surfaced) — have since been run start-to-finish through live sessions
|
|
622
|
+
too. Three of the four needed no `mapping.ts` changes; the fourth (invoice matching) surfaced a
|
|
623
|
+
real correctness gap now fixed in `mapping.ts` itself: Date-typed values are reformatted from the
|
|
624
|
+
recording's ISO to `M/d/yyyy` before being set, since some Date controls reject ISO outright. This
|
|
625
|
+
covers the four starting categories originally proposed for this pass (general journal posting,
|
|
626
|
+
fixed asset acquisition, PO confirmation, invoice matching); broadening further across the rest of
|
|
627
|
+
`recordings/` remains open but is no longer scoped to a specific starting set.
|