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