@provartesting/provardx-cli 1.5.0-dev.2 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +163 -12
  2. package/bin/mcp-start.js +74 -0
  3. package/lib/commands/provar/auth/clear.d.ts +7 -0
  4. package/lib/commands/provar/auth/clear.js +36 -0
  5. package/lib/commands/provar/auth/clear.js.map +1 -0
  6. package/lib/commands/provar/auth/login.d.ts +10 -0
  7. package/lib/commands/provar/auth/login.js +90 -0
  8. package/lib/commands/provar/auth/login.js.map +1 -0
  9. package/lib/commands/provar/auth/rotate.d.ts +7 -0
  10. package/lib/commands/provar/auth/rotate.js +42 -0
  11. package/lib/commands/provar/auth/rotate.js.map +1 -0
  12. package/lib/commands/provar/auth/status.d.ts +7 -0
  13. package/lib/commands/provar/auth/status.js +107 -0
  14. package/lib/commands/provar/auth/status.js.map +1 -0
  15. package/lib/commands/provar/mcp/start.d.ts +2 -0
  16. package/lib/commands/provar/mcp/start.js +14 -1
  17. package/lib/commands/provar/mcp/start.js.map +1 -1
  18. package/lib/mcp/docs/NITROX_CATALOG_SOURCE.json +6 -0
  19. package/lib/mcp/docs/NITROX_COMPONENT_CATALOG.md +2001 -0
  20. package/lib/mcp/docs/PROVAR_TEST_STEP_REFERENCE.md +1430 -0
  21. package/lib/mcp/docs/PROVAR_TOOL_GUIDE.md +187 -0
  22. package/lib/mcp/licensing/algasClient.js +14 -5
  23. package/lib/mcp/licensing/algasClient.js.map +1 -1
  24. package/lib/mcp/licensing/ideDetection.d.ts +0 -12
  25. package/lib/mcp/licensing/ideDetection.js +1 -73
  26. package/lib/mcp/licensing/ideDetection.js.map +1 -1
  27. package/lib/mcp/licensing/licenseCache.js +7 -1
  28. package/lib/mcp/licensing/licenseCache.js.map +1 -1
  29. package/lib/mcp/licensing/licenseValidator.d.ts +3 -3
  30. package/lib/mcp/licensing/licenseValidator.js +11 -4
  31. package/lib/mcp/licensing/licenseValidator.js.map +1 -1
  32. package/lib/mcp/prompts/guidePrompts.d.ts +4 -0
  33. package/lib/mcp/prompts/guidePrompts.js +334 -0
  34. package/lib/mcp/prompts/guidePrompts.js.map +1 -0
  35. package/lib/mcp/prompts/index.d.ts +2 -0
  36. package/lib/mcp/prompts/index.js +23 -0
  37. package/lib/mcp/prompts/index.js.map +1 -0
  38. package/lib/mcp/prompts/loopPrompts.d.ts +6 -0
  39. package/lib/mcp/prompts/loopPrompts.js +435 -0
  40. package/lib/mcp/prompts/loopPrompts.js.map +1 -0
  41. package/lib/mcp/prompts/migrationPrompts.d.ts +4 -0
  42. package/lib/mcp/prompts/migrationPrompts.js +207 -0
  43. package/lib/mcp/prompts/migrationPrompts.js.map +1 -0
  44. package/lib/mcp/rules/provar_best_practices_rules.json +256 -544
  45. package/lib/mcp/security/pathPolicy.d.ts +5 -0
  46. package/lib/mcp/security/pathPolicy.js +58 -3
  47. package/lib/mcp/security/pathPolicy.js.map +1 -1
  48. package/lib/mcp/server.d.ts +18 -0
  49. package/lib/mcp/server.js +232 -19
  50. package/lib/mcp/server.js.map +1 -1
  51. package/lib/mcp/tools/antTools.d.ts +15 -0
  52. package/lib/mcp/tools/antTools.js +369 -170
  53. package/lib/mcp/tools/antTools.js.map +1 -1
  54. package/lib/mcp/tools/automationTools.d.ts +18 -8
  55. package/lib/mcp/tools/automationTools.js +333 -176
  56. package/lib/mcp/tools/automationTools.js.map +1 -1
  57. package/lib/mcp/tools/bestPracticesEngine.js +161 -23
  58. package/lib/mcp/tools/bestPracticesEngine.js.map +1 -1
  59. package/lib/mcp/tools/connectionTools.d.ts +4 -0
  60. package/lib/mcp/tools/connectionTools.js +242 -0
  61. package/lib/mcp/tools/connectionTools.js.map +1 -0
  62. package/lib/mcp/tools/defectTools.d.ts +1 -1
  63. package/lib/mcp/tools/defectTools.js +61 -50
  64. package/lib/mcp/tools/defectTools.js.map +1 -1
  65. package/lib/mcp/tools/descHelper.d.ts +5 -0
  66. package/lib/mcp/tools/descHelper.js +14 -0
  67. package/lib/mcp/tools/descHelper.js.map +1 -0
  68. package/lib/mcp/tools/hierarchyValidate.d.ts +1 -1
  69. package/lib/mcp/tools/hierarchyValidate.js +127 -42
  70. package/lib/mcp/tools/hierarchyValidate.js.map +1 -1
  71. package/lib/mcp/tools/nitroXTools.d.ts +23 -0
  72. package/lib/mcp/tools/nitroXTools.js +863 -0
  73. package/lib/mcp/tools/nitroXTools.js.map +1 -0
  74. package/lib/mcp/tools/pageObjectGenerate.js +150 -57
  75. package/lib/mcp/tools/pageObjectGenerate.js.map +1 -1
  76. package/lib/mcp/tools/pageObjectValidate.js +143 -46
  77. package/lib/mcp/tools/pageObjectValidate.js.map +1 -1
  78. package/lib/mcp/tools/projectInspect.js +79 -32
  79. package/lib/mcp/tools/projectInspect.js.map +1 -1
  80. package/lib/mcp/tools/projectValidateFromPath.js +185 -58
  81. package/lib/mcp/tools/projectValidateFromPath.js.map +1 -1
  82. package/lib/mcp/tools/propertiesTools.d.ts +2 -0
  83. package/lib/mcp/tools/propertiesTools.js +358 -78
  84. package/lib/mcp/tools/propertiesTools.js.map +1 -1
  85. package/lib/mcp/tools/qualityHubApiTools.d.ts +3 -0
  86. package/lib/mcp/tools/qualityHubApiTools.js +139 -0
  87. package/lib/mcp/tools/qualityHubApiTools.js.map +1 -0
  88. package/lib/mcp/tools/qualityHubTools.js +292 -72
  89. package/lib/mcp/tools/qualityHubTools.js.map +1 -1
  90. package/lib/mcp/tools/rcaTools.d.ts +3 -2
  91. package/lib/mcp/tools/rcaTools.js +194 -56
  92. package/lib/mcp/tools/rcaTools.js.map +1 -1
  93. package/lib/mcp/tools/sfSpawn.d.ts +25 -3
  94. package/lib/mcp/tools/sfSpawn.js +154 -6
  95. package/lib/mcp/tools/sfSpawn.js.map +1 -1
  96. package/lib/mcp/tools/testCaseGenerate.js +285 -78
  97. package/lib/mcp/tools/testCaseGenerate.js.map +1 -1
  98. package/lib/mcp/tools/testCaseStepTools.d.ts +4 -0
  99. package/lib/mcp/tools/testCaseStepTools.js +244 -0
  100. package/lib/mcp/tools/testCaseStepTools.js.map +1 -0
  101. package/lib/mcp/tools/testCaseValidate.d.ts +11 -0
  102. package/lib/mcp/tools/testCaseValidate.js +381 -46
  103. package/lib/mcp/tools/testCaseValidate.js.map +1 -1
  104. package/lib/mcp/tools/testPlanTools.d.ts +1 -0
  105. package/lib/mcp/tools/testPlanTools.js +316 -59
  106. package/lib/mcp/tools/testPlanTools.js.map +1 -1
  107. package/lib/mcp/tools/testPlanValidate.js +114 -23
  108. package/lib/mcp/tools/testPlanValidate.js.map +1 -1
  109. package/lib/mcp/tools/testSuiteValidate.js +130 -15
  110. package/lib/mcp/tools/testSuiteValidate.js.map +1 -1
  111. package/lib/mcp/update/updateChecker.d.ts +14 -0
  112. package/lib/mcp/update/updateChecker.js +228 -0
  113. package/lib/mcp/update/updateChecker.js.map +1 -0
  114. package/lib/mcp/utils/detailLevel.d.ts +9 -0
  115. package/lib/mcp/utils/detailLevel.js +20 -0
  116. package/lib/mcp/utils/detailLevel.js.map +1 -0
  117. package/lib/mcp/utils/fieldMask.d.ts +17 -0
  118. package/lib/mcp/utils/fieldMask.js +75 -0
  119. package/lib/mcp/utils/fieldMask.js.map +1 -0
  120. package/lib/mcp/utils/tokenMeta.d.ts +40 -0
  121. package/lib/mcp/utils/tokenMeta.js +90 -0
  122. package/lib/mcp/utils/tokenMeta.js.map +1 -0
  123. package/lib/mcp/utils/validationDiff.d.ts +57 -0
  124. package/lib/mcp/utils/validationDiff.js +191 -0
  125. package/lib/mcp/utils/validationDiff.js.map +1 -0
  126. package/lib/mcp/utils/validationScore.d.ts +15 -0
  127. package/lib/mcp/utils/validationScore.js +31 -0
  128. package/lib/mcp/utils/validationScore.js.map +1 -0
  129. package/lib/services/auth/credentials.d.ts +21 -0
  130. package/lib/services/auth/credentials.js +75 -0
  131. package/lib/services/auth/credentials.js.map +1 -0
  132. package/lib/services/auth/loginFlow.d.ts +68 -0
  133. package/lib/services/auth/loginFlow.js +216 -0
  134. package/lib/services/auth/loginFlow.js.map +1 -0
  135. package/lib/services/projectValidation.d.ts +5 -2
  136. package/lib/services/projectValidation.js +83 -31
  137. package/lib/services/projectValidation.js.map +1 -1
  138. package/lib/services/qualityHub/client.d.ts +161 -0
  139. package/lib/services/qualityHub/client.js +226 -0
  140. package/lib/services/qualityHub/client.js.map +1 -0
  141. package/messages/sf.provar.auth.clear.md +16 -0
  142. package/messages/sf.provar.auth.login.md +31 -0
  143. package/messages/sf.provar.auth.rotate.md +23 -0
  144. package/messages/sf.provar.auth.status.md +16 -0
  145. package/messages/sf.provar.mcp.start.md +83 -48
  146. package/oclif.manifest.json +325 -28
  147. package/package.json +35 -12
