@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.
Files changed (92) hide show
  1. package/README.md +137 -13
  2. package/lib/commands/provar/auth/clear.d.ts +7 -0
  3. package/lib/commands/provar/auth/clear.js +36 -0
  4. package/lib/commands/provar/auth/clear.js.map +1 -0
  5. package/lib/commands/provar/auth/login.d.ts +10 -0
  6. package/lib/commands/provar/auth/login.js +90 -0
  7. package/lib/commands/provar/auth/login.js.map +1 -0
  8. package/lib/commands/provar/auth/rotate.d.ts +7 -0
  9. package/lib/commands/provar/auth/rotate.js +42 -0
  10. package/lib/commands/provar/auth/rotate.js.map +1 -0
  11. package/lib/commands/provar/auth/status.d.ts +7 -0
  12. package/lib/commands/provar/auth/status.js +107 -0
  13. package/lib/commands/provar/auth/status.js.map +1 -0
  14. package/lib/mcp/docs/PROVAR_TEST_STEP_REFERENCE.md +1430 -0
  15. package/lib/mcp/licensing/algasClient.js +14 -5
  16. package/lib/mcp/licensing/algasClient.js.map +1 -1
  17. package/lib/mcp/licensing/ideDetection.d.ts +0 -12
  18. package/lib/mcp/licensing/ideDetection.js +0 -72
  19. package/lib/mcp/licensing/ideDetection.js.map +1 -1
  20. package/lib/mcp/licensing/licenseCache.js +7 -1
  21. package/lib/mcp/licensing/licenseCache.js.map +1 -1
  22. package/lib/mcp/licensing/licenseValidator.d.ts +3 -3
  23. package/lib/mcp/licensing/licenseValidator.js +11 -4
  24. package/lib/mcp/licensing/licenseValidator.js.map +1 -1
  25. package/lib/mcp/prompts/index.d.ts +2 -0
  26. package/lib/mcp/prompts/index.js +19 -0
  27. package/lib/mcp/prompts/index.js.map +1 -0
  28. package/lib/mcp/prompts/loopPrompts.d.ts +6 -0
  29. package/lib/mcp/prompts/loopPrompts.js +435 -0
  30. package/lib/mcp/prompts/loopPrompts.js.map +1 -0
  31. package/lib/mcp/prompts/migrationPrompts.d.ts +4 -0
  32. package/lib/mcp/prompts/migrationPrompts.js +207 -0
  33. package/lib/mcp/prompts/migrationPrompts.js.map +1 -0
  34. package/lib/mcp/rules/provar_best_practices_rules.json +256 -544
  35. package/lib/mcp/security/pathPolicy.d.ts +5 -0
  36. package/lib/mcp/security/pathPolicy.js +30 -2
  37. package/lib/mcp/security/pathPolicy.js.map +1 -1
  38. package/lib/mcp/server.js +51 -4
  39. package/lib/mcp/server.js.map +1 -1
  40. package/lib/mcp/tools/antTools.d.ts +15 -0
  41. package/lib/mcp/tools/antTools.js +216 -50
  42. package/lib/mcp/tools/antTools.js.map +1 -1
  43. package/lib/mcp/tools/automationTools.d.ts +39 -5
  44. package/lib/mcp/tools/automationTools.js +341 -55
  45. package/lib/mcp/tools/automationTools.js.map +1 -1
  46. package/lib/mcp/tools/bestPracticesEngine.js +161 -23
  47. package/lib/mcp/tools/bestPracticesEngine.js.map +1 -1
  48. package/lib/mcp/tools/connectionTools.d.ts +4 -0
  49. package/lib/mcp/tools/connectionTools.js +168 -0
  50. package/lib/mcp/tools/connectionTools.js.map +1 -0
  51. package/lib/mcp/tools/nitroXTools.d.ts +22 -0
  52. package/lib/mcp/tools/nitroXTools.js +750 -0
  53. package/lib/mcp/tools/nitroXTools.js.map +1 -0
  54. package/lib/mcp/tools/pageObjectGenerate.js +103 -35
  55. package/lib/mcp/tools/pageObjectGenerate.js.map +1 -1
  56. package/lib/mcp/tools/propertiesTools.d.ts +2 -0
  57. package/lib/mcp/tools/propertiesTools.js +277 -39
  58. package/lib/mcp/tools/propertiesTools.js.map +1 -1
  59. package/lib/mcp/tools/qualityHubApiTools.d.ts +3 -0
  60. package/lib/mcp/tools/qualityHubApiTools.js +134 -0
  61. package/lib/mcp/tools/qualityHubApiTools.js.map +1 -0
  62. package/lib/mcp/tools/qualityHubTools.js +139 -20
  63. package/lib/mcp/tools/qualityHubTools.js.map +1 -1
  64. package/lib/mcp/tools/rcaTools.d.ts +3 -2
  65. package/lib/mcp/tools/rcaTools.js +145 -20
  66. package/lib/mcp/tools/rcaTools.js.map +1 -1
  67. package/lib/mcp/tools/sfSpawn.d.ts +1 -1
  68. package/lib/mcp/tools/sfSpawn.js +8 -2
  69. package/lib/mcp/tools/sfSpawn.js.map +1 -1
  70. package/lib/mcp/tools/testCaseGenerate.js +88 -59
  71. package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
  72. package/lib/mcp/tools/testCaseStepTools.d.ts +4 -0
  73. package/lib/mcp/tools/testCaseStepTools.js +221 -0
  74. package/lib/mcp/tools/testCaseStepTools.js.map +1 -0
  75. package/lib/mcp/tools/testCaseValidate.d.ts +11 -0
  76. package/lib/mcp/tools/testCaseValidate.js +146 -19
  77. package/lib/mcp/tools/testCaseValidate.js.map +1 -1
  78. package/lib/services/auth/credentials.d.ts +21 -0
  79. package/lib/services/auth/credentials.js +75 -0
  80. package/lib/services/auth/credentials.js.map +1 -0
  81. package/lib/services/auth/loginFlow.d.ts +68 -0
  82. package/lib/services/auth/loginFlow.js +216 -0
  83. package/lib/services/auth/loginFlow.js.map +1 -0
  84. package/lib/services/qualityHub/client.d.ts +161 -0
  85. package/lib/services/qualityHub/client.js +226 -0
  86. package/lib/services/qualityHub/client.js.map +1 -0
  87. package/messages/sf.provar.auth.clear.md +16 -0
  88. package/messages/sf.provar.auth.login.md +31 -0
  89. package/messages/sf.provar.auth.rotate.md +23 -0
  90. package/messages/sf.provar.auth.status.md +16 -0
  91. package/oclif.manifest.json +241 -28
  92. 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
