@provartesting/provardx-cli 1.5.0-dev.1 → 1.5.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 (129) hide show
  1. package/README.md +163 -13
  2. package/bin/mcp-start.js +74 -0
  3. package/lib/commands/provar/auth/clear.d.ts +7 -0
  4. package/lib/commands/provar/auth/clear.js +36 -0
  5. package/lib/commands/provar/auth/clear.js.map +1 -0
  6. package/lib/commands/provar/auth/login.d.ts +10 -0
  7. package/lib/commands/provar/auth/login.js +90 -0
  8. package/lib/commands/provar/auth/login.js.map +1 -0
  9. package/lib/commands/provar/auth/rotate.d.ts +7 -0
  10. package/lib/commands/provar/auth/rotate.js +42 -0
  11. package/lib/commands/provar/auth/rotate.js.map +1 -0
  12. package/lib/commands/provar/auth/status.d.ts +7 -0
  13. package/lib/commands/provar/auth/status.js +107 -0
  14. package/lib/commands/provar/auth/status.js.map +1 -0
  15. package/lib/commands/provar/mcp/start.d.ts +2 -0
  16. package/lib/commands/provar/mcp/start.js +14 -1
  17. package/lib/commands/provar/mcp/start.js.map +1 -1
  18. package/lib/mcp/docs/NITROX_CATALOG_SOURCE.json +6 -0
  19. package/lib/mcp/docs/NITROX_COMPONENT_CATALOG.md +2001 -0
  20. package/lib/mcp/docs/PROVAR_TEST_STEP_REFERENCE.md +1430 -0
  21. package/lib/mcp/docs/PROVAR_TOOL_GUIDE.md +175 -0
  22. package/lib/mcp/licensing/algasClient.js +14 -5
  23. package/lib/mcp/licensing/algasClient.js.map +1 -1
  24. package/lib/mcp/licensing/ideDetection.d.ts +0 -12
  25. package/lib/mcp/licensing/ideDetection.js +1 -73
  26. package/lib/mcp/licensing/ideDetection.js.map +1 -1
  27. package/lib/mcp/licensing/licenseCache.js +7 -1
  28. package/lib/mcp/licensing/licenseCache.js.map +1 -1
  29. package/lib/mcp/licensing/licenseValidator.d.ts +3 -3
  30. package/lib/mcp/licensing/licenseValidator.js +11 -4
  31. package/lib/mcp/licensing/licenseValidator.js.map +1 -1
  32. package/lib/mcp/prompts/guidePrompts.d.ts +4 -0
  33. package/lib/mcp/prompts/guidePrompts.js +324 -0
  34. package/lib/mcp/prompts/guidePrompts.js.map +1 -0
  35. package/lib/mcp/prompts/index.d.ts +2 -0
  36. package/lib/mcp/prompts/index.js +23 -0
  37. package/lib/mcp/prompts/index.js.map +1 -0
  38. package/lib/mcp/prompts/loopPrompts.d.ts +6 -0
  39. package/lib/mcp/prompts/loopPrompts.js +435 -0
  40. package/lib/mcp/prompts/loopPrompts.js.map +1 -0
  41. package/lib/mcp/prompts/migrationPrompts.d.ts +4 -0
  42. package/lib/mcp/prompts/migrationPrompts.js +207 -0
  43. package/lib/mcp/prompts/migrationPrompts.js.map +1 -0
  44. package/lib/mcp/rules/provar_best_practices_rules.json +256 -544
  45. package/lib/mcp/security/pathPolicy.d.ts +5 -0
  46. package/lib/mcp/security/pathPolicy.js +58 -3
  47. package/lib/mcp/security/pathPolicy.js.map +1 -1
  48. package/lib/mcp/server.d.ts +17 -0
  49. package/lib/mcp/server.js +151 -6
  50. package/lib/mcp/server.js.map +1 -1
  51. package/lib/mcp/tools/antTools.d.ts +15 -0
  52. package/lib/mcp/tools/antTools.js +347 -170
  53. package/lib/mcp/tools/antTools.js.map +1 -1
  54. package/lib/mcp/tools/automationTools.d.ts +18 -8
  55. package/lib/mcp/tools/automationTools.js +332 -176
  56. package/lib/mcp/tools/automationTools.js.map +1 -1
  57. package/lib/mcp/tools/bestPracticesEngine.js +161 -23
  58. package/lib/mcp/tools/bestPracticesEngine.js.map +1 -1
  59. package/lib/mcp/tools/connectionTools.d.ts +4 -0
  60. package/lib/mcp/tools/connectionTools.js +172 -0
  61. package/lib/mcp/tools/connectionTools.js.map +1 -0
  62. package/lib/mcp/tools/defectTools.d.ts +1 -1
  63. package/lib/mcp/tools/defectTools.js +56 -50
  64. package/lib/mcp/tools/defectTools.js.map +1 -1
  65. package/lib/mcp/tools/hierarchyValidate.d.ts +1 -1
  66. package/lib/mcp/tools/hierarchyValidate.js +127 -42
  67. package/lib/mcp/tools/hierarchyValidate.js.map +1 -1
  68. package/lib/mcp/tools/nitroXTools.d.ts +23 -0
  69. package/lib/mcp/tools/nitroXTools.js +823 -0
  70. package/lib/mcp/tools/nitroXTools.js.map +1 -0
  71. package/lib/mcp/tools/pageObjectGenerate.js +132 -57
  72. package/lib/mcp/tools/pageObjectGenerate.js.map +1 -1
  73. package/lib/mcp/tools/pageObjectValidate.js +136 -46
  74. package/lib/mcp/tools/pageObjectValidate.js.map +1 -1
  75. package/lib/mcp/tools/projectInspect.js +51 -30
  76. package/lib/mcp/tools/projectInspect.js.map +1 -1
  77. package/lib/mcp/tools/projectValidateFromPath.js +70 -49
  78. package/lib/mcp/tools/projectValidateFromPath.js.map +1 -1
  79. package/lib/mcp/tools/propertiesTools.d.ts +2 -0
  80. package/lib/mcp/tools/propertiesTools.js +332 -78
  81. package/lib/mcp/tools/propertiesTools.js.map +1 -1
  82. package/lib/mcp/tools/qualityHubApiTools.d.ts +3 -0
  83. package/lib/mcp/tools/qualityHubApiTools.js +138 -0
  84. package/lib/mcp/tools/qualityHubApiTools.js.map +1 -0
  85. package/lib/mcp/tools/qualityHubTools.js +219 -70
  86. package/lib/mcp/tools/qualityHubTools.js.map +1 -1
  87. package/lib/mcp/tools/rcaTools.d.ts +3 -2
  88. package/lib/mcp/tools/rcaTools.js +189 -56
  89. package/lib/mcp/tools/rcaTools.js.map +1 -1
  90. package/lib/mcp/tools/sfSpawn.d.ts +25 -3
  91. package/lib/mcp/tools/sfSpawn.js +154 -6
  92. package/lib/mcp/tools/sfSpawn.js.map +1 -1
  93. package/lib/mcp/tools/testCaseGenerate.js +226 -78
  94. package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
  95. package/lib/mcp/tools/testCaseStepTools.d.ts +4 -0
  96. package/lib/mcp/tools/testCaseStepTools.js +226 -0
  97. package/lib/mcp/tools/testCaseStepTools.js.map +1 -0
  98. package/lib/mcp/tools/testCaseValidate.d.ts +11 -0
  99. package/lib/mcp/tools/testCaseValidate.js +307 -46
  100. package/lib/mcp/tools/testCaseValidate.js.map +1 -1
  101. package/lib/mcp/tools/testPlanTools.d.ts +1 -0
  102. package/lib/mcp/tools/testPlanTools.js +299 -59
  103. package/lib/mcp/tools/testPlanTools.js.map +1 -1
  104. package/lib/mcp/tools/testPlanValidate.js +56 -18
  105. package/lib/mcp/tools/testPlanValidate.js.map +1 -1
  106. package/lib/mcp/tools/testSuiteValidate.js +37 -11
  107. package/lib/mcp/tools/testSuiteValidate.js.map +1 -1
  108. package/lib/mcp/update/updateChecker.d.ts +14 -0
  109. package/lib/mcp/update/updateChecker.js +228 -0
  110. package/lib/mcp/update/updateChecker.js.map +1 -0
  111. package/lib/services/auth/credentials.d.ts +21 -0
  112. package/lib/services/auth/credentials.js +75 -0
  113. package/lib/services/auth/credentials.js.map +1 -0
  114. package/lib/services/auth/loginFlow.d.ts +68 -0
  115. package/lib/services/auth/loginFlow.js +216 -0
  116. package/lib/services/auth/loginFlow.js.map +1 -0
  117. package/lib/services/projectValidation.d.ts +5 -2
  118. package/lib/services/projectValidation.js +83 -31
  119. package/lib/services/projectValidation.js.map +1 -1
  120. package/lib/services/qualityHub/client.d.ts +161 -0
  121. package/lib/services/qualityHub/client.js +226 -0
  122. package/lib/services/qualityHub/client.js.map +1 -0
  123. package/messages/sf.provar.auth.clear.md +16 -0
  124. package/messages/sf.provar.auth.login.md +31 -0
  125. package/messages/sf.provar.auth.rotate.md +23 -0
  126. package/messages/sf.provar.auth.status.md +16 -0
  127. package/messages/sf.provar.mcp.start.md +83 -48
  128. package/oclif.manifest.json +299 -2
  129. package/package.json +23 -12
