@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.
- package/README.md +163 -12
- 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 +187 -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 +334 -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 +18 -0
- package/lib/mcp/server.js +232 -19
- package/lib/mcp/server.js.map +1 -1
- package/lib/mcp/tools/antTools.d.ts +15 -0
- package/lib/mcp/tools/antTools.js +369 -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 +333 -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 +242 -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 +61 -50
- package/lib/mcp/tools/defectTools.js.map +1 -1
- package/lib/mcp/tools/descHelper.d.ts +5 -0
- package/lib/mcp/tools/descHelper.js +14 -0
- package/lib/mcp/tools/descHelper.js.map +1 -0
- 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 +863 -0
- package/lib/mcp/tools/nitroXTools.js.map +1 -0
- package/lib/mcp/tools/pageObjectGenerate.js +150 -57
- package/lib/mcp/tools/pageObjectGenerate.js.map +1 -1
- package/lib/mcp/tools/pageObjectValidate.js +143 -46
- package/lib/mcp/tools/pageObjectValidate.js.map +1 -1
- package/lib/mcp/tools/projectInspect.js +79 -32
- package/lib/mcp/tools/projectInspect.js.map +1 -1
- package/lib/mcp/tools/projectValidateFromPath.js +185 -58
- package/lib/mcp/tools/projectValidateFromPath.js.map +1 -1
- package/lib/mcp/tools/propertiesTools.d.ts +2 -0
- package/lib/mcp/tools/propertiesTools.js +358 -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 +139 -0
- package/lib/mcp/tools/qualityHubApiTools.js.map +1 -0
- package/lib/mcp/tools/qualityHubTools.js +292 -72
- package/lib/mcp/tools/qualityHubTools.js.map +1 -1
- package/lib/mcp/tools/rcaTools.d.ts +3 -2
- package/lib/mcp/tools/rcaTools.js +194 -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 +285 -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 +244 -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 +381 -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 +316 -59
- package/lib/mcp/tools/testPlanTools.js.map +1 -1
- package/lib/mcp/tools/testPlanValidate.js +114 -23
- package/lib/mcp/tools/testPlanValidate.js.map +1 -1
- package/lib/mcp/tools/testSuiteValidate.js +130 -15
- 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/mcp/utils/detailLevel.d.ts +9 -0
- package/lib/mcp/utils/detailLevel.js +20 -0
- package/lib/mcp/utils/detailLevel.js.map +1 -0
- package/lib/mcp/utils/fieldMask.d.ts +17 -0
- package/lib/mcp/utils/fieldMask.js +75 -0
- package/lib/mcp/utils/fieldMask.js.map +1 -0
- package/lib/mcp/utils/tokenMeta.d.ts +40 -0
- package/lib/mcp/utils/tokenMeta.js +90 -0
- package/lib/mcp/utils/tokenMeta.js.map +1 -0
- package/lib/mcp/utils/validationDiff.d.ts +57 -0
- package/lib/mcp/utils/validationDiff.js +191 -0
- package/lib/mcp/utils/validationDiff.js.map +1 -0
- package/lib/mcp/utils/validationScore.d.ts +15 -0
- package/lib/mcp/utils/validationScore.js +31 -0
- package/lib/mcp/utils/validationScore.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 +325 -28
- package/package.json +35 -12
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
/* eslint-disable camelcase */
|
|
8
8
|
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
9
10
|
import path from 'node:path';
|
|
10
11
|
import { z } from 'zod';
|
|
11
12
|
import { propertyFileContent } from '@provartesting/provardx-plugins-utils';
|
|
12
13
|
import { assertPathAllowed, PathPolicyError } from '../security/pathPolicy.js';
|
|
13
14
|
import { makeError, makeRequestId } from '../schemas/common.js';
|
|
14
15
|
import { log } from '../logging/logger.js';
|
|
16
|
+
import { desc } from './descHelper.js';
|
|
15
17
|
const TOP_REQUIRED = ['provarHome', 'projectPath', 'resultsPath', 'metadata', 'environment'];
|
|
16
18
|
const METADATA_REQUIRED = ['metadataLevel', 'cachePath'];
|
|
17
19
|
const ENV_REQUIRED = ['webBrowser', 'webBrowserConfig', 'webBrowserProviderName', 'webBrowserDeviceName'];
|
|
@@ -38,10 +40,18 @@ function validateProperties(props) {
|
|
|
38
40
|
if (meta && typeof meta === 'object') {
|
|
39
41
|
for (const f of METADATA_REQUIRED) {
|
|
40
42
|
if (!meta[f])
|
|
41
|
-
errors.push({
|
|
43
|
+
errors.push({
|
|
44
|
+
field: `metadata.${f}`,
|
|
45
|
+
message: `Required field "metadata.${f}" is missing`,
|
|
46
|
+
severity: 'error',
|
|
47
|
+
});
|
|
42
48
|
}
|
|
43
49
|
if (meta['metadataLevel'] && !VALID_METADATA_LEVELS.includes(meta['metadataLevel'])) {
|
|
44
|
-
errors.push({
|
|
50
|
+
errors.push({
|
|
51
|
+
field: 'metadata.metadataLevel',
|
|
52
|
+
message: `metadata.metadataLevel must be one of: ${VALID_METADATA_LEVELS.join(', ')}`,
|
|
53
|
+
severity: 'error',
|
|
54
|
+
});
|
|
45
55
|
}
|
|
46
56
|
}
|
|
47
57
|
// environment object
|
|
@@ -49,27 +59,58 @@ function validateProperties(props) {
|
|
|
49
59
|
if (env && typeof env === 'object') {
|
|
50
60
|
for (const f of ENV_REQUIRED) {
|
|
51
61
|
if (!env[f])
|
|
52
|
-
errors.push({
|
|
62
|
+
errors.push({
|
|
63
|
+
field: `environment.${f}`,
|
|
64
|
+
message: `Required field "environment.${f}" is missing`,
|
|
65
|
+
severity: 'error',
|
|
66
|
+
});
|
|
53
67
|
}
|
|
54
68
|
if (env['webBrowser'] && !VALID_BROWSERS.includes(env['webBrowser'])) {
|
|
55
|
-
errors.push({
|
|
69
|
+
errors.push({
|
|
70
|
+
field: 'environment.webBrowser',
|
|
71
|
+
message: `webBrowser must be one of: ${VALID_BROWSERS.join(', ')}`,
|
|
72
|
+
severity: 'error',
|
|
73
|
+
});
|
|
56
74
|
}
|
|
57
75
|
}
|
|
58
76
|
// Optional enum fields
|
|
59
|
-
if (props['resultsPathDisposition'] &&
|
|
60
|
-
|
|
77
|
+
if (props['resultsPathDisposition'] &&
|
|
78
|
+
!VALID_RESULTS_DISPOSITION.includes(props['resultsPathDisposition'])) {
|
|
79
|
+
errors.push({
|
|
80
|
+
field: 'resultsPathDisposition',
|
|
81
|
+
message: `Must be one of: ${VALID_RESULTS_DISPOSITION.join(', ')}`,
|
|
82
|
+
severity: 'error',
|
|
83
|
+
});
|
|
61
84
|
}
|
|
62
85
|
if (props['testOutputLevel'] && !VALID_OUTPUT_LEVELS.includes(props['testOutputLevel'])) {
|
|
63
|
-
errors.push({
|
|
86
|
+
errors.push({
|
|
87
|
+
field: 'testOutputLevel',
|
|
88
|
+
message: `Must be one of: ${VALID_OUTPUT_LEVELS.join(', ')}`,
|
|
89
|
+
severity: 'error',
|
|
90
|
+
});
|
|
64
91
|
}
|
|
65
92
|
if (props['pluginOutputlevel'] && !VALID_PLUGIN_LEVELS.includes(props['pluginOutputlevel'])) {
|
|
66
|
-
errors.push({
|
|
93
|
+
errors.push({
|
|
94
|
+
field: 'pluginOutputlevel',
|
|
95
|
+
message: `Must be one of: ${VALID_PLUGIN_LEVELS.join(', ')}`,
|
|
96
|
+
severity: 'error',
|
|
97
|
+
});
|
|
67
98
|
}
|
|
68
99
|
// Warn about placeholder values still in file
|
|
69
|
-
const placeholders = [
|
|
100
|
+
const placeholders = [
|
|
101
|
+
'${PROVAR_HOME}',
|
|
102
|
+
'${PROVAR_PROJECT_PATH}',
|
|
103
|
+
'${PROVAR_RESULTS_PATH}',
|
|
104
|
+
'${PROVAR_TEST_ENVIRONMENT}',
|
|
105
|
+
'${PROVAR_TEST_PROJECT_SECRETS}',
|
|
106
|
+
];
|
|
70
107
|
for (const [key, value] of Object.entries(props)) {
|
|
71
108
|
if (typeof value === 'string' && placeholders.includes(value)) {
|
|
72
|
-
errors.push({
|
|
109
|
+
errors.push({
|
|
110
|
+
field: key,
|
|
111
|
+
message: `Field "${key}" still contains placeholder value "${value}" — replace with actual path or use an environment variable`,
|
|
112
|
+
severity: 'warning',
|
|
113
|
+
});
|
|
73
114
|
}
|
|
74
115
|
}
|
|
75
116
|
return errors;
|
|
@@ -78,8 +119,12 @@ function validateProperties(props) {
|
|
|
78
119
|
function deepMerge(target, source) {
|
|
79
120
|
const result = { ...target };
|
|
80
121
|
for (const [key, val] of Object.entries(source)) {
|
|
81
|
-
if (val !== null &&
|
|
82
|
-
|
|
122
|
+
if (val !== null &&
|
|
123
|
+
typeof val === 'object' &&
|
|
124
|
+
!Array.isArray(val) &&
|
|
125
|
+
result[key] !== null &&
|
|
126
|
+
typeof result[key] === 'object' &&
|
|
127
|
+
!Array.isArray(result[key])) {
|
|
83
128
|
result[key] = deepMerge(result[key], val);
|
|
84
129
|
}
|
|
85
130
|
else {
|
|
@@ -88,31 +133,70 @@ function deepMerge(target, source) {
|
|
|
88
133
|
}
|
|
89
134
|
return result;
|
|
90
135
|
}
|
|
91
|
-
// ──
|
|
136
|
+
// ── provar_properties_generate ────────────────────────────────────────────────
|
|
92
137
|
export function registerPropertiesGenerate(server, config) {
|
|
93
|
-
server.
|
|
94
|
-
'Generate
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
138
|
+
server.registerTool('provar_properties_generate', {
|
|
139
|
+
title: 'Generate ProvarDX Properties File',
|
|
140
|
+
description: desc([
|
|
141
|
+
'Generate a provardx-properties.json file from the standard template.',
|
|
142
|
+
'Optionally pre-fills projectPath and provarHome if provided.',
|
|
143
|
+
'The generated file uses ${PLACEHOLDER} values that must be replaced before running tests.',
|
|
144
|
+
'Use provar_properties_set afterwards to update specific fields.',
|
|
145
|
+
].join(' '), 'Generate a provardx-properties.json from the standard template.'),
|
|
146
|
+
inputSchema: {
|
|
147
|
+
output_path: z
|
|
148
|
+
.string()
|
|
149
|
+
.describe(desc('Where to write the file (e.g. /path/to/project/provardx-properties.json)', 'string, absolute path for output .json file')),
|
|
150
|
+
project_path: z
|
|
151
|
+
.string()
|
|
152
|
+
.optional()
|
|
153
|
+
.describe(desc('Pre-fill the projectPath field with this value', 'string, optional; pre-fill projectPath')),
|
|
154
|
+
provar_home: z
|
|
155
|
+
.string()
|
|
156
|
+
.optional()
|
|
157
|
+
.describe(desc('Pre-fill the provarHome field with this value', 'string, optional; pre-fill provarHome')),
|
|
158
|
+
results_path: z
|
|
159
|
+
.string()
|
|
160
|
+
.optional()
|
|
161
|
+
.describe(desc('Pre-fill the resultsPath field with this value', 'string, optional; pre-fill resultsPath')),
|
|
162
|
+
overwrite: z
|
|
163
|
+
.boolean()
|
|
164
|
+
.optional()
|
|
165
|
+
.default(false)
|
|
166
|
+
.describe(desc('Overwrite the file if it already exists (default: false)', 'bool, optional; overwrite if exists')),
|
|
167
|
+
dry_run: z
|
|
168
|
+
.boolean()
|
|
169
|
+
.optional()
|
|
170
|
+
.default(false)
|
|
171
|
+
.describe(desc('Return the content without writing (default: false)', 'bool, optional; default false, skip write')),
|
|
172
|
+
},
|
|
105
173
|
}, ({ output_path, project_path, provar_home, results_path, overwrite, dry_run }) => {
|
|
106
174
|
const requestId = makeRequestId();
|
|
107
|
-
log('info', '
|
|
175
|
+
log('info', 'provar_properties_generate', { requestId, output_path });
|
|
108
176
|
try {
|
|
109
177
|
assertPathAllowed(output_path, config.allowedPaths);
|
|
110
178
|
const resolved = path.resolve(output_path);
|
|
111
179
|
if (!overwrite && fs.existsSync(resolved)) {
|
|
112
|
-
return {
|
|
180
|
+
return {
|
|
181
|
+
isError: true,
|
|
182
|
+
content: [
|
|
183
|
+
{
|
|
184
|
+
type: 'text',
|
|
185
|
+
text: JSON.stringify(makeError('FILE_EXISTS', `File already exists: ${resolved}. Set overwrite: true to replace it.`, requestId)),
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
};
|
|
113
189
|
}
|
|
114
190
|
if (!resolved.endsWith('.json')) {
|
|
115
|
-
return {
|
|
191
|
+
return {
|
|
192
|
+
isError: true,
|
|
193
|
+
content: [
|
|
194
|
+
{
|
|
195
|
+
type: 'text',
|
|
196
|
+
text: JSON.stringify(makeError('INVALID_PATH', 'output_path must end with .json', requestId)),
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
};
|
|
116
200
|
}
|
|
117
201
|
// Start from the template and apply any provided overrides
|
|
118
202
|
const content = { ...propertyFileContent };
|
|
@@ -128,9 +212,16 @@ export function registerPropertiesGenerate(server, config) {
|
|
|
128
212
|
fs.writeFileSync(resolved, json, 'utf-8');
|
|
129
213
|
}
|
|
130
214
|
const nextSteps = dry_run
|
|
131
|
-
? 'Review the content, write to disk, then run
|
|
132
|
-
: `Run
|
|
133
|
-
const response = {
|
|
215
|
+
? 'Review the content, write to disk, then run provar_automation_config_load to register this file before compiling or running tests.'
|
|
216
|
+
: `Run provar_automation_config_load with properties_path "${resolved}" to register this configuration. Required before provar_automation_compile or provar_automation_testrun will work.`;
|
|
217
|
+
const response = {
|
|
218
|
+
requestId,
|
|
219
|
+
file_path: resolved,
|
|
220
|
+
written: !dry_run,
|
|
221
|
+
dry_run: dry_run ?? false,
|
|
222
|
+
content,
|
|
223
|
+
next_steps: nextSteps,
|
|
224
|
+
};
|
|
134
225
|
return {
|
|
135
226
|
content: [{ type: 'text', text: JSON.stringify(response) }],
|
|
136
227
|
structuredContent: response,
|
|
@@ -138,22 +229,83 @@ export function registerPropertiesGenerate(server, config) {
|
|
|
138
229
|
}
|
|
139
230
|
catch (err) {
|
|
140
231
|
const error = err;
|
|
141
|
-
return {
|
|
232
|
+
return {
|
|
233
|
+
isError: true,
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: 'text',
|
|
237
|
+
text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : error.code ?? 'GENERATE_ERROR', error.message, requestId)),
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
};
|
|
142
241
|
}
|
|
143
242
|
});
|
|
144
243
|
}
|
|
145
|
-
// ──
|
|
244
|
+
// ── Runtime divergence detection ──────────────────────────────────────────────
|
|
245
|
+
// Overrideable in tests so we don't touch the real ~/.sf directory
|
|
246
|
+
let sfConfigDirOverride = null;
|
|
247
|
+
/** Exposed for testing only — override the directory that contains config.json. Pass null to reset. */
|
|
248
|
+
export function setSfConfigDirForTesting(dir) {
|
|
249
|
+
sfConfigDirOverride = dir;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Returns the properties file path registered via `sf provar automation config load`,
|
|
253
|
+
* or null if the sf config cannot be read.
|
|
254
|
+
*/
|
|
255
|
+
function readActivePropertiesPath() {
|
|
256
|
+
try {
|
|
257
|
+
const sfDir = sfConfigDirOverride ?? path.join(os.homedir(), '.sf');
|
|
258
|
+
const sfConfigPath = path.join(sfDir, 'config.json');
|
|
259
|
+
if (!fs.existsSync(sfConfigPath))
|
|
260
|
+
return null;
|
|
261
|
+
const sfConfig = JSON.parse(fs.readFileSync(sfConfigPath, 'utf-8'));
|
|
262
|
+
return sfConfig['PROVARDX_PROPERTIES_FILE_PATH'] ?? null;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Compare two properties objects on the keys most likely to cause silent bugs.
|
|
270
|
+
* Returns a human-readable description of any divergent keys, or null if all match.
|
|
271
|
+
*/
|
|
272
|
+
function buildDivergenceWarning(diskPath, diskContent, activePath, activeContent) {
|
|
273
|
+
const KEY_FIELDS = ['provarHome', 'projectPath', 'resultsPath'];
|
|
274
|
+
const divergent = KEY_FIELDS.filter((k) => JSON.stringify(diskContent[k]) !== JSON.stringify(activeContent[k]));
|
|
275
|
+
if (divergent.length === 0)
|
|
276
|
+
return null;
|
|
277
|
+
const details = divergent
|
|
278
|
+
.map((k) => `${k}: disk="${String(diskContent[k])}" vs active="${String(activeContent[k])}"`)
|
|
279
|
+
.join(', ');
|
|
280
|
+
return (`The file you read (${diskPath}) differs from the active sf config (${activePath}) on: ${details}. ` +
|
|
281
|
+
'Test runs use the active config values — run provar_automation_config_load with the correct file to sync.');
|
|
282
|
+
}
|
|
283
|
+
// ── provar_properties_read ────────────────────────────────────────────────────
|
|
146
284
|
export function registerPropertiesRead(server, config) {
|
|
147
|
-
server.
|
|
148
|
-
|
|
285
|
+
server.registerTool('provar_properties_read', {
|
|
286
|
+
title: 'Read Properties File',
|
|
287
|
+
description: desc('Read and parse a provardx-properties.json file. Returns the parsed content so you can inspect current settings before making changes with provar_properties_set.', 'Read and parse a provardx-properties.json file.'),
|
|
288
|
+
inputSchema: {
|
|
289
|
+
file_path: z
|
|
290
|
+
.string()
|
|
291
|
+
.describe(desc('Path to the provardx-properties.json file', 'string, absolute path to provardx-properties.json')),
|
|
292
|
+
},
|
|
149
293
|
}, ({ file_path }) => {
|
|
150
294
|
const requestId = makeRequestId();
|
|
151
|
-
log('info', '
|
|
295
|
+
log('info', 'provar_properties_read', { requestId, file_path });
|
|
152
296
|
try {
|
|
153
297
|
assertPathAllowed(file_path, config.allowedPaths);
|
|
154
298
|
const resolved = path.resolve(file_path);
|
|
155
299
|
if (!fs.existsSync(resolved)) {
|
|
156
|
-
return {
|
|
300
|
+
return {
|
|
301
|
+
isError: true,
|
|
302
|
+
content: [
|
|
303
|
+
{
|
|
304
|
+
type: 'text',
|
|
305
|
+
text: JSON.stringify(makeError('PROPERTIES_FILE_NOT_FOUND', `Properties file not found: ${resolved}. Use provar_properties_generate to create it.`, requestId)),
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
};
|
|
157
309
|
}
|
|
158
310
|
const raw = fs.readFileSync(resolved, 'utf-8');
|
|
159
311
|
let parsed;
|
|
@@ -161,9 +313,34 @@ export function registerPropertiesRead(server, config) {
|
|
|
161
313
|
parsed = JSON.parse(raw);
|
|
162
314
|
}
|
|
163
315
|
catch {
|
|
164
|
-
return {
|
|
316
|
+
return {
|
|
317
|
+
isError: true,
|
|
318
|
+
content: [
|
|
319
|
+
{
|
|
320
|
+
type: 'text',
|
|
321
|
+
text: JSON.stringify(makeError('MALFORMED_JSON', 'File is not valid JSON', requestId)),
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
// Check whether the file being read matches what's registered as active in the sf config.
|
|
327
|
+
// If they differ on critical fields, surface a warning so the agent doesn't silently use stale values.
|
|
328
|
+
let divergenceWarning;
|
|
329
|
+
const activePath = readActivePropertiesPath();
|
|
330
|
+
if (activePath && path.resolve(activePath) !== resolved) {
|
|
331
|
+
try {
|
|
332
|
+
// Guard: only read the active file if it is within the session's allowed paths.
|
|
333
|
+
assertPathAllowed(activePath, config.allowedPaths);
|
|
334
|
+
const activeContent = JSON.parse(fs.readFileSync(activePath, 'utf-8'));
|
|
335
|
+
divergenceWarning = buildDivergenceWarning(resolved, parsed, activePath, activeContent) ?? undefined;
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
// Ignore — active path may be outside allowed paths or unreadable
|
|
339
|
+
}
|
|
165
340
|
}
|
|
166
341
|
const response = { requestId, file_path: resolved, content: parsed };
|
|
342
|
+
if (divergenceWarning)
|
|
343
|
+
response['details'] = { warning: divergenceWarning };
|
|
167
344
|
return {
|
|
168
345
|
content: [{ type: 'text', text: JSON.stringify(response) }],
|
|
169
346
|
structuredContent: response,
|
|
@@ -171,64 +348,118 @@ export function registerPropertiesRead(server, config) {
|
|
|
171
348
|
}
|
|
172
349
|
catch (err) {
|
|
173
350
|
const error = err;
|
|
174
|
-
return {
|
|
351
|
+
return {
|
|
352
|
+
isError: true,
|
|
353
|
+
content: [
|
|
354
|
+
{
|
|
355
|
+
type: 'text',
|
|
356
|
+
text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : error.code ?? 'READ_ERROR', error.message, requestId)),
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
};
|
|
175
360
|
}
|
|
176
361
|
});
|
|
177
362
|
}
|
|
178
|
-
// ──
|
|
179
|
-
const updatesSchema = z
|
|
363
|
+
// ── provar_properties_set ─────────────────────────────────────────────────────
|
|
364
|
+
const updatesSchema = z
|
|
365
|
+
.object({
|
|
180
366
|
provarHome: z.string().optional().describe('Path to Provar installation directory'),
|
|
181
367
|
projectPath: z.string().optional().describe('Path to the Provar test project root'),
|
|
182
368
|
resultsPath: z.string().optional().describe('Path where test results will be written'),
|
|
183
|
-
resultsPathDisposition: z
|
|
369
|
+
resultsPathDisposition: z
|
|
370
|
+
.enum(['Increment', 'Replace', 'Fail'])
|
|
371
|
+
.optional()
|
|
372
|
+
.describe('What to do if results path already exists'),
|
|
184
373
|
testOutputLevel: z.enum(['BASIC', 'DETAILED', 'DIAGNOSTIC']).optional().describe('Amount of test output logged'),
|
|
185
|
-
pluginOutputlevel: z
|
|
374
|
+
pluginOutputlevel: z
|
|
375
|
+
.enum(['SEVERE', 'WARNING', 'INFO', 'FINE', 'FINER', 'FINEST'])
|
|
376
|
+
.optional()
|
|
377
|
+
.describe('Amount of plugin output logged'),
|
|
186
378
|
stopOnError: z.boolean().optional().describe('Abort test run on first failure'),
|
|
187
379
|
excludeCallable: z.boolean().optional().describe('Omit callable test cases from execution'),
|
|
188
|
-
testprojectSecrets: z
|
|
189
|
-
|
|
380
|
+
testprojectSecrets: z
|
|
381
|
+
.string()
|
|
382
|
+
.optional()
|
|
383
|
+
.describe('Encryption key (password string) used to decrypt the .secrets file in the Provar project root. ' +
|
|
384
|
+
'This is the key itself — NOT a file path. Omit this field unless your project uses secrets encryption.'),
|
|
385
|
+
environment: z
|
|
386
|
+
.object({
|
|
190
387
|
testEnvironment: z.string().optional().describe('Name of the test environment to run against'),
|
|
191
388
|
webBrowser: z.enum(['Chrome', 'Safari', 'Edge', 'Edge_Legacy', 'Firefox', 'IE', 'Chrome_Headless']).optional(),
|
|
192
389
|
webBrowserConfig: z.string().optional(),
|
|
193
390
|
webBrowserProviderName: z.string().optional(),
|
|
194
391
|
webBrowserDeviceName: z.string().optional(),
|
|
195
|
-
})
|
|
196
|
-
|
|
392
|
+
})
|
|
393
|
+
.optional()
|
|
394
|
+
.describe('Test execution environment settings'),
|
|
395
|
+
metadata: z
|
|
396
|
+
.object({
|
|
197
397
|
metadataLevel: z.enum(['Reuse', 'Reload', 'Refresh']).optional().describe('Salesforce metadata cache strategy'),
|
|
198
398
|
cachePath: z.string().optional().describe('Path for the metadata cache'),
|
|
199
|
-
})
|
|
200
|
-
|
|
399
|
+
})
|
|
400
|
+
.optional()
|
|
401
|
+
.describe('Salesforce metadata settings'),
|
|
402
|
+
testCase: z
|
|
403
|
+
.array(z.string())
|
|
404
|
+
.optional()
|
|
405
|
+
.describe('Specific test case file paths to run (relative to projectPath/tests/). NOTE: <dataTable> data-driven iteration does NOT work in this mode — data table variables resolve as null. To run data-driven tests, add the test case to a plan with provar_testplan_add-instance and run via testPlan instead.'),
|
|
201
406
|
testPlan: z.array(z.string()).optional().describe('Test plan names to run (wildcards permitted)'),
|
|
202
|
-
connectionOverride: z
|
|
407
|
+
connectionOverride: z
|
|
408
|
+
.array(z.object({
|
|
203
409
|
connection: z.string().describe('Provar connection name'),
|
|
204
410
|
username: z.string().describe('SFDX username or alias to substitute'),
|
|
205
|
-
}))
|
|
206
|
-
|
|
411
|
+
}))
|
|
412
|
+
.optional()
|
|
413
|
+
.describe('Override Provar connections with SFDX usernames'),
|
|
414
|
+
})
|
|
415
|
+
.describe('Fields to update in the properties file — only provided fields are changed');
|
|
207
416
|
export function registerPropertiesSet(server, config) {
|
|
208
|
-
server.
|
|
209
|
-
'
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
417
|
+
server.registerTool('provar_properties_set', {
|
|
418
|
+
title: 'Set Property Value',
|
|
419
|
+
description: desc([
|
|
420
|
+
'Update one or more fields in a provardx-properties.json file.',
|
|
421
|
+
'Only the provided fields are changed — all other fields are preserved.',
|
|
422
|
+
'Object fields (environment, metadata) are deep-merged.',
|
|
423
|
+
'Array fields (testCase, testPlan, connectionOverride) replace the existing value entirely.',
|
|
424
|
+
'Use provar_properties_read first to inspect the current state.',
|
|
425
|
+
].join(' '), 'Update fields in a provardx-properties.json; other fields preserved.'),
|
|
426
|
+
inputSchema: {
|
|
427
|
+
file_path: z
|
|
428
|
+
.string()
|
|
429
|
+
.describe(desc('Path to the provardx-properties.json file to update', 'string, absolute path to provardx-properties.json')),
|
|
430
|
+
updates: updatesSchema,
|
|
431
|
+
},
|
|
217
432
|
}, ({ file_path, updates }) => {
|
|
218
433
|
const requestId = makeRequestId();
|
|
219
|
-
log('info', '
|
|
434
|
+
log('info', 'provar_properties_set', { requestId, file_path });
|
|
220
435
|
try {
|
|
221
436
|
assertPathAllowed(file_path, config.allowedPaths);
|
|
222
437
|
const resolved = path.resolve(file_path);
|
|
223
438
|
if (!fs.existsSync(resolved)) {
|
|
224
|
-
return {
|
|
439
|
+
return {
|
|
440
|
+
isError: true,
|
|
441
|
+
content: [
|
|
442
|
+
{
|
|
443
|
+
type: 'text',
|
|
444
|
+
text: JSON.stringify(makeError('PROPERTIES_FILE_NOT_FOUND', `File not found: ${resolved}. Use provar_properties_generate to create it first.`, requestId)),
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
};
|
|
225
448
|
}
|
|
226
449
|
let current;
|
|
227
450
|
try {
|
|
228
451
|
current = JSON.parse(fs.readFileSync(resolved, 'utf-8'));
|
|
229
452
|
}
|
|
230
453
|
catch {
|
|
231
|
-
return {
|
|
454
|
+
return {
|
|
455
|
+
isError: true,
|
|
456
|
+
content: [
|
|
457
|
+
{
|
|
458
|
+
type: 'text',
|
|
459
|
+
text: JSON.stringify(makeError('MALFORMED_JSON', 'Existing file is not valid JSON', requestId)),
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
};
|
|
232
463
|
}
|
|
233
464
|
const updatesRecord = updates;
|
|
234
465
|
const merged = deepMerge(current, updatesRecord);
|
|
@@ -242,24 +473,50 @@ export function registerPropertiesSet(server, config) {
|
|
|
242
473
|
}
|
|
243
474
|
catch (err) {
|
|
244
475
|
const error = err;
|
|
245
|
-
return {
|
|
476
|
+
return {
|
|
477
|
+
isError: true,
|
|
478
|
+
content: [
|
|
479
|
+
{
|
|
480
|
+
type: 'text',
|
|
481
|
+
text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : error.code ?? 'SET_ERROR', error.message, requestId)),
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
};
|
|
246
485
|
}
|
|
247
486
|
});
|
|
248
487
|
}
|
|
249
|
-
// ──
|
|
488
|
+
// ── provar_properties_validate ────────────────────────────────────────────────
|
|
250
489
|
export function registerPropertiesValidate(server, config) {
|
|
251
|
-
server.
|
|
252
|
-
'Validate
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
490
|
+
server.registerTool('provar_properties_validate', {
|
|
491
|
+
title: 'Validate ProvarDX Properties File',
|
|
492
|
+
description: desc([
|
|
493
|
+
'Validate a provardx-properties.json file against the ProvarDX schema.',
|
|
494
|
+
'Checks required fields, valid enum values, and warns about unfilled placeholder values.',
|
|
495
|
+
'Accepts either a file path or inline JSON content.',
|
|
496
|
+
].join(' '), 'Validate a provardx-properties.json against required fields and enum values.'),
|
|
497
|
+
inputSchema: {
|
|
498
|
+
file_path: z
|
|
499
|
+
.string()
|
|
500
|
+
.optional()
|
|
501
|
+
.describe(desc('Path to the provardx-properties.json file to validate', 'string, optional; path to provardx-properties.json')),
|
|
502
|
+
content: z
|
|
503
|
+
.string()
|
|
504
|
+
.optional()
|
|
505
|
+
.describe(desc('Inline JSON string to validate (alternative to file_path)', 'string, optional; inline JSON to validate')),
|
|
506
|
+
},
|
|
258
507
|
}, ({ file_path, content }) => {
|
|
259
508
|
const requestId = makeRequestId();
|
|
260
|
-
log('info', '
|
|
509
|
+
log('info', 'provar_properties_validate', { requestId, file_path });
|
|
261
510
|
if (!file_path && !content) {
|
|
262
|
-
return {
|
|
511
|
+
return {
|
|
512
|
+
isError: true,
|
|
513
|
+
content: [
|
|
514
|
+
{
|
|
515
|
+
type: 'text',
|
|
516
|
+
text: JSON.stringify(makeError('MISSING_INPUT', 'Provide either file_path or content', requestId)),
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
};
|
|
263
520
|
}
|
|
264
521
|
try {
|
|
265
522
|
let rawJson;
|
|
@@ -267,7 +524,15 @@ export function registerPropertiesValidate(server, config) {
|
|
|
267
524
|
assertPathAllowed(file_path, config.allowedPaths);
|
|
268
525
|
const resolved = path.resolve(file_path);
|
|
269
526
|
if (!fs.existsSync(resolved)) {
|
|
270
|
-
return {
|
|
527
|
+
return {
|
|
528
|
+
isError: true,
|
|
529
|
+
content: [
|
|
530
|
+
{
|
|
531
|
+
type: 'text',
|
|
532
|
+
text: JSON.stringify(makeError('PROPERTIES_FILE_NOT_FOUND', `File not found: ${resolved}`, requestId)),
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
};
|
|
271
536
|
}
|
|
272
537
|
rawJson = fs.readFileSync(resolved, 'utf-8');
|
|
273
538
|
}
|
|
@@ -279,7 +544,14 @@ export function registerPropertiesValidate(server, config) {
|
|
|
279
544
|
parsed = JSON.parse(rawJson);
|
|
280
545
|
}
|
|
281
546
|
catch {
|
|
282
|
-
const response = {
|
|
547
|
+
const response = {
|
|
548
|
+
requestId,
|
|
549
|
+
is_valid: false,
|
|
550
|
+
error_count: 1,
|
|
551
|
+
warning_count: 0,
|
|
552
|
+
errors: [{ field: '(root)', message: 'File is not valid JSON', severity: 'error' }],
|
|
553
|
+
warnings: [],
|
|
554
|
+
};
|
|
283
555
|
return { content: [{ type: 'text', text: JSON.stringify(response) }], structuredContent: response };
|
|
284
556
|
}
|
|
285
557
|
const allIssues = validateProperties(parsed);
|
|
@@ -300,7 +572,15 @@ export function registerPropertiesValidate(server, config) {
|
|
|
300
572
|
}
|
|
301
573
|
catch (err) {
|
|
302
574
|
const error = err;
|
|
303
|
-
return {
|
|
575
|
+
return {
|
|
576
|
+
isError: true,
|
|
577
|
+
content: [
|
|
578
|
+
{
|
|
579
|
+
type: 'text',
|
|
580
|
+
text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : error.code ?? 'VALIDATE_ERROR', error.message, requestId)),
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
};
|
|
304
584
|
}
|
|
305
585
|
});
|
|
306
586
|
}
|