snow-flow 1.1.43 → 1.1.47

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.
@@ -482,6 +482,218 @@ class ServiceNowOperationsMCP {
482
482
  },
483
483
  required: ['action']
484
484
  }
485
+ },
486
+ {
487
+ name: 'snow_catalog_item_search',
488
+ description: 'Search for catalog items with intelligent matching and discovery',
489
+ inputSchema: {
490
+ type: 'object',
491
+ properties: {
492
+ query: {
493
+ type: 'string',
494
+ description: 'Search query (e.g., "iPhone", "laptop", "mobile device")'
495
+ },
496
+ include_inactive: {
497
+ type: 'boolean',
498
+ description: 'Include inactive catalog items',
499
+ default: false
500
+ },
501
+ fuzzy_match: {
502
+ type: 'boolean',
503
+ description: 'Enable fuzzy matching for similar items',
504
+ default: true
505
+ },
506
+ category_filter: {
507
+ type: 'string',
508
+ description: 'Filter by category name or sys_id'
509
+ },
510
+ limit: {
511
+ type: 'number',
512
+ description: 'Maximum results to return',
513
+ default: 50
514
+ },
515
+ include_variables: {
516
+ type: 'boolean',
517
+ description: 'Include catalog item variables in results',
518
+ default: false
519
+ }
520
+ },
521
+ required: ['query']
522
+ }
523
+ },
524
+ {
525
+ name: 'snow_test_flow_with_mock',
526
+ description: 'Test flows with mock data - create test users, mock catalog items, and simulate flow execution',
527
+ inputSchema: {
528
+ type: 'object',
529
+ properties: {
530
+ flow_id: {
531
+ type: 'string',
532
+ description: 'Flow sys_id or name to test'
533
+ },
534
+ create_test_user: {
535
+ type: 'boolean',
536
+ description: 'Create a test user for the flow',
537
+ default: true
538
+ },
539
+ test_user_data: {
540
+ type: 'object',
541
+ description: 'Test user details',
542
+ properties: {
543
+ user_name: { type: 'string' },
544
+ first_name: { type: 'string' },
545
+ last_name: { type: 'string' },
546
+ email: { type: 'string' },
547
+ department: { type: 'string' },
548
+ manager: { type: 'string' }
549
+ }
550
+ },
551
+ mock_catalog_items: {
552
+ type: 'boolean',
553
+ description: 'Create mock catalog items for testing',
554
+ default: true
555
+ },
556
+ mock_catalog_data: {
557
+ type: 'array',
558
+ description: 'Mock catalog items to create',
559
+ items: {
560
+ type: 'object',
561
+ properties: {
562
+ name: { type: 'string' },
563
+ short_description: { type: 'string' },
564
+ price: { type: 'string' },
565
+ variables: { type: 'array' }
566
+ }
567
+ }
568
+ },
569
+ test_inputs: {
570
+ type: 'object',
571
+ description: 'Input values to test the flow with'
572
+ },
573
+ simulate_approvals: {
574
+ type: 'boolean',
575
+ description: 'Automatically approve any approval requests',
576
+ default: true
577
+ },
578
+ cleanup_after_test: {
579
+ type: 'boolean',
580
+ description: 'Remove test data after testing',
581
+ default: true
582
+ }
583
+ },
584
+ required: ['flow_id']
585
+ }
586
+ },
587
+ {
588
+ name: 'snow_link_catalog_to_flow',
589
+ description: 'Link catalog items directly to flows - configure flow as fulfillment process',
590
+ inputSchema: {
591
+ type: 'object',
592
+ properties: {
593
+ catalog_item_id: {
594
+ type: 'string',
595
+ description: 'Catalog item sys_id or name'
596
+ },
597
+ flow_id: {
598
+ type: 'string',
599
+ description: 'Flow sys_id or name to use for fulfillment'
600
+ },
601
+ link_type: {
602
+ type: 'string',
603
+ description: 'Type of link to create',
604
+ enum: ['workflow', 'flow_catalog_process', 'process_engine'],
605
+ default: 'flow_catalog_process'
606
+ },
607
+ variable_mapping: {
608
+ type: 'array',
609
+ description: 'Map catalog variables to flow inputs',
610
+ items: {
611
+ type: 'object',
612
+ properties: {
613
+ catalog_variable: { type: 'string', description: 'Catalog variable name' },
614
+ flow_input: { type: 'string', description: 'Flow input name' },
615
+ transform: { type: 'string', description: 'Optional transformation script' }
616
+ }
617
+ }
618
+ },
619
+ trigger_condition: {
620
+ type: 'string',
621
+ description: 'Condition for when to trigger the flow',
622
+ default: 'current.stage == "request_approved"'
623
+ },
624
+ execution_options: {
625
+ type: 'object',
626
+ description: 'Flow execution options',
627
+ properties: {
628
+ run_as: {
629
+ type: 'string',
630
+ description: 'User context for flow execution',
631
+ enum: ['requester', 'system', 'fulfiller'],
632
+ default: 'system'
633
+ },
634
+ wait_for_completion: {
635
+ type: 'boolean',
636
+ description: 'Wait for flow to complete before closing request',
637
+ default: true
638
+ },
639
+ update_request_on_progress: {
640
+ type: 'boolean',
641
+ description: 'Update request item with flow progress',
642
+ default: true
643
+ }
644
+ }
645
+ },
646
+ test_link: {
647
+ type: 'boolean',
648
+ description: 'Test the link by creating a sample request',
649
+ default: false
650
+ }
651
+ },
652
+ required: ['catalog_item_id', 'flow_id']
653
+ }
654
+ },
655
+ {
656
+ name: 'snow_cleanup_test_artifacts',
657
+ description: 'Clean up test artifacts while preserving Update Set audit trail',
658
+ inputSchema: {
659
+ type: 'object',
660
+ properties: {
661
+ artifact_types: {
662
+ type: 'array',
663
+ description: 'Types of artifacts to clean up',
664
+ items: {
665
+ type: 'string',
666
+ enum: ['catalog_items', 'flows', 'users', 'requests', 'all']
667
+ },
668
+ default: ['catalog_items', 'flows', 'users']
669
+ },
670
+ test_patterns: {
671
+ type: 'array',
672
+ description: 'Name patterns that identify test artifacts',
673
+ items: { type: 'string' },
674
+ default: ['Test%', 'Mock%', 'Demo%', '%_test_%', '%test', '%mock']
675
+ },
676
+ max_age_hours: {
677
+ type: 'number',
678
+ description: 'Only clean artifacts older than this (hours)',
679
+ default: 1
680
+ },
681
+ dry_run: {
682
+ type: 'boolean',
683
+ description: 'Preview what would be deleted without actually deleting',
684
+ default: false
685
+ },
686
+ preserve_update_set_entries: {
687
+ type: 'boolean',
688
+ description: 'Keep Update Set entries as audit trail',
689
+ default: true
690
+ },
691
+ update_set_filter: {
692
+ type: 'string',
693
+ description: 'Only clean from specific Update Set (optional)'
694
+ }
695
+ }
696
+ }
485
697
  }
486
698
  ],
487
699
  };
