@provartesting/provardx-cli 1.5.0-dev.2 → 1.5.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.
Files changed (147) hide show
  1. package/README.md +163 -12
  2. package/bin/mcp-start.js +74 -0
  3. package/lib/commands/provar/auth/clear.d.ts +7 -0
  4. package/lib/commands/provar/auth/clear.js +36 -0
  5. package/lib/commands/provar/auth/clear.js.map +1 -0
  6. package/lib/commands/provar/auth/login.d.ts +10 -0
  7. package/lib/commands/provar/auth/login.js +90 -0
  8. package/lib/commands/provar/auth/login.js.map +1 -0
  9. package/lib/commands/provar/auth/rotate.d.ts +7 -0
  10. package/lib/commands/provar/auth/rotate.js +42 -0
  11. package/lib/commands/provar/auth/rotate.js.map +1 -0
  12. package/lib/commands/provar/auth/status.d.ts +7 -0
  13. package/lib/commands/provar/auth/status.js +107 -0
  14. package/lib/commands/provar/auth/status.js.map +1 -0
  15. package/lib/commands/provar/mcp/start.d.ts +2 -0
  16. package/lib/commands/provar/mcp/start.js +14 -1
  17. package/lib/commands/provar/mcp/start.js.map +1 -1
  18. package/lib/mcp/docs/NITROX_CATALOG_SOURCE.json +6 -0
  19. package/lib/mcp/docs/NITROX_COMPONENT_CATALOG.md +2001 -0
  20. package/lib/mcp/docs/PROVAR_TEST_STEP_REFERENCE.md +1430 -0
  21. package/lib/mcp/docs/PROVAR_TOOL_GUIDE.md +187 -0
  22. package/lib/mcp/licensing/algasClient.js +14 -5
  23. package/lib/mcp/licensing/algasClient.js.map +1 -1
  24. package/lib/mcp/licensing/ideDetection.d.ts +0 -12
  25. package/lib/mcp/licensing/ideDetection.js +1 -73
  26. package/lib/mcp/licensing/ideDetection.js.map +1 -1
  27. package/lib/mcp/licensing/licenseCache.js +7 -1
  28. package/lib/mcp/licensing/licenseCache.js.map +1 -1
  29. package/lib/mcp/licensing/licenseValidator.d.ts +3 -3
  30. package/lib/mcp/licensing/licenseValidator.js +11 -4
  31. package/lib/mcp/licensing/licenseValidator.js.map +1 -1
  32. package/lib/mcp/prompts/guidePrompts.d.ts +4 -0
  33. package/lib/mcp/prompts/guidePrompts.js +334 -0
  34. package/lib/mcp/prompts/guidePrompts.js.map +1 -0
  35. package/lib/mcp/prompts/index.d.ts +2 -0
  36. package/lib/mcp/prompts/index.js +23 -0
  37. package/lib/mcp/prompts/index.js.map +1 -0
  38. package/lib/mcp/prompts/loopPrompts.d.ts +6 -0
  39. package/lib/mcp/prompts/loopPrompts.js +435 -0
  40. package/lib/mcp/prompts/loopPrompts.js.map +1 -0
  41. package/lib/mcp/prompts/migrationPrompts.d.ts +4 -0
  42. package/lib/mcp/prompts/migrationPrompts.js +207 -0
  43. package/lib/mcp/prompts/migrationPrompts.js.map +1 -0
  44. package/lib/mcp/rules/provar_best_practices_rules.json +256 -544
  45. package/lib/mcp/security/pathPolicy.d.ts +5 -0
  46. package/lib/mcp/security/pathPolicy.js +58 -3
  47. package/lib/mcp/security/pathPolicy.js.map +1 -1
  48. package/lib/mcp/server.d.ts +18 -0
  49. package/lib/mcp/server.js +232 -19
  50. package/lib/mcp/server.js.map +1 -1
  51. package/lib/mcp/tools/antTools.d.ts +15 -0
  52. package/lib/mcp/tools/antTools.js +369 -170
  53. package/lib/mcp/tools/antTools.js.map +1 -1
  54. package/lib/mcp/tools/automationTools.d.ts +18 -8
  55. package/lib/mcp/tools/automationTools.js +333 -176
  56. package/lib/mcp/tools/automationTools.js.map +1 -1
  57. package/lib/mcp/tools/bestPracticesEngine.js +161 -23
  58. package/lib/mcp/tools/bestPracticesEngine.js.map +1 -1
  59. package/lib/mcp/tools/connectionTools.d.ts +4 -0
  60. package/lib/mcp/tools/connectionTools.js +242 -0
  61. package/lib/mcp/tools/connectionTools.js.map +1 -0
  62. package/lib/mcp/tools/defectTools.d.ts +1 -1
  63. package/lib/mcp/tools/defectTools.js +61 -50
  64. package/lib/mcp/tools/defectTools.js.map +1 -1
  65. package/lib/mcp/tools/descHelper.d.ts +5 -0
  66. package/lib/mcp/tools/descHelper.js +14 -0
  67. package/lib/mcp/tools/descHelper.js.map +1 -0
  68. package/lib/mcp/tools/hierarchyValidate.d.ts +1 -1
  69. package/lib/mcp/tools/hierarchyValidate.js +127 -42
  70. package/lib/mcp/tools/hierarchyValidate.js.map +1 -1
  71. package/lib/mcp/tools/nitroXTools.d.ts +23 -0
  72. package/lib/mcp/tools/nitroXTools.js +863 -0
  73. package/lib/mcp/tools/nitroXTools.js.map +1 -0
  74. package/lib/mcp/tools/pageObjectGenerate.js +150 -57
  75. package/lib/mcp/tools/pageObjectGenerate.js.map +1 -1
  76. package/lib/mcp/tools/pageObjectValidate.js +143 -46
  77. package/lib/mcp/tools/pageObjectValidate.js.map +1 -1
  78. package/lib/mcp/tools/projectInspect.js +79 -32
  79. package/lib/mcp/tools/projectInspect.js.map +1 -1
  80. package/lib/mcp/tools/projectValidateFromPath.js +185 -58
  81. package/lib/mcp/tools/projectValidateFromPath.js.map +1 -1
  82. package/lib/mcp/tools/propertiesTools.d.ts +2 -0
  83. package/lib/mcp/tools/propertiesTools.js +358 -78
  84. package/lib/mcp/tools/propertiesTools.js.map +1 -1
  85. package/lib/mcp/tools/qualityHubApiTools.d.ts +3 -0
  86. package/lib/mcp/tools/qualityHubApiTools.js +139 -0
  87. package/lib/mcp/tools/qualityHubApiTools.js.map +1 -0
  88. package/lib/mcp/tools/qualityHubTools.js +292 -72
  89. package/lib/mcp/tools/qualityHubTools.js.map +1 -1
  90. package/lib/mcp/tools/rcaTools.d.ts +3 -2
  91. package/lib/mcp/tools/rcaTools.js +194 -56
  92. package/lib/mcp/tools/rcaTools.js.map +1 -1
  93. package/lib/mcp/tools/sfSpawn.d.ts +25 -3
  94. package/lib/mcp/tools/sfSpawn.js +154 -6
  95. package/lib/mcp/tools/sfSpawn.js.map +1 -1
  96. package/lib/mcp/tools/testCaseGenerate.js +285 -78
  97. package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
  98. package/lib/mcp/tools/testCaseStepTools.d.ts +4 -0
  99. package/lib/mcp/tools/testCaseStepTools.js +244 -0
  100. package/lib/mcp/tools/testCaseStepTools.js.map +1 -0
  101. package/lib/mcp/tools/testCaseValidate.d.ts +11 -0
  102. package/lib/mcp/tools/testCaseValidate.js +381 -46
  103. package/lib/mcp/tools/testCaseValidate.js.map +1 -1
  104. package/lib/mcp/tools/testPlanTools.d.ts +1 -0
  105. package/lib/mcp/tools/testPlanTools.js +316 -59
  106. package/lib/mcp/tools/testPlanTools.js.map +1 -1
  107. package/lib/mcp/tools/testPlanValidate.js +114 -23
  108. package/lib/mcp/tools/testPlanValidate.js.map +1 -1
  109. package/lib/mcp/tools/testSuiteValidate.js +130 -15
  110. package/lib/mcp/tools/testSuiteValidate.js.map +1 -1
  111. package/lib/mcp/update/updateChecker.d.ts +14 -0
  112. package/lib/mcp/update/updateChecker.js +228 -0
  113. package/lib/mcp/update/updateChecker.js.map +1 -0
  114. package/lib/mcp/utils/detailLevel.d.ts +9 -0
  115. package/lib/mcp/utils/detailLevel.js +20 -0
  116. package/lib/mcp/utils/detailLevel.js.map +1 -0
  117. package/lib/mcp/utils/fieldMask.d.ts +17 -0
  118. package/lib/mcp/utils/fieldMask.js +75 -0
  119. package/lib/mcp/utils/fieldMask.js.map +1 -0
  120. package/lib/mcp/utils/tokenMeta.d.ts +40 -0
  121. package/lib/mcp/utils/tokenMeta.js +90 -0
  122. package/lib/mcp/utils/tokenMeta.js.map +1 -0
  123. package/lib/mcp/utils/validationDiff.d.ts +57 -0
  124. package/lib/mcp/utils/validationDiff.js +191 -0
  125. package/lib/mcp/utils/validationDiff.js.map +1 -0
  126. package/lib/mcp/utils/validationScore.d.ts +15 -0
  127. package/lib/mcp/utils/validationScore.js +31 -0
  128. package/lib/mcp/utils/validationScore.js.map +1 -0
  129. package/lib/services/auth/credentials.d.ts +21 -0
  130. package/lib/services/auth/credentials.js +75 -0
  131. package/lib/services/auth/credentials.js.map +1 -0
  132. package/lib/services/auth/loginFlow.d.ts +68 -0
  133. package/lib/services/auth/loginFlow.js +216 -0
  134. package/lib/services/auth/loginFlow.js.map +1 -0
  135. package/lib/services/projectValidation.d.ts +5 -2
  136. package/lib/services/projectValidation.js +83 -31
  137. package/lib/services/projectValidation.js.map +1 -1
  138. package/lib/services/qualityHub/client.d.ts +161 -0
  139. package/lib/services/qualityHub/client.js +226 -0
  140. package/lib/services/qualityHub/client.js.map +1 -0
  141. package/messages/sf.provar.auth.clear.md +16 -0
  142. package/messages/sf.provar.auth.login.md +31 -0
  143. package/messages/sf.provar.auth.rotate.md +23 -0
  144. package/messages/sf.provar.auth.status.md +16 -0
  145. package/messages/sf.provar.mcp.start.md +83 -48
  146. package/oclif.manifest.json +325 -28
  147. package/package.json +35 -12