- const resolved = path.resolve(filePath);
32
- const resolvedAllowed = allowedPaths.map((p) => path.resolve(path.normalize(p)));
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;;;;;;;;GAQG;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;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxC,MAAM,eAAe,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjF,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"}
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: '1.0.0',
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: 'provar-mcp@1.0.0' };
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
- registerAllAutomationTools(server);
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
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}
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
- .string()
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('Password for the Provar secrets store. Defaults to reading from the ProvarSecretsPassword environment variable.'),
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
- .boolean()
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 : (error.code ?? 'GENERATE_ERROR'), error.message, requestId, false);
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
- .string()
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, '&amp;')
395
380
  .replace(/"/g, '&quot;')
381
+ .replace(/'/g, '&apos;')
396
382
  .replace(/</g, '&lt;')
397
383
  .replace(/>/g, '&gt;');
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({ rule_id: 'ANT_030', severity: 'WARNING', message: `webBrowser "${webBrowser}" is not a recognised value. Expected one of: ${VALID_BROWSERS.join(', ')}.`, applies_to: 'Run-Test-Case', suggestion: `Use one of the supported browser values: ${VALID_BROWSERS.join(', ')}.` });
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({ rule_id: 'ANT_031', severity: 'WARNING', message: `salesforceMetadataCache "${metadataCache}" is not a recognised value. Expected one of: ${VALID_CACHE.join(', ')}.`, applies_to: 'Run-Test-Case', suggestion: `Use one of: ${VALID_CACHE.join(', ')}.` });
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({ rule_id: 'ANT_032', severity: 'WARNING', message: `testOutputlevel "${testOutputLevel}" is not a recognised value. Expected one of: ${VALID_OUTPUT_LEVELS.join(', ')}.`, applies_to: 'Run-Test-Case', suggestion: `Use one of: ${VALID_OUTPUT_LEVELS.join(', ')}.` });
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({ rule_id: 'ANT_033', severity: 'WARNING', message: `resultsPathDisposition "${disposition}" is not a recognised value. Expected one of: ${VALID_DISPOSITIONS.join(', ')}.`, applies_to: 'Run-Test-Case', suggestion: `Use one of: ${VALID_DISPOSITIONS.join(', ')}.` });
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({ rule_id: 'ANT_021', severity: 'ERROR', message: '<Run-Test-Case> missing required "provarHome" attribute.', applies_to: 'Run-Test-Case', suggestion: 'Add provarHome="${provar.home}" to <Run-Test-Case>.' });
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({ rule_id: 'ANT_022', severity: 'ERROR', message: '<Run-Test-Case> missing required "projectPath" attribute.', applies_to: 'Run-Test-Case', suggestion: 'Add projectPath="${testproject.home}" to <Run-Test-Case>.' });
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({ rule_id: 'ANT_023', severity: 'ERROR', message: '<Run-Test-Case> missing required "resultsPath" attribute.', applies_to: 'Run-Test-Case', suggestion: 'Add resultsPath="${testproject.results}" to <Run-Test-Case>.' });
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({ rule_id: 'ANT_040', severity: 'ERROR', message: '<Run-Test-Case> has no <fileset> children — no tests will be selected.', applies_to: 'Run-Test-Case', suggestion: 'Add at least one <fileset dir="..."/> pointing to your tests or plans folder.' });
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({ rule_id: 'ANT_041', severity: 'ERROR', message: `<fileset> at index ${i} is missing required "dir" attribute.`, applies_to: 'fileset', suggestion: 'Add dir="..." to each <fileset> element.' });
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);