@nbakka/mcp-appium 3.0.23 → 3.0.25

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