@browserstack/mcp-server 1.2.3-beta.1 → 1.2.4

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 (126) hide show
  1. package/README.md +88 -2
  2. package/dist/lib/device-cache.js +20 -17
  3. package/dist/lib/inmemory-store.d.ts +1 -0
  4. package/dist/lib/inmemory-store.js +1 -0
  5. package/dist/lib/utils.d.ts +5 -0
  6. package/dist/lib/utils.js +27 -0
  7. package/dist/server-factory.js +6 -0
  8. package/dist/tools/add-percy-snapshots.d.ts +5 -0
  9. package/dist/tools/add-percy-snapshots.js +17 -0
  10. package/dist/tools/appautomate-utils/appium-sdk/types.d.ts +2 -2
  11. package/dist/tools/appautomate-utils/appium-sdk/types.js +2 -9
  12. package/dist/tools/appautomate-utils/appium-sdk/utils.js +3 -0
  13. package/dist/tools/bstack-sdk.d.ts +2 -15
  14. package/dist/tools/bstack-sdk.js +10 -119
  15. package/dist/tools/build-insights.d.ts +7 -0
  16. package/dist/tools/build-insights.js +67 -0
  17. package/dist/tools/list-test-files.d.ts +2 -0
  18. package/dist/tools/list-test-files.js +36 -0
  19. package/dist/tools/percy-sdk.d.ts +4 -0
  20. package/dist/tools/percy-sdk.js +71 -0
  21. package/dist/tools/percy-snapshot-utils/constants.d.ts +16 -0
  22. package/dist/tools/percy-snapshot-utils/constants.js +500 -0
  23. package/dist/tools/percy-snapshot-utils/detect-test-files.d.ts +10 -0
  24. package/dist/tools/percy-snapshot-utils/detect-test-files.js +175 -0
  25. package/dist/tools/percy-snapshot-utils/types.d.ts +15 -0
  26. package/dist/tools/percy-snapshot-utils/utils.d.ts +4 -0
  27. package/dist/tools/percy-snapshot-utils/utils.js +30 -0
  28. package/dist/tools/rca-agent-utils/constants.d.ts +13 -0
  29. package/dist/tools/rca-agent-utils/constants.js +24 -0
  30. package/dist/tools/rca-agent-utils/format-rca.d.ts +1 -0
  31. package/dist/tools/rca-agent-utils/format-rca.js +37 -0
  32. package/dist/tools/rca-agent-utils/get-build-id.d.ts +1 -0
  33. package/dist/tools/rca-agent-utils/get-build-id.js +18 -0
  34. package/dist/tools/rca-agent-utils/get-failed-test-id.d.ts +2 -0
  35. package/dist/tools/rca-agent-utils/get-failed-test-id.js +69 -0
  36. package/dist/tools/rca-agent-utils/rca-data.d.ts +9 -0
  37. package/dist/tools/rca-agent-utils/rca-data.js +196 -0
  38. package/dist/tools/rca-agent-utils/types.d.ts +48 -0
  39. package/dist/tools/rca-agent-utils/types.js +20 -0
  40. package/dist/tools/rca-agent.d.ts +14 -0
  41. package/dist/tools/rca-agent.js +119 -0
  42. package/dist/tools/review-agent-utils/build-counts.d.ts +7 -0
  43. package/dist/tools/review-agent-utils/build-counts.js +44 -0
  44. package/dist/tools/review-agent-utils/percy-approve-reject.d.ts +6 -0
  45. package/dist/tools/review-agent-utils/percy-approve-reject.js +39 -0
  46. package/dist/tools/review-agent-utils/percy-diffs.d.ts +9 -0
  47. package/dist/tools/review-agent-utils/percy-diffs.js +35 -0
  48. package/dist/tools/review-agent-utils/percy-snapshots.d.ts +11 -0
  49. package/dist/tools/review-agent-utils/percy-snapshots.js +58 -0
  50. package/dist/tools/review-agent.d.ts +5 -0
  51. package/dist/tools/review-agent.js +56 -0
  52. package/dist/tools/run-percy-scan.d.ts +8 -0
  53. package/dist/tools/run-percy-scan.js +37 -0
  54. package/dist/tools/sdk-utils/{commands.d.ts → bstack/commands.d.ts} +1 -1
  55. package/dist/tools/sdk-utils/bstack/commands.js +88 -0
  56. package/dist/tools/sdk-utils/bstack/configUtils.d.ts +4 -0
  57. package/dist/tools/sdk-utils/bstack/configUtils.js +66 -0
  58. package/dist/tools/sdk-utils/bstack/constants.d.ts +58 -0
  59. package/dist/tools/sdk-utils/{constants.js → bstack/constants.js} +117 -78
  60. package/dist/tools/sdk-utils/{constants.d.ts → bstack/frameworks.d.ts} +1 -1
  61. package/dist/tools/sdk-utils/bstack/frameworks.js +57 -0
  62. package/dist/tools/sdk-utils/bstack/index.d.ts +4 -0
  63. package/dist/tools/sdk-utils/bstack/index.js +5 -0
  64. package/dist/tools/sdk-utils/bstack/sdkHandler.d.ts +4 -0
  65. package/dist/tools/sdk-utils/bstack/sdkHandler.js +74 -0
  66. package/dist/tools/sdk-utils/common/constants.d.ts +10 -0
  67. package/dist/tools/sdk-utils/common/constants.js +86 -0
  68. package/dist/tools/sdk-utils/common/formatUtils.d.ts +5 -0
  69. package/dist/tools/sdk-utils/common/formatUtils.js +27 -0
  70. package/dist/tools/sdk-utils/common/index.d.ts +3 -0
  71. package/dist/tools/sdk-utils/common/index.js +4 -0
  72. package/dist/tools/sdk-utils/common/instructionUtils.d.ts +8 -0
  73. package/dist/tools/sdk-utils/common/instructionUtils.js +20 -0
  74. package/dist/tools/sdk-utils/common/schema.d.ts +73 -0
  75. package/dist/tools/sdk-utils/common/schema.js +51 -0
  76. package/dist/tools/sdk-utils/common/types.d.ts +66 -0
  77. package/dist/tools/sdk-utils/{types.js → common/types.js} +15 -2
  78. package/dist/tools/sdk-utils/common/utils.d.ts +25 -0
  79. package/dist/tools/sdk-utils/common/utils.js +90 -0
  80. package/dist/tools/sdk-utils/handler.d.ts +4 -0
  81. package/dist/tools/sdk-utils/handler.js +119 -0
  82. package/dist/tools/sdk-utils/percy-automate/constants.d.ts +11 -0
  83. package/dist/tools/sdk-utils/percy-automate/constants.js +338 -0
  84. package/dist/tools/sdk-utils/percy-automate/frameworks.d.ts +8 -0
  85. package/dist/tools/sdk-utils/percy-automate/frameworks.js +50 -0
  86. package/dist/tools/sdk-utils/percy-automate/handler.d.ts +3 -0
  87. package/dist/tools/sdk-utils/percy-automate/handler.js +30 -0
  88. package/dist/tools/sdk-utils/percy-automate/index.d.ts +1 -0
  89. package/dist/tools/sdk-utils/percy-automate/index.js +2 -0
  90. package/dist/tools/sdk-utils/percy-automate/types.d.ts +13 -0
  91. package/dist/tools/sdk-utils/percy-automate/types.js +1 -0
  92. package/dist/tools/sdk-utils/percy-bstack/constants.d.ts +4 -0
  93. package/dist/tools/sdk-utils/{percy → percy-bstack}/constants.js +13 -39
  94. package/dist/tools/sdk-utils/percy-bstack/frameworks.d.ts +2 -0
  95. package/dist/tools/sdk-utils/percy-bstack/frameworks.js +27 -0
  96. package/dist/tools/sdk-utils/percy-bstack/handler.d.ts +4 -0
  97. package/dist/tools/sdk-utils/percy-bstack/handler.js +99 -0
  98. package/dist/tools/sdk-utils/percy-bstack/index.d.ts +4 -0
  99. package/dist/tools/sdk-utils/percy-bstack/index.js +4 -0
  100. package/dist/tools/sdk-utils/percy-bstack/instructions.d.ts +7 -0
  101. package/dist/tools/sdk-utils/{percy → percy-bstack}/instructions.js +5 -9
  102. package/dist/tools/sdk-utils/percy-bstack/types.d.ts +13 -0
  103. package/dist/tools/sdk-utils/percy-bstack/types.js +5 -0
  104. package/dist/tools/sdk-utils/percy-web/constants.d.ts +41 -0
  105. package/dist/tools/sdk-utils/percy-web/constants.js +883 -0
  106. package/dist/tools/sdk-utils/percy-web/fetchPercyToken.d.ts +4 -0
  107. package/dist/tools/sdk-utils/percy-web/fetchPercyToken.js +32 -0
  108. package/dist/tools/sdk-utils/percy-web/frameworks.d.ts +7 -0
  109. package/dist/tools/sdk-utils/percy-web/frameworks.js +103 -0
  110. package/dist/tools/sdk-utils/percy-web/handler.d.ts +4 -0
  111. package/dist/tools/sdk-utils/percy-web/handler.js +27 -0
  112. package/dist/tools/sdk-utils/percy-web/index.d.ts +4 -0
  113. package/dist/tools/sdk-utils/percy-web/index.js +4 -0
  114. package/dist/tools/sdk-utils/percy-web/types.d.ts +12 -0
  115. package/dist/tools/sdk-utils/percy-web/types.js +1 -0
  116. package/dist/tools/testmanagement-utils/create-testrun.d.ts +4 -4
  117. package/dist/tools/testmanagement-utils/update-testrun.d.ts +4 -4
  118. package/package.json +2 -1
  119. package/dist/tools/sdk-utils/commands.js +0 -65
  120. package/dist/tools/sdk-utils/instructions.d.ts +0 -6
  121. package/dist/tools/sdk-utils/instructions.js +0 -99
  122. package/dist/tools/sdk-utils/percy/constants.d.ts +0 -3
  123. package/dist/tools/sdk-utils/percy/instructions.d.ts +0 -10
  124. package/dist/tools/sdk-utils/percy/types.d.ts +0 -5
  125. package/dist/tools/sdk-utils/types.d.ts +0 -40
  126. /package/dist/tools/{sdk-utils/percy → percy-snapshot-utils}/types.js +0 -0