@@ -12,6 +12,7 @@ import { XMLParser } from 'fast-xml-parser';
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 { desc } from './descHelper.js';
15
16
  // ── Sub-schemas ───────────────────────────────────────────────────────────────
16
17
  const FilesetSchema = z.object({
17
18
  dir: z.string().describe('Directory path (relative or absolute) for the fileset'),
@@ -25,9 +26,7 @@ const FilesetSchema = z.object({
25
26
  .describe('Specific .testcase or .testplan file names to include (e.g. ["Login.testcase"]). Omit to run everything in dir.'),
26
27
  });
27
28
  const PlanFeatureSchema = z.object({
28
- name: z
29
- .enum(['PDF', 'PIECHART', 'EMAIL', 'JUNIT'])
30
- .describe('Feature name (PDF, PIECHART, EMAIL, JUNIT)'),
29
+ name: z.enum(['PDF', 'PIECHART', 'EMAIL', 'JUNIT']).describe('Feature name (PDF, PIECHART, EMAIL, JUNIT)'),
31
30
  type: z.enum(['OUTPUT', 'NOTIFICATION']).describe('Feature type'),
32
31
  enabled: z.boolean().describe('Whether this feature is enabled'),
33
32
  });
@@ -56,148 +55,163 @@ const AttachmentPropertiesSchema = z.object({
56
55
  });
57
56
  // ── Generate tool ─────────────────────────────────────────────────────────────
58
57
  export function registerAntGenerate(server, config) {
59
- server.tool('provar.ant.generate', [
60
- 'Generate a Provar ANT build.xml file.',
61
- 'Produces the standard <project> skeleton with Provar-Compile and Run-Test-Case tasks.',
62
- 'Supports targeting tests by project folder, plan folder, or specific .testcase files via filesets.',
63
- 'Returns XML content. Writes to disk only when dry_run=false.',
64
- ].join(' '), {
65
- // ── Core paths ──────────────────────────────────────────────────────────
66
- provar_home: z
67
- .string()
68
- .describe('Absolute path to the Provar installation directory (e.g. "C:/Program Files/Provar/"). Used for provar.home property and ant taskdef classpaths.'),
69
- project_path: z
70
- .string()
71
- .default('..')
72
- .describe('Path to the Provar test project root. Defaults to ".." (parent of the ANT folder).'),
73
- results_path: z
74
- .string()
75
- .default('../ANT/Results')
76
- .describe('Path where test results are written. Defaults to "../ANT/Results".'),
77
- project_cache_path: z
78
- .string()
79
- .optional()
80
- .describe('Path to the .provarCaches directory. Defaults to "../../.provarCaches" relative to the ANT folder.'),
81
- license_path: z
82
- .string()
83
- .optional()
84
- .describe('Path to the Provar .licenses directory (e.g. "${env.PROVAR_HOME}/.licenses").'),
85
- smtp_path: z
86
- .string()
87
- .optional()
88
- .describe('Path to the Provar .smtp directory (e.g. "${env.PROVAR_HOME}/.smtp").'),
89
- // ── Test selection ──────────────────────────────────────────────────────
90
- filesets: z
91
- .array(FilesetSchema)
92
- .min(1)
93
- .describe('One or more filesets defining which tests to run. ' +
94
- 'To run all tests under a folder: { dir: "../tests" }. ' +
95
- 'To run a plan: { id: "testplan", dir: "../plans/MyPlan" }. ' +
96
- 'To run specific test cases: { dir: "../tests/Suite", includes: ["MyTest.testcase"] }.'),
97
- // ── Browser / environment ───────────────────────────────────────────────
98
- web_browser: z
99
- .enum(['Chrome', 'Chrome_Headless', 'Firefox', 'Edge', 'Edge_Legacy', 'Safari', 'IE'])
100
- .default('Chrome')
101
- .describe('Web browser to use for test execution.'),
102
- web_browser_configuration: z
103
- .string()
104
- .default('Full Screen')
105
- .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").'),
114
- test_environment: z
115
- .string()
116
- .default('')
117
- .describe('Named test environment to use (must match a connection in the project). Empty string uses default.'),
118
- // ── Cache / metadata ────────────────────────────────────────────────────
119
- salesforce_metadata_cache: z
120
- .enum(['Reuse', 'Refresh', 'Reload'])
121
- .default('Reuse')
122
- .describe('Salesforce metadata cache strategy: Reuse (fastest, uses cached), Refresh (re-downloads), Reload (clears and re-downloads).'),
123
- // ── Output / logging ────────────────────────────────────────────────────
124
- results_path_disposition: z
125
- .enum(['Increment', 'Replace', 'Reuse'])
126
- .default('Increment')
127
- .describe('How to handle the results folder when it already exists: Increment (new subfolder), Replace (overwrite), Reuse (append).'),
128
- test_output_level: z
129
- .enum(['BASIC', 'WARNING', 'DEBUG'])
130
- .default('BASIC')
131
- .describe('Verbosity level for test output logs.'),
132
- plugin_output_level: z
133
- .enum(['BASIC', 'WARNING', 'DEBUG'])
134
- .default('WARNING')
135
- .describe('Verbosity level for plugin output logs.'),
136
- // ── Execution behaviour ─────────────────────────────────────────────────
137
- stop_test_run_on_error: z
138
- .boolean()
139
- .default(false)
140
- .describe('Abort the entire test run when any test case fails.'),
141
- exclude_callable_test_cases: z
142
- .boolean()
143
- .default(true)
144
- .describe('Skip test cases marked as callable (library/helper) when true.'),
145
- dont_fail_build: z
146
- .boolean()
147
- .optional()
148
- .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.'),
153
- // ── Secrets / security ──────────────────────────────────────────────────
154
- secrets_password: z
155
- .string()
156
- .default('${env.ProvarSecretsPassword}')
157
- .describe('Password for the Provar secrets store. Defaults to reading from the ProvarSecretsPassword environment variable.'),
158
- test_environment_secrets_password: z
159
- .string()
160
- .optional()
161
- .describe('Per-environment secrets password. Defaults to reading from the ProvarSecretsPassword_EnvName environment variable.'),
162
- // ── Test Cycle ──────────────────────────────────────────────────────────
163
- test_cycle_path: z
164
- .string()
165
- .optional()
166
- .describe('Path to a TestCycle folder (used with test cycle reporting).'),
167
- test_cycle_run_type: z
168
- .enum(['ALL', 'FAILED', 'NEW'])
169
- .optional()
170
- .describe('Which tests in the cycle to run (ALL, FAILED, NEW).'),
171
- // ── Plan features ───────────────────────────────────────────────────────
172
- plan_features: z
173
- .array(PlanFeatureSchema)
174
- .optional()
175
- .describe('Output and notification features to enable/disable (e.g. PDF, PIECHART, EMAIL). ' +
176
- 'Only meaningful when running by test plan.'),
177
- // ── Email / attachment reporting ────────────────────────────────────────
178
- email_properties: EmailPropertiesSchema.optional().describe('Email notification settings. Omit to exclude <emailProperties> from the XML.'),
179
- attachment_properties: AttachmentPropertiesSchema.optional().describe('Attachment/report content settings. Omit to exclude <attachmentProperties> from the XML.'),
180
- // ── File output ─────────────────────────────────────────────────────────
181
- output_path: z
182
- .string()
183
- .optional()
184
- .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.'),
58
+ server.registerTool('provar_ant_generate', {
59
+ title: 'Generate ANT Build File',
60
+ description: desc([
61
+ 'Generate a Provar ANT build.xml file.',
62
+ 'Produces the standard <project> skeleton with Provar-Compile and Run-Test-Case tasks.',
63
+ 'Supports targeting tests by project folder, plan folder, or specific .testcase files via filesets.',
64
+ 'Returns XML content. Writes to disk only when dry_run=false.',
65
+ ].join(' '), 'Generate a Provar ANT build.xml with Provar-Compile and Run-Test-Case tasks.'),
66
+ inputSchema: {
67
+ // ── Core paths ──────────────────────────────────────────────────────────
68
+ provar_home: z
69
+ .string()
70
+ .describe(desc('Absolute path to the Provar installation directory (e.g. "C:/Program Files/Provar/"). Used for provar.home property and ant taskdef classpaths.', 'string, absolute path to Provar installation')),
71
+ project_path: z
72
+ .string()
73
+ .default('..')
74
+ .describe(desc('Path to the Provar test project root. Defaults to ".." (parent of the ANT folder).', 'string, path to project root')),
75
+ results_path: z
76
+ .string()
77
+ .default('../ANT/Results')
78
+ .describe(desc('Path where test results are written. Defaults to "../ANT/Results".', 'string, path for test results')),
79
+ project_cache_path: z
80
+ .string()
81
+ .optional()
82
+ .describe(desc('Path to the .provarCaches directory. Defaults to "../../.provarCaches" relative to the ANT folder.', 'string, optional; path to .provarCaches')),
83
+ license_path: z
84
+ .string()
85
+ .optional()
86
+ .describe(desc('Path to the Provar .licenses directory (e.g. "${env.PROVAR_HOME}/.licenses").', 'string, optional; path to .licenses dir')),
87
+ smtp_path: z
88
+ .string()
89
+ .optional()
90
+ .describe(desc('Path to the Provar .smtp directory (e.g. "${env.PROVAR_HOME}/.smtp").', 'string, optional; path to .smtp dir')),
91
+ // ── Test selection ──────────────────────────────────────────────────────
92
+ filesets: z
93
+ .array(FilesetSchema)
94
+ .min(1)
95
+ .describe(desc('One or more filesets defining which tests to run. ' +
96
+ 'To run all tests under a folder: { dir: "../tests" }. ' +
97
+ 'To run a plan: { id: "testplan", dir: "../plans/MyPlan" }. ' +
98
+ 'To run specific test cases: { dir: "../tests/Suite", includes: ["MyTest.testcase"] }.', 'array, min 1; filesets defining which tests to run')),
99
+ // ── Browser / environment ───────────────────────────────────────────────
100
+ web_browser: z
101
+ .enum(['Chrome', 'Chrome_Headless', 'Firefox', 'Edge', 'Edge_Legacy', 'Safari', 'IE'])
102
+ .default('Chrome')
103
+ .describe(desc('Web browser to use for test execution.', 'enum Chrome|Chrome_Headless|Firefox|Edge|Safari|IE')),
104
+ web_browser_configuration: z
105
+ .string()
106
+ .default('Full Screen')
107
+ .describe(desc('Browser window configuration (e.g. "Full Screen").', 'string, browser window config')),
108
+ web_browser_provider_name: z
109
+ .string()
110
+ .default('Desktop')
111
+ .describe(desc('Browser provider name (e.g. "Desktop").', 'string, browser provider name')),
112
+ web_browser_device_name: z
113
+ .string()
114
+ .default('Full Screen')
115
+ .describe(desc('Browser device name (e.g. "Full Screen").', 'string, browser device name')),
116
+ test_environment: z
117
+ .string()
118
+ .default('')
119
+ .describe(desc('Named test environment to use (must match a connection in the project). Empty string uses default.', 'string, optional; named test environment')),
120
+ // ── Cache / metadata ────────────────────────────────────────────────────
121
+ salesforce_metadata_cache: z
122
+ .enum(['Reuse', 'Refresh', 'Reload'])
123
+ .default('Reuse')
124
+ .describe(desc('Salesforce metadata cache strategy: Reuse (fastest, uses cached), Refresh (re-downloads), Reload (clears and re-downloads).', 'enum Reuse|Refresh|Reload')),
125
+ // ── Output / logging ────────────────────────────────────────────────────
126
+ results_path_disposition: z
127
+ .enum(['Increment', 'Replace', 'Reuse'])
128
+ .default('Increment')
129
+ .describe(desc('How to handle the results folder when it already exists: Increment (new subfolder), Replace (overwrite), Reuse (append).', 'enum Increment|Replace|Reuse')),
130
+ test_output_level: z
131
+ .enum(['BASIC', 'WARNING', 'DEBUG'])
132
+ .default('BASIC')
133
+ .describe(desc('Verbosity level for test output logs.', 'enum BASIC|WARNING|DEBUG')),
134
+ plugin_output_level: z
135
+ .enum(['BASIC', 'WARNING', 'DEBUG'])
136
+ .default('WARNING')
137
+ .describe(desc('Verbosity level for plugin output logs.', 'enum BASIC|WARNING|DEBUG')),
138
+ // ── Execution behaviour ─────────────────────────────────────────────────
139
+ stop_test_run_on_error: z
140
+ .boolean()
141
+ .default(false)
142
+ .describe(desc('Abort the entire test run when any test case fails.', 'bool, optional; abort on failure')),
143
+ exclude_callable_test_cases: z
144
+ .boolean()
145
+ .default(true)
146
+ .describe(desc('Skip test cases marked as callable (library/helper) when true.', 'bool, optional; skip callable tests')),
147
+ dont_fail_build: z
148
+ .boolean()
149
+ .optional()
150
+ .describe(desc('When true, the ANT build does not fail even if tests fail. Useful for CI pipelines that collect results separately.', 'bool, optional; skip build failure on test fail')),
151
+ invoke_test_run_monitor: z
152
+ .boolean()
153
+ .default(true)
154
+ .describe(desc('Enable the Provar test run monitor.', 'bool, optional; enable test run monitor')),
155
+ // ── Secrets / security ──────────────────────────────────────────────────
156
+ secrets_password: z
157
+ .string()
158
+ .default('${env.ProvarSecretsPassword}')
159
+ .describe(desc('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.', 'string, NOT a file path; encryption key for .secrets')),
160
+ test_environment_secrets_password: z
161
+ .string()
162
+ .optional()
163
+ .describe(desc('Per-environment secrets password. Defaults to reading from the ProvarSecretsPassword_EnvName environment variable.', 'string, optional; per-environment secrets key')),
164
+ // ── Test Cycle ──────────────────────────────────────────────────────────
165
+ test_cycle_path: z
166
+ .string()
167
+ .optional()
168
+ .describe(desc('Path to a TestCycle folder (used with test cycle reporting).', 'string, optional; path to TestCycle folder')),
169
+ test_cycle_run_type: z
170
+ .enum(['ALL', 'FAILED', 'NEW'])
171
+ .optional()
172
+ .describe(desc('Which tests in the cycle to run (ALL, FAILED, NEW).', 'enum ALL|FAILED|NEW, optional')),
173
+ // ── Plan features ───────────────────────────────────────────────────────
174
+ plan_features: z
175
+ .array(PlanFeatureSchema)
176
+ .optional()
177
+ .describe(desc('Output and notification features to enable/disable (e.g. PDF, PIECHART, EMAIL). ' +
178
+ 'Only meaningful when running by test plan.', 'array, optional; plan output/notification features')),
179
+ // ── Email / attachment reporting ────────────────────────────────────────
180
+ email_properties: EmailPropertiesSchema.optional().describe(desc('Email notification settings. Omit to exclude <emailProperties> from the XML.', 'object, optional; email notification settings')),
181
+ attachment_properties: AttachmentPropertiesSchema.optional().describe(desc('Attachment/report content settings. Omit to exclude <attachmentProperties> from the XML.', 'object, optional; attachment/report content settings')),
182
+ // ── File output ─────────────────────────────────────────────────────────
183
+ output_path: z
184
+ .string()
185
+ .optional()
186
+ .describe(desc('Where to write the build.xml file (returned in response). Required when dry_run=false.', 'string, optional; absolute path for build.xml output')),
187
+ overwrite: z
188
+ .boolean()
189
+ .default(false)
190
+ .describe(desc('Overwrite output_path if the file already exists.', 'bool, optional; overwrite if exists')),
191
+ dry_run: z
192
+ .boolean()
193
+ .default(true)
194
+ .describe(desc('true = return XML only (default); false = write to output_path.', 'bool, optional; true=return only, false=write')),
195
+ },
193
196
  }, (input) => {
194
197
  const requestId = makeRequestId();
195
- log('info', 'provar.ant.generate', {
198
+ log('info', 'provar_ant_generate', {
196
199
  requestId,
197
200
  output_path: input.output_path,
198
201
  dry_run: input.dry_run,
199
202
  });
200
203
  try {
204
+ // Validate all path inputs before writing anything — these get embedded in the
205
+ // generated ANT build.xml and would be accessed by ANT at execution time.
206
+ assertPathAllowed(input.provar_home, config.allowedPaths);
207
+ assertPathAllowed(input.project_path, config.allowedPaths);
208
+ assertPathAllowed(input.results_path, config.allowedPaths);
209
+ if (input.license_path)
210
+ assertPathAllowed(input.license_path, config.allowedPaths);
211
+ if (input.smtp_path)
212
+ assertPathAllowed(input.smtp_path, config.allowedPaths);
213
+ if (input.project_cache_path)
214
+ assertPathAllowed(input.project_cache_path, config.allowedPaths);
201
215
  const xmlContent = buildAntXml(input);
202
216
  const filePath = input.output_path ? path.resolve(input.output_path) : undefined;
203
217
  let written = false;
@@ -210,7 +224,7 @@ export function registerAntGenerate(server, config) {
210
224
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
211
225
  fs.writeFileSync(filePath, xmlContent, 'utf-8');
212
226
  written = true;
213
- log('info', 'provar.ant.generate: wrote file', { requestId, filePath });
227
+ log('info', 'provar_ant_generate: wrote file', { requestId, filePath });
214
228
  }
215
229
  const result = {
216
230
  requestId,
@@ -226,31 +240,35 @@ export function registerAntGenerate(server, config) {
226
240
  }
227
241
  catch (err) {
228
242
  const error = err;
229
- const errResult = makeError(error instanceof PathPolicyError ? error.code : (error.code ?? 'GENERATE_ERROR'), error.message, requestId, false);
230
- log('error', 'provar.ant.generate failed', { requestId, error: error.message });
243
+ const errResult = makeError(error instanceof PathPolicyError ? error.code : error.code ?? 'GENERATE_ERROR', error.message, requestId, false);
244
+ log('error', 'provar_ant_generate failed', { requestId, error: error.message });
231
245
  return { isError: true, content: [{ type: 'text', text: JSON.stringify(errResult) }] };
232
246
  }
233
247
  });
234
248
  }
235
249
  // ── Validate tool ─────────────────────────────────────────────────────────────
236
250
  export function registerAntValidate(server, config) {
237
- server.tool('provar.ant.validate', [
238
- 'Validate a Provar ANT build.xml for structural correctness.',
239
- 'Checks XML well-formedness, required <taskdef> declarations, <Provar-Compile> step,',
240
- '<Run-Test-Case> with required attributes (provarHome, projectPath, resultsPath),',
241
- 'and at least one <fileset> child. Returns is_valid, issues list, and a validity_score.',
242
- ].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'),
251
+ server.registerTool('provar_ant_validate', {
252
+ title: 'Validate ANT Build File',
253
+ description: desc([
254
+ 'Validate a Provar ANT build.xml for structural correctness.',
255
+ 'Checks XML well-formedness, required <taskdef> declarations, <Provar-Compile> step,',
256
+ '<Run-Test-Case> with required attributes (provarHome, projectPath, resultsPath),',
257
+ 'and at least one <fileset> child. Returns is_valid, issues list, and a validity_score.',
258
+ ].join(' '), 'Validate a Provar ANT build.xml for structural correctness.'),
259
+ inputSchema: {
260
+ content: z
261
+ .string()
262
+ .optional()
263
+ .describe(desc('XML content to validate directly', 'string, optional; inline XML')),
264
+ file_path: z
265
+ .string()
266
+ .optional()
267
+ .describe(desc('Path to the build.xml file to validate', 'string, optional; absolute path to build.xml')),
268
+ },
251
269
  }, ({ content, file_path }) => {
252
270
  const requestId = makeRequestId();
253
- log('info', 'provar.ant.validate', { requestId, has_content: !!content, file_path });
271
+ log('info', 'provar_ant_validate', { requestId, has_content: !!content, file_path });
254
272
  try {
255
273
  let source = content;
256
274
  if (!source && file_path) {
@@ -276,7 +294,7 @@ export function registerAntValidate(server, config) {
276
294
  catch (err) {
277
295
  const error = err;
278
296
  const errResult = makeError(error instanceof PathPolicyError ? error.code : 'VALIDATE_ERROR', error.message, requestId, false);
279
- log('error', 'provar.ant.validate failed', { requestId, error: error.message });
297
+ log('error', 'provar_ant_validate failed', { requestId, error: error.message });
280
298
  return { isError: true, content: [{ type: 'text', text: JSON.stringify(errResult) }] };
281
299
  }
282
300
  });
@@ -393,13 +411,11 @@ function escapeXmlAttr(value) {
393
411
  return value
394
412
  .replace(/&/g, '&amp;')
395
413
  .replace(/"/g, '&quot;')
414
+ .replace(/'/g, '&apos;')
396
415
  .replace(/</g, '&lt;')
397
416
  .replace(/>/g, '&gt;');
398
417
  }
399
- const REQUIRED_TASKDEF_CLASSNAMES = [
400
- 'com.provar.testrunner.ant.CompileTask',
401
- 'com.provar.testrunner.ant.RunnerTask',
402
- ];
418
+ const REQUIRED_TASKDEF_CLASSNAMES = ['com.provar.testrunner.ant.CompileTask', 'com.provar.testrunner.ant.RunnerTask'];
403
419
  const VALID_BROWSERS = ['Chrome', 'Chrome_Headless', 'Firefox', 'Edge', 'Edge_Legacy', 'Safari', 'IE'];
404
420
  const VALID_CACHE = ['Reuse', 'Refresh', 'Reload'];
405
421
  const VALID_OUTPUT_LEVELS = ['BASIC', 'WARNING', 'DEBUG'];
@@ -456,19 +472,43 @@ function validateProjectStructure(project, defaultTarget, issues) {
456
472
  }
457
473
  function validateRtcEnumAttrs(rtc, webBrowser, issues) {
458
474
  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(', ')}.` });
475
+ issues.push({
476
+ rule_id: 'ANT_030',
477
+ severity: 'WARNING',
478
+ message: `webBrowser "${webBrowser}" is not a recognised value. Expected one of: ${VALID_BROWSERS.join(', ')}.`,
479
+ applies_to: 'Run-Test-Case',
480
+ suggestion: `Use one of the supported browser values: ${VALID_BROWSERS.join(', ')}.`,
481
+ });
460
482
  }
461
483
  const metadataCache = rtc['@_salesforceMetadataCache'];
462
484
  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(', ')}.` });
485
+ issues.push({
486
+ rule_id: 'ANT_031',
487
+ severity: 'WARNING',
488
+ message: `salesforceMetadataCache "${metadataCache}" is not a recognised value. Expected one of: ${VALID_CACHE.join(', ')}.`,
489
+ applies_to: 'Run-Test-Case',
490
+ suggestion: `Use one of: ${VALID_CACHE.join(', ')}.`,
491
+ });
464
492
  }
465
493
  const testOutputLevel = rtc['@_testOutputlevel'];
466
494
  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(', ')}.` });
495
+ issues.push({
496
+ rule_id: 'ANT_032',
497
+ severity: 'WARNING',
498
+ message: `testOutputlevel "${testOutputLevel}" is not a recognised value. Expected one of: ${VALID_OUTPUT_LEVELS.join(', ')}.`,
499
+ applies_to: 'Run-Test-Case',
500
+ suggestion: `Use one of: ${VALID_OUTPUT_LEVELS.join(', ')}.`,
501
+ });
468
502
  }
469
503
  const disposition = rtc['@_resultsPathDisposition'];
470
504
  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(', ')}.` });
505
+ issues.push({
506
+ rule_id: 'ANT_033',
507
+ severity: 'WARNING',
508
+ message: `resultsPathDisposition "${disposition}" is not a recognised value. Expected one of: ${VALID_DISPOSITIONS.join(', ')}.`,
509
+ applies_to: 'Run-Test-Case',
510
+ suggestion: `Use one of: ${VALID_DISPOSITIONS.join(', ')}.`,
511
+ });
472
512
  }
473
513
  }
474
514
  function validateRunTestCase(rtc, issues) {
@@ -478,22 +518,52 @@ function validateRunTestCase(rtc, issues) {
478
518
  const webBrowser = rtc['@_webBrowser'] ?? null;
479
519
  const testEnvironment = rtc['@_testEnvironment'] ?? null;
480
520
  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>.' });
521
+ issues.push({
522
+ rule_id: 'ANT_021',
523
+ severity: 'ERROR',
524
+ message: '<Run-Test-Case> missing required "provarHome" attribute.',
525
+ applies_to: 'Run-Test-Case',
526
+ suggestion: 'Add provarHome="${provar.home}" to <Run-Test-Case>.',
527
+ });
482
528
  }
483
529
  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>.' });
530
+ issues.push({
531
+ rule_id: 'ANT_022',
532
+ severity: 'ERROR',
533
+ message: '<Run-Test-Case> missing required "projectPath" attribute.',
534
+ applies_to: 'Run-Test-Case',
535
+ suggestion: 'Add projectPath="${testproject.home}" to <Run-Test-Case>.',
536
+ });
485
537
  }
486
538
  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>.' });
539
+ issues.push({
540
+ rule_id: 'ANT_023',
541
+ severity: 'ERROR',
542
+ message: '<Run-Test-Case> missing required "resultsPath" attribute.',
543
+ applies_to: 'Run-Test-Case',
544
+ suggestion: 'Add resultsPath="${testproject.results}" to <Run-Test-Case>.',
545
+ });
488
546
  }
489
547
  validateRtcEnumAttrs(rtc, webBrowser, issues);
490
548
  const filesets = rtc['fileset'] ?? [];
491
549
  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.' });
550
+ issues.push({
551
+ rule_id: 'ANT_040',
552
+ severity: 'ERROR',
553
+ message: '<Run-Test-Case> has no <fileset> children — no tests will be selected.',
554
+ applies_to: 'Run-Test-Case',
555
+ suggestion: 'Add at least one <fileset dir="..."/> pointing to your tests or plans folder.',
556
+ });
493
557
  }
494
558
  for (const [i, fsEntry] of filesets.entries()) {
495
559
  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.' });
560
+ issues.push({
561
+ rule_id: 'ANT_041',
562
+ severity: 'ERROR',
563
+ message: `<fileset> at index ${i} is missing required "dir" attribute.`,
564
+ applies_to: 'fileset',
565
+ suggestion: 'Add dir="..." to each <fileset> element.',
566
+ });
497
567
  }
498
568
  }
499
569
  return { provarHome, projectPath, resultsPath, webBrowser, testEnvironment, filesetCount: filesets.length };
@@ -594,6 +664,135 @@ function finalizeAnt(issues, provarHome, projectPath, resultsPath, webBrowser, t
594
664
  issues,
595
665
  };
596
666
  }
667
+ function extractFailureText(el) {
668
+ if (!el)
669
+ return undefined;
670
+ if (typeof el === 'string')
671
+ return el.trim() || undefined;
672
+ if (typeof el === 'object') {
673
+ const obj = el;
674
+ // Prefer CDATA body ('#text') — it has the specific error. Fall back to 'message' attribute.
675
+ const body = obj['#text']?.trim();
676
+ const msg = obj['message']?.trim();
677
+ if (body && msg && body !== msg)
678
+ return `${msg}: ${body}`;
679
+ return body ?? msg;
680
+ }
681
+ return undefined;
682
+ }
683
+ function extractStepsFromJUnit(parsed) {
684
+ const steps = [];
685
+ let idx = 0;
686
+ // Normalise to array of suites — handles both <testsuites> and bare <testsuite>
687
+ let suites = [];
688
+ if (parsed['testsuites']) {
689
+ const inner = parsed['testsuites']['testsuite'];
690
+ suites = Array.isArray(inner)
691
+ ? inner
692
+ : inner
693
+ ? [inner]
694
+ : [];
695
+ }
696
+ else if (parsed['testsuite']) {
697
+ const ts = parsed['testsuite'];
698
+ suites = Array.isArray(ts) ? ts : [ts];
699
+ }
700
+ for (const suite of suites) {
701
+ const rawTc = suite['testcase'];
702
+ if (!rawTc)
703
+ continue;
704
+ const testcases = Array.isArray(rawTc)
705
+ ? rawTc
706
+ : [rawTc];
707
+ for (const tc of testcases) {
708
+ idx++;
709
+ // Provar JUnit: name = test case file name (no attribute prefix since attributeNamePrefix: '')
710
+ const title = tc['name'] ?? `Test ${idx}`;
711
+ const hasFailure = 'failure' in tc || 'error' in tc;
712
+ const hasSkipped = 'skipped' in tc;
713
+ let status = 'pass';
714
+ if (hasFailure)
715
+ status = 'fail';
716
+ else if (hasSkipped)
717
+ status = 'skip';
718
+ const errorMessage = extractFailureText(tc['failure'] ?? tc['error']);
719
+ const step = { testItemId: String(idx), title, status };
720
+ if (errorMessage)
721
+ step.errorMessage = errorMessage;
722
+ steps.push(step);
723
+ }
724
+ }
725
+ return steps;
726
+ }
727
+ function findXmlFiles(dir) {
728
+ const results = [];
729
+ try {
730
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
731
+ if (entry.isFile() && entry.name.endsWith('.xml') && !entry.name.startsWith('.')) {
732
+ results.push(path.join(dir, entry.name));
733
+ }
734
+ }
735
+ }
736
+ catch {
737
+ // ignore unreadable dirs
738
+ }
739
+ return results;
740
+ }
741
+ /**
742
+ * Scan a Provar results directory for JUnit XML files and return structured step results.
743
+ * Returns an empty steps array (+ optional warning) when no XML is found or parsing fails.
744
+ */
745
+ export function parseJUnitResults(resultsDir) {
746
+ if (!fs.existsSync(resultsDir)) {
747
+ return { steps: [], warning: `Results directory not found: ${resultsDir}` };
748
+ }
749
+ const xmlFiles = findXmlFiles(resultsDir);
750
+ if (xmlFiles.length === 0) {
751
+ return {
752
+ steps: [],
753
+ warning: 'No JUnit XML files found in results directory — structured step output unavailable.',
754
+ };
755
+ }
756
+ const parser = new XMLParser({
757
+ ignoreAttributes: false,
758
+ attributeNamePrefix: '',
759
+ textNodeName: '#text',
760
+ allowBooleanAttributes: true,
761
+ parseAttributeValue: false,
762
+ isArray: (tagName) => ['testsuite', 'testcase'].includes(tagName),
763
+ });
764
+ const allSteps = [];
765
+ let parsedAny = false;
766
+ let parseFailures = 0;
767
+ for (const xmlFile of xmlFiles) {
768
+ try {
769
+ const content = fs.readFileSync(xmlFile, 'utf-8');
770
+ const parsed = parser.parse(content);
771
+ const steps = extractStepsFromJUnit(parsed);
772
+ allSteps.push(...steps);
773
+ parsedAny = true;
774
+ }
775
+ catch {
776
+ parseFailures++;
777
+ }
778
+ }
779
+ if (!parsedAny) {
780
+ return {
781
+ steps: [],
782
+ warning: 'JUnit XML files found but could not be parsed — structured step output unavailable.',
783
+ };
784
+ }
785
+ if (allSteps.length === 0) {
786
+ return {
787
+ steps: [],
788
+ warning: 'JUnit XML found but no test steps could be extracted — files may not be standard JUnit format.',
789
+ };
790
+ }
791
+ const warning = parseFailures > 0
792
+ ? `${parseFailures} JUnit XML file(s) could not be parsed — step data may be incomplete.`
793
+ : undefined;
794
+ return { steps: allSteps, warning };
795
+ }
597
796
  // ── Registration ──────────────────────────────────────────────────────────────
598
797
  export function registerAllAntTools(server, config) {
599
798
  registerAntGenerate(server, config);