@@ -4,6 +4,9 @@
4
4
  * Licensed under the BSD 3-Clause license.
5
5
  * For full license text, see LICENSE.md file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
6
  */
7
+ import fs from 'node:fs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
7
10
  import { spawnSync as _spawnSync } from 'node:child_process';
8
11
  /**
9
12
  * Thin wrapper around spawnSync so tests can stub sfSpawnHelper.spawnSync.
@@ -15,21 +18,166 @@ export const sfSpawnHelper = {
15
18
  // ── Shared error type ─────────────────────────────────────────────────────────
16
19
  export class SfNotFoundError extends Error {
17
20
  code = 'SF_NOT_FOUND';
18
- constructor() {
19
- super('sf CLI not found in PATH. Install Salesforce CLI (`npm install -g @salesforce/cli`) and ensure it is in your PATH.');
21
+ constructor(sfPath) {
22
+ const where = sfPath ? `at explicit path "${sfPath}"` : 'in PATH or common npm/nvm/volta install locations';
23
+ super(`sf CLI not found ${where}. ` +
24
+ 'Install Salesforce CLI (npm install -g @salesforce/cli) and ensure the install directory is in your PATH, ' +
25
+ 'or pass sf_path pointing to the sf executable directly ' +
26
+ '(e.g. "~/.nvm/versions/node/v22.0.0/bin/sf").');
20
27
  this.name = 'SfNotFoundError';
21
28
  }
22
29
  }
30
+ const MAX_BUFFER = 50 * 1024 * 1024; // 50 MB — prevents ENOBUFS on verbose Provar runs
31
+ // ── SF CLI discovery ──────────────────────────────────────────────────────────
32
+ /**
33
+ * Returns candidate sf CLI paths in common install locations.
34
+ * Used as a fallback when `sf` is not in PATH.
35
+ */
36
+ export function getSfCommonPaths() {
37
+ const home = os.homedir();
38
+ if (process.platform === 'win32') {
39
+ const appData = process.env['APPDATA'] ?? path.join(home, 'AppData', 'Roaming');
40
+ return [
41
+ path.join(appData, 'npm', 'sf.cmd'),
42
+ path.join('C:', 'Program Files', 'nodejs', 'sf.cmd'),
43
+ path.join('C:', 'Program Files (x86)', 'nodejs', 'sf.cmd'),
44
+ // Windows standalone installer (https://developer.salesforce.com/tools/salesforcecli)
45
+ path.join('C:', 'Program Files', 'sf', 'bin', 'sf.cmd'),
46
+ path.join('C:', 'Program Files', 'sf', 'client', 'bin', 'sf.cmd'),
47
+ ];
48
+ }
49
+ const candidates = [
50
+ '/usr/local/bin/sf',
51
+ path.join(home, '.npm-global', 'bin', 'sf'),
52
+ path.join(home, '.local', 'bin', 'sf'),
53
+ path.join(home, '.volta', 'bin', 'sf'),
54
+ ];
55
+ // nvm — scan the three most-recently installed Node versions
56
+ const nvmBinDir = path.join(process.env['NVM_DIR'] ?? path.join(home, '.nvm'), 'versions', 'node');
57
+ if (fs.existsSync(nvmBinDir)) {
58
+ try {
59
+ for (const v of fs.readdirSync(nvmBinDir).sort().reverse().slice(0, 3)) {
60
+ candidates.push(path.join(nvmBinDir, v, 'bin', 'sf'));
61
+ }
62
+ }
63
+ catch {
64
+ /* skip */
65
+ }
66
+ }
67
+ return candidates;
68
+ }
69
+ // Proactively resolve the sf executable path once on first use and cache it.
70
+ // This ensures sf is always found even when ENOENT is masked by other errors (e.g. ENOBUFS).
71
+ let cachedSfPath; // undefined = not yet probed
72
+ /**
73
+ * Exposed for testing only — pre-seeds the cached sf executable path, bypassing the probe spawn.
74
+ * Pass `undefined` to reset the cache so the next call triggers a fresh probe.
75
+ */
76
+ export function setSfPathCacheForTesting(value) {
77
+ cachedSfPath = value;
78
+ }
79
+ // Platform override used in tests so Windows-specific shell logic can be exercised on any OS.
80
+ let sfPlatformOverride;
81
+ /** Exposed for testing only — overrides process.platform for needsWindowsShell decisions. */
82
+ export function setSfPlatformForTesting(platform) {
83
+ sfPlatformOverride = platform;
84
+ }
85
+ /**
86
+ * Returns true when spawning `executable` requires the Windows shell.
87
+ * On Windows, `.cmd` and `.bat` batch scripts cannot be executed directly by
88
+ * Node's spawnSync — they must be invoked through cmd.exe (i.e. shell: true).
89
+ * The bare name "sf" also needs this treatment on Windows because the file on
90
+ * disk is actually "sf.cmd" and Node won't auto-append the extension.
91
+ */
92
+ export function needsWindowsShell(executable, platform = process.platform) {
93
+ if (platform !== 'win32')
94
+ return false;
95
+ const lower = executable.toLowerCase();
96
+ return lower.endsWith('.cmd') || lower.endsWith('.bat') || !path.extname(lower);
97
+ }
98
+ function resolveSfExecutable() {
99
+ if (cachedSfPath !== undefined)
100
+ return cachedSfPath;
101
+ const platform = sfPlatformOverride ?? process.platform;
102
+ // Two-phase probe avoids false-positives on Windows with shell:true.
103
+ // When shell:true is used, cmd.exe spawns successfully even when `sf` is
104
+ // missing — it exits non-zero with "not recognised" in stderr but sets no
105
+ // probe.error. Trying shell:false first catches both cases correctly.
106
+ //
107
+ // First attempt: shell:false (works on Linux/macOS; gives ENOENT on Windows if
108
+ // sf.cmd is on PATH but requires the shell).
109
+ const probe = sfSpawnHelper.spawnSync('sf', ['--version'], {
110
+ encoding: 'utf-8',
111
+ shell: false,
112
+ maxBuffer: 1024 * 1024,
113
+ });
114
+ if (!probe.error && probe.status === 0) {
115
+ cachedSfPath = 'sf';
116
+ return cachedSfPath;
117
+ }
118
+ // Windows fallback: retry with shell:true when the plain probe failed
119
+ // with ENOENT — meaning sf.cmd exists on PATH but can't run without the shell.
120
+ if (platform === 'win32' && probe.error?.code === 'ENOENT') {
121
+ const probeShell = sfSpawnHelper.spawnSync('sf', ['--version'], {
122
+ encoding: 'utf-8',
123
+ shell: true,
124
+ maxBuffer: 1024 * 1024,
125
+ });
126
+ if (!probeShell.error && probeShell.status === 0) {
127
+ cachedSfPath = 'sf';
128
+ return cachedSfPath;
129
+ }
130
+ }
131
+ // Fall back to common install locations
132
+ for (const candidate of getSfCommonPaths()) {
133
+ if (fs.existsSync(candidate)) {
134
+ cachedSfPath = candidate;
135
+ return cachedSfPath;
136
+ }
137
+ }
138
+ cachedSfPath = null;
139
+ return null;
140
+ }
141
+ /**
142
+ * Reject shell metacharacters in an sf_path that will be executed via shell:true.
143
+ * On Windows, cmd.exe interprets & | ; < > ` ' " and newlines as shell syntax.
144
+ * A valid filesystem path should never contain these characters.
145
+ */
146
+ function assertShellSafePath(sfPath) {
147
+ if (/[&|;<>`'"\n\r]/.test(sfPath)) {
148
+ throw Object.assign(new Error('sf_path contains characters that are unsafe for shell execution on Windows ' +
149
+ '(& | ; < > ` \' " or line-breaks). Provide an absolute filesystem path to the sf executable.'), { code: 'INVALID_SF_PATH' });
150
+ }
151
+ }
23
152
  /**
24
153
  * Run `sf <args>` synchronously and return stdout, stderr, and exit code.
25
- * Throws SfNotFoundError if the `sf` binary is not in PATH.
154
+ * Throws SfNotFoundError if the `sf` binary cannot be found.
155
+ * Pass `sfPath` to override auto-discovery with an explicit executable path.
26
156
  */
