@provartesting/provardx-cli 1.5.0-dev.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +163 -13
  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 +175 -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 +324 -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 +17 -0
  49. package/lib/mcp/server.js +151 -6
  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 +347 -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 +332 -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 +172 -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 +56 -50
  64. package/lib/mcp/tools/defectTools.js.map +1 -1
  65. package/lib/mcp/tools/hierarchyValidate.d.ts +1 -1
  66. package/lib/mcp/tools/hierarchyValidate.js +127 -42
  67. package/lib/mcp/tools/hierarchyValidate.js.map +1 -1
  68. package/lib/mcp/tools/nitroXTools.d.ts +23 -0
  69. package/lib/mcp/tools/nitroXTools.js +823 -0
  70. package/lib/mcp/tools/nitroXTools.js.map +1 -0
  71. package/lib/mcp/tools/pageObjectGenerate.js +132 -57
  72. package/lib/mcp/tools/pageObjectGenerate.js.map +1 -1
  73. package/lib/mcp/tools/pageObjectValidate.js +136 -46
  74. package/lib/mcp/tools/pageObjectValidate.js.map +1 -1
  75. package/lib/mcp/tools/projectInspect.js +51 -30
  76. package/lib/mcp/tools/projectInspect.js.map +1 -1
  77. package/lib/mcp/tools/projectValidateFromPath.js +70 -49
  78. package/lib/mcp/tools/projectValidateFromPath.js.map +1 -1
  79. package/lib/mcp/tools/propertiesTools.d.ts +2 -0
  80. package/lib/mcp/tools/propertiesTools.js +332 -78
  81. package/lib/mcp/tools/propertiesTools.js.map +1 -1
  82. package/lib/mcp/tools/qualityHubApiTools.d.ts +3 -0
  83. package/lib/mcp/tools/qualityHubApiTools.js +138 -0
  84. package/lib/mcp/tools/qualityHubApiTools.js.map +1 -0
  85. package/lib/mcp/tools/qualityHubTools.js +219 -70
  86. package/lib/mcp/tools/qualityHubTools.js.map +1 -1
  87. package/lib/mcp/tools/rcaTools.d.ts +3 -2
  88. package/lib/mcp/tools/rcaTools.js +189 -56
  89. package/lib/mcp/tools/rcaTools.js.map +1 -1
  90. package/lib/mcp/tools/sfSpawn.d.ts +25 -3
  91. package/lib/mcp/tools/sfSpawn.js +154 -6
  92. package/lib/mcp/tools/sfSpawn.js.map +1 -1
  93. package/lib/mcp/tools/testCaseGenerate.js +226 -78
  94. package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
  95. package/lib/mcp/tools/testCaseStepTools.d.ts +4 -0
  96. package/lib/mcp/tools/testCaseStepTools.js +226 -0
  97. package/lib/mcp/tools/testCaseStepTools.js.map +1 -0
  98. package/lib/mcp/tools/testCaseValidate.d.ts +11 -0
  99. package/lib/mcp/tools/testCaseValidate.js +307 -46
  100. package/lib/mcp/tools/testCaseValidate.js.map +1 -1
  101. package/lib/mcp/tools/testPlanTools.d.ts +1 -0
  102. package/lib/mcp/tools/testPlanTools.js +299 -59
  103. package/lib/mcp/tools/testPlanTools.js.map +1 -1
  104. package/lib/mcp/tools/testPlanValidate.js +56 -18
  105. package/lib/mcp/tools/testPlanValidate.js.map +1 -1
  106. package/lib/mcp/tools/testSuiteValidate.js +37 -11
  107. package/lib/mcp/tools/testSuiteValidate.js.map +1 -1
  108. package/lib/mcp/update/updateChecker.d.ts +14 -0
  109. package/lib/mcp/update/updateChecker.js +228 -0
  110. package/lib/mcp/update/updateChecker.js.map +1 -0
  111. package/lib/services/auth/credentials.d.ts +21 -0
  112. package/lib/services/auth/credentials.js +75 -0
  113. package/lib/services/auth/credentials.js.map +1 -0
  114. package/lib/services/auth/loginFlow.d.ts +68 -0
  115. package/lib/services/auth/loginFlow.js +216 -0
  116. package/lib/services/auth/loginFlow.js.map +1 -0
  117. package/lib/services/projectValidation.d.ts +5 -2
  118. package/lib/services/projectValidation.js +83 -31
  119. package/lib/services/projectValidation.js.map +1 -1
  120. package/lib/services/qualityHub/client.d.ts +161 -0
  121. package/lib/services/qualityHub/client.js +226 -0
  122. package/lib/services/qualityHub/client.js.map +1 -0
  123. package/messages/sf.provar.auth.clear.md +16 -0
  124. package/messages/sf.provar.auth.login.md +31 -0
  125. package/messages/sf.provar.auth.rotate.md +23 -0
  126. package/messages/sf.provar.auth.status.md +16 -0
  127. package/messages/sf.provar.mcp.start.md +83 -48
  128. package/oclif.manifest.json +299 -2
  129. package/package.json +23 -12
