@browserstack/mcp-server 2.2.6-beta.2 → 2.2.6

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 (37) hide show
  1. package/dist/server-factory.js +1 -6
  2. package/dist/tools/accessibility.js +69 -680
  3. package/dist/tools/accessiblity-utils/accessibility-rag.d.ts +12 -0
  4. package/dist/tools/accessiblity-utils/accessibility-rag.js +59 -0
  5. package/dist/tools/accessiblity-utils/report-fetcher.d.ts +7 -116
  6. package/dist/tools/accessiblity-utils/report-fetcher.js +29 -268
  7. package/dist/tools/accessiblity-utils/report-parser.d.ts +1 -41
  8. package/dist/tools/accessiblity-utils/report-parser.js +43 -142
  9. package/dist/tools/accessiblity-utils/scanner.d.ts +1 -15
  10. package/dist/tools/accessiblity-utils/scanner.js +4 -3
  11. package/dist/tools/appautomate-utils/appium-sdk/config-generator.d.ts +0 -2
  12. package/dist/tools/appautomate-utils/appium-sdk/config-generator.js +5 -15
  13. package/dist/tools/appautomate-utils/appium-sdk/constants.d.ts +1 -2
  14. package/dist/tools/appautomate-utils/appium-sdk/constants.js +1 -6
  15. package/dist/tools/appautomate-utils/appium-sdk/handler.js +1 -12
  16. package/dist/tools/appautomate-utils/appium-sdk/instructions.d.ts +1 -3
  17. package/dist/tools/appautomate-utils/appium-sdk/instructions.js +2 -2
  18. package/dist/tools/appautomate-utils/appium-sdk/languages/nodejs.d.ts +1 -3
  19. package/dist/tools/appautomate-utils/appium-sdk/languages/nodejs.js +6 -19
  20. package/dist/tools/appautomate-utils/native-execution/appautomate.d.ts +2 -7
  21. package/dist/tools/appautomate-utils/native-execution/appautomate.js +12 -15
  22. package/dist/tools/appautomate-utils/native-execution/constants.d.ts +1 -2
  23. package/dist/tools/appautomate-utils/native-execution/constants.js +1 -6
  24. package/dist/tools/appautomate.js +16 -29
  25. package/dist/tools/testmanagement-utils/create-testrun.d.ts +1 -1
  26. package/dist/tools/testmanagement-utils/update-testrun.d.ts +1 -1
  27. package/dist/tools/tool-handoff.d.ts +2 -3
  28. package/dist/tools/tool-handoff.js +2 -3
  29. package/package.json +1 -1
  30. package/dist/lib/accessibility-routing.d.ts +0 -6
  31. package/dist/lib/accessibility-routing.js +0 -6
  32. package/dist/tools/accessiblity-utils/app-report-fetcher.d.ts +0 -412
  33. package/dist/tools/accessiblity-utils/app-report-fetcher.js +0 -926
  34. package/dist/tools/accessiblity-utils/paginate.d.ts +0 -27
  35. package/dist/tools/accessiblity-utils/paginate.js +0 -50
  36. package/dist/tools/appautomate-utils/appium-sdk/languages/accessibility.d.ts +0 -4
  37. package/dist/tools/appautomate-utils/appium-sdk/languages/accessibility.js +0 -47