27
- export function runSfCommand(args) {
28
- const result = sfSpawnHelper.spawnSync('sf', args, { encoding: 'utf-8', shell: false });
157
+ export function runSfCommand(args, sfPath) {
158
+ // Treat empty/whitespace sfPath as absent so auto-discovery runs instead of
159
+ // throwing SfNotFoundError with no useful path hint.
160
+ const trimmedSfPath = sfPath?.trim();
161
+ const resolvedSfPath = trimmedSfPath !== '' ? trimmedSfPath : undefined;
162
+ const executable = resolvedSfPath ?? resolveSfExecutable();
163
+ if (!executable)
164
+ throw new SfNotFoundError();
165
+ const platform = sfPlatformOverride ?? process.platform;
166
+ const useShell = needsWindowsShell(executable, platform);
167
+ // Guard against injection when shell:true is used with a user-supplied path.
168
+ // Common install locations returned by resolveSfExecutable() are safe by construction.
169
+ if (useShell && resolvedSfPath) {
170
+ assertShellSafePath(resolvedSfPath);
171
+ }
172
+ const result = sfSpawnHelper.spawnSync(executable, args, {
173
+ encoding: 'utf-8',
174
+ shell: useShell,
175
+ maxBuffer: MAX_BUFFER,
176
+ });
29
177
  if (result.error) {
30
178
  const err = result.error;
31
179
  if (err.code === 'ENOENT') {
32
- throw new SfNotFoundError();
180
+ throw new SfNotFoundError(resolvedSfPath);
33
181
  }
34
182
  throw result.error;
35
183
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sfSpawn.js","sourceRoot":"","sources":["../../../src/mcp/tools/sfSpawn.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,IAAI,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAE7D;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,SAAS,EAAE,UAAU;CACtB,CAAC;AAEF,iFAAiF;AAEjF,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACxB,IAAI,GAAG,cAAc,CAAC;IACtC;QACE,KAAK,CACH,oHAAoH,CACrH,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAUD;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,IAAc;IACzC,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAExF,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,MAAM,GAAG,GAAG,MAAM,CAAC,KAA8B,CAAC;QAClD,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC1B,MAAM,IAAI,eAAe,EAAE,CAAC;QAC9B,CAAC;QACD,MAAM,MAAM,CAAC,KAAK,CAAC;IACrB,CAAC;IAED,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;QAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,EAAE;QAC3B,QAAQ,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC;KAC7B,CAAC;AACJ,CAAC;AAED,iFAAiF;AAEjF;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACpC,CAAC"}
1
+ {"version":3,"file":"sfSpawn.js","sourceRoot":"","sources":["../../../src/mcp/tools/sfSpawn.ts"],"names":[],"mappings":"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"}
@@ -54,6 +54,14 @@ function buildStepWarnings(steps) {
54
54
  'To assert SOQL results use either: (a) a ForEach loop over the result list with AssertValues inside, ' +
55
55
  'or (b) a SetValues step to extract a specific field into a named variable, then assert that variable.');
56
56
  }
57
+ // D7: Cleanup steps placed after a potential failure point are skipped when stopOnError=false.
58
+ if (resolvedIds.includes(SHORTHAND_TO_FQID['ApexDeleteObject'] ?? '')) {
59
+ warnings.push('ApexDeleteObject detected (likely cleanup): with stopOnError=false Provar skips all steps after ' +
60
+ 'the first failure, so cleanup steps placed at the end of the test will NOT run when an earlier ' +
61
+ 'step fails — leaving orphaned records in the org. ' +
62
+ 'Wrap cleanup in a Provar TearDown callable, or place create/delete inside the same UiWithScreen ' +
63
+ 'clause so both run as a unit regardless of failure.');
64
+ }
57
65
  return warnings;
58
66
  }
59
67
  // ── Schema ────────────────────────────────────────────────────────────────────
@@ -69,58 +77,92 @@ const StepSchema = z.object({
69
77
  attributes: z
70
78
  .record(z.string())
71
79
  .default({})
72
- .describe('Step argument values as key/value pairs. Written as <arguments><argument id="key"><value .../></argument></arguments> ' +
73
- 'inside the <apiCall> element — the format Provar runtime requires. ' +
80
+ .describe('Step argument values as key/value pairs. Written as <arguments><argument id="key"><value .../></argument></arguments>. ' +
74
81
  'Do NOT rely on XML attributes on <apiCall>; the runtime silently ignores them. ' +
75
- 'Example: { "connectionName": "MyOrg", "objectApiName": "Opportunity" }'),
82
+ 'Special value conventions (applied automatically by the generator): ' +
83
+ '(1) Variable references: wrap the name in braces, e.g. "{MyVar}" → emitted as class="variable" <path element="MyVar"/>. ' +
84
+ ' Dotted paths are also supported: "{Obj.Field}" → two <path> elements. ' +
85
+ '(2) SetValues: pass each variable name and its value as a flat key/value pair; ' +
86
+ ' the generator wraps them in <value class="valueList"><namedValues>...</namedValues></value> automatically. ' +
87
+ ' Example: { "testCaseName": "TC_New", "testType": "Acceptance testing" } ' +
88
+ '(3) AssertValues: pass assertion arguments as flat key/value pairs; emitted as flat <argument> elements, NOT wrapped in valueList/namedValues. ' +
89
+ '(4) target argument (UiWithScreen / UiWithRow): pass the sf:ui:target or ui:pageobject:target URI; ' +
90
+ ' emitted as class="uiTarget" uri="...". ' +
91
+ '(5) locator argument (UiDoAction / UiAssert): pass the locator URI; emitted as class="uiLocator" uri="...". ' +
92
+ 'All other string values use class="value" valueClass="string".'),
76
93
  });
77
94
  const TOOL_DESCRIPTION = [
78
95
  'Generate a Provar XML test case skeleton with proper UUID v4 guids, sequential testItemId values, and <steps> structure.',
79
96
  'Returns XML content. Writes to disk only when dry_run=false.',
97
+ 'Generated structure: <?xml version="1.0" encoding="UTF-8" standalone="no"?> with <testCase guid="..." id="1" registryId="..."> (id is always the integer literal "1" as required by the Provar runtime), a <summary/> child, then <steps>.',
98
+ 'URI-aware generation: use target_uri to control the XML nesting structure.',
99
+ ' - sf:ui:target (or omit target_uri) → flat Salesforce XML structure (existing behaviour).',
100
+ ' - ui:pageobject:target?pageId=pageobjects.PageClass → wraps all steps in a UiWithScreen element targeting that non-SF page object.',
80
101
  'API IDs: shorthand forms (e.g. UiConnect, ApexSoqlQuery) are automatically expanded to fully-qualified IDs required by the Provar runtime.',
81
102
  'Step arguments: attributes are emitted as <arguments><argument id="..."><value .../></argument></arguments> — the only format the Provar runtime processes.',
82
103
  'Shorthand XML attributes on <apiCall> are silently ignored at runtime; always supply arguments via the attributes map.',
104
+ 'ApexSoqlQuery argument IDs: soqlQuery (the SOQL SELECT statement), resultListName (binds result list to a variable), apexConnectionName (named connection), resultScope (optional).',
83
105
  'Data-driven note: <dataTable> only iterates rows when the test case runs via a test plan instance (.testinstance).',
84
106
  'Running directly via the provardx testCase property resolves all data table variables as null.',
85
- 'Use provar.testplan.add-instance to wire into a plan for data-driven execution.',
107
+ 'Use provar_testplan_add-instance to wire into a plan for data-driven execution.',
86
108
  'ApexReadObject requires field names in attributes; omitting them produces MALFORMED_QUERY. Prefer ApexSoqlQuery.',
87
109
  'AssertValues on SOQL results: index paths like "ResultList[0].Field" are not supported.',
88
110
  'Use ForEach to iterate the result list, or SetValues to extract a field into a variable first.',
89
- 'Validation: the response always includes a validation field with is_valid, validity_score, quality_score, and any structural issues — check this before attempting to run the test case.',
111
+ 'SetValues: pass named variable values as flat key/value pairs in attributes; ' +
112
+ 'the generator wraps them in <value class="valueList"><namedValues>...</namedValues></value> automatically.',
113
+ 'AssertValues: pass assertion values as flat key/value argument pairs; emitted as flat arguments, NOT wrapped in namedValues. ' +
114
+ 'If AssertValues uses namedValues-shaped content, validation reports warning ASSERT-001.',
115
+ 'Variable references: pass values as "{VarName}" (braces); emitted as class="variable" <path element="VarName"/>.',
116
+ 'target argument (UiWithScreen/UiWithRow): pass the URI value; emitted as class="uiTarget" uri="...".',
117
+ 'locator argument (UiDoAction/UiAssert): pass the URI value; emitted as class="uiLocator" uri="...".',
118
+ 'Edit page objects: action=Edit targets require a compiled page object for the SF object. ' +
119
+ 'If none exists in the project page-objects directory, the locator binding will fail at runtime. ' +
120
+ 'For objects without a compiled Edit page object, use inline edit instead: sfIleActivate to activate the field, ' +
121
+ 'set the value, then SaveEdit binding on the Record view screen.',
122
+ 'Provar IDE warning: opening a generated test case in Provar IDE injects empty <argument id="..."/> elements for known parameter IDs. ' +
123
+ 'If the empty element appears after a populated one, the empty version wins at runtime. ' +
124
+ 'Always check for and remove duplicate empty arguments after any IDE open/save cycle before re-running.',
125
+ 'Cleanup warning: ApexDeleteObject steps near end of test will be skipped if an earlier step fails (stopOnError=false). Use a TearDown callable.',
126
+ 'Validation: when validate_after_edit=true (default) the response includes a validation field and returns TESTCASE_INVALID if the generated XML fails structural checks.',
127
+ 'Grounding: call provar_qualityhub_examples_retrieve before generating to get corpus examples for the scenario — correct XML structure for the step types you need.',
128
+ 'If the response has count: 0 with a warning field (API unavailable or not configured), fall back: read the provar://docs/step-reference MCP resource for step types and attribute formats, then continue.',
90
129
  ].join(' ');
91
130
  export function registerTestCaseGenerate(server, config) {
92
- server.tool('provar.testcase.generate', TOOL_DESCRIPTION, {
93
- test_case_name: z.string().describe('Test case name (human-readable label)'),
94
- test_case_id: z
95
- .string()
96
- .optional()
97
- .describe('Explicit test case id; auto-generated UUID v4 if omitted'),
98
- steps: z.array(StepSchema).default([]).describe('Ordered list of test steps'),
99
- output_path: z
100
- .string()
101
- .optional()
102
- .describe('Suggested file path for the .xml file (returned in response)'),
103
- overwrite: z.boolean().default(false).describe('Overwrite if output_path file already exists'),
104
- dry_run: z
105
- .boolean()
106
- .default(true)
107
- .describe('true = return XML only (default); false = write to output_path'),
108
- idempotency_key: z
109
- .string()
110
- .optional()
111
- .describe('Caller-provided key echoed back for deduplication tracking'),
131
+ server.registerTool('provar_testcase_generate', {
132
+ title: 'Generate Test Case',
133
+ description: TOOL_DESCRIPTION,
134
+ inputSchema: {
135
+ test_case_name: z.string().describe('Test case name (human-readable label)'),
136
+ steps: z.array(StepSchema).default([]).describe('Ordered list of test steps'),
137
+ target_uri: z
138
+ .string()
139
+ .optional()
140
+ .describe('Page object URI that determines the XML nesting structure. ' +
141
+ 'Omit or use "sf:ui:target" for Salesforce targets (flat structure). ' +
142
+ 'Use "ui:pageobject:target?pageId=pageobjects.PageClass" for non-SF page objects ' +
143
+ 'steps are wrapped in a UiWithScreen element targeting that class.'),
144
+ output_path: z.string().optional().describe('Suggested file path for the .xml file (returned in response)'),
145
+ overwrite: z.boolean().default(false).describe('Overwrite if output_path file already exists'),
146
+ dry_run: z.boolean().default(true).describe('true = return XML only (default); false = write to output_path'),
147
+ validate_after_edit: z
148
+ .boolean()
149
+ .default(true)
150
+ .describe('Run structural validation after generation (default: true). ' +
151
+ 'Returns TESTCASE_INVALID error if the generated XML fails validation. ' +
152
+ 'Set false to skip validation and omit the validation field from the response.'),
153
+ idempotency_key: z.string().optional().describe('Caller-provided key echoed back for deduplication tracking'),
154
+ },
112
155
  }, (input) => {
113
156
  const requestId = makeRequestId();
114
- log('info', 'provar.testcase.generate', {
157
+ log('info', 'provar_testcase_generate', {
115
158
  requestId,
116
159
  test_case_name: input.test_case_name,
117
160
  dry_run: input.dry_run,
161
+ target_uri: input.target_uri,
118
162
  });
119
163
  try {
120
164
  const xmlContent = buildTestCaseXml(input);
121
- const filePath = input.output_path
122
- ? path.resolve(input.output_path)
123
- : undefined;
165
+ const filePath = input.output_path ? path.resolve(input.output_path) : undefined;
124
166
  let written = false;
125
167
  if (filePath && !input.dry_run) {
126
168
  assertPathAllowed(filePath, config.allowedPaths);
@@ -131,19 +173,11 @@ export function registerTestCaseGenerate(server, config) {
131
173
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
132
174
  fs.writeFileSync(filePath, xmlContent, 'utf-8');
133
175
  written = true;
134
- log('info', 'provar.testcase.generate: wrote file', { requestId, filePath });
176
+ log('info', 'provar_testcase_generate: wrote file', { requestId, filePath });
135
177
  }
136
178
  const warnings = buildStepWarnings(input.steps);
137
- const validationFull = validateTestCase(xmlContent, input.test_case_name);
138
- const validationSlim = {
139
- is_valid: validationFull.is_valid,
140
- validity_score: validationFull.validity_score,
141
- quality_score: validationFull.quality_score,
142
- error_count: validationFull.error_count,
143
- warning_count: validationFull.warning_count,
144
- issues: validationFull.issues,
145
- };
146
- const result = {
179
+ const runValidation = input.validate_after_edit !== false;
180
+ const baseResult = {
147
181
  requestId,
148
182
  xml_content: xmlContent,
149
183
  file_path: filePath,
@@ -151,71 +185,185 @@ export function registerTestCaseGenerate(server, config) {
151
185
  dry_run: input.dry_run,
152
186
  step_count: input.steps.length,
153
187
  idempotency_key: input.idempotency_key,
154
- validation: validationSlim,
155
188
  ...(warnings.length > 0 ? { warnings } : {}),
156
189
  };
190
+ if (runValidation) {
191
+ const validationFull = validateTestCase(xmlContent, input.test_case_name);
192
+ const validationSlim = {
193
+ is_valid: validationFull.is_valid,
194
+ validity_score: validationFull.validity_score,
195
+ quality_score: validationFull.quality_score,
196
+ error_count: validationFull.error_count,
197
+ warning_count: validationFull.warning_count,
198
+ issues: validationFull.issues,
199
+ };
200
+ if (!validationFull.is_valid) {
201
+ const errResult = makeError('TESTCASE_INVALID', `Generated test case failed structural validation (${validationFull.error_count} error(s)). See details.validation.`, requestId, false, { validation: validationSlim });
202
+ log('warn', 'provar_testcase_generate: TESTCASE_INVALID', { requestId });
203
+ return { isError: true, content: [{ type: 'text', text: JSON.stringify(errResult) }] };
204
+ }
205
+ const result = { ...baseResult, validation: validationSlim };
206
+ return {
207
+ content: [{ type: 'text', text: JSON.stringify(result) }],
208
+ structuredContent: result,
209
+ };
210
+ }
157
211
  return {
158
- content: [{ type: 'text', text: JSON.stringify(result) }],
159
- structuredContent: result,
212
+ content: [{ type: 'text', text: JSON.stringify(baseResult) }],
213
+ structuredContent: baseResult,
160
214
  };
161
215
  }
162
216
  catch (err) {
163
217
  const error = err;
164
- const errResult = makeError(error instanceof PathPolicyError ? error.code : (error.code ?? 'GENERATE_ERROR'), error.message, requestId, false);
165
- log('error', 'provar.testcase.generate failed', { requestId, error: error.message });
218
+ const errResult = makeError(error instanceof PathPolicyError ? error.code : error.code ?? 'GENERATE_ERROR', error.message, requestId, false);
219
+ log('error', 'provar_testcase_generate failed', { requestId, error: error.message });
166
220
  return { isError: true, content: [{ type: 'text', text: JSON.stringify(errResult) }] };
167
221
  }
168
222
  });
169
223
  }
170
224
  // ── XML builder ───────────────────────────────────────────────────────────────
171
- function buildArgumentsXml(attributes) {
225
+ // F1/F3: build class="compound" for strings that mix literal text with {VarName} tokens.
226
+ function buildCompoundValue(val, indent) {
227
+ const i = `${indent} `;
228
+ const parts = [];
229
+ const tokenRe = /\{([\w.]+)\}/g;
230
+ let last = 0;
231
+ let m;
232
+ while ((m = tokenRe.exec(val)) !== null) {
233
+ const before = val.slice(last, m.index);
234
+ if (before)
235
+ parts.push(`${i}<value valueClass="string">${escapeXmlContent(before)}</value>`);
236
+ const pathElements = m[1]
237
+ .split('.')
238
+ .map((p) => `${i} <path element="${escapeXmlAttr(p)}"/>`)
239
+ .join('\n');
240
+ parts.push(`${i}<variable>\n${pathElements}\n${i}</variable>`);
241
+ last = m.index + m[0].length;
242
+ }
243
+ const tail = val.slice(last);
244
+ if (tail)
245
+ parts.push(`${i}<value valueClass="string">${escapeXmlContent(tail)}</value>`);
246
+ return `${indent}<value class="compound">\n${i}<parts>\n${parts.join('\n')}\n${i}</parts>\n${indent}</value>`;
247
+ }
248
+ // Build the <value> element for a single argument (D2/D4/F1 aware).
249
+ // inNamedValues: when true (inside SetValues namedValues), skip uiTarget/uiLocator dispatch.
250
+ // apiId: resolved API ID used to restrict key-name dispatch to the correct UI APIs.
251
+ function buildArgumentValue(key, val, indent, inNamedValues = false, apiId = '') {
252
+ // D4: {VarName} or {Obj.Field} → class="variable" with <path> elements.
253
+ const varMatch = /^\{([\w.]+)\}$/.exec(val);
254
+ if (varMatch) {
255
+ const pathElements = varMatch[1]
256
+ .split('.')
257
+ .map((p) => `${indent} <path element="${escapeXmlAttr(p)}"/>`)
258
+ .join('\n');
259
+ return `${indent}<value class="variable">\n${pathElements}\n${indent}</value>`;
260
+ }
261
+ // F1/F3: {VarName} embedded in surrounding text → class="compound" with <parts>.
262
+ if (/\{[\w.]+\}/.test(val)) {
263
+ return buildCompoundValue(val, indent);
264
+ }
265
+ if (!inNamedValues) {
266
+ // D2: 'target' argument → class="uiTarget" (only for UiWithScreen / UiWithRow).
267
+ if (key === 'target' && (apiId.includes('UiWithScreen') || apiId.includes('UiWithRow'))) {
268
+ return `${indent}<value class="uiTarget" uri="${escapeXmlAttr(val)}"/>`;
269
+ }
270
+ // D2: 'locator' argument → class="uiLocator" (only for UiDoAction / UiAssert).
271
+ if (key === 'locator' && (apiId.includes('UiDoAction') || apiId.includes('UiAssert'))) {
272
+ return `${indent}<value class="uiLocator" uri="${escapeXmlAttr(val)}"/>`;
273
+ }
274
+ }
275
+ return `${indent}<value class="value" valueClass="string">${escapeXmlContent(val)}</value>`;
276
+ }
277
+ function buildArgumentsXml(attributes, baseIndent = ' ', apiId = '') {
172
278
  const entries = Object.entries(attributes);
173
279
  if (entries.length === 0)
174
280
  return '';
175
281
  const argLines = entries
176
- .map(([k, v]) => ` <argument id="${escapeXmlAttr(k)}">\n` +
177
- ` <value class="value" valueClass="String">${escapeXmlContent(v)}</value>\n` +
178
- ' </argument>')
282
+ .map(([k, v]) => {
283
+ const valueXml = buildArgumentValue(k, v, `${baseIndent} `, false, apiId);
284
+ return `${baseIndent}<argument id="${escapeXmlAttr(k)}">\n` + valueXml + '\n' + `${baseIndent}</argument>`;
285
+ })
179
286
  .join('\n');
180
- return `\n <arguments>\n${argLines}\n </arguments>\n `;
287
+ return `\n${baseIndent}<arguments>\n${argLines}\n${baseIndent}</arguments>\n${baseIndent.slice(0, -2)}`;
288
+ }
289
+ // D3: SetValues — all attributes become <namedValues> under a single 'values' argument.
290
+ function buildSetValuesXml(attributes, baseIndent) {
291
+ const entries = Object.entries(attributes);
292
+ if (entries.length === 0)
293
+ return '';
294
+ const i = (n) => baseIndent + ' '.repeat(n);
295
+ const namedValueLines = entries
296
+ .map(([name, val]) => {
297
+ const valueXml = buildArgumentValue(name, val, `${i(3)} `, true);
298
+ return `${i(3)}<namedValue name="${escapeXmlAttr(name)}">\n${valueXml}\n${i(3)}</namedValue>`;
299
+ })
300
+ .join('\n');
301
+ return (`\n${i(0)}<arguments>\n` +
302
+ `${i(0)}<argument id="values">\n` +
303
+ `${i(1)}<value class="valueList" mutable="Mutable">\n` +
304
+ `${i(2)}<namedValues>\n` +
305
+ namedValueLines +
306
+ '\n' +
307
+ `${i(2)}</namedValues>\n` +
308
+ `${i(1)}</value>\n` +
309
+ `${i(0)}</argument>\n` +
310
+ `${i(0)}</arguments>\n` +
311
+ `${baseIndent.slice(0, -2)}`);
312
+ }
313
+ function buildFlatStepXml(step, testItemId, indent) {
314
+ const guid = randomUUID();
315
+ const resolvedApiId = resolveApiId(step.api_id);
316
+ const baseIndent = indent + ' ';
317
+ // Use SetValues structure for any SetValues API (string-match mirrors the validator).
318
+ const argumentsXml = resolvedApiId.includes('SetValues')
319
+ ? buildSetValuesXml(step.attributes, baseIndent)
320
+ : buildArgumentsXml(step.attributes, baseIndent, resolvedApiId);
321
+ if (argumentsXml) {
322
+ return (`${indent}<apiCall guid="${guid}" apiId="${escapeXmlAttr(resolvedApiId)}"` +
323
+ ` name="${escapeXmlAttr(step.name)}" testItemId="${testItemId}">${argumentsXml}</apiCall>`);
324
+ }
325
+ return (`${indent}<apiCall guid="${guid}" apiId="${escapeXmlAttr(resolvedApiId)}"` +
326
+ ` name="${escapeXmlAttr(step.name)}" testItemId="${testItemId}"/>`);
181
327
  }
182
328
  function buildTestCaseXml(input) {
183
- const testCaseId = input.test_case_id ?? randomUUID();
184
329
  const testCaseGuid = randomUUID();
185
330
  const registryId = randomUUID();
186
- const stepLines = input.steps
187
- .map((step, i) => {
188
- const guid = randomUUID();
189
- const testItemId = i + 1;
190
- const resolvedApiId = resolveApiId(step.api_id);
191
- const argumentsXml = buildArgumentsXml(step.attributes);
192
- if (argumentsXml) {
193
- return (` <apiCall guid="${guid}" apiId="${escapeXmlAttr(resolvedApiId)}"` +
194
- ` name="${escapeXmlAttr(step.name)}" testItemId="${testItemId}">${argumentsXml}</apiCall>`);
195
- }
196
- return (` <apiCall guid="${guid}" apiId="${escapeXmlAttr(resolvedApiId)}"` +
197
- ` name="${escapeXmlAttr(step.name)}" testItemId="${testItemId}"/>`);
198
- })
199
- .join('\n');
200
- return ('<?xml version="1.0" encoding="UTF-8"?>\n' +
201
- `<testCase id="${testCaseId}" guid="${testCaseGuid}" registryId="${registryId}"` +
202
- ` name="${escapeXmlAttr(input.test_case_name)}">\n` +
331
+ let stepLines;
332
+ const isNonSf = !!input.target_uri && input.target_uri.startsWith('ui:');
333
+ if (isNonSf && input.target_uri) {
334
+ stepLines = buildUiWithScreenXml(input.steps, input.target_uri);
335
+ }
336
+ else {
337
+ const lines = input.steps.map((step, i) => buildFlatStepXml(step, i + 1, ' ')).join('\n');
338
+ stepLines = lines || ' <!-- TODO: Add test steps here -->';
339
+ }
340
+ // Provar requires: standalone="no", id="1" (integer literal), no name attr, <summary/> before <steps>.
341
+ return ('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' +
342
+ `<testCase guid="${testCaseGuid}" id="1" registryId="${registryId}">\n` +
343
+ ' <summary/>\n' +
203
344
  ' <steps>\n' +
204
- (stepLines || ' <!-- TODO: Add test steps here -->') +
345
+ stepLines +
205
346
  '\n </steps>\n' +
206
347
  '</testCase>\n');
207
348
  }
349
+ function buildUiWithScreenXml(steps, targetUri) {
350
+ const wrapperGuid = randomUUID();
351
+ const wrapperApiId = resolveApiId('UiWithScreen');
352
+ // Inner steps use testItemIds starting at 3; the substeps clause itself occupies testItemId=2
353
+ const innerLines = steps.map((step, i) => buildFlatStepXml(step, i + 3, ' ')).join('\n');
354
+ const stepsContent = innerLines ? `\n${innerLines}\n ` : '';
355
+ const clausesXml = '\n <clauses>\n' +
356
+ ' <clause name="substeps" testItemId="2">\n' +
357
+ ` <steps>${stepsContent}</steps>\n` +
358
+ ' </clause>\n' +
359
+ ' </clauses>\n ';
360
+ return (` <apiCall guid="${wrapperGuid}" apiId="${wrapperApiId}"` +
361
+ ` name="With page" testItemId="1">${buildArgumentsXml({ target: targetUri }, ' ', wrapperApiId).trimEnd()}${clausesXml}</apiCall>`);
362
+ }
208
363
  function escapeXmlAttr(value) {
209
- return value
210
- .replace(/&/g, '&amp;')
211
- .replace(/"/g, '&quot;')
212
- .replace(/</g, '&lt;')
213
- .replace(/>/g, '&gt;');
364
+ return value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
214
365
  }
215
366
  function escapeXmlContent(value) {
216
- return value
217
- .replace(/&/g, '&amp;')
218
- .replace(/</g, '&lt;')
219
- .replace(/>/g, '&gt;');
367
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
220
368
  }
221
369
  //# sourceMappingURL=testCaseGenerate.js.map