@@ -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
  });
@@ -56,148 +54,148 @@ const AttachmentPropertiesSchema = z.object({
56
54
  });
57
55
  // ── Generate tool ─────────────────────────────────────────────────────────────
58
56
  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.'),
57
+ server.registerTool('provar_ant_generate', {
58
+ title: 'Generate ANT Build File',
59
+ description: [
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
+ inputSchema: {
66
+ // ── Core paths ──────────────────────────────────────────────────────────
67
+ provar_home: z
68
+ .string()
69
+ .describe('Absolute path to the Provar installation directory (e.g. "C:/Program Files/Provar/"). Used for provar.home property and ant taskdef classpaths.'),
70
+ project_path: z
71
+ .string()
72
+ .default('..')
73
+ .describe('Path to the Provar test project root. Defaults to ".." (parent of the ANT folder).'),
74
+ results_path: z
75
+ .string()
76
+ .default('../ANT/Results')
77
+ .describe('Path where test results are written. Defaults to "../ANT/Results".'),
78
+ project_cache_path: z
79
+ .string()
80
+ .optional()
81
+ .describe('Path to the .provarCaches directory. Defaults to "../../.provarCaches" relative to the ANT folder.'),
82
+ license_path: z
83
+ .string()
84
+ .optional()
85
+ .describe('Path to the Provar .licenses directory (e.g. "${env.PROVAR_HOME}/.licenses").'),
86
+ smtp_path: z
87
+ .string()
88
+ .optional()
89
+ .describe('Path to the Provar .smtp directory (e.g. "${env.PROVAR_HOME}/.smtp").'),
90
+ // ── Test selection ──────────────────────────────────────────────────────
91
+ filesets: z
92
+ .array(FilesetSchema)
93
+ .min(1)
94
+ .describe('One or more filesets defining which tests to run. ' +
95
+ 'To run all tests under a folder: { dir: "../tests" }. ' +
96
+ 'To run a plan: { id: "testplan", dir: "../plans/MyPlan" }. ' +
97
+ 'To run specific test cases: { dir: "../tests/Suite", includes: ["MyTest.testcase"] }.'),
98
+ // ── Browser / environment ───────────────────────────────────────────────
99
+ web_browser: z
100
+ .enum(['Chrome', 'Chrome_Headless', 'Firefox', 'Edge', 'Edge_Legacy', 'Safari', 'IE'])
101
+ .default('Chrome')
102
+ .describe('Web browser to use for test execution.'),
103
+ web_browser_configuration: z
104
+ .string()
105
+ .default('Full Screen')
106
+ .describe('Browser window configuration (e.g. "Full Screen").'),
107
+ web_browser_provider_name: z.string().default('Desktop').describe('Browser provider name (e.g. "Desktop").'),
108
+ web_browser_device_name: z
109
+ .string()
110
+ .default('Full Screen')
111
+ .describe('Browser device name (e.g. "Full Screen").'),
112
+ test_environment: z
113
+ .string()
114
+ .default('')
115
+ .describe('Named test environment to use (must match a connection in the project). Empty string uses default.'),
116
+ // ── Cache / metadata ────────────────────────────────────────────────────
117
+ salesforce_metadata_cache: z
118
+ .enum(['Reuse', 'Refresh', 'Reload'])
119
+ .default('Reuse')
120
+ .describe('Salesforce metadata cache strategy: Reuse (fastest, uses cached), Refresh (re-downloads), Reload (clears and re-downloads).'),
121
+ // ── Output / logging ────────────────────────────────────────────────────
122
+ results_path_disposition: z
123
+ .enum(['Increment', 'Replace', 'Reuse'])
124
+ .default('Increment')
125
+ .describe('How to handle the results folder when it already exists: Increment (new subfolder), Replace (overwrite), Reuse (append).'),
126
+ test_output_level: z
127
+ .enum(['BASIC', 'WARNING', 'DEBUG'])
128
+ .default('BASIC')
129
+ .describe('Verbosity level for test output logs.'),
130
+ plugin_output_level: z
131
+ .enum(['BASIC', 'WARNING', 'DEBUG'])
132
+ .default('WARNING')
133
+ .describe('Verbosity level for plugin output logs.'),
134
+ // ── Execution behaviour ─────────────────────────────────────────────────
135
+ stop_test_run_on_error: z
136
+ .boolean()
137
+ .default(false)
138
+ .describe('Abort the entire test run when any test case fails.'),
139
+ exclude_callable_test_cases: z
140
+ .boolean()
141
+ .default(true)
142
+ .describe('Skip test cases marked as callable (library/helper) when true.'),
143
+ dont_fail_build: z
144
+ .boolean()
145
+ .optional()
146
+ .describe('When true, the ANT build does not fail even if tests fail. Useful for CI pipelines that collect results separately.'),
147
+ invoke_test_run_monitor: z.boolean().default(true).describe('Enable the Provar test run monitor.'),
148
+ // ── Secrets / security ──────────────────────────────────────────────────
149
+ secrets_password: z
150
+ .string()
151
+ .default('${env.ProvarSecretsPassword}')
152
+ .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.'),
153
+ test_environment_secrets_password: z
154
+ .string()
155
+ .optional()
156
+ .describe('Per-environment secrets password. Defaults to reading from the ProvarSecretsPassword_EnvName environment variable.'),
157
+ // ── Test Cycle ──────────────────────────────────────────────────────────
158
+ test_cycle_path: z.string().optional().describe('Path to a TestCycle folder (used with test cycle reporting).'),
159
+ test_cycle_run_type: z
160
+ .enum(['ALL', 'FAILED', 'NEW'])
161
+ .optional()
162
+ .describe('Which tests in the cycle to run (ALL, FAILED, NEW).'),
163
+ // ── Plan features ───────────────────────────────────────────────────────
164
+ plan_features: z
165
+ .array(PlanFeatureSchema)
166
+ .optional()
167
+ .describe('Output and notification features to enable/disable (e.g. PDF, PIECHART, EMAIL). ' +
168
+ 'Only meaningful when running by test plan.'),
169
+ // ── Email / attachment reporting ────────────────────────────────────────
170
+ email_properties: EmailPropertiesSchema.optional().describe('Email notification settings. Omit to exclude <emailProperties> from the XML.'),
171
+ attachment_properties: AttachmentPropertiesSchema.optional().describe('Attachment/report content settings. Omit to exclude <attachmentProperties> from the XML.'),
172
+ // ── File output ─────────────────────────────────────────────────────────
173
+ output_path: z
174
+ .string()
175
+ .optional()
176
+ .describe('Where to write the build.xml file (returned in response). Required when dry_run=false.'),
177
+ overwrite: z.boolean().default(false).describe('Overwrite output_path if the file already exists.'),
178
+ dry_run: z.boolean().default(true).describe('true = return XML only (default); false = write to output_path.'),
179
+ },
193
180
  }, (input) => {
194
181
  const requestId = makeRequestId();
195
- log('info', 'provar.ant.generate', {
182
+ log('info', 'provar_ant_generate', {
196
183
  requestId,
197
184
  output_path: input.output_path,
198
185
  dry_run: input.dry_run,
199
186
  });
200
187
  try {
188
+ // Validate all path inputs before writing anything — these get embedded in the
189
+ // generated ANT build.xml and would be accessed by ANT at execution time.
190
+ assertPathAllowed(input.provar_home, config.allowedPaths);
191
+ assertPathAllowed(input.project_path, config.allowedPaths);
192
+ assertPathAllowed(input.results_path, config.allowedPaths);
193
+ if (input.license_path)
194
+ assertPathAllowed(input.license_path, config.allowedPaths);
195
+ if (input.smtp_path)
196
+ assertPathAllowed(input.smtp_path, config.allowedPaths);
197
+ if (input.project_cache_path)
198
+ assertPathAllowed(input.project_cache_path, config.allowedPaths);
201
199
  const xmlContent = buildAntXml(input);
202
200
  const filePath = input.output_path ? path.resolve(input.output_path) : undefined;
203
201
  let written = false;
@@ -210,7 +208,7 @@ export function registerAntGenerate(server, config) {
210
208
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
211
209
  fs.writeFileSync(filePath, xmlContent, 'utf-8');
212
210
  written = true;
213
- log('info', 'provar.ant.generate: wrote file', { requestId, filePath });
211
+ log('info', 'provar_ant_generate: wrote file', { requestId, filePath });
214
212
  }
215
213
  const result = {
216
214
  requestId,
@@ -226,31 +224,29 @@ export function registerAntGenerate(server, config) {
226
224
  }
227
225
  catch (err) {
228
226
  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 });
227
+ const errResult = makeError(error instanceof PathPolicyError ? error.code : error.code ?? 'GENERATE_ERROR', error.message, requestId, false);
228
+ log('error', 'provar_ant_generate failed', { requestId, error: error.message });
231
229
  return { isError: true, content: [{ type: 'text', text: JSON.stringify(errResult) }] };
232
230
  }
233
231
  });
234
232
  }
235
233
  // ── Validate tool ─────────────────────────────────────────────────────────────
236
234
  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'),
235
+ server.registerTool('provar_ant_validate', {
236
+ title: 'Validate ANT Build File',
237
+ description: [
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
+ inputSchema: {
244
+ content: z.string().optional().describe('XML content to validate directly'),
245
+ file_path: z.string().optional().describe('Path to the build.xml file to validate'),
246
+ },
251
247
  }, ({ content, file_path }) => {
252
248
  const requestId = makeRequestId();
253
- log('info', 'provar.ant.validate', { requestId, has_content: !!content, file_path });
249
+ log('info', 'provar_ant_validate', { requestId, has_content: !!content, file_path });
254
250
  try {
255
251
  let source = content;
256
252
  if (!source && file_path) {
@@ -276,7 +272,7 @@ export function registerAntValidate(server, config) {
276
272
  catch (err) {
277
273
  const error = err;
278
274
  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 });
275
+ log('error', 'provar_ant_validate failed', { requestId, error: error.message });
280
276
  return { isError: true, content: [{ type: 'text', text: JSON.stringify(errResult) }] };
281
277
  }
282
278
  });
@@ -393,13 +389,11 @@ function escapeXmlAttr(value) {
393
389
  return value
394
390
  .replace(/&/g, '&amp;')
395
391
  .replace(/"/g, '&quot;')
392
+ .replace(/'/g, '&apos;')
396
393
  .replace(/</g, '&lt;')
397
394
  .replace(/>/g, '&gt;');
398
395
  }
399
- const REQUIRED_TASKDEF_CLASSNAMES = [
400
- 'com.provar.testrunner.ant.CompileTask',
401
- 'com.provar.testrunner.ant.RunnerTask',
402
- ];
396
+ const REQUIRED_TASKDEF_CLASSNAMES = ['com.provar.testrunner.ant.CompileTask', 'com.provar.testrunner.ant.RunnerTask'];
403
397
  const VALID_BROWSERS = ['Chrome', 'Chrome_Headless', 'Firefox', 'Edge', 'Edge_Legacy', 'Safari', 'IE'];
404
398
  const VALID_CACHE = ['Reuse', 'Refresh', 'Reload'];
405
399
  const VALID_OUTPUT_LEVELS = ['BASIC', 'WARNING', 'DEBUG'];
@@ -456,19 +450,43 @@ function validateProjectStructure(project, defaultTarget, issues) {
456
450
  }
457
451
  function validateRtcEnumAttrs(rtc, webBrowser, issues) {
458
452
  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(', ')}.` });
453
+ issues.push({
454
+ rule_id: 'ANT_030',
455
+ severity: 'WARNING',
456
+ message: `webBrowser "${webBrowser}" is not a recognised value. Expected one of: ${VALID_BROWSERS.join(', ')}.`,
457
+ applies_to: 'Run-Test-Case',
458
+ suggestion: `Use one of the supported browser values: ${VALID_BROWSERS.join(', ')}.`,
459
+ });
460
460
  }
461
461
  const metadataCache = rtc['@_salesforceMetadataCache'];
462
462
  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(', ')}.` });
463
+ issues.push({
464
+ rule_id: 'ANT_031',
465
+ severity: 'WARNING',
466
+ message: `salesforceMetadataCache "${metadataCache}" is not a recognised value. Expected one of: ${VALID_CACHE.join(', ')}.`,
467
+ applies_to: 'Run-Test-Case',
468
+ suggestion: `Use one of: ${VALID_CACHE.join(', ')}.`,
469
+ });
464
470
  }
465
471
  const testOutputLevel = rtc['@_testOutputlevel'];
466
472
  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(', ')}.` });
473
+ issues.push({
474
+ rule_id: 'ANT_032',
475
+ severity: 'WARNING',
476
+ message: `testOutputlevel "${testOutputLevel}" is not a recognised value. Expected one of: ${VALID_OUTPUT_LEVELS.join(', ')}.`,
477
+ applies_to: 'Run-Test-Case',
478
+ suggestion: `Use one of: ${VALID_OUTPUT_LEVELS.join(', ')}.`,
479
+ });
468
480
  }
469
481
  const disposition = rtc['@_resultsPathDisposition'];
470
482
  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(', ')}.` });
