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.
Files changed (155) hide show
  1. package/.claude/skills/replay-trxa-testplan/SKILL.md +627 -0
  2. package/LICENSE +120 -0
  3. package/MANUAL.md +556 -0
  4. package/README.md +91 -3
  5. package/SETUP.md +401 -0
  6. package/STUDIO.md +427 -0
  7. package/dist/analyze.d.ts +58 -0
  8. package/dist/analyze.js +122 -0
  9. package/dist/bpm.d.ts +40 -0
  10. package/dist/bpm.js +60 -0
  11. package/dist/build-test-script.d.ts +24 -0
  12. package/dist/build-test-script.js +40 -0
  13. package/dist/csv.d.ts +1 -0
  14. package/dist/csv.js +11 -0
  15. package/dist/extract.d.ts +14 -0
  16. package/dist/extract.js +34 -0
  17. package/dist/fixture-schema.d.ts +52 -0
  18. package/dist/fixture-schema.js +112 -0
  19. package/dist/fixture-store.d.ts +37 -0
  20. package/dist/fixture-store.js +75 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.js +850 -0
  23. package/dist/library-archive.d.ts +37 -0
  24. package/dist/library-archive.js +106 -0
  25. package/dist/mapping.d.ts +25 -0
  26. package/dist/mapping.js +393 -0
  27. package/dist/mcp-bridge.d.ts +52 -0
  28. package/dist/mcp-bridge.js +290 -0
  29. package/dist/normalize.d.ts +21 -0
  30. package/dist/normalize.js +43 -0
  31. package/dist/parse.d.ts +3 -0
  32. package/dist/parse.js +233 -0
  33. package/dist/plan-runner.d.ts +27 -0
  34. package/dist/plan-runner.js +36 -0
  35. package/dist/plan-schema.d.ts +27 -0
  36. package/dist/plan-schema.js +45 -0
  37. package/dist/planrun-schema.d.ts +53 -0
  38. package/dist/planrun-schema.js +111 -0
  39. package/dist/planrun.d.ts +20 -0
  40. package/dist/planrun.js +46 -0
  41. package/dist/report-schema.d.ts +92 -0
  42. package/dist/report-schema.js +115 -0
  43. package/dist/report.d.ts +45 -0
  44. package/dist/report.js +77 -0
  45. package/dist/schema.d.ts +96 -0
  46. package/dist/schema.js +10 -0
  47. package/dist/scrub.d.ts +44 -0
  48. package/dist/scrub.js +166 -0
  49. package/dist/suggest-inputs.d.ts +19 -0
  50. package/dist/suggest-inputs.js +92 -0
  51. package/dist/suite-runner.d.ts +59 -0
  52. package/dist/suite-runner.js +204 -0
  53. package/dist/suite-schema.d.ts +59 -0
  54. package/dist/suite-schema.js +95 -0
  55. package/dist/testcase-schema.d.ts +102 -0
  56. package/dist/testcase-schema.js +132 -0
  57. package/dist/token-usage.d.ts +38 -0
  58. package/dist/token-usage.js +95 -0
  59. package/media/studio/fixtures-test-cases.png +0 -0
  60. package/media/studio/plans-editor.png +0 -0
  61. package/media/studio/recordings-create-test-case.png +0 -0
  62. package/media/studio/run-dialog.png +0 -0
  63. package/media/studio/settings-agent-runs.png +0 -0
  64. package/media/studio/settings-working-folder.png +0 -0
  65. package/media/studio/studio-recordings.png +0 -0
  66. package/media/studio/suites-canvas.png +0 -0
  67. package/media/studio/suites-inspector.png +0 -0
  68. package/media/studio/testcases-expectations.png +0 -0
  69. package/media/studio/testcases-outputs.png +0 -0
  70. package/media/studio/testrun-details.png +0 -0
  71. package/media/studio/testruns-list.png +0 -0
  72. package/package.json +62 -6
  73. package/studio/client-dist/assets/CreatePanel-C5bb5OIs.js +6 -0
  74. package/studio/client-dist/assets/FactTile-G1pURLys.js +1 -0
  75. package/studio/client-dist/assets/FieldLabel-DPmIEQ08.js +1 -0
  76. package/studio/client-dist/assets/FixtureNamePicker-137AAgwZ.js +1 -0
  77. package/studio/client-dist/assets/Fixtures-D0t-soph.js +1 -0
  78. package/studio/client-dist/assets/GeneralSection-CWkTeSX2.js +1 -0
  79. package/studio/client-dist/assets/ListPaneHeader-DXKqX36N.js +1 -0
  80. package/studio/client-dist/assets/Plans-C70evkGc.js +1 -0
  81. package/studio/client-dist/assets/Recordings-BT0dyVLs.js +1 -0
  82. package/studio/client-dist/assets/Settings-k-J3u5lH.js +1 -0
  83. package/studio/client-dist/assets/Suites-DLioOiRN.css +1 -0
  84. package/studio/client-dist/assets/Suites-DQhhMxjT.js +7 -0
  85. package/studio/client-dist/assets/TestCases-BdiopVGI.js +1 -0
  86. package/studio/client-dist/assets/TestRuns-CylAe-3t.js +9 -0
  87. package/studio/client-dist/assets/api-DJZ703_F.js +24 -0
  88. package/studio/client-dist/assets/bucket-18-viuwGcaj.js +1 -0
  89. package/studio/client-dist/assets/fixtureName-CTZT_jKu.js +1 -0
  90. package/studio/client-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  91. package/studio/client-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  92. package/studio/client-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  93. package/studio/client-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  94. package/studio/client-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  95. package/studio/client-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  96. package/studio/client-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  97. package/studio/client-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  98. package/studio/client-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  99. package/studio/client-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  100. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-300-normal-LoaTnByX.woff +0 -0
  101. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-300-normal-RhT0Ayi3.woff2 +0 -0
  102. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-400-italic-Bf1n82Tb.woff +0 -0
  103. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-400-italic-D9J6q4_O.woff2 +0 -0
  104. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
  105. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
  106. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
  107. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
  108. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-300-normal-Bbd5v1xK.woff +0 -0
  109. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-300-normal-DqmgDkIN.woff2 +0 -0
  110. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-400-italic-BELOEI8R.woff2 +0 -0
  111. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-400-italic-CfSXsIbO.woff +0 -0
  112. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
  113. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
  114. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
  115. package/studio/client-dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
  116. package/studio/client-dist/assets/ibm-plex-sans-greek-300-normal-J-CBqZoJ.woff2 +0 -0
  117. package/studio/client-dist/assets/ibm-plex-sans-greek-300-normal-STc8XiDr.woff +0 -0
  118. package/studio/client-dist/assets/ibm-plex-sans-greek-400-italic-BEZwmy7D.woff2 +0 -0
  119. package/studio/client-dist/assets/ibm-plex-sans-greek-400-italic-CTYLy0lX.woff +0 -0
  120. package/studio/client-dist/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
  121. package/studio/client-dist/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
  122. package/studio/client-dist/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
  123. package/studio/client-dist/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
  124. package/studio/client-dist/assets/ibm-plex-sans-latin-300-normal-C9Of02-5.woff +0 -0
  125. package/studio/client-dist/assets/ibm-plex-sans-latin-300-normal-b4i7YzLG.woff2 +0 -0
  126. package/studio/client-dist/assets/ibm-plex-sans-latin-400-italic-CZTNEAuW.woff2 +0 -0
  127. package/studio/client-dist/assets/ibm-plex-sans-latin-400-italic-CsGl1sm0.woff +0 -0
  128. package/studio/client-dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  129. package/studio/client-dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  130. package/studio/client-dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  131. package/studio/client-dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  132. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-300-normal-B8Zgwjh3.woff +0 -0
  133. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-300-normal-BLFuR0P1.woff2 +0 -0
  134. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-400-italic-Du9zzoL2.woff2 +0 -0
  135. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-400-italic-V_9i1P_m.woff +0 -0
  136. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
  137. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
  138. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
  139. package/studio/client-dist/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
  140. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-300-normal-CkYRZD2a.woff +0 -0
  141. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-300-normal-SDfX6ktj.woff2 +0 -0
  142. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-400-italic-78jqtfZH.woff +0 -0
  143. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-400-italic-BbRnwouv.woff2 +0 -0
  144. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
  145. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
  146. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
  147. package/studio/client-dist/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
  148. package/studio/client-dist/assets/index-cxBc2iRv.js +10 -0
  149. package/studio/client-dist/assets/index-wjWuVHXN.css +1 -0
  150. package/studio/client-dist/assets/status-DUGxC7G7.js +1 -0
  151. package/studio/client-dist/assets/testCaseTree-Cl1-bc5O.js +1 -0
  152. package/studio/client-dist/axtr-icon.svg +2 -0
  153. package/studio/client-dist/favicon.svg +2 -0
  154. package/studio/client-dist/index.html +15 -0
  155. 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.