@nbakka/mcp-appium 3.0.24 → 3.0.26

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/lib/server.js CHANGED
@@ -362,6 +362,31 @@ tool(
362
362
  }
363
363
  );
364
364
 
365
+ tool(
366
+ "mobile_learn_app_context_and_navigation_guidelines",
367
+ "Reads previously saved app context and navigation guidelines and returns them so they can be used to navigate through the app before generating test steps.",
368
+ {},
369
+ async () => {
370
+ try {
371
+ const guidelinesFilePath = path.join(os.homedir(), 'Desktop', 'app_context_and_navigation_guidelines.txt');
372
+ const fileContent = await fs.readFile(guidelinesFilePath, "utf-8");
373
+
374
+ const guidelines = fileContent
375
+ .split("\n")
376
+ .map(line => line.trim())
377
+ .filter(line => line.length > 0);
378
+
379
+ if (guidelines.length === 0) {
380
+ return "No app context and navigation guidelines found.";
381
+ }
382
+
383
+ return `App context and navigation guidelines loaded: ${JSON.stringify({ guidelines })}`;
384
+ } catch (error) {
385
+ return `Error reading app context and navigation guidelines: ${error.message}`;
386
+ }
387
+ }
388
+ );
389
+
365
390
  // Tool 3: Update Test Scenario (Always appends with green color)