483
+ issues.push({
484
+ rule_id: 'ANT_033',
485
+ severity: 'WARNING',
486
+ message: `resultsPathDisposition "${disposition}" is not a recognised value. Expected one of: ${VALID_DISPOSITIONS.join(', ')}.`,
487
+ applies_to: 'Run-Test-Case',
488
+ suggestion: `Use one of: ${VALID_DISPOSITIONS.join(', ')}.`,
489
+ });
472
490
  }
473
491
  }
474
492
  function validateRunTestCase(rtc, issues) {
@@ -478,22 +496,52 @@ function validateRunTestCase(rtc, issues) {
478
496
  const webBrowser = rtc['@_webBrowser'] ?? null;
479
497
  const testEnvironment = rtc['@_testEnvironment'] ?? null;
480
498
  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>.' });
499
+ issues.push({
500
+ rule_id: 'ANT_021',
501
+ severity: 'ERROR',
502
+ message: '<Run-Test-Case> missing required "provarHome" attribute.',
503
+ applies_to: 'Run-Test-Case',
504
+ suggestion: 'Add provarHome="${provar.home}" to <Run-Test-Case>.',
505
+ });
482
506
  }
483
507
  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>.' });
508
+ issues.push({
509
+ rule_id: 'ANT_022',
510
+ severity: 'ERROR',
511
+ message: '<Run-Test-Case> missing required "projectPath" attribute.',
512
+ applies_to: 'Run-Test-Case',
513
+ suggestion: 'Add projectPath="${testproject.home}" to <Run-Test-Case>.',
514
+ });
485
515
  }