@@ -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({ field: `metadata.${f}`, message: `Required field "metadata.${f}" is missing`, severity: 'error' });
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({ field: 'metadata.metadataLevel', message: `metadata.metadataLevel must be one of: ${VALID_METADATA_LEVELS.join(', ')}`, severity: 'error' });
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({ field: `environment.${f}`, message: `Required field "environment.${f}" is missing`, severity: 'error' });
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({ field: 'environment.webBrowser', message: `webBrowser must be one of: ${VALID_BROWSERS.join(', ')}`, severity: 'error' });
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'] && !VALID_RESULTS_DISPOSITION.includes(props['resultsPathDisposition'])) {
60
- errors.push({ field: 'resultsPathDisposition', message: `Must be one of: ${VALID_RESULTS_DISPOSITION.join(', ')}`, severity: 'error' });
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({ field: 'testOutputLevel', message: `Must be one of: ${VALID_OUTPUT_LEVELS.join(', ')}`, severity: 'error' });
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({ field: 'pluginOutputlevel', message: `Must be one of: ${VALID_PLUGIN_LEVELS.join(', ')}`, severity: 'error' });
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 = ['${PROVAR_HOME}', '${PROVAR_PROJECT_PATH}', '${PROVAR_RESULTS_PATH}', '${PROVAR_TEST_ENVIRONMENT}', '${PROVAR_TEST_PROJECT_SECRETS}'];
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({ field: key, message: `Field "${key}" still contains placeholder value "${value}" — replace with actual path or use an environment variable`, severity: 'warning' });
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 && typeof val === 'object' && !Array.isArray(val) &&
82
- result[key] !== null && typeof result[key] === 'object' && !Array.isArray(result[key])) {
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
- // ── provar.properties.generate ────────────────────────────────────────────────
136
+ // ── provar_properties_generate ────────────────────────────────────────────────
92
137
  export function registerPropertiesGenerate(server, config) {
93
- server.tool('provar.properties.generate', [
94
- 'Generate a provardx-properties.json file from the standard template.',
95
- 'Optionally pre-fills projectPath and provarHome if provided.',
96
- 'The generated file uses ${PLACEHOLDER} values that must be replaced before running tests.',
97
- 'Use provar.properties.set afterwards to update specific fields.',
98
- ].join(' '), {
99
- output_path: z.string().describe('Where to write the file (e.g. /path/to/project/provardx-properties.json)'),
100
- project_path: z.string().optional().describe('Pre-fill the projectPath field with this value'),
101
- provar_home: z.string().optional().describe('Pre-fill the provarHome field with this value'),
102
- results_path: z.string().optional().describe('Pre-fill the resultsPath field with this value'),
103
- overwrite: z.boolean().optional().default(false).describe('Overwrite the file if it already exists (default: false)'),
104
- dry_run: z.boolean().optional().default(false).describe('Return the content without writing (default: false)'),
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', 'provar.properties.generate', { requestId, output_path });
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 { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('FILE_EXISTS', `File already exists: ${resolved}. Set overwrite: true to replace it.`, requestId)) }] };
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 { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('INVALID_PATH', 'output_path must end with .json', requestId)) }] };
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 provar.automation.config.load to register this file before compiling or running tests.'
132
- : `Run provar.automation.config.load with properties_path "${resolved}" to register this configuration. Required before provar.automation.compile or provar.automation.testrun will work.`;
133
- const response = { requestId, file_path: resolved, written: !dry_run, dry_run: dry_run ?? false, content, next_steps: nextSteps };
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 { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : (error.code ?? 'GENERATE_ERROR'), error.message, requestId)) }] };
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
- // ── provar.properties.read ────────────────────────────────────────────────────
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.tool('provar.properties.read', '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.', {
148
- file_path: z.string().describe('Path to the provardx-properties.json file'),
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', 'provar.properties.read', { requestId, file_path });
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 { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('FILE_NOT_FOUND', `File not found: ${resolved}`, requestId)) }] };
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 { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('MALFORMED_JSON', 'File is not valid JSON', requestId)) }] };
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 { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : (error.code ?? 'READ_ERROR'), error.message, requestId)) }] };
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
- // ── provar.properties.set ─────────────────────────────────────────────────────
179
- const updatesSchema = z.object({
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.enum(['Increment', 'Replace', 'Fail']).optional().describe('What to do if results path already exists'),
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.enum(['SEVERE', 'WARNING', 'INFO', 'FINE', 'FINER', 'FINEST']).optional().describe('Amount of plugin output logged'),
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.string().optional().describe('Test project secrets encryption password'),
189
- environment: z.object({
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
- }).optional().describe('Test execution environment settings'),
196
- metadata: z.object({
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
- }).optional().describe('Salesforce metadata settings'),
200
- testCase: z.array(z.string()).optional().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.'),
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.array(z.object({
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
- })).optional().describe('Override Provar connections with SFDX usernames'),
206
- }).describe('Fields to update in the properties file — only provided fields are changed');
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.tool('provar.properties.set', [
209
- 'Update one or more fields in a provardx-properties.json file.',
210
- 'Only the provided fields are changed — all other fields are preserved.',
211
- 'Object fields (environment, metadata) are deep-merged.',
212
- 'Array fields (testCase, testPlan, connectionOverride) replace the existing value entirely.',
213
- 'Use provar.properties.read first to inspect the current state.',
214
- ].join(' '), {
215
- file_path: z.string().describe('Path to the provardx-properties.json file to update'),
216
- updates: updatesSchema,
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', 'provar.properties.set', { requestId, file_path });
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 { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('FILE_NOT_FOUND', `File not found: ${resolved}. Use provar.properties.generate to create it first.`, requestId)) }] };
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 { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('MALFORMED_JSON', 'Existing file is not valid JSON', requestId)) }] };
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 { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : (error.code ?? 'SET_ERROR'), error.message, requestId)) }] };
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
- // ── provar.properties.validate ────────────────────────────────────────────────
488
+ // ── provar_properties_validate ────────────────────────────────────────────────
250
489
  export function registerPropertiesValidate(server, config) {
251
- server.tool('provar.properties.validate', [
252
- 'Validate a provardx-properties.json file against the ProvarDX schema.',
253
- 'Checks required fields, valid enum values, and warns about unfilled placeholder values.',
254
- 'Accepts either a file path or inline JSON content.',
255
- ].join(' '), {
256
- file_path: z.string().optional().describe('Path to the provardx-properties.json file to validate'),
257
- content: z.string().optional().describe('Inline JSON string to validate (alternative to file_path)'),
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', 'provar.properties.validate', { requestId, file_path });
509
+ log('info', 'provar_properties_validate', { requestId, file_path });
261
510
  if (!file_path && !content) {
262
- return { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('MISSING_INPUT', 'Provide either file_path or content', requestId)) }] };
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 { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError('FILE_NOT_FOUND', `File not found: ${resolved}`, requestId)) }] };
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 = { requestId, is_valid: false, error_count: 1, warning_count: 0, errors: [{ field: '(root)', message: 'File is not valid JSON', severity: 'error' }], warnings: [] };
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 { isError: true, content: [{ type: 'text', text: JSON.stringify(makeError(error instanceof PathPolicyError ? error.code : (error.code ?? 'VALIDATE_ERROR'), error.message, requestId)) }] };
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
  }