366
391
  tool(
367
392
  "mobile_update_test_scenario",
@@ -471,7 +496,7 @@ tool(
471
496
 
472
497
  tool("mobile_list_elements_on_screennn", "List elements on screen and their coordinates, with display text or accessibility label. Returns the complete XML structure to maintain hierarchy for XPath creation. Do not cache this result.", {}, async ({}) => {
473
498
  requireRobot();
474
- const xmlStructure = await robot.getElementsOnScreen();
499
+ const xmlStructure = await robot.getXmlStructure();
475
500
  return `Complete XML structure of current screen: ${JSON.stringify(xmlStructure)}`;
476
501
  });
477
502
 
@@ -558,1349 +583,1349 @@ tool(
558
583
  }
559
584
  );
560
585
 
561
- tool(
562
- "mobile_fetch_jira_ticket",
563
- "Fetch JIRA ticket information including summary, description, and extract Figma links from description",
564
- {
565
- ticketId: zod_1.z.string().describe("The JIRA ticket ID (e.g., HDA-434)"),
566
- },
567
- async ({ ticketId }) => {
568
- try {
569
- // Read JIRA credentials from desktop/jira.json file
570
- const jiraConfigPath = path.join(os.homedir(), 'Desktop', 'jira.json');
571
-
572
- let jiraConfig;
573
- try {
574
- const configContent = await fs.readFile(jiraConfigPath, 'utf-8');
575
- jiraConfig = JSON.parse(configContent);
576
- } catch (error) {
577
- throw new Error(`Failed to read JIRA config from ${jiraConfigPath}: ${error.message}`);
578
- }
579
-
580
- // Extract all required values from JSON file
581
- const { api: jiraApiToken, baseUrl: jiraBaseUrl, email: jiraEmail } = jiraConfig;
582
-
583
- if (!jiraApiToken) {
584
- throw new Error('JIRA API token not found in jira.json file. Please ensure the file contains "api" field.');
585
- }
586
-
587
- if (!jiraBaseUrl) {
588
- throw new Error('JIRA base URL not found in jira.json file. Please ensure the file contains "baseUrl" field.');
589
- }
590
-
591
- if (!jiraEmail) {
592
- throw new Error('JIRA email not found in jira.json file. Please ensure the file contains "email" field.');
593
- }
594
-
595
- // Create Basic Auth token
596
- const auth = Buffer.from(`${jiraEmail}:${jiraApiToken}`).toString('base64');
597
-
598
- // Fetch ticket from JIRA API
599
- const response = await axios.get(
600
- `${jiraBaseUrl}/rest/api/3/issue/${ticketId}`,
601
- {
602
- headers: {
603
- 'Authorization': `Basic ${auth}`,
604
- 'Accept': 'application/json',
605
- 'Content-Type': 'application/json'
606
- }
607
- }
608
- );
609
-
610
- const issue = response.data;
611
-
612
- // Extract summary and description
613
- const summary = issue.fields.summary || 'No summary available';
614
- const description = issue.fields.description ?
615
- extractTextFromADF(issue.fields.description) :
616
- 'No description available';
617
-
618
- // Extract Figma links directly from ADF structure
619
- const figmaLinks = extractFigmaLinksFromADF(issue.fields.description);
620
-
621
- // Format response
622
- const result = {
623
- ticketId: ticketId,
624
- summary: summary,
625
- description: description,
626
- figmaLinks: figmaLinks.length > 0 ? figmaLinks : ['No Figma links found']
627
- };
628
-
629
- return `JIRA Ticket Information:
630
- Ticket ID: ${result.ticketId}
631
- Summary: ${result.summary}
632
- Description: ${result.description}
633
- Figma Links: ${result.figmaLinks.join(', ')}`;
634
-
635
- } catch (error) {
636
- if (error.response && error.response.status === 404) {
637
- return `Error: JIRA ticket ${ticketId} not found. Please check the ticket ID.`;
638
- } else if (error.response && error.response.status === 401) {
639
- return `Error: Authentication failed. Please check your JIRA credentials.`;
640
- } else {
641
- return `Error fetching JIRA ticket: ${error.message}`;
642
- }
643
- }
644
- }
645
- );
646
-
647
- // Helper function to extract text from Atlassian Document Format (ADF)
648
- function extractTextFromADF(adfContent) {
649
- if (!adfContent || typeof adfContent !== 'object') {
650
- return String(adfContent || '');
651
- }
652
-
653
- let text = '';
654
-
655
- function traverse(node) {
656
- if (node.type === 'text') {
657
- text += node.text || '';
658
- } else if (node.content && Array.isArray(node.content)) {
659
- node.content.forEach(traverse);
660
- }
661
-
662
- // Add line breaks for paragraphs
663
- if (node.type === 'paragraph') {
664
- text += '\n';
665
- }
666
- }
667
-
668
- if (adfContent.content) {
669
- adfContent.content.forEach(traverse);
670
- }
671
-
672
- return text.trim();
673
- }
674
-
675
- // Helper function to extract Figma links directly from ADF structure
676
- function extractFigmaLinksFromADF(adfContent) {
677
- if (!adfContent || typeof adfContent !== 'object') {
678
- return [];
679
- }
680
-
681
- const figmaLinks = [];
682
-
683
- function traverse(node) {
684
- // Check for inlineCard nodes with Figma URLs
685
- if (node.type === 'inlineCard' && node.attrs && node.attrs.url) {
686
- const url = node.attrs.url;
687
- if (url.includes('figma.com')) {
688
- figmaLinks.push(url);
689
- }
690
- }
691
-
692
- // Check for link marks with Figma URLs
693
- if (node.marks && Array.isArray(node.marks)) {
694
- node.marks.forEach(mark => {
695
- if (mark.type === 'link' && mark.attrs && mark.attrs.href) {
696
- const href = mark.attrs.href;
697
- if (href.includes('figma.com')) {
698
- figmaLinks.push(href);
699
- }
700
- }
701
- });
702
- }
703
-
704
- // Traverse child content
705
- if (node.content && Array.isArray(node.content)) {
706
- node.content.forEach(traverse);
707
- }
708
- }
709
-
710
- if (adfContent.content) {
711
- adfContent.content.forEach(traverse);
712
- }
713
-
714
- // Remove duplicates and return
715
- return [...new Set(figmaLinks)];
716
- }
717
-
718
- // ----------------------
719
- // Helper: Extract File ID & Node ID
720
- // ----------------------
721
- function extractFileAndNodeId(url) {
722
- const patterns = [
723
- /figma\.com\/file\/([a-zA-Z0-9]+)/,
724
- /figma\.com\/design\/([a-zA-Z0-9]+)/,
725
- /figma\.com\/proto\/([a-zA-Z0-9]+)/
726
- ];
727
-
728
- let fileId = null;
729
- for (const pattern of patterns) {
730
- const match = url.match(pattern);
731
- if (match) {
732
- fileId = match[1];
733
- break;
734
- }
735
- }
736
-
737
- // Extract node-id if present
738
- const nodeMatch = url.match(/[?&]node-id=([^&]+)/);
739
- let nodeId = null;
740
- if (nodeMatch) {
741
- // Replace dash with colon (Figma expects 13:5951 instead of 13-5951)
742
- nodeId = decodeURIComponent(nodeMatch[1]).replace(/-/g, ":");
743
- }
744
-
745
- return { fileId, nodeId };
746
- }
747
-
748
- // ----------------------
749
- // TOOL 1: Export Figma to PNG
750
- // ----------------------
751
- tool(
752
- "mobile_export_figma_png",
753
- "Export Figma file as PNG",
754
- {
755
- figmaUrl: zod_1.z.string().describe("The Figma file URL to export as PNG")
756
- },
757
- async ({ figmaUrl }) => {
758
- try {
759
- // Load Figma token from Desktop/figma.json
760
- const figmaConfigPath = path.join(os.homedir(), "Desktop", "figma.json");
761
- const configContent = await fs.readFile(figmaConfigPath, "utf-8");
762
- const { token: figmaToken } = JSON.parse(configContent);
763
-
764
- if (!figmaToken) throw new Error("Figma API token missing in figma.json");
765
-
766
- // Extract fileId and nodeId from URL
767
- const { fileId, nodeId } = extractFileAndNodeId(figmaUrl);
768
- if (!fileId) throw new Error("Invalid Figma URL - cannot extract fileId");
769
-
770
- let idsToExport = [];
771
-
772
- if (nodeId) {
773
- // Use node-id directly from URL
774
- idsToExport = [nodeId];
775
- } else {
776
- // Fallback: scan file to collect all top-level frames
777
- const fileResponse = await axios.get(
778
- `https://api.figma.com/v1/files/${fileId}`,
779
- { headers: { "X-Figma-Token": figmaToken } }
780
- );
781
-
782
- fileResponse.data.document.children?.forEach(page => {
783
- page.children?.forEach(child => {
784
- if (child.type === "FRAME") idsToExport.push(child.id);
785
- });
786
- });
787
-
788
- if (idsToExport.length === 0)
789
- throw new Error("No frames found in Figma file");
790
- }
791
-
792
- // Request PNG export with higher scale for better quality
793
- const exportResponse = await axios.get(
794
- `https://api.figma.com/v1/images/${fileId}`,
795
- {
796
- headers: { "X-Figma-Token": figmaToken },
797
- params: {
798
- ids: idsToExport.join(","),
799
- format: "png",
800
- scale: "2" // 2x scale for better quality
801
- }
802
- }
803
- );
804
-
805
- const exportPath = path.join(os.homedir(), "Desktop", "figma");
806
-
807
- // Clear the folder before creating new PNGs
808
- try {
809
- // Check if folder exists
810
- await fs.access(exportPath);
811
- // If folder exists, remove all contents
812
- const files = await fs.readdir(exportPath);
813
- await Promise.all(
814
- files.map(file => fs.unlink(path.join(exportPath, file)))
815
- );
816
- } catch (err) {
817
- // Folder doesn't exist or is empty, no need to clear
818
- if (err.code !== 'ENOENT') {
819
- }
820
- }
821
-
822
- // Ensure directory exists
823
- await fs.mkdir(exportPath, { recursive: true });
824
-
825
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5);
826
-
827
- // Download all PNG images
828
- const downloadPromises = Object.entries(exportResponse.data.images).map(
829
- async ([nodeId, pngUrl], index) => {
830
- if (!pngUrl) throw new Error(`No PNG export URL returned for node ${nodeId}`);
831
-
832
- const pngResponse = await axios.get(pngUrl, { responseType: "arraybuffer" });
833
- const filename = idsToExport.length > 1
834
- ? `figma-export-${timestamp}-${index + 1}.png`
835
- : `figma-export-${timestamp}.png`;
836
- const pngPath = path.join(exportPath, filename);
837
-
838
- await fs.writeFile(pngPath, pngResponse.data);
839
- return pngPath;
840
- }
841
- );
842
-
843
- const savedPaths = await Promise.all(downloadPromises);
844
-
845
- return `✅ PNG Export Complete: ${savedPaths.length} file(s) saved to ${exportPath}`;
846
- } catch (err) {
847
- return `❌ Error exporting Figma PNG: ${err.message}`;
848
- }
849
- }
850
- );
851
-
852
- tool(
853
- "fetch_testcases_from_tcms",
854
- "Before calling these tool, folder name can be analysed by data fetched from jira ticket info. Before generating test cases for a jira ticket, always fetch existing test cases from TCMS tool for a specific folder",
855
- {
856
- projectKey: zod_1.z.string().describe("The project key Default: SCRUM"),
857
- folderName: zod_1.z.string().describe("The folder name to filter test cases (e.g., PDP), folder name can to be fetched from jira ticket info")
858
- },
859
- async ({ projectKey, folderName }) => {
860
- try {
861
- // Load AIO token from Desktop/aio.json
862
- const aioConfigPath = path.join(os.homedir(), "Desktop", "aio.json");
863
- const configContent = await fs.readFile(aioConfigPath, "utf-8");
864
- const { token } = JSON.parse(configContent);
865
-
866
- if (!token) throw new Error("AIO token missing in aio.json");
867
-
868
- // Make API request to TCMS
869
- const response = await axios.get(
870
- `https://tcms.aiojiraapps.com/aio-tcms/api/v1/project/${projectKey}/testcase`,
871
- {
872
- headers: {
873
- "accept": "application/json;charset=utf-8",
874
- "Authorization": `AioAuth ${token}`
875
- }
876
- }
877
- );
878
-
879
- const testCases = response.data.items || [];
880
-
881
- // Filter test cases by folder name
882
- const filteredTestCases = testCases.filter(testCase =>
883
- testCase.folder && testCase.folder.name === folderName
884
- );
885
-
886
- if (filteredTestCases.length === 0) {
887
- return `No test cases found in folder: ${folderName}`;
888
- }
889
-
890
- // Extract key, title, and folder name
891
- const extractedTestCases = filteredTestCases.map(testCase => ({
892
- key: testCase.key,
893
- title: testCase.title,
894
- folderName: testCase.folder.name
895
- }));
896
-
897
- // Format as string response
898
- const result = `✅ Found ${extractedTestCases.length} test cases in folder: ${folderName}\n\n` +
899
- extractedTestCases.map(tc =>
900
- `Key: ${tc.key}\nTitle: ${tc.title}\nFolder: ${tc.folderName}\n---`
901
- ).join('\n');
902
-
903
- return result;
904
-
905
- } catch (err) {
906
- if (err.response) {
907
- return `❌ TCMS API Error: ${err.response.status} - ${err.response.data?.message || err.response.statusText}`;
908
- }
909
- return `❌ Error fetching test cases: ${err.message}`;
910
- }
911
- }
912
- );
913
-
914
- tool(
915
- "generate_testcases_from_ticket_data",
916
- "Generate manual test cases by analyzing PNG design with JIRA requirements",
917
- {
918
- jiraSummary: zod_1.z.string().describe("Jira issue summary"),
919
- jiraDescription: zod_1.z.string().describe("Jira issue description"),
920
- existingTestCases: zod_1.z.string().optional().describe("Existing test cases from TCMS")
921
- },
922
- async ({ jiraSummary, jiraDescription, existingTestCases }) => {
923
- try {
924
- // Clear the generated test cases file before starting
925
- const testCasesFilePath = path.join(__dirname, 'generated-testcases.txt');
926
- await fs.writeFile(testCasesFilePath, ''); // Clear the file
927
-
928
- // Load OpenAI API key from Desktop/openai.json
929
- const openaiConfigPath = path.join(os.homedir(), "Desktop", "openai.json");
930
- const configContent = await fs.readFile(openaiConfigPath, "utf-8");
931
- const { apiKey } = JSON.parse(configContent.trim());
932
-
933
- // Load test case generation guidelines
934
- const guidelinesPath = path.join(__dirname, 'testcases-generation-context.txt');
935
- const guidelines = await fs.readFile(guidelinesPath, "utf-8");
936
-
937
- const figmaDir = path.join(os.homedir(), "Desktop", "figma");
938
- const files = await fs.readdir(figmaDir);
939
- const pngFiles = files.filter(file => file.toLowerCase().endsWith('.png'));
940
-
941
- if (pngFiles.length === 0) throw new Error("No PNG files found in figma folder");
942
-
943
- // Get the latest PNG file
944
- const latestPng = pngFiles.sort((a, b) => b.localeCompare(a))[0];
945
- const pngPath = path.join(figmaDir, latestPng);
946
-
947
- const client = new OpenAI({ apiKey });
948
-
949
- // Convert PNG to base64 for vision API
950
- const pngBuffer = await fs.readFile(pngPath);
951
- const base64Image = pngBuffer.toString('base64');
952
-
953
- // Start OpenAI generation (this will run in background due to timeout)
954
- client.chat.completions.create({
955
- model: "gpt-5", // Use GPT-5 model for image analysis
956
- messages: [{
957
- role: "user",
958
- content: [
959
- {
960
- type: "text",
961
- text: `Generate manual test cases based on the following:
962
-
963
- JIRA Summary: ${jiraSummary}
964
-
965
- JIRA Description: ${jiraDescription}
966
-
967
- ${existingTestCases ? `Existing Test Cases from TCMS:
968
- ${existingTestCases}
969
-
970
- Please consider these existing test cases and generate additional comprehensive test cases that complement them.` : ''}
971
-
972
- Test Case Generation Guidelines:
973
- ${guidelines}`
974
- },
975
- {
976
- type: "image_url",
977
- image_url: {
978
- url: `data:image/png;base64,${base64Image}`,
979
- detail: "high"
980
- }
981
- }
982
- ]
983
- }],
984
- max_completion_tokens: 10000
985
- }).then(async (completion) => {
986
- // Save test cases to file when generation completes
987
- const testCases = completion.choices[0].message.content;
988
- await fs.writeFile(testCasesFilePath, testCases);
989
- }).catch(async (error) => {
990
- // Save error to file if generation fails
991
- await fs.writeFile(testCasesFilePath, `Error generating test cases: ${error.message}`);
992
- });
993
-
994
- return "✅ Test case generation started. Use 'check_testcases_status' tool to check if generation is complete. Try max 10 times";
995
- } catch (err) {
996
- return `❌ Error starting test case generation: ${err.message}`;
997
- }
998
- }
999
- );
1000
-
1001
- tool(
1002
- "check_testcases_status",
1003
- "Check if test cases have been generated and saved to file",
1004
- {},
1005
- async () => {
1006
- try {
1007
- // Wait for 20 seconds before checking
1008
- await new Promise(resolve => setTimeout(resolve, 25000));
1009
-
1010
- const testCasesFilePath = path.join(__dirname, 'generated-testcases.txt');
1011
-
1012
- // Check if file exists and has content
1013
- try {
1014
- const fileContent = await fs.readFile(testCasesFilePath, 'utf-8');
1015
-
1016
- if (fileContent.trim().length === 0) {
1017
- return "❌ Test cases are still being generated. Please wait and try again.";
1018
- }
1019
-
1020
- if (fileContent.startsWith('Error generating test cases:')) {
1021
- return `❌ ${fileContent}`;
1022
- }
1023
-
1024
- return `✅ Test cases generated successfully!\n\n${fileContent}`;
1025
- } catch (fileError) {
1026
- return "❌ Test cases file not found or still being created. Please wait and try again.";
1027
- }
1028
- } catch (err) {
1029
- return `❌ Error checking test cases status: ${err.message}`;
1030
- }
1031
- }
1032
- );
1033
-
1034
- // Fix 2: Updated review_testcases tool with proper JSON handling and open module usage
1035
- tool(
1036
- "review_testcases",
1037
- "Open test cases in browser for manual approval.",
1038
- {
1039
- testCases: zod_1.z.array(zod_1.z.array(zod_1.z.string())).describe("test cases array generated by tool generate_testcases_from_ticket_data")
1040
- },
1041
- async ({ testCases }) => {
1042
- try {
1043
- const app = express();
1044
- let port = 3001;
1045
-
1046
- // Find an available port
1047
- const findAvailablePort = async (startPort) => {
1048
- const net = require('net');
1049
- return new Promise((resolve) => {
1050
- const server = net.createServer();
1051
- server.listen(startPort, () => {
1052
- const port = server.address().port;
1053
- server.close(() => resolve(port));
1054
- });
1055
- server.on('error', () => {
1056
- resolve(findAvailablePort(startPort + 1));
1057
- });
1058
- });
1059
- };
1060
-
1061
- port = await findAvailablePort(port);
1062
-
1063
- // Generate unique session ID
1064
- const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1065
-
1066
- // Store approval status
1067
- let approvalStatus = 'pending';
1068
- let finalTestCases = [];
1069
-
1070
- app.use(express.json({ limit: '10mb' }));
1071
- app.use(express.urlencoded({ extended: true, limit: '10mb' }));
1072
-
1073
- // Process test cases - handle the specific format properly
1074
- const processedTestCases = testCases.map((testCase, index) => {
1075
- if (Array.isArray(testCase)) {
1076
- const arrayLength = testCase.length;
1077
-
1078
- if (arrayLength === 4) {
1079
- // Modify case: ["original title", "new description", "Modify", "SCRUM-TC-1"]
1080
- return {
1081
- originalTitle: testCase[0] || `Test Case ${index + 1}`,
1082
- newDescription: testCase[1] || '',
1083
- status: testCase[2] || 'Modify',
1084
- testId: testCase[3] || '',
1085
- index: index
1086
- };
1087
- } else if (arrayLength === 3) {
1088
- // Remove case: ["title", "Remove", "SCRUM-TC-2"]
1089
- return {
1090
- title: testCase[0] || `Test Case ${index + 1}`,
1091
- status: testCase[1] || 'Remove',
1092
- testId: testCase[2] || '',
1093
- index: index
1094
- };
1095
- } else if (arrayLength === 2) {
1096
- // New case: ["title", "New"]
1097
- return {
1098
- title: testCase[0] || `Test Case ${index + 1}`,
1099
- status: testCase[1] || 'New',
1100
- index: index
1101
- };
1102
- } else {
1103
- // Fallback for unexpected format
1104
- return {
1105
- title: testCase[0] || `Test Case ${index + 1}`,
1106
- status: 'New',
1107
- index: index
1108
- };
1109
- }
1110
- } else {
1111
- // Fallback for non-array format
1112
- return {
1113
- title: String(testCase) || `Test Case ${index + 1}`,
1114
- status: 'New',
1115
- index: index
1116
- };
1117
- }
1118
- });
1119
-
1120
- // Helper function to get display text for test cases
1121
- const getTestCaseDisplayText = (testCase) => {
1122
- const status = testCase.status.toLowerCase();
1123
-
1124
- if (status === 'modify') {
1125
- // For modify cases, show original → changed format
1126
- return `Original: ${testCase.originalTitle}\nChanged to: ${testCase.newDescription}`;
1127
- } else if (status === 'remove') {
1128
- // For remove cases, show the title
1129
- return testCase.title;
1130
- } else {
1131
- // For new cases, show the title
1132
- return testCase.title;
1133
- }
1134
- };
1135
-
1136
- // Main review page with proper handling
1137
- app.get('/', (req, res) => {
1138
- try {
1139
- const htmlContent = `
1140
- <!DOCTYPE html>
1141
- <html lang="en">
1142
- <head>
1143
- <meta charset="UTF-8">
1144
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
1145
- <title>Test Cases Review & Approval</title>
1146
- <style>
1147
- * {
1148
- margin: 0;
1149
- padding: 0;
1150
- box-sizing: border-box;
1151
- }
1152
-
1153
- body {
1154
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1155
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1156
- min-height: 100vh;
1157
- padding: 20px;
1158
- }
1159
-
1160
- .container {
1161
- max-width: 1200px;
1162
- margin: 0 auto;
1163
- background: white;
1164
- border-radius: 15px;
1165
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
1166
- overflow: hidden;
1167
- }
1168
-
1169
- .header {
1170
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
1171
- color: white;
1172
- padding: 30px;
1173
- text-align: center;
1174
- }
1175
-
1176
- .header h1 {
1177
- font-size: 2.5rem;
1178
- margin-bottom: 10px;
1179
- font-weight: 700;
1180
- }
1181
-
1182
- .header p {
1183
- font-size: 1.1rem;
1184
- opacity: 0.9;
1185
- }
1186
-
1187
- .stats {
1188
- display: flex;
1189
- justify-content: space-around;
1190
- background: #f8f9fa;
1191
- padding: 20px;
1192
- border-bottom: 1px solid #e9ecef;
1193
- }
1194
-
1195
- .stat-item {
1196
- text-align: center;
1197
- }
1198
-
1199
- .stat-number {
1200
- font-size: 2rem;
1201
- font-weight: bold;
1202
- color: #495057;
1203
- }
1204
-
1205
- .stat-label {
1206
- color: #6c757d;
1207
- font-size: 0.9rem;
1208
- margin-top: 5px;
1209
- }
1210
-
1211
- .controls {
1212
- padding: 20px;
1213
- background: #f8f9fa;
1214
- display: flex;
1215
- justify-content: space-between;
1216
- align-items: center;
1217
- flex-wrap: wrap;
1218
- gap: 10px;
1219
- }
1220
-
1221
- .btn {
1222
- padding: 12px 24px;
1223
- border: none;
1224
- border-radius: 8px;
1225
- font-weight: 600;
1226
- cursor: pointer;
1227
- transition: all 0.3s ease;
1228
- text-decoration: none;
1229
- display: inline-block;
1230
- font-size: 14px;
1231
- }
1232
-
1233
- .btn-primary {
1234
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1235
- color: white;
1236
- }
1237
-
1238
- .btn-primary:hover {
1239
- transform: translateY(-2px);
1240
- box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
1241
- }
1242
-
1243
- .btn-secondary {
1244
- background: #6c757d;
1245
- color: white;
1246
- }
1247
-
1248
- .btn-secondary:hover {
1249
- background: #5a6268;
1250
- }
1251
-
1252
- .btn-delete {
1253
- background: #dc3545;
1254
- color: white;
1255
- padding: 8px 16px;
1256
- font-size: 12px;
1257
- }
1258
-
1259
- .btn-delete:hover {
1260
- background: #c82333;
1261
- }
1262
-
1263
- .btn-restore {
1264
- background: #28a745;
1265
- color: white;
1266
- padding: 8px 16px;
1267
- font-size: 12px;
1268
- }
1269
-
1270
- .btn-restore:hover {
1271
- background: #218838;
1272
- }
1273
-
1274
- .test-cases {
1275
- max-height: 70vh;
1276
- overflow-y: auto;
1277
- }
1278
-
1279
- .test-case {
1280
- border-bottom: 1px solid #e9ecef;
1281
- padding: 20px;
1282
- transition: all 0.3s ease;
1283
- }
1284
-
1285
- .test-case:hover {
1286
- background: #f8f9fa;
1287
- }
1288
-
1289
- .test-case.deleted {
1290
- opacity: 0.5;
1291
- background: #f8d7da;
1292
- }
1293
-
1294
- .test-case-header {
1295
- display: flex;
1296
- justify-content: space-between;
1297
- align-items: center;
1298
- margin-bottom: 15px;
1299
- }
1300
-
1301
- .test-case-meta {
1302
- display: flex;
1303
- gap: 15px;
1304
- align-items: center;
1305
- }
1306
-
1307
- .test-case-index {
1308
- background: #007bff;
1309
- color: white;
1310
- padding: 4px 8px;
1311
- border-radius: 4px;
1312
- font-size: 12px;
1313
- font-weight: bold;
1314
- }
1315
-
1316
- .test-case-status {
1317
- padding: 4px 12px;
1318
- border-radius: 12px;
1319
- font-size: 12px;
1320
- font-weight: 600;
1321
- }
1322
-
1323
- .status-new {
1324
- background: #d4edda;
1325
- color: #155724;
1326
- }
1327
-
1328
- .status-modify {
1329
- background: #fff3cd;
1330
- color: #856404;
1331
- }
1332
-
1333
- .status-remove {
1334
- background: #f8d7da;
1335
- color: #721c24;
1336
- }
1337
-
1338
- .test-case textarea {
1339
- width: 100%;
1340
- min-height: 100px;
1341
- padding: 15px;
1342
- border: 2px solid #e9ecef;
1343
- border-radius: 8px;
1344
- font-family: inherit;
1345
- font-size: 14px;
1346
- line-height: 1.5;
1347
- resize: vertical;
1348
- transition: border-color 0.3s ease;
1349
- }
1350
-
1351
- .test-case textarea:focus {
1352
- outline: none;
1353
- border-color: #007bff;
1354
- box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
1355
- }
1356
-
1357
- .notification {
1358
- position: fixed;
1359
- top: 20px;
1360
- right: 20px;
1361
- padding: 15px 25px;
1362
- border-radius: 8px;
1363
- color: white;
1364
- font-weight: 600;
1365
- display: none;
1366
- z-index: 1000;
1367
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
1368
- }
1369
-
1370
- @media (max-width: 768px) {
1371
- .container {
1372
- margin: 10px;
1373
- border-radius: 10px;
1374
- }
1375
-
1376
- .header h1 {
1377
- font-size: 2rem;
1378
- }
1379
-
1380
- .stats {
1381
- flex-direction: column;
1382
- gap: 15px;
1383
- }
1384
-
1385
- .controls {
1386
- flex-direction: column;
1387
- gap: 15px;
1388
- }
1389
-
1390
- .test-case-header {
1391
- flex-direction: column;
1392
- align-items: flex-start;
1393
- gap: 10px;
1394
- }
1395
- }
1396
- </style>
1397
- </head>
1398
- <body>
1399
- <div class="notification" id="notification"></div>
1400
-
1401
- <div class="container">
1402
- <div class="header">
1403
- <h1>🔍 Test Cases Review & Approval</h1>
1404
- <p>Review, edit, and approve your test cases. Make any necessary changes before final approval.</p>
1405
- </div>
1406
-
1407
- <div class="stats">
1408
- <div class="stat-item">
1409
- <div class="stat-number" id="totalCount">${processedTestCases.length}</div>
1410
- <div class="stat-label">Total Cases</div>
1411
- </div>
1412
- <div class="stat-item">
1413
- <div class="stat-number" id="activeCount">${processedTestCases.length}</div>
1414
- <div class="stat-label">Active Cases</div>
1415
- </div>
1416
- <div class="stat-item">
1417
- <div class="stat-number" id="deletedCount">0</div>
1418
- <div class="stat-label">Deleted Cases</div>
1419
- </div>
1420
- </div>
1421
-
1422
- <div class="controls">
1423
- <div>
1424
- <button class="btn btn-secondary" onclick="resetAll()">🔄 Reset All</button>
1425
- </div>
1426
- <div>
1427
- <button class="btn btn-primary" onclick="approveTestCases()">✅ Approve Test Cases</button>
1428
- </div>
1429
- </div>
1430
-
1431
- <div class="test-cases">
1432
- ${processedTestCases.map((testCase, index) => {
1433
- const displayText = getTestCaseDisplayText(testCase);
1434
- const statusLabel = testCase.status === 'Remove' ? 'Remove' : testCase.status;
1435
-
1436
- // Create proper label and test ID display for modify/remove cases
1437
- let labelAndIdDisplay = '';
1438
- if (testCase.status.toLowerCase() === 'modify' && testCase.testId) {
1439
- labelAndIdDisplay = `<div style="margin-bottom: 8px; font-weight: 600; color: #856404; font-size: 13px;">Modify - ${testCase.testId}</div>`;
1440
- } else if (testCase.status.toLowerCase() === 'remove' && testCase.testId) {
1441
- labelAndIdDisplay = `<div style="margin-bottom: 8px; font-weight: 600; color: #721c24; font-size: 13px;">Remove - ${testCase.testId}</div>`;
1442
- }
1443
-
1444
- return `
1445
- <div class="test-case" data-index="${index}">
1446
- <div class="test-case-header">
1447
- <div class="test-case-meta">
1448
- <span class="test-case-index">#${index + 1}</span>
1449
- <span class="test-case-status status-${testCase.status.toLowerCase()}">${statusLabel}</span>
1450
- </div>
1451
- <button class="btn btn-delete" onclick="toggleDelete(${index})">Delete</button>
1452
- </div>
1453
- ${labelAndIdDisplay}
1454
- <textarea data-index="${index}" placeholder="Enter test case details...">${displayText}</textarea>
1455
- </div>
1456
- `;
1457
- }).join('')}
1458
- </div>
1459
- </div>
1460
-
1461
- <script>
1462
- let testCases = ${JSON.stringify(processedTestCases).replace(/</g, '\\u003c').replace(/>/g, '\\u003e')};
1463
- let deletedIndices = new Set();
1464
-
1465
- function updateStats() {
1466
- document.getElementById('activeCount').textContent = testCases.length - deletedIndices.size;
1467
- document.getElementById('deletedCount').textContent = deletedIndices.size;
1468
- }
1469
-
1470
- function toggleDelete(index) {
1471
- const testCaseEl = document.querySelector(\`[data-index="\${index}"]\`);
1472
- const btn = testCaseEl.querySelector('.btn-delete, .btn-restore');
1473
-
1474
- if (deletedIndices.has(index)) {
1475
- // Restore
1476
- deletedIndices.delete(index);
1477
- testCaseEl.classList.remove('deleted');
1478
- btn.textContent = 'Delete';
1479
- btn.className = 'btn btn-delete';
1480
- } else {
1481
- // Delete
1482
- deletedIndices.add(index);
1483
- testCaseEl.classList.add('deleted');
1484
- btn.textContent = 'Restore';
1485
- btn.className = 'btn btn-restore';
1486
- }
1487
- updateStats();
1488
- }
1489
-
1490
- function resetAll() {
1491
- deletedIndices.clear();
1492
- document.querySelectorAll('.test-case').forEach((el, index) => {
1493
- el.classList.remove('deleted');
1494
- const btn = el.querySelector('.btn-delete, .btn-restore');
1495
- btn.textContent = 'Delete';
1496
- btn.className = 'btn btn-delete';
1497
-
1498
- // Reset textarea value
1499
- const textarea = el.querySelector('textarea');
1500
- const originalTestCase = testCases[index];
1501
-
1502
- // Use proper display format based on status
1503
- let resetValue = '';
1504
- const status = originalTestCase.status.toLowerCase();
1505
-
1506
- if (status === 'modify') {
1507
- resetValue = 'Original: ' + originalTestCase.originalTitle + '\\nChanged to: ' + originalTestCase.newDescription;
1508
- } else if (status === 'remove') {
1509
- resetValue = originalTestCase.title;
1510
- } else {
1511
- resetValue = originalTestCase.title;
1512
- }
1513
-
1514
- textarea.value = resetValue;
1515
- });
1516
- updateStats();
1517
- }
1518
-
1519
- function showNotification(message, type = 'success') {
1520
- const notification = document.getElementById('notification');
1521
- notification.textContent = message;
1522
- notification.style.background = type === 'success' ? '#28a745' : '#dc3545';
1523
- notification.style.display = 'block';
1524
-
1525
- setTimeout(() => {
1526
- notification.style.display = 'none';
1527
- }, 3000);
1528
- }
1529
-
1530
- function approveTestCases() {
1531
- try {
1532
- // Collect updated test cases
1533
- const updatedTestCases = [];
1534
- document.querySelectorAll('.test-case').forEach((el, index) => {
1535
- if (!deletedIndices.has(index)) {
1536
- const textarea = el.querySelector('textarea');
1537
- const originalTestCase = testCases[index];
1538
-
1539
- // Create the updated test case array based on the original format
1540
- let updatedCase;
1541
- const status = originalTestCase.status.toLowerCase();
1542
-
1543
- if (status === 'modify') {
1544
- // For modify: [updatedContent, newDescription, "Modify", testId]
1545
- updatedCase = [
1546
- textarea.value.trim(),
1547
- originalTestCase.newDescription,
1548
- originalTestCase.status,
1549
- originalTestCase.testId
1550
- ];
1551
- } else if (status === 'remove') {
1552
- // For remove: [updatedContent, "Remove", testId]
1553
- updatedCase = [
1554
- textarea.value.trim(),
1555
- originalTestCase.status,
1556
- originalTestCase.testId
1557
- ];
1558
- } else {
1559
- // For new: [updatedContent, "New"]
1560
- updatedCase = [
1561
- textarea.value.trim(),
1562
- originalTestCase.status
1563
- ];
1564
- }
1565
-
1566
- updatedTestCases.push(updatedCase);
1567
- }
1568
- });
1569
-
1570
- // Send approval to server
1571
- fetch('/approve', {
1572
- method: 'POST',
1573
- headers: {
1574
- 'Content-Type': 'application/json',
1575
- },
1576
- body: JSON.stringify({
1577
- sessionId: '${sessionId}',
1578
- testCases: updatedTestCases
1579
- })
1580
- })
1581
- .then(response => response.json())
1582
- .then(data => {
1583
- if (data.success) {
1584
- showNotification('Test cases approved successfully!');
1585
- setTimeout(() => {
1586
- window.close();
1587
- }, 2000);
1588
- } else {
1589
- showNotification('Error approving test cases', 'error');
1590
- }
1591
- })
1592
- .catch(error => {
1593
- showNotification('Error approving test cases', 'error');
1594
- console.error('Error:', error);
1595
- });
1596
- } catch (error) {
1597
- showNotification('Error processing test cases', 'error');
1598
- console.error('Error:', error);
1599
- }
1600
- }
1601
-
1602
- // Update test cases when textarea changes
1603
- document.addEventListener('input', function(e) {
1604
- if (e.target.tagName === 'TEXTAREA') {
1605
- const index = parseInt(e.target.getAttribute('data-index'));
1606
- if (!isNaN(index) && testCases[index]) {
1607
- // Update the title with the textarea content
1608
- testCases[index].title = e.target.value.trim();
1609
- }
1610
- }
1611
- });
1612
- </script>
1613
- </body>
1614
- </html>
1615
- `;
1616
- res.send(htmlContent);
1617
- } catch (error) {
1618
- console.error('Error rendering page:', error);
1619
- res.status(500).send('Error rendering page');
1620
- }
1621
- });
1622
-
1623
- // Approval endpoint with better error handling
1624
- app.post('/approve', (req, res) => {
1625
- try {
1626
- const { testCases: approvedTestCases, sessionId: receivedSessionId } = req.body;
1627
-
1628
- if (receivedSessionId !== sessionId) {
1629
- return res.status(400).json({ success: false, message: 'Invalid session ID' });
1630
- }
1631
-
1632
- finalTestCases = approvedTestCases;
1633
- approvalStatus = 'approved';
1634
-
1635
- // Save to global state for the check tool
1636
- global.approvalSessions = global.approvalSessions || {};
1637
- global.approvalSessions[sessionId] = {
1638
- status: 'approved',
1639
- testCases: finalTestCases,
1640
- timestamp: Date.now()
1641
- };
1642
-
1643
- res.json({ success: true, message: 'Test cases approved successfully' });
1644
-
1645
- // Close server after approval
1646
- setTimeout(() => {
1647
- if (server && server.listening) {
1648
- server.close();
1649
- }
1650
- }, 3000);
1651
- } catch (error) {
1652
- console.error('Approval error:', error);
1653
- res.status(500).json({ success: false, message: error.message });
1654
- }
1655
- });
1656
-
1657
- // Error handling middleware
1658
- app.use((err, req, res, next) => {
1659
- console.error('Express error:', err);
1660
- res.status(500).json({ error: 'Internal server error' });
1661
- });
1662
-
1663
- // 404 handler
1664
- app.use((req, res) => {
1665
- res.status(404).json({ error: 'Not found' });
1666
- });
1667
-
1668
- // Start server with promise-based approach
1669
- const server = await new Promise((resolve, reject) => {
1670
- const srv = app.listen(port, (err) => {
1671
- if (err) {
1672
- reject(err);
1673
- return;
1674
- }
1675
- console.log(`✅ Test case review session started. Session ID: ${sessionId}.`);
1676
- console.log(`Server running at http://localhost:${port}`);
1677
- console.log(`Browser should open automatically.`);
1678
- resolve(srv);
1679
- });
1680
-
1681
- srv.on('error', (error) => {
1682
- reject(error);
1683
- });
1684
- });
1685
-
1686
- // Open browser with proper error handling
1687
- let openAttemptFailed = false;
1688
- try {
1689
- await openBrowser(`http://localhost:${port}`);
1690
- } catch (err) {
1691
- openAttemptFailed = true;
1692
- console.error('Failed to open browser automatically:', err.message);
1693
- // Continue without opening browser - user can manually navigate to the URL
1694
- }
1695
-
1696
- // Store session globally for status checking
1697
- global.approvalSessions = global.approvalSessions || {};
1698
- global.approvalSessions[sessionId] = {
1699
- status: 'pending',
1700
- testCases: processedTestCases,
1701
- timestamp: Date.now(),
1702
- server: server
1703
- };
1704
-
1705
- return `✅ Test case review session started. Session ID: ${sessionId}.\nServer running at http://localhost:${port}\n${openAttemptFailed ? 'Please manually open the URL in your browser.' : 'Browser should open automatically.'}`;
1706
-
1707
- } catch (err) {
1708
- console.error('Review tool error:', err);
1709
- return `❌ Error starting test case review: ${err.message}`;
1710
- }
1711
- }
1712
- );
1713
-
1714
- // Fix 3: Updated check_approval_status tool with better error handling
1715
- tool(
1716
- "check_approval_status",
1717
- "Check the approval status of test cases review session (waits 25 seconds before checking)",
1718
- {
1719
- sessionId: zod_1.z.string().describe("Session ID from review_testcases")
1720
- },
1721
- async ({ sessionId }) => {
1722
- try {
1723
- // Wait for 25 seconds
1724
- await new Promise(resolve => setTimeout(resolve, 25000));
1725
-
1726
- // Check global approval sessions
1727
- if (!global.approvalSessions || !global.approvalSessions[sessionId]) {
1728
- return "❌ Session not found. Please ensure the review session is still active.";
1729
- }
1730
-
1731
- const session = global.approvalSessions[sessionId];
1732
-
1733
- if (session.status === 'approved') {
1734
- const result = {
1735
- status: 'approved',
1736
- testCases: session.testCases,
1737
- approvedCount: session.testCases.length,
1738
- sessionId: sessionId
1739
- };
1740
-
1741
- // Format the approved test cases properly
1742
- const formattedTestCases = result.testCases.map((tc, index) => {
1743
- if (!Array.isArray(tc)) {
1744
- return `${index + 1}. ${String(tc)} (New)`;
1745
- }
1746
-
1747
- // Handle different array structures based on length and content
1748
- let title, description, status, originalCase;
1749
-
1750
- if (tc.length === 4) {
1751
- // Standard format: [title, description, status, originalCase]
1752
- title = tc[0] || `Test Case ${index + 1}`;
1753
- description = tc[1] || '';
1754
- status = tc[2] || 'New';
1755
- originalCase = tc[3] || '';
1756
- } else if (tc.length === 3) {
1757
- // Could be [title, status, originalCase] for remove cases
1758
- title = tc[0] || `Test Case ${index + 1}`;
1759
- if (tc[1] && tc[1].toLowerCase() === 'remove') {
1760
- status = tc[1];
1761
- originalCase = tc[2] || '';
1762
- description = '';
1763
- } else {
1764
- // [title, description, status]
1765
- description = tc[1] || '';
1766
- status = tc[2] || 'New';
1767
- originalCase = '';
1768
- }
1769
- } else {
1770
- // Fallback
1771
- title = tc[0] || `Test Case ${index + 1}`;
1772
- description = tc[1] || '';
1773
- status = tc[2] || 'New';
1774
- originalCase = tc[3] || '';
1775
- }
1776
-
1777
- const statusLower = status.toLowerCase();
1778
-
1779
- if (statusLower === 'modify') {
1780
- // For modify cases: show "Original: ... Changed to: ..." format with proper test ID
1781
- return `${index + 1}. Original: ${title}\n Changed to: ${description} (Modify) (${originalCase})`;
1782
- } else if (statusLower === 'remove') {
1783
- // For remove cases: show title with Remove label and reference
1784
- return `${index + 1}. ${title} (Remove) (${originalCase})`;
1785
- } else {
1786
- // For new cases: just show title with New label
1787
- return `${index + 1}. ${title} (New)`;
1788
- }
1789
- }).join('\n');
1790
-
1791
- // Clean up session after returning result
1792
- delete global.approvalSessions[sessionId];
1793
-
1794
- return `✅ Test cases approved successfully!\n\nApproved ${result.approvedCount} test cases:\n\n${formattedTestCases}\n\nSession completed: ${sessionId}`;
1795
- } else {
1796
- return "⏳ Still waiting for approval. The review session is active but not yet approved. Please complete the review in the browser.";
1797
- }
1798
-
1799
- } catch (err) {
1800
- console.error('Check approval status error:', err);
1801
- return `❌ Error checking approval status: ${err.message}`;
1802
- }
1803
- }
1804
- );
1805
-
1806
- tool(
1807
- "update_testcases_to_tcms",
1808
- "Create new test cases in TCMS from approved test cases. Only processes test cases with 'New' status, ignores Modify and Remove cases since APIs are not available.",
1809
- {
1810
- testCases: zod_1.z.array(zod_1.z.array(zod_1.z.string())).describe("Array of test case arrays from approved test cases")
1811
- },
1812
- async ({ testCases }) => {
1813
- try {
1814
- // Load AIO token from Desktop/aio.json
1815
- const aioConfigPath = path.join(os.homedir(), "Desktop", "aio.json");
1816
- const configContent = await fs.readFile(aioConfigPath, "utf-8");
1817
- const { token } = JSON.parse(configContent);
1818
-
1819
- if (!token) throw new Error("AIO token missing in aio.json");
1820
-
1821
- // Filter test cases to extract only "New" test cases
1822
- const newTestCases = [];
1823
-
1824
- for (const testCase of testCases) {
1825
- if (Array.isArray(testCase) && testCase.length >= 2) {
1826
- // Check if the last element or second-to-last element is "New"
1827
- const status = testCase.length === 2 ? testCase[1] : testCase[testCase.length - 2];
1828
-
1829
- if (status && status.toLowerCase() === 'new') {
1830
- const title = testCase[0]; // First element is always the title
1831
- if (title && title.trim().length > 0) {
1832
- newTestCases.push(title.trim());
1833
- }
1834
- }
1835
- }
1836
- }
1837
-
1838
- if (newTestCases.length === 0) {
1839
- return "No new test cases found to create in TCMS. Only test cases marked as '(New)' are processed.";
1840
- }
1841
-
1842
- // Hard-coded values as requested
1843
- const projectKey = "SCRUM";
1844
- const folderId = 1;
1845
- const ownerId = "712020:37085ff2-5a05-47eb-8977-50a485355755";
1846
-
1847
- // Create test cases in TCMS one by one
1848
- for (let i = 0; i < newTestCases.length; i++) {
1849
- const title = newTestCases[i];
1850
-
1851
- try {
1852
- const requestBody = {
1853
- title: title,
1854
- ownedByID: ownerId,
1855
- folder: {
1856
- ID: folderId
1857
- },
1858
- status: {
1859
- name: "Published",
1860
- description: "The test is ready for execution",
1861
- ID: 1
1862
- }
1863
- };
1864
-
1865
- (0, logger_1.trace)(`Creating test case ${i + 1}/${newTestCases.length}: ${title}`);
1866
-
1867
- const response = await axios.post(
1868
- `https://tcms.aiojiraapps.com/aio-tcms/api/v1/project/${projectKey}/testcase`,
1869
- requestBody,
1870
- {
1871
- headers: {
1872
- "accept": "application/json;charset=utf-8",
1873
- "Authorization": `AioAuth ${token}`,
1874
- "Content-Type": "application/json"
1875
- }
1876
- }
1877
- );
1878
-
1879
- if (response.status === 200 || response.status === 201) {
1880
- const testCaseKey = response.data.key || `${projectKey}-TC-${response.data.ID}`;
1881
- (0, logger_1.trace)(`Successfully created test case: ${testCaseKey} - ${title}`);
1882
- }
1883
-
1884
- // Add a small delay between requests to avoid rate limiting
1885
- await new Promise(resolve => setTimeout(resolve, 500));
1886
-
1887
- } catch (error) {
1888
- (0, logger_1.trace)(`Failed to create test case: ${title} - ${error.message}`);
1889
- throw new Error(`Failed to create test case "${title}": ${error.message}`);
1890
- }
1891
- }
1892
-
1893
- return "All test cases have been updated to TCMS";
1894
-
1895
- } catch (error) {
1896
- console.error('TCMS update error:', error);
1897
- if (error.response) {
1898
- return `❌ TCMS API Error: ${error.response.status} - ${error.response.data?.message || error.response.statusText}`;
1899
- }
1900
- return `❌ Error updating test cases to TCMS: ${error.message}`;
1901
- }
1902
- }
1903
- );
586
+ // tool(
587
+ // "mobile_fetch_jira_ticket",
588
+ // "Fetch JIRA ticket information including summary, description, and extract Figma links from description",
589
+ // {
590
+ // ticketId: zod_1.z.string().describe("The JIRA ticket ID (e.g., HDA-434)"),
591
+ // },
592
+ // async ({ ticketId }) => {
593
+ // try {
594
+ // // Read JIRA credentials from desktop/jira.json file
595
+ // const jiraConfigPath = path.join(os.homedir(), 'Desktop', 'jira.json');
596
+ //
597
+ // let jiraConfig;
598
+ // try {
599
+ // const configContent = await fs.readFile(jiraConfigPath, 'utf-8');
600
+ // jiraConfig = JSON.parse(configContent);
601
+ // } catch (error) {
602
+ // throw new Error(`Failed to read JIRA config from ${jiraConfigPath}: ${error.message}`);
603
+ // }
604
+ //
605
+ // // Extract all required values from JSON file
606
+ // const { api: jiraApiToken, baseUrl: jiraBaseUrl, email: jiraEmail } = jiraConfig;
607
+ //
608
+ // if (!jiraApiToken) {
609
+ // throw new Error('JIRA API token not found in jira.json file. Please ensure the file contains "api" field.');
610
+ // }
611
+ //
612
+ // if (!jiraBaseUrl) {
613
+ // throw new Error('JIRA base URL not found in jira.json file. Please ensure the file contains "baseUrl" field.');
614
+ // }
615
+ //
616
+ // if (!jiraEmail) {
617
+ // throw new Error('JIRA email not found in jira.json file. Please ensure the file contains "email" field.');
618
+ // }
619
+ //
620
+ // // Create Basic Auth token
621
+ // const auth = Buffer.from(`${jiraEmail}:${jiraApiToken}`).toString('base64');
622
+ //
623
+ // // Fetch ticket from JIRA API
624
+ // const response = await axios.get(
625
+ // `${jiraBaseUrl}/rest/api/3/issue/${ticketId}`,
626
+ // {
627
+ // headers: {
628
+ // 'Authorization': `Basic ${auth}`,
629
+ // 'Accept': 'application/json',
630
+ // 'Content-Type': 'application/json'
631
+ // }
632
+ // }
633
+ // );
634
+ //
635
+ // const issue = response.data;
636
+ //
637
+ // // Extract summary and description
638
+ // const summary = issue.fields.summary || 'No summary available';
639
+ // const description = issue.fields.description ?
640
+ // extractTextFromADF(issue.fields.description) :
641
+ // 'No description available';
642
+ //
643
+ // // Extract Figma links directly from ADF structure
644
+ // const figmaLinks = extractFigmaLinksFromADF(issue.fields.description);
645
+ //
646
+ // // Format response
647
+ // const result = {
648
+ // ticketId: ticketId,
649
+ // summary: summary,
650
+ // description: description,
651
+ // figmaLinks: figmaLinks.length > 0 ? figmaLinks : ['No Figma links found']
652
+ // };
653
+ //
654
+ // return `JIRA Ticket Information:
655
+ // Ticket ID: ${result.ticketId}
656
+ // Summary: ${result.summary}
657
+ // Description: ${result.description}
658
+ // Figma Links: ${result.figmaLinks.join(', ')}`;
659
+ //
660
+ // } catch (error) {
661
+ // if (error.response && error.response.status === 404) {
662
+ // return `Error: JIRA ticket ${ticketId} not found. Please check the ticket ID.`;
663
+ // } else if (error.response && error.response.status === 401) {
664
+ // return `Error: Authentication failed. Please check your JIRA credentials.`;
665
+ // } else {
666
+ // return `Error fetching JIRA ticket: ${error.message}`;
667
+ // }
668
+ // }
669
+ // }
670
+ // );
671
+ //
672
+ // // Helper function to extract text from Atlassian Document Format (ADF)
673
+ // function extractTextFromADF(adfContent) {
674
+ // if (!adfContent || typeof adfContent !== 'object') {
675
+ // return String(adfContent || '');
676
+ // }
677
+ //
678
+ // let text = '';
679
+ //
680
+ // function traverse(node) {
681
+ // if (node.type === 'text') {
682
+ // text += node.text || '';
683
+ // } else if (node.content && Array.isArray(node.content)) {
684
+ // node.content.forEach(traverse);
685
+ // }
686
+ //
687
+ // // Add line breaks for paragraphs
688
+ // if (node.type === 'paragraph') {
689
+ // text += '\n';
690
+ // }
691
+ // }
692
+ //
693
+ // if (adfContent.content) {
694
+ // adfContent.content.forEach(traverse);
695
+ // }
696
+ //
697
+ // return text.trim();
698
+ // }
699
+ //
700
+ // // Helper function to extract Figma links directly from ADF structure
701
+ // function extractFigmaLinksFromADF(adfContent) {
702
+ // if (!adfContent || typeof adfContent !== 'object') {
703
+ // return [];
704
+ // }
705
+ //
706
+ // const figmaLinks = [];
707
+ //
708
+ // function traverse(node) {
709
+ // // Check for inlineCard nodes with Figma URLs
710
+ // if (node.type === 'inlineCard' && node.attrs && node.attrs.url) {
711
+ // const url = node.attrs.url;
712
+ // if (url.includes('figma.com')) {
713
+ // figmaLinks.push(url);
714
+ // }
715
+ // }
716
+ //
717
+ // // Check for link marks with Figma URLs
718
+ // if (node.marks && Array.isArray(node.marks)) {
719
+ // node.marks.forEach(mark => {
720
+ // if (mark.type === 'link' && mark.attrs && mark.attrs.href) {
721
+ // const href = mark.attrs.href;
722
+ // if (href.includes('figma.com')) {
723
+ // figmaLinks.push(href);
724
+ // }
725
+ // }
726
+ // });
727
+ // }
728
+ //
729
+ // // Traverse child content
730
+ // if (node.content && Array.isArray(node.content)) {
731
+ // node.content.forEach(traverse);
732
+ // }
733
+ // }
734
+ //
735
+ // if (adfContent.content) {
736
+ // adfContent.content.forEach(traverse);
737
+ // }
738
+ //
739
+ // // Remove duplicates and return
740
+ // return [...new Set(figmaLinks)];
741
+ // }
742
+ //
743
+ //// ----------------------
744
+ //// Helper: Extract File ID & Node ID
745
+ //// ----------------------
746
+ //function extractFileAndNodeId(url) {
747
+ // const patterns = [
748
+ // /figma\.com\/file\/([a-zA-Z0-9]+)/,
749
+ // /figma\.com\/design\/([a-zA-Z0-9]+)/,
750
+ // /figma\.com\/proto\/([a-zA-Z0-9]+)/
751
+ // ];
752
+ //
753
+ // let fileId = null;
754
+ // for (const pattern of patterns) {
755
+ // const match = url.match(pattern);
756
+ // if (match) {
757
+ // fileId = match[1];
758
+ // break;
759
+ // }
760
+ // }
761
+ //
762
+ // // Extract node-id if present
763
+ // const nodeMatch = url.match(/[?&]node-id=([^&]+)/);
764
+ // let nodeId = null;
765
+ // if (nodeMatch) {
766
+ // // Replace dash with colon (Figma expects 13:5951 instead of 13-5951)
767
+ // nodeId = decodeURIComponent(nodeMatch[1]).replace(/-/g, ":");
768
+ // }
769
+ //
770
+ // return { fileId, nodeId };
771
+ //}
772
+ //
773
+ //// ----------------------
774
+ //// TOOL 1: Export Figma to PNG
775
+ //// ----------------------
776
+ //tool(
777
+ // "mobile_export_figma_png",
778
+ // "Export Figma file as PNG",
779
+ // {
780
+ // figmaUrl: zod_1.z.string().describe("The Figma file URL to export as PNG")
781
+ // },
782
+ // async ({ figmaUrl }) => {
783
+ // try {
784
+ // // Load Figma token from Desktop/figma.json
785
+ // const figmaConfigPath = path.join(os.homedir(), "Desktop", "figma.json");
786
+ // const configContent = await fs.readFile(figmaConfigPath, "utf-8");
787
+ // const { token: figmaToken } = JSON.parse(configContent);
788
+ //
789
+ // if (!figmaToken) throw new Error("Figma API token missing in figma.json");
790
+ //
791
+ // // Extract fileId and nodeId from URL
792
+ // const { fileId, nodeId } = extractFileAndNodeId(figmaUrl);
793
+ // if (!fileId) throw new Error("Invalid Figma URL - cannot extract fileId");
794
+ //
795
+ // let idsToExport = [];
796
+ //
797
+ // if (nodeId) {
798
+ // // Use node-id directly from URL
799
+ // idsToExport = [nodeId];
800
+ // } else {
801
+ // // Fallback: scan file to collect all top-level frames
802
+ // const fileResponse = await axios.get(
803
+ // `https://api.figma.com/v1/files/${fileId}`,
804
+ // { headers: { "X-Figma-Token": figmaToken } }
805
+ // );
806
+ //
807
+ // fileResponse.data.document.children?.forEach(page => {
808
+ // page.children?.forEach(child => {
809
+ // if (child.type === "FRAME") idsToExport.push(child.id);
810
+ // });
811
+ // });
812
+ //
813
+ // if (idsToExport.length === 0)
814
+ // throw new Error("No frames found in Figma file");
815
+ // }
816
+ //
817
+ // // Request PNG export with higher scale for better quality
818
+ // const exportResponse = await axios.get(
819
+ // `https://api.figma.com/v1/images/${fileId}`,
820
+ // {
821
+ // headers: { "X-Figma-Token": figmaToken },
822
+ // params: {
823
+ // ids: idsToExport.join(","),
824
+ // format: "png",
825
+ // scale: "2" // 2x scale for better quality
826
+ // }
827
+ // }
828
+ // );
829
+ //
830
+ // const exportPath = path.join(os.homedir(), "Desktop", "figma");
831
+ //
832
+ // // Clear the folder before creating new PNGs
833
+ // try {
834
+ // // Check if folder exists
835
+ // await fs.access(exportPath);
836
+ // // If folder exists, remove all contents
837
+ // const files = await fs.readdir(exportPath);
838
+ // await Promise.all(
839
+ // files.map(file => fs.unlink(path.join(exportPath, file)))
840
+ // );
841
+ // } catch (err) {
842
+ // // Folder doesn't exist or is empty, no need to clear
843
+ // if (err.code !== 'ENOENT') {
844
+ // }
845
+ // }
846
+ //
847
+ // // Ensure directory exists
848
+ // await fs.mkdir(exportPath, { recursive: true });
849
+ //
850
+ // const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5);
851
+ //
852
+ // // Download all PNG images
853
+ // const downloadPromises = Object.entries(exportResponse.data.images).map(
854
+ // async ([nodeId, pngUrl], index) => {
855
+ // if (!pngUrl) throw new Error(`No PNG export URL returned for node ${nodeId}`);
856
+ //
857
+ // const pngResponse = await axios.get(pngUrl, { responseType: "arraybuffer" });
858
+ // const filename = idsToExport.length > 1
859
+ // ? `figma-export-${timestamp}-${index + 1}.png`
860
+ // : `figma-export-${timestamp}.png`;
861
+ // const pngPath = path.join(exportPath, filename);
862
+ //
863
+ // await fs.writeFile(pngPath, pngResponse.data);
864
+ // return pngPath;
865
+ // }
866
+ // );
867
+ //
868
+ // const savedPaths = await Promise.all(downloadPromises);
869
+ //
870
+ // return `✅ PNG Export Complete: ${savedPaths.length} file(s) saved to ${exportPath}`;
871
+ // } catch (err) {
872
+ // return `❌ Error exporting Figma PNG: ${err.message}`;
873
+ // }
874
+ // }
875
+ //);
876
+
877
+ //tool(
878
+ // "fetch_testcases_from_tcms",
879
+ // "Before calling these tool, folder name can be analysed by data fetched from jira ticket info. Before generating test cases for a jira ticket, always fetch existing test cases from TCMS tool for a specific folder",
880
+ // {
881
+ // projectKey: zod_1.z.string().describe("The project key Default: SCRUM"),
882
+ // folderName: zod_1.z.string().describe("The folder name to filter test cases (e.g., PDP), folder name can to be fetched from jira ticket info")
883
+ // },
884
+ // async ({ projectKey, folderName }) => {
885
+ // try {
886
+ // // Load AIO token from Desktop/aio.json
887
+ // const aioConfigPath = path.join(os.homedir(), "Desktop", "aio.json");
888
+ // const configContent = await fs.readFile(aioConfigPath, "utf-8");
889
+ // const { token } = JSON.parse(configContent);
890
+ //
891
+ // if (!token) throw new Error("AIO token missing in aio.json");
892
+ //
893
+ // // Make API request to TCMS
894
+ // const response = await axios.get(
895
+ // `https://tcms.aiojiraapps.com/aio-tcms/api/v1/project/${projectKey}/testcase`,
896
+ // {
897
+ // headers: {
898
+ // "accept": "application/json;charset=utf-8",
899
+ // "Authorization": `AioAuth ${token}`
900
+ // }
901
+ // }
902
+ // );
903
+ //
904
+ // const testCases = response.data.items || [];
905
+ //
906
+ // // Filter test cases by folder name
907
+ // const filteredTestCases = testCases.filter(testCase =>
908
+ // testCase.folder && testCase.folder.name === folderName
909
+ // );
910
+ //
911
+ // if (filteredTestCases.length === 0) {
912
+ // return `No test cases found in folder: ${folderName}`;
913
+ // }
914
+ //
915
+ // // Extract key, title, and folder name
916
+ // const extractedTestCases = filteredTestCases.map(testCase => ({
917
+ // key: testCase.key,
918
+ // title: testCase.title,
919
+ // folderName: testCase.folder.name
920
+ // }));
921
+ //
922
+ // // Format as string response
923
+ // const result = `✅ Found ${extractedTestCases.length} test cases in folder: ${folderName}\n\n` +
924
+ // extractedTestCases.map(tc =>
925
+ // `Key: ${tc.key}\nTitle: ${tc.title}\nFolder: ${tc.folderName}\n---`
926
+ // ).join('\n');
927
+ //
928
+ // return result;
929
+ //
930
+ // } catch (err) {
931
+ // if (err.response) {
932
+ // return `❌ TCMS API Error: ${err.response.status} - ${err.response.data?.message || err.response.statusText}`;
933
+ // }
934
+ // return `❌ Error fetching test cases: ${err.message}`;
935
+ // }
936
+ // }
937
+ //);
938
+
939
+ //tool(
940
+ // "generate_testcases_from_ticket_data",
941
+ // "Generate manual test cases by analyzing PNG design with JIRA requirements",
942
+ // {
943
+ // jiraSummary: zod_1.z.string().describe("Jira issue summary"),
944
+ // jiraDescription: zod_1.z.string().describe("Jira issue description"),
945
+ // existingTestCases: zod_1.z.string().optional().describe("Existing test cases from TCMS")
946
+ // },
947
+ // async ({ jiraSummary, jiraDescription, existingTestCases }) => {
948
+ // try {
949
+ // // Clear the generated test cases file before starting
950
+ // const testCasesFilePath = path.join(__dirname, 'generated-testcases.txt');
951
+ // await fs.writeFile(testCasesFilePath, ''); // Clear the file
952
+ //
953
+ // // Load OpenAI API key from Desktop/openai.json
954
+ // const openaiConfigPath = path.join(os.homedir(), "Desktop", "openai.json");
955
+ // const configContent = await fs.readFile(openaiConfigPath, "utf-8");
956
+ // const { apiKey } = JSON.parse(configContent.trim());
957
+ //
958
+ // // Load test case generation guidelines
959
+ // const guidelinesPath = path.join(__dirname, 'testcases-generation-context.txt');
960
+ // const guidelines = await fs.readFile(guidelinesPath, "utf-8");
961
+ //
962
+ // const figmaDir = path.join(os.homedir(), "Desktop", "figma");
963
+ // const files = await fs.readdir(figmaDir);
964
+ // const pngFiles = files.filter(file => file.toLowerCase().endsWith('.png'));
965
+ //
966
+ // if (pngFiles.length === 0) throw new Error("No PNG files found in figma folder");
967
+ //
968
+ // // Get the latest PNG file
969
+ // const latestPng = pngFiles.sort((a, b) => b.localeCompare(a))[0];
970
+ // const pngPath = path.join(figmaDir, latestPng);
971
+ //
972
+ // const client = new OpenAI({ apiKey });
973
+ //
974
+ // // Convert PNG to base64 for vision API
975
+ // const pngBuffer = await fs.readFile(pngPath);
976
+ // const base64Image = pngBuffer.toString('base64');
977
+ //
978
+ // // Start OpenAI generation (this will run in background due to timeout)
979
+ // client.chat.completions.create({
980
+ // model: "gpt-5", // Use GPT-5 model for image analysis
981
+ // messages: [{
982
+ // role: "user",
983
+ // content: [
984
+ // {
985
+ // type: "text",
986
+ // text: `Generate manual test cases based on the following:
987
+ //
988
+ //JIRA Summary: ${jiraSummary}
989
+ //
990
+ //JIRA Description: ${jiraDescription}
991
+ //
992
+ //${existingTestCases ? `Existing Test Cases from TCMS:
993
+ //${existingTestCases}
994
+ //
995
+ //Please consider these existing test cases and generate additional comprehensive test cases that complement them.` : ''}
996
+ //
997
+ //Test Case Generation Guidelines:
998
+ //${guidelines}`
999
+ // },
1000
+ // {
1001
+ // type: "image_url",
1002
+ // image_url: {
1003
+ // url: `data:image/png;base64,${base64Image}`,
1004
+ // detail: "high"
1005
+ // }
1006
+ // }
1007
+ // ]
1008
+ // }],
1009
+ // max_completion_tokens: 10000
1010
+ // }).then(async (completion) => {
1011
+ // // Save test cases to file when generation completes
1012
+ // const testCases = completion.choices[0].message.content;
1013
+ // await fs.writeFile(testCasesFilePath, testCases);
1014
+ // }).catch(async (error) => {
1015
+ // // Save error to file if generation fails
1016
+ // await fs.writeFile(testCasesFilePath, `Error generating test cases: ${error.message}`);
1017
+ // });
1018
+ //
1019
+ // return "✅ Test case generation started. Use 'check_testcases_status' tool to check if generation is complete. Try max 10 times";
1020
+ // } catch (err) {
1021
+ // return `❌ Error starting test case generation: ${err.message}`;
1022
+ // }
1023
+ // }
1024
+ //);
1025
+ //
1026
+ //tool(
1027
+ // "check_testcases_status",
1028
+ // "Check if test cases have been generated and saved to file",
1029
+ // {},
1030
+ // async () => {
1031
+ // try {
1032
+ // // Wait for 20 seconds before checking
1033
+ // await new Promise(resolve => setTimeout(resolve, 25000));
1034
+ //
1035
+ // const testCasesFilePath = path.join(__dirname, 'generated-testcases.txt');
1036
+ //
1037
+ // // Check if file exists and has content
1038
+ // try {
1039
+ // const fileContent = await fs.readFile(testCasesFilePath, 'utf-8');
1040
+ //
1041
+ // if (fileContent.trim().length === 0) {
1042
+ // return "❌ Test cases are still being generated. Please wait and try again.";
1043
+ // }
1044
+ //
1045
+ // if (fileContent.startsWith('Error generating test cases:')) {
1046
+ // return `❌ ${fileContent}`;
1047
+ // }
1048
+ //
1049
+ // return `✅ Test cases generated successfully!\n\n${fileContent}`;
1050
+ // } catch (fileError) {
1051
+ // return "❌ Test cases file not found or still being created. Please wait and try again.";
1052
+ // }
1053
+ // } catch (err) {
1054
+ // return `❌ Error checking test cases status: ${err.message}`;
1055
+ // }
1056
+ // }
1057
+ //);
1058
+ //
1059
+ //// Fix 2: Updated review_testcases tool with proper JSON handling and open module usage
1060
+ //tool(
1061
+ // "review_testcases",
1062
+ // "Open test cases in browser for manual approval.",
1063
+ // {
1064
+ // testCases: zod_1.z.array(zod_1.z.array(zod_1.z.string())).describe("test cases array generated by tool generate_testcases_from_ticket_data")
1065
+ // },
1066
+ // async ({ testCases }) => {
1067
+ // try {
1068
+ // const app = express();
1069
+ // let port = 3001;
1070
+ //
1071
+ // // Find an available port
1072
+ // const findAvailablePort = async (startPort) => {
1073
+ // const net = require('net');
1074
+ // return new Promise((resolve) => {
1075
+ // const server = net.createServer();
1076
+ // server.listen(startPort, () => {
1077
+ // const port = server.address().port;
1078
+ // server.close(() => resolve(port));
1079
+ // });
1080
+ // server.on('error', () => {
1081
+ // resolve(findAvailablePort(startPort + 1));
1082
+ // });
1083
+ // });
1084
+ // };
1085
+ //
1086
+ // port = await findAvailablePort(port);
1087
+ //
1088
+ // // Generate unique session ID
1089
+ // const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1090
+ //
1091
+ // // Store approval status
1092
+ // let approvalStatus = 'pending';
1093
+ // let finalTestCases = [];
1094
+ //
1095
+ // app.use(express.json({ limit: '10mb' }));
1096
+ // app.use(express.urlencoded({ extended: true, limit: '10mb' }));
1097
+ //
1098
+ // // Process test cases - handle the specific format properly
1099
+ // const processedTestCases = testCases.map((testCase, index) => {
1100
+ // if (Array.isArray(testCase)) {
1101
+ // const arrayLength = testCase.length;
1102
+ //
1103
+ // if (arrayLength === 4) {
1104
+ // // Modify case: ["original title", "new description", "Modify", "SCRUM-TC-1"]
1105
+ // return {
1106
+ // originalTitle: testCase[0] || `Test Case ${index + 1}`,
1107
+ // newDescription: testCase[1] || '',
1108
+ // status: testCase[2] || 'Modify',
1109
+ // testId: testCase[3] || '',
1110
+ // index: index
1111
+ // };
1112
+ // } else if (arrayLength === 3) {
1113
+ // // Remove case: ["title", "Remove", "SCRUM-TC-2"]
1114
+ // return {
1115
+ // title: testCase[0] || `Test Case ${index + 1}`,
1116
+ // status: testCase[1] || 'Remove',
1117
+ // testId: testCase[2] || '',
1118
+ // index: index
1119
+ // };
1120
+ // } else if (arrayLength === 2) {
1121
+ // // New case: ["title", "New"]
1122
+ // return {
1123
+ // title: testCase[0] || `Test Case ${index + 1}`,
1124
+ // status: testCase[1] || 'New',
1125
+ // index: index
1126
+ // };
1127
+ // } else {
1128
+ // // Fallback for unexpected format
1129
+ // return {
1130
+ // title: testCase[0] || `Test Case ${index + 1}`,
1131
+ // status: 'New',
1132
+ // index: index
1133
+ // };
1134
+ // }
1135
+ // } else {
1136
+ // // Fallback for non-array format
1137
+ // return {
1138
+ // title: String(testCase) || `Test Case ${index + 1}`,
1139
+ // status: 'New',
1140
+ // index: index
1141
+ // };
1142
+ // }
1143
+ // });
1144
+ //
1145
+ // // Helper function to get display text for test cases
1146
+ // const getTestCaseDisplayText = (testCase) => {
1147
+ // const status = testCase.status.toLowerCase();
1148
+ //
1149
+ // if (status === 'modify') {
1150
+ // // For modify cases, show original → changed format
1151
+ // return `Original: ${testCase.originalTitle}\nChanged to: ${testCase.newDescription}`;
1152
+ // } else if (status === 'remove') {
1153
+ // // For remove cases, show the title
1154
+ // return testCase.title;
1155
+ // } else {
1156
+ // // For new cases, show the title
1157
+ // return testCase.title;
1158
+ // }
1159
+ // };
1160
+ //
1161
+ // // Main review page with proper handling
1162
+ // app.get('/', (req, res) => {
1163
+ // try {
1164
+ // const htmlContent = `
1165
+ //<!DOCTYPE html>
1166
+ //<html lang="en">
1167
+ //<head>
1168
+ // <meta charset="UTF-8">
1169
+ // <meta name="viewport" content="width=device-width, initial-scale=1.0">
1170
+ // <title>Test Cases Review & Approval</title>
1171
+ // <style>
1172
+ // * {
1173
+ // margin: 0;
1174
+ // padding: 0;
1175
+ // box-sizing: border-box;
1176
+ // }
1177
+ //
1178
+ // body {
1179
+ // font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1180
+ // background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1181
+ // min-height: 100vh;
1182
+ // padding: 20px;
1183
+ // }
1184
+ //
1185
+ // .container {
1186
+ // max-width: 1200px;
1187
+ // margin: 0 auto;
1188
+ // background: white;
1189
+ // border-radius: 15px;
1190
+ // box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
1191
+ // overflow: hidden;
1192
+ // }
1193
+ //
1194
+ // .header {
1195
+ // background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
1196
+ // color: white;
1197
+ // padding: 30px;
1198
+ // text-align: center;
1199
+ // }
1200
+ //
1201
+ // .header h1 {
1202
+ // font-size: 2.5rem;
1203
+ // margin-bottom: 10px;
1204
+ // font-weight: 700;
1205
+ // }
1206
+ //
1207
+ // .header p {
1208
+ // font-size: 1.1rem;
1209
+ // opacity: 0.9;
1210
+ // }
1211
+ //
1212
+ // .stats {
1213
+ // display: flex;
1214
+ // justify-content: space-around;
1215
+ // background: #f8f9fa;
1216
+ // padding: 20px;
1217
+ // border-bottom: 1px solid #e9ecef;
1218
+ // }
1219
+ //
1220
+ // .stat-item {
1221
+ // text-align: center;
1222
+ // }
1223
+ //
1224
+ // .stat-number {
1225
+ // font-size: 2rem;
1226
+ // font-weight: bold;
1227
+ // color: #495057;
1228
+ // }
1229
+ //
1230
+ // .stat-label {
1231
+ // color: #6c757d;
1232
+ // font-size: 0.9rem;
1233
+ // margin-top: 5px;
1234
+ // }
1235
+ //
1236
+ // .controls {
1237
+ // padding: 20px;
1238
+ // background: #f8f9fa;
1239
+ // display: flex;
1240
+ // justify-content: space-between;
1241
+ // align-items: center;
1242
+ // flex-wrap: wrap;
1243
+ // gap: 10px;
1244
+ // }
1245
+ //
1246
+ // .btn {
1247
+ // padding: 12px 24px;
1248
+ // border: none;
1249
+ // border-radius: 8px;
1250
+ // font-weight: 600;
1251
+ // cursor: pointer;
1252
+ // transition: all 0.3s ease;
1253
+ // text-decoration: none;
1254
+ // display: inline-block;
1255
+ // font-size: 14px;
1256
+ // }
1257
+ //
1258
+ // .btn-primary {
1259
+ // background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1260
+ // color: white;
1261
+ // }
1262
+ //
1263
+ // .btn-primary:hover {
1264
+ // transform: translateY(-2px);
1265
+ // box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
1266
+ // }
1267
+ //
1268
+ // .btn-secondary {
1269
+ // background: #6c757d;
1270
+ // color: white;
1271
+ // }
1272
+ //
1273
+ // .btn-secondary:hover {
1274
+ // background: #5a6268;
1275
+ // }
1276
+ //
1277
+ // .btn-delete {
1278
+ // background: #dc3545;
1279
+ // color: white;
1280
+ // padding: 8px 16px;
1281
+ // font-size: 12px;
1282
+ // }
1283
+ //
1284
+ // .btn-delete:hover {
1285
+ // background: #c82333;
1286
+ // }
1287
+ //
1288
+ // .btn-restore {
1289
+ // background: #28a745;
1290
+ // color: white;
1291
+ // padding: 8px 16px;
1292
+ // font-size: 12px;
1293
+ // }
1294
+ //
1295
+ // .btn-restore:hover {
1296
+ // background: #218838;
1297
+ // }
1298
+ //
1299
+ // .test-cases {
1300
+ // max-height: 70vh;
1301
+ // overflow-y: auto;
1302
+ // }
1303
+ //
1304
+ // .test-case {
1305
+ // border-bottom: 1px solid #e9ecef;
1306
+ // padding: 20px;
1307
+ // transition: all 0.3s ease;
1308
+ // }
1309
+ //
1310
+ // .test-case:hover {
1311
+ // background: #f8f9fa;
1312
+ // }
1313
+ //
1314
+ // .test-case.deleted {
1315
+ // opacity: 0.5;
1316
+ // background: #f8d7da;
1317
+ // }
1318
+ //
1319
+ // .test-case-header {
1320
+ // display: flex;
1321
+ // justify-content: space-between;
1322
+ // align-items: center;
1323
+ // margin-bottom: 15px;
1324
+ // }
1325
+ //
1326
+ // .test-case-meta {
1327
+ // display: flex;
1328
+ // gap: 15px;
1329
+ // align-items: center;
1330
+ // }
1331
+ //
1332
+ // .test-case-index {
1333
+ // background: #007bff;
1334
+ // color: white;
1335
+ // padding: 4px 8px;
1336
+ // border-radius: 4px;
1337
+ // font-size: 12px;
1338
+ // font-weight: bold;
1339
+ // }
1340
+ //
1341
+ // .test-case-status {
1342
+ // padding: 4px 12px;
1343
+ // border-radius: 12px;
1344
+ // font-size: 12px;
1345
+ // font-weight: 600;
1346
+ // }
1347
+ //
1348
+ // .status-new {
1349
+ // background: #d4edda;
1350
+ // color: #155724;
1351
+ // }
1352
+ //
1353
+ // .status-modify {
1354
+ // background: #fff3cd;
1355
+ // color: #856404;
1356
+ // }
1357
+ //
1358
+ // .status-remove {
1359
+ // background: #f8d7da;
1360
+ // color: #721c24;
1361
+ // }
1362
+ //
1363
+ // .test-case textarea {
1364
+ // width: 100%;
1365
+ // min-height: 100px;
1366
+ // padding: 15px;
1367
+ // border: 2px solid #e9ecef;
1368
+ // border-radius: 8px;
1369
+ // font-family: inherit;
1370
+ // font-size: 14px;
1371
+ // line-height: 1.5;
1372
+ // resize: vertical;
1373
+ // transition: border-color 0.3s ease;
1374
+ // }
1375
+ //
1376
+ // .test-case textarea:focus {
1377
+ // outline: none;
1378
+ // border-color: #007bff;
1379
+ // box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
1380
+ // }
1381
+ //
1382
+ // .notification {
1383
+ // position: fixed;
1384
+ // top: 20px;
1385
+ // right: 20px;
1386
+ // padding: 15px 25px;
1387
+ // border-radius: 8px;
1388
+ // color: white;
1389
+ // font-weight: 600;
1390
+ // display: none;
1391
+ // z-index: 1000;
1392
+ // box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
1393
+ // }
1394
+ //
1395
+ // @media (max-width: 768px) {
1396
+ // .container {
1397
+ // margin: 10px;
1398
+ // border-radius: 10px;
1399
+ // }
1400
+ //
1401
+ // .header h1 {
1402
+ // font-size: 2rem;
1403
+ // }
1404
+ //
1405
+ // .stats {
1406
+ // flex-direction: column;
1407
+ // gap: 15px;
1408
+ // }
1409
+ //
1410
+ // .controls {
1411
+ // flex-direction: column;
1412
+ // gap: 15px;
1413
+ // }
1414
+ //
1415
+ // .test-case-header {
1416
+ // flex-direction: column;
1417
+ // align-items: flex-start;
1418
+ // gap: 10px;
1419
+ // }
1420
+ // }
1421
+ // </style>
1422
+ //</head>
1423
+ //<body>
1424
+ // <div class="notification" id="notification"></div>
1425
+ //
1426
+ // <div class="container">
1427
+ // <div class="header">
1428
+ // <h1>🔍 Test Cases Review & Approval</h1>
1429
+ // <p>Review, edit, and approve your test cases. Make any necessary changes before final approval.</p>
1430
+ // </div>
1431
+ //
1432
+ // <div class="stats">
1433
+ // <div class="stat-item">
1434
+ // <div class="stat-number" id="totalCount">${processedTestCases.length}</div>
1435
+ // <div class="stat-label">Total Cases</div>
1436
+ // </div>
1437
+ // <div class="stat-item">
1438
+ // <div class="stat-number" id="activeCount">${processedTestCases.length}</div>
1439
+ // <div class="stat-label">Active Cases</div>
1440
+ // </div>
1441
+ // <div class="stat-item">
1442
+ // <div class="stat-number" id="deletedCount">0</div>
1443
+ // <div class="stat-label">Deleted Cases</div>
1444
+ // </div>
1445
+ // </div>
1446
+ //
1447
+ // <div class="controls">
1448
+ // <div>
1449
+ // <button class="btn btn-secondary" onclick="resetAll()">🔄 Reset All</button>
1450
+ // </div>
1451
+ // <div>
1452
+ // <button class="btn btn-primary" onclick="approveTestCases()">✅ Approve Test Cases</button>
1453
+ // </div>
1454
+ // </div>
1455
+ //
1456
+ // <div class="test-cases">
1457
+ // ${processedTestCases.map((testCase, index) => {
1458
+ // const displayText = getTestCaseDisplayText(testCase);
1459
+ // const statusLabel = testCase.status === 'Remove' ? 'Remove' : testCase.status;
1460
+ //
1461
+ // // Create proper label and test ID display for modify/remove cases
1462
+ // let labelAndIdDisplay = '';
1463
+ // if (testCase.status.toLowerCase() === 'modify' && testCase.testId) {
1464
+ // labelAndIdDisplay = `<div style="margin-bottom: 8px; font-weight: 600; color: #856404; font-size: 13px;">Modify - ${testCase.testId}</div>`;
1465
+ // } else if (testCase.status.toLowerCase() === 'remove' && testCase.testId) {
1466
+ // labelAndIdDisplay = `<div style="margin-bottom: 8px; font-weight: 600; color: #721c24; font-size: 13px;">Remove - ${testCase.testId}</div>`;
1467
+ // }
1468
+ //
1469
+ // return `
1470
+ // <div class="test-case" data-index="${index}">
1471
+ // <div class="test-case-header">
1472
+ // <div class="test-case-meta">
1473
+ // <span class="test-case-index">#${index + 1}</span>
1474
+ // <span class="test-case-status status-${testCase.status.toLowerCase()}">${statusLabel}</span>
1475
+ // </div>
1476
+ // <button class="btn btn-delete" onclick="toggleDelete(${index})">Delete</button>
1477
+ // </div>
1478
+ // ${labelAndIdDisplay}
1479
+ // <textarea data-index="${index}" placeholder="Enter test case details...">${displayText}</textarea>
1480
+ // </div>
1481
+ // `;
1482
+ // }).join('')}
1483
+ // </div>
1484
+ // </div>
1485
+ //
1486
+ // <script>
1487
+ // let testCases = ${JSON.stringify(processedTestCases).replace(/</g, '\\u003c').replace(/>/g, '\\u003e')};
1488
+ // let deletedIndices = new Set();
1489
+ //
1490
+ // function updateStats() {
1491
+ // document.getElementById('activeCount').textContent = testCases.length - deletedIndices.size;
1492
+ // document.getElementById('deletedCount').textContent = deletedIndices.size;
1493
+ // }
1494
+ //
1495
+ // function toggleDelete(index) {
1496
+ // const testCaseEl = document.querySelector(\`[data-index="\${index}"]\`);
1497
+ // const btn = testCaseEl.querySelector('.btn-delete, .btn-restore');
1498
+ //
1499
+ // if (deletedIndices.has(index)) {
1500
+ // // Restore
1501
+ // deletedIndices.delete(index);
1502
+ // testCaseEl.classList.remove('deleted');
1503
+ // btn.textContent = 'Delete';
1504
+ // btn.className = 'btn btn-delete';
1505
+ // } else {
1506
+ // // Delete
1507
+ // deletedIndices.add(index);
1508
+ // testCaseEl.classList.add('deleted');
1509
+ // btn.textContent = 'Restore';
1510
+ // btn.className = 'btn btn-restore';
1511
+ // }
1512
+ // updateStats();
1513
+ // }
1514
+ //
1515
+ // function resetAll() {
1516
+ // deletedIndices.clear();
1517
+ // document.querySelectorAll('.test-case').forEach((el, index) => {
1518
+ // el.classList.remove('deleted');
1519
+ // const btn = el.querySelector('.btn-delete, .btn-restore');
1520
+ // btn.textContent = 'Delete';
1521
+ // btn.className = 'btn btn-delete';
1522
+ //
1523
+ // // Reset textarea value
1524
+ // const textarea = el.querySelector('textarea');
1525
+ // const originalTestCase = testCases[index];
1526
+ //
1527
+ // // Use proper display format based on status
1528
+ // let resetValue = '';
1529
+ // const status = originalTestCase.status.toLowerCase();
1530
+ //
1531
+ // if (status === 'modify') {
1532
+ // resetValue = 'Original: ' + originalTestCase.originalTitle + '\\nChanged to: ' + originalTestCase.newDescription;
1533
+ // } else if (status === 'remove') {
1534
+ // resetValue = originalTestCase.title;
1535
+ // } else {
1536
+ // resetValue = originalTestCase.title;
1537
+ // }
1538
+ //
1539
+ // textarea.value = resetValue;
1540
+ // });
1541
+ // updateStats();
1542
+ // }
1543
+ //
1544
+ // function showNotification(message, type = 'success') {
1545
+ // const notification = document.getElementById('notification');
1546
+ // notification.textContent = message;
1547
+ // notification.style.background = type === 'success' ? '#28a745' : '#dc3545';
1548
+ // notification.style.display = 'block';
1549
+ //
1550
+ // setTimeout(() => {
1551
+ // notification.style.display = 'none';
1552
+ // }, 3000);
1553
+ // }
1554
+ //
1555
+ // function approveTestCases() {
1556
+ // try {
1557
+ // // Collect updated test cases
1558
+ // const updatedTestCases = [];
1559
+ // document.querySelectorAll('.test-case').forEach((el, index) => {
1560
+ // if (!deletedIndices.has(index)) {
1561
+ // const textarea = el.querySelector('textarea');
1562
+ // const originalTestCase = testCases[index];
1563
+ //
1564
+ // // Create the updated test case array based on the original format
1565
+ // let updatedCase;
1566
+ // const status = originalTestCase.status.toLowerCase();
1567
+ //
1568
+ // if (status === 'modify') {
1569
+ // // For modify: [updatedContent, newDescription, "Modify", testId]
1570
+ // updatedCase = [
1571
+ // textarea.value.trim(),
1572
+ // originalTestCase.newDescription,
1573
+ // originalTestCase.status,
1574
+ // originalTestCase.testId
1575
+ // ];
1576
+ // } else if (status === 'remove') {
1577
+ // // For remove: [updatedContent, "Remove", testId]
1578
+ // updatedCase = [
1579
+ // textarea.value.trim(),
1580
+ // originalTestCase.status,
1581
+ // originalTestCase.testId
1582
+ // ];
1583
+ // } else {
1584
+ // // For new: [updatedContent, "New"]
1585
+ // updatedCase = [
1586
+ // textarea.value.trim(),
1587
+ // originalTestCase.status
1588
+ // ];
1589
+ // }
1590
+ //
1591
+ // updatedTestCases.push(updatedCase);
1592
+ // }
1593
+ // });
1594
+ //
1595
+ // // Send approval to server
1596
+ // fetch('/approve', {
1597
+ // method: 'POST',
1598
+ // headers: {
1599
+ // 'Content-Type': 'application/json',
1600
+ // },
1601
+ // body: JSON.stringify({
1602
+ // sessionId: '${sessionId}',
1603
+ // testCases: updatedTestCases
1604
+ // })
1605
+ // })
1606
+ // .then(response => response.json())
1607
+ // .then(data => {
1608
+ // if (data.success) {
1609
+ // showNotification('Test cases approved successfully!');
1610
+ // setTimeout(() => {
1611
+ // window.close();
1612
+ // }, 2000);
1613
+ // } else {
1614
+ // showNotification('Error approving test cases', 'error');
1615
+ // }
1616
+ // })
1617
+ // .catch(error => {
1618
+ // showNotification('Error approving test cases', 'error');
1619
+ // console.error('Error:', error);
1620
+ // });
1621
+ // } catch (error) {
1622
+ // showNotification('Error processing test cases', 'error');
1623
+ // console.error('Error:', error);
1624
+ // }
1625
+ // }
1626
+ //
1627
+ // // Update test cases when textarea changes
1628
+ // document.addEventListener('input', function(e) {
1629
+ // if (e.target.tagName === 'TEXTAREA') {
1630
+ // const index = parseInt(e.target.getAttribute('data-index'));
1631
+ // if (!isNaN(index) && testCases[index]) {
1632
+ // // Update the title with the textarea content
1633
+ // testCases[index].title = e.target.value.trim();
1634
+ // }
1635
+ // }
1636
+ // });
1637
+ // </script>
1638
+ //</body>
1639
+ //</html>
1640
+ // `;
1641
+ // res.send(htmlContent);
1642
+ // } catch (error) {
1643
+ // console.error('Error rendering page:', error);
1644
+ // res.status(500).send('Error rendering page');
1645
+ // }
1646
+ // });
1647
+ //
1648
+ // // Approval endpoint with better error handling
1649
+ // app.post('/approve', (req, res) => {
1650
+ // try {
1651
+ // const { testCases: approvedTestCases, sessionId: receivedSessionId } = req.body;
1652
+ //
1653
+ // if (receivedSessionId !== sessionId) {
1654
+ // return res.status(400).json({ success: false, message: 'Invalid session ID' });
1655
+ // }
1656
+ //
1657
+ // finalTestCases = approvedTestCases;
1658
+ // approvalStatus = 'approved';
1659
+ //
1660
+ // // Save to global state for the check tool
1661
+ // global.approvalSessions = global.approvalSessions || {};
1662
+ // global.approvalSessions[sessionId] = {
1663
+ // status: 'approved',
1664
+ // testCases: finalTestCases,
1665
+ // timestamp: Date.now()
1666
+ // };
1667
+ //
1668
+ // res.json({ success: true, message: 'Test cases approved successfully' });
1669
+ //
1670
+ // // Close server after approval
1671
+ // setTimeout(() => {
1672
+ // if (server && server.listening) {
1673
+ // server.close();
1674
+ // }
1675
+ // }, 3000);
1676
+ // } catch (error) {
1677
+ // console.error('Approval error:', error);
1678
+ // res.status(500).json({ success: false, message: error.message });
1679
+ // }
1680
+ // });
1681
+ //
1682
+ // // Error handling middleware
1683
+ // app.use((err, req, res, next) => {
1684
+ // console.error('Express error:', err);
1685
+ // res.status(500).json({ error: 'Internal server error' });
1686
+ // });
1687
+ //
1688
+ // // 404 handler
1689
+ // app.use((req, res) => {
1690
+ // res.status(404).json({ error: 'Not found' });
1691
+ // });
1692
+ //
1693
+ // // Start server with promise-based approach
1694
+ // const server = await new Promise((resolve, reject) => {
1695
+ // const srv = app.listen(port, (err) => {
1696
+ // if (err) {
1697
+ // reject(err);
1698
+ // return;
1699
+ // }
1700
+ // console.log(`✅ Test case review session started. Session ID: ${sessionId}.`);
1701
+ // console.log(`Server running at http://localhost:${port}`);
1702
+ // console.log(`Browser should open automatically.`);
1703
+ // resolve(srv);
1704
+ // });
1705
+ //
1706
+ // srv.on('error', (error) => {
1707
+ // reject(error);
1708
+ // });
1709
+ // });
1710
+ //
1711
+ // // Open browser with proper error handling
1712
+ // let openAttemptFailed = false;
1713
+ // try {
1714
+ // await openBrowser(`http://localhost:${port}`);
1715
+ // } catch (err) {
1716
+ // openAttemptFailed = true;
1717
+ // console.error('Failed to open browser automatically:', err.message);
1718
+ // // Continue without opening browser - user can manually navigate to the URL
1719
+ // }
1720
+ //
1721
+ // // Store session globally for status checking
1722
+ // global.approvalSessions = global.approvalSessions || {};
1723
+ // global.approvalSessions[sessionId] = {
1724
+ // status: 'pending',
1725
+ // testCases: processedTestCases,
1726
+ // timestamp: Date.now(),
1727
+ // server: server
1728
+ // };
1729
+ //
1730
+ // return `✅ Test case review session started. Session ID: ${sessionId}.\nServer running at http://localhost:${port}\n${openAttemptFailed ? 'Please manually open the URL in your browser.' : 'Browser should open automatically.'}`;
1731
+ //
1732
+ // } catch (err) {
1733
+ // console.error('Review tool error:', err);
1734
+ // return `❌ Error starting test case review: ${err.message}`;
1735
+ // }
1736
+ // }
1737
+ //);
1738
+ //
1739
+ //// Fix 3: Updated check_approval_status tool with better error handling
1740
+ //tool(
1741
+ // "check_approval_status",
1742
+ // "Check the approval status of test cases review session (waits 25 seconds before checking)",
1743
+ // {
1744
+ // sessionId: zod_1.z.string().describe("Session ID from review_testcases")
1745
+ // },
1746
+ // async ({ sessionId }) => {
1747
+ // try {
1748
+ // // Wait for 25 seconds
1749
+ // await new Promise(resolve => setTimeout(resolve, 25000));
1750
+ //
1751
+ // // Check global approval sessions
1752
+ // if (!global.approvalSessions || !global.approvalSessions[sessionId]) {
1753
+ // return "❌ Session not found. Please ensure the review session is still active.";
1754
+ // }
1755
+ //
1756
+ // const session = global.approvalSessions[sessionId];
1757
+ //
1758
+ // if (session.status === 'approved') {
1759
+ // const result = {
1760
+ // status: 'approved',
1761
+ // testCases: session.testCases,
1762
+ // approvedCount: session.testCases.length,
1763
+ // sessionId: sessionId
1764
+ // };
1765
+ //
1766
+ // // Format the approved test cases properly
1767
+ // const formattedTestCases = result.testCases.map((tc, index) => {
1768
+ // if (!Array.isArray(tc)) {
1769
+ // return `${index + 1}. ${String(tc)} (New)`;
1770
+ // }
1771
+ //
1772
+ // // Handle different array structures based on length and content
1773
+ // let title, description, status, originalCase;
1774
+ //
1775
+ // if (tc.length === 4) {
1776
+ // // Standard format: [title, description, status, originalCase]
1777
+ // title = tc[0] || `Test Case ${index + 1}`;
1778
+ // description = tc[1] || '';
1779
+ // status = tc[2] || 'New';
1780
+ // originalCase = tc[3] || '';
1781
+ // } else if (tc.length === 3) {
1782
+ // // Could be [title, status, originalCase] for remove cases
1783
+ // title = tc[0] || `Test Case ${index + 1}`;
1784
+ // if (tc[1] && tc[1].toLowerCase() === 'remove') {
1785
+ // status = tc[1];
1786
+ // originalCase = tc[2] || '';
1787
+ // description = '';
1788
+ // } else {
1789
+ // // [title, description, status]
1790
+ // description = tc[1] || '';
1791
+ // status = tc[2] || 'New';
1792
+ // originalCase = '';
1793
+ // }
1794
+ // } else {
1795
+ // // Fallback
1796
+ // title = tc[0] || `Test Case ${index + 1}`;
1797
+ // description = tc[1] || '';
1798
+ // status = tc[2] || 'New';
1799
+ // originalCase = tc[3] || '';
1800
+ // }
1801
+ //
1802
+ // const statusLower = status.toLowerCase();
1803
+ //
1804
+ // if (statusLower === 'modify') {
1805
+ // // For modify cases: show "Original: ... Changed to: ..." format with proper test ID
1806
+ // return `${index + 1}. Original: ${title}\n Changed to: ${description} (Modify) (${originalCase})`;
1807
+ // } else if (statusLower === 'remove') {
1808
+ // // For remove cases: show title with Remove label and reference
1809
+ // return `${index + 1}. ${title} (Remove) (${originalCase})`;
1810
+ // } else {
1811
+ // // For new cases: just show title with New label
1812
+ // return `${index + 1}. ${title} (New)`;
1813
+ // }
1814
+ // }).join('\n');
1815
+ //
1816
+ // // Clean up session after returning result
1817
+ // delete global.approvalSessions[sessionId];
1818
+ //
1819
+ // return `✅ Test cases approved successfully!\n\nApproved ${result.approvedCount} test cases:\n\n${formattedTestCases}\n\nSession completed: ${sessionId}`;
1820
+ // } else {
1821
+ // return "⏳ Still waiting for approval. The review session is active but not yet approved. Please complete the review in the browser.";
1822
+ // }
1823
+ //
1824
+ // } catch (err) {
1825
+ // console.error('Check approval status error:', err);
1826
+ // return `❌ Error checking approval status: ${err.message}`;
1827
+ // }
1828
+ // }
1829
+ //);
1830
+ //
1831
+ //tool(
1832
+ // "update_testcases_to_tcms",
1833
+ // "Create new test cases in TCMS from approved test cases. Only processes test cases with 'New' status, ignores Modify and Remove cases since APIs are not available.",
1834
+ // {
1835
+ // testCases: zod_1.z.array(zod_1.z.array(zod_1.z.string())).describe("Array of test case arrays from approved test cases")
1836
+ // },
1837
+ // async ({ testCases }) => {
1838
+ // try {
1839
+ // // Load AIO token from Desktop/aio.json
1840
+ // const aioConfigPath = path.join(os.homedir(), "Desktop", "aio.json");
1841
+ // const configContent = await fs.readFile(aioConfigPath, "utf-8");
1842
+ // const { token } = JSON.parse(configContent);
1843
+ //
1844
+ // if (!token) throw new Error("AIO token missing in aio.json");
1845
+ //
1846
+ // // Filter test cases to extract only "New" test cases
1847
+ // const newTestCases = [];
1848
+ //
1849
+ // for (const testCase of testCases) {
1850
+ // if (Array.isArray(testCase) && testCase.length >= 2) {
1851
+ // // Check if the last element or second-to-last element is "New"
1852
+ // const status = testCase.length === 2 ? testCase[1] : testCase[testCase.length - 2];
1853
+ //
1854
+ // if (status && status.toLowerCase() === 'new') {
1855
+ // const title = testCase[0]; // First element is always the title
1856
+ // if (title && title.trim().length > 0) {
1857
+ // newTestCases.push(title.trim());
1858
+ // }
1859
+ // }
1860
+ // }
1861
+ // }
1862
+ //
1863
+ // if (newTestCases.length === 0) {
1864
+ // return "No new test cases found to create in TCMS. Only test cases marked as '(New)' are processed.";
1865
+ // }
1866
+ //
1867
+ // // Hard-coded values as requested
1868
+ // const projectKey = "SCRUM";
1869
+ // const folderId = 1;
1870
+ // const ownerId = "712020:37085ff2-5a05-47eb-8977-50a485355755";
1871
+ //
1872
+ // // Create test cases in TCMS one by one
1873
+ // for (let i = 0; i < newTestCases.length; i++) {
1874
+ // const title = newTestCases[i];
1875
+ //
1876
+ // try {
1877
+ // const requestBody = {
1878
+ // title: title,
1879
+ // ownedByID: ownerId,
1880
+ // folder: {
1881
+ // ID: folderId
1882
+ // },
1883
+ // status: {
1884
+ // name: "Published",
1885
+ // description: "The test is ready for execution",
1886
+ // ID: 1
1887
+ // }
1888
+ // };
1889
+ //
1890
+ // (0, logger_1.trace)(`Creating test case ${i + 1}/${newTestCases.length}: ${title}`);
1891
+ //
1892
+ // const response = await axios.post(
1893
+ // `https://tcms.aiojiraapps.com/aio-tcms/api/v1/project/${projectKey}/testcase`,
1894
+ // requestBody,
1895
+ // {
1896
+ // headers: {
1897
+ // "accept": "application/json;charset=utf-8",
1898
+ // "Authorization": `AioAuth ${token}`,
1899
+ // "Content-Type": "application/json"
1900
+ // }
1901
+ // }
1902
+ // );
1903
+ //
1904
+ // if (response.status === 200 || response.status === 201) {
1905
+ // const testCaseKey = response.data.key || `${projectKey}-TC-${response.data.ID}`;
1906
+ // (0, logger_1.trace)(`Successfully created test case: ${testCaseKey} - ${title}`);
1907
+ // }
1908
+ //
1909
+ // // Add a small delay between requests to avoid rate limiting
1910
+ // await new Promise(resolve => setTimeout(resolve, 500));
1911
+ //
1912
+ // } catch (error) {
1913
+ // (0, logger_1.trace)(`Failed to create test case: ${title} - ${error.message}`);
1914
+ // throw new Error(`Failed to create test case "${title}": ${error.message}`);
1915
+ // }
1916
+ // }
1917
+ //
1918
+ // return "All test cases have been updated to TCMS";
1919
+ //
1920
+ // } catch (error) {
1921
+ // console.error('TCMS update error:', error);
1922
+ // if (error.response) {
1923
+ // return `❌ TCMS API Error: ${error.response.status} - ${error.response.data?.message || error.response.statusText}`;
1924
+ // }
1925
+ // return `❌ Error updating test cases to TCMS: ${error.message}`;
1926
+ // }
1927
+ // }
1928
+ //);
1904
1929
 
1905
1930
  return server;
1906
1931
  };