@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.
- package/README.md +163 -13
- package/bin/mcp-start.js +74 -0
- package/lib/commands/provar/auth/clear.d.ts +7 -0
- package/lib/commands/provar/auth/clear.js +36 -0
- package/lib/commands/provar/auth/clear.js.map +1 -0
- package/lib/commands/provar/auth/login.d.ts +10 -0
- package/lib/commands/provar/auth/login.js +90 -0
- package/lib/commands/provar/auth/login.js.map +1 -0
- package/lib/commands/provar/auth/rotate.d.ts +7 -0
- package/lib/commands/provar/auth/rotate.js +42 -0
- package/lib/commands/provar/auth/rotate.js.map +1 -0
- package/lib/commands/provar/auth/status.d.ts +7 -0
- package/lib/commands/provar/auth/status.js +107 -0
- package/lib/commands/provar/auth/status.js.map +1 -0
- package/lib/commands/provar/mcp/start.d.ts +2 -0
- package/lib/commands/provar/mcp/start.js +14 -1
- package/lib/commands/provar/mcp/start.js.map +1 -1
- package/lib/mcp/docs/NITROX_CATALOG_SOURCE.json +6 -0
- package/lib/mcp/docs/NITROX_COMPONENT_CATALOG.md +2001 -0
- package/lib/mcp/docs/PROVAR_TEST_STEP_REFERENCE.md +1430 -0
- package/lib/mcp/docs/PROVAR_TOOL_GUIDE.md +175 -0
- package/lib/mcp/licensing/algasClient.js +14 -5
- package/lib/mcp/licensing/algasClient.js.map +1 -1
- package/lib/mcp/licensing/ideDetection.d.ts +0 -12
- package/lib/mcp/licensing/ideDetection.js +1 -73
- package/lib/mcp/licensing/ideDetection.js.map +1 -1
- package/lib/mcp/licensing/licenseCache.js +7 -1
- package/lib/mcp/licensing/licenseCache.js.map +1 -1
- package/lib/mcp/licensing/licenseValidator.d.ts +3 -3
- package/lib/mcp/licensing/licenseValidator.js +11 -4
- package/lib/mcp/licensing/licenseValidator.js.map +1 -1
- package/lib/mcp/prompts/guidePrompts.d.ts +4 -0
- package/lib/mcp/prompts/guidePrompts.js +324 -0
- package/lib/mcp/prompts/guidePrompts.js.map +1 -0
- package/lib/mcp/prompts/index.d.ts +2 -0
- package/lib/mcp/prompts/index.js +23 -0
- package/lib/mcp/prompts/index.js.map +1 -0
- package/lib/mcp/prompts/loopPrompts.d.ts +6 -0
- package/lib/mcp/prompts/loopPrompts.js +435 -0
- package/lib/mcp/prompts/loopPrompts.js.map +1 -0
- package/lib/mcp/prompts/migrationPrompts.d.ts +4 -0
- package/lib/mcp/prompts/migrationPrompts.js +207 -0
- package/lib/mcp/prompts/migrationPrompts.js.map +1 -0
- package/lib/mcp/rules/provar_best_practices_rules.json +256 -544
- package/lib/mcp/security/pathPolicy.d.ts +5 -0
- package/lib/mcp/security/pathPolicy.js +58 -3
- package/lib/mcp/security/pathPolicy.js.map +1 -1
- package/lib/mcp/server.d.ts +17 -0
- package/lib/mcp/server.js +151 -6
- package/lib/mcp/server.js.map +1 -1
- package/lib/mcp/tools/antTools.d.ts +15 -0
- package/lib/mcp/tools/antTools.js +347 -170
- package/lib/mcp/tools/antTools.js.map +1 -1
- package/lib/mcp/tools/automationTools.d.ts +18 -8
- package/lib/mcp/tools/automationTools.js +332 -176
- package/lib/mcp/tools/automationTools.js.map +1 -1
- package/lib/mcp/tools/bestPracticesEngine.js +161 -23
- package/lib/mcp/tools/bestPracticesEngine.js.map +1 -1
- package/lib/mcp/tools/connectionTools.d.ts +4 -0
- package/lib/mcp/tools/connectionTools.js +172 -0
- package/lib/mcp/tools/connectionTools.js.map +1 -0
- package/lib/mcp/tools/defectTools.d.ts +1 -1
- package/lib/mcp/tools/defectTools.js +56 -50
- package/lib/mcp/tools/defectTools.js.map +1 -1
- package/lib/mcp/tools/hierarchyValidate.d.ts +1 -1
- package/lib/mcp/tools/hierarchyValidate.js +127 -42
- package/lib/mcp/tools/hierarchyValidate.js.map +1 -1
- package/lib/mcp/tools/nitroXTools.d.ts +23 -0
- package/lib/mcp/tools/nitroXTools.js +823 -0
- package/lib/mcp/tools/nitroXTools.js.map +1 -0
- package/lib/mcp/tools/pageObjectGenerate.js +132 -57
- package/lib/mcp/tools/pageObjectGenerate.js.map +1 -1
- package/lib/mcp/tools/pageObjectValidate.js +136 -46
- package/lib/mcp/tools/pageObjectValidate.js.map +1 -1
- package/lib/mcp/tools/projectInspect.js +51 -30
- package/lib/mcp/tools/projectInspect.js.map +1 -1
- package/lib/mcp/tools/projectValidateFromPath.js +70 -49
- package/lib/mcp/tools/projectValidateFromPath.js.map +1 -1
- package/lib/mcp/tools/propertiesTools.d.ts +2 -0
- package/lib/mcp/tools/propertiesTools.js +332 -78
- package/lib/mcp/tools/propertiesTools.js.map +1 -1
- package/lib/mcp/tools/qualityHubApiTools.d.ts +3 -0
- package/lib/mcp/tools/qualityHubApiTools.js +138 -0
- package/lib/mcp/tools/qualityHubApiTools.js.map +1 -0
- package/lib/mcp/tools/qualityHubTools.js +219 -70
- package/lib/mcp/tools/qualityHubTools.js.map +1 -1
- package/lib/mcp/tools/rcaTools.d.ts +3 -2
- package/lib/mcp/tools/rcaTools.js +189 -56
- package/lib/mcp/tools/rcaTools.js.map +1 -1
- package/lib/mcp/tools/sfSpawn.d.ts +25 -3
- package/lib/mcp/tools/sfSpawn.js +154 -6
- package/lib/mcp/tools/sfSpawn.js.map +1 -1
- package/lib/mcp/tools/testCaseGenerate.js +226 -78
- package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
- package/lib/mcp/tools/testCaseStepTools.d.ts +4 -0
- package/lib/mcp/tools/testCaseStepTools.js +226 -0
- package/lib/mcp/tools/testCaseStepTools.js.map +1 -0
- package/lib/mcp/tools/testCaseValidate.d.ts +11 -0
- package/lib/mcp/tools/testCaseValidate.js +307 -46
- package/lib/mcp/tools/testCaseValidate.js.map +1 -1
- package/lib/mcp/tools/testPlanTools.d.ts +1 -0
- package/lib/mcp/tools/testPlanTools.js +299 -59
- package/lib/mcp/tools/testPlanTools.js.map +1 -1
- package/lib/mcp/tools/testPlanValidate.js +56 -18
- package/lib/mcp/tools/testPlanValidate.js.map +1 -1
- package/lib/mcp/tools/testSuiteValidate.js +37 -11
- package/lib/mcp/tools/testSuiteValidate.js.map +1 -1
- package/lib/mcp/update/updateChecker.d.ts +14 -0
- package/lib/mcp/update/updateChecker.js +228 -0
- package/lib/mcp/update/updateChecker.js.map +1 -0
- package/lib/services/auth/credentials.d.ts +21 -0
- package/lib/services/auth/credentials.js +75 -0
- package/lib/services/auth/credentials.js.map +1 -0
- package/lib/services/auth/loginFlow.d.ts +68 -0
- package/lib/services/auth/loginFlow.js +216 -0
- package/lib/services/auth/loginFlow.js.map +1 -0
- package/lib/services/projectValidation.d.ts +5 -2
- package/lib/services/projectValidation.js +83 -31
- package/lib/services/projectValidation.js.map +1 -1
- package/lib/services/qualityHub/client.d.ts +161 -0
- package/lib/services/qualityHub/client.js +226 -0
- package/lib/services/qualityHub/client.js.map +1 -0
- package/messages/sf.provar.auth.clear.md +16 -0
- package/messages/sf.provar.auth.login.md +31 -0
- package/messages/sf.provar.auth.rotate.md +23 -0
- package/messages/sf.provar.auth.status.md +16 -0
- package/messages/sf.provar.mcp.start.md +83 -48
- package/oclif.manifest.json +299 -2
- 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.
|
|
60
|
-
'Generate
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.describe('Browser provider name (e.g. "Desktop").'),
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
.optional()
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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', '
|
|
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', '
|
|
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 :
|
|
230
|
-
log('error', '
|
|
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.
|
|
238
|
-
'Validate
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
.describe('XML content to validate directly'),
|
|
247
|
-
|
|
248
|
-
|
|
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', '
|
|
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', '
|
|
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, '&')
|
|
395
391
|
.replace(/"/g, '"')
|
|
392
|
+
.replace(/'/g, ''')
|
|
396
393
|
.replace(/</g, '<')
|
|
397
394
|
.replace(/>/g, '>');
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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);
|