486
516
  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>.' });
517
+ issues.push({
518
+ rule_id: 'ANT_023',
519
+ severity: 'ERROR',
520
+ message: '<Run-Test-Case> missing required "resultsPath" attribute.',
521
+ applies_to: 'Run-Test-Case',
522
+ suggestion: 'Add resultsPath="${testproject.results}" to <Run-Test-Case>.',
523
+ });
488
524
  }
489
525
  validateRtcEnumAttrs(rtc, webBrowser, issues);
490
526
  const filesets = rtc['fileset'] ?? [];
491
527
  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.' });
528
+ issues.push({
529
+ rule_id: 'ANT_040',
530
+ severity: 'ERROR',
531
+ message: '<Run-Test-Case> has no <fileset> children — no tests will be selected.',
532
+ applies_to: 'Run-Test-Case',
533
+ suggestion: 'Add at least one <fileset dir="..."/> pointing to your tests or plans folder.',
534
+ });
493
535
  }
494
536
  for (const [i, fsEntry] of filesets.entries()) {
495
537
  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.' });
538
+ issues.push({
539
+ rule_id: 'ANT_041',
540
+ severity: 'ERROR',
541
+ message: `<fileset> at index ${i} is missing required "dir" attribute.`,
542
+ applies_to: 'fileset',
543
+ suggestion: 'Add dir="..." to each <fileset> element.',
544
+ });
497
545
  }
