@provartesting/provardx-cli 1.5.0-beta → 1.5.0-beta.10
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 +137 -13
- 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/mcp/docs/PROVAR_TEST_STEP_REFERENCE.md +1430 -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 +0 -72
- 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/index.d.ts +2 -0
- package/lib/mcp/prompts/index.js +19 -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 +30 -2
- package/lib/mcp/security/pathPolicy.js.map +1 -1
- package/lib/mcp/server.js +51 -4
- package/lib/mcp/server.js.map +1 -1
- package/lib/mcp/tools/antTools.d.ts +15 -0
- package/lib/mcp/tools/antTools.js +216 -50
- package/lib/mcp/tools/antTools.js.map +1 -1
- package/lib/mcp/tools/automationTools.d.ts +39 -5
- package/lib/mcp/tools/automationTools.js +341 -55
- 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 +168 -0
- package/lib/mcp/tools/connectionTools.js.map +1 -0
- package/lib/mcp/tools/nitroXTools.d.ts +22 -0
- package/lib/mcp/tools/nitroXTools.js +750 -0
- package/lib/mcp/tools/nitroXTools.js.map +1 -0
- package/lib/mcp/tools/pageObjectGenerate.js +103 -35
- package/lib/mcp/tools/pageObjectGenerate.js.map +1 -1
- package/lib/mcp/tools/propertiesTools.d.ts +2 -0
- package/lib/mcp/tools/propertiesTools.js +277 -39
- package/lib/mcp/tools/propertiesTools.js.map +1 -1
- package/lib/mcp/tools/qualityHubApiTools.d.ts +3 -0
- package/lib/mcp/tools/qualityHubApiTools.js +134 -0
- package/lib/mcp/tools/qualityHubApiTools.js.map +1 -0
- package/lib/mcp/tools/qualityHubTools.js +139 -20
- package/lib/mcp/tools/qualityHubTools.js.map +1 -1
- package/lib/mcp/tools/rcaTools.d.ts +3 -2
- package/lib/mcp/tools/rcaTools.js +145 -20
- package/lib/mcp/tools/rcaTools.js.map +1 -1
- package/lib/mcp/tools/sfSpawn.d.ts +1 -1
- package/lib/mcp/tools/sfSpawn.js +8 -2
- package/lib/mcp/tools/sfSpawn.js.map +1 -1
- package/lib/mcp/tools/testCaseGenerate.js +88 -59
- package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
- package/lib/mcp/tools/testCaseStepTools.d.ts +4 -0
- package/lib/mcp/tools/testCaseStepTools.js +221 -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 +146 -19
- package/lib/mcp/tools/testCaseValidate.js.map +1 -1
- 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/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/oclif.manifest.json +241 -28
- package/package.json +8 -5
|
@@ -10,5 +10,10 @@ export declare class PathPolicyError extends Error {
|
|
|
10
10
|
* - PATH_NOT_ALLOWED — resolved path is outside all allowed roots
|
|
11
11
|
*
|
|
12
12
|
* When allowedPaths is empty, all paths are permitted (unrestricted mode).
|
|
13
|
+
*
|
|
14
|
+
* Symlinks are resolved via fs.realpathSync so that a symlink inside an allowed
|
|
15
|
+
* directory pointing to a location outside it cannot bypass containment.
|
|
16
|
+
* If the path does not yet exist (e.g. an output file to be created), the parent
|
|
17
|
+
* directory is resolved instead and the basename re-attached.
|
|
13
18
|
*/
|
|
14
19
|
export declare function assertPathAllowed(filePath: string, allowedPaths: string[]): void;
|
|
@@ -4,6 +4,7 @@
|
|
|
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';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
export class PathPolicyError extends Error {
|
|
9
10
|
code;
|
|
@@ -21,6 +22,11 @@ export class PathPolicyError extends Error {
|
|
|
21
22
|
* - PATH_NOT_ALLOWED — resolved path is outside all allowed roots
|
|
22
23
|
*
|
|
23
24
|
* When allowedPaths is empty, all paths are permitted (unrestricted mode).
|
|
25
|
+
*
|
|
26
|
+
* Symlinks are resolved via fs.realpathSync so that a symlink inside an allowed
|
|
27
|
+
* directory pointing to a location outside it cannot bypass containment.
|
|
28
|
+
* If the path does not yet exist (e.g. an output file to be created), the parent
|
|
29
|
+
* directory is resolved instead and the basename re-attached.
|
|
24
30
|
*/
|
|
25
31
|
export function assertPathAllowed(filePath, allowedPaths) {
|
|
26
32
|
// Check the original path for `..` segments before any normalization resolves them away
|
|
@@ -28,8 +34,30 @@ export function assertPathAllowed(filePath, allowedPaths) {
|
|
|
28
34
|
if (rawSegments.some((s) => s === '..')) {
|
|
29
35
|
throw new PathPolicyError('PATH_TRAVERSAL', `Path traversal detected: ${filePath}`);
|
|
30
36
|
}
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
// Resolve symlinks so a symlink inside an allowed dir that points outside cannot bypass
|
|
38
|
+
// the containment check. Fall back to lexical resolution when the path doesn't exist yet.
|
|
39
|
+
let resolved;
|
|
40
|
+
try {
|
|
41
|
+
resolved = fs.realpathSync(filePath);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Path doesn't exist — resolve the parent (which should exist) to catch symlinks there
|
|
45
|
+
const parent = path.dirname(path.resolve(filePath));
|
|
46
|
+
try {
|
|
47
|
+
resolved = path.join(fs.realpathSync(parent), path.basename(filePath));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
resolved = path.resolve(filePath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const resolvedAllowed = allowedPaths.map((p) => {
|
|
54
|
+
try {
|
|
55
|
+
return fs.realpathSync(p);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return path.resolve(path.normalize(p));
|
|
59
|
+
}
|
|
60
|
+
});
|
|
33
61
|
if (resolvedAllowed.length > 0 &&
|
|
34
62
|
!resolvedAllowed.some((base) => resolved === base || resolved.startsWith(base + path.sep))) {
|
|
35
63
|
throw new PathPolicyError('PATH_NOT_ALLOWED', `Path "${resolved}" is not within allowed paths: [${resolvedAllowed.join(', ')}]`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pathPolicy.js","sourceRoot":"","sources":["../../../src/mcp/security/pathPolicy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACxB,IAAI,CAAS;IAC7B,YAAmB,IAAY,EAAE,OAAe;QAC9C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;QAC9B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED
|
|
1
|
+
{"version":3,"file":"pathPolicy.js","sourceRoot":"","sources":["../../../src/mcp/security/pathPolicy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACxB,IAAI,CAAS;IAC7B,YAAmB,IAAY,EAAE,OAAe;QAC9C,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;QAC9B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,YAAsB;IACxE,wFAAwF;IACxF,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzE,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,eAAe,CAAC,gBAAgB,EAAE,4BAA4B,QAAQ,EAAE,CAAC,CAAC;IACtF,CAAC;IAED,wFAAwF;IACxF,0FAA0F;IAC1F,IAAI,QAAgB,CAAC;IACrB,IAAI,CAAC;QACH,QAAQ,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,uFAAuF;QACvF,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;QACpD,IAAI,CAAC;YACH,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;QACzE,CAAC;QAAC,MAAM,CAAC;YACP,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED,MAAM,eAAe,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC7C,IAAI,CAAC;YACH,OAAO,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC5B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QACzC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,IACE,eAAe,CAAC,MAAM,GAAG,CAAC;QAC1B,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAC1F,CAAC;QACD,MAAM,IAAI,eAAe,CACvB,kBAAkB,EAClB,SAAS,QAAQ,mCAAmC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAClF,CAAC;IACJ,CAAC;AACH,CAAC"}
|
package/lib/mcp/server.js
CHANGED
|
@@ -4,9 +4,15 @@
|
|
|
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 { createRequire } from 'node:module';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
7
11
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
12
|
import { z } from 'zod';
|
|
9
13
|
import { log } from './logging/logger.js';
|
|
14
|
+
const requireJson = createRequire(import.meta.url);
|
|
15
|
+
const SERVER_VERSION = requireJson('../../package.json').version;
|
|
10
16
|
import { registerProjectInspect } from './tools/projectInspect.js';
|
|
11
17
|
import { registerPageObjectGenerate } from './tools/pageObjectGenerate.js';
|
|
12
18
|
import { registerPageObjectValidate } from './tools/pageObjectValidate.js';
|
|
@@ -17,22 +23,27 @@ import { registerTestPlanValidate } from './tools/testPlanValidate.js';
|
|
|
17
23
|
import { registerProjectValidateFromPath } from './tools/projectValidateFromPath.js';
|
|
18
24
|
import { registerAllPropertiesTools } from './tools/propertiesTools.js';
|
|
19
25
|
import { registerAllQualityHubTools } from './tools/qualityHubTools.js';
|
|
26
|
+
import { registerAllQualityHubApiTools } from './tools/qualityHubApiTools.js';
|
|
20
27
|
import { registerAllAutomationTools } from './tools/automationTools.js';
|
|
21
28
|
import { registerAllDefectTools } from './tools/defectTools.js';
|
|
22
29
|
import { registerAllAntTools } from './tools/antTools.js';
|
|
23
30
|
import { registerAllRcaTools } from './tools/rcaTools.js';
|
|
24
31
|
import { registerAllTestPlanTools } from './tools/testPlanTools.js';
|
|
32
|
+
import { registerAllNitroXTools } from './tools/nitroXTools.js';
|
|
33
|
+
import { registerAllTestCaseStepTools } from './tools/testCaseStepTools.js';
|
|
34
|
+
import { registerAllConnectionTools } from './tools/connectionTools.js';
|
|
35
|
+
import { registerAllPrompts } from './prompts/index.js';
|
|
25
36
|
export function createProvarMcpServer(config) {
|
|
26
37
|
log('info', 'Creating Provar MCP server', { allowedPaths: config.allowedPaths });
|
|
27
38
|
const server = new McpServer({
|
|
28
39
|
name: 'provar-mcp',
|
|
29
|
-
version:
|
|
40
|
+
version: SERVER_VERSION,
|
|
30
41
|
});
|
|
31
42
|
// ── Sanity-check tool ────────────────────────────────────────────────────────
|
|
32
43
|
server.tool('provardx.ping', 'Sanity-check tool. Echoes back a message with a timestamp. Use this to verify the MCP server is reachable before calling other tools.', {
|
|
33
44
|
message: z.string().optional().default('ping').describe('Optional message to echo back'),
|
|
34
45
|
}, ({ message }) => {
|
|
35
|
-
const result = { pong: message, ts: new Date().toISOString(), server:
|
|
46
|
+
const result = { pong: message, ts: new Date().toISOString(), server: `provar-mcp@${SERVER_VERSION}` };
|
|
36
47
|
return {
|
|
37
48
|
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
38
49
|
structuredContent: result,
|
|
@@ -49,11 +60,47 @@ export function createProvarMcpServer(config) {
|
|
|
49
60
|
registerProjectValidateFromPath(server, config);
|
|
50
61
|
registerAllPropertiesTools(server, config);
|
|
51
62
|
registerAllQualityHubTools(server);
|
|
52
|
-
|
|
63
|
+
registerAllQualityHubApiTools(server);
|
|
64
|
+
registerAllAutomationTools(server, config);
|
|
53
65
|
registerAllDefectTools(server);
|
|
54
66
|
registerAllAntTools(server, config);
|
|
55
|
-
registerAllRcaTools(server);
|
|
67
|
+
registerAllRcaTools(server, config);
|
|
56
68
|
registerAllTestPlanTools(server, config);
|
|
69
|
+
registerAllNitroXTools(server, config);
|
|
70
|
+
registerAllTestCaseStepTools(server, config);
|
|
71
|
+
registerAllConnectionTools(server, config);
|
|
72
|
+
// ── Provar prompts ───────────────────────────────────────────────────────────
|
|
73
|
+
registerAllPrompts(server);
|
|
74
|
+
// ── Documentation resources ──────────────────────────────────────────────────
|
|
75
|
+
const docsDir = join(dirname(fileURLToPath(import.meta.url)), 'docs');
|
|
76
|
+
server.resource('provar-step-reference', 'provar://docs/step-reference', {
|
|
77
|
+
description: 'Canonical reference for all Provar XML test step API IDs, argument formats, validation rules, and corpus-verified examples. Use this to understand correct step structure when generating or reviewing test cases.',
|
|
78
|
+
mimeType: 'text/markdown',
|
|
79
|
+
}, () => {
|
|
80
|
+
try {
|
|
81
|
+
const content = readFileSync(join(docsDir, 'PROVAR_TEST_STEP_REFERENCE.md'), 'utf-8');
|
|
82
|
+
return {
|
|
83
|
+
contents: [
|
|
84
|
+
{
|
|
85
|
+
uri: 'provar://docs/step-reference',
|
|
86
|
+
mimeType: 'text/markdown',
|
|
87
|
+
text: content,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return {
|
|
94
|
+
contents: [
|
|
95
|
+
{
|
|
96
|
+
uri: 'provar://docs/step-reference',
|
|
97
|
+
mimeType: 'text/markdown',
|
|
98
|
+
text: '# Provar Test Step Reference\n\nReference doc not found. If you are developing from source, rebuild the package. Otherwise, reinstall or upgrade the plugin/package and try again.',
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
});
|
|
57
104
|
return server;
|
|
58
105
|
}
|
|
59
106
|
//# sourceMappingURL=server.js.map
|
package/lib/mcp/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,GAAG,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -18,4 +18,19 @@ export interface AntValidationResult {
|
|
|
18
18
|
}
|
|
19
19
|
/** Pure function — exported for unit testing */
|
|
20
20
|
export declare function validateAntXml(xmlContent: string): AntValidationResult;
|
|
21
|
+
export interface JUnitStepResult {
|
|
22
|
+
testItemId: string;
|
|
23
|
+
title: string;
|
|
24
|
+
status: 'pass' | 'fail' | 'skip';
|
|
25
|
+
errorMessage?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface JUnitParseResult {
|
|
28
|
+
steps: JUnitStepResult[];
|
|
29
|
+
warning?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Scan a Provar results directory for JUnit XML files and return structured step results.
|
|
33
|
+
* Returns an empty steps array (+ optional warning) when no XML is found or parsing fails.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseJUnitResults(resultsDir: string): JUnitParseResult;
|
|
21
36
|
export declare function registerAllAntTools(server: McpServer, config: ServerConfig): void;
|
|
@@ -25,9 +25,7 @@ const FilesetSchema = z.object({
|
|
|
25
25
|
.describe('Specific .testcase or .testplan file names to include (e.g. ["Login.testcase"]). Omit to run everything in dir.'),
|
|
26
26
|
});
|
|
27
27
|
const PlanFeatureSchema = z.object({
|
|
28
|
-
name: z
|
|
29
|
-
.enum(['PDF', 'PIECHART', 'EMAIL', 'JUNIT'])
|
|
30
|
-
.describe('Feature name (PDF, PIECHART, EMAIL, JUNIT)'),
|
|
28
|
+
name: z.enum(['PDF', 'PIECHART', 'EMAIL', 'JUNIT']).describe('Feature name (PDF, PIECHART, EMAIL, JUNIT)'),
|
|
31
29
|
type: z.enum(['OUTPUT', 'NOTIFICATION']).describe('Feature type'),
|
|
32
30
|
enabled: z.boolean().describe('Whether this feature is enabled'),
|
|
33
31
|
});
|
|
@@ -103,14 +101,8 @@ export function registerAntGenerate(server, config) {
|
|
|
103
101
|
.string()
|
|
104
102
|
.default('Full Screen')
|
|
105
103
|
.describe('Browser window configuration (e.g. "Full Screen").'),
|
|
106
|
-
web_browser_provider_name: z
|
|
107
|
-
|
|
108
|
-
.default('Desktop')
|
|
109
|
-
.describe('Browser provider name (e.g. "Desktop").'),
|
|
110
|
-
web_browser_device_name: z
|
|
111
|
-
.string()
|
|
112
|
-
.default('Full Screen')
|
|
113
|
-
.describe('Browser device name (e.g. "Full Screen").'),
|
|
104
|
+
web_browser_provider_name: z.string().default('Desktop').describe('Browser provider name (e.g. "Desktop").'),
|
|
105
|
+
web_browser_device_name: z.string().default('Full Screen').describe('Browser device name (e.g. "Full Screen").'),
|
|
114
106
|
test_environment: z
|
|
115
107
|
.string()
|
|
116
108
|
.default('')
|
|
@@ -146,24 +138,18 @@ export function registerAntGenerate(server, config) {
|
|
|
146
138
|
.boolean()
|
|
147
139
|
.optional()
|
|
148
140
|
.describe('When true, the ANT build does not fail even if tests fail. Useful for CI pipelines that collect results separately.'),
|
|
149
|
-
invoke_test_run_monitor: z
|
|
150
|
-
.boolean()
|
|
151
|
-
.default(true)
|
|
152
|
-
.describe('Enable the Provar test run monitor.'),
|
|
141
|
+
invoke_test_run_monitor: z.boolean().default(true).describe('Enable the Provar test run monitor.'),
|
|
153
142
|
// ── Secrets / security ──────────────────────────────────────────────────
|
|
154
143
|
secrets_password: z
|
|
155
144
|
.string()
|
|
156
145
|
.default('${env.ProvarSecretsPassword}')
|
|
157
|
-
.describe('
|
|
146
|
+
.describe('Encryption key used to decrypt the Provar .secrets file (the password string itself, not a file path). Defaults to reading from the ProvarSecretsPassword environment variable.'),
|
|
158
147
|
test_environment_secrets_password: z
|
|
159
148
|
.string()
|
|
160
149
|
.optional()
|
|
161
150
|
.describe('Per-environment secrets password. Defaults to reading from the ProvarSecretsPassword_EnvName environment variable.'),
|
|
162
151
|
// ── Test Cycle ──────────────────────────────────────────────────────────
|
|
163
|
-
test_cycle_path: z
|
|
164
|
-
.string()
|
|
165
|
-
.optional()
|
|
166
|
-
.describe('Path to a TestCycle folder (used with test cycle reporting).'),
|
|
152
|
+
test_cycle_path: z.string().optional().describe('Path to a TestCycle folder (used with test cycle reporting).'),
|
|
167
153
|
test_cycle_run_type: z
|
|
168
154
|
.enum(['ALL', 'FAILED', 'NEW'])
|
|
169
155
|
.optional()
|
|
@@ -182,14 +168,8 @@ export function registerAntGenerate(server, config) {
|
|
|
182
168
|
.string()
|
|
183
169
|
.optional()
|
|
184
170
|
.describe('Where to write the build.xml file (returned in response). Required when dry_run=false.'),
|
|
185
|
-
overwrite: z
|
|
186
|
-
|
|
187
|
-
.default(false)
|
|
188
|
-
.describe('Overwrite output_path if the file already exists.'),
|
|
189
|
-
dry_run: z
|
|
190
|
-
.boolean()
|
|
191
|
-
.default(true)
|
|
192
|
-
.describe('true = return XML only (default); false = write to output_path.'),
|
|
171
|
+
overwrite: z.boolean().default(false).describe('Overwrite output_path if the file already exists.'),
|
|
172
|
+
dry_run: z.boolean().default(true).describe('true = return XML only (default); false = write to output_path.'),
|
|
193
173
|
}, (input) => {
|
|
194
174
|
const requestId = makeRequestId();
|
|
195
175
|
log('info', 'provar.ant.generate', {
|
|
@@ -198,6 +178,17 @@ export function registerAntGenerate(server, config) {
|
|
|
198
178
|
dry_run: input.dry_run,
|
|
199
179
|
});
|
|
200
180
|
try {
|
|
181
|
+
// Validate all path inputs before writing anything — these get embedded in the
|
|
182
|
+
// generated ANT build.xml and would be accessed by ANT at execution time.
|
|
183
|
+
assertPathAllowed(input.provar_home, config.allowedPaths);
|
|
184
|
+
assertPathAllowed(input.project_path, config.allowedPaths);
|
|
185
|
+
assertPathAllowed(input.results_path, config.allowedPaths);
|
|
186
|
+
if (input.license_path)
|
|
187
|
+
assertPathAllowed(input.license_path, config.allowedPaths);
|
|
188
|
+
if (input.smtp_path)
|
|
189
|
+
assertPathAllowed(input.smtp_path, config.allowedPaths);
|
|
190
|
+
if (input.project_cache_path)
|
|
191
|
+
assertPathAllowed(input.project_cache_path, config.allowedPaths);
|
|
201
192
|
const xmlContent = buildAntXml(input);
|
|
202
193
|
const filePath = input.output_path ? path.resolve(input.output_path) : undefined;
|
|
203
194
|
let written = false;
|
|
@@ -226,7 +217,7 @@ export function registerAntGenerate(server, config) {
|
|
|
226
217
|
}
|
|
227
218
|
catch (err) {
|
|
228
219
|
const error = err;
|
|
229
|
-
const errResult = makeError(error instanceof PathPolicyError ? error.code :
|
|
220
|
+
const errResult = makeError(error instanceof PathPolicyError ? error.code : error.code ?? 'GENERATE_ERROR', error.message, requestId, false);
|
|
230
221
|
log('error', 'provar.ant.generate failed', { requestId, error: error.message });
|
|
231
222
|
return { isError: true, content: [{ type: 'text', text: JSON.stringify(errResult) }] };
|
|
232
223
|
}
|
|
@@ -240,14 +231,8 @@ export function registerAntValidate(server, config) {
|
|
|
240
231
|
'<Run-Test-Case> with required attributes (provarHome, projectPath, resultsPath),',
|
|
241
232
|
'and at least one <fileset> child. Returns is_valid, issues list, and a validity_score.',
|
|
242
233
|
].join(' '), {
|
|
243
|
-
content: z
|
|
244
|
-
|
|
245
|
-
.optional()
|
|
246
|
-
.describe('XML content to validate directly'),
|
|
247
|
-
file_path: z
|
|
248
|
-
.string()
|
|
249
|
-
.optional()
|
|
250
|
-
.describe('Path to the build.xml file to validate'),
|
|
234
|
+
content: z.string().optional().describe('XML content to validate directly'),
|
|
235
|
+
file_path: z.string().optional().describe('Path to the build.xml file to validate'),
|
|
251
236
|
}, ({ content, file_path }) => {
|
|
252
237
|
const requestId = makeRequestId();
|
|
253
238
|
log('info', 'provar.ant.validate', { requestId, has_content: !!content, file_path });
|
|
@@ -393,13 +378,11 @@ function escapeXmlAttr(value) {
|
|
|
393
378
|
return value
|
|
394
379
|
.replace(/&/g, '&')
|
|
395
380
|
.replace(/"/g, '"')
|
|
381
|
+
.replace(/'/g, ''')
|
|
396
382
|
.replace(/</g, '<')
|
|
397
383
|
.replace(/>/g, '>');
|
|
398
384
|
}
|
|
399
|
-
const REQUIRED_TASKDEF_CLASSNAMES = [
|
|
400
|
-
'com.provar.testrunner.ant.CompileTask',
|
|
401
|
-
'com.provar.testrunner.ant.RunnerTask',
|
|
402
|
-
];
|
|
385
|
+
const REQUIRED_TASKDEF_CLASSNAMES = ['com.provar.testrunner.ant.CompileTask', 'com.provar.testrunner.ant.RunnerTask'];
|
|
403
386
|
const VALID_BROWSERS = ['Chrome', 'Chrome_Headless', 'Firefox', 'Edge', 'Edge_Legacy', 'Safari', 'IE'];
|
|
404
387
|
const VALID_CACHE = ['Reuse', 'Refresh', 'Reload'];
|
|
405
388
|
const VALID_OUTPUT_LEVELS = ['BASIC', 'WARNING', 'DEBUG'];
|
|
@@ -456,19 +439,43 @@ function validateProjectStructure(project, defaultTarget, issues) {
|
|
|
456
439
|
}
|
|
457
440
|
function validateRtcEnumAttrs(rtc, webBrowser, issues) {
|
|
458
441
|
if (webBrowser && !VALID_BROWSERS.includes(webBrowser)) {
|
|
459
|
-
issues.push({
|
|
442
|
+
issues.push({
|
|
443
|
+
rule_id: 'ANT_030',
|
|
444
|
+
severity: 'WARNING',
|
|
445
|
+
message: `webBrowser "${webBrowser}" is not a recognised value. Expected one of: ${VALID_BROWSERS.join(', ')}.`,
|
|
446
|
+
applies_to: 'Run-Test-Case',
|
|
447
|
+
suggestion: `Use one of the supported browser values: ${VALID_BROWSERS.join(', ')}.`,
|
|
448
|
+
});
|
|
460
449
|
}
|
|
461
450
|
const metadataCache = rtc['@_salesforceMetadataCache'];
|
|
462
451
|
if (metadataCache && !VALID_CACHE.includes(metadataCache)) {
|
|
463
|
-
issues.push({
|
|
452
|
+
issues.push({
|
|
453
|
+
rule_id: 'ANT_031',
|
|
454
|
+
severity: 'WARNING',
|
|
455
|
+
message: `salesforceMetadataCache "${metadataCache}" is not a recognised value. Expected one of: ${VALID_CACHE.join(', ')}.`,
|
|
456
|
+
applies_to: 'Run-Test-Case',
|
|
457
|
+
suggestion: `Use one of: ${VALID_CACHE.join(', ')}.`,
|
|
458
|
+
});
|
|
464
459
|
}
|
|
465
460
|
const testOutputLevel = rtc['@_testOutputlevel'];
|
|
466
461
|
if (testOutputLevel && !VALID_OUTPUT_LEVELS.includes(testOutputLevel)) {
|
|
467
|
-
issues.push({
|
|
462
|
+
issues.push({
|
|
463
|
+
rule_id: 'ANT_032',
|
|
464
|
+
severity: 'WARNING',
|
|
465
|
+
message: `testOutputlevel "${testOutputLevel}" is not a recognised value. Expected one of: ${VALID_OUTPUT_LEVELS.join(', ')}.`,
|
|
466
|
+
applies_to: 'Run-Test-Case',
|
|
467
|
+
suggestion: `Use one of: ${VALID_OUTPUT_LEVELS.join(', ')}.`,
|
|
468
|
+
});
|
|
468
469
|
}
|
|
469
470
|
const disposition = rtc['@_resultsPathDisposition'];
|
|
470
471
|
if (disposition && !VALID_DISPOSITIONS.includes(disposition)) {
|
|
471
|
-
issues.push({
|
|
472
|
+
issues.push({
|
|
473
|
+
rule_id: 'ANT_033',
|
|
474
|
+
severity: 'WARNING',
|
|
475
|
+
message: `resultsPathDisposition "${disposition}" is not a recognised value. Expected one of: ${VALID_DISPOSITIONS.join(', ')}.`,
|
|
476
|
+
applies_to: 'Run-Test-Case',
|
|
477
|
+
suggestion: `Use one of: ${VALID_DISPOSITIONS.join(', ')}.`,
|
|
478
|
+
});
|
|
472
479
|
}
|
|
473
480
|
}
|
|
474
481
|
function validateRunTestCase(rtc, issues) {
|
|
@@ -478,22 +485,52 @@ function validateRunTestCase(rtc, issues) {
|
|
|
478
485
|
const webBrowser = rtc['@_webBrowser'] ?? null;
|
|
479
486
|
const testEnvironment = rtc['@_testEnvironment'] ?? null;
|
|
480
487
|
if (!provarHome) {
|
|
481
|
-
issues.push({
|
|
488
|
+
issues.push({
|
|
489
|
+
rule_id: 'ANT_021',
|
|
490
|
+
severity: 'ERROR',
|
|
491
|
+
message: '<Run-Test-Case> missing required "provarHome" attribute.',
|
|
492
|
+
applies_to: 'Run-Test-Case',
|
|
493
|
+
suggestion: 'Add provarHome="${provar.home}" to <Run-Test-Case>.',
|
|
494
|
+
});
|
|
482
495
|
}
|
|
483
496
|
if (!projectPath) {
|
|
484
|
-
issues.push({
|
|
497
|
+
issues.push({
|
|
498
|
+
rule_id: 'ANT_022',
|
|
499
|
+
severity: 'ERROR',
|
|
500
|
+
message: '<Run-Test-Case> missing required "projectPath" attribute.',
|
|
501
|
+
applies_to: 'Run-Test-Case',
|
|
502
|
+
suggestion: 'Add projectPath="${testproject.home}" to <Run-Test-Case>.',
|
|
503
|
+
});
|
|
485
504
|
}
|
|
486
505
|
if (!resultsPath) {
|
|
487
|
-
issues.push({
|
|
506
|
+
issues.push({
|
|
507
|
+
rule_id: 'ANT_023',
|
|
508
|
+
severity: 'ERROR',
|
|
509
|
+
message: '<Run-Test-Case> missing required "resultsPath" attribute.',
|
|
510
|
+
applies_to: 'Run-Test-Case',
|
|
511
|
+
suggestion: 'Add resultsPath="${testproject.results}" to <Run-Test-Case>.',
|
|
512
|
+
});
|
|
488
513
|
}
|
|
489
514
|
validateRtcEnumAttrs(rtc, webBrowser, issues);
|
|
490
515
|
const filesets = rtc['fileset'] ?? [];
|
|
491
516
|
if (filesets.length === 0) {
|
|
492
|
-
issues.push({
|
|
517
|
+
issues.push({
|
|
518
|
+
rule_id: 'ANT_040',
|
|
519
|
+
severity: 'ERROR',
|
|
520
|
+
message: '<Run-Test-Case> has no <fileset> children — no tests will be selected.',
|
|
521
|
+
applies_to: 'Run-Test-Case',
|
|
522
|
+
suggestion: 'Add at least one <fileset dir="..."/> pointing to your tests or plans folder.',
|
|
523
|
+
});
|
|
493
524
|
}
|
|
494
525
|
for (const [i, fsEntry] of filesets.entries()) {
|
|
495
526
|
if (!fsEntry['@_dir']) {
|
|
496
|
-
issues.push({
|
|
527
|
+
issues.push({
|
|
528
|
+
rule_id: 'ANT_041',
|
|
529
|
+
severity: 'ERROR',
|
|
530
|
+
message: `<fileset> at index ${i} is missing required "dir" attribute.`,
|
|
531
|
+
applies_to: 'fileset',
|
|
532
|
+
suggestion: 'Add dir="..." to each <fileset> element.',
|
|
533
|
+
});
|
|
497
534
|
}
|
|
498
535
|
}
|
|
499
536
|
return { provarHome, projectPath, resultsPath, webBrowser, testEnvironment, filesetCount: filesets.length };
|
|
@@ -594,6 +631,135 @@ function finalizeAnt(issues, provarHome, projectPath, resultsPath, webBrowser, t
|
|
|
594
631
|
issues,
|
|
595
632
|
};
|
|
596
633
|
}
|
|
634
|
+
function extractFailureText(el) {
|
|
635
|
+
if (!el)
|
|
636
|
+
return undefined;
|
|
637
|
+
if (typeof el === 'string')
|
|
638
|
+
return el.trim() || undefined;
|
|
639
|
+
if (typeof el === 'object') {
|
|
640
|
+
const obj = el;
|
|
641
|
+
// Prefer CDATA body ('#text') — it has the specific error. Fall back to 'message' attribute.
|
|
642
|
+
const body = obj['#text']?.trim();
|
|
643
|
+
const msg = obj['message']?.trim();
|
|
644
|
+
if (body && msg && body !== msg)
|
|
645
|
+
return `${msg}: ${body}`;
|
|
646
|
+
return body ?? msg;
|
|
647
|
+
}
|
|
648
|
+
return undefined;
|
|
649
|
+
}
|
|
650
|
+
function extractStepsFromJUnit(parsed) {
|
|
651
|
+
const steps = [];
|
|
652
|
+
let idx = 0;
|
|
653
|
+
// Normalise to array of suites — handles both <testsuites> and bare <testsuite>
|
|
654
|
+
let suites = [];
|
|
655
|
+
if (parsed['testsuites']) {
|
|
656
|
+
const inner = parsed['testsuites']['testsuite'];
|
|
657
|
+
suites = Array.isArray(inner)
|
|
658
|
+
? inner
|
|
659
|
+
: inner
|
|
660
|
+
? [inner]
|
|
661
|
+
: [];
|
|
662
|
+
}
|
|
663
|
+
else if (parsed['testsuite']) {
|
|
664
|
+
const ts = parsed['testsuite'];
|
|
665
|
+
suites = Array.isArray(ts) ? ts : [ts];
|
|
666
|
+
}
|
|
667
|
+
for (const suite of suites) {
|
|
668
|
+
const rawTc = suite['testcase'];
|
|
669
|
+
if (!rawTc)
|
|
670
|
+
continue;
|
|
671
|
+
const testcases = Array.isArray(rawTc)
|
|
672
|
+
? rawTc
|
|
673
|
+
: [rawTc];
|
|
674
|
+
for (const tc of testcases) {
|
|
675
|
+
idx++;
|
|
676
|
+
// Provar JUnit: name = test case file name (no attribute prefix since attributeNamePrefix: '')
|
|
677
|
+
const title = tc['name'] ?? `Test ${idx}`;
|
|
678
|
+
const hasFailure = 'failure' in tc || 'error' in tc;
|
|
679
|
+
const hasSkipped = 'skipped' in tc;
|
|
680
|
+
let status = 'pass';
|
|
681
|
+
if (hasFailure)
|
|
682
|
+
status = 'fail';
|
|
683
|
+
else if (hasSkipped)
|
|
684
|
+
status = 'skip';
|
|
685
|
+
const errorMessage = extractFailureText(tc['failure'] ?? tc['error']);
|
|
686
|
+
const step = { testItemId: String(idx), title, status };
|
|
687
|
+
if (errorMessage)
|
|
688
|
+
step.errorMessage = errorMessage;
|
|
689
|
+
steps.push(step);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return steps;
|
|
693
|
+
}
|
|
694
|
+
function findXmlFiles(dir) {
|
|
695
|
+
const results = [];
|
|
696
|
+
try {
|
|
697
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
698
|
+
if (entry.isFile() && entry.name.endsWith('.xml') && !entry.name.startsWith('.')) {
|
|
699
|
+
results.push(path.join(dir, entry.name));
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
catch {
|
|
704
|
+
// ignore unreadable dirs
|
|
705
|
+
}
|
|
706
|
+
return results;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Scan a Provar results directory for JUnit XML files and return structured step results.
|
|
710
|
+
* Returns an empty steps array (+ optional warning) when no XML is found or parsing fails.
|
|
711
|
+
*/
|
|
712
|
+
export function parseJUnitResults(resultsDir) {
|
|
713
|
+
if (!fs.existsSync(resultsDir)) {
|
|
714
|
+
return { steps: [], warning: `Results directory not found: ${resultsDir}` };
|
|
715
|
+
}
|
|
716
|
+
const xmlFiles = findXmlFiles(resultsDir);
|
|
717
|
+
if (xmlFiles.length === 0) {
|
|
718
|
+
return {
|
|
719
|
+
steps: [],
|
|
720
|
+
warning: 'No JUnit XML files found in results directory — structured step output unavailable.',
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
const parser = new XMLParser({
|
|
724
|
+
ignoreAttributes: false,
|
|
725
|
+
attributeNamePrefix: '',
|
|
726
|
+
textNodeName: '#text',
|
|
727
|
+
allowBooleanAttributes: true,
|
|
728
|
+
parseAttributeValue: false,
|
|
729
|
+
isArray: (tagName) => ['testsuite', 'testcase'].includes(tagName),
|
|
730
|
+
});
|
|
731
|
+
const allSteps = [];
|
|
732
|
+
let parsedAny = false;
|
|
733
|
+
let parseFailures = 0;
|
|
734
|
+
for (const xmlFile of xmlFiles) {
|
|
735
|
+
try {
|
|
736
|
+
const content = fs.readFileSync(xmlFile, 'utf-8');
|
|
737
|
+
const parsed = parser.parse(content);
|
|
738
|
+
const steps = extractStepsFromJUnit(parsed);
|
|
739
|
+
allSteps.push(...steps);
|
|
740
|
+
parsedAny = true;
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
parseFailures++;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (!parsedAny) {
|
|
747
|
+
return {
|
|
748
|
+
steps: [],
|
|
749
|
+
warning: 'JUnit XML files found but could not be parsed — structured step output unavailable.',
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
if (allSteps.length === 0) {
|
|
753
|
+
return {
|
|
754
|
+
steps: [],
|
|
755
|
+
warning: 'JUnit XML found but no test steps could be extracted — files may not be standard JUnit format.',
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
const warning = parseFailures > 0
|
|
759
|
+
? `${parseFailures} JUnit XML file(s) could not be parsed — step data may be incomplete.`
|
|
760
|
+
: undefined;
|
|
761
|
+
return { steps: allSteps, warning };
|
|
762
|
+
}
|
|
597
763
|
// ── Registration ──────────────────────────────────────────────────────────────
|
|
598
764
|
export function registerAllAntTools(server, config) {
|
|
599
765
|
registerAntGenerate(server, config);
|