@@ -515,6 +727,14 @@ class ServiceNowOperationsMCP {
515
727
  return await this.handlePredictiveAnalysis(args);
516
728
  case 'snow_catalog_item_manager':
517
729
  return await this.handleCatalogItemManager(args);
730
+ case 'snow_catalog_item_search':
731
+ return await this.handleCatalogItemSearch(args);
732
+ case 'snow_test_flow_with_mock':
733
+ return await this.handleTestFlowWithMock(args);
734
+ case 'snow_link_catalog_to_flow':
735
+ return await this.handleLinkCatalogToFlow(args);
736
+ case 'snow_cleanup_test_artifacts':
737
+ return await this.handleCleanupTestArtifacts(args);
518
738
  default:
519
739
  throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Tool ${name} not found`);
520
740
  }
@@ -1382,6 +1602,34 @@ class ServiceNowOperationsMCP {
1382
1602
  try {
1383
1603
  switch (action) {
1384
1604
  case 'create': {
1605
+ // Get default catalog if none provided
1606
+ let catalogId = params.sc_catalogs || params.catalog_id;
1607
+ let categoryId = params.sc_categories || params.category_id;
1608
+ if (!catalogId) {
1609
+ // Find default Service Catalog
1610
+ const defaultCatalogResult = await this.client.searchRecords('sc_catalog', 'active=true^ORDERBYsys_created_on', 1);
1611
+ if (defaultCatalogResult.success && defaultCatalogResult.data?.result?.length > 0) {
1612
+ catalogId = defaultCatalogResult.data.result[0].sys_id;
1613
+ logger_js_1.logger.info('Using default catalog:', {
1614
+ catalogId,
1615
+ catalogName: defaultCatalogResult.data.result[0].title
1616
+ });
1617
+ }
1618
+ else {
1619
+ logger_js_1.logger.warn('No catalogs found, catalog item may not be visible');
1620
+ }
1621
+ }
1622
+ if (!categoryId) {
1623
+ // Find a default category like "Hardware" or "General"
1624
+ const defaultCategoryResult = await this.client.searchRecords('sc_category', 'active=true^titleLIKEHardware^ORtitleLIKEGeneral^ORtitleLIKEIT^ORDERBYsys_created_on', 1);
1625
+ if (defaultCategoryResult.success && defaultCategoryResult.data?.result?.length > 0) {
1626
+ categoryId = defaultCategoryResult.data.result[0].sys_id;
1627
+ logger_js_1.logger.info('Using default category:', {
1628
+ categoryId,
1629
+ categoryName: defaultCategoryResult.data.result[0].title
1630
+ });
1631
+ }
1632
+ }
1385
1633
  const catalogItem = {
1386
1634
  name: params.name,
1387
1635
  short_description: params.short_description,
@@ -1392,8 +1640,8 @@ class ServiceNowOperationsMCP {
1392
1640
  active: params.active !== false,
1393
1641
  workflow: params.workflow,
1394
1642
  picture: params.picture,
1395
- sc_catalogs: params.sc_catalogs || params.catalog_id,
1396
- sc_categories: params.sc_categories || params.category_id,
1643
+ sc_catalogs: catalogId,
1644
+ sc_categories: categoryId,
1397
1645
  sys_class_name: 'sc_cat_item'
1398
1646
  };
1399
1647
  const result = await this.client.createRecord('sc_cat_item', catalogItem);
@@ -1597,6 +1845,998 @@ class ServiceNowOperationsMCP {
1597
1845
  };
1598
1846
  return typeMap[type] || '6'; // Default to single line text
1599
1847
  }
1848
+ async handleCatalogItemSearch(args) {
1849
+ const { query, include_inactive = false, fuzzy_match = true, category_filter, limit = 50, include_variables = false } = args;
1850
+ logger_js_1.logger.info(`Searching catalog items for: ${query}`, { fuzzy_match, category_filter, limit });
1851
+ try {
1852
+ // Build search queries
1853
+ const searchQueries = [];
1854
+ // Exact match
1855
+ searchQueries.push(`name=${query}`);
1856
+ // Contains match
1857
+ searchQueries.push(`nameLIKE${query}`);
1858
+ searchQueries.push(`short_descriptionLIKE${query}`);
1859
+ // Fuzzy matching for similar items
1860
+ if (fuzzy_match) {
1861
+ // Handle common variations
1862
+ const variations = this.generateSearchVariations(query);
1863
+ variations.forEach(variation => {
1864
+ searchQueries.push(`nameLIKE${variation}`);
1865
+ searchQueries.push(`short_descriptionLIKE${variation}`);
1866
+ });
1867
+ }
1868
+ // Add active filter
1869
+ const activeFilter = include_inactive ? '' : '^active=true';
1870
+ // Add category filter if provided
1871
+ let categoryFilter = '';
1872
+ if (category_filter) {
1873
+ // First try to find the category
1874
+ const categoryResult = await this.client.searchRecords('sc_category', `name=${category_filter}^ORsys_id=${category_filter}`, 1);
1875
+ if (categoryResult.success && categoryResult.data?.result?.length > 0) {
1876
+ categoryFilter = `^sc_categories=${categoryResult.data.result[0].sys_id}`;
1877
+ }
1878
+ }
1879
+ // Execute searches
1880
+ const allResults = new Map();
1881
+ for (const searchQuery of searchQueries) {
1882
+ const fullQuery = searchQuery + activeFilter + categoryFilter;
1883
+ const result = await this.client.searchRecords('sc_cat_item', fullQuery, limit);
1884
+ if (result.success && result.data?.result) {
1885
+ result.data.result.forEach((item) => {
1886
+ if (!allResults.has(item.sys_id)) {
1887
+ allResults.set(item.sys_id, {
1888
+ sys_id: item.sys_id,
1889
+ name: item.name,
1890
+ short_description: item.short_description,
1891
+ price: item.price,
1892
+ recurring_price: item.recurring_price,
1893
+ active: item.active,
1894
+ category: item.sc_categories?.display_value,
1895
+ catalog: item.sc_catalogs?.display_value,
1896
+ model_number: item.model,
1897
+ manufacturer: item.vendor,
1898
+ variables: []
1899
+ });
1900
+ }
1901
+ });
1902
+ }
1903
+ }
1904
+ // Get variables if requested
1905
+ if (include_variables && allResults.size > 0) {
1906
+ for (const [sys_id, item] of allResults) {
1907
+ const variablesResult = await this.client.searchRecords('item_option_new', `cat_item=${sys_id}`, 50);
1908
+ if (variablesResult.success && variablesResult.data?.result) {
1909
+ item.variables = variablesResult.data.result.map((v) => ({
1910
+ name: v.name,
1911
+ label: v.question_text,
1912
+ type: v.type,
1913
+ mandatory: v.mandatory,
1914
+ default_value: v.default_value
1915
+ }));
1916
+ }
1917
+ }
1918
+ }
1919
+ const resultsArray = Array.from(allResults.values());
1920
+ // Sort by relevance (exact matches first)
1921
+ resultsArray.sort((a, b) => {
1922
+ const aExact = a.name.toLowerCase() === query.toLowerCase();
1923
+ const bExact = b.name.toLowerCase() === query.toLowerCase();
1924
+ if (aExact && !bExact)
1925
+ return -1;
1926
+ if (!aExact && bExact)
1927
+ return 1;
1928
+ return 0;
1929
+ });
1930
+ if (resultsArray.length === 0) {
1931
+ // Provide suggestions if no results found
1932
+ const suggestions = this.generateSearchSuggestions(query);
1933
+ return {
1934
+ content: [{
1935
+ type: 'text',
1936
+ text: `❌ No catalog items found for "${query}"\n\n` +
1937
+ `💡 Suggestions:\n` +
1938
+ suggestions.map(s => `- Try searching for "${s}"`).join('\n') +
1939
+ `\n\n💡 Tips:\n` +
1940
+ `- Check if the item name is spelled correctly\n` +
1941
+ `- Try searching with partial names (e.g., "iPhone" instead of "iPhone 6S")\n` +
1942
+ `- Use category filter to narrow down results\n` +
1943
+ `- Enable include_inactive if looking for retired items`
1944
+ }]
1945
+ };
1946
+ }
1947
+ // Format results
1948
+ let resultText = `🔍 Found ${resultsArray.length} catalog items matching "${query}":\n\n`;
1949
+ resultsArray.forEach((item, index) => {
1950
+ resultText += `${index + 1}. **${item.name}** (${item.sys_id})\n`;
1951
+ resultText += ` 📝 ${item.short_description || 'No description'}\n`;
1952
+ resultText += ` 💰 Price: $${item.price || '0'} ${item.recurring_price ? `(+ $${item.recurring_price}/month)` : ''}\n`;
1953
+ resultText += ` 📁 Category: ${item.category || 'Uncategorized'}\n`;
1954
+ resultText += ` 📚 Catalog: ${item.catalog || 'Default'}\n`;
1955
+ if (item.model_number) {
1956
+ resultText += ` 🔢 Model: ${item.model_number}\n`;
1957
+ }
1958
+ if (item.manufacturer) {
1959
+ resultText += ` 🏭 Manufacturer: ${item.manufacturer}\n`;
1960
+ }
1961
+ resultText += ` ✅ Status: ${item.active ? 'Active' : 'Inactive'}\n`;
1962
+ if (item.variables && item.variables.length > 0) {
1963
+ resultText += ` 📋 Variables (${item.variables.length}):\n`;
1964
+ item.variables.forEach((v) => {
1965
+ resultText += ` - ${v.label} (${v.name}) - ${v.type}${v.mandatory ? ' *Required' : ''}\n`;
1966
+ });
1967
+ }
1968
+ resultText += '\n';
1969
+ });
1970
+ resultText += `\n💡 Use snow_catalog_item_manager to create or update catalog items`;
1971
+ return {
1972
+ content: [{
1973
+ type: 'text',
1974
+ text: resultText
1975
+ }]
1976
+ };
1977
+ }
1978
+ catch (error) {
1979
+ logger_js_1.logger.error('Error searching catalog items:', error);
1980
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Catalog search error: ${error}`);
1981
+ }
1982
+ }
1983
+ generateSearchVariations(query) {
1984
+ const variations = [];
1985
+ // Handle common product variations
1986
+ if (query.toLowerCase().includes('iphone')) {
1987
+ // Add variations without spaces, with different numbers
1988
+ const baseModel = query.replace(/iphone\s*/i, 'iPhone ');
1989
+ variations.push(baseModel);
1990
+ variations.push(baseModel.replace(' ', ''));
1991
+ // Try different model numbers
1992
+ const modelMatch = query.match(/\d+/);
1993
+ if (modelMatch) {
1994
+ const modelNum = parseInt(modelMatch[0]);
1995
+ variations.push(query.replace(modelNum.toString(), (modelNum + 1).toString()));
1996
+ variations.push(query.replace(modelNum.toString(), (modelNum - 1).toString()));
1997
+ }
1998
+ }
1999
+ // Handle laptop/computer variations
2000
+ if (query.toLowerCase().includes('laptop') || query.toLowerCase().includes('computer')) {
2001
+ variations.push('notebook', 'macbook', 'thinkpad', 'dell', 'hp');
2002
+ }
2003
+ // Handle phone variations
2004
+ if (query.toLowerCase().includes('phone') || query.toLowerCase().includes('mobile')) {
2005
+ variations.push('smartphone', 'android', 'samsung', 'pixel');
2006
+ }
2007
+ return variations;
2008
+ }
2009
+ generateSearchSuggestions(query) {
2010
+ const suggestions = [];
2011
+ if (query.toLowerCase().includes('iphone')) {
2012
+ suggestions.push('iPhone', 'Apple iPhone', 'iOS device', 'Apple mobile');
2013
+ }
2014
+ else if (query.toLowerCase().includes('laptop')) {
2015
+ suggestions.push('notebook', 'computer', 'workstation', 'MacBook', 'ThinkPad');
2016
+ }
2017
+ else if (query.toLowerCase().includes('software')) {
2018
+ suggestions.push('license', 'application', 'subscription', 'SaaS');
2019
+ }
2020
+ else {
2021
+ // Generic suggestions
2022
+ suggestions.push(query.split(' ')[0], // First word only
2023
+ query.substring(0, Math.floor(query.length / 2)), // First half
2024
+ 'hardware', 'software', 'equipment', 'device');
2025
+ }
2026
+ return [...new Set(suggestions)].filter(s => s && s !== query);
2027
+ }
2028
+ async handleTestFlowWithMock(args) {
2029
+ const { flow_id, create_test_user = true, test_user_data, mock_catalog_items = true, mock_catalog_data, test_inputs = {}, simulate_approvals = true, cleanup_after_test = true } = args;
2030
+ logger_js_1.logger.info(`Testing flow ${flow_id} with mock data`, {
2031
+ create_test_user,
2032
+ mock_catalog_items,
2033
+ simulate_approvals,
2034
+ cleanup_after_test
2035
+ });
2036
+ const testResults = {
2037
+ flow_id,
2038
+ test_run_id: `test_${Date.now()}`,
2039
+ created_data: {
2040
+ test_user: null,
2041
+ catalog_items: [],
2042
+ test_requests: []
2043
+ },
2044
+ execution_results: {
2045
+ status: 'pending',
2046
+ start_time: new Date().toISOString(),
2047
+ end_time: null,
2048
+ duration_ms: 0,
2049
+ execution_id: null,
2050
+ steps_executed: [],
2051
+ errors: [],
2052
+ approvals_simulated: []
2053
+ },
2054
+ cleanup_status: null
2055
+ };
2056
+ try {
2057
+ // Step 1: Find the flow
2058
+ const flowResult = await this.client.searchRecords('sys_hub_flow', `sys_id=${flow_id}^ORname=${flow_id}`, 1);
2059
+ if (!flowResult.success || !flowResult.data?.result?.length) {
2060
+ throw new Error(`Flow not found: ${flow_id}`);
2061
+ }
2062
+ const flow = flowResult.data.result[0];
2063
+ testResults.flow_id = flow.sys_id;
2064
+ // Step 2: Create test user if requested
2065
+ if (create_test_user) {
2066
+ const userData = test_user_data || {
2067
+ user_name: `test_user_${Date.now()}`,
2068
+ first_name: 'Test',
2069
+ last_name: 'User',
2070
+ email: `test_${Date.now()}@example.com`,
2071
+ department: 'IT',
2072
+ active: true
2073
+ };
2074
+ const userResult = await this.client.createRecord('sys_user', userData);
2075
+ if (userResult.success && userResult.data?.result) {
2076
+ testResults.created_data.test_user = {
2077
+ sys_id: userResult.data.result.sys_id,
2078
+ user_name: userData.user_name,
2079
+ email: userData.email
2080
+ };
2081
+ logger_js_1.logger.info('Created test user:', testResults.created_data.test_user);
2082
+ }
2083
+ }
2084
+ // Step 3: Create mock catalog items if requested
2085
+ if (mock_catalog_items) {
2086
+ const catalogData = mock_catalog_data || [
2087
+ {
2088
+ name: 'Test iPhone 6S',
2089
+ short_description: 'Test mobile device for flow testing',
2090
+ price: '699.00',
2091
+ active: true,
2092
+ billable: true
2093
+ },
2094
+ {
2095
+ name: 'Test Laptop',
2096
+ short_description: 'Test laptop for equipment provisioning',
2097
+ price: '1299.00',
2098
+ active: true,
2099
+ billable: true
2100
+ }
2101
+ ];
2102
+ for (const item of catalogData) {
2103
+ const catalogResult = await this.client.createRecord('sc_cat_item', item);
2104
+ if (catalogResult.success && catalogResult.data?.result) {
2105
+ testResults.created_data.catalog_items.push({
2106
+ sys_id: catalogResult.data.result.sys_id,
2107
+ name: item.name,
2108
+ price: item.price
2109
+ });
2110
+ logger_js_1.logger.info('Created mock catalog item:', item.name);
2111
+ }
2112
+ }
2113
+ }
2114
+ // Step 4: Execute the flow with test data
2115
+ try {
2116
+ // Prepare test inputs
2117
+ const flowInputs = {
2118
+ ...test_inputs,
2119
+ test_mode: true,
2120
+ test_run_id: testResults.test_run_id
2121
+ };
2122
+ // If we created a test user, use them as the requested_for
2123
+ if (testResults.created_data.test_user) {
2124
+ flowInputs.requested_for = testResults.created_data.test_user.sys_id;
2125
+ flowInputs.opened_by = testResults.created_data.test_user.sys_id;
2126
+ }
2127
+ // If we created catalog items, use the first one
2128
+ if (testResults.created_data.catalog_items.length > 0) {
2129
+ flowInputs.cat_item = testResults.created_data.catalog_items[0].sys_id;
2130
+ }
2131
+ // Execute flow via REST API
2132
+ const executeResult = await this.executeFlowViaAPI(flow.sys_id, flowInputs);
2133
+ if (executeResult.success) {
2134
+ testResults.execution_results.status = 'running';
2135
+ testResults.execution_results.execution_id = executeResult.execution_id;
2136
+ // Monitor flow execution
2137
+ const executionStatus = await this.monitorFlowExecution(executeResult.execution_id, simulate_approvals);
2138
+ testResults.execution_results = {
2139
+ ...testResults.execution_results,
2140
+ ...executionStatus
2141
+ };
2142
+ }
2143
+ else {
2144
+ testResults.execution_results.status = 'failed';
2145
+ testResults.execution_results.errors.push(executeResult.error || 'Unknown execution error');
2146
+ }
2147
+ }
2148
+ catch (execError) {
2149
+ testResults.execution_results.status = 'error';
2150
+ testResults.execution_results.errors.push(execError.message);
2151
+ logger_js_1.logger.error('Flow execution error:', execError);
2152
+ }
2153
+ // Step 5: Cleanup if requested
2154
+ if (cleanup_after_test) {
2155
+ const cleanupResults = {
2156
+ user_deleted: false,
2157
+ catalog_items_deleted: 0,
2158
+ requests_cancelled: 0
2159
+ };
2160
+ // Delete test user
2161
+ if (testResults.created_data.test_user) {
2162
+ const deleteUserResult = await this.client.deleteRecord('sys_user', testResults.created_data.test_user.sys_id);
2163
+ cleanupResults.user_deleted = deleteUserResult.success;
2164
+ }
2165
+ // Delete catalog items
2166
+ for (const item of testResults.created_data.catalog_items) {
2167
+ const deleteItemResult = await this.client.deleteRecord('sc_cat_item', item.sys_id);
2168
+ if (deleteItemResult.success) {
2169
+ cleanupResults.catalog_items_deleted++;
2170
+ }
2171
+ }
2172
+ testResults.cleanup_status = cleanupResults;
2173
+ }
2174
+ // Format results
2175
+ const resultText = this.formatTestResults(testResults);
2176
+ return {
2177
+ content: [{
2178
+ type: 'text',
2179
+ text: resultText
2180
+ }]
2181
+ };
2182
+ }
2183
+ catch (error) {
2184
+ logger_js_1.logger.error('Error testing flow with mock data:', error);
2185
+ testResults.execution_results.status = 'error';
2186
+ testResults.execution_results.errors.push(error.message);
2187
+ return {
2188
+ content: [{
2189
+ type: 'text',
2190
+ text: `❌ Flow test failed\n\nError: ${error.message}\n\nTest Results:\n${JSON.stringify(testResults, null, 2)}`
2191
+ }]
2192
+ };
2193
+ }
2194
+ }
2195
+ async executeFlowViaAPI(flowId, inputs) {
2196
+ try {
2197
+ // Use the ServiceNow Flow API to execute the flow
2198
+ const result = await this.client.createRecord('sys_flow_context', {
2199
+ flow: flowId,
2200
+ inputs: JSON.stringify(inputs),
2201
+ state: 'executing'
2202
+ });
2203
+ if (result.success && result.data?.result) {
2204
+ return {
2205
+ success: true,
2206
+ execution_id: result.data.result.sys_id
2207
+ };
2208
+ }
2209
+ return {
2210
+ success: false,
2211
+ error: 'Failed to start flow execution'
2212
+ };
2213
+ }
2214
+ catch (error) {
2215
+ logger_js_1.logger.error('Error executing flow via API:', error);
2216
+ return {
2217
+ success: false,
2218
+ error: error.message
2219
+ };
2220
+ }
2221
+ }
2222
+ async monitorFlowExecution(executionId, simulateApprovals) {
2223
+ const maxWaitTime = 60000; // 60 seconds
2224
+ const pollInterval = 2000; // 2 seconds
2225
+ const startTime = Date.now();
2226
+ const executionResults = {
2227
+ status: 'running',
2228
+ steps_executed: [],
2229
+ approvals_simulated: [],
2230
+ errors: [],
2231
+ end_time: null,
2232
+ duration_ms: 0
2233
+ };
2234
+ while (Date.now() - startTime < maxWaitTime) {
2235
+ try {
2236
+ // Check flow context status
2237
+ const contextResult = await this.client.getRecord('sys_flow_context', executionId);
2238
+ if (!contextResult.success || !contextResult.data?.result) {
2239
+ executionResults.errors.push('Failed to get flow context');
2240
+ break;
2241
+ }
2242
+ const context = contextResult.data.result;
2243
+ executionResults.status = context.state;
2244
+ // Check for completed state
2245
+ if (context.state === 'complete' || context.state === 'cancelled' || context.state === 'error') {
2246
+ executionResults.end_time = context.sys_updated_on;
2247
+ executionResults.duration_ms = Date.now() - startTime;
2248
+ break;
2249
+ }
2250
+ // Check for approvals if simulating
2251
+ if (simulateApprovals) {
2252
+ const approvalResult = await this.client.searchRecords('sysapproval_approver', `source_table=sys_flow_context^document_id=${executionId}^state=requested`, 10);
2253
+ if (approvalResult.success && approvalResult.data?.result?.length > 0) {
2254
+ for (const approval of approvalResult.data.result) {
2255
+ // Auto-approve
2256
+ const approveResult = await this.client.updateRecord('sysapproval_approver', approval.sys_id, {
2257
+ state: 'approved',
2258
+ comments: 'Auto-approved by test framework'
2259
+ });
2260
+ if (approveResult.success) {
2261
+ executionResults.approvals_simulated.push({
2262
+ sys_id: approval.sys_id,
2263
+ approver: approval.approver.display_value,
2264
+ approved_at: new Date().toISOString()
2265
+ });
2266
+ }
2267
+ }
2268
+ }
2269
+ }
2270
+ // Wait before next poll
2271
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
2272
+ }
2273
+ catch (error) {
2274
+ executionResults.errors.push(error.message);
2275
+ executionResults.status = 'error';
2276
+ break;
2277
+ }
2278
+ }
2279
+ if (executionResults.status === 'running') {
2280
+ executionResults.status = 'timeout';
2281
+ executionResults.errors.push('Flow execution timed out after 60 seconds');
2282
+ }
2283
+ return executionResults;
2284
+ }
2285
+ formatTestResults(testResults) {
2286
+ let text = `🧪 Flow Test Results\n${'='.repeat(50)}\n\n`;
2287
+ text += `**Flow ID:** ${testResults.flow_id}\n`;
2288
+ text += `**Test Run ID:** ${testResults.test_run_id}\n\n`;
2289
+ // Test Data Created
2290
+ text += `📋 **Test Data Created:**\n`;
2291
+ if (testResults.created_data.test_user) {
2292
+ text += ` ✅ Test User: ${testResults.created_data.test_user.user_name} (${testResults.created_data.test_user.sys_id})\n`;
2293
+ }
2294
+ if (testResults.created_data.catalog_items.length > 0) {
2295
+ text += ` ✅ Catalog Items: ${testResults.created_data.catalog_items.length} created\n`;
2296
+ testResults.created_data.catalog_items.forEach((item) => {
2297
+ text += ` - ${item.name} ($${item.price})\n`;
2298
+ });
2299
+ }
2300
+ // Execution Results
2301
+ text += `\n🚀 **Execution Results:**\n`;
2302
+ text += ` Status: ${testResults.execution_results.status === 'complete' ? '✅' : '❌'} ${testResults.execution_results.status}\n`;
2303
+ if (testResults.execution_results.start_time) {
2304
+ text += ` Start Time: ${testResults.execution_results.start_time}\n`;
2305
+ }
2306
+ if (testResults.execution_results.end_time) {
2307
+ text += ` End Time: ${testResults.execution_results.end_time}\n`;
2308
+ }
2309
+ if (testResults.execution_results.duration_ms) {
2310
+ text += ` Duration: ${(testResults.execution_results.duration_ms / 1000).toFixed(2)}s\n`;
2311
+ }
2312
+ if (testResults.execution_results.approvals_simulated.length > 0) {
2313
+ text += `\n ✅ Approvals Simulated: ${testResults.execution_results.approvals_simulated.length}\n`;
2314
+ testResults.execution_results.approvals_simulated.forEach((approval) => {
2315
+ text += ` - ${approval.approver} at ${approval.approved_at}\n`;
2316
+ });
2317
+ }
2318
+ if (testResults.execution_results.errors.length > 0) {
2319
+ text += `\n ❌ Errors:\n`;
2320
+ testResults.execution_results.errors.forEach((error) => {
2321
+ text += ` - ${error}\n`;
2322
+ });
2323
+ }
2324
+ // Cleanup Status
2325
+ if (testResults.cleanup_status) {
2326
+ text += `\n🧹 **Cleanup Status:**\n`;
2327
+ text += ` User Deleted: ${testResults.cleanup_status.user_deleted ? '✅' : '❌'}\n`;
2328
+ text += ` Catalog Items Deleted: ${testResults.cleanup_status.catalog_items_deleted}\n`;
2329
+ text += ` Requests Cancelled: ${testResults.cleanup_status.requests_cancelled}\n`;
2330
+ }
2331
+ // Recommendations
2332
+ text += `\n💡 **Recommendations:**\n`;
2333
+ if (testResults.execution_results.status === 'complete') {
2334
+ text += ` ✅ Flow executed successfully! Ready for production use.\n`;
2335
+ }
2336
+ else if (testResults.execution_results.status === 'timeout') {
2337
+ text += ` ⚠️ Flow execution timed out. Consider:\n`;
2338
+ text += ` - Checking for infinite loops\n`;
2339
+ text += ` - Reviewing wait conditions\n`;
2340
+ text += ` - Optimizing flow performance\n`;
2341
+ }
2342
+ else {
2343
+ text += ` ❌ Flow execution failed. Please:\n`;
2344
+ text += ` - Review error messages above\n`;
2345
+ text += ` - Check flow configuration\n`;
2346
+ text += ` - Verify all dependencies exist\n`;
2347
+ }
2348
+ return text;
2349
+ }
2350
+ async handleLinkCatalogToFlow(args) {
2351
+ const { catalog_item_id, flow_id, link_type = 'flow_catalog_process', variable_mapping = [], trigger_condition = 'current.stage == "request_approved"', execution_options = {}, test_link = false } = args;
2352
+ logger_js_1.logger.info(`Linking catalog item ${catalog_item_id} to flow ${flow_id}`, {
2353
+ link_type,
2354
+ variable_mapping_count: variable_mapping.length
2355
+ });
2356
+ const linkResults = {
2357
+ catalog_item: null,
2358
+ flow: null,
2359
+ link_created: false,
2360
+ link_details: null,
2361
+ variable_mappings: [],
2362
+ test_results: null,
2363
+ errors: []
2364
+ };
2365
+ try {
2366
+ // Step 1: Find the catalog item
2367
+ const catalogResult = await this.client.searchRecords('sc_cat_item', `sys_id=${catalog_item_id}^ORname=${catalog_item_id}`, 1);
2368
+ if (!catalogResult.success || !catalogResult.data?.result?.length) {
2369
+ throw new Error(`Catalog item not found: ${catalog_item_id}`);
2370
+ }
2371
+ linkResults.catalog_item = {
2372
+ sys_id: catalogResult.data.result[0].sys_id,
2373
+ name: catalogResult.data.result[0].name,
2374
+ category: catalogResult.data.result[0].sc_categories?.display_value
2375
+ };
2376
+ // Step 2: Find the flow
2377
+ const flowResult = await this.client.searchRecords('sys_hub_flow', `sys_id=${flow_id}^ORname=${flow_id}`, 1);
2378
+ if (!flowResult.success || !flowResult.data?.result?.length) {
2379
+ throw new Error(`Flow not found: ${flow_id}`);
2380
+ }
2381
+ linkResults.flow = {
2382
+ sys_id: flowResult.data.result[0].sys_id,
2383
+ name: flowResult.data.result[0].name,
2384
+ type: flowResult.data.result[0].type
2385
+ };
2386
+ // Step 3: Create the link based on link_type
2387
+ switch (link_type) {
2388
+ case 'flow_catalog_process': {
2389
+ // Modern approach: Use Flow Designer catalog process
2390
+ const processData = {
2391
+ catalog_item: linkResults.catalog_item.sys_id,
2392
+ flow: linkResults.flow.sys_id,
2393
+ active: true,
2394
+ condition: trigger_condition,
2395
+ run_as: execution_options.run_as || 'system',
2396
+ wait_for_completion: execution_options.wait_for_completion !== false
2397
+ };
2398
+ const processResult = await this.client.createRecord('sc_cat_item_producer', processData);
2399
+ if (processResult.success && processResult.data?.result) {
2400
+ linkResults.link_created = true;
2401
+ linkResults.link_details = {
2402
+ type: 'flow_catalog_process',
2403
+ sys_id: processResult.data.result.sys_id,
2404
+ table: 'sc_cat_item_producer'
2405
+ };
2406
+ // Create variable mappings
2407
+ for (const mapping of variable_mapping) {
2408
+ const mappingData = {
2409
+ producer: processResult.data.result.sys_id,
2410
+ catalog_variable: mapping.catalog_variable,
2411
+ flow_input: mapping.flow_input,
2412
+ transform_script: mapping.transform || ''
2413
+ };
2414
+ const mappingResult = await this.client.createRecord('sc_cat_item_producer_mapping', mappingData);
2415
+ if (mappingResult.success) {
2416
+ linkResults.variable_mappings.push({
2417
+ catalog_variable: mapping.catalog_variable,
2418
+ flow_input: mapping.flow_input,
2419
+ mapping_sys_id: mappingResult.data?.result?.sys_id
2420
+ });
2421
+ }
2422
+ }
2423
+ }
2424
+ break;
2425
+ }
2426
+ case 'workflow': {
2427
+ // Legacy approach: Direct workflow assignment
2428
+ const updateData = {
2429
+ workflow: linkResults.flow.sys_id,
2430
+ no_order: false,
2431
+ no_proceed_checkout: false,
2432
+ no_quantity: false,
2433
+ no_delivery_time: false
2434
+ };
2435
+ const updateResult = await this.client.updateRecord('sc_cat_item', linkResults.catalog_item.sys_id, updateData);
2436
+ if (updateResult.success) {
2437
+ linkResults.link_created = true;
2438
+ linkResults.link_details = {
2439
+ type: 'workflow',
2440
+ field: 'workflow',
2441
+ value: linkResults.flow.sys_id
2442
+ };
2443
+ }
2444
+ break;
2445
+ }
2446
+ case 'process_engine': {
2447
+ // Process Engine approach
2448
+ const engineData = {
2449
+ catalog_item: linkResults.catalog_item.sys_id,
2450
+ engine_type: 'flow',
2451
+ engine_id: linkResults.flow.sys_id,
2452
+ active: true,
2453
+ order: 100,
2454
+ condition: trigger_condition
2455
+ };
2456
+ const engineResult = await this.client.createRecord('sc_process_flow', engineData);
2457
+ if (engineResult.success && engineResult.data?.result) {
2458
+ linkResults.link_created = true;
2459
+ linkResults.link_details = {
2460
+ type: 'process_engine',
2461
+ sys_id: engineResult.data.result.sys_id,
2462
+ table: 'sc_process_flow'
2463
+ };
2464
+ }
2465
+ break;
2466
+ }
2467
+ }
2468
+ // Step 4: Test the link if requested
2469
+ if (test_link && linkResults.link_created) {
2470
+ logger_js_1.logger.info('Testing catalog-flow link with sample request');
2471
+ const testRequestData = {
2472
+ cat_item: linkResults.catalog_item.sys_id,
2473
+ requested_for: 'admin', // Default to admin user
2474
+ quantity: 1,
2475
+ comments: `Test request for catalog-flow link verification [${new Date().toISOString()}]`
2476
+ };
2477
+ // Add any mapped variables with test values
2478
+ for (const mapping of linkResults.variable_mappings) {
2479
+ testRequestData[`variables.${mapping.catalog_variable}`] = 'Test Value';
2480
+ }
2481
+ const testResult = await this.client.createRecord('sc_request', testRequestData);
2482
+ if (testResult.success && testResult.data?.result) {
2483
+ linkResults.test_results = {
2484
+ request_created: true,
2485
+ request_number: testResult.data.result.number,
2486
+ request_sys_id: testResult.data.result.sys_id,
2487
+ status: 'Check the request in ServiceNow to verify flow execution'
2488
+ };
2489
+ }
2490
+ else {
2491
+ linkResults.test_results = {
2492
+ request_created: false,
2493
+ error: 'Failed to create test request'
2494
+ };
2495
+ }
2496
+ }
2497
+ // Format results
2498
+ let resultText = `🔗 Catalog-Flow Link Results\n${'='.repeat(50)}\n\n`;
2499
+ resultText += `**Catalog Item:** ${linkResults.catalog_item.name} (${linkResults.catalog_item.sys_id})\n`;
2500
+ resultText += `**Flow:** ${linkResults.flow.name} (${linkResults.flow.sys_id})\n`;
2501
+ resultText += `**Link Type:** ${link_type}\n\n`;
2502
+ if (linkResults.link_created) {
2503
+ resultText += `✅ **Link Created Successfully!**\n`;
2504
+ resultText += ` Type: ${linkResults.link_details.type}\n`;
2505
+ if (linkResults.link_details.sys_id) {
2506
+ resultText += ` Record: ${linkResults.link_details.table} (${linkResults.link_details.sys_id})\n`;
2507
+ }
2508
+ if (linkResults.variable_mappings.length > 0) {
2509
+ resultText += `\n📋 **Variable Mappings Created:**\n`;
2510
+ linkResults.variable_mappings.forEach(mapping => {
2511
+ resultText += ` - ${mapping.catalog_variable} → ${mapping.flow_input}\n`;
2512
+ });
2513
+ }
2514
+ if (linkResults.test_results) {
2515
+ resultText += `\n🧪 **Test Results:**\n`;
2516
+ if (linkResults.test_results.request_created) {
2517
+ resultText += ` ✅ Test request created: ${linkResults.test_results.request_number}\n`;
2518
+ resultText += ` 📍 ${linkResults.test_results.status}\n`;
2519
+ }
2520
+ else {
2521
+ resultText += ` ❌ ${linkResults.test_results.error}\n`;
2522
+ }
2523
+ }
2524
+ resultText += `\n💡 **Next Steps:**\n`;
2525
+ resultText += `1. Test the catalog item by creating a request\n`;
2526
+ resultText += `2. Verify the flow executes with correct inputs\n`;
2527
+ resultText += `3. Monitor the request fulfillment process\n`;
2528
+ if (link_type === 'flow_catalog_process') {
2529
+ resultText += `\n📝 **Note:** Using modern Flow Designer catalog process\n`;
2530
+ resultText += ` - Flow will trigger on: ${trigger_condition}\n`;
2531
+ resultText += ` - Execution context: ${execution_options.run_as || 'system'}\n`;
2532
+ }
2533
+ }
2534
+ else {
2535
+ resultText += `❌ **Failed to create link**\n`;
2536
+ if (linkResults.errors.length > 0) {
2537
+ resultText += `\nErrors:\n`;
2538
+ linkResults.errors.forEach(error => {
2539
+ resultText += ` - ${error}\n`;
2540
+ });
2541
+ }
2542
+ }
2543
+ return {
2544
+ content: [{
2545
+ type: 'text',
2546
+ text: resultText
2547
+ }]
2548
+ };
2549
+ }
2550
+ catch (error) {
2551
+ logger_js_1.logger.error('Error linking catalog to flow:', error);
2552
+ linkResults.errors.push(error.message);
2553
+ return {
2554
+ content: [{
2555
+ type: 'text',
2556
+ text: `❌ Failed to link catalog to flow\n\nError: ${error.message}\n\n` +
2557
+ `💡 Troubleshooting:\n` +
2558
+ `1. Verify catalog item exists: ${catalog_item_id}\n` +
2559
+ `2. Verify flow exists: ${flow_id}\n` +
2560
+ `3. Check permissions for creating ${link_type} links\n` +
2561
+ `4. Ensure flow is active and published\n\n` +
2562
+ `Results: ${JSON.stringify(linkResults, null, 2)}`
2563
+ }]
2564
+ };
2565
+ }
2566
+ }
2567
+ async handleCleanupTestArtifacts(args) {
2568
+ const { artifact_types = ['catalog_items', 'flows', 'users'], test_patterns = ['Test%', 'Mock%', 'Demo%', '%_test_%', '%test', '%mock'], max_age_hours = 1, dry_run = false, preserve_update_set_entries = true, update_set_filter } = args;
2569
+ logger_js_1.logger.info('Starting test artifact cleanup', {
2570
+ artifact_types,
2571
+ test_patterns,
2572
+ max_age_hours,
2573
+ dry_run
2574
+ });
2575
+ const cleanupResults = {
2576
+ start_time: new Date().toISOString(),
2577
+ dry_run,
2578
+ artifacts_found: {
2579
+ catalog_items: [],
2580
+ flows: [],
2581
+ users: [],
2582
+ requests: []
2583
+ },
2584
+ artifacts_deleted: {
2585
+ catalog_items: 0,
2586
+ flows: 0,
2587
+ users: 0,
2588
+ requests: 0
2589
+ },
2590
+ update_set_entries_preserved: 0,
2591
+ errors: [],
2592
+ summary: ''
2593
+ };
2594
+ try {
2595
+ // Calculate cutoff time
2596
+ const cutoffTime = new Date();
2597
+ cutoffTime.setHours(cutoffTime.getHours() - max_age_hours);
2598
+ const cutoffISO = cutoffTime.toISOString();
2599
+ // Process each artifact type
2600
+ for (const artifactType of artifact_types) {
2601
+ if (artifactType === 'all' || artifact_types.includes('catalog_items')) {
2602
+ await this.cleanupTestCatalogItems(test_patterns, cutoffISO, dry_run, cleanupResults, update_set_filter);
2603
+ }
2604
+ if (artifactType === 'all' || artifact_types.includes('flows')) {
2605
+ await this.cleanupTestFlows(test_patterns, cutoffISO, dry_run, cleanupResults, update_set_filter);
2606
+ }
2607
+ if (artifactType === 'all' || artifact_types.includes('users')) {
2608
+ await this.cleanupTestUsers(test_patterns, cutoffISO, dry_run, cleanupResults, update_set_filter);
2609
+ }
2610
+ if (artifactType === 'all' || artifact_types.includes('requests')) {
2611
+ await this.cleanupTestRequests(test_patterns, cutoffISO, dry_run, cleanupResults, update_set_filter);
2612
+ }
2613
+ }
2614
+ // Generate summary
2615
+ const totalFound = Object.values(cleanupResults.artifacts_found)
2616
+ .reduce((sum, items) => sum + (Array.isArray(items) ? items.length : 0), 0);
2617
+ const totalDeleted = Object.values(cleanupResults.artifacts_deleted)
2618
+ .reduce((sum, count) => sum + count, 0);
2619
+ cleanupResults.summary = dry_run
2620
+ ? `🔍 Dry Run: Found ${totalFound} test artifacts that would be cleaned up`
2621
+ : `🧹 Cleaned up ${totalDeleted} test artifacts successfully`;
2622
+ // Format results
2623
+ let resultText = `🧹 Test Artifact Cleanup Results\n${'='.repeat(50)}\n\n`;
2624
+ if (dry_run) {
2625
+ resultText += `🔍 **DRY RUN MODE** - No actual deletion performed\n\n`;
2626
+ }
2627
+ resultText += `⏰ **Cleanup Configuration:**\n`;
2628
+ resultText += `- Artifact Types: ${artifact_types.join(', ')}\n`;
2629
+ resultText += `- Test Patterns: ${test_patterns.join(', ')}\n`;
2630
+ resultText += `- Max Age: ${max_age_hours} hours\n`;
2631
+ resultText += `- Cutoff Time: ${cutoffISO}\n`;
2632
+ resultText += `- Preserve Update Set Entries: ${preserve_update_set_entries ? '✅' : '❌'}\n\n`;
2633
+ // Report findings by type
2634
+ ['catalog_items', 'flows', 'users', 'requests'].forEach(type => {
2635
+ const found = cleanupResults.artifacts_found[type] || [];
2636
+ const deleted = cleanupResults.artifacts_deleted[type] || 0;
2637
+ if (found.length > 0) {
2638
+ resultText += `📦 **${type.replace('_', ' ').toUpperCase()}:**\n`;
2639
+ resultText += ` ${dry_run ? 'Found' : 'Deleted'}: ${dry_run ? found.length : deleted}\n`;
2640
+ if (dry_run && found.length > 0) {
2641
+ found.slice(0, 10).forEach((item) => {
2642
+ resultText += ` - ${item.name} (${item.sys_id}) - Created: ${item.sys_created_on}\n`;
2643
+ });
2644
+ if (found.length > 10) {
2645
+ resultText += ` ... and ${found.length - 10} more\n`;
2646
+ }
2647
+ }
2648
+ resultText += '\n';
2649
+ }
2650
+ });
2651
+ if (cleanupResults.update_set_entries_preserved > 0) {
2652
+ resultText += `📋 **Update Set Audit Trail Preserved:** ${cleanupResults.update_set_entries_preserved} entries\n\n`;
2653
+ }
2654
+ if (cleanupResults.errors.length > 0) {
2655
+ resultText += `❌ **Errors:**\n`;
2656
+ cleanupResults.errors.forEach(error => {
2657
+ resultText += ` - ${error}\n`;
2658
+ });
2659
+ resultText += '\n';
2660
+ }
2661
+ resultText += `✅ **${cleanupResults.summary}**\n\n`;
2662
+ if (!dry_run && totalDeleted > 0) {
2663
+ resultText += `💡 **Note:** Update Set entries have been preserved as audit trail.\n`;
2664
+ resultText += `This shows that testing was performed and cleanup was completed.\n\n`;
2665
+ }
2666
+ if (dry_run) {
2667
+ resultText += `▶️ **Next Steps:**\n`;
2668
+ resultText += `1. Review the artifacts that would be deleted\n`;
2669
+ resultText += `2. Run again with dry_run: false to perform actual cleanup\n`;
2670
+ resultText += `3. Verify Update Set entries are preserved as intended\n`;
2671
+ }
2672
+ return {
2673
+ content: [{
2674
+ type: 'text',
2675
+ text: resultText
2676
+ }]
2677
+ };
2678
+ }
2679
+ catch (error) {
2680
+ logger_js_1.logger.error('Error during test artifact cleanup:', error);
2681
+ return {
2682
+ content: [{
2683
+ type: 'text',
2684
+ text: `❌ Test artifact cleanup failed\n\nError: ${error.message}\n\n` +
2685
+ `🔧 Troubleshooting:\n` +
2686
+ `1. Check ServiceNow connection and permissions\n` +
2687
+ `2. Verify test patterns are correct\n` +
2688
+ `3. Ensure artifacts exist and are accessible\n\n` +
2689
+ `Debug Info: ${JSON.stringify(cleanupResults, null, 2)}`
2690
+ }]
2691
+ };
2692
+ }
2693
+ }
2694
+ async cleanupTestCatalogItems(patterns, cutoffTime, dryRun, results, updateSetFilter) {
2695
+ try {
2696
+ // Build query for test catalog items
2697
+ const patternQueries = patterns.map(pattern => {
2698
+ if (pattern.includes('%')) {
2699
+ return `nameLIKE${pattern.replace(/%/g, '')}`;
2700
+ }
2701
+ else {
2702
+ return `name=${pattern}`;
2703
+ }
2704
+ });
2705
+ const query = `(${patternQueries.join('^OR')})^sys_created_on<${cutoffTime}`;
2706
+ const searchResult = await this.client.searchRecords('sc_cat_item', query, 100);
2707
+ if (searchResult.success && searchResult.data?.result) {
2708
+ results.artifacts_found.catalog_items = searchResult.data.result.map((item) => ({
2709
+ sys_id: item.sys_id,
2710
+ name: item.name,
2711
+ sys_created_on: item.sys_created_on
2712
+ }));
2713
+ if (!dryRun) {
2714
+ // Delete each catalog item
2715
+ for (const item of searchResult.data.result) {
2716
+ const deleteResult = await this.client.deleteRecord('sc_cat_item', item.sys_id);
2717
+ if (deleteResult.success) {
2718
+ results.artifacts_deleted.catalog_items++;
2719
+ }
2720
+ else {
2721
+ results.errors.push(`Failed to delete catalog item ${item.name}: ${deleteResult.error}`);
2722
+ }
2723
+ }
2724
+ }
2725
+ }
2726
+ }
2727
+ catch (error) {
2728
+ results.errors.push(`Error cleaning catalog items: ${error.message}`);
2729
+ }
2730
+ }
2731
+ async cleanupTestFlows(patterns, cutoffTime, dryRun, results, updateSetFilter) {
2732
+ try {
2733
+ const patternQueries = patterns.map(pattern => {
2734
+ if (pattern.includes('%')) {
2735
+ return `nameLIKE${pattern.replace(/%/g, '')}`;
2736
+ }
2737
+ else {
2738
+ return `name=${pattern}`;
2739
+ }
2740
+ });
2741
+ const query = `(${patternQueries.join('^OR')})^sys_created_on<${cutoffTime}`;
2742
+ const searchResult = await this.client.searchRecords('sys_hub_flow', query, 100);
2743
+ if (searchResult.success && searchResult.data?.result) {
2744
+ results.artifacts_found.flows = searchResult.data.result.map((flow) => ({
2745
+ sys_id: flow.sys_id,
2746
+ name: flow.name,
2747
+ sys_created_on: flow.sys_created_on
2748
+ }));
2749
+ if (!dryRun) {
2750
+ for (const flow of searchResult.data.result) {
2751
+ const deleteResult = await this.client.deleteRecord('sys_hub_flow', flow.sys_id);
2752
+ if (deleteResult.success) {
2753
+ results.artifacts_deleted.flows++;
2754
+ }
2755
+ else {
2756
+ results.errors.push(`Failed to delete flow ${flow.name}: ${deleteResult.error}`);
2757
+ }
2758
+ }
2759
+ }
2760
+ }
2761
+ }
2762
+ catch (error) {
2763
+ results.errors.push(`Error cleaning flows: ${error.message}`);
2764
+ }
2765
+ }
2766
+ async cleanupTestUsers(patterns, cutoffTime, dryRun, results, updateSetFilter) {
2767
+ try {
2768
+ const patternQueries = patterns.map(pattern => {
2769
+ if (pattern.includes('%')) {
2770
+ return `user_nameLIKE${pattern.replace(/%/g, '')}^ORfirst_nameLIKE${pattern.replace(/%/g, '')}`;
2771
+ }
2772
+ else {
2773
+ return `user_name=${pattern}^ORfirst_name=${pattern}`;
2774
+ }
2775
+ });
2776
+ const query = `(${patternQueries.join('^OR')})^sys_created_on<${cutoffTime}^active=false`;
2777
+ const searchResult = await this.client.searchRecords('sys_user', query, 100);
2778
+ if (searchResult.success && searchResult.data?.result) {
2779
+ results.artifacts_found.users = searchResult.data.result.map((user) => ({
2780
+ sys_id: user.sys_id,
2781
+ name: user.user_name || user.name,
2782
+ sys_created_on: user.sys_created_on
2783
+ }));
2784
+ if (!dryRun) {
2785
+ for (const user of searchResult.data.result) {
2786
+ const deleteResult = await this.client.deleteRecord('sys_user', user.sys_id);
2787
+ if (deleteResult.success) {
2788
+ results.artifacts_deleted.users++;
2789
+ }
2790
+ else {
2791
+ results.errors.push(`Failed to delete user ${user.user_name}: ${deleteResult.error}`);
2792
+ }
2793
+ }
2794
+ }
2795
+ }
2796
+ }
2797
+ catch (error) {
2798
+ results.errors.push(`Error cleaning users: ${error.message}`);
2799
+ }
2800
+ }
2801
+ async cleanupTestRequests(patterns, cutoffTime, dryRun, results, updateSetFilter) {
2802
+ try {
2803
+ const patternQueries = patterns.map(pattern => {
2804
+ if (pattern.includes('%')) {
2805
+ return `short_descriptionLIKE${pattern.replace(/%/g, '')}`;
2806
+ }
2807
+ else {
2808
+ return `short_description=${pattern}`;
2809
+ }
2810
+ });
2811
+ const query = `(${patternQueries.join('^OR')})^sys_created_on<${cutoffTime}`;
2812
+ const searchResult = await this.client.searchRecords('sc_request', query, 100);
2813
+ if (searchResult.success && searchResult.data?.result) {
2814
+ results.artifacts_found.requests = searchResult.data.result.map((req) => ({
2815
+ sys_id: req.sys_id,
2816
+ name: req.number || req.short_description,
2817
+ sys_created_on: req.sys_created_on
2818
+ }));
2819
+ if (!dryRun) {
2820
+ for (const request of searchResult.data.result) {
2821
+ // Cancel request instead of deleting (safer)
2822
+ const updateResult = await this.client.updateRecord('sc_request', request.sys_id, {
2823
+ state: '4', // Cancelled
2824
+ comments: 'Cancelled by test cleanup automation'
2825
+ });
2826
+ if (updateResult.success) {
2827
+ results.artifacts_deleted.requests++;
2828
+ }
2829
+ else {
2830
+ results.errors.push(`Failed to cancel request ${request.number}: ${updateResult.error}`);
2831
+ }
2832
+ }
2833
+ }
2834
+ }
2835
+ }
2836
+ catch (error) {
2837
+ results.errors.push(`Error cleaning requests: ${error.message}`);
2838
+ }
2839
+ }
1600
2840
  getServiceNowUrl() {
1601
2841
  // This would need to get the instance URL from credentials
1602
2842
  return 'https://instance.service-now.com';