498
546
  }
499
547
  return { provarHome, projectPath, resultsPath, webBrowser, testEnvironment, filesetCount: filesets.length };
@@ -594,6 +642,135 @@ function finalizeAnt(issues, provarHome, projectPath, resultsPath, webBrowser, t
594
642
  issues,
595
643
  };
596
644
  }
645
+ function extractFailureText(el) {
646
+ if (!el)
647
+ return undefined;
648
+ if (typeof el === 'string')
649
+ return el.trim() || undefined;
650
+ if (typeof el === 'object') {
651
+ const obj = el;
652
+ // Prefer CDATA body ('#text') — it has the specific error. Fall back to 'message' attribute.
653
+ const body = obj['#text']?.trim();
654
+ const msg = obj['message']?.trim();
655
+ if (body && msg && body !== msg)
656
+ return `${msg}: ${body}`;
657
+ return body ?? msg;
658
+ }
659
+ return undefined;
660
+ }
661
+ function extractStepsFromJUnit(parsed) {
662
+ const steps = [];
663
+ let idx = 0;
664
+ // Normalise to array of suites — handles both <testsuites> and bare <testsuite>
665
+ let suites = [];
666
+ if (parsed['testsuites']) {
667
+ const inner = parsed['testsuites']['testsuite'];
668
+ suites = Array.isArray(inner)
669
+ ? inner
670
+ : inner
671
+ ? [inner]
672
+ : [];
673
+ }
674
+ else if (parsed['testsuite']) {
675
+ const ts = parsed['testsuite'];
676
+ suites = Array.isArray(ts) ? ts : [ts];
677
+ }
678
+ for (const suite of suites) {
679
+ const rawTc = suite['testcase'];
680
+ if (!rawTc)
681
+ continue;
682
+ const testcases = Array.isArray(rawTc)
683
+ ? rawTc
684
+ : [rawTc];
685
+ for (const tc of testcases) {
686
+ idx++;
687
+ // Provar JUnit: name = test case file name (no attribute prefix since attributeNamePrefix: '')
688
+ const title = tc['name'] ?? `Test ${idx}`;
689
+ const hasFailure = 'failure' in tc || 'error' in tc;
690
+ const hasSkipped = 'skipped' in tc;
691
+ let status = 'pass';
692
+ if (hasFailure)
693
+ status = 'fail';
694
+ else if (hasSkipped)
695
+ status = 'skip';
696
+ const errorMessage = extractFailureText(tc['failure'] ?? tc['error']);
697
+ const step = { testItemId: String(idx), title, status };
698
+ if (errorMessage)
699
+ step.errorMessage = errorMessage;
700
+ steps.push(step);
701
+ }
702
+ }
703
+ return steps;
704
+ }
705
+ function findXmlFiles(dir) {
706
+ const results = [];
707
+ try {
708
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
709
+ if (entry.isFile() && entry.name.endsWith('.xml') && !entry.name.startsWith('.')) {
710
+ results.push(path.join(dir, entry.name));
711
+ }
712
+ }
713
+ }
714
+ catch {
715
+ // ignore unreadable dirs
716
+ }
717
+ return results;
718
+ }
719
+ /**
720
+ * Scan a Provar results directory for JUnit XML files and return structured step results.
721
+ * Returns an empty steps array (+ optional warning) when no XML is found or parsing fails.
722
+ */
723
+ export function parseJUnitResults(resultsDir) {
724
+ if (!fs.existsSync(resultsDir)) {
725
+ return { steps: [], warning: `Results directory not found: ${resultsDir}` };
726
+ }
727
+ const xmlFiles = findXmlFiles(resultsDir);
728
+ if (xmlFiles.length === 0) {
729
+ return {
730
+ steps: [],
731
+ warning: 'No JUnit XML files found in results directory — structured step output unavailable.',
732
+ };
733
+ }
734
+ const parser = new XMLParser({
735
+ ignoreAttributes: false,
736
+ attributeNamePrefix: '',
737
+ textNodeName: '#text',
738
+ allowBooleanAttributes: true,
739
+ parseAttributeValue: false,
740
+ isArray: (tagName) => ['testsuite', 'testcase'].includes(tagName),
741
+ });
742
+ const allSteps = [];
743
+ let parsedAny = false;
744
+ let parseFailures = 0;
745
+ for (const xmlFile of xmlFiles) {
746
+ try {
747
+ const content = fs.readFileSync(xmlFile, 'utf-8');
748
+ const parsed = parser.parse(content);
749
+ const steps = extractStepsFromJUnit(parsed);
750
+ allSteps.push(...steps);
751
+ parsedAny = true;
752
+ }
753
+ catch {
754
+ parseFailures++;
755
+ }
756
+ }
757
+ if (!parsedAny) {
758
+ return {
759
+ steps: [],
760
+ warning: 'JUnit XML files found but could not be parsed — structured step output unavailable.',
761
+ };
762
+ }
763
+ if (allSteps.length === 0) {
764
+ return {
765
+ steps: [],
766
+ warning: 'JUnit XML found but no test steps could be extracted — files may not be standard JUnit format.',
767
+ };
768
+ }
769
+ const warning = parseFailures > 0
770
+ ? `${parseFailures} JUnit XML file(s) could not be parsed — step data may be incomplete.`
771
+ : undefined;
772
+ return { steps: allSteps, warning };
773
+ }
597
774
  // ── Registration ──────────────────────────────────────────────────────────────
598
775
  export function registerAllAntTools(server, config) {
599
776
  registerAntGenerate(server, config);