@provartesting/provardx-cli 1.5.3 → 1.6.1
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 +5 -37
- package/lib/commands/provar/automation/project/validate.js +5 -3
- package/lib/commands/provar/automation/project/validate.js.map +1 -1
- package/lib/mcp/docs/PROVAR_TEST_STEP_REFERENCE.md +18 -6
- package/lib/mcp/docs/VALIDATION_RULE_REGISTRY.md +225 -0
- package/lib/mcp/prompts/loopPrompts.js +2 -2
- package/lib/mcp/rules/comparisonTypeSets.d.ts +21 -0
- package/lib/mcp/rules/comparisonTypeSets.js +45 -0
- package/lib/mcp/rules/comparisonTypeSets.js.map +1 -0
- package/lib/mcp/rules/provar_best_practices_rules.json +119 -8
- package/lib/mcp/rules/provar_layer1_rules.json +151 -0
- package/lib/mcp/rules/provar_test_step_schema.json +3005 -0
- package/lib/mcp/server.d.ts +15 -0
- package/lib/mcp/server.js +64 -1
- package/lib/mcp/server.js.map +1 -1
- package/lib/mcp/tools/automationTools.js +31 -7
- package/lib/mcp/tools/automationTools.js.map +1 -1
- package/lib/mcp/tools/bestPracticesEngine.js +1069 -8
- package/lib/mcp/tools/bestPracticesEngine.js.map +1 -1
- package/lib/mcp/tools/hierarchyValidate.d.ts +2 -1
- package/lib/mcp/tools/hierarchyValidate.js +7 -1
- package/lib/mcp/tools/hierarchyValidate.js.map +1 -1
- package/lib/mcp/tools/projectValidateFromPath.js +1 -2
- package/lib/mcp/tools/projectValidateFromPath.js.map +1 -1
- package/lib/mcp/tools/qualityHubTools.js +1 -13
- package/lib/mcp/tools/qualityHubTools.js.map +1 -1
- package/lib/mcp/tools/sfSpawn.d.ts +23 -0
- package/lib/mcp/tools/sfSpawn.js +170 -15
- package/lib/mcp/tools/sfSpawn.js.map +1 -1
- package/lib/mcp/tools/spawnErrors.d.ts +13 -0
- package/lib/mcp/tools/spawnErrors.js +51 -0
- package/lib/mcp/tools/spawnErrors.js.map +1 -0
- package/lib/mcp/tools/testCaseGenerate.js +146 -12
- package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
- package/lib/mcp/tools/testCaseValidate.d.ts +46 -0
- package/lib/mcp/tools/testCaseValidate.js +307 -41
- package/lib/mcp/tools/testCaseValidate.js.map +1 -1
- package/lib/mcp/tools/testPlanValidate.js +3 -2
- package/lib/mcp/tools/testPlanValidate.js.map +1 -1
- package/lib/mcp/tools/testSuiteValidate.js +3 -2
- package/lib/mcp/tools/testSuiteValidate.js.map +1 -1
- package/lib/mcp/utils/qualityThreshold.d.ts +8 -0
- package/lib/mcp/utils/qualityThreshold.js +42 -0
- package/lib/mcp/utils/qualityThreshold.js.map +1 -0
- package/lib/mcp/utils/testCaseId.d.ts +23 -0
- package/lib/mcp/utils/testCaseId.js +156 -0
- package/lib/mcp/utils/testCaseId.js.map +1 -0
- package/lib/services/projectValidation.js +2 -1
- package/lib/services/projectValidation.js.map +1 -1
- package/messages/sf.provar.automation.project.validate.md +1 -1
- package/messages/sf.provar.mcp.start.md +1 -0
- package/oclif.manifest.json +4 -4
- package/package.json +3 -2
package/lib/mcp/tools/sfSpawn.js
CHANGED
|
@@ -27,7 +27,50 @@ export class SfNotFoundError extends Error {
|
|
|
27
27
|
this.name = 'SfNotFoundError';
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
/** Remediation command for a missing Provar sf plugin. */
|
|
31
|
+
export const PROVAR_PLUGIN_INSTALL_HINT = 'sf plugins install @provartesting/provardx-cli';
|
|
32
|
+
/**
|
|
33
|
+
* Raised when the `sf` binary is present but the `@provartesting/provardx-cli`
|
|
34
|
+
* plugin is not installed (the `sf` CLI has no `provar` topic). Mirrors
|
|
35
|
+
* SfNotFoundError so callers get a dedicated code + actionable remediation
|
|
36
|
+
* instead of an opaque `AUTOMATION_*_FAILED` carrying "Command provar not found".
|
|
37
|
+
*/
|
|
38
|
+
export class ProvarPluginNotFoundError extends Error {
|
|
39
|
+
code = 'PROVAR_PLUGIN_NOT_FOUND';
|
|
40
|
+
constructor() {
|
|
41
|
+
super('The Provar sf plugin is not installed — the sf CLI has no "provar" topic. ' +
|
|
42
|
+
`Install it with: ${PROVAR_PLUGIN_INSTALL_HINT}`);
|
|
43
|
+
this.name = 'ProvarPluginNotFoundError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Patterns sf emits when the `provar` topic/command is unknown (plugin missing).
|
|
47
|
+
const PROVAR_PLUGIN_MISSING_PATTERNS = [
|
|
48
|
+
/command\s+provar\S*\s+not\s+found/i, // "command provar not found" / "command provar:automation:... not found"
|
|
49
|
+
/\bprovar\S*\s+is not a[n]?\s+(?:sf|@salesforce\/cli)\s+command/i, // oclif "provar is not a sf command"
|
|
50
|
+
/no such (?:command|topic)[^\n]*\bprovar\b/i,
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* Heuristic: does sf stdout/stderr indicate the `provar` topic/plugin is missing?
|
|
54
|
+
* Used to translate a generic non-zero sf exit into PROVAR_PLUGIN_NOT_FOUND.
|
|
55
|
+
*/
|
|
56
|
+
export function isProvarPluginMissing(stdout, stderr) {
|
|
57
|
+
const haystack = `${stderr ?? ''}\n${stdout ?? ''}`;
|
|
58
|
+
return PROVAR_PLUGIN_MISSING_PATTERNS.some((re) => re.test(haystack));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Lightweight probe for the `provar` topic. Returns true when `sf provar --help`
|
|
62
|
+
* succeeds (plugin installed), false when sf is missing or the topic is absent.
|
|
63
|
+
* Non-throwing so it can be used as a best-effort startup health check.
|
|
64
|
+
*/
|
|
65
|
+
export function probeProvarTopic(sfPath) {
|
|
66
|
+
try {
|
|
67
|
+
const result = runSfCommand(['provar', '--help'], sfPath);
|
|
68
|
+
return result.exitCode === 0 && !isProvarPluginMissing(result.stdout, result.stderr);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
31
74
|
// ── SF CLI discovery ──────────────────────────────────────────────────────────
|
|
32
75
|
/**
|
|
33
76
|
* Returns candidate sf CLI paths in common install locations.
|
|
@@ -149,6 +192,22 @@ function assertShellSafePath(sfPath) {
|
|
|
149
192
|
'(& | ; < > ` \' " or line-breaks). Provide an absolute filesystem path to the sf executable.'), { code: 'INVALID_SF_PATH' });
|
|
150
193
|
}
|
|
151
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Quote a single command-line token for cmd.exe when the whole command is run
|
|
197
|
+
* through `shell: true`. Tokens containing whitespace or cmd-significant
|
|
198
|
+
* characters are wrapped in double quotes (embedded `"` doubled per cmd rules);
|
|
199
|
+
* simple tokens are left as-is. This is what keeps spaced paths — e.g.
|
|
200
|
+
* `C:\Program Files\sf\client\bin\sf.cmd` or a `--properties-file` value under a
|
|
201
|
+
* "Provar Manager" directory — from being split by cmd.exe.
|
|
202
|
+
*/
|
|
203
|
+
function quoteWindowsToken(token) {
|
|
204
|
+
if (token === '')
|
|
205
|
+
return '""';
|
|
206
|
+
if (/[\s"&|<>^()]/.test(token)) {
|
|
207
|
+
return `"${token.replace(/"/g, '""')}"`;
|
|
208
|
+
}
|
|
209
|
+
return token;
|
|
210
|
+
}
|
|
152
211
|
/**
|
|
153
212
|
* Run `sf <args>` synchronously and return stdout, stderr, and exit code.
|
|
154
213
|
* Throws SfNotFoundError if the `sf` binary cannot be found.
|
|
@@ -169,23 +228,119 @@ export function runSfCommand(args, sfPath) {
|
|
|
169
228
|
if (useShell && resolvedSfPath) {
|
|
170
229
|
assertShellSafePath(resolvedSfPath);
|
|
171
230
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
231
|
+
// On Windows, `.cmd`/`.bat`/bare-name executables must run through cmd.exe
|
|
232
|
+
// (shell:true). Under shell:true Node concatenates executable + args into one
|
|
233
|
+
// unquoted string, so spaces in the auto-resolved Program Files path, an
|
|
234
|
+
// explicit sf_path, or any argument value would split the command
|
|
235
|
+
// (`'C:\Program' is not recognized ...`). Pre-quote the executable and each
|
|
236
|
+
// argument: Node joins them and wraps the whole line in cmd's outer quotes,
|
|
237
|
+
// and cmd's `/s` rule strips only that outermost pair, preserving our
|
|
238
|
+
// per-token quoting. This applies to auto-resolved paths too — not just
|
|
239
|
+
// user-supplied ones. On non-Windows the args pass through verbatim.
|
|
240
|
+
const spawnExecutable = useShell ? quoteWindowsToken(executable) : executable;
|
|
241
|
+
const spawnArgs = useShell ? args.map(quoteWindowsToken) : args;
|
|
242
|
+
return captureSpawnToFiles(spawnExecutable, spawnArgs, useShell, resolvedSfPath);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Run spawnSync capturing the child's stdout/stderr to temp files instead of an
|
|
246
|
+
* in-memory pipe, then read them back after the child exits.
|
|
247
|
+
*
|
|
248
|
+
* spawnSync's `maxBuffer` buffers all child output in RAM and aborts the whole
|
|
249
|
+
* call with `ENOBUFS` the moment that ceiling is crossed — a verbose Provar run
|
|
250
|
+
* (e.g. testOutputLevel DETAILED) routinely overflowed even a 50 MB cap and lost
|
|
251
|
+
* the run. Streaming straight to file descriptors removes the in-memory ceiling
|
|
252
|
+
* during capture, so `maxBuffer`-induced ENOBUFS is structurally impossible.
|
|
253
|
+
* Because the child writes directly to a file descriptor there is also no pipe to
|
|
254
|
+
* fill, which avoids the OS-level `ENOBUFS` seen on Windows under `shell: true`.
|
|
255
|
+
*
|
|
256
|
+
* (The captured output is still read back into a string after the child exits, so
|
|
257
|
+
* an extreme multi-hundred-MB run can hit Node's max string length — far above the
|
|
258
|
+
* 50 MB pipe boundary this removes. Bounding that read is tracked separately.)
|
|
259
|
+
*
|
|
260
|
+
* The temp directory is removed on a best-effort basis. Throws SfNotFoundError on
|
|
261
|
+
* ENOENT (sf missing) and rethrows any other spawn error.
|
|
262
|
+
*/
|
|
263
|
+
function captureSpawnToFiles(spawnExecutable, spawnArgs, useShell, resolvedSfPath) {
|
|
264
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'provar-sf-'));
|
|
265
|
+
try {
|
|
266
|
+
return spawnCapturingToDir(dir, spawnExecutable, spawnArgs, useShell, resolvedSfPath);
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
// Best-effort cleanup: a temp-dir removal failure (e.g. Windows EBUSY/EPERM
|
|
270
|
+
// from an antivirus/indexer handle, which `force: true` does NOT suppress)
|
|
271
|
+
// must never overwrite the real return value or mask the real spawn error.
|
|
272
|
+
try {
|
|
273
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
/* leave the dir; the OS reclaims its temp directory eventually */
|
|
181
277
|
}
|
|
182
|
-
throw result.error;
|
|
183
278
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Open the capture files in `dir`, run the child with its stdout/stderr inherited
|
|
282
|
+
* onto those descriptors, and read the captured output back. Split out from
|
|
283
|
+
* captureSpawnToFiles so the temp-dir removal in its `finally` also covers the
|
|
284
|
+
* case where opening the second descriptor fails.
|
|
285
|
+
*/
|
|
286
|
+
function spawnCapturingToDir(dir, spawnExecutable, spawnArgs, useShell, resolvedSfPath) {
|
|
287
|
+
const outPath = path.join(dir, 'stdout.log');
|
|
288
|
+
const errPath = path.join(dir, 'stderr.log');
|
|
289
|
+
const outFd = fs.openSync(outPath, 'w');
|
|
290
|
+
let errFd;
|
|
291
|
+
try {
|
|
292
|
+
errFd = fs.openSync(errPath, 'w');
|
|
293
|
+
}
|
|
294
|
+
catch (openErr) {
|
|
295
|
+
// The first descriptor is already open; close it before unwinding so a
|
|
296
|
+
// failure to open the second (e.g. EMFILE) doesn't leak it.
|
|
297
|
+
fs.closeSync(outFd);
|
|
298
|
+
throw openErr;
|
|
299
|
+
}
|
|
300
|
+
let fdsClosed = false;
|
|
301
|
+
const closeFds = () => {
|
|
302
|
+
if (fdsClosed)
|
|
303
|
+
return;
|
|
304
|
+
fdsClosed = true;
|
|
305
|
+
try {
|
|
306
|
+
fs.closeSync(outFd);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
/* already closed */
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
fs.closeSync(errFd);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
/* already closed */
|
|
316
|
+
}
|
|
188
317
|
};
|
|
318
|
+
try {
|
|
319
|
+
// No `encoding`/`maxBuffer`: stdout/stderr are streamed to the inherited file
|
|
320
|
+
// descriptors rather than captured in memory, so there is no buffer to overflow.
|
|
321
|
+
const result = sfSpawnHelper.spawnSync(spawnExecutable, spawnArgs, {
|
|
322
|
+
shell: useShell,
|
|
323
|
+
stdio: ['ignore', outFd, errFd],
|
|
324
|
+
});
|
|
325
|
+
// spawnSync has already reaped the child, so its writes are flushed to disk;
|
|
326
|
+
// close our inherited copies of the descriptors before reading the files back.
|
|
327
|
+
closeFds();
|
|
328
|
+
if (result.error) {
|
|
329
|
+
const err = result.error;
|
|
330
|
+
if (err.code === 'ENOENT') {
|
|
331
|
+
throw new SfNotFoundError(resolvedSfPath);
|
|
332
|
+
}
|
|
333
|
+
throw result.error;
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
stdout: fs.readFileSync(outPath, 'utf-8'),
|
|
337
|
+
stderr: fs.readFileSync(errPath, 'utf-8'),
|
|
338
|
+
exitCode: result.status ?? 1,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
closeFds();
|
|
343
|
+
}
|
|
189
344
|
}
|
|
190
345
|
// ── SOQL safety ───────────────────────────────────────────────────────────────
|
|
191
346
|
/**
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
1
|
+
{"version":3,"file":"sfSpawn.js","sourceRoot":"","sources":["../../../src/mcp/tools/sfSpawn.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared error handler for the sf-CLI-backed tools (automation + Quality Hub).
|
|
3
|
+
* Surfaces a thrown spawn error as an MCP error response, translating a residual
|
|
4
|
+
* ENOBUFS into actionable remediation and otherwise preserving the error's own
|
|
5
|
+
* code (falling back to SF_ERROR).
|
|
6
|
+
*/
|
|
7
|
+
export declare function handleSpawnError(err: unknown, requestId: string, toolName: string): {
|
|
8
|
+
isError: true;
|
|
9
|
+
content: Array<{
|
|
10
|
+
type: 'text';
|
|
11
|
+
text: string;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2024 Provar Limited.
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* Licensed under the BSD 3-Clause license.
|
|
5
|
+
* For full license text, see LICENSE.md file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
6
|
+
*/
|
|
7
|
+
import { makeError } from '../schemas/common.js';
|
|
8
|
+
import { log } from '../logging/logger.js';
|
|
9
|
+
// Defense in depth: runSfCommand streams child output to disk, so a maxBuffer
|
|
10
|
+
// overflow can no longer raise ENOBUFS. Should a residual ENOBUFS still surface
|
|
11
|
+
// (e.g. an exotic OS-level pipe condition), translate the opaque
|
|
12
|
+
// `spawnSync … ENOBUFS` into something the agent can act on rather than retry.
|
|
13
|
+
// Kept tool-agnostic because this handler is shared by every sf-backed tool
|
|
14
|
+
// (automation + Quality Hub); the test-run example is illustrative, not assumed.
|
|
15
|
+
const ENOBUFS_MESSAGE = 'The sf command produced more output than could be captured. Its full output was written to disk ' +
|
|
16
|
+
'(for test runs, under the resultsPath configured in your provardx-properties.json). ' +
|
|
17
|
+
'Re-run the command directly in a terminal with --json, or reduce its output verbosity ' +
|
|
18
|
+
'(for test runs, lower testOutputLevel, e.g. DETAILED → BASIC).';
|
|
19
|
+
const ENOBUFS_SUGGESTION = 'Re-run the sf command directly with --json, or reduce its output verbosity ' +
|
|
20
|
+
'(for test runs, lower testOutputLevel in provardx-properties.json).';
|
|
21
|
+
/**
|
|
22
|
+
* Shared error handler for the sf-CLI-backed tools (automation + Quality Hub).
|
|
23
|
+
* Surfaces a thrown spawn error as an MCP error response, translating a residual
|
|
24
|
+
* ENOBUFS into actionable remediation and otherwise preserving the error's own
|
|
25
|
+
* code (falling back to SF_ERROR).
|
|
26
|
+
*/
|
|
27
|
+
export function handleSpawnError(err, requestId, toolName) {
|
|
28
|
+
const error = err;
|
|
29
|
+
log('error', `${toolName} failed`, { requestId, error: error.message });
|
|
30
|
+
if (error.code === 'ENOBUFS') {
|
|
31
|
+
return {
|
|
32
|
+
isError: true,
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: 'text',
|
|
36
|
+
text: JSON.stringify(makeError('ENOBUFS', ENOBUFS_MESSAGE, requestId, false, { suggestion: ENOBUFS_SUGGESTION })),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
isError: true,
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: 'text',
|
|
46
|
+
text: JSON.stringify(makeError(error.code ?? 'SF_ERROR', error.message, requestId, false)),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=spawnErrors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spawnErrors.js","sourceRoot":"","sources":["../../../src/mcp/tools/spawnErrors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAC;AAE3C,8EAA8E;AAC9E,gFAAgF;AAChF,iEAAiE;AACjE,+EAA+E;AAC/E,4EAA4E;AAC5E,iFAAiF;AACjF,MAAM,eAAe,GACnB,kGAAkG;IAClG,sFAAsF;IACtF,wFAAwF;IACxF,gEAAgE,CAAC;AACnE,MAAM,kBAAkB,GACtB,6EAA6E;IAC7E,qEAAqE,CAAC;AAExE;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAC9B,GAAY,EACZ,SAAiB,EACjB,QAAgB;IAEhB,MAAM,KAAK,GAAG,GAAgC,CAAC;IAC/C,GAAG,CAAC,OAAO,EAAE,GAAG,QAAQ,SAAS,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IACxE,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC7B,OAAO;YACL,OAAO,EAAE,IAAa;YACtB,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAClB,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,UAAU,EAAE,kBAAkB,EAAE,CAAC,CAC5F;iBACF;aACF;SACF,CAAC;IACJ,CAAC;IACD,OAAO;QACL,OAAO,EAAE,IAAa;QACtB,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAe;gBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,IAAI,UAAU,EAAE,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;aAC3F;SACF;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -12,6 +12,7 @@ import { z } from 'zod';
|
|
|
12
12
|
import { assertPathAllowed, PathPolicyError } from '../security/pathPolicy.js';
|
|
13
13
|
import { makeError, makeRequestId } from '../schemas/common.js';
|
|
14
14
|
import { log } from '../logging/logger.js';
|
|
15
|
+
import { allocateTestCaseId, DEFAULT_TESTCASE_ID } from '../utils/testCaseId.js';
|
|
15
16
|
import { validateTestCase } from './testCaseValidate.js';
|
|
16
17
|
import { desc } from './descHelper.js';
|
|
17
18
|
import { UI_ACTION_API_IDS, UI_SCREEN_CONTAINER_API_IDS, UI_LOCATOR_BEARING_API_IDS } from './uiActionApiIds.js';
|
|
@@ -165,7 +166,7 @@ const TOOL_DESCRIPTION = [
|
|
|
165
166
|
// ── Existing description (unchanged below) ───────────────────────────────────
|
|
166
167
|
'Generate a Provar XML test case skeleton with proper UUID v4 guids, sequential testItemId values, and <steps> structure.',
|
|
167
168
|
'Returns XML content. Writes to disk only when dry_run=false.',
|
|
168
|
-
'Generated structure: <?xml version="1.0" encoding="UTF-8" standalone="no"?> with <testCase guid="..." id="
|
|
169
|
+
'Generated structure: <?xml version="1.0" encoding="UTF-8" standalone="no"?> with <testCase guid="..." id="N" registryId="..."> (a numeric integer id), a <summary/> child, then <steps>. The unique identifier is the guid; id is a human-facing label. When writing into an existing project the id is auto-allocated as the highest in-use id + 1; otherwise it defaults to 1.',
|
|
169
170
|
'URI-aware generation: use target_uri to control the XML nesting structure.',
|
|
170
171
|
' - sf:ui:target (or omit target_uri) → flat Salesforce XML structure (existing behaviour).',
|
|
171
172
|
' - ui:pageobject:target?pageId=pageobjects.PageClass → wraps all steps in a UiWithScreen element targeting that non-SF page object.',
|
|
@@ -190,9 +191,9 @@ const TOOL_DESCRIPTION = [
|
|
|
190
191
|
'target argument (UiWithScreen/UiWithRow): pass the URI value; emitted as class="uiTarget" uri="...".',
|
|
191
192
|
'locator argument (UiDoAction/UiAssert/UiRead/UiFill): pass the URI value; emitted as class="uiLocator" uri="...".',
|
|
192
193
|
'valueClass auto-detection: argument values are typed automatically before XML emission. ' +
|
|
193
|
-
'ISO-8601 date "YYYY-MM-DD" → valueClass="date"; ISO-8601 datetime "YYYY-MM-DDTHH:MM:SS" (optional fractional seconds + timezone) → "
|
|
194
|
+
'ISO-8601 date "YYYY-MM-DD" → valueClass="date"; ISO-8601 datetime "YYYY-MM-DDTHH:MM:SS" (optional fractional seconds + timezone) → "dateTime"; ' +
|
|
194
195
|
'"true"/"false" → "boolean"; numeric string (e.g. "42", "-5", "3.14") → "decimal"; otherwise "string". ' +
|
|
195
|
-
'Pass dates
|
|
196
|
+
'Pass dates in ISO-8601 — the generator converts them to the epoch-millisecond value Provar requires (a date/dateTime field emitted as an ISO string, or as valueClass="string", fails to load in the IDE). A bare date is treated as UTC midnight; a datetime without a timezone is treated as UTC. ' +
|
|
196
197
|
'Note: numbers always emit valueClass="decimal" per the canonical Provar reference (there is no separate "integer" valueClass).',
|
|
197
198
|
'Edit page objects: action=Edit targets require a compiled page object for the SF object. ' +
|
|
198
199
|
'If none exists in the project page-objects directory, the locator binding will fail at runtime. ' +
|
|
@@ -303,11 +304,18 @@ export function registerTestCaseGenerate(server, config) {
|
|
|
303
304
|
return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
|
|
304
305
|
}
|
|
305
306
|
try {
|
|
306
|
-
const xmlContent = buildTestCaseXml(input);
|
|
307
307
|
const filePath = input.output_path ? path.resolve(input.output_path) : undefined;
|
|
308
|
-
|
|
308
|
+
// Allocate the root testCase id from surrounding project context, but only
|
|
309
|
+
// when actually persisting (a write path that has cleared the path policy).
|
|
310
|
+
// Preview/dry-run runs have no project anchor, so they keep the default id.
|
|
311
|
+
let idAllocation = { id: DEFAULT_TESTCASE_ID, basis: 'default' };
|
|
309
312
|
if (filePath && !input.dry_run) {
|
|
310
313
|
assertPathAllowed(filePath, config.allowedPaths);
|
|
314
|
+
idAllocation = allocateTestCaseId(filePath, config.allowedPaths);
|
|
315
|
+
}
|
|
316
|
+
const xmlContent = buildTestCaseXml(input, idAllocation.id);
|
|
317
|
+
let written = false;
|
|
318
|
+
if (filePath && !input.dry_run) {
|
|
311
319
|
if (fs.existsSync(filePath) && !input.overwrite) {
|
|
312
320
|
const err = makeError('FILE_EXISTS', `File already exists: ${filePath}. Set overwrite=true to replace.`, requestId);
|
|
313
321
|
return { isError: true, content: [{ type: 'text', text: JSON.stringify(err) }] };
|
|
@@ -315,7 +323,12 @@ export function registerTestCaseGenerate(server, config) {
|
|
|
315
323
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
316
324
|
fs.writeFileSync(filePath, xmlContent, 'utf-8');
|
|
317
325
|
written = true;
|
|
318
|
-
log('info', 'provar_testcase_generate: wrote file', {
|
|
326
|
+
log('info', 'provar_testcase_generate: wrote file', {
|
|
327
|
+
requestId,
|
|
328
|
+
filePath,
|
|
329
|
+
testCaseId: idAllocation.id,
|
|
330
|
+
idBasis: idAllocation.basis,
|
|
331
|
+
});
|
|
319
332
|
}
|
|
320
333
|
const warnings = buildStepWarnings(input.steps);
|
|
321
334
|
const runValidation = input.validate_after_edit !== false;
|
|
@@ -326,6 +339,7 @@ export function registerTestCaseGenerate(server, config) {
|
|
|
326
339
|
written,
|
|
327
340
|
dry_run: input.dry_run,
|
|
328
341
|
step_count: input.steps.length,
|
|
342
|
+
test_case_id: idAllocation.id,
|
|
329
343
|
idempotency_key: input.idempotency_key,
|
|
330
344
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
331
345
|
};
|
|
@@ -450,13 +464,47 @@ function buildArgumentValue(key, val, indent, inNamedValues = false, apiId = '')
|
|
|
450
464
|
if (key === 'locator' && UI_LOCATOR_BEARING_API_IDS.has(apiId)) {
|
|
451
465
|
return `${indent}<value class="uiLocator" uri="${escapeXmlAttr(val)}"/>`;
|
|
452
466
|
}
|
|
467
|
+
// D2: 'interaction' argument → class="uiInteraction" (UiDoAction Action widget).
|
|
468
|
+
// PDX-506: the IDE step editor binds its Action only from a typed uiInteraction;
|
|
469
|
+
// a plain string runs green from the CLI but renders the Action field blank.
|
|
470
|
+
// Gated on the shared UI-action API set so generator + validator stay aligned.
|
|
471
|
+
if (key === 'interaction' && UI_ACTION_API_IDS.has(apiId)) {
|
|
472
|
+
return `${indent}<value class="uiInteraction" uri="${escapeXmlAttr(val)}"/>`;
|
|
473
|
+
}
|
|
453
474
|
}
|
|
454
475
|
// PDX-493 (H3): infer valueClass for date / datetime / boolean / decimal / string. The
|
|
455
476
|
// `fieldTypeHint` parameter on `inferSalesforceValueClass` is intentionally not threaded
|
|
456
477
|
// through here yet — it lands in PDX-492 (H2b) along with the `field_type_hints` tool input.
|
|
457
478
|
const inferred = inferSalesforceValueClass(key, val);
|
|
479
|
+
if (inferred === 'date' || inferred === 'datetime') {
|
|
480
|
+
// PDX-509: Provar stores date/dateTime as epoch MILLISECONDS, never an ISO string —
|
|
481
|
+
// an ISO string fails to load in the IDE (RENDER-DATE-VALUECLASS-001), and the real
|
|
482
|
+
// corpus uses epoch ms exclusively with camelCase `dateTime`. Convert here.
|
|
483
|
+
const ms = isoToEpochMs(val, inferred);
|
|
484
|
+
if (ms !== null) {
|
|
485
|
+
const valueClass = inferred === 'datetime' ? 'dateTime' : 'date';
|
|
486
|
+
return `${indent}<value class="value" valueClass="${valueClass}">${ms}</value>`;
|
|
487
|
+
}
|
|
488
|
+
// A value that matches the ISO shape but is not a real date (e.g. "2026-99-99")
|
|
489
|
+
// cannot be converted — fall back to a plain string rather than emit a
|
|
490
|
+
// load-breaking date/dateTime with a non-epoch value.
|
|
491
|
+
return `${indent}<value class="value" valueClass="string">${escapeXmlContent(val)}</value>`;
|
|
492
|
+
}
|
|
458
493
|
return `${indent}<value class="value" valueClass="${inferred}">${escapeXmlContent(val)}</value>`;
|
|
459
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Convert a validated ISO-8601 date / datetime string to epoch milliseconds.
|
|
497
|
+
* A date-only string ("YYYY-MM-DD") is parsed as UTC midnight per the ES spec. A
|
|
498
|
+
* datetime WITHOUT an explicit timezone would otherwise parse as machine-local
|
|
499
|
+
* time (non-deterministic), so we pin it to UTC by appending 'Z' — matching the
|
|
500
|
+
* Provar corpus, where date/dateTime values are stored as UTC epoch ms.
|
|
501
|
+
* Returns null if the value cannot be parsed (caller falls back to the raw text).
|
|
502
|
+
*/
|
|
503
|
+
function isoToEpochMs(val, kind) {
|
|
504
|
+
const needsUtc = kind === 'datetime' && !/(Z|[+-]\d{2}:?\d{2})$/.test(val);
|
|
505
|
+
const ms = Date.parse(needsUtc ? `${val}Z` : val);
|
|
506
|
+
return Number.isNaN(ms) ? null : ms;
|
|
507
|
+
}
|
|
460
508
|
function buildArgumentsXml(attributes, baseIndent = ' ', apiId = '') {
|
|
461
509
|
const entries = Object.entries(attributes);
|
|
462
510
|
if (entries.length === 0)
|
|
@@ -493,6 +541,82 @@ function buildSetValuesXml(attributes, baseIndent) {
|
|
|
493
541
|
`${i(0)}</arguments>\n` +
|
|
494
542
|
`${baseIndent.slice(0, -2)}`);
|
|
495
543
|
}
|
|
544
|
+
// PDX-507: derive the field name for a uiFieldAssertion from a locator URI's
|
|
545
|
+
// `name=` parameter (e.g. ui:locator?name=LastName&binding=… → "LastName").
|
|
546
|
+
function extractFieldName(uri) {
|
|
547
|
+
const m = /[?&]name=([^&]*)/.exec(uri);
|
|
548
|
+
if (!m)
|
|
549
|
+
return 'Field';
|
|
550
|
+
try {
|
|
551
|
+
return decodeURIComponent(m[1]) || 'Field';
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return m[1] || 'Field';
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// PDX-507: UiAssert — assemble the nested fieldAssertions / uiFieldAssertion
|
|
558
|
+
// structure the Provar IDE Result Assertions tab binds from. The generator
|
|
559
|
+
// previously emitted the flat shape (top-level fieldLocator / attributeName /
|
|
560
|
+
// comparisonType / expectedValue arguments), which runs green from the CLI but
|
|
561
|
+
// renders the Result Assertions tab blank in the IDE. Shape confirmed against
|
|
562
|
+
// the real test corpus (AllPOCProjects): 3,743/3,778 UiAssert steps use this
|
|
563
|
+
// nested form, with a BARE <fieldLocator uri="…"/> element (never class="uiLocator"
|
|
564
|
+
// — see best-practice rule UI-ASSERT-FIELDLOCATOR-002) and no `assertionType`.
|
|
565
|
+
// Only a `fieldLocator` attribute triggers the nested form; a UiAssert that
|
|
566
|
+
// carries a plain `locator` argument keeps the documented locator→uiLocator
|
|
567
|
+
// contract via the flat fallback (buildArgumentValue dispatches it). When no
|
|
568
|
+
// fieldLocator is supplied the step is not a field assertion, so fall back to
|
|
569
|
+
// flat argument emission and leave other UiAssert shapes untouched.
|
|
570
|
+
function buildUiAssertXml(attributes, baseIndent, apiId) {
|
|
571
|
+
const a = attributes;
|
|
572
|
+
const fieldLocator = a['fieldLocator'] ?? '';
|
|
573
|
+
if (!fieldLocator)
|
|
574
|
+
return buildArgumentsXml(attributes, baseIndent, apiId);
|
|
575
|
+
const i = (n) => baseIndent + ' '.repeat(n);
|
|
576
|
+
const resultName = a['resultName'] ?? 'Values';
|
|
577
|
+
const resultScope = a['resultScope'] ?? 'Test';
|
|
578
|
+
const captureAfter = a['captureAfter'] ?? 'false';
|
|
579
|
+
const attributeName = a['attributeName'] ?? 'value';
|
|
580
|
+
const comparisonType = a['comparisonType'] ?? 'EqualTo';
|
|
581
|
+
const fieldName = extractFieldName(fieldLocator);
|
|
582
|
+
const expected = a['expectedValue'];
|
|
583
|
+
// resultName / resultScope / captureAfter are control-plane literals that the
|
|
584
|
+
// real corpus ALWAYS emits as valueClass="string" — including captureAfter
|
|
585
|
+
// ("true"/"false"), which is intentionally NOT valueClass="boolean" here.
|
|
586
|
+
// Routing these through inferSalesforceValueClass would diverge from the
|
|
587
|
+
// corpus (it would infer boolean), so they are emitted as string literals.
|
|
588
|
+
const strVal = (v) => `<value class="value" valueClass="string">${escapeXmlContent(v)}</value>`;
|
|
589
|
+
// uiAttributeAssertion — self-closing when there is no expected value. This is
|
|
590
|
+
// a real corpus form (e.g. ADP_POV "UI Assert - Date Empty" emits
|
|
591
|
+
// <uiAttributeAssertion attributeName="value" comparisonType="EqualTo"/>);
|
|
592
|
+
// otherwise it carries a typed <value> child built via buildArgumentValue so
|
|
593
|
+
// {Var} expands to class="variable".
|
|
594
|
+
const attrOpen = `${i(5)}<uiAttributeAssertion attributeName="${escapeXmlAttr(attributeName)}" comparisonType="${escapeXmlAttr(comparisonType)}"`;
|
|
595
|
+
const attrAssertion = expected != null && expected !== ''
|
|
596
|
+
? `${attrOpen}>\n${buildArgumentValue('expectedValue', expected, i(6), true)}\n${i(5)}</uiAttributeAssertion>`
|
|
597
|
+
: `${attrOpen}/>`;
|
|
598
|
+
const fieldAssertion = `${i(4)}<uiFieldAssertion resultName="${escapeXmlAttr(fieldName)}">\n` +
|
|
599
|
+
`${i(5)}<fieldLocator uri="${escapeXmlAttr(fieldLocator)}"/>\n` +
|
|
600
|
+
`${i(5)}<attributeAssertions>\n` +
|
|
601
|
+
`${attrAssertion}\n` +
|
|
602
|
+
`${i(5)}</attributeAssertions>\n` +
|
|
603
|
+
`${i(4)}</uiFieldAssertion>`;
|
|
604
|
+
return (`\n${i(0)}<arguments>\n` +
|
|
605
|
+
`${i(0)}<argument id="resultName">\n${i(1)}${strVal(resultName)}\n${i(0)}</argument>\n` +
|
|
606
|
+
`${i(0)}<argument id="resultScope">\n${i(1)}${strVal(resultScope)}\n${i(0)}</argument>\n` +
|
|
607
|
+
`${i(0)}<argument id="fieldAssertions">\n` +
|
|
608
|
+
`${i(1)}<value class="valueList" mutable="Mutable">\n` +
|
|
609
|
+
`${fieldAssertion}\n` +
|
|
610
|
+
`${i(1)}</value>\n` +
|
|
611
|
+
`${i(0)}</argument>\n` +
|
|
612
|
+
`${i(0)}<argument id="captureAfter">\n${i(1)}${strVal(captureAfter)}\n${i(0)}</argument>\n` +
|
|
613
|
+
`${i(0)}<argument id="columnAssertions">\n${i(1)}<value class="valueList" mutable="Mutable"/>\n${i(0)}</argument>\n` +
|
|
614
|
+
`${i(0)}<argument id="pageAssertions">\n${i(1)}<value class="valueList" mutable="Mutable"/>\n${i(0)}</argument>\n` +
|
|
615
|
+
`${i(0)}<argument id="beforeWait"/>\n` +
|
|
616
|
+
`${i(0)}<argument id="autoRetry"/>\n` +
|
|
617
|
+
`${i(0)}</arguments>\n` +
|
|
618
|
+
`${baseIndent.slice(0, -2)}`);
|
|
619
|
+
}
|
|
496
620
|
function buildFlatStepXml(step, testItemId, indent) {
|
|
497
621
|
return buildStepXmlWithChildren(step, testItemId, indent, '', undefined);
|
|
498
622
|
}
|
|
@@ -510,9 +634,17 @@ function buildStepXmlWithChildren(step, testItemId, indent, childrenXml, substep
|
|
|
510
634
|
const resolvedApiId = resolveApiId(step.api_id);
|
|
511
635
|
const baseIndent = indent + ' ';
|
|
512
636
|
// Use SetValues structure for any SetValues API (string-match mirrors the validator).
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
637
|
+
// PDX-507: UiAssert uses the nested fieldAssertions/uiFieldAssertion structure.
|
|
638
|
+
let argumentsXml;
|
|
639
|
+
if (resolvedApiId.includes('SetValues')) {
|
|
640
|
+
argumentsXml = buildSetValuesXml(step.attributes, baseIndent);
|
|
641
|
+
}
|
|
642
|
+
else if (resolvedApiId.endsWith('.UiAssert') || resolvedApiId === 'UiAssert') {
|
|
643
|
+
argumentsXml = buildUiAssertXml(step.attributes, baseIndent, resolvedApiId);
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
argumentsXml = buildArgumentsXml(step.attributes, baseIndent, resolvedApiId);
|
|
647
|
+
}
|
|
516
648
|
const hasClauses = substepsTestItemId !== undefined;
|
|
517
649
|
const open = `${indent}<apiCall guid="${guid}" apiId="${escapeXmlAttr(resolvedApiId)}" name="${escapeXmlAttr(step.name)}" testItemId="${testItemId}">`;
|
|
518
650
|
const close = `${indent}</apiCall>`;
|
|
@@ -614,7 +746,7 @@ function emitGroupedSteps(nodes, indent, startId) {
|
|
|
614
746
|
}
|
|
615
747
|
return { xml: lines.join('\n'), nextId: id };
|
|
616
748
|
}
|
|
617
|
-
function buildTestCaseXml(input) {
|
|
749
|
+
function buildTestCaseXml(input, testCaseId = DEFAULT_TESTCASE_ID) {
|
|
618
750
|
const testCaseGuid = randomUUID();
|
|
619
751
|
const registryId = randomUUID();
|
|
620
752
|
const groupingMode = input.grouping_mode ?? 'auto';
|
|
@@ -660,9 +792,11 @@ function buildTestCaseXml(input) {
|
|
|
660
792
|
const lines = input.steps.map((step, i) => buildFlatStepXml(step, i + 1, ' ')).join('\n');
|
|
661
793
|
stepLines = lines || ' <!-- TODO: Add test steps here -->';
|
|
662
794
|
}
|
|
663
|
-
// Provar requires: standalone="no",
|
|
795
|
+
// Provar requires: standalone="no", a numeric integer id, no name attr, <summary/> before <steps>.
|
|
796
|
+
// The id is a human-facing label, not a uniqueness key (guid is) — see allocateTestCaseId:
|
|
797
|
+
// when writing into an existing project we pass highest-in-use + 1; otherwise it defaults to 1.
|
|
664
798
|
return ('<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' +
|
|
665
|
-
`<testCase guid="${testCaseGuid}" id="
|
|
799
|
+
`<testCase guid="${testCaseGuid}" id="${testCaseId}" registryId="${registryId}">\n` +
|
|
666
800
|
' <summary/>\n' +
|
|
667
801
|
' <steps>\n' +
|
|
668
802
|
stepLines +
|