@@ -0,0 +1,175 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { EXCLUDED_DIRS, TEST_FILE_DETECTION, backendIndicators, strongUIIndicators, } from "../percy-snapshot-utils/constants.js";
4
+ import logger from "../../logger.js";
5
+ async function walkDir(dir, extensions, depth = 6) {
6
+ const result = [];
7
+ if (depth < 0)
8
+ return result;
9
+ try {
10
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
11
+ for (const entry of entries) {
12
+ const fullPath = path.join(dir, entry.name);
13
+ if (entry.isDirectory()) {
14
+ if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
15
+ result.push(...(await walkDir(fullPath, extensions, depth - 1)));
16
+ }
17
+ }
18
+ else if (extensions.some((ext) => entry.name.endsWith(ext))) {
19
+ result.push(fullPath);
20
+ }
21
+ }
22
+ }
23
+ catch {
24
+ logger.info("Failed to read user directory");
25
+ }
26
+ return result;
27
+ }
28
+ async function fileContainsRegex(filePath, regexes) {
29
+ if (!regexes.length)
30
+ return false;
31
+ try {
32
+ const content = await fs.promises.readFile(filePath, "utf8");
33
+ return regexes.some((re) => re.test(content));
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ async function batchRegexCheck(filePath, regexGroups) {
40
+ try {
41
+ const content = await fs.promises.readFile(filePath, "utf8");
42
+ return regexGroups.map((regexes) => regexes.length > 0 ? regexes.some((re) => re.test(content)) : false);
43
+ }
44
+ catch {
45
+ return regexGroups.map(() => false);
46
+ }
47
+ }
48
+ async function isLikelyUITest(filePath) {
49
+ try {
50
+ const content = await fs.promises.readFile(filePath, "utf8");
51
+ if (backendIndicators.some((pattern) => pattern.test(content))) {
52
+ return false;
53
+ }
54
+ return strongUIIndicators.some((pattern) => pattern.test(content));
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ function getFileScore(fileName, config) {
61
+ let score = 0;
62
+ // Higher score for explicit UI test naming
63
+ if (/ui|web|e2e|integration|functional/i.test(fileName))
64
+ score += 3;
65
+ if (config.namePatterns.some((pattern) => pattern.test(fileName)))
66
+ score += 2;
67
+ return score;
68
+ }
69
+ export async function listTestFiles(options) {
70
+ const { language, framework, baseDir, strictMode = false } = options;
71
+ const config = TEST_FILE_DETECTION[language];
72
+ if (!config) {
73
+ return [];
74
+ }
75
+ // Step 1: Collect all files with matching extensions
76
+ let files = [];
77
+ try {
78
+ files = await walkDir(baseDir, config.extensions, 6);
79
+ }
80
+ catch {
81
+ return [];
82
+ }
83
+ if (files.length === 0) {
84
+ throw new Error("No files found with the specified extensions");
85
+ }
86
+ const candidateFiles = new Map();
87
+ // Step 2: Fast name-based identification with scoring
88
+ for (const file of files) {
89
+ const fileName = path.basename(file);
90
+ const score = getFileScore(fileName, config);
91
+ if (config.namePatterns.some((pattern) => pattern.test(fileName))) {
92
+ candidateFiles.set(file, score);
93
+ }
94
+ }
95
+ // Step 3: Content-based test detection for remaining files
96
+ const remainingFiles = files.filter((file) => !candidateFiles.has(file));
97
+ const contentCheckPromises = remainingFiles.map(async (file) => {
98
+ const hasTestContent = await fileContainsRegex(file, config.contentRegex);
99
+ if (hasTestContent) {
100
+ const fileName = path.basename(file);
101
+ const score = getFileScore(fileName, config);
102
+ candidateFiles.set(file, score);
103
+ }
104
+ });
105
+ await Promise.all(contentCheckPromises);
106
+ // Step 4: Handle SpecFlow .feature files for C# + SpecFlow
107
+ if (language === "csharp" && framework === "specflow") {
108
+ try {
109
+ const featureFiles = await walkDir(baseDir, [".feature"], 6);
110
+ featureFiles.forEach((file) => candidateFiles.set(file, 2));
111
+ }
112
+ catch {
113
+ // ignore
114
+ }
115
+ }
116
+ if (candidateFiles.size === 0) {
117
+ return [];
118
+ }
119
+ // Step 6: UI Detection with fallback patterns
120
+ const uiFiles = [];
121
+ const filesToCheck = Array.from(candidateFiles.keys());
122
+ // Batch process UI detection for better performance
123
+ const batchSize = 10;
124
+ for (let i = 0; i < filesToCheck.length; i += batchSize) {
125
+ const batch = filesToCheck.slice(i, i + batchSize);
126
+ const batchPromises = batch.map(async (file) => {
127
+ // First, use the new precise UI detection
128
+ const isUITest = await isLikelyUITest(file);
129
+ if (isUITest) {
130
+ return file;
131
+ }
132
+ // If not clearly UI, run the traditional checks
133
+ const [hasExplicitUI, hasUIIndicators, hasBackend, shouldExclude] = await batchRegexCheck(file, [
134
+ config.uiDriverRegex,
135
+ config.uiIndicatorRegex,
136
+ config.backendRegex,
137
+ config.excludeRegex || [],
138
+ ]);
139
+ if (shouldExclude) {
140
+ return null;
141
+ }
142
+ if (hasBackend) {
143
+ return null;
144
+ }
145
+ if (hasExplicitUI) {
146
+ return file;
147
+ }
148
+ if (hasUIIndicators) {
149
+ return file;
150
+ }
151
+ if (!strictMode) {
152
+ const score = candidateFiles.get(file) || 0;
153
+ if (score >= 3) {
154
+ return file;
155
+ }
156
+ }
157
+ return null;
158
+ });
159
+ const batchResults = await Promise.all(batchPromises);
160
+ uiFiles.push(...batchResults.filter((file) => file !== null));
161
+ }
162
+ // Step 7: Sort by score (higher confidence files first)
163
+ uiFiles.sort((a, b) => {
164
+ const scoreA = candidateFiles.get(a) || 0;
165
+ const scoreB = candidateFiles.get(b) || 0;
166
+ return scoreB - scoreA;
167
+ });
168
+ return uiFiles;
169
+ }
170
+ export async function listUITestFilesStrict(options) {
171
+ return listTestFiles({ ...options, strictMode: true });
172
+ }
173
+ export async function listUITestFilesRelaxed(options) {
174
+ return listTestFiles({ ...options, strictMode: false });
175
+ }
@@ -0,0 +1,15 @@
1
+ import { SDKSupportedTestingFrameworkEnum, SDKSupportedLanguageEnum } from "../sdk-utils/common/types.js";
2
+ export type ListTestFilesParams = {
3
+ dirs: string[];
4
+ language: SDKSupportedLanguageEnum;
5
+ framework?: SDKSupportedTestingFrameworkEnum;
6
+ };
7
+ export interface DetectionConfig {
8
+ extensions: string[];
9
+ namePatterns: RegExp[];
10
+ contentRegex: RegExp[];
11
+ uiDriverRegex: RegExp[];
12
+ uiIndicatorRegex: RegExp[];
13
+ backendRegex: RegExp[];
14
+ excludeRegex?: RegExp[];
15
+ }
@@ -0,0 +1,4 @@
1
+ export declare function updateFileAndStep(file: string, idx: number, total: number, instruction: string): Promise<{
2
+ type: "text";
3
+ text: string;
4
+ }[]>;
@@ -0,0 +1,30 @@
1
+ const content = [];
2
+ export async function updateFileAndStep(file, idx, total, instruction) {
3
+ content.length = 0;
4
+ const nextIndex = idx + 1;
5
+ content.push({
6
+ type: "text",
7
+ text: `Complete all steps in order. If a tool call is requested, update the file first, then call the tool. Follow instructions exactly— do not skip any steps to ensure all files are updated.`,
8
+ });
9
+ content.push({
10
+ type: "text",
11
+ text: `Step 1 : You need to add percy snapshot commands in some key test cases in the file ${file} use the following instructions: \n${instruction}`,
12
+ });
13
+ content.push({
14
+ type: "text",
15
+ text: `Step 2 : Confirm that Percy snapshot commands have been added at all key points of visual change in the file ${file}.`,
16
+ });
17
+ if (nextIndex < total) {
18
+ content.push({
19
+ type: "text",
20
+ text: `Step 3 : Call the tool updateTestFileWithInstructions with index as ${nextIndex} out of ${total}`,
21
+ });
22
+ }
23
+ if (nextIndex === total) {
24
+ content.push({
25
+ type: "text",
26
+ text: `Step 3: Percy snapshot commands have been added to all files. You can now run the tool runPercyScan to run the percy scan.`,
27
+ });
28
+ }
29
+ return content;
30
+ }
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import { TestStatus } from "./types.js";
3
+ export declare const FETCH_RCA_PARAMS: {
4
+ testId: z.ZodArray<z.ZodString, "many">;
5
+ };
6
+ export declare const GET_BUILD_ID_PARAMS: {
7
+ browserStackProjectName: z.ZodString;
8
+ browserStackBuildName: z.ZodString;
9
+ };
10
+ export declare const LIST_TEST_IDS_PARAMS: {
11
+ buildId: z.ZodString;
12
+ status: z.ZodNativeEnum<typeof TestStatus>;
13
+ };
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { TestStatus } from "./types.js";
3
+ export const FETCH_RCA_PARAMS = {
4
+ testId: z
5
+ .array(z.string())
6
+ .max(3)
7
+ .describe("Array of test IDs to fetch RCA data for (maximum 3 IDs). If not provided, use the listTestIds tool get all failed testcases. If more than 3 IDs are provided, only the first 3 will be processed."),
8
+ };
9
+ export const GET_BUILD_ID_PARAMS = {
10
+ browserStackProjectName: z
11
+ .string()
12
+ .describe("The BrowserStack project name used during test run creation. Action: First, check browserstack.yml or any equivalent project configuration files. If the project name is found, extract and return it. If it is not found or if there is any uncertainty, immediately prompt the user to provide the value. Do not infer, guess, or assume a default."),
13
+ browserStackBuildName: z
14
+ .string()
15
+ .describe("The BrowserStack build name used during test run creation. Action: First, check browserstack.yml or any equivalent project configuration files. If the build name is found, extract and return it. If it is not found or if there is any uncertainty, immediately prompt the user to provide the value. Do not infer, guess, or assume a default."),
16
+ };
17
+ export const LIST_TEST_IDS_PARAMS = {
18
+ buildId: z
19
+ .string()
20
+ .describe("The Browserstack Build ID of the test run. If not known, use the getBuildId tool to fetch it using project and build name"),
21
+ status: z
22
+ .nativeEnum(TestStatus)
23
+ .describe("Filter tests by status. If not provided, all tests are returned. Example for RCA usecase always use failed status"),
24
+ };
@@ -0,0 +1 @@
1
+ export declare function formatRCAData(rcaData: any): string;
@@ -0,0 +1,37 @@
1
+ // Utility function to format RCA data for better readability
2
+ export function formatRCAData(rcaData) {
3
+ if (!rcaData || !rcaData.testCases || rcaData.testCases.length === 0) {
4
+ return "No RCA data available.";
5
+ }
6
+ let output = "## Root Cause Analysis Report\n\n";
7
+ rcaData.testCases.forEach((testCase, index) => {
8
+ // Show test case ID with smaller heading
9
+ output += `### Test Case ${index + 1}\n`;
10
+ output += `**Test ID:** ${testCase.id}\n`;
11
+ output += `**Status:** ${testCase.state}\n\n`;
12
+ // Access RCA data from the correct path
13
+ const rca = testCase.rcaData?.rcaData;
14
+ if (rca) {
15
+ if (rca.root_cause) {
16
+ output += `**Root Cause:** ${rca.root_cause}\n\n`;
17
+ }
18
+ if (rca.failure_type) {
19
+ output += `**Failure Type:** ${rca.failure_type}\n\n`;
20
+ }
21
+ if (rca.description) {
22
+ output += `**Detailed Analysis:**\n${rca.description}\n\n`;
23
+ }
24
+ if (rca.possible_fix) {
25
+ output += `**Recommended Fix:**\n${rca.possible_fix}\n\n`;
26
+ }
27
+ }
28
+ else if (testCase.rcaData?.error) {
29
+ output += `**Error:** ${testCase.rcaData.error}\n\n`;
30
+ }
31
+ else if (testCase.state === "failed") {
32
+ output += `**Note:** RCA analysis failed or is not available for this test case.\n\n`;
33
+ }
34
+ output += "---\n\n";
35
+ });
36
+ return output;
37
+ }
@@ -0,0 +1 @@
1
+ export declare function getBuildId(projectName: string, buildName: string, username: string, accessKey: string): Promise<string>;
@@ -0,0 +1,18 @@
1
+ export async function getBuildId(projectName, buildName, username, accessKey) {
2
+ const url = new URL("https://api-automation.browserstack.com/ext/v1/builds/latest");
3
+ url.searchParams.append("project_name", projectName);
4
+ url.searchParams.append("build_name", buildName);
5
+ url.searchParams.append("user_name", username);
6
+ const authHeader = "Basic " + Buffer.from(`${username}:${accessKey}`).toString("base64");
7
+ const response = await fetch(url.toString(), {
8
+ headers: {
9
+ Authorization: authHeader,
10
+ "Content-Type": "application/json",
11
+ },
12
+ });
13
+ if (!response.ok) {
14
+ throw new Error(`Failed to fetch build ID: ${response.status} ${response.statusText}`);
15
+ }
16
+ const data = await response.json();
17
+ return data.build_id;
18
+ }
@@ -0,0 +1,2 @@
1
+ import { TestStatus, FailedTestInfo } from "./types.js";
2
+ export declare function getTestIds(buildId: string, authString: string, status?: TestStatus): Promise<FailedTestInfo[]>;
@@ -0,0 +1,69 @@
1
+ import logger from "../../logger.js";
2
+ export async function getTestIds(buildId, authString, status) {
3
+ const baseUrl = `https://api-automation.browserstack.com/ext/v1/builds/${buildId}/testRuns`;
4
+ let url = status ? `${baseUrl}?test_statuses=${status}` : baseUrl;
5
+ let allFailedTests = [];
6
+ let requestNumber = 0;
7
+ // Construct Basic auth header
8
+ const encodedCredentials = Buffer.from(authString).toString("base64");
9
+ const authHeader = `Basic ${encodedCredentials}`;
10
+ try {
11
+ while (true) {
12
+ requestNumber++;
13
+ const response = await fetch(url, {
14
+ headers: {
15
+ Authorization: authHeader,
16
+ "Content-Type": "application/json",
17
+ },
18
+ });
19
+ if (!response.ok) {
20
+ throw new Error(`Failed to fetch test runs: ${response.status} ${response.statusText}`);
21
+ }
22
+ const data = (await response.json());
23
+ // Extract failed IDs from current page
24
+ if (data.hierarchy && data.hierarchy.length > 0) {
25
+ const currentFailedTests = extractFailedTestIds(data.hierarchy, status);
26
+ allFailedTests = allFailedTests.concat(currentFailedTests);
27
+ }
28
+ // Check for pagination termination conditions
29
+ if (!data.pagination?.has_next ||
30
+ !data.pagination.next_page ||
31
+ requestNumber >= 5) {
32
+ break;
33
+ }
34
+ const params = {
35
+ next_page: data.pagination.next_page,
36
+ };
37
+ if (status)
38
+ params.test_statuses = status;
39
+ url = `${baseUrl}?${new URLSearchParams(params).toString()}`;
40
+ }
41
+ // Return unique failed test IDs
42
+ return allFailedTests;
43
+ }
44
+ catch (error) {
45
+ logger.error("Error fetching failed tests:", error);
46
+ throw error;
47
+ }
48
+ }
49
+ // Recursive function to extract failed test IDs from hierarchy
50
+ function extractFailedTestIds(hierarchy, status) {
51
+ let failedTests = [];
52
+ for (const node of hierarchy) {
53
+ if (node.details?.status === status && node.details?.run_count) {
54
+ if (node.details?.observability_url) {
55
+ const idMatch = node.details.observability_url.match(/details=(\d+)/);
56
+ if (idMatch) {
57
+ failedTests.push({
58
+ test_id: idMatch[1],
59
+ test_name: node.display_name || `Test ${idMatch[1]}`,
60
+ });
61
+ }
62
+ }
63
+ }
64
+ if (node.children && node.children.length > 0) {
65
+ failedTests = failedTests.concat(extractFailedTestIds(node.children, status));
66
+ }
67
+ }
68
+ return failedTests;
69
+ }
@@ -0,0 +1,9 @@
1
+ import { RCAResponse } from "./types.js";
2
+ interface ScanProgressContext {
3
+ sendNotification: (notification: any) => Promise<void>;
4
+ _meta?: {
5
+ progressToken?: string | number;
6
+ };
7
+ }
8
+ export declare function getRCAData(testIds: string[], authString: string, context?: ScanProgressContext): Promise<RCAResponse>;
9
+ export {};
@@ -0,0 +1,196 @@
1
+ import { RCAState } from "./types.js";
2
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3
+ function isInProgressState(state) {
4
+ return [
5
+ RCAState.PENDING,
6
+ RCAState.FETCHING_LOGS,
7
+ RCAState.GENERATING_RCA,
8
+ RCAState.GENERATED_RCA,
9
+ ].includes(state);
10
+ }
11
+ function isFailedState(state) {
12
+ return [
13
+ RCAState.FAILED,
14
+ RCAState.LLM_SERVICE_ERROR,
15
+ RCAState.LOG_FETCH_ERROR,
16
+ RCAState.UNKNOWN_ERROR,
17
+ RCAState.TIMEOUT,
18
+ ].includes(state);
19
+ }
20
+ function calculateProgress(resolvedCount, totalCount, baseProgress = 10) {
21
+ if (totalCount === 0)
22
+ return 100; // ✅ fix divide by zero
23
+ const progressRange = 90 - baseProgress;
24
+ const completionProgress = (resolvedCount / totalCount) * progressRange;
25
+ return Math.min(100, baseProgress + completionProgress);
26
+ }
27
+ // ✅ centralized mapping function
28
+ function mapApiState(apiState) {
29
+ const state = apiState?.toLowerCase();
30
+ switch (state) {
31
+ case "completed":
32
+ return RCAState.COMPLETED;
33
+ case "pending":
34
+ return RCAState.PENDING;
35
+ case "fetching_logs":
36
+ return RCAState.FETCHING_LOGS;
37
+ case "generating_rca":
38
+ return RCAState.GENERATING_RCA;
39
+ case "generated_rca":
40
+ return RCAState.GENERATED_RCA;
41
+ case "error":
42
+ return RCAState.UNKNOWN_ERROR;
43
+ default:
44
+ return RCAState.UNKNOWN_ERROR;
45
+ }
46
+ }
47
+ async function notifyProgress(context, message, progress) {
48
+ if (!context?.sendNotification)
49
+ return;
50
+ await context.sendNotification({
51
+ method: "notifications/progress",
52
+ params: {
53
+ progressToken: context._meta?.progressToken?.toString(),
54
+ message,
55
+ progress,
56
+ total: 100,
57
+ },
58
+ });
59
+ }
60
+ async function updateProgress(context, testCases, message) {
61
+ const inProgressCases = testCases.filter((tc) => isInProgressState(tc.state));
62
+ const resolvedCount = testCases.length - inProgressCases.length;
63
+ await notifyProgress(context, message ??
64
+ `RCA analysis in progress (${resolvedCount}/${testCases.length} resolved)`, inProgressCases.length === 0
65
+ ? 100
66
+ : calculateProgress(resolvedCount, testCases.length));
67
+ }
68
+ async function fetchInitialRCA(testId, headers, baseUrl) {
69
+ const url = baseUrl.replace("{testId}", testId);
70
+ try {
71
+ const response = await fetch(url, { headers });
72
+ if (!response.ok) {
73
+ return {
74
+ id: testId,
75
+ testRunId: testId,
76
+ state: RCAState.LOG_FETCH_ERROR,
77
+ rcaData: {
78
+ error: `HTTP ${response.status}: Failed to start RCA analysis`,
79
+ },
80
+ };
81
+ }
82
+ const data = await response.json();
83
+ const resultState = mapApiState(data.state);
84
+ return {
85
+ id: testId,
86
+ testRunId: testId,
87
+ state: resultState,
88
+ ...(resultState === RCAState.COMPLETED && { rcaData: data }),
89
+ ...(isFailedState(resultState) &&
90
+ data.state && {
91
+ rcaData: {
92
+ error: `API returned state: ${data.state}`,
93
+ originalResponse: data,
94
+ },
95
+ }),
96
+ };
97
+ }
98
+ catch (error) {
99
+ return {
100
+ id: testId,
101
+ testRunId: testId,
102
+ state: RCAState.LLM_SERVICE_ERROR,
103
+ rcaData: {
104
+ error: error instanceof Error ? error.message : "Network or parsing error",
105
+ },
106
+ };
107
+ }
108
+ }
109
+ async function pollRCAResults(testCases, headers, baseUrl, context, pollInterval, timeout, initialDelay) {
110
+ const startTime = Date.now();
111
+ await delay(initialDelay);
112
+ try {
113
+ while (true) {
114
+ const inProgressCases = testCases.filter((tc) => isInProgressState(tc.state));
115
+ await updateProgress(context, testCases);
116
+ if (inProgressCases.length === 0)
117
+ break;
118
+ if (Date.now() - startTime >= timeout) {
119
+ inProgressCases.forEach((tc) => {
120
+ tc.state = RCAState.TIMEOUT;
121
+ tc.rcaData = { error: `Timeout after ${timeout}ms` };
122
+ });
123
+ await updateProgress(context, testCases, "RCA analysis timed out");
124
+ break;
125
+ }
126
+ await Promise.allSettled(inProgressCases.map(async (tc) => {
127
+ try {
128
+ const pollUrl = baseUrl.replace("{testId}", tc.id);
129
+ const response = await fetch(pollUrl, { headers });
130
+ if (!response.ok) {
131
+ const errorText = await response.text();
132
+ tc.state = RCAState.LOG_FETCH_ERROR;
133
+ tc.rcaData = {
134
+ error: `HTTP ${response.status}: Polling failed - ${errorText}`,
135
+ };
136
+ return;
137
+ }
138
+ const data = await response.json();
139
+ if (!isFailedState(tc.state)) {
140
+ const mappedState = mapApiState(data.state);
141
+ tc.state = mappedState;
142
+ if (mappedState === RCAState.COMPLETED) {
143
+ tc.rcaData = data;
144
+ }
145
+ else if (mappedState === RCAState.UNKNOWN_ERROR) {
146
+ tc.rcaData = {
147
+ error: `API returned state: ${data.state}`,
148
+ originalResponse: data,
149
+ };
150
+ }
151
+ }
152
+ }
153
+ catch (err) {
154
+ if (!isFailedState(tc.state)) {
155
+ tc.state = RCAState.LLM_SERVICE_ERROR;
156
+ tc.rcaData = {
157
+ error: err instanceof Error
158
+ ? err.message
159
+ : "Network or parsing error",
160
+ };
161
+ }
162
+ }
163
+ }));
164
+ await delay(pollInterval);
165
+ }
166
+ }
167
+ catch (err) {
168
+ testCases
169
+ .filter((tc) => isInProgressState(tc.state))
170
+ .forEach((tc) => {
171
+ tc.state = RCAState.UNKNOWN_ERROR;
172
+ tc.rcaData = {
173
+ error: err instanceof Error ? err.message : "Unexpected error",
174
+ };
175
+ });
176
+ await updateProgress(context, testCases, "RCA analysis failed due to unexpected error");
177
+ }
178
+ return { testCases };
179
+ }
180
+ export async function getRCAData(testIds, authString, context) {
181
+ const pollInterval = 5000;
182
+ const timeout = 40000;
183
+ const initialDelay = 20000;
184
+ const baseUrl = "https://api-observability.browserstack.com/ext/v1/testRun/{testId}/testRca";
185
+ const headers = {
186
+ Authorization: `Basic ${Buffer.from(authString).toString("base64")}`,
187
+ "Content-Type": "application/json",
188
+ };
189
+ await notifyProgress(context, "Starting RCA analysis for test cases...", 0);
190
+ const testCases = await Promise.all(testIds.map((testId) => fetchInitialRCA(testId, headers, baseUrl)));
191
+ const inProgressCount = testCases.filter((tc) => isInProgressState(tc.state)).length;
192
+ await notifyProgress(context, `Initial RCA requests completed. ${inProgressCount} cases pending analysis...`, 10);
193
+ if (inProgressCount === 0)
194
+ return { testCases };
195
+ return await pollRCAResults(testCases, headers, baseUrl, context, pollInterval, timeout, initialDelay);
196
+ }
@@ -0,0 +1,48 @@
1
+ export declare enum TestStatus {
2
+ PASSED = "passed",
3
+ FAILED = "failed",
4
+ PENDING = "pending",
5
+ SKIPPED = "skipped"
6
+ }
7
+ export interface TestDetails {
8
+ status: TestStatus;
9
+ details: any;
10
+ children?: TestDetails[];
11
+ display_name?: string;
12
+ }
13
+ export interface TestRun {
14
+ hierarchy: TestDetails[];
15
+ pagination?: {
16
+ has_next: boolean;
17
+ next_page: string | null;
18
+ };
19
+ }
20
+ export interface FailedTestInfo {
21
+ test_id: string;
22
+ test_name: string;
23
+ }
24
+ export declare enum RCAState {
25
+ PENDING = "pending",
26
+ FETCHING_LOGS = "fetching_logs",
27
+ GENERATING_RCA = "generating_rca",
28
+ GENERATED_RCA = "generated_rca",
29
+ COMPLETED = "completed",
30
+ FAILED = "failed",
31
+ LLM_SERVICE_ERROR = "LLM_SERVICE_ERROR",
32
+ LOG_FETCH_ERROR = "LOG_FETCH_ERROR",
33
+ UNKNOWN_ERROR = "UNKNOWN_ERROR",
34
+ TIMEOUT = "TIMEOUT"
35
+ }
36
+ export interface RCATestCase {
37
+ id: string;
38
+ testRunId: string;
39
+ state: RCAState;
40
+ rcaData?: any;
41
+ }
42
+ export interface RCAResponse {
43
+ testCases: RCATestCase[];
44
+ }
45
+ export interface BuildIdArgs {
46
+ browserStackProjectName: string;
47
+ browserStackBuildName: string;
48
+ }
@@ -0,0 +1,20 @@
1
+ export var TestStatus;
2
+ (function (TestStatus) {
3
+ TestStatus["PASSED"] = "passed";
4
+ TestStatus["FAILED"] = "failed";
5
+ TestStatus["PENDING"] = "pending";
6
+ TestStatus["SKIPPED"] = "skipped";
7
+ })(TestStatus || (TestStatus = {}));
8
+ export var RCAState;
9
+ (function (RCAState) {
10
+ RCAState["PENDING"] = "pending";
11
+ RCAState["FETCHING_LOGS"] = "fetching_logs";
12
+ RCAState["GENERATING_RCA"] = "generating_rca";
13
+ RCAState["GENERATED_RCA"] = "generated_rca";
14
+ RCAState["COMPLETED"] = "completed";
15
+ RCAState["FAILED"] = "failed";
16
+ RCAState["LLM_SERVICE_ERROR"] = "LLM_SERVICE_ERROR";
17
+ RCAState["LOG_FETCH_ERROR"] = "LOG_FETCH_ERROR";
18
+ RCAState["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
19
+ RCAState["TIMEOUT"] = "TIMEOUT";
20
+ })(RCAState || (RCAState = {}));