@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.
- package/README.md +163 -13
- package/bin/mcp-start.js +74 -0
- package/lib/commands/provar/auth/clear.d.ts +7 -0
- package/lib/commands/provar/auth/clear.js +36 -0
- package/lib/commands/provar/auth/clear.js.map +1 -0
- package/lib/commands/provar/auth/login.d.ts +10 -0
- package/lib/commands/provar/auth/login.js +90 -0
- package/lib/commands/provar/auth/login.js.map +1 -0
- package/lib/commands/provar/auth/rotate.d.ts +7 -0
- package/lib/commands/provar/auth/rotate.js +42 -0
- package/lib/commands/provar/auth/rotate.js.map +1 -0
- package/lib/commands/provar/auth/status.d.ts +7 -0
- package/lib/commands/provar/auth/status.js +107 -0
- package/lib/commands/provar/auth/status.js.map +1 -0
- package/lib/commands/provar/mcp/start.d.ts +2 -0
- package/lib/commands/provar/mcp/start.js +14 -1
- package/lib/commands/provar/mcp/start.js.map +1 -1
- package/lib/mcp/docs/NITROX_CATALOG_SOURCE.json +6 -0
- package/lib/mcp/docs/NITROX_COMPONENT_CATALOG.md +2001 -0
- package/lib/mcp/docs/PROVAR_TEST_STEP_REFERENCE.md +1430 -0
- package/lib/mcp/docs/PROVAR_TOOL_GUIDE.md +175 -0
- package/lib/mcp/licensing/algasClient.js +14 -5
- package/lib/mcp/licensing/algasClient.js.map +1 -1
- package/lib/mcp/licensing/ideDetection.d.ts +0 -12
- package/lib/mcp/licensing/ideDetection.js +1 -73
- package/lib/mcp/licensing/ideDetection.js.map +1 -1
- package/lib/mcp/licensing/licenseCache.js +7 -1
- package/lib/mcp/licensing/licenseCache.js.map +1 -1
- package/lib/mcp/licensing/licenseValidator.d.ts +3 -3
- package/lib/mcp/licensing/licenseValidator.js +11 -4
- package/lib/mcp/licensing/licenseValidator.js.map +1 -1
- package/lib/mcp/prompts/guidePrompts.d.ts +4 -0
- package/lib/mcp/prompts/guidePrompts.js +324 -0
- package/lib/mcp/prompts/guidePrompts.js.map +1 -0
- package/lib/mcp/prompts/index.d.ts +2 -0
- package/lib/mcp/prompts/index.js +23 -0
- package/lib/mcp/prompts/index.js.map +1 -0
- package/lib/mcp/prompts/loopPrompts.d.ts +6 -0
- package/lib/mcp/prompts/loopPrompts.js +435 -0
- package/lib/mcp/prompts/loopPrompts.js.map +1 -0
- package/lib/mcp/prompts/migrationPrompts.d.ts +4 -0
- package/lib/mcp/prompts/migrationPrompts.js +207 -0
- package/lib/mcp/prompts/migrationPrompts.js.map +1 -0
- package/lib/mcp/rules/provar_best_practices_rules.json +256 -544
- package/lib/mcp/security/pathPolicy.d.ts +5 -0
- package/lib/mcp/security/pathPolicy.js +58 -3
- package/lib/mcp/security/pathPolicy.js.map +1 -1
- package/lib/mcp/server.d.ts +17 -0
- package/lib/mcp/server.js +151 -6
- package/lib/mcp/server.js.map +1 -1
- package/lib/mcp/tools/antTools.d.ts +15 -0
- package/lib/mcp/tools/antTools.js +347 -170
- package/lib/mcp/tools/antTools.js.map +1 -1
- package/lib/mcp/tools/automationTools.d.ts +18 -8
- package/lib/mcp/tools/automationTools.js +332 -176
- package/lib/mcp/tools/automationTools.js.map +1 -1
- package/lib/mcp/tools/bestPracticesEngine.js +161 -23
- package/lib/mcp/tools/bestPracticesEngine.js.map +1 -1
- package/lib/mcp/tools/connectionTools.d.ts +4 -0
- package/lib/mcp/tools/connectionTools.js +172 -0
- package/lib/mcp/tools/connectionTools.js.map +1 -0
- package/lib/mcp/tools/defectTools.d.ts +1 -1
- package/lib/mcp/tools/defectTools.js +56 -50
- package/lib/mcp/tools/defectTools.js.map +1 -1
- package/lib/mcp/tools/hierarchyValidate.d.ts +1 -1
- package/lib/mcp/tools/hierarchyValidate.js +127 -42
- package/lib/mcp/tools/hierarchyValidate.js.map +1 -1
- package/lib/mcp/tools/nitroXTools.d.ts +23 -0
- package/lib/mcp/tools/nitroXTools.js +823 -0
- package/lib/mcp/tools/nitroXTools.js.map +1 -0
- package/lib/mcp/tools/pageObjectGenerate.js +132 -57
- package/lib/mcp/tools/pageObjectGenerate.js.map +1 -1
- package/lib/mcp/tools/pageObjectValidate.js +136 -46
- package/lib/mcp/tools/pageObjectValidate.js.map +1 -1
- package/lib/mcp/tools/projectInspect.js +51 -30
- package/lib/mcp/tools/projectInspect.js.map +1 -1
- package/lib/mcp/tools/projectValidateFromPath.js +70 -49
- package/lib/mcp/tools/projectValidateFromPath.js.map +1 -1
- package/lib/mcp/tools/propertiesTools.d.ts +2 -0
- package/lib/mcp/tools/propertiesTools.js +332 -78
- package/lib/mcp/tools/propertiesTools.js.map +1 -1
- package/lib/mcp/tools/qualityHubApiTools.d.ts +3 -0
- package/lib/mcp/tools/qualityHubApiTools.js +138 -0
- package/lib/mcp/tools/qualityHubApiTools.js.map +1 -0
- package/lib/mcp/tools/qualityHubTools.js +219 -70
- package/lib/mcp/tools/qualityHubTools.js.map +1 -1
- package/lib/mcp/tools/rcaTools.d.ts +3 -2
- package/lib/mcp/tools/rcaTools.js +189 -56
- package/lib/mcp/tools/rcaTools.js.map +1 -1
- package/lib/mcp/tools/sfSpawn.d.ts +25 -3
- package/lib/mcp/tools/sfSpawn.js +154 -6
- package/lib/mcp/tools/sfSpawn.js.map +1 -1
- package/lib/mcp/tools/testCaseGenerate.js +226 -78
- package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
- package/lib/mcp/tools/testCaseStepTools.d.ts +4 -0
- package/lib/mcp/tools/testCaseStepTools.js +226 -0
- package/lib/mcp/tools/testCaseStepTools.js.map +1 -0
- package/lib/mcp/tools/testCaseValidate.d.ts +11 -0
- package/lib/mcp/tools/testCaseValidate.js +307 -46
- package/lib/mcp/tools/testCaseValidate.js.map +1 -1
- package/lib/mcp/tools/testPlanTools.d.ts +1 -0
- package/lib/mcp/tools/testPlanTools.js +299 -59
- package/lib/mcp/tools/testPlanTools.js.map +1 -1
- package/lib/mcp/tools/testPlanValidate.js +56 -18
- package/lib/mcp/tools/testPlanValidate.js.map +1 -1
- package/lib/mcp/tools/testSuiteValidate.js +37 -11
- package/lib/mcp/tools/testSuiteValidate.js.map +1 -1
- package/lib/mcp/update/updateChecker.d.ts +14 -0
- package/lib/mcp/update/updateChecker.js +228 -0
- package/lib/mcp/update/updateChecker.js.map +1 -0
- package/lib/services/auth/credentials.d.ts +21 -0
- package/lib/services/auth/credentials.js +75 -0
- package/lib/services/auth/credentials.js.map +1 -0
- package/lib/services/auth/loginFlow.d.ts +68 -0
- package/lib/services/auth/loginFlow.js +216 -0
- package/lib/services/auth/loginFlow.js.map +1 -0
- package/lib/services/projectValidation.d.ts +5 -2
- package/lib/services/projectValidation.js +83 -31
- package/lib/services/projectValidation.js.map +1 -1
- package/lib/services/qualityHub/client.d.ts +161 -0
- package/lib/services/qualityHub/client.js +226 -0
- package/lib/services/qualityHub/client.js.map +1 -0
- package/messages/sf.provar.auth.clear.md +16 -0
- package/messages/sf.provar.auth.login.md +31 -0
- package/messages/sf.provar.auth.rotate.md +23 -0
- package/messages/sf.provar.auth.status.md +16 -0
- package/messages/sf.provar.mcp.start.md +83 -48
- package/oclif.manifest.json +299 -2
- package/package.json +23 -12
package/lib/mcp/tools/sfSpawn.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
|
1
|
+
{"version":3,"file":"sfSpawn.js","sourceRoot":"","sources":["../../../src/mcp/tools/sfSpawn.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,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,YAAmB,MAAe;QAChC,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,qBAAqB,MAAM,GAAG,CAAC,CAAC,CAAC,mDAAmD,CAAC;QAC5G,KAAK,CACH,oBAAoB,KAAK,IAAI;YAC3B,4GAA4G;YAC5G,yDAAyD;YACzD,+CAA+C,CAClD,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAUD,MAAM,UAAU,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,kDAAkD;AAEvF,iFAAiF;AAEjF;;;GAGG;AACH,MAAM,UAAU,gBAAgB;IAC9B,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;IAC1B,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QACjC,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;QAChF,OAAO;YACL,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,CAAC;YACnC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,EAAE,QAAQ,CAAC;YACpD,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,qBAAqB,EAAE,QAAQ,EAAE,QAAQ,CAAC;YAC1D,sFAAsF;YACtF,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC;YACvD,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,CAAC;SAClE,CAAC;IACJ,CAAC;IACD,MAAM,UAAU,GAAG;QACjB,mBAAmB;QACnB,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,IAAI,CAAC;QAC3C,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC;QACtC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC;KACvC,CAAC;IACF,6DAA6D;IAC7D,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;IACnG,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;gBACvE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;YACxD,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,UAAU;QACZ,CAAC;IACH,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,6EAA6E;AAC7E,6FAA6F;AAC7F,IAAI,YAAuC,CAAC,CAAC,6BAA6B;AAE1E;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAgC;IACvE,YAAY,GAAG,KAAK,CAAC;AACvB,CAAC;AAED,8FAA8F;AAC9F,IAAI,kBAA+C,CAAC;AAEpD,6FAA6F;AAC7F,MAAM,UAAU,uBAAuB,CAAC,QAAqC;IAC3E,kBAAkB,GAAG,QAAQ,CAAC;AAChC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAAkB,EAAE,QAAQ,GAAG,OAAO,CAAC,QAAQ;IAC/E,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC;IACvC,MAAM,KAAK,GAAG,UAAU,CAAC,WAAW,EAAE,CAAC;IACvC,OAAO,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAClF,CAAC;AAED,SAAS,mBAAmB;IAC1B,IAAI,YAAY,KAAK,SAAS;QAAE,OAAO,YAAY,CAAC;IACpD,MAAM,QAAQ,GAAG,kBAAkB,IAAI,OAAO,CAAC,QAAQ,CAAC;IAExD,qEAAqE;IACrE,yEAAyE;IACzE,0EAA0E;IAC1E,sEAAsE;IACtE,EAAE;IACF,+EAA+E;IAC/E,6CAA6C;IAC7C,MAAM,KAAK,GAAG,aAAa,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,WAAW,CAAC,EAAE;QACzD,QAAQ,EAAE,OAAO;QACjB,KAAK,EAAE,KAAK;QACZ,SAAS,EAAE,IAAI,GAAG,IAAI;KACvB,CAAC,CAAC;IACH,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvC,YAAY,GAAG,IAAI,CAAC;QACpB,OAAO,YAAY,CAAC;IACtB,CAAC;IAED,sEAAsE;IACtE,+EAA+E;IAC/E,IAAI,QAAQ,KAAK,OAAO,IAAK,KAAK,CAAC,KAA2C,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;QAClG,MAAM,UAAU,GAAG,aAAa,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,WAAW,CAAC,EAAE;YAC9D,QAAQ,EAAE,OAAO;YACjB,KAAK,EAAE,IAAI;YACX,SAAS,EAAE,IAAI,GAAG,IAAI;SACvB,CAAC,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,KAAK,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACjD,YAAY,GAAG,IAAI,CAAC;YACpB,OAAO,YAAY,CAAC;QACtB,CAAC;IACH,CAAC;IAED,wCAAwC;IACxC,KAAK,MAAM,SAAS,IAAI,gBAAgB,EAAE,EAAE,CAAC;QAC3C,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC7B,YAAY,GAAG,SAAS,CAAC;YACzB,OAAO,YAAY,CAAC;QACtB,CAAC;IACH,CAAC;IACD,YAAY,GAAG,IAAI,CAAC;IACpB,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,SAAS,mBAAmB,CAAC,MAAc;IACzC,IAAI,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAClC,MAAM,MAAM,CAAC,MAAM,CACjB,IAAI,KAAK,CACP,6EAA6E;YAC3E,8FAA8F,CACjG,EACD,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAC5B,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,IAAc,EAAE,MAAe;IAC1D,4EAA4E;IAC5E,qDAAqD;IACrD,MAAM,aAAa,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC;IACrC,MAAM,cAAc,GAAG,aAAa,KAAK,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC;IACxE,MAAM,UAAU,GAAG,cAAc,IAAI,mBAAmB,EAAE,CAAC;IAC3D,IAAI,CAAC,UAAU;QAAE,MAAM,IAAI,eAAe,EAAE,CAAC;IAE7C,MAAM,QAAQ,GAAG,kBAAkB,IAAI,OAAO,CAAC,QAAQ,CAAC;IACxD,MAAM,QAAQ,GAAG,iBAAiB,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAEzD,6EAA6E;IAC7E,uFAAuF;IACvF,IAAI,QAAQ,IAAI,cAAc,EAAE,CAAC;QAC/B,mBAAmB,CAAC,cAAc,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE;QACvD,QAAQ,EAAE,OAAO;QACjB,KAAK,EAAE,QAAQ;QACf,SAAS,EAAE,UAAU;KACtB,CAAC,CAAC;IAEH,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,CAAC,cAAc,CAAC,CAAC;QAC5C,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"}
|
|
@@ -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
|
-
'
|
|
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
|
|
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
|
-
'
|
|
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.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.
|
|
97
|
-
.describe('
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.
|
|
106
|
-
.default(
|
|
107
|
-
.describe('true = return XML only (default); false = write to output_path'),
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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', '
|
|
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', '
|
|
176
|
+
log('info', 'provar_testcase_generate: wrote file', { requestId, filePath });
|
|
135
177
|
}
|
|
136
178
|
const warnings = buildStepWarnings(input.steps);
|
|
137
|
-
const
|
|
138
|
-
const
|
|
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(
|
|
159
|
-
structuredContent:
|
|
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 :
|
|
165
|
-
log('error', '
|
|
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
|
-
|
|
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]) =>
|
|
177
|
-
|
|
178
|
-
'
|
|
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
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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, '&')
|
|
211
|
-
.replace(/"/g, '"')
|
|
212
|
-
.replace(/</g, '<')
|
|
213
|
-
.replace(/>/g, '>');
|
|
364
|
+
return value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
214
365
|
}
|
|
215
366
|
function escapeXmlContent(value) {
|
|
216
|
-
return value
|
|
217
|
-
.replace(/&/g, '&')
|
|
218
|
-
.replace(/</g, '<')
|
|
219
|
-
.replace(/>/g, '>');
|
|
367
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
220
368
|
}
|
|
221
369
|
//# sourceMappingURL=testCaseGenerate.js.map
|