@@ -0,0 +1,12 @@
1
+ export interface RAGChunk {
2
+ url: string;
3
+ content: string;
4
+ }
5
+ import { BrowserStackConfig } from "../../lib/types.js";
6
+ export interface AccessibilityRAGResponse {
7
+ content: Array<{
8
+ type: "text";
9
+ text: string;
10
+ }>;
11
+ }
12
+ export declare function queryAccessibilityRAG(userQuery: string, config: BrowserStackConfig): Promise<any>;
@@ -0,0 +1,59 @@
1
+ import { apiClient } from "../../lib/apiClient.js";
2
+ import { wrapUntrusted } from "../../lib/untrusted-content.js";
3
+ import { getBrowserStackAuth } from "../../lib/get-auth.js";
4
+ export async function queryAccessibilityRAG(userQuery, config) {
5
+ const url = "https://accessibility.browserstack.com/api/tcg-proxy/search";
6
+ const authString = getBrowserStackAuth(config);
7
+ const auth = Buffer.from(authString).toString("base64");
8
+ const response = await apiClient.post({
9
+ url,
10
+ headers: {
11
+ "Content-Type": "application/json",
12
+ Authorization: `Basic ${auth}`,
13
+ },
14
+ body: {
15
+ query: userQuery,
16
+ },
17
+ raise_error: false,
18
+ });
19
+ if (!response.ok) {
20
+ const errorText = typeof response.data === "string"
21
+ ? response.data
22
+ : JSON.stringify(response.data);
23
+ throw new Error(`RAG endpoint error: ${response.status} ${errorText}`);
24
+ }
25
+ const responseData = response.data;
26
+ if (!responseData.success) {
27
+ throw new Error("Something went wrong: " + responseData.message);
28
+ }
29
+ // Parse the stringified JSON data
30
+ let parsedData;
31
+ try {
32
+ parsedData = JSON.parse(responseData.data);
33
+ }
34
+ catch (err) {
35
+ throw new Error("Failed to parse RAG response data as JSON: " +
36
+ (err instanceof Error ? err.message : String(err)));
37
+ }
38
+ if (!parsedData ||
39
+ !parsedData.data ||
40
+ !Array.isArray(parsedData.data.chunks)) {
41
+ throw new Error("RAG response data is missing expected 'data.chunks' array");
42
+ }
43
+ const chunks = parsedData.data.chunks;
44
+ // Format the response properly
45
+ const instruction = "IMPORTANT: Use ONLY the data provided below to answer the user's accessibility question. Do not use any external knowledge. When answering, you MUST include the relevant BrowserStack documentation links provided in the sources for personalization and further reference.\n\n";
46
+ const formattedChunks = chunks
47
+ .map((chunk, index) => `${index + 1}: Source: ${chunk.url}\n\n${chunk.content}`)
48
+ .join("\n\n---\n\n");
49
+ const formattedResponse = instruction +
50
+ wrapUntrusted("BrowserStack accessibility documentation", formattedChunks);
51
+ return {
52
+ content: [
53
+ {
54
+ type: "text",
55
+ text: formattedResponse,
56
+ },
57
+ ],
58
+ };
59
+ }
@@ -1,9 +1,4 @@
1
- import { type AppBuildIssues, type AppBuildRef, type AppBuildReport, type AppBuildRow, type AppManualReport, type AppManualReportRow, type AppProjectRow } from "./app-report-fetcher.js";
2
- export { MAX_APP_BUILD_LIST_PAGES } from "./app-report-fetcher.js";
3
- /**
4
- * What to read issues from: a Website Scanner run, an Automated Tests build, a Workflow
5
- * Analyzer report, or an App Accessibility build (a mobile app run through App Automate).
6
- */
1
+ /** What to read issues from: a Website Scanner run, an Automated Tests build or a Workflow Analyzer report. */
7
2
  export type IssueSource = {
8
3
  kind: "scan";
9
4
  scanId: string;
@@ -14,12 +9,6 @@ export type IssueSource = {
14
9
  } | {
15
10
  kind: "workflow";
16
11
  reportId: string;
17
- } | {
18
- kind: "appBuild";
19
- buildId: string;
20
- } | {
21
- kind: "appReport";
22
- reportId: string;
23
12
  };
24
13
  /**
25
14
  * How far a report link's scan is searched for. The scan list has no name or link filter and
@@ -28,46 +17,8 @@ export type IssueSource = {
28
17
  * at limit=50 or 100 the cursor skips scans, so a larger page cannot be used to go faster.
29
18
  */
30
19
  export declare const MAX_SCAN_LIST_PAGES = 15;
31
- /**
32
- * How long one call waits on a CSV export. Export time grows with the report's rows: a few
33
- * seconds for hundreds, ~17 s for 18k rows, over 2 minutes for 30k issues. A call stays well
34
- * under the 60 s request timeout MCP clients default to; an export still running after this
35
- * is handed back to be continued by the next call rather than started again.
36
- */
37
- export declare const EXPORT_WAIT_MS = 45000;
38
- /**
39
- * Exports per report, kept for the life of the server: the finished link to reuse for later
40
- * pages, or the task of an export still running, to continue instead of starting another.
41
- * Keyed by the account and the report, so one account never sees another's.
42
- */
43
- export declare const reportExports: Map<string, {
44
- link?: string;
45
- expiresAt?: number;
46
- taskId?: string;
47
- }>;
48
- /** An export still being generated when a call stopped waiting on it. */
49
- export declare class ExportPendingError extends Error {
50
- readonly taskId: string;
51
- readonly waitedMs: number;
52
- constructor(taskId: string, waitedMs: number);
53
- }
54
- /** A run in a scan's history, newest first in the API's list. */
55
- export interface ScanRun {
56
- id: number;
57
- scanDate: string;
58
- issues?: number;
59
- }
60
- /** What the dashboard counts between a run and the run before it. */
61
- export interface RunChanges {
62
- newIssues: number;
63
- retainedIssues: number;
64
- resolvedIssues: number;
65
- }
66
20
  export declare class AccessibilityReportFetcher {
67
21
  private auth;
68
- private readonly app;
69
- /** Web builds whose overview was read, by id, so a status check need not read it again. */
70
- private readonly webBuildProjects;
71
22
  setAuth(auth: {
72
23
  username: string;
73
24
  password: string;
@@ -75,78 +26,18 @@ export declare class AccessibilityReportFetcher {
75
26
  private authHeaders;
76
27
  /**
77
28
  * The source a dashboard report link points at. An Automated Tests link carries the build
78
- * id itself. An Automate build or session link is mapped to that build through a session
79
- * (see buildOfAutomateSession). A Website Scanner link carries the thBuildId of one run,
80
- * which only the scan list can map to ids: each scan there carries its latest run's link.
81
- * A link to an older run therefore matches no scan, and is reported as such rather than
82
- * guessed at. A scanner share link carries the dashboard link it shows. An App
83
- * Accessibility link names its build by project, name and number, which the project's
84
- * build list maps to the id. An App Automate build or session link maps to the
85
- * accessibility build through a session's test code.
29
+ * id itself. A Website Scanner link carries the thBuildId of one run, which only the scan
30
+ * list can map to ids: each scan there carries its latest run's link. A link to an older
31
+ * run therefore matches no scan, and is reported as such rather than guessed at.
86
32
  */
87
33
  sourceFromReportUrl(reportUrl: string): Promise<IssueSource>;
88
- /**
89
- * A build id names a web build or a mobile app build: both are TestHub ids, and only the
90
- * API that ran the build knows it. The web API is asked first; a build it does not know
91
- * that the App Accessibility API does is an app build. One neither knows is refused
92
- * here, naming both.
93
- */
94
- resolveBuild(source: IssueSource): Promise<IssueSource>;
95
- /** An app build's issues, read inline from its scanned screens. */
96
- appBuildReport(buildId: string, cursor?: number, maxCharacterLength?: number): Promise<AppBuildReport>;
97
- /** Every issue of an app build at once, to compare it with another. */
98
- appBuildIssues(buildId: string): Promise<AppBuildIssues>;
99
- /** The build before an app build in its project, if it has one. */
100
- previousAppBuild(buildId: string): Promise<AppBuildRef | undefined>;
101
- /** A manual (Workflow Analyzer) app report's issues, read inline from its screens. */
102
- appReportIssues(reportId: string, cursor?: number, maxCharacterLength?: number): Promise<AppManualReport>;
103
- listAppProjects(limit: number): Promise<{
104
- projects: AppProjectRow[];
105
- total: number;
106
- }>;
107
- listAppBuilds(projectName: string, buildName: string | undefined, limit: number): Promise<{
108
- project: {
109
- id: number;
110
- name: string;
111
- };
112
- total: number;
113
- builds: AppBuildRow[];
114
- }>;
115
- listAppManualReports(name: string | undefined, limit: number): Promise<{
116
- total: number;
117
- reports: AppManualReportRow[];
118
- }>;
119
- /** Any one session of an Automate build; every session of an SDK run names the same build. */
120
- private anyAutomateSession;
121
- /**
122
- * The Accessibility build an Automate session ran in. A session run through the BrowserStack
123
- * SDK belongs to the SDK's build, whose id is also the Accessibility build id, and the
124
- * session's test-code record names it (build_uuid). A session run without the SDK has no
125
- * such record, and an SDK run without accessibility testing has a build the Accessibility
126
- * API does not know; both are said plainly instead of surfacing later as a bare 404.
127
- */
128
- private buildOfAutomateSession;
129
34
  /** Walk the newest scans for the one whose own link carries this thBuildId. */
130
35
  private findScanRun;
131
- /**
132
- * The CSV link of a report's issues. A finished export is reused until its link expires.
133
- * Otherwise an export is started, or continued (`exportTaskId`, or the one an earlier call
134
- * left running), and waited on for up to EXPORT_WAIT_MS; one still running then is handed
135
- * back as an ExportPendingError carrying its task id.
136
- */
137
- getReportLink(source: IssueSource, options?: {
138
- exportTaskId?: string;
139
- onWait?: (waitedMs: number) => Promise<void>;
140
- }): Promise<string>;
141
- /** The run before `scanRunId` in its scan's history, or undefined for the first run. */
142
- previousScanRun(scanId: string, scanRunId: string): Promise<ScanRun | undefined>;
143
- /** The dashboard's new / retained / resolved counts against the run before this one. */
144
- changesSinceLastRun(scanId: string, scanRunId: string): Promise<RunChanges | undefined>;
36
+ getReportLink(source: IssueSource): Promise<string>;
145
37
  /**
146
38
  * Where the report is in its processing, as the report's own status reads it: `completed`,
147
- * or `queued` / `running` / `processing` / `finalizing` while issues are still being added
148
- * (an app build reads `running` / `rule_processing` until its rules have run). Undefined
149
- * when it cannot be read. A web build's overview carries no status, so a build's comes
39
+ * or `queued` / `running` / `processing` / `finalizing` while issues are still being added.
40
+ * Undefined when it cannot be read. A build's overview carries no status, so a build's comes
150
41
  * from its project's build list (newest first), found by id.
151
42
  */
152
43
  reportStatus(source: IssueSource): Promise<string | undefined>;
@@ -1,9 +1,4 @@
1
1
  import { apiClient } from "../../lib/apiClient.js";
2
- import { AppAccessibilityApi, manualReportIdFromDashboardLink, manualReportStatus, } from "./app-report-fetcher.js";
3
- export { MAX_APP_BUILD_LIST_PAGES } from "./app-report-fetcher.js";
4
- import { SessionType } from "../../lib/constants.js";
5
- import { sessionsListUrl } from "../automate-utils/list-session-ids.js";
6
- import { getAutomationBaseUrl } from "../rca-agent-utils/constants.js";
7
2
  const API_BASE = "https://api-accessibility.browserstack.com/api";
8
3
  /**
9
4
  * How far a report link's scan is searched for. The scan list has no name or link filter and
@@ -12,50 +7,8 @@ const API_BASE = "https://api-accessibility.browserstack.com/api";
12
7
  * at limit=50 or 100 the cursor skips scans, so a larger page cannot be used to go faster.
13
8
  */
14
9
  export const MAX_SCAN_LIST_PAGES = 15;
15
- /** How many pages of a scan's runs are read to find the run before a given one. */
16
- const MAX_SCAN_RUN_PAGES = 10;
17
10
  /** A build whose status is checked is one just run, so it sits at the top of its project's list. */
18
11
  const MAX_BUILD_LIST_PAGES = 3;
19
- /**
20
- * How long one call waits on a CSV export. Export time grows with the report's rows: a few
21
- * seconds for hundreds, ~17 s for 18k rows, over 2 minutes for 30k issues. A call stays well
22
- * under the 60 s request timeout MCP clients default to; an export still running after this
23
- * is handed back to be continued by the next call rather than started again.
24
- */
25
- export const EXPORT_WAIT_MS = 45_000;
26
- const EXPORT_POLL_MS = [1000, 2000, 3000, 5000];
27
- /** A finished export's link is reused until shortly before it expires (about 12 hours). */
28
- const LINK_EXPIRY_MARGIN_MS = 10 * 60 * 1000;
29
- const LINK_FALLBACK_TTL_MS = 60 * 60 * 1000;
30
- /**
31
- * Exports per report, kept for the life of the server: the finished link to reuse for later
32
- * pages, or the task of an export still running, to continue instead of starting another.
33
- * Keyed by the account and the report, so one account never sees another's.
34
- */
35
- export const reportExports = new Map();
36
- /** An export still being generated when a call stopped waiting on it. */
37
- export class ExportPendingError extends Error {
38
- taskId;
39
- waitedMs;
40
- constructor(taskId, waitedMs) {
41
- super(`The report's CSV export is still being generated after ${Math.round(waitedMs / 1000)}s (export task ${taskId}).`);
42
- this.taskId = taskId;
43
- this.waitedMs = waitedMs;
44
- this.name = "ExportPendingError";
45
- }
46
- }
47
- /** When a presigned export link stops working, from its Expires parameter. */
48
- function linkExpiry(link) {
49
- try {
50
- const expires = Number(new URL(link).searchParams.get("Expires"));
51
- if (expires > 0)
52
- return expires * 1000 - LINK_EXPIRY_MARGIN_MS;
53
- }
54
- catch {
55
- // Not a URL we can read; fall back below.
56
- }
57
- return Date.now() + LINK_FALLBACK_TTL_MS;
58
- }
59
12
  /** The thBuildId a dashboard report link carries, if any. */
60
13
  function thBuildIdOf(link) {
61
14
  try {
@@ -94,9 +47,6 @@ function exportUrls(source) {
94
47
  poll: (taskId) => `${route}?taskId=${q(taskId)}`,
95
48
  };
96
49
  }
97
- case "appBuild":
98
- case "appReport":
99
- throw new Error("An App Accessibility build or report has no CSV export; its issues are read inline");
100
50
  }
101
51
  }
102
52
  /** Why an export did not start, in the API's words, and where a build's id comes from when it is not found. */
@@ -106,24 +56,12 @@ function startFailure(source, status, body) {
106
56
  `HTTP ${status}`;
107
57
  // The API takes only its own build id; the Automate build id is a 404 "Build not found".
108
58
  const hint = status === 404 && source.kind === "build"
109
- ? ". Pass the Accessibility build id: the thBuildId in the build's accessibility report link (40 lowercase letters and digits), not the Automate build id or the build name. For an Automate build or session, pass its Automate dashboard link as reportUrl"
59
+ ? ". Pass the Accessibility build id: the thBuildId in the build's accessibility report link (40 lowercase letters and digits), not the Automate build id or the build name"
110
60
  : "";
111
61
  return `Failed to initiate report: ${reason}${hint}`;
112
62
  }
113
- /** The build and session ids an Automate dashboard link carries, if it is one. */
114
- function automateIds(url) {
115
- if (url.hostname !== "automate.browserstack.com")
116
- return undefined;
117
- return {
118
- buildId: url.pathname.match(/\/builds\/([0-9a-f]{40})(?:\/|$)/i)?.[1],
119
- sessionId: url.pathname.match(/\/sessions\/([0-9a-f]{40})(?:\/|$)/i)?.[1],
120
- };
121
- }
122
63
  export class AccessibilityReportFetcher {
123
64
  auth;
124
- app = new AppAccessibilityApi(() => this.authHeaders());
125
- /** Web builds whose overview was read, by id, so a status check need not read it again. */
126
- webBuildProjects = new Map();
127
65
  setAuth(auth) {
128
66
  this.auth = auth;
129
67
  }
@@ -137,14 +75,9 @@ export class AccessibilityReportFetcher {
137
75
  }
138
76
  /**
139
77
  * The source a dashboard report link points at. An Automated Tests link carries the build
140
- * id itself. An Automate build or session link is mapped to that build through a session
141
- * (see buildOfAutomateSession). A Website Scanner link carries the thBuildId of one run,
142
- * which only the scan list can map to ids: each scan there carries its latest run's link.
143
- * A link to an older run therefore matches no scan, and is reported as such rather than
144
- * guessed at. A scanner share link carries the dashboard link it shows. An App
145
- * Accessibility link names its build by project, name and number, which the project's
146
- * build list maps to the id. An App Automate build or session link maps to the
147
- * accessibility build through a session's test code.
78
+ * id itself. A Website Scanner link carries the thBuildId of one run, which only the scan
79
+ * list can map to ids: each scan there carries its latest run's link. A link to an older
80
+ * run therefore matches no scan, and is reported as such rather than guessed at.
148
81
  */
149
82
  async sourceFromReportUrl(reportUrl) {
150
83
  let url;
@@ -154,26 +87,6 @@ export class AccessibilityReportFetcher {
154
87
  catch {
155
88
  url = undefined;
156
89
  }
157
- // A scanner share link (scanner.browserstack.com/.../a11y-report) carries the dashboard
158
- // link it shows as its reportUrl parameter.
159
- const shared = url?.searchParams.get("reportUrl");
160
- if (shared && shared !== reportUrl)
161
- return this.sourceFromReportUrl(shared);
162
- const manualReportId = url && manualReportIdFromDashboardLink(url);
163
- if (manualReportId)
164
- return { kind: "appReport", reportId: manualReportId };
165
- const appBuildId = url &&
166
- ((await this.app.buildIdFromDashboardLink(url)) ??
167
- (await this.app.buildIdFromAppAutomateLink(url)));
168
- if (appBuildId)
169
- return { kind: "appBuild", buildId: appBuildId };
170
- const automate = url && automateIds(url);
171
- if (automate?.sessionId) {
172
- return this.buildOfAutomateSession(automate.sessionId);
173
- }
174
- if (automate?.buildId) {
175
- return this.buildOfAutomateSession(await this.anyAutomateSession(automate.buildId));
176
- }
177
90
  const thBuildId = url?.searchParams.get("thBuildId");
178
91
  if (url && thBuildId && url.pathname.includes("/automated-tests/")) {
179
92
  return { kind: "build", buildId: thBuildId };
@@ -182,107 +95,7 @@ export class AccessibilityReportFetcher {
182
95
  if (thBuildId && scanName) {
183
96
  return this.findScanRun(thBuildId, decodeURIComponent(scanName));
184
97
  }
185
- throw new Error("This link does not name a report the API can look up. Pass an Automated Tests or Website Scanner report link from the Accessibility dashboard (both carry thBuildId) or a scanner share link, an Automate build or session link, an App Accessibility build link (automated-tests/projects/…/builds/…), an App Automate build or session link, an App Accessibility manual report link (report/…), or the ids: scanId with scanRunId, buildId, workflowReportId, or appReportId");
186
- }
187
- /**
188
- * A build id names a web build or a mobile app build: both are TestHub ids, and only the
189
- * API that ran the build knows it. The web API is asked first; a build it does not know
190
- * that the App Accessibility API does is an app build. One neither knows is refused
191
- * here, naming both.
192
- */
193
- async resolveBuild(source) {
194
- if (source.kind !== "build")
195
- return source;
196
- const web = await this.getData(`${API_BASE}/automated-tests/v1/builds/${encodeURIComponent(source.buildId)}`);
197
- if (web) {
198
- if (web.projectName) {
199
- this.webBuildProjects.set(source.buildId, web.projectName);
200
- }
201
- return source;
202
- }
203
- const meta = await this.app.buildMeta(source.buildId);
204
- if (meta)
205
- return { kind: "appBuild", buildId: source.buildId };
206
- throw new Error(`No Automated Tests (web) or App Accessibility build with id "${source.buildId}". Pass the id the BrowserStack SDK printed as "testhub id" at the end of the run (40 lowercase letters and digits), or the report's dashboard link as reportUrl`);
207
- }
208
- /** An app build's issues, read inline from its scanned screens. */
209
- appBuildReport(buildId, cursor = 0, maxCharacterLength) {
210
- return this.app.report(buildId, cursor, maxCharacterLength);
211
- }
212
- /** Every issue of an app build at once, to compare it with another. */
213
- appBuildIssues(buildId) {
214
- return this.app.buildIssues(buildId);
215
- }
216
- /** The build before an app build in its project, if it has one. */
217
- previousAppBuild(buildId) {
218
- return this.app.previousBuild(buildId);
219
- }
220
- /** A manual (Workflow Analyzer) app report's issues, read inline from its screens. */
221
- appReportIssues(reportId, cursor = 0, maxCharacterLength) {
222
- return this.app.appReportIssues(reportId, cursor, maxCharacterLength);
223
- }
224
- listAppProjects(limit) {
225
- return this.app.listAppProjects(limit);
226
- }
227
- listAppBuilds(projectName, buildName, limit) {
228
- return this.app.listAppBuilds(projectName, buildName, limit);
229
- }
230
- listAppManualReports(name, limit) {
231
- return this.app.listAppManualReports(name, limit);
232
- }
233
- /** Any one session of an Automate build; every session of an SDK run names the same build. */
234
- async anyAutomateSession(automateBuildId) {
235
- const resp = await apiClient.get({
236
- url: `${sessionsListUrl(SessionType.Automate, automateBuildId)}?limit=1`,
237
- headers: this.authHeaders(),
238
- raise_error: false,
239
- });
240
- if (resp.status === 404) {
241
- throw new Error(`No Automate build with id ${automateBuildId}`);
242
- }
243
- if (!resp.ok) {
244
- throw new Error(`Could not list the sessions of Automate build ${automateBuildId}: HTTP ${resp.status}`);
245
- }
246
- const sessionId = resp.data?.[0]?.automation_session?.hashed_id;
247
- if (!sessionId) {
248
- throw new Error(`Automate build ${automateBuildId} has no sessions`);
249
- }
250
- return sessionId;
251
- }
252
- /**
253
- * The Accessibility build an Automate session ran in. A session run through the BrowserStack
254
- * SDK belongs to the SDK's build, whose id is also the Accessibility build id, and the
255
- * session's test-code record names it (build_uuid). A session run without the SDK has no
256
- * such record, and an SDK run without accessibility testing has a build the Accessibility
257
- * API does not know; both are said plainly instead of surfacing later as a bare 404.
258
- */
259
- async buildOfAutomateSession(sessionId) {
260
- const resp = await apiClient.get({
261
- url: `${getAutomationBaseUrl()}/ext/v1/sessions/${encodeURIComponent(sessionId)}/testCode`,
262
- headers: this.authHeaders(),
263
- raise_error: false,
264
- });
265
- if (!resp.ok && resp.status !== 404) {
266
- throw new Error(`Could not read Automate session ${sessionId}: HTTP ${resp.status}`);
267
- }
268
- const buildId = resp.ok
269
- ? resp.data?.build_uuid
270
- : undefined;
271
- if (!buildId) {
272
- throw new Error(`Automate session ${sessionId} is not linked to a BrowserStack SDK build (it was not run through the SDK, or belongs to another account), so it has no accessibility results`);
273
- }
274
- const build = await apiClient.get({
275
- url: `${API_BASE}/automated-tests/v1/builds/${encodeURIComponent(buildId)}`,
276
- headers: this.authHeaders(),
277
- raise_error: false,
278
- });
279
- if (build.status === 404) {
280
- throw new Error(`This Automate run belongs to build ${buildId}, which has no accessibility results: accessibility testing was not on for it`);
281
- }
282
- if (!build.ok) {
283
- throw new Error(`Could not read accessibility build ${buildId} of this Automate run: HTTP ${build.status}`);
284
- }
285
- return { kind: "build", buildId };
98
+ throw new Error("This link does not name a report the API can look up. Pass an Automated Tests or Website Scanner report link from the Accessibility dashboard (both carry thBuildId), or the ids: scanId with scanRunId, buildId, or workflowReportId");
286
99
  }
287
100
  /** Walk the newest scans for the one whose own link carries this thBuildId. */
288
101
  async findScanRun(thBuildId, scanName) {
@@ -317,7 +130,7 @@ export class AccessibilityReportFetcher {
317
130
  nextPage = data.pagination.nextPage;
318
131
  }
319
132
  if (sameName !== undefined) {
320
- throw new Error(`This link is to an earlier run of scan "${scanName}" (scanId ${sameName}). A link resolves only to a scan's latest run; ask the user for that run's scanRunId and pass it with scanId ${sameName}, or read the latest run with compareWith: "previous" to compare it with the run before it`);
133
+ throw new Error(`This link is to an earlier run of scan "${scanName}" (scanId ${sameName}). A link resolves only to a scan's latest run; ask the user for that run's scanRunId and pass it with scanId ${sameName}`);
321
134
  }
322
135
  // nextPage is still set only when the walk stopped at the page cap, not at the end.
323
136
  const searched = nextPage
@@ -325,89 +138,44 @@ export class AccessibilityReportFetcher {
325
138
  : "";
326
139
  throw new Error(`No Website Scanner scan${searched} matches this link. Pass scanId and scanRunId instead`);
327
140
  }
328
- /**
329
- * The CSV link of a report's issues. A finished export is reused until its link expires.
330
- * Otherwise an export is started, or continued (`exportTaskId`, or the one an earlier call
331
- * left running), and waited on for up to EXPORT_WAIT_MS; one still running then is handed
332
- * back as an ExportPendingError carrying its task id.
333
- */
334
- async getReportLink(source, options = {}) {
335
- const key = `${this.auth?.username ?? ""}|${JSON.stringify(source)}`;
336
- const known = reportExports.get(key);
337
- if (known?.link && (known.expiresAt ?? 0) > Date.now())
338
- return known.link;
141
+ async getReportLink(source) {
339
142
  const urls = exportUrls(source);
340
- let taskId = options.exportTaskId ?? known?.taskId;
341
- if (!taskId) {
342
- const initResp = await apiClient.get({
343
- url: urls.start,
344
- headers: this.authHeaders(),
345
- raise_error: false,
346
- });
347
- const initData = initResp.data;
348
- if (!initResp.ok || !initData?.success) {
349
- throw new Error(startFailure(source, initResp.status, initData));
350
- }
351
- taskId = initData.data.task_id;
352
- }
353
- reportExports.set(key, { taskId });
354
- const started = Date.now();
355
- for (let attempt = 0;; attempt++) {
143
+ // Initiate CSV link generation
144
+ const initResp = await apiClient.get({
145
+ url: urls.start,
146
+ headers: this.authHeaders(),
147
+ raise_error: false,
148
+ });
149
+ const initData = initResp.data;
150
+ if (!initResp.ok || !initData?.success) {
151
+ throw new Error(startFailure(source, initResp.status, initData));
152
+ }
153
+ const taskId = initData.data.task_id;
154
+ // Poll for the generated CSV link (task is async, may take a few seconds)
155
+ const reportUrl = urls.poll(taskId);
156
+ const maxAttempts = 3;
157
+ const pollIntervalMs = 2000;
158
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
356
159
  const reportResp = await apiClient.get({
357
- url: urls.poll(taskId),
160
+ url: reportUrl,
358
161
  headers: this.authHeaders(),
359
162
  });
360
163
  const reportData = reportResp.data;
361
164
  if (!reportData.success) {
362
- reportExports.delete(key);
363
165
  throw new Error(`Failed to fetch report: ${reportData.error}`);
364
166
  }
365
167
  const link = reportData.data?.reportLink;
366
168
  if (link) {
367
- reportExports.set(key, { link, expiresAt: linkExpiry(link) });
368
169
  return link;
369
170
  }
370
- const message = reportData.data?.message ?? "";
371
- if (/generation failed/i.test(message)) {
372
- reportExports.delete(key);
373
- throw new Error(`The report's CSV export failed: ${message}`);
374
- }
375
- const waited = Date.now() - started;
376
- if (waited >= EXPORT_WAIT_MS)
377
- throw new ExportPendingError(taskId, waited);
378
- await options.onWait?.(waited);
379
- const pause = EXPORT_POLL_MS[Math.min(attempt, EXPORT_POLL_MS.length - 1)];
380
- await new Promise((resolve) => setTimeout(resolve, pause));
171
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
381
172
  }
382
- }
383
- /** The run before `scanRunId` in its scan's history, or undefined for the first run. */
384
- async previousScanRun(scanId, scanRunId) {
385
- // The runs come a page at a time; the one before may be on a later page.
386
- const runs = [];
387
- const base = `${API_BASE}/website-scanner/v1/scans/${encodeURIComponent(scanId)}/scan_runs`;
388
- let nextPage;
389
- for (let page = 0; page < MAX_SCAN_RUN_PAGES; page++) {
390
- const data = await this.getData(nextPage ? `${base}?nextPage=${encodeURIComponent(nextPage)}` : base);
391
- runs.push(...(data?.scanRuns ?? []));
392
- if (!data?.pagination?.hasNext || !data.pagination.nextPage)
393
- break;
394
- nextPage = data.pagination.nextPage;
395
- }
396
- const newestFirst = [...runs].sort((a, b) => b.scanDate.localeCompare(a.scanDate));
397
- const at = newestFirst.findIndex((run) => String(run.id) === scanRunId);
398
- return at >= 0 ? newestFirst[at + 1] : undefined;
399
- }
400
- /** The dashboard's new / retained / resolved counts against the run before this one. */
401
- async changesSinceLastRun(scanId, scanRunId) {
402
- const q = encodeURIComponent;
403
- const changes = (await this.getData(`${API_BASE}/website-scanner/v1/scans/${q(scanId)}/scan_runs/${q(scanRunId)}`))?.changesSinceLastRun;
404
- return changes ?? undefined;
173
+ throw new Error(`Report link was not available after ${maxAttempts} attempts. The CSV generation may still be in progress.`);
405
174
  }
406
175
  /**
407
176
  * Where the report is in its processing, as the report's own status reads it: `completed`,
408
- * or `queued` / `running` / `processing` / `finalizing` while issues are still being added
409
- * (an app build reads `running` / `rule_processing` until its rules have run). Undefined
410
- * when it cannot be read. A web build's overview carries no status, so a build's comes
177
+ * or `queued` / `running` / `processing` / `finalizing` while issues are still being added.
178
+ * Undefined when it cannot be read. A build's overview carries no status, so a build's comes
411
179
  * from its project's build list (newest first), found by id.
412
180
  */
413
181
  async reportStatus(source) {
@@ -420,12 +188,6 @@ export class AccessibilityReportFetcher {
420
188
  return (await this.getData(`${API_BASE}/workflow-analyzer/v1/reports/${q(source.reportId)}`))?.summary?.status;
421
189
  case "build":
422
190
  return await this.buildStatus(source.buildId);
423
- case "appBuild":
424
- return (await this.app.buildMeta(source.buildId))?.status;
425
- case "appReport": {
426
- const detail = await this.app.manualReport(source.reportId);
427
- return detail ? manualReportStatus(detail) : undefined;
428
- }
429
191
  }
430
192
  }
431
193
  catch {
@@ -434,8 +196,7 @@ export class AccessibilityReportFetcher {
434
196
  }
435
197
  async buildStatus(buildId) {
436
198
  const q = encodeURIComponent;
437
- const project = this.webBuildProjects.get(buildId) ??
438
- (await this.getData(`${API_BASE}/automated-tests/v1/builds/${q(buildId)}`))?.projectName;
199
+ const project = (await this.getData(`${API_BASE}/automated-tests/v1/builds/${q(buildId)}`))?.projectName;
439
200
  if (!project)
440
201
  return undefined;
441
202
  let nextPage;