snow-flow 1.3.22 → 1.3.24
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/dist/mcp/base-mcp-server.js +183 -15
- package/dist/mcp/servicenow-deployment-mcp.js +258 -9
- package/dist/mcp/servicenow-flow-composer-mcp.js +218 -32
- package/dist/mcp/servicenow-intelligent-mcp.js +354 -25
- package/dist/mcp/servicenow-security-compliance-mcp-refactored.js +9 -9
- package/dist/memory/memory-client.js +30 -2
- package/dist/memory/memory-operations.js +63 -11
- package/dist/memory/memory-system.js +89 -17
- package/dist/utils/servicenow-client.js +178 -48
- package/dist/utils/xml-first-flow-generator.js +24 -2
- package/dist/version.js +1 -1
- package/npm-publish-summary.txt +35 -0
- package/package.json +1 -1
|
@@ -26,6 +26,8 @@ class BaseMCPServer {
|
|
|
26
26
|
this.tools = new Map();
|
|
27
27
|
// Performance tracking
|
|
28
28
|
this.toolMetrics = new Map();
|
|
29
|
+
// 🔴 SNOW-003 FIX: Circuit breaker implementation
|
|
30
|
+
this.circuitBreakers = new Map();
|
|
29
31
|
/**
|
|
30
32
|
* Tool handlers map
|
|
31
33
|
*/
|
|
@@ -159,48 +161,214 @@ class BaseMCPServer {
|
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
163
|
/**
|
|
162
|
-
*
|
|
164
|
+
* 🔴 SNOW-003 FIX: Enhanced retry logic with intelligent backoff and circuit breaker
|
|
165
|
+
* Addresses the 19% failure rate with better retry strategies and failure prevention
|
|
163
166
|
*/
|
|
164
167
|
async executeWithRetry(toolName, args, attempt = 1) {
|
|
165
|
-
|
|
166
|
-
const
|
|
168
|
+
// 🔴 CRITICAL: Increased retries from 3 to 6 for better resilience
|
|
169
|
+
const maxRetries = 6;
|
|
170
|
+
// 🔴 CRITICAL: Intelligent backoff based on error type
|
|
171
|
+
const backoffMs = this.calculateBackoff(attempt, toolName);
|
|
172
|
+
// 🔴 CRITICAL: Dynamic timeout based on tool complexity
|
|
173
|
+
const timeout = this.calculateTimeout(toolName);
|
|
167
174
|
try {
|
|
168
175
|
// Get tool handler
|
|
169
176
|
const handler = this.getToolHandler(toolName);
|
|
170
177
|
if (!handler) {
|
|
171
178
|
throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`);
|
|
172
179
|
}
|
|
173
|
-
//
|
|
174
|
-
|
|
180
|
+
// 🔴 CRITICAL: Memory usage check before execution
|
|
181
|
+
if (attempt === 1) {
|
|
182
|
+
await this.checkMemoryUsage();
|
|
183
|
+
}
|
|
184
|
+
// Execute with dynamic timeout
|
|
175
185
|
const result = await Promise.race([
|
|
176
186
|
handler(args),
|
|
177
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(
|
|
187
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Tool execution timeout after ${timeout}ms`)), timeout)),
|
|
178
188
|
]);
|
|
189
|
+
// 🔴 SUCCESS: Reset circuit breaker on success
|
|
190
|
+
this.resetCircuitBreaker(toolName);
|
|
179
191
|
return result;
|
|
180
192
|
}
|
|
181
193
|
catch (error) {
|
|
182
|
-
this.logger.error(
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
194
|
+
this.logger.error(`🔴 Tool ${toolName} execution failed (attempt ${attempt}/${maxRetries}):`, error);
|
|
195
|
+
// 🔴 CRITICAL: Update circuit breaker
|
|
196
|
+
this.updateCircuitBreaker(toolName, error);
|
|
197
|
+
// Check if retryable and within limits
|
|
198
|
+
if (attempt < maxRetries && this.isRetryableError(error) && !this.isCircuitBreakerOpen(toolName)) {
|
|
199
|
+
this.logger.info(`🔄 Retrying ${toolName} after ${backoffMs}ms (attempt ${attempt + 1}/${maxRetries})...`);
|
|
186
200
|
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
187
201
|
return this.executeWithRetry(toolName, args, attempt + 1);
|
|
188
202
|
}
|
|
189
|
-
//
|
|
190
|
-
|
|
203
|
+
// 🔴 FINAL FAILURE: Enhanced error reporting
|
|
204
|
+
const errorMessage = this.createEnhancedErrorMessage(toolName, error, attempt, maxRetries);
|
|
205
|
+
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, errorMessage);
|
|
191
206
|
}
|
|
192
207
|
}
|
|
193
208
|
/**
|
|
194
|
-
*
|
|
209
|
+
* 🔴 SNOW-003 FIX: Calculate intelligent backoff based on error type and attempt
|
|
210
|
+
*/
|
|
211
|
+
calculateBackoff(attempt, toolName) {
|
|
212
|
+
// Base exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s
|
|
213
|
+
const baseBackoff = 1000 * Math.pow(2, attempt - 1);
|
|
214
|
+
// Add jitter to prevent thundering herd (±25%)
|
|
215
|
+
const jitter = baseBackoff * 0.25 * (Math.random() - 0.5);
|
|
216
|
+
// Cap maximum backoff at 30 seconds
|
|
217
|
+
const maxBackoff = 30000;
|
|
218
|
+
return Math.min(baseBackoff + jitter, maxBackoff);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* 🔴 SNOW-003 FIX: Calculate dynamic timeout based on tool complexity
|
|
222
|
+
*/
|
|
223
|
+
calculateTimeout(toolName) {
|
|
224
|
+
// Tool-specific timeouts based on complexity
|
|
225
|
+
const timeoutMap = {
|
|
226
|
+
'snow_create_flow': 90000, // Flow creation: 90s
|
|
227
|
+
'snow_deploy': 120000, // Deployment: 2 minutes
|
|
228
|
+
'snow_comprehensive_search': 45000, // Search: 45s
|
|
229
|
+
'snow_find_artifact': 30000, // Find: 30s
|
|
230
|
+
'snow_validate_live_connection': 15000, // Validation: 15s
|
|
231
|
+
};
|
|
232
|
+
// Default timeout for unknown tools
|
|
233
|
+
return timeoutMap[toolName] || 60000; // 60s default (increased from 30s)
|
|
234
|
+
}
|
|
235
|
+
updateCircuitBreaker(toolName, error) {
|
|
236
|
+
const breaker = this.circuitBreakers.get(toolName) || { failures: 0, lastFailure: 0, isOpen: false };
|
|
237
|
+
breaker.failures++;
|
|
238
|
+
breaker.lastFailure = Date.now();
|
|
239
|
+
// Open circuit breaker after 5 failures within 5 minutes
|
|
240
|
+
if (breaker.failures >= 5 && (Date.now() - breaker.lastFailure) < 300000) {
|
|
241
|
+
breaker.isOpen = true;
|
|
242
|
+
this.logger.warn(`🚨 Circuit breaker opened for ${toolName} due to repeated failures`);
|
|
243
|
+
}
|
|
244
|
+
this.circuitBreakers.set(toolName, breaker);
|
|
245
|
+
}
|
|
246
|
+
isCircuitBreakerOpen(toolName) {
|
|
247
|
+
const breaker = this.circuitBreakers.get(toolName);
|
|
248
|
+
if (!breaker || !breaker.isOpen)
|
|
249
|
+
return false;
|
|
250
|
+
// Auto-reset circuit breaker after 10 minutes
|
|
251
|
+
if (Date.now() - breaker.lastFailure > 600000) {
|
|
252
|
+
breaker.isOpen = false;
|
|
253
|
+
breaker.failures = 0;
|
|
254
|
+
this.circuitBreakers.set(toolName, breaker);
|
|
255
|
+
this.logger.info(`✅ Circuit breaker reset for ${toolName}`);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
resetCircuitBreaker(toolName) {
|
|
261
|
+
const breaker = this.circuitBreakers.get(toolName);
|
|
262
|
+
if (breaker) {
|
|
263
|
+
breaker.failures = 0;
|
|
264
|
+
breaker.isOpen = false;
|
|
265
|
+
this.circuitBreakers.set(toolName, breaker);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* 🔴 SNOW-003 FIX: Memory usage monitoring to prevent memory exhaustion failures
|
|
270
|
+
*/
|
|
271
|
+
async checkMemoryUsage() {
|
|
272
|
+
try {
|
|
273
|
+
const memUsage = process.memoryUsage();
|
|
274
|
+
const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
|
|
275
|
+
const heapTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024);
|
|
276
|
+
// Log memory usage if high (>200MB)
|
|
277
|
+
if (heapUsedMB > 200) {
|
|
278
|
+
this.logger.warn(`⚠️ High memory usage: ${heapUsedMB}MB heap used, ${heapTotalMB}MB total`);
|
|
279
|
+
}
|
|
280
|
+
// Trigger garbage collection if memory usage is very high (>500MB)
|
|
281
|
+
if (heapUsedMB > 500 && global.gc) {
|
|
282
|
+
this.logger.info('🧹 Triggering garbage collection due to high memory usage');
|
|
283
|
+
global.gc();
|
|
284
|
+
}
|
|
285
|
+
// Fail fast if memory usage is critical (>800MB)
|
|
286
|
+
if (heapUsedMB > 800) {
|
|
287
|
+
throw new Error(`Critical memory usage: ${heapUsedMB}MB. Operation aborted to prevent system instability.`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
this.logger.warn('Could not check memory usage:', error);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* 🔴 SNOW-003 FIX: Enhanced error message with troubleshooting guidance
|
|
296
|
+
*/
|
|
297
|
+
createEnhancedErrorMessage(toolName, error, attempts, maxRetries) {
|
|
298
|
+
const baseMessage = `Tool '${toolName}' failed after ${attempts}/${maxRetries} attempts`;
|
|
299
|
+
const errorDetail = error instanceof Error ? error.message : String(error);
|
|
300
|
+
let troubleshooting = '';
|
|
301
|
+
// Add specific troubleshooting based on error type
|
|
302
|
+
if (error.response?.status === 401) {
|
|
303
|
+
troubleshooting = '\n💡 Authentication issue: Run "snow-flow auth login" to re-authenticate';
|
|
304
|
+
}
|
|
305
|
+
else if (error.response?.status === 403) {
|
|
306
|
+
troubleshooting = '\n💡 Permission issue: Check ServiceNow user permissions and OAuth scopes';
|
|
307
|
+
}
|
|
308
|
+
else if (error.response?.status >= 500) {
|
|
309
|
+
troubleshooting = '\n💡 ServiceNow server issue: Try again later or contact ServiceNow administrator';
|
|
310
|
+
}
|
|
311
|
+
else if (errorDetail.includes('timeout')) {
|
|
312
|
+
troubleshooting = '\n💡 Timeout issue: ServiceNow instance may be slow - try again later';
|
|
313
|
+
}
|
|
314
|
+
else if (errorDetail.includes('network') || errorDetail.includes('connection')) {
|
|
315
|
+
troubleshooting = '\n💡 Network issue: Check internet connection and ServiceNow instance availability';
|
|
316
|
+
}
|
|
317
|
+
return `${baseMessage}: ${errorDetail}${troubleshooting}`;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* 🔴 SNOW-003 FIX: Enhanced error classification for ServiceNow specific errors
|
|
321
|
+
* Addresses the 19% failure rate by properly categorizing retryable errors
|
|
195
322
|
*/
|
|
196
323
|
isRetryableError(error) {
|
|
197
324
|
if (error instanceof Error) {
|
|
198
325
|
const message = error.message.toLowerCase();
|
|
199
|
-
|
|
326
|
+
// 🔴 CRITICAL: ServiceNow specific retryable errors
|
|
327
|
+
const serviceNowRetryable = (message.includes('timeout') ||
|
|
200
328
|
message.includes('econnreset') ||
|
|
201
329
|
message.includes('socket hang up') ||
|
|
202
330
|
message.includes('enotfound') ||
|
|
203
|
-
message.includes('rate limit')
|
|
331
|
+
message.includes('rate limit') ||
|
|
332
|
+
message.includes('service unavailable') ||
|
|
333
|
+
message.includes('bad gateway') ||
|
|
334
|
+
message.includes('gateway timeout') ||
|
|
335
|
+
message.includes('connection refused') ||
|
|
336
|
+
message.includes('network error') ||
|
|
337
|
+
message.includes('dns lookup failed') ||
|
|
338
|
+
message.includes('connect etimedout') ||
|
|
339
|
+
message.includes('index not available') ||
|
|
340
|
+
message.includes('search index updating') ||
|
|
341
|
+
message.includes('temporary failure') ||
|
|
342
|
+
message.includes('server is busy') ||
|
|
343
|
+
message.includes('database lock') ||
|
|
344
|
+
message.includes('deadlock detected'));
|
|
345
|
+
// 🔴 CRITICAL: HTTP status code based retry logic
|
|
346
|
+
if (error.response?.status) {
|
|
347
|
+
const status = error.response.status;
|
|
348
|
+
const httpRetryable = (status === 429 || // Rate limit
|
|
349
|
+
status === 502 || // Bad Gateway
|
|
350
|
+
status === 503 || // Service Unavailable
|
|
351
|
+
status === 504 || // Gateway Timeout
|
|
352
|
+
status === 507 || // Insufficient Storage
|
|
353
|
+
status === 520 || // CloudFlare unknown error
|
|
354
|
+
status === 521 || // Web server is down
|
|
355
|
+
status === 522 || // Connection timed out
|
|
356
|
+
status === 523 || // Origin is unreachable
|
|
357
|
+
status === 524 // A timeout occurred
|
|
358
|
+
);
|
|
359
|
+
// 401 is retryable only once (for token refresh)
|
|
360
|
+
const authRetryable = status === 401 && !error.config?._retry;
|
|
361
|
+
return httpRetryable || authRetryable;
|
|
362
|
+
}
|
|
363
|
+
return serviceNowRetryable;
|
|
364
|
+
}
|
|
365
|
+
// Handle specific error types
|
|
366
|
+
if (error.code) {
|
|
367
|
+
const retryableCodes = [
|
|
368
|
+
'ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT',
|
|
369
|
+
'ESOCKETTIMEDOUT', 'EHOSTUNREACH', 'EPIPE', 'EAI_AGAIN'
|
|
370
|
+
];
|
|
371
|
+
return retryableCodes.includes(error.code);
|
|
204
372
|
}
|
|
205
373
|
return false;
|
|
206
374
|
}
|
|
@@ -789,6 +789,56 @@ class ServiceNowDeploymentMCP {
|
|
|
789
789
|
};
|
|
790
790
|
let troubleshootingSteps = '';
|
|
791
791
|
if (is403Error(directError) || is403Error(tableError)) {
|
|
792
|
+
// CRITICAL FIX: Check if widget was actually created despite 403 error
|
|
793
|
+
this.logger.info('403 error detected, verifying if widget was actually created...');
|
|
794
|
+
const verificationResult = await this.verifyWidgetInServiceNow(args.name);
|
|
795
|
+
if (verificationResult.exists) {
|
|
796
|
+
// Widget was created successfully despite 403 error!
|
|
797
|
+
this.logger.info('🎉 Widget verification SUCCESS: Widget exists despite 403 error', {
|
|
798
|
+
widgetName: args.name,
|
|
799
|
+
sys_id: verificationResult.sys_id,
|
|
800
|
+
completenessScore: verificationResult.completenessScore
|
|
801
|
+
});
|
|
802
|
+
// Format successful response similar to normal deployment
|
|
803
|
+
const credentials = await this.oauth.loadCredentials();
|
|
804
|
+
const widgetUrl = credentials?.instance ?
|
|
805
|
+
`https://${credentials.instance}/sp_config?id=widget_editor&sys_id=${verificationResult.sys_id}` :
|
|
806
|
+
'ServiceNow instance URL not available';
|
|
807
|
+
return {
|
|
808
|
+
content: [
|
|
809
|
+
{
|
|
810
|
+
type: 'text',
|
|
811
|
+
text: `✅ Widget deployed successfully! (Despite 403 error)
|
|
812
|
+
|
|
813
|
+
🎯 Widget Details:
|
|
814
|
+
- Name: ${args.name}
|
|
815
|
+
- Title: ${verificationResult.title || args.title}
|
|
816
|
+
- Sys ID: ${verificationResult.sys_id}
|
|
817
|
+
- Deployment Method: ${deploymentMethod} (with error recovery)
|
|
818
|
+
- Verification: ✅ Confirmed (${verificationResult.completenessScore}/100 complete)
|
|
819
|
+
|
|
820
|
+
📦 Update Set:
|
|
821
|
+
- Name: ${updateSetName}
|
|
822
|
+
- ID: ${updateSetId || 'None'}
|
|
823
|
+
- Status: ${updateSetId ? '✅ Tracked' : '⚠️ Not tracked'}
|
|
824
|
+
|
|
825
|
+
🔗 Direct Links:
|
|
826
|
+
- Widget Editor: ${widgetUrl}
|
|
827
|
+
- Service Portal Designer: https://${credentials?.instance}/sp_config?id=designer
|
|
828
|
+
|
|
829
|
+
🔧 Note: Widget was created successfully despite receiving a 403 error. This is a known issue with ServiceNow permissions that has been automatically resolved.
|
|
830
|
+
|
|
831
|
+
⚡ **Ready for Testing**
|
|
832
|
+
Your widget has been deployed and is ready for testing in Service Portal.`
|
|
833
|
+
}
|
|
834
|
+
]
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
// Widget was NOT created, continue with error handling
|
|
838
|
+
this.logger.warn('Widget verification failed: Widget does not exist after deployment attempts', {
|
|
839
|
+
widgetName: args.name,
|
|
840
|
+
verificationDetails: verificationResult.debugInfo
|
|
841
|
+
});
|
|
792
842
|
// Run authentication diagnostics automatically on 403 errors
|
|
793
843
|
this.logger.info('403 error detected, running automatic authentication diagnostics...');
|
|
794
844
|
let diagnosticsResult = '';
|
|
@@ -2079,15 +2129,44 @@ ${sessionSummary.statusCounts.pending > 0 ? '- 📋 Complete pending deployments
|
|
|
2079
2129
|
if (!data || typeof data !== 'object') {
|
|
2080
2130
|
throw new Error('Invalid diagnostics data received from ServiceNow');
|
|
2081
2131
|
}
|
|
2082
|
-
// Format test results
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2132
|
+
// Format test results with enhanced null safety
|
|
2133
|
+
let testResults = 'No test results available';
|
|
2134
|
+
if (tests && typeof tests === 'object') {
|
|
2135
|
+
try {
|
|
2136
|
+
const entries = Object.entries(tests);
|
|
2137
|
+
if (entries.length > 0) {
|
|
2138
|
+
testResults = entries.map(([name, result]) => {
|
|
2139
|
+
// CRITICAL: Extra null checks for each property
|
|
2140
|
+
const status = result?.status || 'Unknown';
|
|
2141
|
+
const description = result?.description || 'No description';
|
|
2142
|
+
const error = result?.error && typeof result.error === 'string' ? `- Error: ${result.error}` : '';
|
|
2143
|
+
const httpStatus = result?.http_status && typeof result.http_status === 'number' ? `- HTTP Status: ${result.http_status}` : '';
|
|
2144
|
+
return `**${name}:** ${status}
|
|
2145
|
+
- ${description}
|
|
2146
|
+
${error}
|
|
2147
|
+
${httpStatus}`;
|
|
2148
|
+
}).join('\n\n');
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
catch (testFormatError) {
|
|
2152
|
+
this.logger.error('Error formatting test results:', testFormatError);
|
|
2153
|
+
testResults = '❌ Error formatting test results - check ServiceNow connection';
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
// Format recommendations with null safety
|
|
2157
|
+
let recommendationText = '';
|
|
2158
|
+
if (args.include_recommendations !== false && Array.isArray(recommendations) && recommendations.length > 0) {
|
|
2159
|
+
try {
|
|
2160
|
+
const validRecommendations = recommendations.filter(rec => rec && typeof rec === 'string');
|
|
2161
|
+
if (validRecommendations.length > 0) {
|
|
2162
|
+
recommendationText = `\n\n**🔧 Troubleshooting Recommendations:**\n${validRecommendations.map((rec) => `- ${rec}`).join('\n')}`;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
catch (recFormatError) {
|
|
2166
|
+
this.logger.error('Error formatting recommendations:', recFormatError);
|
|
2167
|
+
recommendationText = '\n\n**🔧 Troubleshooting Recommendations:**\n- Unable to format recommendations due to error';
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2091
2170
|
// Generate URL fix recommendation if we detect the trailing slash issue
|
|
2092
2171
|
const urlFixRecommendation = data.instance_url && typeof data.instance_url === 'string' && data.instance_url.includes('//')
|
|
2093
2172
|
? '\n\n**🚨 CRITICAL URL ISSUE DETECTED:**\n- Your SNOW_INSTANCE in .env has a trailing slash\n- This causes malformed URLs like https://instance.com//api/\n- Remove the trailing slash from SNOW_INSTANCE=your-instance.com/'
|
|
@@ -5506,6 +5585,176 @@ Use individual deployment tools like \`snow_deploy_${args.type}\` with manual co
|
|
|
5506
5585
|
|
|
5507
5586
|
**Error Details**: ${error.message || error}`;
|
|
5508
5587
|
}
|
|
5588
|
+
/**
|
|
5589
|
+
* Verify widget exists in ServiceNow with comprehensive retry logic
|
|
5590
|
+
* Addresses the critical false negative bug where widgets show 403 errors but are actually created
|
|
5591
|
+
*/
|
|
5592
|
+
async verifyWidgetInServiceNow(widgetName) {
|
|
5593
|
+
const maxRetries = 5;
|
|
5594
|
+
const baseDelay = 2000; // Start with 2 seconds
|
|
5595
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
5596
|
+
try {
|
|
5597
|
+
// Progressive delay - ServiceNow needs time to process
|
|
5598
|
+
if (attempt > 1) {
|
|
5599
|
+
const delay = baseDelay * attempt; // 2s, 4s, 6s, 8s, 10s
|
|
5600
|
+
this.logger.info(`Waiting ${delay}ms before widget verification attempt ${attempt}/${maxRetries}`);
|
|
5601
|
+
await this.sleep(delay);
|
|
5602
|
+
}
|
|
5603
|
+
this.logger.info(`Widget verification attempt ${attempt}/${maxRetries}`, { widgetName });
|
|
5604
|
+
// Multi-table verification approach (similar to flow verification)
|
|
5605
|
+
const [mainWidgetCheck, widgetSearchCheck] = await Promise.allSettled([
|
|
5606
|
+
// Check 1: Direct sp_widget table search by name
|
|
5607
|
+
this.client.searchRecords('sp_widget', `name=${widgetName}`, 1),
|
|
5608
|
+
// Check 2: Broader search with ID field
|
|
5609
|
+
this.client.searchRecords('sp_widget', `name=${widgetName}^ORid=${widgetName}`, 5)
|
|
5610
|
+
]);
|
|
5611
|
+
let mainWidget = null;
|
|
5612
|
+
let searchResults = null;
|
|
5613
|
+
let verificationDetails = {
|
|
5614
|
+
attempt,
|
|
5615
|
+
mainWidgetCheck: 'pending',
|
|
5616
|
+
widgetSearchCheck: 'pending',
|
|
5617
|
+
totalFound: 0
|
|
5618
|
+
};
|
|
5619
|
+
// Process main widget check
|
|
5620
|
+
if (mainWidgetCheck.status === 'fulfilled' && mainWidgetCheck.value.success) {
|
|
5621
|
+
mainWidget = mainWidgetCheck.value.data?.[0];
|
|
5622
|
+
verificationDetails.mainWidgetCheck = mainWidget ? 'found' : 'not_found';
|
|
5623
|
+
verificationDetails.totalFound = mainWidgetCheck.value.data?.length || 0;
|
|
5624
|
+
}
|
|
5625
|
+
else {
|
|
5626
|
+
verificationDetails.mainWidgetCheck = `failed: ${mainWidgetCheck.status === 'rejected' ? mainWidgetCheck.reason : 'unknown error'}`;
|
|
5627
|
+
}
|
|
5628
|
+
// Process widget search check
|
|
5629
|
+
if (widgetSearchCheck.status === 'fulfilled' && widgetSearchCheck.value.success) {
|
|
5630
|
+
searchResults = widgetSearchCheck.value.data || [];
|
|
5631
|
+
verificationDetails.widgetSearchCheck = `found_${searchResults.length}`;
|
|
5632
|
+
// Use search results if main check didn't find anything
|
|
5633
|
+
if (!mainWidget && searchResults.length > 0) {
|
|
5634
|
+
mainWidget = searchResults[0];
|
|
5635
|
+
verificationDetails.totalFound = searchResults.length;
|
|
5636
|
+
}
|
|
5637
|
+
}
|
|
5638
|
+
else {
|
|
5639
|
+
verificationDetails.widgetSearchCheck = `failed: ${widgetSearchCheck.status === 'rejected' ? widgetSearchCheck.reason : 'unknown error'}`;
|
|
5640
|
+
}
|
|
5641
|
+
// Calculate completeness score
|
|
5642
|
+
let completenessScore = 0;
|
|
5643
|
+
if (mainWidget) {
|
|
5644
|
+
completenessScore += mainWidget.sys_id ? 25 : 0;
|
|
5645
|
+
completenessScore += mainWidget.name ? 25 : 0;
|
|
5646
|
+
completenessScore += mainWidget.title ? 25 : 0;
|
|
5647
|
+
completenessScore += mainWidget.template ? 25 : 0;
|
|
5648
|
+
}
|
|
5649
|
+
if (mainWidget && completenessScore >= 75) {
|
|
5650
|
+
// Widget found and appears complete
|
|
5651
|
+
this.logger.info(`✅ Widget verification SUCCESS on attempt ${attempt}`, {
|
|
5652
|
+
widgetName,
|
|
5653
|
+
sys_id: mainWidget.sys_id,
|
|
5654
|
+
completenessScore,
|
|
5655
|
+
totalRetries: attempt
|
|
5656
|
+
});
|
|
5657
|
+
return {
|
|
5658
|
+
exists: true,
|
|
5659
|
+
sys_id: mainWidget.sys_id,
|
|
5660
|
+
name: mainWidget.name,
|
|
5661
|
+
title: mainWidget.title,
|
|
5662
|
+
completenessScore,
|
|
5663
|
+
attempt,
|
|
5664
|
+
verificationDetails,
|
|
5665
|
+
debugInfo: {
|
|
5666
|
+
foundVia: verificationDetails.mainWidgetCheck === 'found' ? 'main_check' : 'search_check',
|
|
5667
|
+
totalFound: verificationDetails.totalFound,
|
|
5668
|
+
retriesNeeded: attempt
|
|
5669
|
+
}
|
|
5670
|
+
};
|
|
5671
|
+
}
|
|
5672
|
+
else if (mainWidget && completenessScore < 75) {
|
|
5673
|
+
// Widget found but incomplete - might still be processing
|
|
5674
|
+
this.logger.warn(`⚠️ Widget found but incomplete on attempt ${attempt}`, {
|
|
5675
|
+
widgetName,
|
|
5676
|
+
sys_id: mainWidget.sys_id,
|
|
5677
|
+
completenessScore,
|
|
5678
|
+
remainingRetries: maxRetries - attempt
|
|
5679
|
+
});
|
|
5680
|
+
if (attempt === maxRetries) {
|
|
5681
|
+
// Last attempt - return what we have
|
|
5682
|
+
return {
|
|
5683
|
+
exists: true,
|
|
5684
|
+
sys_id: mainWidget.sys_id,
|
|
5685
|
+
name: mainWidget.name,
|
|
5686
|
+
title: mainWidget.title,
|
|
5687
|
+
completenessScore,
|
|
5688
|
+
attempt,
|
|
5689
|
+
verificationDetails,
|
|
5690
|
+
debugInfo: {
|
|
5691
|
+
warning: 'Widget exists but appears incomplete',
|
|
5692
|
+
foundVia: verificationDetails.mainWidgetCheck === 'found' ? 'main_check' : 'search_check',
|
|
5693
|
+
totalFound: verificationDetails.totalFound,
|
|
5694
|
+
retriesNeeded: attempt
|
|
5695
|
+
}
|
|
5696
|
+
};
|
|
5697
|
+
}
|
|
5698
|
+
}
|
|
5699
|
+
else {
|
|
5700
|
+
// Widget not found
|
|
5701
|
+
this.logger.warn(`❌ Widget not found on attempt ${attempt}`, {
|
|
5702
|
+
widgetName,
|
|
5703
|
+
verificationDetails,
|
|
5704
|
+
remainingRetries: maxRetries - attempt
|
|
5705
|
+
});
|
|
5706
|
+
if (attempt === maxRetries) {
|
|
5707
|
+
// Final attempt failed
|
|
5708
|
+
return {
|
|
5709
|
+
exists: false,
|
|
5710
|
+
attempt,
|
|
5711
|
+
verificationDetails,
|
|
5712
|
+
debugInfo: {
|
|
5713
|
+
finalAttempt: true,
|
|
5714
|
+
allChecks: {
|
|
5715
|
+
mainWidgetCheck: verificationDetails.mainWidgetCheck,
|
|
5716
|
+
widgetSearchCheck: verificationDetails.widgetSearchCheck
|
|
5717
|
+
},
|
|
5718
|
+
totalRetries: maxRetries
|
|
5719
|
+
}
|
|
5720
|
+
};
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
catch (verificationError) {
|
|
5725
|
+
this.logger.error(`Widget verification attempt ${attempt} failed`, {
|
|
5726
|
+
widgetName,
|
|
5727
|
+
error: verificationError instanceof Error ? verificationError.message : String(verificationError),
|
|
5728
|
+
remainingRetries: maxRetries - attempt
|
|
5729
|
+
});
|
|
5730
|
+
if (attempt === maxRetries) {
|
|
5731
|
+
// Final attempt - return failure with error details
|
|
5732
|
+
return {
|
|
5733
|
+
exists: false,
|
|
5734
|
+
attempt,
|
|
5735
|
+
error: verificationError instanceof Error ? verificationError.message : String(verificationError),
|
|
5736
|
+
debugInfo: {
|
|
5737
|
+
finalAttempt: true,
|
|
5738
|
+
verificationError: true,
|
|
5739
|
+
totalRetries: maxRetries
|
|
5740
|
+
}
|
|
5741
|
+
};
|
|
5742
|
+
}
|
|
5743
|
+
}
|
|
5744
|
+
}
|
|
5745
|
+
// Should not reach here, but safety fallback
|
|
5746
|
+
return {
|
|
5747
|
+
exists: false,
|
|
5748
|
+
attempt: maxRetries,
|
|
5749
|
+
debugInfo: { unexpectedFallback: true }
|
|
5750
|
+
};
|
|
5751
|
+
}
|
|
5752
|
+
/**
|
|
5753
|
+
* Sleep utility for retry delays
|
|
5754
|
+
*/
|
|
5755
|
+
sleep(ms) {
|
|
5756
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
5757
|
+
}
|
|
5509
5758
|
/**
|
|
5510
5759
|
* Format successful deployment response
|
|
5511
5760
|
*/
|