@bugbug-io/sdk 13.39.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +576 -0
- package/dist/constants/sdk-error-codes.d.ts +35 -0
- package/dist/constants/sdk-error-codes.d.ts.map +1 -0
- package/dist/index.d.ts +87 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3844 -0
- package/dist/modules/auth/auth.d.ts +11 -0
- package/dist/modules/auth/auth.d.ts.map +1 -0
- package/dist/modules/auth/auth.types.d.ts +34 -0
- package/dist/modules/auth/auth.types.d.ts.map +1 -0
- package/dist/modules/auth/auth.utils.d.ts +4 -0
- package/dist/modules/auth/auth.utils.d.ts.map +1 -0
- package/dist/modules/components/components.d.ts +43 -0
- package/dist/modules/components/components.d.ts.map +1 -0
- package/dist/modules/config/config.d.ts +48 -0
- package/dist/modules/config/config.d.ts.map +1 -0
- package/dist/modules/groups/groups.d.ts +62 -0
- package/dist/modules/groups/groups.d.ts.map +1 -0
- package/dist/modules/profiles/profiles.d.ts +71 -0
- package/dist/modules/profiles/profiles.d.ts.map +1 -0
- package/dist/modules/project/project.d.ts +58 -0
- package/dist/modules/project/project.d.ts.map +1 -0
- package/dist/modules/projectArtifacts/projectArtifacts.d.ts +8 -0
- package/dist/modules/projectArtifacts/projectArtifacts.d.ts.map +1 -0
- package/dist/modules/projectArtifacts/projectArtifacts.types.d.ts +14 -0
- package/dist/modules/projectArtifacts/projectArtifacts.types.d.ts.map +1 -0
- package/dist/modules/projects/projects.d.ts +36 -0
- package/dist/modules/projects/projects.d.ts.map +1 -0
- package/dist/modules/projects/projects.types.d.ts +6 -0
- package/dist/modules/projects/projects.types.d.ts.map +1 -0
- package/dist/modules/stepRuns/stepRuns.d.ts +22 -0
- package/dist/modules/stepRuns/stepRuns.d.ts.map +1 -0
- package/dist/modules/steps/steps.d.ts +47 -0
- package/dist/modules/steps/steps.d.ts.map +1 -0
- package/dist/modules/steps/steps.utils.d.ts +11 -0
- package/dist/modules/steps/steps.utils.d.ts.map +1 -0
- package/dist/modules/suites/suites.d.ts +153 -0
- package/dist/modules/suites/suites.d.ts.map +1 -0
- package/dist/modules/suites/suites.types.d.ts +31 -0
- package/dist/modules/suites/suites.types.d.ts.map +1 -0
- package/dist/modules/tests/tests.d.ts +253 -0
- package/dist/modules/tests/tests.d.ts.map +1 -0
- package/dist/modules/tests/tests.types.d.ts +34 -0
- package/dist/modules/tests/tests.types.d.ts.map +1 -0
- package/dist/modules/tests/tests.utils.d.ts +11 -0
- package/dist/modules/tests/tests.utils.d.ts.map +1 -0
- package/dist/modules/variables/variables.d.ts +14 -0
- package/dist/modules/variables/variables.d.ts.map +1 -0
- package/dist/modules/variables/variables.types.d.ts +7 -0
- package/dist/modules/variables/variables.types.d.ts.map +1 -0
- package/dist/modules/visualRegression/visualRegression.constants.d.ts +7 -0
- package/dist/modules/visualRegression/visualRegression.constants.d.ts.map +1 -0
- package/dist/modules/visualRegression/visualRegression.d.ts +69 -0
- package/dist/modules/visualRegression/visualRegression.d.ts.map +1 -0
- package/dist/modules/visualRegression/visualRegression.types.d.ts +12 -0
- package/dist/modules/visualRegression/visualRegression.types.d.ts.map +1 -0
- package/dist/sdk.d.ts +126 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/services/apiClient/apiClient.d.ts +167 -0
- package/dist/services/apiClient/apiClient.d.ts.map +1 -0
- package/dist/services/apiClient/apiClient.types.d.ts +19 -0
- package/dist/services/apiClient/apiClient.types.d.ts.map +1 -0
- package/dist/services/apiClient/apiClient.utils.d.ts +32 -0
- package/dist/services/apiClient/apiClient.utils.d.ts.map +1 -0
- package/dist/services/rateLimiter/rateLimiter.constants.d.ts +5 -0
- package/dist/services/rateLimiter/rateLimiter.constants.d.ts.map +1 -0
- package/dist/services/rateLimiter/rateLimiter.d.ts +49 -0
- package/dist/services/rateLimiter/rateLimiter.d.ts.map +1 -0
- package/dist/services/rateLimiter/rateLimiter.types.d.ts +34 -0
- package/dist/services/rateLimiter/rateLimiter.types.d.ts.map +1 -0
- package/dist/testUtils/fakeFactory.d.ts +15 -0
- package/dist/testUtils/fakeFactory.d.ts.map +1 -0
- package/dist/testUtils/setup.d.ts +2 -0
- package/dist/testUtils/setup.d.ts.map +1 -0
- package/dist/types/config.d.ts +121 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/errors.d.ts +162 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/sdk.d.ts +28 -0
- package/dist/types/sdk.d.ts.map +1 -0
- package/dist/utils/pagination.d.ts +10 -0
- package/dist/utils/pagination.d.ts.map +1 -0
- package/dist/utils/polling.d.ts +20 -0
- package/dist/utils/polling.d.ts.map +1 -0
- package/package.json +83 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3844 @@
|
|
|
1
|
+
// src/modules/auth/auth.ts
|
|
2
|
+
var AuthModule = class {
|
|
3
|
+
#client;
|
|
4
|
+
constructor(client) {
|
|
5
|
+
this.#client = client;
|
|
6
|
+
}
|
|
7
|
+
getAuth = async (params, options = {}) => {
|
|
8
|
+
const response = await this.#client.getAppRoot("/auth/authorize-url", {
|
|
9
|
+
...options,
|
|
10
|
+
authenticated: false,
|
|
11
|
+
acceptRedirect: true,
|
|
12
|
+
redirect: "manual",
|
|
13
|
+
queryParams: {
|
|
14
|
+
client_id: params.clientId,
|
|
15
|
+
redirect_uri: params.redirectUri,
|
|
16
|
+
code_challenge: params.codeChallenge,
|
|
17
|
+
code_challenge_method: params.codeChallengeMethod,
|
|
18
|
+
state: params.state
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
const authorizeUrl = response instanceof Response ? response.headers.get("location") : void 0;
|
|
22
|
+
if (!authorizeUrl) {
|
|
23
|
+
throw new Error("BugBug OAuth authorize response did not include a redirect URL.");
|
|
24
|
+
}
|
|
25
|
+
return { authorizeUrl: new URL(authorizeUrl, response.url).toString() };
|
|
26
|
+
};
|
|
27
|
+
exchangeCode = async (payload, options = {}) => this.#client.postAppRoot("/auth/oauth/token/", {
|
|
28
|
+
...options,
|
|
29
|
+
authenticated: false,
|
|
30
|
+
body: {
|
|
31
|
+
grant_type: payload.grantType,
|
|
32
|
+
client_id: payload.clientId,
|
|
33
|
+
redirect_uri: payload.redirectUri,
|
|
34
|
+
code: payload.code,
|
|
35
|
+
code_verifier: payload.codeVerifier
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
identity = async (options = {}) => this.#client.getAppRoot("/auth/identity", options);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/modules/components/components.ts
|
|
42
|
+
var ComponentsModule = class {
|
|
43
|
+
#client;
|
|
44
|
+
constructor(client) {
|
|
45
|
+
this.#client = client;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* List all components with optional filtering and pagination
|
|
49
|
+
*
|
|
50
|
+
* @param options - Query options including search query, page, and page size
|
|
51
|
+
* @returns Paginated list of components with nested structure
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* // List all components
|
|
56
|
+
* const components = await sdk.components.list();
|
|
57
|
+
*
|
|
58
|
+
* // Search for specific components
|
|
59
|
+
* const loginComponents = await sdk.components.list({
|
|
60
|
+
* query: 'login',
|
|
61
|
+
* cursor: nextToken,
|
|
62
|
+
* pageSize: 50
|
|
63
|
+
* });
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
list = async (options = {}) => {
|
|
67
|
+
const { query: searchQuery, cursor, pageSize, ordering, ...requestOptions } = options;
|
|
68
|
+
return this.#client.get("/components/", {
|
|
69
|
+
queryParams: {
|
|
70
|
+
query: searchQuery,
|
|
71
|
+
cursor,
|
|
72
|
+
page_size: pageSize,
|
|
73
|
+
ordering
|
|
74
|
+
},
|
|
75
|
+
...requestOptions
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Fetch component usage — how many tests reference this component and which.
|
|
80
|
+
*
|
|
81
|
+
* @param componentId - UUID of the component.
|
|
82
|
+
* @returns `{ usage, testsIds }` from `GET /components/{id}/usage/`.
|
|
83
|
+
*/
|
|
84
|
+
getUsage = async (componentId, options = {}) => this.#client.get("/components/{id}/usage/", {
|
|
85
|
+
pathParams: { id: componentId },
|
|
86
|
+
...options
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ../core/dist/utils/time.js
|
|
91
|
+
var hoursAgo = (hours) => new Date(Date.now() - hours * 60 * 60 * 1e3).toISOString();
|
|
92
|
+
var now = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
93
|
+
|
|
94
|
+
// src/modules/config/config.ts
|
|
95
|
+
var ConfigModule = class {
|
|
96
|
+
#client;
|
|
97
|
+
constructor(client) {
|
|
98
|
+
this.#client = client;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Fetch the BugBug infrastructure IP ranges that should be allowlisted for
|
|
102
|
+
* outbound runner traffic.
|
|
103
|
+
*/
|
|
104
|
+
getIpAddresses = async (options = {}) => this.#client.get("/config/ips/", options);
|
|
105
|
+
/**
|
|
106
|
+
* Liveness probe + token check.
|
|
107
|
+
*
|
|
108
|
+
* `GET /config/status/` is unauthenticated. If the SDK's `apiToken` is sent,
|
|
109
|
+
* the server validates it and reports the result via `authenticated`; an
|
|
110
|
+
* invalid token still returns 200, so this method only throws on
|
|
111
|
+
* transport-level failures.
|
|
112
|
+
*/
|
|
113
|
+
getStatus = async (options = {}) => this.#client.get("/config/status/", options);
|
|
114
|
+
/**
|
|
115
|
+
* Probe API connectivity and authentication by making one lightweight call.
|
|
116
|
+
*
|
|
117
|
+
* Never throws — transport failures are returned as `connected: false` with
|
|
118
|
+
* the error message. When the call succeeds, `authenticated` reflects whether
|
|
119
|
+
* the configured API token is currently valid.
|
|
120
|
+
*
|
|
121
|
+
* @returns A `ConnectivityStatus` snapshot with response time and message.
|
|
122
|
+
*/
|
|
123
|
+
testConnectivity = async (options = {}) => {
|
|
124
|
+
const startTime = Date.now();
|
|
125
|
+
try {
|
|
126
|
+
const { authenticated } = await this.getStatus(options);
|
|
127
|
+
return {
|
|
128
|
+
connected: true,
|
|
129
|
+
responseTime: Date.now() - startTime,
|
|
130
|
+
timestamp: now(),
|
|
131
|
+
message: "API connectivity successful",
|
|
132
|
+
authenticated
|
|
133
|
+
};
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return {
|
|
136
|
+
connected: false,
|
|
137
|
+
responseTime: Date.now() - startTime,
|
|
138
|
+
timestamp: now(),
|
|
139
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
140
|
+
message: "API connectivity failed"
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Single-shot system summary: IP ranges + connectivity probe.
|
|
146
|
+
*
|
|
147
|
+
* Internally issues one IP-fetch and reuses it as the connectivity probe to
|
|
148
|
+
* avoid duplicate requests.
|
|
149
|
+
*
|
|
150
|
+
* @returns A {@link SdkSystemInfo} snapshot with `{ ipAddresses, connectivity, timestamp }`.
|
|
151
|
+
*/
|
|
152
|
+
getSystemInfo = async (options = {}) => {
|
|
153
|
+
const startTime = Date.now();
|
|
154
|
+
let ipAddresses = null;
|
|
155
|
+
let connectivity;
|
|
156
|
+
try {
|
|
157
|
+
ipAddresses = await this.getIpAddresses(options);
|
|
158
|
+
connectivity = {
|
|
159
|
+
connected: true,
|
|
160
|
+
responseTime: Date.now() - startTime,
|
|
161
|
+
timestamp: now(),
|
|
162
|
+
message: "API connectivity successful"
|
|
163
|
+
};
|
|
164
|
+
} catch (error) {
|
|
165
|
+
connectivity = {
|
|
166
|
+
connected: false,
|
|
167
|
+
responseTime: Date.now() - startTime,
|
|
168
|
+
timestamp: now(),
|
|
169
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
170
|
+
message: "API connectivity failed"
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
ipAddresses,
|
|
175
|
+
connectivity,
|
|
176
|
+
timestamp: now()
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// src/utils/pagination.ts
|
|
182
|
+
var DEFAULT_PAGE_SIZE = 100;
|
|
183
|
+
var getAllPages = async (listPage, options = {}) => {
|
|
184
|
+
const items = [];
|
|
185
|
+
const baseOptions = options;
|
|
186
|
+
let { cursor } = baseOptions;
|
|
187
|
+
do {
|
|
188
|
+
const response = await listPage({
|
|
189
|
+
...baseOptions,
|
|
190
|
+
cursor,
|
|
191
|
+
pageSize: baseOptions.pageSize ?? DEFAULT_PAGE_SIZE
|
|
192
|
+
});
|
|
193
|
+
items.push(...response.results ?? []);
|
|
194
|
+
cursor = response.nextToken ?? void 0;
|
|
195
|
+
} while (cursor);
|
|
196
|
+
return items;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// src/modules/groups/groups.ts
|
|
200
|
+
var GroupsModule = class {
|
|
201
|
+
#client;
|
|
202
|
+
constructor(client) {
|
|
203
|
+
this.#client = client;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* List groups with pagination, search, and ordering.
|
|
207
|
+
*
|
|
208
|
+
* @param options.query - Search query matched against group name.
|
|
209
|
+
* @param options.ordering - Sort key; prefix with `-` for descending.
|
|
210
|
+
* @returns Paginated `GroupsListResponse`.
|
|
211
|
+
*/
|
|
212
|
+
list = async (options = {}) => {
|
|
213
|
+
const { query: searchQuery, cursor, pageSize, ordering, ...requestOptions } = options;
|
|
214
|
+
return this.#client.get("/groups/", {
|
|
215
|
+
queryParams: {
|
|
216
|
+
query: searchQuery,
|
|
217
|
+
cursor,
|
|
218
|
+
page_size: pageSize,
|
|
219
|
+
ordering
|
|
220
|
+
},
|
|
221
|
+
...requestOptions
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* Fetch every group across all pages.
|
|
226
|
+
*/
|
|
227
|
+
getAll = async (options = {}) => getAllPages((pageOptions) => this.list({ ...options, ...pageOptions }));
|
|
228
|
+
/**
|
|
229
|
+
* Fetch a single group by ID (with nested steps).
|
|
230
|
+
*
|
|
231
|
+
* @throws {NotFoundError} When no group matches `groupId`.
|
|
232
|
+
*/
|
|
233
|
+
get = async (groupId, options = {}) => this.#client.get("/groups/{id}/", {
|
|
234
|
+
pathParams: { id: groupId },
|
|
235
|
+
...options
|
|
236
|
+
});
|
|
237
|
+
/**
|
|
238
|
+
* Create a new (empty) group.
|
|
239
|
+
*
|
|
240
|
+
* @throws {ValidationError} On invalid payload.
|
|
241
|
+
*/
|
|
242
|
+
create = async (data, options = {}) => this.#client.post("/groups/", {
|
|
243
|
+
body: data,
|
|
244
|
+
...options
|
|
245
|
+
});
|
|
246
|
+
/**
|
|
247
|
+
* Fully replace a group via PUT. Use {@link partialUpdate} for PATCH semantics.
|
|
248
|
+
*
|
|
249
|
+
* @throws {NotFoundError} When the group does not exist.
|
|
250
|
+
* @throws {ValidationError} On invalid payload.
|
|
251
|
+
*/
|
|
252
|
+
update = async (groupId, data, options = {}) => this.#client.put("/groups/{id}/", {
|
|
253
|
+
pathParams: { id: groupId },
|
|
254
|
+
body: data,
|
|
255
|
+
...options
|
|
256
|
+
});
|
|
257
|
+
/**
|
|
258
|
+
* Patch selected fields on a group (PATCH semantics).
|
|
259
|
+
*
|
|
260
|
+
* @throws {NotFoundError} When the group does not exist.
|
|
261
|
+
* @throws {ValidationError} On invalid payload.
|
|
262
|
+
*/
|
|
263
|
+
partialUpdate = async (groupId, data, options = {}) => this.#client.patch("/groups/{id}/", {
|
|
264
|
+
pathParams: { id: groupId },
|
|
265
|
+
body: data,
|
|
266
|
+
...options
|
|
267
|
+
});
|
|
268
|
+
/**
|
|
269
|
+
* Delete a group by ID. Tests that reference this group will lose those steps.
|
|
270
|
+
*
|
|
271
|
+
* @throws {NotFoundError} When the group does not exist.
|
|
272
|
+
*/
|
|
273
|
+
delete = async (groupId, options = {}) => this.#client.delete("/groups/{id}/", {
|
|
274
|
+
pathParams: { id: groupId },
|
|
275
|
+
...options
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// ../core/dist/utils/validation.js
|
|
280
|
+
var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
281
|
+
var isUuid = (value) => UUID_REGEX.test(value);
|
|
282
|
+
|
|
283
|
+
// src/constants/sdk-error-codes.ts
|
|
284
|
+
var SDK_ERROR_CODES = {
|
|
285
|
+
AUTHENTICATION_ERROR: "AUTHENTICATION_ERROR",
|
|
286
|
+
AUTHORIZATION_ERROR: "AUTHORIZATION_ERROR",
|
|
287
|
+
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
288
|
+
CONFIGURATION_ERROR: "CONFIGURATION_ERROR",
|
|
289
|
+
MISSING_PROJECT_ID_ERROR: "MISSING_PROJECT_ID_ERROR",
|
|
290
|
+
NOT_FOUND_ERROR: "NOT_FOUND_ERROR",
|
|
291
|
+
RATE_LIMIT_ERROR: "RATE_LIMIT_ERROR",
|
|
292
|
+
TIMEOUT_ERROR: "TIMEOUT_ERROR",
|
|
293
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
294
|
+
CANCELLATION_ERROR: "CANCELLATION_ERROR",
|
|
295
|
+
INTERNAL_SERVER_ERROR: "INTERNAL_SERVER_ERROR",
|
|
296
|
+
SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
|
|
297
|
+
UNEXPECTED_RESPONSE: "UNEXPECTED_RESPONSE",
|
|
298
|
+
HTTP_ERROR: "HTTP_ERROR"
|
|
299
|
+
};
|
|
300
|
+
var STATUS_NO_RESPONSE = 0;
|
|
301
|
+
var STATUS_CLIENT_TIMEOUT = 408;
|
|
302
|
+
var STATUS_CLIENT_CANCELLED = 499;
|
|
303
|
+
|
|
304
|
+
// src/types/errors.ts
|
|
305
|
+
var HTTP_STATUS_INTERNAL_SERVER_ERROR = 500;
|
|
306
|
+
var HTTP_STATUS_SERVICE_UNAVAILABLE = 503;
|
|
307
|
+
var BugBugError = class _BugBugError extends Error {
|
|
308
|
+
code;
|
|
309
|
+
statusCode;
|
|
310
|
+
response;
|
|
311
|
+
request;
|
|
312
|
+
cause;
|
|
313
|
+
constructor(code, message, statusCode, response, cause, request) {
|
|
314
|
+
super(message);
|
|
315
|
+
this.name = "BugBugError";
|
|
316
|
+
this.code = code;
|
|
317
|
+
this.statusCode = statusCode;
|
|
318
|
+
this.response = response;
|
|
319
|
+
this.request = request ?? (response ? { url: response.url } : void 0);
|
|
320
|
+
this.cause = cause;
|
|
321
|
+
if (Error.captureStackTrace) {
|
|
322
|
+
Error.captureStackTrace(this, _BugBugError);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
toJSON() {
|
|
326
|
+
return {
|
|
327
|
+
name: this.name,
|
|
328
|
+
code: this.code,
|
|
329
|
+
message: this.message,
|
|
330
|
+
statusCode: this.statusCode,
|
|
331
|
+
request: this.request,
|
|
332
|
+
stack: this.stack
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
var AuthenticationError = class extends BugBugError {
|
|
337
|
+
constructor(message = "Authentication failed", response, request) {
|
|
338
|
+
super(
|
|
339
|
+
SDK_ERROR_CODES.AUTHENTICATION_ERROR,
|
|
340
|
+
message,
|
|
341
|
+
response?.status ?? 401,
|
|
342
|
+
response,
|
|
343
|
+
void 0,
|
|
344
|
+
request
|
|
345
|
+
);
|
|
346
|
+
this.name = "AuthenticationError";
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
var AuthorizationError = class extends BugBugError {
|
|
350
|
+
constructor(message = "Access denied", response, request) {
|
|
351
|
+
super(
|
|
352
|
+
SDK_ERROR_CODES.AUTHORIZATION_ERROR,
|
|
353
|
+
message,
|
|
354
|
+
response?.status ?? 403,
|
|
355
|
+
response,
|
|
356
|
+
void 0,
|
|
357
|
+
request
|
|
358
|
+
);
|
|
359
|
+
this.name = "AuthorizationError";
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
var ValidationError = class extends BugBugError {
|
|
363
|
+
fields;
|
|
364
|
+
constructor(message, response, fields, request) {
|
|
365
|
+
super(
|
|
366
|
+
SDK_ERROR_CODES.VALIDATION_ERROR,
|
|
367
|
+
message,
|
|
368
|
+
response?.status ?? 400,
|
|
369
|
+
response,
|
|
370
|
+
void 0,
|
|
371
|
+
request
|
|
372
|
+
);
|
|
373
|
+
this.name = "ValidationError";
|
|
374
|
+
this.fields = fields;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
var ConfigurationError = class extends BugBugError {
|
|
378
|
+
constructor(message) {
|
|
379
|
+
super(SDK_ERROR_CODES.CONFIGURATION_ERROR, message, STATUS_NO_RESPONSE);
|
|
380
|
+
this.name = "ConfigurationError";
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
var MissingProjectIdError = class extends BugBugError {
|
|
384
|
+
constructor(message = "No project selected. Provide a project id (--project-id) or switch project.", response, request) {
|
|
385
|
+
super(
|
|
386
|
+
SDK_ERROR_CODES.MISSING_PROJECT_ID_ERROR,
|
|
387
|
+
message,
|
|
388
|
+
response?.status ?? 400,
|
|
389
|
+
response,
|
|
390
|
+
void 0,
|
|
391
|
+
request
|
|
392
|
+
);
|
|
393
|
+
this.name = "MissingProjectIdError";
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
var NotFoundError = class extends BugBugError {
|
|
397
|
+
constructor(message = "Resource not found", response, request) {
|
|
398
|
+
super(
|
|
399
|
+
SDK_ERROR_CODES.NOT_FOUND_ERROR,
|
|
400
|
+
message,
|
|
401
|
+
response?.status ?? 404,
|
|
402
|
+
response,
|
|
403
|
+
void 0,
|
|
404
|
+
request
|
|
405
|
+
);
|
|
406
|
+
this.name = "NotFoundError";
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
var RateLimitError = class extends BugBugError {
|
|
410
|
+
retryAfter;
|
|
411
|
+
constructor(retryAfter, message = "Rate limit exceeded", response, request) {
|
|
412
|
+
super(
|
|
413
|
+
SDK_ERROR_CODES.RATE_LIMIT_ERROR,
|
|
414
|
+
message,
|
|
415
|
+
response?.status ?? 429,
|
|
416
|
+
response,
|
|
417
|
+
void 0,
|
|
418
|
+
request
|
|
419
|
+
);
|
|
420
|
+
this.name = "RateLimitError";
|
|
421
|
+
this.retryAfter = retryAfter;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
var TimeoutError = class extends BugBugError {
|
|
425
|
+
timeout;
|
|
426
|
+
constructor(timeout, message, request) {
|
|
427
|
+
const defaultMessage = `Request timed out after ${timeout}ms`;
|
|
428
|
+
super(
|
|
429
|
+
SDK_ERROR_CODES.TIMEOUT_ERROR,
|
|
430
|
+
message || defaultMessage,
|
|
431
|
+
STATUS_CLIENT_TIMEOUT,
|
|
432
|
+
void 0,
|
|
433
|
+
void 0,
|
|
434
|
+
request
|
|
435
|
+
);
|
|
436
|
+
this.name = "TimeoutError";
|
|
437
|
+
this.timeout = timeout;
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
var NetworkError = class extends BugBugError {
|
|
441
|
+
constructor(message, cause, request) {
|
|
442
|
+
super(SDK_ERROR_CODES.NETWORK_ERROR, message, STATUS_NO_RESPONSE, void 0, cause, request);
|
|
443
|
+
this.name = "NetworkError";
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
var CancellationError = class extends BugBugError {
|
|
447
|
+
constructor(message = "Request was cancelled", request) {
|
|
448
|
+
super(
|
|
449
|
+
SDK_ERROR_CODES.CANCELLATION_ERROR,
|
|
450
|
+
message,
|
|
451
|
+
STATUS_CLIENT_CANCELLED,
|
|
452
|
+
void 0,
|
|
453
|
+
void 0,
|
|
454
|
+
request
|
|
455
|
+
);
|
|
456
|
+
this.name = "CancellationError";
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
var InternalServerError = class extends BugBugError {
|
|
460
|
+
constructor(message = "Internal server error", response, request) {
|
|
461
|
+
super(
|
|
462
|
+
SDK_ERROR_CODES.INTERNAL_SERVER_ERROR,
|
|
463
|
+
message,
|
|
464
|
+
response?.status ?? HTTP_STATUS_INTERNAL_SERVER_ERROR,
|
|
465
|
+
response,
|
|
466
|
+
void 0,
|
|
467
|
+
request
|
|
468
|
+
);
|
|
469
|
+
this.name = "InternalServerError";
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
var ServiceUnavailableError = class extends BugBugError {
|
|
473
|
+
constructor(message = "Service temporarily unavailable", response, request) {
|
|
474
|
+
super(
|
|
475
|
+
SDK_ERROR_CODES.SERVICE_UNAVAILABLE,
|
|
476
|
+
message,
|
|
477
|
+
response?.status ?? HTTP_STATUS_SERVICE_UNAVAILABLE,
|
|
478
|
+
response,
|
|
479
|
+
void 0,
|
|
480
|
+
request
|
|
481
|
+
);
|
|
482
|
+
this.name = "ServiceUnavailableError";
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
var UnexpectedResponseError = class extends BugBugError {
|
|
486
|
+
constructor(message, cause, request) {
|
|
487
|
+
super(
|
|
488
|
+
SDK_ERROR_CODES.UNEXPECTED_RESPONSE,
|
|
489
|
+
message,
|
|
490
|
+
STATUS_NO_RESPONSE,
|
|
491
|
+
void 0,
|
|
492
|
+
cause,
|
|
493
|
+
request
|
|
494
|
+
);
|
|
495
|
+
this.name = "UnexpectedResponseError";
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
var HttpError = class extends BugBugError {
|
|
499
|
+
constructor(message, statusCode, response, request) {
|
|
500
|
+
super(SDK_ERROR_CODES.HTTP_ERROR, message, statusCode, response, void 0, request);
|
|
501
|
+
this.name = "HttpError";
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
function isBugBugError(error) {
|
|
505
|
+
return error instanceof BugBugError;
|
|
506
|
+
}
|
|
507
|
+
function isAuthenticationError(error) {
|
|
508
|
+
return error instanceof AuthenticationError;
|
|
509
|
+
}
|
|
510
|
+
function isAuthorizationError(error) {
|
|
511
|
+
return error instanceof AuthorizationError;
|
|
512
|
+
}
|
|
513
|
+
function isValidationError(error) {
|
|
514
|
+
return error instanceof ValidationError;
|
|
515
|
+
}
|
|
516
|
+
function isConfigurationError(error) {
|
|
517
|
+
return error instanceof ConfigurationError;
|
|
518
|
+
}
|
|
519
|
+
function isMissingProjectIdError(error) {
|
|
520
|
+
return error instanceof MissingProjectIdError;
|
|
521
|
+
}
|
|
522
|
+
function isNotFoundError(error) {
|
|
523
|
+
return error instanceof NotFoundError;
|
|
524
|
+
}
|
|
525
|
+
function isRateLimitError(error) {
|
|
526
|
+
return error instanceof RateLimitError;
|
|
527
|
+
}
|
|
528
|
+
function isTimeoutError(error) {
|
|
529
|
+
return error instanceof TimeoutError;
|
|
530
|
+
}
|
|
531
|
+
function isNetworkError(error) {
|
|
532
|
+
return error instanceof NetworkError;
|
|
533
|
+
}
|
|
534
|
+
function isCancellationError(error) {
|
|
535
|
+
return error instanceof CancellationError;
|
|
536
|
+
}
|
|
537
|
+
function isInternalServerError(error) {
|
|
538
|
+
return error instanceof InternalServerError;
|
|
539
|
+
}
|
|
540
|
+
function isServiceUnavailableError(error) {
|
|
541
|
+
return error instanceof ServiceUnavailableError;
|
|
542
|
+
}
|
|
543
|
+
function isUnexpectedResponseError(error) {
|
|
544
|
+
return error instanceof UnexpectedResponseError;
|
|
545
|
+
}
|
|
546
|
+
function isHttpError(error) {
|
|
547
|
+
return error instanceof HttpError;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/modules/profiles/profiles.ts
|
|
551
|
+
var ProfilesModule = class {
|
|
552
|
+
#client;
|
|
553
|
+
constructor(client) {
|
|
554
|
+
this.#client = client;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* List run profiles, one page at a time.
|
|
558
|
+
*
|
|
559
|
+
* @param options.cursor - Opaque cursor returned as `nextToken` by the previous page.
|
|
560
|
+
* @param options.pageSize - Items per page.
|
|
561
|
+
* @returns Paginated `ProfilesListResponse` (validated via Zod).
|
|
562
|
+
* @throws {UnexpectedResponseError} On Zod validation failure.
|
|
563
|
+
*/
|
|
564
|
+
list = async (options = {}) => {
|
|
565
|
+
const { cursor, pageSize, ...requestOptions } = options;
|
|
566
|
+
return this.#client.get("/profiles/", {
|
|
567
|
+
queryParams: {
|
|
568
|
+
cursor,
|
|
569
|
+
page_size: pageSize
|
|
570
|
+
},
|
|
571
|
+
...requestOptions
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
/**
|
|
575
|
+
* Fetch a single profile by ID.
|
|
576
|
+
*
|
|
577
|
+
* @throws {NotFoundError} When no profile matches `profileId`.
|
|
578
|
+
*/
|
|
579
|
+
get = async (profileId, options = {}) => this.#client.get("/profiles/{id}/", {
|
|
580
|
+
pathParams: { id: profileId },
|
|
581
|
+
...options
|
|
582
|
+
});
|
|
583
|
+
/**
|
|
584
|
+
* Find a profile by name across all pages.
|
|
585
|
+
*
|
|
586
|
+
* @returns The matching profile, or `null` when no profile has that name.
|
|
587
|
+
*/
|
|
588
|
+
findByName = async (profileName, options = {}) => {
|
|
589
|
+
const profiles = await this.getAll(options);
|
|
590
|
+
return profiles.find((profile) => profile.name === profileName) || null;
|
|
591
|
+
};
|
|
592
|
+
/**
|
|
593
|
+
* Fetch every profile across all pages.
|
|
594
|
+
*
|
|
595
|
+
* Internally follows `nextToken` until the API signals exhaustion.
|
|
596
|
+
* For large tenants this can issue multiple sequential requests; cache the
|
|
597
|
+
* result where possible.
|
|
598
|
+
*/
|
|
599
|
+
getAll = async (options = {}) => getAllPages((pageOptions) => this.list({ ...options, ...pageOptions }));
|
|
600
|
+
/**
|
|
601
|
+
* Return the profile flagged `isDefault`, falling back to the first profile.
|
|
602
|
+
*
|
|
603
|
+
* @returns The default `Profile`, or `null` when the tenant has no profiles.
|
|
604
|
+
*/
|
|
605
|
+
getDefault = async (options = {}) => {
|
|
606
|
+
const profiles = await this.getAll(options);
|
|
607
|
+
return profiles.find((profile) => profile.isDefault) || profiles[0] || null;
|
|
608
|
+
};
|
|
609
|
+
/**
|
|
610
|
+
* Resolve a profile name **or** UUID to a profile UUID suitable for the
|
|
611
|
+
* `runProfileId` field on a run request.
|
|
612
|
+
*
|
|
613
|
+
* - UUID input → returned unchanged with **no** API call.
|
|
614
|
+
* - Name input → looked up via {@link findByName}.
|
|
615
|
+
*
|
|
616
|
+
* @param profileNameOrId - Human-readable profile name (e.g. `"Production"`) or UUID.
|
|
617
|
+
* @returns The matching profile UUID.
|
|
618
|
+
* @throws {NotFoundError} When `profileNameOrId` is not a UUID and no profile has that name.
|
|
619
|
+
*
|
|
620
|
+
* @example
|
|
621
|
+
* ```typescript
|
|
622
|
+
* const profileId = await sdk.profiles.resolveId('Production');
|
|
623
|
+
* await sdk.tests.startRun(testId, { profileName: profileId });
|
|
624
|
+
* ```
|
|
625
|
+
*/
|
|
626
|
+
resolveId = async (profileNameOrId, options = {}) => {
|
|
627
|
+
if (isUuid(profileNameOrId)) {
|
|
628
|
+
return profileNameOrId;
|
|
629
|
+
}
|
|
630
|
+
const profile = await this.findByName(profileNameOrId, options);
|
|
631
|
+
if (!profile) {
|
|
632
|
+
throw new NotFoundError(`Profile '${profileNameOrId}' not found`);
|
|
633
|
+
}
|
|
634
|
+
return profile.id;
|
|
635
|
+
};
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// src/modules/project/project.ts
|
|
639
|
+
var ProjectModule = class {
|
|
640
|
+
#client;
|
|
641
|
+
constructor(client) {
|
|
642
|
+
this.#client = client;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Fetch project settings (e.g. retention, integrations).
|
|
646
|
+
*
|
|
647
|
+
* @returns The `ProjectSettings` object for the project.
|
|
648
|
+
*/
|
|
649
|
+
getSettings = async (options = {}) => this.#client.get("/settings/", options);
|
|
650
|
+
/**
|
|
651
|
+
* Export the entire project as a ZIP archive (tests, suites, components,
|
|
652
|
+
* profiles, settings).
|
|
653
|
+
*
|
|
654
|
+
* @returns Raw ZIP bytes ready to be written to disk or uploaded elsewhere.
|
|
655
|
+
*
|
|
656
|
+
* @example
|
|
657
|
+
* ```typescript
|
|
658
|
+
* import { writeFile } from 'node:fs/promises';
|
|
659
|
+
* const zipBytes = await sdk.project.export();
|
|
660
|
+
* await writeFile('project.zip', zipBytes);
|
|
661
|
+
* ```
|
|
662
|
+
*/
|
|
663
|
+
export = async (options = {}) => this.#client.getBinary("/project/export/", options);
|
|
664
|
+
/**
|
|
665
|
+
* Import a project from a ZIP archive.
|
|
666
|
+
*
|
|
667
|
+
* Performs the two-step server flow:
|
|
668
|
+
* 1. **Validate** the upload — surfaces server-side errors (malformed ZIP,
|
|
669
|
+
* naming conflicts, …) as {@link ValidationError}.
|
|
670
|
+
* 2. **Run** the import asynchronously using the validated upload id.
|
|
671
|
+
*
|
|
672
|
+
* @param zipBytes - Raw ZIP archive bytes.
|
|
673
|
+
* @returns The response from the import run endpoint (typically a 202 with
|
|
674
|
+
* empty body — kept as `unknown` until the API stabilizes the shape).
|
|
675
|
+
* @throws {UnexpectedResponseError} When validation succeeds but the server
|
|
676
|
+
* omits the `uploadId` needed to trigger the run step.
|
|
677
|
+
* @throws {ValidationError} When the ZIP is rejected by validate.
|
|
678
|
+
*
|
|
679
|
+
* @example
|
|
680
|
+
* ```typescript
|
|
681
|
+
* import { readFile } from 'node:fs/promises';
|
|
682
|
+
* const bytes = await readFile('project.zip');
|
|
683
|
+
* await sdk.project.import(bytes);
|
|
684
|
+
* ```
|
|
685
|
+
*/
|
|
686
|
+
import = async (zipBytes, options = {}) => {
|
|
687
|
+
const validation = await this.#validateImport(zipBytes, options);
|
|
688
|
+
const uploadId = typeof validation.uploadId === "string" ? validation.uploadId : void 0;
|
|
689
|
+
if (!uploadId) {
|
|
690
|
+
throw new UnexpectedResponseError(
|
|
691
|
+
"Import validation succeeded but did not return an uploadId."
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
return this.#client.post("/project/import/run/", {
|
|
695
|
+
...options,
|
|
696
|
+
body: { uploadId }
|
|
697
|
+
});
|
|
698
|
+
};
|
|
699
|
+
#validateImport = async (zipBytes, options) => {
|
|
700
|
+
const form = new FormData();
|
|
701
|
+
form.append(
|
|
702
|
+
"file",
|
|
703
|
+
new Blob([zipBytes], { type: "application/zip" }),
|
|
704
|
+
"project.zip"
|
|
705
|
+
);
|
|
706
|
+
return this.#client.postForm(
|
|
707
|
+
"/project/import/validate/",
|
|
708
|
+
form,
|
|
709
|
+
options
|
|
710
|
+
);
|
|
711
|
+
};
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// src/modules/projectArtifacts/projectArtifacts.ts
|
|
715
|
+
var ProjectArtifactsModule = class {
|
|
716
|
+
#client;
|
|
717
|
+
constructor(client) {
|
|
718
|
+
this.#client = client;
|
|
719
|
+
}
|
|
720
|
+
upload = async ({
|
|
721
|
+
projectId,
|
|
722
|
+
file,
|
|
723
|
+
fileName,
|
|
724
|
+
contentType = "application/octet-stream",
|
|
725
|
+
stepId,
|
|
726
|
+
...options
|
|
727
|
+
}) => {
|
|
728
|
+
const form = new FormData();
|
|
729
|
+
const blob = file instanceof Blob ? file : new Blob([file], { type: contentType });
|
|
730
|
+
form.append("file", blob, fileName);
|
|
731
|
+
if (stepId) {
|
|
732
|
+
form.append("stepId", stepId);
|
|
733
|
+
}
|
|
734
|
+
return this.#client.postForm("/projects/{projectId}/artifacts/", form, {
|
|
735
|
+
...options,
|
|
736
|
+
pathParams: { projectId },
|
|
737
|
+
useAppRoot: true
|
|
738
|
+
});
|
|
739
|
+
};
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
// src/modules/projects/projects.ts
|
|
743
|
+
var ProjectsModule = class {
|
|
744
|
+
#client;
|
|
745
|
+
constructor(client) {
|
|
746
|
+
this.#client = client;
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* List projects accessible with the current token, one page at a time.
|
|
750
|
+
*
|
|
751
|
+
* @param options.cursor - Opaque cursor returned as `nextToken` by the previous page.
|
|
752
|
+
* @param options.pageSize - Items per page.
|
|
753
|
+
* @param options.query - Case-insensitive name filter.
|
|
754
|
+
* @returns Paginated `ProjectsListResponse`.
|
|
755
|
+
*/
|
|
756
|
+
list = async (options = {}) => {
|
|
757
|
+
const { cursor, pageSize, query, ...requestOptions } = options;
|
|
758
|
+
return this.#client.get("/projects/", {
|
|
759
|
+
...requestOptions,
|
|
760
|
+
queryParams: {
|
|
761
|
+
cursor,
|
|
762
|
+
page_size: pageSize,
|
|
763
|
+
query
|
|
764
|
+
},
|
|
765
|
+
projectScoped: false
|
|
766
|
+
});
|
|
767
|
+
};
|
|
768
|
+
/**
|
|
769
|
+
* Fetch every project across all pages.
|
|
770
|
+
*
|
|
771
|
+
* Internally follows `nextToken` until the API signals exhaustion.
|
|
772
|
+
*/
|
|
773
|
+
getAll = async (options = {}) => getAllPages((pageOptions) => this.list({ ...options, ...pageOptions }));
|
|
774
|
+
/**
|
|
775
|
+
* Find a project by its id across all pages.
|
|
776
|
+
*
|
|
777
|
+
* @returns The matching project, or `null` when no project has that id.
|
|
778
|
+
*/
|
|
779
|
+
findById = async (projectId, options = {}) => {
|
|
780
|
+
const projects = await this.getAll(options);
|
|
781
|
+
return projects.find((project) => project.id === projectId) ?? null;
|
|
782
|
+
};
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
// src/modules/stepRuns/stepRuns.ts
|
|
786
|
+
var StepRunsModule = class {
|
|
787
|
+
#client;
|
|
788
|
+
constructor(client) {
|
|
789
|
+
this.#client = client;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Fetch full details for a single step run.
|
|
793
|
+
*
|
|
794
|
+
* @param stepRunId - UUID of the step run (from a `TestRun.stepsRuns` entry).
|
|
795
|
+
* @returns The step-run record including error code and timing.
|
|
796
|
+
* @throws {NotFoundError} When no step run matches `stepRunId`.
|
|
797
|
+
*/
|
|
798
|
+
get = async (stepRunId, options = {}) => this.#client.get("/stepruns/{id}/", {
|
|
799
|
+
pathParams: { id: stepRunId },
|
|
800
|
+
...options
|
|
801
|
+
});
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
// src/modules/steps/steps.ts
|
|
805
|
+
var StepsModule = class {
|
|
806
|
+
#client;
|
|
807
|
+
constructor(client) {
|
|
808
|
+
this.#client = client;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* Fetch full details of a single step.
|
|
812
|
+
*
|
|
813
|
+
* @throws {NotFoundError} When no step matches `stepId`.
|
|
814
|
+
*/
|
|
815
|
+
get = async (stepId, options = {}) => this.#client.get("/steps/{id}/", {
|
|
816
|
+
pathParams: { id: stepId },
|
|
817
|
+
...options
|
|
818
|
+
});
|
|
819
|
+
/**
|
|
820
|
+
* Create a new step inside an existing group.
|
|
821
|
+
*
|
|
822
|
+
* @param data - Step payload; `type` discriminates the validation schema.
|
|
823
|
+
* @throws {ValidationError} On invalid payload (DRF field errors on `error.fields`).
|
|
824
|
+
*/
|
|
825
|
+
create = async (data, options = {}) => this.#client.post("/steps/", {
|
|
826
|
+
body: data,
|
|
827
|
+
...options
|
|
828
|
+
});
|
|
829
|
+
/**
|
|
830
|
+
* Fully replace a step via PUT. Use {@link partialUpdate} for PATCH semantics.
|
|
831
|
+
*
|
|
832
|
+
* @throws {NotFoundError} When the step does not exist.
|
|
833
|
+
* @throws {ValidationError} On invalid payload.
|
|
834
|
+
*/
|
|
835
|
+
update = async (stepId, data, options = {}) => this.#client.put("/steps/{id}/", {
|
|
836
|
+
pathParams: { id: stepId },
|
|
837
|
+
body: data,
|
|
838
|
+
...options
|
|
839
|
+
});
|
|
840
|
+
/**
|
|
841
|
+
* Patch selected fields on a step (PATCH semantics).
|
|
842
|
+
*
|
|
843
|
+
* @throws {NotFoundError} When the step does not exist.
|
|
844
|
+
* @throws {ValidationError} On invalid payload.
|
|
845
|
+
*/
|
|
846
|
+
partialUpdate = async (stepId, data, options = {}) => this.#client.patch("/steps/{id}/", {
|
|
847
|
+
pathParams: { id: stepId },
|
|
848
|
+
body: data,
|
|
849
|
+
...options
|
|
850
|
+
});
|
|
851
|
+
/**
|
|
852
|
+
* Delete a step by ID.
|
|
853
|
+
*
|
|
854
|
+
* @throws {NotFoundError} When the step does not exist.
|
|
855
|
+
*/
|
|
856
|
+
delete = async (stepId, options = {}) => this.#client.delete("/steps/{id}/", {
|
|
857
|
+
pathParams: { id: stepId },
|
|
858
|
+
...options
|
|
859
|
+
});
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
// src/types/sdk.ts
|
|
863
|
+
var isRunningStatus = (status) => ["initialized", "queued", "running", "auto_retrying"].includes(status ?? "");
|
|
864
|
+
var isEditingStatus = (status) => ["recording", "paused"].includes(status ?? "");
|
|
865
|
+
var isFailedStatus = (status) => ["failed", "error"].includes(status ?? "");
|
|
866
|
+
var isPassedStatus = (status) => ["passed"].includes(status ?? "");
|
|
867
|
+
var isCompletedStatus = (status) => ["passed", "failed", "error", "stopped"].includes(status ?? "");
|
|
868
|
+
|
|
869
|
+
// ../core/dist/utils/async.js
|
|
870
|
+
var sleep = (ms) => new Promise((resolve4) => {
|
|
871
|
+
setTimeout(resolve4, ms);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// src/utils/polling.ts
|
|
875
|
+
var pollRunUntilComplete = async (getRun, isComplete, options) => {
|
|
876
|
+
const { pollInterval = 2e3, timeout = options.defaultTimeout, onProgress } = options;
|
|
877
|
+
const endTime = Date.now() + timeout;
|
|
878
|
+
const sleepWithinTimeout = async () => {
|
|
879
|
+
const remainingMs = endTime - Date.now();
|
|
880
|
+
if (remainingMs <= 0) throw new TimeoutError(timeout, options.timeoutMessage(timeout));
|
|
881
|
+
await sleep(Math.min(pollInterval, remainingMs));
|
|
882
|
+
};
|
|
883
|
+
while (Date.now() < endTime) {
|
|
884
|
+
try {
|
|
885
|
+
const run = await getRun();
|
|
886
|
+
await onProgress?.(run);
|
|
887
|
+
if (isComplete(run)) {
|
|
888
|
+
return run;
|
|
889
|
+
}
|
|
890
|
+
await sleepWithinTimeout();
|
|
891
|
+
} catch (error) {
|
|
892
|
+
if (!isNetworkError(error)) {
|
|
893
|
+
throw error;
|
|
894
|
+
}
|
|
895
|
+
await sleepWithinTimeout();
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
throw new TimeoutError(timeout, options.timeoutMessage(timeout));
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
// src/modules/suites/suites.ts
|
|
902
|
+
var SuitesModule = class {
|
|
903
|
+
#client;
|
|
904
|
+
#profiles;
|
|
905
|
+
constructor(client) {
|
|
906
|
+
this.#client = client;
|
|
907
|
+
this.#profiles = new ProfilesModule(client);
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* List suites with pagination, search, and ordering.
|
|
911
|
+
*
|
|
912
|
+
* @param options.cursor - Opaque cursor returned as `nextToken` by the previous page.
|
|
913
|
+
* @param options.pageSize - Items per page.
|
|
914
|
+
* @param options.query - Search query matched against suite name.
|
|
915
|
+
* @param options.ordering - Sort key; prefix with `-` for descending.
|
|
916
|
+
* @returns Paginated `SuitesListResponse` envelope (validated via Zod).
|
|
917
|
+
* @throws {UnexpectedResponseError} On Zod validation failure.
|
|
918
|
+
*/
|
|
919
|
+
list = async (options = {}) => {
|
|
920
|
+
const { cursor, pageSize, query: searchQuery, ordering, ...requestOptions } = options;
|
|
921
|
+
return this.#client.get("/suites/", {
|
|
922
|
+
queryParams: {
|
|
923
|
+
cursor,
|
|
924
|
+
page_size: pageSize,
|
|
925
|
+
query: searchQuery,
|
|
926
|
+
ordering
|
|
927
|
+
},
|
|
928
|
+
...requestOptions
|
|
929
|
+
});
|
|
930
|
+
};
|
|
931
|
+
/**
|
|
932
|
+
* Fetch every suite across all pages.
|
|
933
|
+
*/
|
|
934
|
+
getAll = async (options = {}) => getAllPages((pageOptions) => this.list(pageOptions), options);
|
|
935
|
+
/**
|
|
936
|
+
* Fetch a single suite by ID.
|
|
937
|
+
*
|
|
938
|
+
* @throws {NotFoundError} When no suite matches `suiteId`.
|
|
939
|
+
*/
|
|
940
|
+
get = async (suiteId, options = {}) => this.#client.get("/suites/{id}/", {
|
|
941
|
+
pathParams: { id: suiteId },
|
|
942
|
+
...options
|
|
943
|
+
});
|
|
944
|
+
/**
|
|
945
|
+
* Create a new suite.
|
|
946
|
+
*
|
|
947
|
+
* @throws {ValidationError} On invalid payload.
|
|
948
|
+
*/
|
|
949
|
+
create = async (data, options = {}) => this.#client.post("/suites/", {
|
|
950
|
+
body: data,
|
|
951
|
+
...options
|
|
952
|
+
});
|
|
953
|
+
/**
|
|
954
|
+
* Fully replace a suite via PUT (all fields required).
|
|
955
|
+
* Use {@link partialUpdate} for sparse PATCH semantics.
|
|
956
|
+
*
|
|
957
|
+
* @throws {NotFoundError} When the suite does not exist.
|
|
958
|
+
* @throws {ValidationError} On invalid payload.
|
|
959
|
+
*/
|
|
960
|
+
update = async (suiteId, data, options = {}) => this.#client.put("/suites/{id}/", {
|
|
961
|
+
pathParams: { id: suiteId },
|
|
962
|
+
body: data,
|
|
963
|
+
...options
|
|
964
|
+
});
|
|
965
|
+
/**
|
|
966
|
+
* Patch selected fields on a suite (PATCH semantics).
|
|
967
|
+
*
|
|
968
|
+
* @throws {NotFoundError} When the suite does not exist.
|
|
969
|
+
* @throws {ValidationError} On invalid payload.
|
|
970
|
+
*/
|
|
971
|
+
partialUpdate = async (suiteId, data, options = {}) => this.#client.patch("/suites/{id}/", {
|
|
972
|
+
pathParams: { id: suiteId },
|
|
973
|
+
body: data,
|
|
974
|
+
...options
|
|
975
|
+
});
|
|
976
|
+
/**
|
|
977
|
+
* Delete a suite by ID.
|
|
978
|
+
*
|
|
979
|
+
* @throws {NotFoundError} When the suite does not exist.
|
|
980
|
+
*/
|
|
981
|
+
delete = async (suiteId, options = {}) => this.#client.delete("/suites/{id}/", {
|
|
982
|
+
pathParams: { id: suiteId },
|
|
983
|
+
...options
|
|
984
|
+
});
|
|
985
|
+
async startRun(suiteId, options = {}) {
|
|
986
|
+
const {
|
|
987
|
+
watchProgress,
|
|
988
|
+
pollInterval,
|
|
989
|
+
timeout,
|
|
990
|
+
onProgress,
|
|
991
|
+
profileName,
|
|
992
|
+
variables,
|
|
993
|
+
...runOptions
|
|
994
|
+
} = options;
|
|
995
|
+
const runProfileId = profileName ? await this.#profiles.resolveId(profileName, runOptions) : void 0;
|
|
996
|
+
const runResult = await this.#client.post("/suiteruns/", {
|
|
997
|
+
body: {
|
|
998
|
+
suiteId,
|
|
999
|
+
runProfileId,
|
|
1000
|
+
variables: variables ?? [],
|
|
1001
|
+
triggeredBy: this.#client.getConfig().triggeredBy
|
|
1002
|
+
},
|
|
1003
|
+
...runOptions
|
|
1004
|
+
});
|
|
1005
|
+
if (watchProgress) {
|
|
1006
|
+
return this.watchRunProgress(runResult.id, onProgress ?? (() => {
|
|
1007
|
+
}), {
|
|
1008
|
+
pollInterval,
|
|
1009
|
+
timeout
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
return runResult;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Fetch full suite-run details including child test runs.
|
|
1016
|
+
*
|
|
1017
|
+
* @throws {NotFoundError} When the run does not exist.
|
|
1018
|
+
*/
|
|
1019
|
+
getRun = async (runId, options = {}) => this.#client.get("/suiteruns/{id}/", {
|
|
1020
|
+
pathParams: { id: runId },
|
|
1021
|
+
...options
|
|
1022
|
+
});
|
|
1023
|
+
/**
|
|
1024
|
+
* Get list of suite runs.
|
|
1025
|
+
*
|
|
1026
|
+
*/
|
|
1027
|
+
listRuns = async (options = {}) => {
|
|
1028
|
+
const {
|
|
1029
|
+
cursor,
|
|
1030
|
+
pageSize,
|
|
1031
|
+
ordering,
|
|
1032
|
+
startedAfter,
|
|
1033
|
+
startedBefore,
|
|
1034
|
+
status,
|
|
1035
|
+
suiteId,
|
|
1036
|
+
...requestOptions
|
|
1037
|
+
} = options;
|
|
1038
|
+
return this.#client.get("/suiteruns/", {
|
|
1039
|
+
queryParams: {
|
|
1040
|
+
cursor,
|
|
1041
|
+
page_size: pageSize,
|
|
1042
|
+
ordering,
|
|
1043
|
+
started_after: startedAfter,
|
|
1044
|
+
started_before: startedBefore,
|
|
1045
|
+
status,
|
|
1046
|
+
suiteId
|
|
1047
|
+
},
|
|
1048
|
+
...requestOptions
|
|
1049
|
+
});
|
|
1050
|
+
};
|
|
1051
|
+
/**
|
|
1052
|
+
* Get recent suite runs within a time window
|
|
1053
|
+
*/
|
|
1054
|
+
getRecentRuns = async (options = {}) => {
|
|
1055
|
+
const {
|
|
1056
|
+
hours = 24,
|
|
1057
|
+
pageSize = 50,
|
|
1058
|
+
ordering = "-started",
|
|
1059
|
+
suiteId,
|
|
1060
|
+
...requestOptions
|
|
1061
|
+
} = options;
|
|
1062
|
+
const startedAfter = hoursAgo(hours);
|
|
1063
|
+
return this.listRuns({
|
|
1064
|
+
pageSize,
|
|
1065
|
+
ordering,
|
|
1066
|
+
startedAfter,
|
|
1067
|
+
suiteId,
|
|
1068
|
+
...requestOptions
|
|
1069
|
+
});
|
|
1070
|
+
};
|
|
1071
|
+
/**
|
|
1072
|
+
* Fetch only the lightweight status of a suite run. Prefer this over
|
|
1073
|
+
* {@link getRun} when polling.
|
|
1074
|
+
*/
|
|
1075
|
+
getRunProgress = async (runId, options = {}) => this.#client.get("/suiteruns/{id}/status/", {
|
|
1076
|
+
pathParams: { id: runId },
|
|
1077
|
+
...options
|
|
1078
|
+
});
|
|
1079
|
+
/**
|
|
1080
|
+
* Request cancellation of an in-progress suite run.
|
|
1081
|
+
*
|
|
1082
|
+
* @returns The updated status; the run may still take a moment to settle.
|
|
1083
|
+
*/
|
|
1084
|
+
stopRun = async (runId, options = {}) => this.#client.post("/suiteruns/{id}/stop/", {
|
|
1085
|
+
pathParams: { id: runId },
|
|
1086
|
+
...options
|
|
1087
|
+
});
|
|
1088
|
+
/**
|
|
1089
|
+
* Fetch screenshots captured across all child test runs of a suite run.
|
|
1090
|
+
*/
|
|
1091
|
+
getRunScreenshots = async (runId, options = {}) => {
|
|
1092
|
+
const screenshots = await this.#client.get(
|
|
1093
|
+
"/suiteruns/{id}/screenshots/",
|
|
1094
|
+
{
|
|
1095
|
+
pathParams: { id: runId },
|
|
1096
|
+
...options
|
|
1097
|
+
}
|
|
1098
|
+
);
|
|
1099
|
+
return screenshots.testsRuns;
|
|
1100
|
+
};
|
|
1101
|
+
/**
|
|
1102
|
+
* Download the combined JUnit XML report for a suite run, aggregating all
|
|
1103
|
+
* child test runs.
|
|
1104
|
+
*
|
|
1105
|
+
* @returns The JUnit report body as a string.
|
|
1106
|
+
*/
|
|
1107
|
+
downloadRunJunitReport = async (runId, options = {}) => this.#client.getText("/suiteruns/{id}/report/junit/", {
|
|
1108
|
+
pathParams: { id: runId },
|
|
1109
|
+
...options
|
|
1110
|
+
});
|
|
1111
|
+
/**
|
|
1112
|
+
* List artifacts attached to a suite run. The current public API exposes the
|
|
1113
|
+
* combined JUnit report artifact here.
|
|
1114
|
+
*/
|
|
1115
|
+
listRunDebugArtifacts = async (runId, options = {}) => {
|
|
1116
|
+
const { signal, timeout, headers } = options;
|
|
1117
|
+
return this.#client.get("/suiteruns/{id}/debug-artifacts/", {
|
|
1118
|
+
pathParams: { id: runId },
|
|
1119
|
+
signal,
|
|
1120
|
+
timeout,
|
|
1121
|
+
headers
|
|
1122
|
+
});
|
|
1123
|
+
};
|
|
1124
|
+
/**
|
|
1125
|
+
* Poll a suite run's lightweight `/status/` endpoint until it reaches a
|
|
1126
|
+
* terminal status, then return the full `SuiteRun` once.
|
|
1127
|
+
*
|
|
1128
|
+
* Transient network errors are retried; non-network errors propagate.
|
|
1129
|
+
*
|
|
1130
|
+
* @param runId - UUID of the suite run.
|
|
1131
|
+
* @param onProgress - Invoked on every successful poll with the latest `SuiteRunState`
|
|
1132
|
+
* (including the terminal one). Receives `finishedTests`/`totalTests`/`webappUrl`
|
|
1133
|
+
* for in-flight progress UI.
|
|
1134
|
+
* @param options.pollInterval - Poll interval in ms (default 2000).
|
|
1135
|
+
* @param options.timeout - Wait timeout in ms (default 600_000 = 10 minutes; suites typically take longer than single tests).
|
|
1136
|
+
* @throws {TimeoutError} When the run does not complete in time.
|
|
1137
|
+
*/
|
|
1138
|
+
watchRunProgress = async (runId, onProgress, options = {}) => {
|
|
1139
|
+
const { pollInterval, timeout, ...requestOptions } = options;
|
|
1140
|
+
await pollRunUntilComplete(
|
|
1141
|
+
() => this.getRunProgress(runId, requestOptions),
|
|
1142
|
+
(status) => isCompletedStatus(status.status),
|
|
1143
|
+
{
|
|
1144
|
+
pollInterval,
|
|
1145
|
+
timeout,
|
|
1146
|
+
onProgress,
|
|
1147
|
+
defaultTimeout: 6e5,
|
|
1148
|
+
// 10 minutes (suites typically take longer)
|
|
1149
|
+
timeoutMessage: (t) => `Suite run ${runId} did not complete within ${t}ms`
|
|
1150
|
+
}
|
|
1151
|
+
);
|
|
1152
|
+
return this.getRun(runId, requestOptions);
|
|
1153
|
+
};
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
// src/modules/tests/tests.utils.ts
|
|
1157
|
+
function buildTestImportFormData(payload, fmt) {
|
|
1158
|
+
const form = new FormData();
|
|
1159
|
+
const filename = fmt === "yaml" ? "test.yaml" : "test.zip";
|
|
1160
|
+
const mime = fmt === "yaml" ? "application/x-yaml" : "application/zip";
|
|
1161
|
+
const blob = typeof payload === "string" ? new Blob([payload], { type: mime }) : new Blob([payload], { type: mime });
|
|
1162
|
+
form.append("file", blob, filename);
|
|
1163
|
+
return form;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// src/modules/tests/tests.ts
|
|
1167
|
+
var TestsModule = class {
|
|
1168
|
+
#client;
|
|
1169
|
+
#profiles;
|
|
1170
|
+
constructor(client) {
|
|
1171
|
+
this.#client = client;
|
|
1172
|
+
this.#profiles = new ProfilesModule(client);
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* List tests with pagination, search, and ordering.
|
|
1176
|
+
*
|
|
1177
|
+
* @param options.cursor - Opaque cursor returned as `nextToken` by the previous page.
|
|
1178
|
+
* @param options.pageSize - Number of items per page.
|
|
1179
|
+
* @param options.query - Search query matched against test name.
|
|
1180
|
+
* @param options.ordering - Sort key; prefix with `-` for descending.
|
|
1181
|
+
* @returns Paginated `TestsListResponse` envelope (validated via Zod).
|
|
1182
|
+
* @throws {UnexpectedResponseError} When the server response fails Zod validation.
|
|
1183
|
+
*/
|
|
1184
|
+
list = async (options = {}) => {
|
|
1185
|
+
const { cursor, pageSize, query: searchQuery, ordering, ...requestOptions } = options;
|
|
1186
|
+
return this.#client.get("/tests/", {
|
|
1187
|
+
queryParams: {
|
|
1188
|
+
cursor,
|
|
1189
|
+
page_size: pageSize,
|
|
1190
|
+
query: searchQuery,
|
|
1191
|
+
ordering
|
|
1192
|
+
},
|
|
1193
|
+
...requestOptions
|
|
1194
|
+
});
|
|
1195
|
+
};
|
|
1196
|
+
/**
|
|
1197
|
+
* Fetch every test across all pages.
|
|
1198
|
+
*/
|
|
1199
|
+
getAll = async (options = {}) => getAllPages((pageOptions) => this.list(pageOptions), options);
|
|
1200
|
+
/**
|
|
1201
|
+
* Fetch a single test by ID.
|
|
1202
|
+
*
|
|
1203
|
+
* @param testId - UUID of the test.
|
|
1204
|
+
* @returns The full `Test` resource.
|
|
1205
|
+
* @throws {NotFoundError} When no test matches `testId`.
|
|
1206
|
+
*/
|
|
1207
|
+
get = async (testId, options = {}) => this.#client.get("/tests/{id}/", {
|
|
1208
|
+
pathParams: { id: testId },
|
|
1209
|
+
...options
|
|
1210
|
+
});
|
|
1211
|
+
/**
|
|
1212
|
+
* Create a new empty test.
|
|
1213
|
+
*
|
|
1214
|
+
* @param data - Test creation payload (at minimum, `name` and `screenSizeType`).
|
|
1215
|
+
* @returns The newly-created `Test`.
|
|
1216
|
+
* @throws {ValidationError} On invalid payload (DRF field errors on `error.fields`).
|
|
1217
|
+
*/
|
|
1218
|
+
create = async (data, options = {}) => this.#client.post("/tests/", {
|
|
1219
|
+
body: data,
|
|
1220
|
+
...options
|
|
1221
|
+
});
|
|
1222
|
+
/**
|
|
1223
|
+
* Update a test. The tests endpoint accepts partial payloads via PATCH, so
|
|
1224
|
+
* this and {@link partialUpdate} share the same semantics.
|
|
1225
|
+
*
|
|
1226
|
+
* @param testId - UUID of the test to update.
|
|
1227
|
+
* @param data - Fields to patch.
|
|
1228
|
+
* @throws {NotFoundError} When the test does not exist.
|
|
1229
|
+
* @throws {ValidationError} When the payload fails server-side validation.
|
|
1230
|
+
*/
|
|
1231
|
+
update = async (testId, data, options = {}) => this.#client.patch("/tests/{id}/", {
|
|
1232
|
+
pathParams: { id: testId },
|
|
1233
|
+
body: data,
|
|
1234
|
+
...options
|
|
1235
|
+
});
|
|
1236
|
+
/** Alias for {@link update}. The tests endpoint already uses PATCH semantics. */
|
|
1237
|
+
partialUpdate = async (testId, data, options = {}) => this.update(testId, data, options);
|
|
1238
|
+
/**
|
|
1239
|
+
* Delete a test by ID.
|
|
1240
|
+
*
|
|
1241
|
+
* @throws {NotFoundError} When the test does not exist.
|
|
1242
|
+
*/
|
|
1243
|
+
delete = async (testId, options = {}) => this.#client.delete("/tests/{id}/", {
|
|
1244
|
+
pathParams: { id: testId },
|
|
1245
|
+
...options
|
|
1246
|
+
});
|
|
1247
|
+
/**
|
|
1248
|
+
* Link an existing component/group into a test at an optional position.
|
|
1249
|
+
*
|
|
1250
|
+
* @param testId - UUID of the test to insert into.
|
|
1251
|
+
* @param data - `{ groupId, atIndex? }` insertion descriptor.
|
|
1252
|
+
* @returns Insert response with the resulting step layout.
|
|
1253
|
+
*/
|
|
1254
|
+
linkComponent = async (testId, data, options = {}) => this.#client.post("/tests/{id}/insert-component/", {
|
|
1255
|
+
pathParams: { id: testId },
|
|
1256
|
+
body: data,
|
|
1257
|
+
...options
|
|
1258
|
+
});
|
|
1259
|
+
/**
|
|
1260
|
+
* Unlink a component/group from a test.
|
|
1261
|
+
*
|
|
1262
|
+
* @param testId - UUID of the test to unlink from.
|
|
1263
|
+
* @param data - Unlink descriptor identifying which component to remove.
|
|
1264
|
+
*/
|
|
1265
|
+
unlinkComponent = async (testId, data, options = {}) => this.#client.post("/tests/{id}/unlink-component/", {
|
|
1266
|
+
pathParams: { id: testId },
|
|
1267
|
+
body: data,
|
|
1268
|
+
...options
|
|
1269
|
+
});
|
|
1270
|
+
/**
|
|
1271
|
+
* Run a test by ID
|
|
1272
|
+
*/
|
|
1273
|
+
#run = async (testId, options = {}) => {
|
|
1274
|
+
const { profileName, variables, ...requestOptions } = options;
|
|
1275
|
+
const runProfileId = profileName ? await this.#profiles.resolveId(profileName, requestOptions) : void 0;
|
|
1276
|
+
return this.#client.post("/testruns/", {
|
|
1277
|
+
body: {
|
|
1278
|
+
testId,
|
|
1279
|
+
runProfileId,
|
|
1280
|
+
variables: variables ?? [],
|
|
1281
|
+
triggeredBy: this.#client.getConfig().triggeredBy
|
|
1282
|
+
},
|
|
1283
|
+
...requestOptions
|
|
1284
|
+
});
|
|
1285
|
+
};
|
|
1286
|
+
/**
|
|
1287
|
+
* Run a test by name (finds test by name first)
|
|
1288
|
+
*/
|
|
1289
|
+
#runByName = async (testName, options = {}) => {
|
|
1290
|
+
const test = await this.#findByName(testName, options);
|
|
1291
|
+
if (!test) {
|
|
1292
|
+
throw new NotFoundError(`Test '${testName}' not found`);
|
|
1293
|
+
}
|
|
1294
|
+
return this.#run(test.id, options);
|
|
1295
|
+
};
|
|
1296
|
+
#findByName = async (testName, options = {}) => {
|
|
1297
|
+
let cursor;
|
|
1298
|
+
const pageSize = 100;
|
|
1299
|
+
while (true) {
|
|
1300
|
+
const response = await this.list({ ...options, query: testName, cursor, pageSize });
|
|
1301
|
+
const results = response.results ?? [];
|
|
1302
|
+
const exactMatch = results.find((testSummary) => testSummary.name === testName);
|
|
1303
|
+
if (exactMatch) return exactMatch;
|
|
1304
|
+
cursor = response.nextToken ?? void 0;
|
|
1305
|
+
if (!cursor) {
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
async startRun(testNameOrId, options = {}) {
|
|
1311
|
+
const { watchProgress, pollInterval, timeout, onProgress, ...runOptions } = options;
|
|
1312
|
+
const callRun = isUuid(testNameOrId) ? this.#run : this.#runByName;
|
|
1313
|
+
const runResult = await callRun(testNameOrId, runOptions);
|
|
1314
|
+
if (!watchProgress) return runResult;
|
|
1315
|
+
return this.watchRunProgress(runResult.id, onProgress ?? (() => {
|
|
1316
|
+
}), {
|
|
1317
|
+
pollInterval,
|
|
1318
|
+
timeout
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Fetch full test-run details including step runs.
|
|
1323
|
+
*
|
|
1324
|
+
* @param runId - UUID of the test run.
|
|
1325
|
+
* @throws {NotFoundError} When the run does not exist.
|
|
1326
|
+
*/
|
|
1327
|
+
getRun = async (runId, options = {}) => this.#client.get("/testruns/{id}/", {
|
|
1328
|
+
pathParams: { id: runId },
|
|
1329
|
+
...options
|
|
1330
|
+
});
|
|
1331
|
+
/**
|
|
1332
|
+
* Fetch only the lightweight status of a test run (no step detail).
|
|
1333
|
+
* Prefer this over {@link getRun} when polling.
|
|
1334
|
+
*/
|
|
1335
|
+
getRunProgress = async (runId, options = {}) => this.#client.get("/testruns/{id}/status/", {
|
|
1336
|
+
pathParams: { id: runId },
|
|
1337
|
+
...options
|
|
1338
|
+
});
|
|
1339
|
+
/**
|
|
1340
|
+
* Request cancellation of an in-progress test run.
|
|
1341
|
+
*
|
|
1342
|
+
* @param runId - UUID of the run to stop.
|
|
1343
|
+
* @returns The updated status; the run may still take a moment to settle.
|
|
1344
|
+
*/
|
|
1345
|
+
stopRun = async (runId, options = {}) => this.#client.post("/testruns/{id}/stop/", {
|
|
1346
|
+
pathParams: { id: runId },
|
|
1347
|
+
...options
|
|
1348
|
+
});
|
|
1349
|
+
/**
|
|
1350
|
+
* Fetch step-level screenshots captured during a run.
|
|
1351
|
+
*
|
|
1352
|
+
* @returns Per-step-run screenshot entries; empty array when none available.
|
|
1353
|
+
*/
|
|
1354
|
+
getRunScreenshots = async (runId, options = {}) => {
|
|
1355
|
+
const screenshots = await this.#client.get(
|
|
1356
|
+
"/testruns/{id}/screenshots/",
|
|
1357
|
+
{
|
|
1358
|
+
pathParams: { id: runId },
|
|
1359
|
+
...options
|
|
1360
|
+
}
|
|
1361
|
+
);
|
|
1362
|
+
return screenshots.stepsRuns || [];
|
|
1363
|
+
};
|
|
1364
|
+
/**
|
|
1365
|
+
* List test runs filtered by status, time window, and optional test ID.
|
|
1366
|
+
*
|
|
1367
|
+
* @param options.testId - When set, restricts to runs of that test (UUID).
|
|
1368
|
+
* @param options.startedAfter - ISO timestamp; only runs started after this point.
|
|
1369
|
+
* @param options.startedBefore - ISO timestamp; only runs started before this point.
|
|
1370
|
+
* @param options.status - Filter by `TestRunState['status']`.
|
|
1371
|
+
* @returns Paginated `TestRunsListResponse`.
|
|
1372
|
+
*/
|
|
1373
|
+
listRuns = async (options = {}) => {
|
|
1374
|
+
const {
|
|
1375
|
+
cursor,
|
|
1376
|
+
pageSize,
|
|
1377
|
+
ordering,
|
|
1378
|
+
startedAfter,
|
|
1379
|
+
startedBefore,
|
|
1380
|
+
status,
|
|
1381
|
+
testId,
|
|
1382
|
+
...requestOptions
|
|
1383
|
+
} = options;
|
|
1384
|
+
return this.#client.get("/testruns/", {
|
|
1385
|
+
queryParams: {
|
|
1386
|
+
cursor,
|
|
1387
|
+
page_size: pageSize,
|
|
1388
|
+
ordering,
|
|
1389
|
+
started_after: startedAfter,
|
|
1390
|
+
started_before: startedBefore,
|
|
1391
|
+
status,
|
|
1392
|
+
testId
|
|
1393
|
+
},
|
|
1394
|
+
...requestOptions
|
|
1395
|
+
});
|
|
1396
|
+
};
|
|
1397
|
+
/**
|
|
1398
|
+
* Move a step to a new position within a test's step list.
|
|
1399
|
+
*
|
|
1400
|
+
* @param testId - UUID of the test to reorder.
|
|
1401
|
+
* @param data - `{ stepId, atIndex }` move descriptor.
|
|
1402
|
+
* @returns The updated `Test` with the new step ordering.
|
|
1403
|
+
*/
|
|
1404
|
+
updateStepPosition = async (testId, data, options = {}) => this.#client.postNoContent("/tests/{id}/update-step-position/", {
|
|
1405
|
+
pathParams: { id: testId },
|
|
1406
|
+
body: data,
|
|
1407
|
+
...options
|
|
1408
|
+
});
|
|
1409
|
+
/**
|
|
1410
|
+
* Convenience wrapper around {@link listRuns} that returns runs started in
|
|
1411
|
+
* the last N hours, newest first by default.
|
|
1412
|
+
*
|
|
1413
|
+
* @param options.hours - Look-back window in hours (default 24).
|
|
1414
|
+
* @param options.pageSize - Max runs per page (default 50).
|
|
1415
|
+
* @param options.ordering - Sort order; defaults to most-recent first.
|
|
1416
|
+
*/
|
|
1417
|
+
getRecentRuns = async (options = {}) => {
|
|
1418
|
+
const { hours = 24, pageSize = 50, ordering = "-started", ...requestOptions } = options;
|
|
1419
|
+
const startedAfter = hoursAgo(hours);
|
|
1420
|
+
return this.listRuns({
|
|
1421
|
+
pageSize,
|
|
1422
|
+
ordering,
|
|
1423
|
+
startedAfter,
|
|
1424
|
+
...requestOptions
|
|
1425
|
+
});
|
|
1426
|
+
};
|
|
1427
|
+
/**
|
|
1428
|
+
* Download the JUnit XML report for a test run. Useful for CI integrations
|
|
1429
|
+
* that consume JUnit (e.g. Jenkins, GitLab CI, GitHub Actions test reporters).
|
|
1430
|
+
*
|
|
1431
|
+
* @returns The JUnit report body as a string.
|
|
1432
|
+
*/
|
|
1433
|
+
downloadRunJunitReport = async (runId, options = {}) => this.#client.getText("/testruns/{id}/report/junit/", {
|
|
1434
|
+
pathParams: { id: runId },
|
|
1435
|
+
...options
|
|
1436
|
+
});
|
|
1437
|
+
/**
|
|
1438
|
+
* Fetch test-run log output. The server may return either log content
|
|
1439
|
+
* inline or a redirect/URL string — both are returned verbatim as text.
|
|
1440
|
+
*/
|
|
1441
|
+
getRunLogs = async (runId, options = {}) => this.#client.getText("/testruns/{id}/logs/", {
|
|
1442
|
+
pathParams: { id: runId },
|
|
1443
|
+
...options
|
|
1444
|
+
});
|
|
1445
|
+
/**
|
|
1446
|
+
* Poll a test run's lightweight `/status/` endpoint until it reaches a
|
|
1447
|
+
* terminal status.
|
|
1448
|
+
*
|
|
1449
|
+
* Transient network errors are retried; non-network errors propagate.
|
|
1450
|
+
*
|
|
1451
|
+
* @param runId - UUID of the test run to watch.
|
|
1452
|
+
* @param onProgress - Invoked on every successful poll with the latest `TestRunState`
|
|
1453
|
+
* (including the terminal one). Receives `finishedSteps`/`totalSteps`/`webappUrl`
|
|
1454
|
+
* for in-flight progress UI.
|
|
1455
|
+
* @param options.pollInterval - Poll interval in ms (default 2000).
|
|
1456
|
+
* @param options.timeout - Wait timeout in ms (default 300_000 = 5 minutes).
|
|
1457
|
+
* @throws {TimeoutError} When the run does not complete within the timeout.
|
|
1458
|
+
*
|
|
1459
|
+
* @example
|
|
1460
|
+
* ```typescript
|
|
1461
|
+
* const run = await sdk.tests.watchRunProgress(
|
|
1462
|
+
* runId,
|
|
1463
|
+
* (s) => console.log(`${s.status} ${s.finishedSteps}/${s.totalSteps}`),
|
|
1464
|
+
* { pollInterval: 3000, timeout: 600_000 },
|
|
1465
|
+
* );
|
|
1466
|
+
* ```
|
|
1467
|
+
*/
|
|
1468
|
+
watchRunProgress = async (runId, onProgress, options = {}) => {
|
|
1469
|
+
const { pollInterval, timeout, ...requestOptions } = options;
|
|
1470
|
+
await pollRunUntilComplete(
|
|
1471
|
+
() => this.getRunProgress(runId, requestOptions),
|
|
1472
|
+
(status) => isCompletedStatus(status.status),
|
|
1473
|
+
{
|
|
1474
|
+
pollInterval,
|
|
1475
|
+
timeout,
|
|
1476
|
+
onProgress,
|
|
1477
|
+
defaultTimeout: 3e5,
|
|
1478
|
+
// 5 minutes
|
|
1479
|
+
timeoutMessage: (t) => `Test run ${runId} did not complete within ${t}ms`
|
|
1480
|
+
}
|
|
1481
|
+
);
|
|
1482
|
+
return this.getRun(runId, requestOptions);
|
|
1483
|
+
};
|
|
1484
|
+
/**
|
|
1485
|
+
* List debug artifacts (HARs, traces, etc.) produced by a test run.
|
|
1486
|
+
*/
|
|
1487
|
+
listRunDebugArtifacts = async (runId, options = {}) => {
|
|
1488
|
+
const { signal, timeout, headers } = options;
|
|
1489
|
+
return this.#client.get("/testruns/{id}/debug-artifacts/", {
|
|
1490
|
+
pathParams: { id: runId },
|
|
1491
|
+
signal,
|
|
1492
|
+
timeout,
|
|
1493
|
+
headers
|
|
1494
|
+
});
|
|
1495
|
+
};
|
|
1496
|
+
/**
|
|
1497
|
+
* Fetch a single debug artifact (e.g. HAR, network trace) for a run.
|
|
1498
|
+
*
|
|
1499
|
+
* @param runId - UUID of the run.
|
|
1500
|
+
* @param artifactType - Artifact discriminator (e.g. `'har'`).
|
|
1501
|
+
*/
|
|
1502
|
+
getRunDebugArtifact = async (runId, artifactType, options = {}) => this.#client.get("/testruns/{id}/debug-artifacts/{artifact_type}/", {
|
|
1503
|
+
pathParams: { id: runId, artifact_type: artifactType },
|
|
1504
|
+
...options
|
|
1505
|
+
});
|
|
1506
|
+
export(testId, fmt, options = {}) {
|
|
1507
|
+
if (fmt === "yaml") {
|
|
1508
|
+
return this.#client.getText("/tests/{id}/export/{fmt}/", {
|
|
1509
|
+
pathParams: { id: testId, fmt },
|
|
1510
|
+
...options
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
return this.#client.getBinary("/tests/{id}/export/{fmt}/", {
|
|
1514
|
+
pathParams: { id: testId, fmt },
|
|
1515
|
+
...options
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Import a single test from YAML (string) or ZIP (Uint8Array). Returns the
|
|
1520
|
+
* created test summary.
|
|
1521
|
+
*
|
|
1522
|
+
* @param payload - YAML text or raw ZIP bytes.
|
|
1523
|
+
* @param fmt - Format matching `payload` (`'yaml'` or `'zip'`).
|
|
1524
|
+
* @param options.conflictMode - How to resolve naming collisions on the server
|
|
1525
|
+
* (e.g. `'rename'`). Forwarded as a query string parameter.
|
|
1526
|
+
* @throws {ValidationError} When the server rejects the payload.
|
|
1527
|
+
*
|
|
1528
|
+
* @example
|
|
1529
|
+
* ```typescript
|
|
1530
|
+
* const yaml = await readFile('test.yaml', 'utf8');
|
|
1531
|
+
* const test = await sdk.tests.import(yaml, 'yaml', { conflictMode: 'rename' });
|
|
1532
|
+
* ```
|
|
1533
|
+
*/
|
|
1534
|
+
import = async (payload, fmt, options = {}) => {
|
|
1535
|
+
const { conflictMode, ...requestOptions } = options;
|
|
1536
|
+
const form = buildTestImportFormData(payload, fmt);
|
|
1537
|
+
return this.#client.postForm("/tests/import/", form, {
|
|
1538
|
+
...requestOptions,
|
|
1539
|
+
queryParams: {
|
|
1540
|
+
conflictMode
|
|
1541
|
+
}
|
|
1542
|
+
});
|
|
1543
|
+
};
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
// src/modules/variables/variables.ts
|
|
1547
|
+
var VariablesModule = class {
|
|
1548
|
+
#client;
|
|
1549
|
+
constructor(client) {
|
|
1550
|
+
this.#client = client;
|
|
1551
|
+
}
|
|
1552
|
+
list = async (options = {}) => this.#client.get("/variables/", options);
|
|
1553
|
+
get = async (key, options = {}) => this.#client.get("/variables/{key}/", {
|
|
1554
|
+
pathParams: { key },
|
|
1555
|
+
...options
|
|
1556
|
+
});
|
|
1557
|
+
create = async (options) => {
|
|
1558
|
+
const { key, value, type, isSecret, description, ...requestOptions } = options;
|
|
1559
|
+
return this.#client.post("/variables/", {
|
|
1560
|
+
body: {
|
|
1561
|
+
key,
|
|
1562
|
+
value,
|
|
1563
|
+
type,
|
|
1564
|
+
isSecret,
|
|
1565
|
+
description
|
|
1566
|
+
},
|
|
1567
|
+
...requestOptions
|
|
1568
|
+
});
|
|
1569
|
+
};
|
|
1570
|
+
update = async (key, options) => {
|
|
1571
|
+
const { value, type, isSecret, description, ...requestOptions } = options;
|
|
1572
|
+
return this.#client.patch("/variables/{key}/", {
|
|
1573
|
+
pathParams: { key },
|
|
1574
|
+
body: {
|
|
1575
|
+
value,
|
|
1576
|
+
type,
|
|
1577
|
+
isSecret,
|
|
1578
|
+
description
|
|
1579
|
+
},
|
|
1580
|
+
...requestOptions
|
|
1581
|
+
});
|
|
1582
|
+
};
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1585
|
+
// src/modules/visualRegression/visualRegression.ts
|
|
1586
|
+
var VisualRegressionModule = class {
|
|
1587
|
+
#client;
|
|
1588
|
+
constructor(client) {
|
|
1589
|
+
this.#client = client;
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* List reference screenshots for a given step.
|
|
1593
|
+
*
|
|
1594
|
+
* @param options.stepId - UUID of the visual-regression step (required).
|
|
1595
|
+
* @param options.cursor - Opaque cursor returned as `nextToken` by the previous page.
|
|
1596
|
+
* @param options.pageSize - Items per page.
|
|
1597
|
+
*/
|
|
1598
|
+
listReferenceScreenshots = async (options) => {
|
|
1599
|
+
const { stepId, cursor, pageSize, ...requestOptions } = options;
|
|
1600
|
+
return this.#client.get(
|
|
1601
|
+
"/visual-regression/reference-screenshots/",
|
|
1602
|
+
{
|
|
1603
|
+
queryParams: { step_id: stepId, cursor, page_size: pageSize },
|
|
1604
|
+
...requestOptions
|
|
1605
|
+
}
|
|
1606
|
+
);
|
|
1607
|
+
};
|
|
1608
|
+
/**
|
|
1609
|
+
* Fetch a single reference screenshot.
|
|
1610
|
+
*
|
|
1611
|
+
* @throws {NotFoundError} When no reference screenshot matches `id`.
|
|
1612
|
+
*/
|
|
1613
|
+
getReferenceScreenshot = async (id, options = {}) => this.#client.get(
|
|
1614
|
+
"/visual-regression/reference-screenshots/{id}/",
|
|
1615
|
+
{ pathParams: { id }, ...options }
|
|
1616
|
+
);
|
|
1617
|
+
/**
|
|
1618
|
+
* Create a new reference screenshot for a visual-regression step.
|
|
1619
|
+
*
|
|
1620
|
+
* @throws {ValidationError} On invalid payload.
|
|
1621
|
+
*/
|
|
1622
|
+
createReferenceScreenshot = async (data, options = {}) => this.#client.post("/visual-regression/reference-screenshots/", {
|
|
1623
|
+
body: data,
|
|
1624
|
+
...options
|
|
1625
|
+
});
|
|
1626
|
+
/**
|
|
1627
|
+
* Patch a reference screenshot.
|
|
1628
|
+
*
|
|
1629
|
+
* @throws {NotFoundError} When the reference screenshot does not exist.
|
|
1630
|
+
* @throws {ValidationError} On invalid payload.
|
|
1631
|
+
*/
|
|
1632
|
+
updateReferenceScreenshot = async (id, data, options = {}) => this.#client.patch(
|
|
1633
|
+
"/visual-regression/reference-screenshots/{id}/",
|
|
1634
|
+
{ pathParams: { id }, body: data, ...options }
|
|
1635
|
+
);
|
|
1636
|
+
/**
|
|
1637
|
+
* Delete a reference screenshot.
|
|
1638
|
+
*
|
|
1639
|
+
* @throws {NotFoundError} When the reference screenshot does not exist.
|
|
1640
|
+
*/
|
|
1641
|
+
deleteReferenceScreenshot = async (id, options = {}) => this.#client.delete("/visual-regression/reference-screenshots/{id}/", {
|
|
1642
|
+
pathParams: { id },
|
|
1643
|
+
...options
|
|
1644
|
+
});
|
|
1645
|
+
/**
|
|
1646
|
+
* Resolve failed visual-regression checks.
|
|
1647
|
+
*
|
|
1648
|
+
* @param options.type - `set_new_reference` accepts the observed screenshot as the new
|
|
1649
|
+
* baseline; `set_new_max_difference` raises the step's allowed difference to just above
|
|
1650
|
+
* the observed value. The value is computed server-side and cannot be supplied.
|
|
1651
|
+
* @param options.stepRunId - Resolve a single step run.
|
|
1652
|
+
* @param options.testRunId - Resolve every failed visual-regression step in a test run.
|
|
1653
|
+
*
|
|
1654
|
+
* Provide exactly one of `stepRunId` or `testRunId`.
|
|
1655
|
+
*/
|
|
1656
|
+
resolveReview = async (options) => {
|
|
1657
|
+
const { type, testRunId, stepRunId, ...requestOptions } = options;
|
|
1658
|
+
return this.#client.post("/visual-regression/resolve/", {
|
|
1659
|
+
body: {
|
|
1660
|
+
type,
|
|
1661
|
+
...stepRunId !== void 0 ? { stepRunId } : {},
|
|
1662
|
+
...testRunId !== void 0 ? { testRunId } : {}
|
|
1663
|
+
},
|
|
1664
|
+
...requestOptions
|
|
1665
|
+
});
|
|
1666
|
+
};
|
|
1667
|
+
};
|
|
1668
|
+
|
|
1669
|
+
// ../core/dist/utils/envConfig.js
|
|
1670
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
1671
|
+
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
1672
|
+
import { parse as parseDotenv } from "dotenv";
|
|
1673
|
+
|
|
1674
|
+
// ../core/dist/constants/defaults.js
|
|
1675
|
+
var DEFAULT_CONFIG_FILE_NAME = "bugbug.yaml";
|
|
1676
|
+
var DEFAULT_API_URL = "https://app.bugbug.io/api/v2";
|
|
1677
|
+
var DEFAULT_YAML_SCHEMA_URL = `${DEFAULT_API_URL}/schema/yaml/v1/`;
|
|
1678
|
+
var DEFAULT_MCP_URL = "https://mcp.bugbug.io";
|
|
1679
|
+
var DEFAULT_MCP_ACCESS_PATH = "/mcp";
|
|
1680
|
+
var DEFAULT_MCP_ACCESS_URL = `${DEFAULT_MCP_URL}${DEFAULT_MCP_ACCESS_PATH}`;
|
|
1681
|
+
var DOCS_BASE_URL = "https://docs.bugbug.io";
|
|
1682
|
+
var DOCS_ASK_URL = `${DOCS_BASE_URL}/master.md`;
|
|
1683
|
+
var CREDENTIALS_DIR_NAME = ".bugbug";
|
|
1684
|
+
var GLOBAL_CONFIG_FILE_NAME = "config.yaml";
|
|
1685
|
+
var DEFAULT_ROOT_CONFIG_FILE_NAME = `~/${CREDENTIALS_DIR_NAME}/${GLOBAL_CONFIG_FILE_NAME}`;
|
|
1686
|
+
var DEFAULT_ROOT_CONFIG_TEMP = `~/${CREDENTIALS_DIR_NAME}/tmp`;
|
|
1687
|
+
|
|
1688
|
+
// ../core/dist/utils/globalConfigFile.js
|
|
1689
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1690
|
+
import { homedir } from "os";
|
|
1691
|
+
import { dirname, resolve } from "path";
|
|
1692
|
+
|
|
1693
|
+
// ../core/dist/utils/yaml.js
|
|
1694
|
+
import { parse, stringify } from "yaml";
|
|
1695
|
+
|
|
1696
|
+
// ../core/dist/utils/globalConfigFile.js
|
|
1697
|
+
var globalConfigFilePath = (home = homedir()) => resolve(home, CREDENTIALS_DIR_NAME, GLOBAL_CONFIG_FILE_NAME);
|
|
1698
|
+
var readGlobalConfig = (home = homedir()) => {
|
|
1699
|
+
const path = globalConfigFilePath(home);
|
|
1700
|
+
if (!existsSync(path))
|
|
1701
|
+
return {};
|
|
1702
|
+
try {
|
|
1703
|
+
const parsed = parse(readFileSync(path, "utf8"));
|
|
1704
|
+
if (!parsed || typeof parsed !== "object")
|
|
1705
|
+
return {};
|
|
1706
|
+
return extractKnownFields(parsed);
|
|
1707
|
+
} catch {
|
|
1708
|
+
return {};
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
var resolveTokenForProject = (projectId, home = homedir()) => readGlobalConfig(home).credentials?.[projectId]?.token;
|
|
1712
|
+
var getUserToken = (home = homedir()) => readGlobalConfig(home).userToken;
|
|
1713
|
+
var getCurrentProject = (home = homedir()) => readGlobalConfig(home).currentProject;
|
|
1714
|
+
var extractKnownFields = (obj) => {
|
|
1715
|
+
const out = {};
|
|
1716
|
+
if (obj.credentials && typeof obj.credentials === "object") {
|
|
1717
|
+
const creds = {};
|
|
1718
|
+
for (const [key, raw] of Object.entries(obj.credentials)) {
|
|
1719
|
+
const entry = toCredentialEntry(key, raw);
|
|
1720
|
+
if (entry)
|
|
1721
|
+
creds[key] = entry;
|
|
1722
|
+
}
|
|
1723
|
+
out.credentials = creds;
|
|
1724
|
+
}
|
|
1725
|
+
if (typeof obj.userToken === "string" && obj.userToken.length > 0)
|
|
1726
|
+
out.userToken = obj.userToken;
|
|
1727
|
+
if (typeof obj.currentProject === "string" && obj.currentProject.length > 0) {
|
|
1728
|
+
out.currentProject = obj.currentProject;
|
|
1729
|
+
}
|
|
1730
|
+
return out;
|
|
1731
|
+
};
|
|
1732
|
+
var toCredentialEntry = (key, raw) => {
|
|
1733
|
+
if (!raw || typeof raw !== "object")
|
|
1734
|
+
return void 0;
|
|
1735
|
+
const r = raw;
|
|
1736
|
+
const id = typeof r.id === "string" && r.id.length > 0 ? r.id : key;
|
|
1737
|
+
if (typeof r.token !== "string" || r.token.length === 0)
|
|
1738
|
+
return void 0;
|
|
1739
|
+
return { id, token: r.token };
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
// ../core/dist/utils/localConfigFile.js
|
|
1743
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1744
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
1745
|
+
|
|
1746
|
+
// ../core/dist/utils/userCwd.js
|
|
1747
|
+
var userCwd = () => process.env.INIT_CWD ?? process.cwd();
|
|
1748
|
+
|
|
1749
|
+
// ../core/dist/utils/localConfigFile.js
|
|
1750
|
+
var localConfigFilePath = (cwd = userCwd()) => resolve2(cwd, DEFAULT_CONFIG_FILE_NAME);
|
|
1751
|
+
var findLocalConfigFile = (cwd = userCwd()) => {
|
|
1752
|
+
let dir = resolve2(cwd);
|
|
1753
|
+
while (true) {
|
|
1754
|
+
const candidate = localConfigFilePath(dir);
|
|
1755
|
+
if (existsSync2(candidate))
|
|
1756
|
+
return candidate;
|
|
1757
|
+
const parent = dirname2(dir);
|
|
1758
|
+
if (parent === dir)
|
|
1759
|
+
return void 0;
|
|
1760
|
+
dir = parent;
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1763
|
+
var readLocalConfigFile = (cwd = userCwd()) => {
|
|
1764
|
+
const path = findLocalConfigFile(cwd);
|
|
1765
|
+
if (!path)
|
|
1766
|
+
return {};
|
|
1767
|
+
const raw = readFileSync2(path, "utf8");
|
|
1768
|
+
try {
|
|
1769
|
+
const parsed = parse(raw);
|
|
1770
|
+
if (!parsed || typeof parsed !== "object")
|
|
1771
|
+
return {};
|
|
1772
|
+
return extractKnownFields2(parsed);
|
|
1773
|
+
} catch (cause) {
|
|
1774
|
+
throw new Error(`Failed to parse ${DEFAULT_CONFIG_FILE_NAME} at ${path}: ${cause.message}`);
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
var extractKnownFields2 = (obj) => {
|
|
1778
|
+
const out = {};
|
|
1779
|
+
if (typeof obj.projectId === "string")
|
|
1780
|
+
out.projectId = obj.projectId;
|
|
1781
|
+
if (typeof obj.token === "string")
|
|
1782
|
+
out.token = obj.token;
|
|
1783
|
+
if (typeof obj.apiUrl === "string")
|
|
1784
|
+
out.apiUrl = obj.apiUrl;
|
|
1785
|
+
if (typeof obj.telemetry === "boolean")
|
|
1786
|
+
out.telemetry = obj.telemetry;
|
|
1787
|
+
if (typeof obj.logLevel === "string")
|
|
1788
|
+
out.logLevel = obj.logLevel;
|
|
1789
|
+
return out;
|
|
1790
|
+
};
|
|
1791
|
+
|
|
1792
|
+
// ../core/dist/utils/envConfig.js
|
|
1793
|
+
var resolveEnvConfig = (opts = {}) => {
|
|
1794
|
+
const cwd = opts.cwd ?? userCwd();
|
|
1795
|
+
const overrides = opts.overrides ?? {};
|
|
1796
|
+
const localConfigFile = readLocalConfigFile(cwd);
|
|
1797
|
+
const envFile = readEnvFiles(cwd);
|
|
1798
|
+
const projectId = overrides.projectId ?? process.env.BUGBUG_PROJECT_ID ?? localConfigFile.projectId ?? envFile.projectId ?? getCurrentProject(opts.home);
|
|
1799
|
+
const globalToken = (projectId ? resolveTokenForProject(projectId, opts.home) : void 0) ?? getUserToken(opts.home);
|
|
1800
|
+
const logLevel = process.env.LOG_LEVEL ?? localConfigFile.logLevel ?? envFile.logLevel ?? "info";
|
|
1801
|
+
return {
|
|
1802
|
+
token: overrides.token ?? process.env.BUGBUG_API_TOKEN ?? localConfigFile.token ?? envFile.token ?? globalToken,
|
|
1803
|
+
apiUrl: overrides.apiUrl ?? process.env.BUGBUG_API_URL ?? localConfigFile.apiUrl ?? envFile.apiUrl ?? DEFAULT_API_URL,
|
|
1804
|
+
projectId,
|
|
1805
|
+
verbose: overrides.verbose ?? logLevel === "debug",
|
|
1806
|
+
nodeEnv: process.env.NODE_ENV ?? envFile.nodeEnv,
|
|
1807
|
+
sentryDsn: process.env.SENTRY_DSN ?? envFile.sentryDsn
|
|
1808
|
+
};
|
|
1809
|
+
};
|
|
1810
|
+
var readEnvFiles = (cwd) => {
|
|
1811
|
+
const currentNodeEnv = process.env.NODE_ENV;
|
|
1812
|
+
const fileNames = [".env"];
|
|
1813
|
+
if (currentNodeEnv)
|
|
1814
|
+
fileNames.push(`.env.${currentNodeEnv}`);
|
|
1815
|
+
let token;
|
|
1816
|
+
let apiUrl;
|
|
1817
|
+
let projectId;
|
|
1818
|
+
let logLevel;
|
|
1819
|
+
let nodeEnv;
|
|
1820
|
+
let sentryDsn;
|
|
1821
|
+
let dir = resolve3(cwd);
|
|
1822
|
+
while (true) {
|
|
1823
|
+
for (const name of fileNames) {
|
|
1824
|
+
const path = resolve3(dir, name);
|
|
1825
|
+
if (!existsSync3(path))
|
|
1826
|
+
continue;
|
|
1827
|
+
try {
|
|
1828
|
+
const parsed = parseDotenv(readFileSync3(path, "utf8"));
|
|
1829
|
+
token ??= typeof parsed.BUGBUG_API_TOKEN === "string" ? parsed.BUGBUG_API_TOKEN : void 0;
|
|
1830
|
+
apiUrl ??= typeof parsed.BUGBUG_API_URL === "string" ? parsed.BUGBUG_API_URL : void 0;
|
|
1831
|
+
projectId ??= typeof parsed.BUGBUG_PROJECT_ID === "string" ? parsed.BUGBUG_PROJECT_ID : void 0;
|
|
1832
|
+
logLevel ??= typeof parsed.BUGBUG_LOG_LEVEL === "string" ? parsed.BUGBUG_LOG_LEVEL : void 0;
|
|
1833
|
+
nodeEnv ??= typeof parsed.NODE_ENV === "string" ? parsed.NODE_ENV : void 0;
|
|
1834
|
+
sentryDsn ??= typeof parsed.SENTRY_DSN === "string" ? parsed.SENTRY_DSN : void 0;
|
|
1835
|
+
} catch {
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
if (token && apiUrl && projectId)
|
|
1839
|
+
break;
|
|
1840
|
+
const parent = dirname3(dir);
|
|
1841
|
+
if (parent === dir)
|
|
1842
|
+
break;
|
|
1843
|
+
dir = parent;
|
|
1844
|
+
}
|
|
1845
|
+
return { token, apiUrl, projectId, logLevel, nodeEnv, sentryDsn };
|
|
1846
|
+
};
|
|
1847
|
+
|
|
1848
|
+
// ../core/dist/utils/userAgent.js
|
|
1849
|
+
var buildUserAgent = (clientType, version) => `BugBug ${clientType.toUpperCase()} ${version}`;
|
|
1850
|
+
|
|
1851
|
+
// ../core/package.json
|
|
1852
|
+
var package_default = {
|
|
1853
|
+
name: "@bugbug-io/core",
|
|
1854
|
+
version: "13.39.1",
|
|
1855
|
+
private: true,
|
|
1856
|
+
type: "module",
|
|
1857
|
+
main: "dist/index.js",
|
|
1858
|
+
types: "dist/index.d.ts",
|
|
1859
|
+
exports: {
|
|
1860
|
+
".": {
|
|
1861
|
+
development: "./src/index.ts",
|
|
1862
|
+
import: "./dist/index.js",
|
|
1863
|
+
types: "./dist/index.d.ts"
|
|
1864
|
+
},
|
|
1865
|
+
"./constants/defaults": {
|
|
1866
|
+
development: "./src/constants/defaults.ts",
|
|
1867
|
+
import: "./dist/constants/defaults.js",
|
|
1868
|
+
types: "./dist/constants/defaults.d.ts"
|
|
1869
|
+
},
|
|
1870
|
+
"./constants/status": {
|
|
1871
|
+
development: "./src/constants/status.ts",
|
|
1872
|
+
import: "./dist/constants/status.js",
|
|
1873
|
+
types: "./dist/constants/status.d.ts"
|
|
1874
|
+
},
|
|
1875
|
+
"./utils/async": {
|
|
1876
|
+
development: "./src/utils/async.ts",
|
|
1877
|
+
import: "./dist/utils/async.js",
|
|
1878
|
+
types: "./dist/utils/async.d.ts"
|
|
1879
|
+
},
|
|
1880
|
+
"./utils/collections": {
|
|
1881
|
+
development: "./src/utils/collections.ts",
|
|
1882
|
+
import: "./dist/utils/collections.js",
|
|
1883
|
+
types: "./dist/utils/collections.d.ts"
|
|
1884
|
+
},
|
|
1885
|
+
"./utils/localConfigFile": {
|
|
1886
|
+
development: "./src/utils/localConfigFile.ts",
|
|
1887
|
+
import: "./dist/utils/localConfigFile.js",
|
|
1888
|
+
types: "./dist/utils/localConfigFile.d.ts"
|
|
1889
|
+
},
|
|
1890
|
+
"./utils/env": {
|
|
1891
|
+
development: "./src/utils/env.ts",
|
|
1892
|
+
import: "./dist/utils/env.js",
|
|
1893
|
+
types: "./dist/utils/env.d.ts"
|
|
1894
|
+
},
|
|
1895
|
+
"./utils/envConfig": {
|
|
1896
|
+
development: "./src/utils/envConfig.ts",
|
|
1897
|
+
import: "./dist/utils/envConfig.js",
|
|
1898
|
+
types: "./dist/utils/envConfig.d.ts"
|
|
1899
|
+
},
|
|
1900
|
+
"./utils/globalConfigFile": {
|
|
1901
|
+
development: "./src/utils/globalConfigFile.ts",
|
|
1902
|
+
import: "./dist/utils/globalConfigFile.js",
|
|
1903
|
+
types: "./dist/utils/globalConfigFile.d.ts"
|
|
1904
|
+
},
|
|
1905
|
+
"./utils/duration": {
|
|
1906
|
+
development: "./src/utils/duration.ts",
|
|
1907
|
+
import: "./dist/utils/duration.js",
|
|
1908
|
+
types: "./dist/utils/duration.d.ts"
|
|
1909
|
+
},
|
|
1910
|
+
"./utils/logger": {
|
|
1911
|
+
development: "./src/utils/logger.ts",
|
|
1912
|
+
import: "./dist/utils/logger.js",
|
|
1913
|
+
types: "./dist/utils/logger.d.ts"
|
|
1914
|
+
},
|
|
1915
|
+
"./utils/strings": {
|
|
1916
|
+
development: "./src/utils/strings.ts",
|
|
1917
|
+
import: "./dist/utils/strings.js",
|
|
1918
|
+
types: "./dist/utils/strings.d.ts"
|
|
1919
|
+
},
|
|
1920
|
+
"./utils/time": {
|
|
1921
|
+
development: "./src/utils/time.ts",
|
|
1922
|
+
import: "./dist/utils/time.js",
|
|
1923
|
+
types: "./dist/utils/time.d.ts"
|
|
1924
|
+
},
|
|
1925
|
+
"./utils/sentry": {
|
|
1926
|
+
development: "./src/utils/sentry.ts",
|
|
1927
|
+
import: "./dist/utils/sentry.js",
|
|
1928
|
+
types: "./dist/utils/sentry.d.ts"
|
|
1929
|
+
},
|
|
1930
|
+
"./utils/userAgent": {
|
|
1931
|
+
development: "./src/utils/userAgent.ts",
|
|
1932
|
+
import: "./dist/utils/userAgent.js",
|
|
1933
|
+
types: "./dist/utils/userAgent.d.ts"
|
|
1934
|
+
},
|
|
1935
|
+
"./utils/userCwd": {
|
|
1936
|
+
development: "./src/utils/userCwd.ts",
|
|
1937
|
+
import: "./dist/utils/userCwd.js",
|
|
1938
|
+
types: "./dist/utils/userCwd.d.ts"
|
|
1939
|
+
},
|
|
1940
|
+
"./utils/version": {
|
|
1941
|
+
development: "./src/utils/version.ts",
|
|
1942
|
+
import: "./dist/utils/version.js",
|
|
1943
|
+
types: "./dist/utils/version.d.ts"
|
|
1944
|
+
},
|
|
1945
|
+
"./utils/validation": {
|
|
1946
|
+
development: "./src/utils/validation.ts",
|
|
1947
|
+
import: "./dist/utils/validation.js",
|
|
1948
|
+
types: "./dist/utils/validation.d.ts"
|
|
1949
|
+
},
|
|
1950
|
+
"./utils/variables": {
|
|
1951
|
+
development: "./src/utils/variables.ts",
|
|
1952
|
+
import: "./dist/utils/variables.js",
|
|
1953
|
+
types: "./dist/utils/variables.d.ts"
|
|
1954
|
+
},
|
|
1955
|
+
"./utils/yaml": {
|
|
1956
|
+
development: "./src/utils/yaml.ts",
|
|
1957
|
+
import: "./dist/utils/yaml.js",
|
|
1958
|
+
types: "./dist/utils/yaml.d.ts"
|
|
1959
|
+
},
|
|
1960
|
+
"./services/telemetry": {
|
|
1961
|
+
development: "./src/services/telemetry/index.ts",
|
|
1962
|
+
import: "./dist/services/telemetry/index.js",
|
|
1963
|
+
types: "./dist/services/telemetry/index.d.ts"
|
|
1964
|
+
},
|
|
1965
|
+
"./services/telemetry/analytics": {
|
|
1966
|
+
development: "./src/services/telemetry/analytics/index.ts",
|
|
1967
|
+
import: "./dist/services/telemetry/analytics/index.js",
|
|
1968
|
+
types: "./dist/services/telemetry/analytics/index.d.ts"
|
|
1969
|
+
},
|
|
1970
|
+
"./services/telemetry/sentry": {
|
|
1971
|
+
development: "./src/services/telemetry/sentry.ts",
|
|
1972
|
+
import: "./dist/services/telemetry/sentry.js",
|
|
1973
|
+
types: "./dist/services/telemetry/sentry.d.ts"
|
|
1974
|
+
},
|
|
1975
|
+
"./install/install.types": {
|
|
1976
|
+
development: "./src/install/install.types.ts",
|
|
1977
|
+
import: "./dist/install/install.types.js",
|
|
1978
|
+
types: "./dist/install/install.types.d.ts"
|
|
1979
|
+
},
|
|
1980
|
+
"./install/clients": {
|
|
1981
|
+
development: "./src/install/clients.ts",
|
|
1982
|
+
import: "./dist/install/clients.js",
|
|
1983
|
+
types: "./dist/install/clients.d.ts"
|
|
1984
|
+
},
|
|
1985
|
+
"./install/installPlugin": {
|
|
1986
|
+
development: "./src/install/installPlugin.ts",
|
|
1987
|
+
import: "./dist/install/installPlugin.js",
|
|
1988
|
+
types: "./dist/install/installPlugin.d.ts"
|
|
1989
|
+
},
|
|
1990
|
+
"./install/detect": {
|
|
1991
|
+
development: "./src/install/detect.ts",
|
|
1992
|
+
import: "./dist/install/detect.js",
|
|
1993
|
+
types: "./dist/install/detect.d.ts"
|
|
1994
|
+
},
|
|
1995
|
+
"./install/installContext": {
|
|
1996
|
+
development: "./src/install/installContext.ts",
|
|
1997
|
+
import: "./dist/install/installContext.js",
|
|
1998
|
+
types: "./dist/install/installContext.d.ts"
|
|
1999
|
+
},
|
|
2000
|
+
"./install/external": {
|
|
2001
|
+
development: "./src/install/external.ts",
|
|
2002
|
+
import: "./dist/install/external.js",
|
|
2003
|
+
types: "./dist/install/external.d.ts"
|
|
2004
|
+
},
|
|
2005
|
+
"./install/jsonConfig": {
|
|
2006
|
+
development: "./src/install/jsonConfig.ts",
|
|
2007
|
+
import: "./dist/install/jsonConfig.js",
|
|
2008
|
+
types: "./dist/install/jsonConfig.d.ts"
|
|
2009
|
+
},
|
|
2010
|
+
"./install/writers": {
|
|
2011
|
+
development: "./src/install/writers.ts",
|
|
2012
|
+
import: "./dist/install/writers.js",
|
|
2013
|
+
types: "./dist/install/writers.d.ts"
|
|
2014
|
+
},
|
|
2015
|
+
"./install/skills": {
|
|
2016
|
+
development: "./src/install/skills.ts",
|
|
2017
|
+
import: "./dist/install/skills.js",
|
|
2018
|
+
types: "./dist/install/skills.d.ts"
|
|
2019
|
+
},
|
|
2020
|
+
"./install/pluginRef": {
|
|
2021
|
+
development: "./src/install/pluginRef.ts",
|
|
2022
|
+
import: "./dist/install/pluginRef.js",
|
|
2023
|
+
types: "./dist/install/pluginRef.d.ts"
|
|
2024
|
+
},
|
|
2025
|
+
"./install/pluginsCli": {
|
|
2026
|
+
development: "./src/install/pluginsCli.ts",
|
|
2027
|
+
import: "./dist/install/pluginsCli.js",
|
|
2028
|
+
types: "./dist/install/pluginsCli.d.ts"
|
|
2029
|
+
},
|
|
2030
|
+
"./install/localCatalog": {
|
|
2031
|
+
development: "./src/install/localCatalog.ts",
|
|
2032
|
+
import: "./dist/install/localCatalog.js",
|
|
2033
|
+
types: "./dist/install/localCatalog.d.ts"
|
|
2034
|
+
},
|
|
2035
|
+
"./testUtils/mockRequest": {
|
|
2036
|
+
development: "./src/testUtils/mockRequest.ts",
|
|
2037
|
+
import: "./dist/testUtils/mockRequest.js",
|
|
2038
|
+
types: "./dist/testUtils/mockRequest.d.ts"
|
|
2039
|
+
},
|
|
2040
|
+
"./package.json": "./package.json"
|
|
2041
|
+
},
|
|
2042
|
+
files: [
|
|
2043
|
+
"dist/**/*"
|
|
2044
|
+
],
|
|
2045
|
+
scripts: {
|
|
2046
|
+
build: "tsc -b --force",
|
|
2047
|
+
dev: "tsc -b --watch",
|
|
2048
|
+
test: "vitest run",
|
|
2049
|
+
"test:watch": "vitest --watch",
|
|
2050
|
+
"format:check": "oxfmt --check",
|
|
2051
|
+
"oxlint:check": "oxlint .",
|
|
2052
|
+
"oxlint:ci": "oxlint . --max-warnings=0",
|
|
2053
|
+
lint: "npm run format:check && npm run oxlint:check",
|
|
2054
|
+
"lint:fix": "oxfmt && oxlint . --fix",
|
|
2055
|
+
"lint:ci": "npm run format:check && npm run oxlint:ci",
|
|
2056
|
+
typecheck: "tsc --noEmit",
|
|
2057
|
+
clean: "rm -rf dist node_modules/.tmp/tsconfig.tsbuildinfo"
|
|
2058
|
+
},
|
|
2059
|
+
dependencies: {
|
|
2060
|
+
"@sentry/node": "^10.66.0",
|
|
2061
|
+
dotenv: "^16.6.1",
|
|
2062
|
+
yaml: "^2.8.4"
|
|
2063
|
+
},
|
|
2064
|
+
devDependencies: {
|
|
2065
|
+
"@bugbug-io/config": "*",
|
|
2066
|
+
"@bugbug-io/oxlint-config": "*",
|
|
2067
|
+
nock: "^14.0.9"
|
|
2068
|
+
},
|
|
2069
|
+
engines: {
|
|
2070
|
+
node: ">=24"
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
|
|
2074
|
+
// ../core/dist/utils/version.js
|
|
2075
|
+
var getVersion = () => package_default.version;
|
|
2076
|
+
|
|
2077
|
+
// src/services/apiClient/apiClient.ts
|
|
2078
|
+
import createClient from "openapi-fetch";
|
|
2079
|
+
|
|
2080
|
+
// src/services/rateLimiter/rateLimiter.constants.ts
|
|
2081
|
+
var RATE_LIMIT_MIN_REQUESTS = 1;
|
|
2082
|
+
var RATE_LIMIT_MAX_REQUESTS = 100;
|
|
2083
|
+
var RATE_LIMIT_MIN_WINDOW_MS = 1e3;
|
|
2084
|
+
var RATE_LIMIT_MAX_WINDOW_MS = 6e4;
|
|
2085
|
+
|
|
2086
|
+
// src/services/rateLimiter/rateLimiter.ts
|
|
2087
|
+
var RateLimiter = class {
|
|
2088
|
+
#requests = [];
|
|
2089
|
+
#config;
|
|
2090
|
+
constructor(config) {
|
|
2091
|
+
this.#warnIfUnsupportedConfig(config);
|
|
2092
|
+
this.#config = {
|
|
2093
|
+
maxRequests: config.maxRequests,
|
|
2094
|
+
windowMs: config.windowMs,
|
|
2095
|
+
retryAfter: config.retryAfter || 1e3,
|
|
2096
|
+
retryServer429: config.retryServer429 ?? true,
|
|
2097
|
+
exponentialBackoff: {
|
|
2098
|
+
baseDelay: config.exponentialBackoff.baseDelay,
|
|
2099
|
+
maxDelay: config.exponentialBackoff.maxDelay,
|
|
2100
|
+
maxRetries: config.exponentialBackoff.maxRetries
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
#warnIfUnsupportedConfig(config) {
|
|
2105
|
+
if (config.maxRequests < RATE_LIMIT_MIN_REQUESTS || config.maxRequests > RATE_LIMIT_MAX_REQUESTS) {
|
|
2106
|
+
console.warn(
|
|
2107
|
+
`BugBug SDK rateLimit.maxRequests should be between ${RATE_LIMIT_MIN_REQUESTS} and ${RATE_LIMIT_MAX_REQUESTS}.`
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
if (config.windowMs < RATE_LIMIT_MIN_WINDOW_MS || config.windowMs > RATE_LIMIT_MAX_WINDOW_MS) {
|
|
2111
|
+
console.warn(
|
|
2112
|
+
`BugBug SDK rateLimit.windowMs should be between ${RATE_LIMIT_MIN_WINDOW_MS} and ${RATE_LIMIT_MAX_WINDOW_MS}.`
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Execute a function with rate limiting and exponential backoff.
|
|
2118
|
+
*
|
|
2119
|
+
* The retry loop fires when:
|
|
2120
|
+
* 1. The **client-side** gate trips (`canMakeRequest()` returns false) — `fn`
|
|
2121
|
+
* was never called.
|
|
2122
|
+
* 2. The function throws `RateLimitError` (typically a server 429) AND
|
|
2123
|
+
* `retryServer429` is enabled. Retrying server 429s on non-idempotent
|
|
2124
|
+
* requests (POST startRun, POST startRun) can produce duplicate
|
|
2125
|
+
* resources — set `retryServer429: false` to disable.
|
|
2126
|
+
*
|
|
2127
|
+
* @param fn - The function to execute under rate limiting.
|
|
2128
|
+
* @returns The function's resolved value.
|
|
2129
|
+
* @throws {RateLimitError} When retries are exhausted or `retryServer429` is
|
|
2130
|
+
* disabled and the server returned 429.
|
|
2131
|
+
*/
|
|
2132
|
+
async execute(fn) {
|
|
2133
|
+
let lastError = null;
|
|
2134
|
+
for (let attempt = 0; attempt <= this.#config.exponentialBackoff.maxRetries; attempt++) {
|
|
2135
|
+
if (!this.#canMakeRequest()) {
|
|
2136
|
+
const waitTime = this.#getWaitTime();
|
|
2137
|
+
const gateError = new RateLimitError(
|
|
2138
|
+
waitTime,
|
|
2139
|
+
`Rate limit exceeded. Wait ${waitTime}ms before retrying.`
|
|
2140
|
+
);
|
|
2141
|
+
lastError = gateError;
|
|
2142
|
+
if (attempt === this.#config.exponentialBackoff.maxRetries) {
|
|
2143
|
+
throw gateError;
|
|
2144
|
+
}
|
|
2145
|
+
await this.#sleep(this.#computeBackoff(attempt, gateError.retryAfter));
|
|
2146
|
+
continue;
|
|
2147
|
+
}
|
|
2148
|
+
const stamp = this.#recordRequest();
|
|
2149
|
+
try {
|
|
2150
|
+
return await fn();
|
|
2151
|
+
} catch (fnError) {
|
|
2152
|
+
this.#releaseRequest(stamp);
|
|
2153
|
+
if (!(fnError instanceof RateLimitError)) {
|
|
2154
|
+
throw fnError;
|
|
2155
|
+
}
|
|
2156
|
+
lastError = fnError;
|
|
2157
|
+
if (!this.#config.retryServer429) {
|
|
2158
|
+
throw fnError;
|
|
2159
|
+
}
|
|
2160
|
+
if (attempt === this.#config.exponentialBackoff.maxRetries) {
|
|
2161
|
+
throw fnError;
|
|
2162
|
+
}
|
|
2163
|
+
await this.#sleep(this.#computeBackoff(attempt, fnError.retryAfter));
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
throw lastError || new Error("Max retries exceeded");
|
|
2167
|
+
}
|
|
2168
|
+
#computeBackoff = (attempt, retryAfter) => {
|
|
2169
|
+
const baseDelay = retryAfter || this.#config.exponentialBackoff.baseDelay;
|
|
2170
|
+
const delay = Math.min(
|
|
2171
|
+
baseDelay * Math.pow(2, attempt),
|
|
2172
|
+
this.#config.exponentialBackoff.maxDelay
|
|
2173
|
+
);
|
|
2174
|
+
const jitter = Math.random() * 0.1 * delay;
|
|
2175
|
+
return delay + jitter;
|
|
2176
|
+
};
|
|
2177
|
+
/**
|
|
2178
|
+
* Sleep utility
|
|
2179
|
+
*/
|
|
2180
|
+
#sleep(ms) {
|
|
2181
|
+
return new Promise((resolve4) => {
|
|
2182
|
+
setTimeout(resolve4, ms);
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
/**
|
|
2186
|
+
* Check if we can make a request within the rate limit
|
|
2187
|
+
*/
|
|
2188
|
+
#canMakeRequest() {
|
|
2189
|
+
this.#cleanOldRequests();
|
|
2190
|
+
return this.#requests.length < this.#config.maxRequests;
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Record a new request. Returns the timestamp pushed so callers can later
|
|
2194
|
+
* release **this specific** stamp via {@link releaseRequest}, avoiding the
|
|
2195
|
+
* pop()-the-wrong-stamp race when multiple `execute()` calls overlap.
|
|
2196
|
+
*/
|
|
2197
|
+
#recordRequest() {
|
|
2198
|
+
const stamp = Date.now();
|
|
2199
|
+
this.#requests.push(stamp);
|
|
2200
|
+
return stamp;
|
|
2201
|
+
}
|
|
2202
|
+
/**
|
|
2203
|
+
* Remove a previously-recorded stamp by identity (first occurrence). Safe to
|
|
2204
|
+
* call when the stamp has already been cleaned by the window-trim sweep —
|
|
2205
|
+
* the lookup simply finds no match and the array is left untouched.
|
|
2206
|
+
*/
|
|
2207
|
+
#releaseRequest = (stamp) => {
|
|
2208
|
+
const index = this.#requests.indexOf(stamp);
|
|
2209
|
+
if (index !== -1) {
|
|
2210
|
+
this.#requests.splice(index, 1);
|
|
2211
|
+
}
|
|
2212
|
+
};
|
|
2213
|
+
/**
|
|
2214
|
+
* Clean up old requests outside the time window
|
|
2215
|
+
*/
|
|
2216
|
+
#cleanOldRequests() {
|
|
2217
|
+
const now2 = Date.now();
|
|
2218
|
+
const cutoff = now2 - this.#config.windowMs;
|
|
2219
|
+
this.#requests = this.#requests.filter((timestamp) => timestamp > cutoff);
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Get the time to wait before the next request can be made
|
|
2223
|
+
*/
|
|
2224
|
+
#getWaitTime() {
|
|
2225
|
+
if (this.#requests.length === 0) {
|
|
2226
|
+
return 0;
|
|
2227
|
+
}
|
|
2228
|
+
const oldestRequest = Math.min(...this.#requests);
|
|
2229
|
+
const waitTime = this.#config.windowMs - (Date.now() - oldestRequest);
|
|
2230
|
+
return Math.max(0, waitTime);
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Snapshot of the current rate-limiter state.
|
|
2234
|
+
*
|
|
2235
|
+
* @returns `requestsInWindow`, `remainingRequests`, `canMakeRequest`,
|
|
2236
|
+
* `resetTime` (epoch ms when the oldest tracked request leaves the
|
|
2237
|
+
* window — `null` when no requests are tracked).
|
|
2238
|
+
*/
|
|
2239
|
+
getStatus() {
|
|
2240
|
+
this.#cleanOldRequests();
|
|
2241
|
+
return {
|
|
2242
|
+
requestsInWindow: this.#requests.length,
|
|
2243
|
+
maxRequests: this.#config.maxRequests,
|
|
2244
|
+
remainingRequests: Math.max(0, this.#config.maxRequests - this.#requests.length),
|
|
2245
|
+
windowMs: this.#config.windowMs,
|
|
2246
|
+
resetTime: this.#requests.length > 0 ? Math.min(...this.#requests) + this.#config.windowMs : null,
|
|
2247
|
+
canMakeRequest: this.#canMakeRequest()
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Discard all tracked request timestamps. Useful in tests or after a clock
|
|
2252
|
+
* jump. Not normally called in production.
|
|
2253
|
+
*/
|
|
2254
|
+
reset() {
|
|
2255
|
+
this.#requests = [];
|
|
2256
|
+
}
|
|
2257
|
+
};
|
|
2258
|
+
|
|
2259
|
+
// src/services/apiClient/apiClient.utils.ts
|
|
2260
|
+
function formatApiErrorMessage(error) {
|
|
2261
|
+
if (isHtmlErrorBody(error)) return "";
|
|
2262
|
+
return formatApiErrorValue(error) ?? "";
|
|
2263
|
+
}
|
|
2264
|
+
function isHtmlErrorBody(error) {
|
|
2265
|
+
if (typeof error !== "string") return false;
|
|
2266
|
+
const trimmed = error.trimStart().toLowerCase();
|
|
2267
|
+
return trimmed.startsWith("<!doctype html") || trimmed.startsWith("<html");
|
|
2268
|
+
}
|
|
2269
|
+
function formatNetworkErrorMessage(error) {
|
|
2270
|
+
const { cause } = error;
|
|
2271
|
+
const causeMessage = formatNetworkErrorCause(cause);
|
|
2272
|
+
return causeMessage ? `Network error: ${error.message} (${causeMessage})` : `Network error: ${error.message}`;
|
|
2273
|
+
}
|
|
2274
|
+
function formatUnexpectedResponseMessage(error, method, url) {
|
|
2275
|
+
return `Unexpected non-JSON response from ${method} ${url}: ${error.message}. Check BUGBUG_API_URL; this usually means the server returned an HTML page instead of the BugBug API.`;
|
|
2276
|
+
}
|
|
2277
|
+
function formatNetworkErrorCause(cause) {
|
|
2278
|
+
if (!cause) return void 0;
|
|
2279
|
+
if (cause instanceof Error && cause.message) return cause.message;
|
|
2280
|
+
if (typeof cause === "string") return cause;
|
|
2281
|
+
if (typeof cause !== "object") return String(cause);
|
|
2282
|
+
const obj = cause;
|
|
2283
|
+
const code = typeof obj.code === "string" ? obj.code : void 0;
|
|
2284
|
+
const address = typeof obj.address === "string" ? obj.address : void 0;
|
|
2285
|
+
const port = typeof obj.port === "number" || typeof obj.port === "string" ? String(obj.port) : void 0;
|
|
2286
|
+
if (code && address && port) return `${code} ${address}:${port}`;
|
|
2287
|
+
if (code && address) return `${code} ${address}`;
|
|
2288
|
+
if (code) return code;
|
|
2289
|
+
try {
|
|
2290
|
+
return JSON.stringify(obj);
|
|
2291
|
+
} catch {
|
|
2292
|
+
return String(cause);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
function parseErrorBody(error) {
|
|
2296
|
+
if (typeof error !== "string") return error;
|
|
2297
|
+
const trimmed = error.trim();
|
|
2298
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return void 0;
|
|
2299
|
+
try {
|
|
2300
|
+
return JSON.parse(trimmed);
|
|
2301
|
+
} catch {
|
|
2302
|
+
return void 0;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
function extractFieldErrors(body) {
|
|
2306
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return void 0;
|
|
2307
|
+
const out = {};
|
|
2308
|
+
const walk = (value, path) => {
|
|
2309
|
+
if (value === null || value === void 0) return;
|
|
2310
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
2311
|
+
if (path) (out[path] ??= []).push(String(value));
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
if (Array.isArray(value)) {
|
|
2315
|
+
const stringEntries = value.filter(
|
|
2316
|
+
(v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean"
|
|
2317
|
+
);
|
|
2318
|
+
if (stringEntries.length === value.length && path) {
|
|
2319
|
+
out[path] = [...out[path] ?? [], ...stringEntries.map(String)];
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
value.forEach((item, index) => walk(item, path ? `${path}[${index}]` : `[${index}]`));
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
if (typeof value === "object") {
|
|
2326
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
2327
|
+
if (!path && (key === "detail" || key === "message" || key === "error")) continue;
|
|
2328
|
+
walk(nested, path ? `${path}.${key}` : key);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
};
|
|
2332
|
+
walk(body, "");
|
|
2333
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
2334
|
+
}
|
|
2335
|
+
function formatApiErrorValue(value, path = "") {
|
|
2336
|
+
if (typeof value === "string") return value;
|
|
2337
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
2338
|
+
if (Array.isArray(value)) {
|
|
2339
|
+
const parts2 = value.map((item) => formatApiErrorValue(item, path)).filter((item) => Boolean(item));
|
|
2340
|
+
return parts2.length ? parts2.join(", ") : void 0;
|
|
2341
|
+
}
|
|
2342
|
+
if (!value || typeof value !== "object") return void 0;
|
|
2343
|
+
const obj = value;
|
|
2344
|
+
for (const key of ["message", "detail", "error"]) {
|
|
2345
|
+
const message = formatApiErrorValue(obj[key], path);
|
|
2346
|
+
if (message) return message;
|
|
2347
|
+
}
|
|
2348
|
+
const parts = Object.entries(obj).map(([key, nested]) => {
|
|
2349
|
+
const nestedPath = path ? `${path}.${key}` : key;
|
|
2350
|
+
const formatted = formatApiErrorValue(nested, nestedPath);
|
|
2351
|
+
if (!formatted) return void 0;
|
|
2352
|
+
return formatted.includes(":") ? formatted : `${nestedPath}: ${formatted}`;
|
|
2353
|
+
}).filter((item) => Boolean(item));
|
|
2354
|
+
return parts.length ? parts.join("; ") : void 0;
|
|
2355
|
+
}
|
|
2356
|
+
function parseJsonOrText(text) {
|
|
2357
|
+
if (!text) return void 0;
|
|
2358
|
+
try {
|
|
2359
|
+
return JSON.parse(text);
|
|
2360
|
+
} catch {
|
|
2361
|
+
return text;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// src/services/apiClient/apiClient.ts
|
|
2366
|
+
var BugBugApiClient = class {
|
|
2367
|
+
#config;
|
|
2368
|
+
#client;
|
|
2369
|
+
#rateLimiter;
|
|
2370
|
+
constructor(config) {
|
|
2371
|
+
const resolved = resolveEnvConfig({
|
|
2372
|
+
overrides: { token: config.apiToken, apiUrl: config.apiUrl, projectId: config.projectId }
|
|
2373
|
+
});
|
|
2374
|
+
this.#config = {
|
|
2375
|
+
apiToken: resolved.token ?? "",
|
|
2376
|
+
projectId: resolved.projectId ?? "",
|
|
2377
|
+
triggeredBy: config.triggeredBy || "api",
|
|
2378
|
+
apiUrl: this.#normalizeApiUrl(resolved.apiUrl),
|
|
2379
|
+
verbose: config.verbose || false,
|
|
2380
|
+
logLevel: config.logLevel || "info",
|
|
2381
|
+
timeout: config.timeout || 3e4,
|
|
2382
|
+
rateLimit: config.rateLimit || {
|
|
2383
|
+
maxRequests: 100,
|
|
2384
|
+
windowMs: 6e4,
|
|
2385
|
+
exponentialBackoff: {
|
|
2386
|
+
baseDelay: 1e3,
|
|
2387
|
+
maxDelay: 3e4,
|
|
2388
|
+
maxRetries: 3
|
|
2389
|
+
}
|
|
2390
|
+
},
|
|
2391
|
+
requestHeaders: {
|
|
2392
|
+
"User-Agent": buildUserAgent("sdk", getVersion()),
|
|
2393
|
+
...config.requestHeaders
|
|
2394
|
+
},
|
|
2395
|
+
onBeforeRequest: config.onBeforeRequest || (() => {
|
|
2396
|
+
}),
|
|
2397
|
+
onAfterRequest: config.onAfterRequest || (() => {
|
|
2398
|
+
}),
|
|
2399
|
+
onRequestError: config.onRequestError || (() => {
|
|
2400
|
+
})
|
|
2401
|
+
};
|
|
2402
|
+
this.#validateConfig();
|
|
2403
|
+
this.#client = this.#createApiClient();
|
|
2404
|
+
this.#rateLimiter = new RateLimiter(this.#config.rateLimit);
|
|
2405
|
+
}
|
|
2406
|
+
#createApiClient = () => createClient({
|
|
2407
|
+
baseUrl: this.#config.apiUrl,
|
|
2408
|
+
headers: {
|
|
2409
|
+
...this.#identityHeaders(),
|
|
2410
|
+
"Content-Type": "application/json"
|
|
2411
|
+
}
|
|
2412
|
+
});
|
|
2413
|
+
/** Auth + identification headers shared by the openapi client and raw fetch paths. */
|
|
2414
|
+
#identityHeaders = () => ({
|
|
2415
|
+
...this.#config.requestHeaders,
|
|
2416
|
+
Authorization: `Token ${this.#config.apiToken}`
|
|
2417
|
+
});
|
|
2418
|
+
/**
|
|
2419
|
+
* Project-scoping query param for organization/user tokens. Project tokens don't
|
|
2420
|
+
* need it (their project is implied), so it's only sent when a project id is
|
|
2421
|
+
* configured. The backend public API reads `projectId` from query params,
|
|
2422
|
+
* request body, or URL kwargs.
|
|
2423
|
+
*/
|
|
2424
|
+
#scopedParams = (params, options = {}) => {
|
|
2425
|
+
const { projectScoped = true } = options;
|
|
2426
|
+
if (!projectScoped || !this.#config.projectId) return params;
|
|
2427
|
+
const query = params?.query ?? {};
|
|
2428
|
+
if (query.projectId !== void 0 || query.project_id !== void 0) return params;
|
|
2429
|
+
return {
|
|
2430
|
+
...params?.path ? { path: params.path } : {},
|
|
2431
|
+
query: {
|
|
2432
|
+
...query,
|
|
2433
|
+
projectId: this.#config.projectId
|
|
2434
|
+
}
|
|
2435
|
+
};
|
|
2436
|
+
};
|
|
2437
|
+
/**
|
|
2438
|
+
* Detect the "project id required" 400 the backend returns when an
|
|
2439
|
+
* organization/user token hits a project-scoped endpoint without a project.
|
|
2440
|
+
*
|
|
2441
|
+
* STUB: keyed off message/field text until the backend exposes a stable
|
|
2442
|
+
* error code for this case.
|
|
2443
|
+
*/
|
|
2444
|
+
#isMissingProjectId = (message, fields) => {
|
|
2445
|
+
const haystacks = [
|
|
2446
|
+
message,
|
|
2447
|
+
...Object.keys(fields ?? {}),
|
|
2448
|
+
...Object.values(fields ?? {}).flat()
|
|
2449
|
+
];
|
|
2450
|
+
return haystacks.some((value) => /project[\s_-]*id/i.test(value) && /requir/i.test(value)) || Object.keys(fields ?? {}).some((key) => /^project[\s_-]*id$/i.test(key));
|
|
2451
|
+
};
|
|
2452
|
+
/**
|
|
2453
|
+
* Validate the configuration
|
|
2454
|
+
*/
|
|
2455
|
+
#validateConfig = () => {
|
|
2456
|
+
if (!this.#config.apiToken) {
|
|
2457
|
+
throw new ConfigurationError("API token is required");
|
|
2458
|
+
}
|
|
2459
|
+
if (!this.#config.apiUrl) {
|
|
2460
|
+
throw new ConfigurationError("API URL is required");
|
|
2461
|
+
}
|
|
2462
|
+
if (!URL.canParse(this.#config.apiUrl)) {
|
|
2463
|
+
throw new ConfigurationError("Invalid API URL format");
|
|
2464
|
+
}
|
|
2465
|
+
};
|
|
2466
|
+
/**
|
|
2467
|
+
* Make an HTTP request with rate limiting using openapi-fetch
|
|
2468
|
+
*/
|
|
2469
|
+
#makeRequest = async (method, path, options = {}) => {
|
|
2470
|
+
const { body, params, signal, timeout, headers: customHeaders, projectScoped = true } = options;
|
|
2471
|
+
const scopedParams = this.#scopedParams(params, { projectScoped });
|
|
2472
|
+
const requestUrl = this.#buildRequestUrl(path, scopedParams);
|
|
2473
|
+
const requestOrigin = { method, url: requestUrl };
|
|
2474
|
+
const requestData = {
|
|
2475
|
+
method,
|
|
2476
|
+
path,
|
|
2477
|
+
body,
|
|
2478
|
+
params: scopedParams,
|
|
2479
|
+
signal,
|
|
2480
|
+
timeout,
|
|
2481
|
+
headers: customHeaders
|
|
2482
|
+
};
|
|
2483
|
+
this.#config.onBeforeRequest(requestData);
|
|
2484
|
+
return this.#rateLimiter.execute(async () => {
|
|
2485
|
+
const abortContext = this.#createAbortContext(signal, timeout);
|
|
2486
|
+
try {
|
|
2487
|
+
if (this.#config.verbose) {
|
|
2488
|
+
this.#log("debug", `\u2192 ${method} ${path}`, { params, body });
|
|
2489
|
+
}
|
|
2490
|
+
const requestOptions = {
|
|
2491
|
+
signal: abortContext.signal,
|
|
2492
|
+
headers: {
|
|
2493
|
+
...customHeaders,
|
|
2494
|
+
...this.#identityHeaders()
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
if (scopedParams) {
|
|
2498
|
+
requestOptions.params = scopedParams;
|
|
2499
|
+
}
|
|
2500
|
+
if (body !== void 0) {
|
|
2501
|
+
requestOptions.body = body;
|
|
2502
|
+
}
|
|
2503
|
+
let response;
|
|
2504
|
+
switch (method) {
|
|
2505
|
+
case "GET":
|
|
2506
|
+
response = await this.#client.GET(path, requestOptions);
|
|
2507
|
+
break;
|
|
2508
|
+
case "POST":
|
|
2509
|
+
response = await this.#client.POST(path, requestOptions);
|
|
2510
|
+
break;
|
|
2511
|
+
case "PUT":
|
|
2512
|
+
response = await this.#client.PUT(path, requestOptions);
|
|
2513
|
+
break;
|
|
2514
|
+
case "DELETE":
|
|
2515
|
+
response = await this.#client.DELETE(path, requestOptions);
|
|
2516
|
+
break;
|
|
2517
|
+
case "PATCH":
|
|
2518
|
+
response = await this.#client.PATCH(path, requestOptions);
|
|
2519
|
+
break;
|
|
2520
|
+
default:
|
|
2521
|
+
throw new ValidationError(`Unsupported HTTP method: ${method}`);
|
|
2522
|
+
}
|
|
2523
|
+
abortContext.clear();
|
|
2524
|
+
if (this.#config.verbose) {
|
|
2525
|
+
this.#log("debug", `\u2190 ${response.response?.status} ${method} ${path}`, {
|
|
2526
|
+
status: response.response?.status,
|
|
2527
|
+
statusText: response.response?.statusText
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
if (response.error) {
|
|
2531
|
+
this.#config.onRequestError(requestData, response.response, response.error);
|
|
2532
|
+
await this.#handleOpenApiError(response.error, response.response, requestOrigin);
|
|
2533
|
+
}
|
|
2534
|
+
const result = response.data;
|
|
2535
|
+
this.#config.onAfterRequest(requestData);
|
|
2536
|
+
return result;
|
|
2537
|
+
} catch (error) {
|
|
2538
|
+
abortContext.clear();
|
|
2539
|
+
if (error instanceof SyntaxError) {
|
|
2540
|
+
throw new UnexpectedResponseError(
|
|
2541
|
+
formatUnexpectedResponseMessage(error, method, requestUrl),
|
|
2542
|
+
error,
|
|
2543
|
+
requestOrigin
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
throw this.#toTransportError(error, requestOrigin, abortContext);
|
|
2547
|
+
}
|
|
2548
|
+
});
|
|
2549
|
+
};
|
|
2550
|
+
/**
|
|
2551
|
+
* Handle OpenAPI fetch errors. Always reads the real `response.status` so
|
|
2552
|
+
* "no response" never masquerades as 500.
|
|
2553
|
+
*/
|
|
2554
|
+
#handleOpenApiError = async (error, response, request) => {
|
|
2555
|
+
const status = response?.status;
|
|
2556
|
+
const errorMessage = formatApiErrorMessage(error);
|
|
2557
|
+
const errorBody = parseErrorBody(error);
|
|
2558
|
+
const fields = extractFieldErrors(errorBody);
|
|
2559
|
+
switch (status) {
|
|
2560
|
+
case 401:
|
|
2561
|
+
throw new AuthenticationError(
|
|
2562
|
+
errorMessage || "Invalid API token or authentication failed",
|
|
2563
|
+
response,
|
|
2564
|
+
request
|
|
2565
|
+
);
|
|
2566
|
+
case 403:
|
|
2567
|
+
throw new AuthorizationError(
|
|
2568
|
+
errorMessage || "Access denied. Check your permissions.",
|
|
2569
|
+
response,
|
|
2570
|
+
request
|
|
2571
|
+
);
|
|
2572
|
+
case 404:
|
|
2573
|
+
throw new NotFoundError(errorMessage || "Resource not found", response, request);
|
|
2574
|
+
case 400:
|
|
2575
|
+
case 422:
|
|
2576
|
+
if (status === 400 && this.#isMissingProjectId(errorMessage, fields)) {
|
|
2577
|
+
throw new MissingProjectIdError(void 0, response, request);
|
|
2578
|
+
}
|
|
2579
|
+
throw new ValidationError(
|
|
2580
|
+
errorMessage || "Validation failed. Check your request data.",
|
|
2581
|
+
response,
|
|
2582
|
+
fields,
|
|
2583
|
+
request
|
|
2584
|
+
);
|
|
2585
|
+
case 429: {
|
|
2586
|
+
const retryAfter = response?.headers.get("Retry-After");
|
|
2587
|
+
const retryAfterMs = retryAfter ? parseInt(retryAfter) * 1e3 : void 0;
|
|
2588
|
+
throw new RateLimitError(
|
|
2589
|
+
retryAfterMs,
|
|
2590
|
+
errorMessage || "Rate limit exceeded",
|
|
2591
|
+
response,
|
|
2592
|
+
request
|
|
2593
|
+
);
|
|
2594
|
+
}
|
|
2595
|
+
case 500:
|
|
2596
|
+
throw new InternalServerError(
|
|
2597
|
+
errorMessage || "Internal server error. Please try again later.",
|
|
2598
|
+
response,
|
|
2599
|
+
request
|
|
2600
|
+
);
|
|
2601
|
+
case 502:
|
|
2602
|
+
case 503:
|
|
2603
|
+
case 504:
|
|
2604
|
+
throw new ServiceUnavailableError(
|
|
2605
|
+
errorMessage || "Service temporarily unavailable. Please try again later.",
|
|
2606
|
+
response,
|
|
2607
|
+
request
|
|
2608
|
+
);
|
|
2609
|
+
default:
|
|
2610
|
+
if (status === void 0) {
|
|
2611
|
+
throw new NetworkError(
|
|
2612
|
+
errorMessage || "No response received from BugBug API",
|
|
2613
|
+
void 0,
|
|
2614
|
+
request
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
throw new HttpError(
|
|
2618
|
+
errorMessage || `HTTP ${status}: ${response?.statusText || "Unknown error"}`,
|
|
2619
|
+
status,
|
|
2620
|
+
response,
|
|
2621
|
+
request
|
|
2622
|
+
);
|
|
2623
|
+
}
|
|
2624
|
+
};
|
|
2625
|
+
/**
|
|
2626
|
+
* Issue a GET request through the openapi-fetch JSON path.
|
|
2627
|
+
*
|
|
2628
|
+
* @param path - API path template (e.g. `/tests/{id}/`).
|
|
2629
|
+
* @param options.queryParams - Mapped to URL search params; `undefined`/`null` values are dropped.
|
|
2630
|
+
* @param options.pathParams - Used to substitute `{name}` placeholders in `path`.
|
|
2631
|
+
* @returns Parsed JSON response body typed as `T`.
|
|
2632
|
+
*/
|
|
2633
|
+
get = async (path, options = {}) => {
|
|
2634
|
+
const { queryParams, pathParams, ...requestOptions } = options;
|
|
2635
|
+
return this.#makeRequest("GET", path, {
|
|
2636
|
+
params: {
|
|
2637
|
+
query: queryParams,
|
|
2638
|
+
path: pathParams
|
|
2639
|
+
},
|
|
2640
|
+
...requestOptions
|
|
2641
|
+
});
|
|
2642
|
+
};
|
|
2643
|
+
/**
|
|
2644
|
+
* Issue a POST request. The body is JSON-serialized by openapi-fetch.
|
|
2645
|
+
*
|
|
2646
|
+
* @param path - API path template.
|
|
2647
|
+
* @param options.body - Request body (JSON-serialized).
|
|
2648
|
+
* @param options.queryParams - URL search params.
|
|
2649
|
+
* @param options.pathParams - Path-segment values.
|
|
2650
|
+
*/
|
|
2651
|
+
post = async (path, options = {}) => {
|
|
2652
|
+
const { body, queryParams, pathParams, ...requestOptions } = options;
|
|
2653
|
+
return this.#makeRequest("POST", path, {
|
|
2654
|
+
body,
|
|
2655
|
+
params: {
|
|
2656
|
+
query: queryParams,
|
|
2657
|
+
path: pathParams
|
|
2658
|
+
},
|
|
2659
|
+
...requestOptions
|
|
2660
|
+
});
|
|
2661
|
+
};
|
|
2662
|
+
getAppRoot = async (path, options = {}) => {
|
|
2663
|
+
const { acceptRedirect = false, authenticated = true, redirect, ...requestOptions } = options;
|
|
2664
|
+
return this.#rawFetch(
|
|
2665
|
+
"GET",
|
|
2666
|
+
path,
|
|
2667
|
+
{ ...requestOptions, acceptRedirect, authenticated, redirect, useAppRoot: true },
|
|
2668
|
+
async (response) => {
|
|
2669
|
+
if (acceptRedirect && response.status >= 300 && response.status < 400) {
|
|
2670
|
+
return response;
|
|
2671
|
+
}
|
|
2672
|
+
const text = await response.text();
|
|
2673
|
+
if (!text) return response;
|
|
2674
|
+
return parseJsonOrText(text);
|
|
2675
|
+
}
|
|
2676
|
+
);
|
|
2677
|
+
};
|
|
2678
|
+
postAppRoot = async (path, options = {}) => {
|
|
2679
|
+
const { authenticated = true, body, ...requestOptions } = options;
|
|
2680
|
+
return this.#rawFetch(
|
|
2681
|
+
"POST",
|
|
2682
|
+
path,
|
|
2683
|
+
{
|
|
2684
|
+
...requestOptions,
|
|
2685
|
+
authenticated,
|
|
2686
|
+
useAppRoot: true,
|
|
2687
|
+
...body !== void 0 ? { body: JSON.stringify(body), contentType: "application/json" } : {}
|
|
2688
|
+
},
|
|
2689
|
+
async (response) => parseJsonOrText(await response.text())
|
|
2690
|
+
);
|
|
2691
|
+
};
|
|
2692
|
+
/**
|
|
2693
|
+
* Issue a JSON POST request for endpoints that intentionally return no
|
|
2694
|
+
* response body even on 2xx success.
|
|
2695
|
+
*/
|
|
2696
|
+
postNoContent = async (path, options = {}) => {
|
|
2697
|
+
const { body, queryParams, pathParams, ...requestOptions } = options;
|
|
2698
|
+
return this.#rawFetch(
|
|
2699
|
+
"POST",
|
|
2700
|
+
path,
|
|
2701
|
+
{
|
|
2702
|
+
...requestOptions,
|
|
2703
|
+
queryParams,
|
|
2704
|
+
pathParams,
|
|
2705
|
+
...body !== void 0 ? { body: JSON.stringify(body), contentType: "application/json" } : {}
|
|
2706
|
+
},
|
|
2707
|
+
async () => void 0
|
|
2708
|
+
);
|
|
2709
|
+
};
|
|
2710
|
+
/** Issue a PUT request (full-replace semantics). See {@link post} for parameters. */
|
|
2711
|
+
put = async (path, options = {}) => {
|
|
2712
|
+
const { body, queryParams, pathParams, ...requestOptions } = options;
|
|
2713
|
+
return this.#makeRequest("PUT", path, {
|
|
2714
|
+
body,
|
|
2715
|
+
params: {
|
|
2716
|
+
query: queryParams,
|
|
2717
|
+
path: pathParams
|
|
2718
|
+
},
|
|
2719
|
+
...requestOptions
|
|
2720
|
+
});
|
|
2721
|
+
};
|
|
2722
|
+
/** Issue a PATCH request (partial-update semantics). See {@link post} for parameters. */
|
|
2723
|
+
patch = async (path, options = {}) => {
|
|
2724
|
+
const { body, queryParams, pathParams, ...requestOptions } = options;
|
|
2725
|
+
return this.#makeRequest("PATCH", path, {
|
|
2726
|
+
body,
|
|
2727
|
+
params: {
|
|
2728
|
+
query: queryParams,
|
|
2729
|
+
path: pathParams
|
|
2730
|
+
},
|
|
2731
|
+
...requestOptions
|
|
2732
|
+
});
|
|
2733
|
+
};
|
|
2734
|
+
/**
|
|
2735
|
+
* Issue a DELETE request. An optional `body` is accepted for DRF endpoints
|
|
2736
|
+
* that require one, though `openapi-fetch` only sends it when the OpenAPI
|
|
2737
|
+
* spec declares it.
|
|
2738
|
+
*/
|
|
2739
|
+
delete = async (path, options = {}) => {
|
|
2740
|
+
const { body, queryParams, pathParams, ...requestOptions } = options;
|
|
2741
|
+
return this.#makeRequest("DELETE", path, {
|
|
2742
|
+
body,
|
|
2743
|
+
params: {
|
|
2744
|
+
query: queryParams,
|
|
2745
|
+
path: pathParams
|
|
2746
|
+
},
|
|
2747
|
+
...requestOptions
|
|
2748
|
+
});
|
|
2749
|
+
};
|
|
2750
|
+
/**
|
|
2751
|
+
* GET an endpoint that returns plain text (e.g. JUnit XML, logs).
|
|
2752
|
+
*
|
|
2753
|
+
* Bypasses the openapi-fetch JSON parser. Errors still flow through the
|
|
2754
|
+
* shared status→error mapping.
|
|
2755
|
+
*
|
|
2756
|
+
* @returns The response body as a string.
|
|
2757
|
+
*/
|
|
2758
|
+
getText = async (path, options = {}) => this.#rawFetch("GET", path, options, async (response) => response.text());
|
|
2759
|
+
/**
|
|
2760
|
+
* GET an endpoint returning binary bytes (ZIP / octet-stream).
|
|
2761
|
+
*
|
|
2762
|
+
* @returns The response body as `Uint8Array`.
|
|
2763
|
+
*/
|
|
2764
|
+
getBinary = async (path, options = {}) => this.#rawFetch(
|
|
2765
|
+
"GET",
|
|
2766
|
+
path,
|
|
2767
|
+
options,
|
|
2768
|
+
async (response) => new Uint8Array(await response.arrayBuffer())
|
|
2769
|
+
);
|
|
2770
|
+
/**
|
|
2771
|
+
* POST raw bytes or text with an explicit `Content-Type`.
|
|
2772
|
+
*
|
|
2773
|
+
* Used for binary (ZIP) and YAML uploads that don't fit the openapi-fetch
|
|
2774
|
+
* JSON path. The response is parsed as JSON when possible, otherwise the
|
|
2775
|
+
* raw text is returned.
|
|
2776
|
+
*
|
|
2777
|
+
* @param path - API path template.
|
|
2778
|
+
* @param body - Raw bytes or string to send as the request body.
|
|
2779
|
+
* @param options.contentType - MIME type sent in the `Content-Type` header.
|
|
2780
|
+
* @returns Parsed JSON body or raw text.
|
|
2781
|
+
*/
|
|
2782
|
+
postRaw = async (path, body, options) => {
|
|
2783
|
+
const { contentType, ...rest } = options;
|
|
2784
|
+
return this.#rawFetch(
|
|
2785
|
+
"POST",
|
|
2786
|
+
path,
|
|
2787
|
+
{ ...rest, body, contentType },
|
|
2788
|
+
async (response) => parseJsonOrText(await response.text())
|
|
2789
|
+
);
|
|
2790
|
+
};
|
|
2791
|
+
/**
|
|
2792
|
+
* POST a `multipart/form-data` body. The browser/fetch sets the
|
|
2793
|
+
* `Content-Type` boundary automatically.
|
|
2794
|
+
*
|
|
2795
|
+
* @param path - API path template.
|
|
2796
|
+
* @param form - The `FormData` instance to send.
|
|
2797
|
+
* @returns Parsed JSON body or raw text.
|
|
2798
|
+
*/
|
|
2799
|
+
postForm = async (path, form, options = {}) => this.#rawFetch(
|
|
2800
|
+
"POST",
|
|
2801
|
+
path,
|
|
2802
|
+
{ ...options, body: form },
|
|
2803
|
+
async (response) => parseJsonOrText(await response.text())
|
|
2804
|
+
);
|
|
2805
|
+
/**
|
|
2806
|
+
* Shared scaffolding for the raw-`fetch` paths (text/binary/multipart/custom
|
|
2807
|
+
* content type) that bypass openapi-fetch. Owns URL building, request hooks,
|
|
2808
|
+
* rate limiting, timeout/abort handling, verbose logging, error mapping, and
|
|
2809
|
+
* the transport-level catch — so each public method only supplies how to read
|
|
2810
|
+
* the successful response body.
|
|
2811
|
+
*/
|
|
2812
|
+
#rawFetch = async (method, path, options, readBody) => {
|
|
2813
|
+
const {
|
|
2814
|
+
queryParams,
|
|
2815
|
+
pathParams,
|
|
2816
|
+
signal,
|
|
2817
|
+
timeout,
|
|
2818
|
+
headers: customHeaders,
|
|
2819
|
+
body,
|
|
2820
|
+
contentType,
|
|
2821
|
+
authenticated = true,
|
|
2822
|
+
acceptRedirect = false,
|
|
2823
|
+
redirect,
|
|
2824
|
+
useAppRoot = false,
|
|
2825
|
+
projectScoped = true
|
|
2826
|
+
} = options;
|
|
2827
|
+
const scopedParams = this.#scopedParams(
|
|
2828
|
+
{ path: pathParams, query: queryParams },
|
|
2829
|
+
{ projectScoped: projectScoped && authenticated && !useAppRoot }
|
|
2830
|
+
);
|
|
2831
|
+
const url = this.#buildRequestUrl(path, scopedParams, useAppRoot);
|
|
2832
|
+
const requestData = {
|
|
2833
|
+
method,
|
|
2834
|
+
path,
|
|
2835
|
+
params: scopedParams,
|
|
2836
|
+
signal,
|
|
2837
|
+
timeout,
|
|
2838
|
+
headers: customHeaders
|
|
2839
|
+
};
|
|
2840
|
+
const requestOrigin = { method, url };
|
|
2841
|
+
this.#config.onBeforeRequest(requestData);
|
|
2842
|
+
return this.#rateLimiter.execute(async () => {
|
|
2843
|
+
const abortContext = this.#createAbortContext(signal, timeout);
|
|
2844
|
+
try {
|
|
2845
|
+
if (this.#config.verbose) {
|
|
2846
|
+
this.#log("debug", `\u2192 ${method} ${path} (raw)`, { queryParams, pathParams });
|
|
2847
|
+
}
|
|
2848
|
+
const response = await fetch(url, {
|
|
2849
|
+
method,
|
|
2850
|
+
redirect,
|
|
2851
|
+
headers: {
|
|
2852
|
+
// Caller headers go first so the SDK's auth + identification
|
|
2853
|
+
// headers always win — a stray `Authorization` in caller-supplied
|
|
2854
|
+
// headers would otherwise silently replace the SDK token.
|
|
2855
|
+
...customHeaders,
|
|
2856
|
+
...authenticated ? this.#identityHeaders() : this.#requestHeaders(),
|
|
2857
|
+
...contentType ? { "Content-Type": contentType } : {}
|
|
2858
|
+
},
|
|
2859
|
+
...body !== void 0 ? { body } : {},
|
|
2860
|
+
signal: abortContext.signal
|
|
2861
|
+
});
|
|
2862
|
+
abortContext.clear();
|
|
2863
|
+
if (this.#config.verbose) {
|
|
2864
|
+
this.#log("debug", `\u2190 ${response.status} ${method} ${path}`, {
|
|
2865
|
+
status: response.status,
|
|
2866
|
+
statusText: response.statusText
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
if (!response.ok && !(acceptRedirect && response.status >= 300 && response.status < 400)) {
|
|
2870
|
+
const errorText = await response.text();
|
|
2871
|
+
this.#config.onRequestError(requestData, response, new Error(errorText));
|
|
2872
|
+
await this.#handleOpenApiError(errorText, response, requestOrigin);
|
|
2873
|
+
}
|
|
2874
|
+
const result = await readBody(response);
|
|
2875
|
+
this.#config.onAfterRequest(requestData);
|
|
2876
|
+
return result;
|
|
2877
|
+
} catch (error) {
|
|
2878
|
+
abortContext.clear();
|
|
2879
|
+
throw this.#toTransportError(error, requestOrigin, abortContext);
|
|
2880
|
+
}
|
|
2881
|
+
});
|
|
2882
|
+
};
|
|
2883
|
+
#createAbortContext = (signal, timeout) => {
|
|
2884
|
+
const controller = new AbortController();
|
|
2885
|
+
const requestTimeout = timeout || this.#config.timeout;
|
|
2886
|
+
let timedOut = false;
|
|
2887
|
+
const timeoutId = setTimeout(() => {
|
|
2888
|
+
timedOut = true;
|
|
2889
|
+
controller.abort();
|
|
2890
|
+
}, requestTimeout);
|
|
2891
|
+
const abortFromCaller = () => {
|
|
2892
|
+
if (!controller.signal.aborted) {
|
|
2893
|
+
controller.abort(signal?.reason);
|
|
2894
|
+
}
|
|
2895
|
+
};
|
|
2896
|
+
if (signal) {
|
|
2897
|
+
if (signal.aborted) {
|
|
2898
|
+
abortFromCaller();
|
|
2899
|
+
} else {
|
|
2900
|
+
signal.addEventListener("abort", abortFromCaller, { once: true });
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
return {
|
|
2904
|
+
signal: controller.signal,
|
|
2905
|
+
timeout: requestTimeout,
|
|
2906
|
+
isTimedOut: () => timedOut,
|
|
2907
|
+
clear: () => {
|
|
2908
|
+
clearTimeout(timeoutId);
|
|
2909
|
+
signal?.removeEventListener("abort", abortFromCaller);
|
|
2910
|
+
}
|
|
2911
|
+
};
|
|
2912
|
+
};
|
|
2913
|
+
/**
|
|
2914
|
+
* Map a thrown transport-level error to the matching SDK error, or rethrow
|
|
2915
|
+
* SDK errors (e.g. from `handleOpenApiError`) untouched.
|
|
2916
|
+
*/
|
|
2917
|
+
#toTransportError = (error, requestOrigin, abortContext) => {
|
|
2918
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
2919
|
+
if (abortContext?.isTimedOut()) {
|
|
2920
|
+
return new TimeoutError(abortContext.timeout, void 0, requestOrigin);
|
|
2921
|
+
}
|
|
2922
|
+
return new CancellationError("Request was cancelled", requestOrigin);
|
|
2923
|
+
}
|
|
2924
|
+
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
2925
|
+
return new NetworkError(formatNetworkErrorMessage(error), error, requestOrigin);
|
|
2926
|
+
}
|
|
2927
|
+
return error;
|
|
2928
|
+
};
|
|
2929
|
+
/**
|
|
2930
|
+
* Return a frozen snapshot of the resolved configuration (defaults applied).
|
|
2931
|
+
*/
|
|
2932
|
+
getConfig = () => ({ ...this.#config });
|
|
2933
|
+
/**
|
|
2934
|
+
* Patch configuration at runtime. Re-builds the openapi-fetch client when
|
|
2935
|
+
* `apiToken` or `apiUrl` changes, and rebuilds the rate limiter when
|
|
2936
|
+
* `rateLimit` changes — other fields take effect on the next request.
|
|
2937
|
+
*
|
|
2938
|
+
* @param updates - Partial configuration overrides to merge in.
|
|
2939
|
+
*/
|
|
2940
|
+
updateConfig = (updates) => {
|
|
2941
|
+
this.#config = { ...this.#config, ...updates };
|
|
2942
|
+
this.#config.apiUrl = this.#normalizeApiUrl(this.#config.apiUrl);
|
|
2943
|
+
if (updates.apiToken || updates.apiUrl) {
|
|
2944
|
+
this.#client = this.#createApiClient();
|
|
2945
|
+
}
|
|
2946
|
+
if (updates.rateLimit) {
|
|
2947
|
+
this.#rateLimiter = new RateLimiter(this.#config.rateLimit);
|
|
2948
|
+
}
|
|
2949
|
+
};
|
|
2950
|
+
/**
|
|
2951
|
+
* Snapshot of the in-process rate-limiter state.
|
|
2952
|
+
*
|
|
2953
|
+
* @returns `{ requestsInWindow, remainingRequests, maxRequests, windowMs, resetTime, canMakeRequest }`.
|
|
2954
|
+
*/
|
|
2955
|
+
getRateLimitStatus = () => this.#rateLimiter.getStatus();
|
|
2956
|
+
/**
|
|
2957
|
+
* Log messages based on log level
|
|
2958
|
+
*/
|
|
2959
|
+
#log = (level, message, data) => {
|
|
2960
|
+
const levels = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
2961
|
+
const currentLevel = levels[this.#config.logLevel];
|
|
2962
|
+
const messageLevel = levels[level];
|
|
2963
|
+
if (messageLevel >= currentLevel) {
|
|
2964
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2965
|
+
const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
|
|
2966
|
+
if (data) {
|
|
2967
|
+
console[level](logMessage, data);
|
|
2968
|
+
} else {
|
|
2969
|
+
console[level](logMessage);
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
};
|
|
2973
|
+
#normalizeApiUrl = (apiUrl) => apiUrl.replace(/\/+$/, "");
|
|
2974
|
+
#requestHeaders = () => ({
|
|
2975
|
+
...this.#config.requestHeaders
|
|
2976
|
+
});
|
|
2977
|
+
/**
|
|
2978
|
+
* Base URL for the few endpoints Django serves at its site root rather than
|
|
2979
|
+
* under `/api/v2` (`/auth/authorize-url`, `/auth/identity`,
|
|
2980
|
+
* `/auth/oauth/token/`).
|
|
2981
|
+
*
|
|
2982
|
+
* Stripping the `/api/v<n>` suffix is correct when talking straight to Django
|
|
2983
|
+
* (local dev, in-cluster `backend:8000`). Hosted BugBug sits behind nginx,
|
|
2984
|
+
* which forwards those routes only under its `/rest/` prefix — bare requests
|
|
2985
|
+
* fall through to the web app and return HTML — so those deployments set
|
|
2986
|
+
* `BUGBUG_APP_URL` to override this.
|
|
2987
|
+
*/
|
|
2988
|
+
#appBaseUrl = () => this.#normalizeApiUrl(
|
|
2989
|
+
process.env.BUGBUG_APP_URL || this.#config.apiUrl.replace(/\/api\/v\d+\/?$/i, "")
|
|
2990
|
+
);
|
|
2991
|
+
#buildRequestUrl = (path, params, useAppRoot = false) => {
|
|
2992
|
+
let url = `${useAppRoot ? this.#appBaseUrl() : this.#config.apiUrl}${path}`;
|
|
2993
|
+
if (params?.path) {
|
|
2994
|
+
Object.entries(params.path).forEach(([key, value]) => {
|
|
2995
|
+
url = url.replace(`{${key}}`, encodeURIComponent(value));
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
if (params?.query) {
|
|
2999
|
+
const searchParams = new URLSearchParams();
|
|
3000
|
+
Object.entries(params.query).forEach(([key, value]) => {
|
|
3001
|
+
if (value !== void 0 && value !== null) {
|
|
3002
|
+
searchParams.append(key, String(value));
|
|
3003
|
+
}
|
|
3004
|
+
});
|
|
3005
|
+
const query = searchParams.toString();
|
|
3006
|
+
if (query) url += `?${query}`;
|
|
3007
|
+
}
|
|
3008
|
+
return url;
|
|
3009
|
+
};
|
|
3010
|
+
};
|
|
3011
|
+
|
|
3012
|
+
// src/sdk.ts
|
|
3013
|
+
var BugBugSDK = class {
|
|
3014
|
+
#client;
|
|
3015
|
+
tests;
|
|
3016
|
+
suites;
|
|
3017
|
+
profiles;
|
|
3018
|
+
config;
|
|
3019
|
+
groups;
|
|
3020
|
+
components;
|
|
3021
|
+
steps;
|
|
3022
|
+
stepRuns;
|
|
3023
|
+
project;
|
|
3024
|
+
projectArtifacts;
|
|
3025
|
+
projects;
|
|
3026
|
+
variables;
|
|
3027
|
+
visualRegression;
|
|
3028
|
+
auth;
|
|
3029
|
+
/**
|
|
3030
|
+
* @param config - SDK configuration (API token, base URL, rate limits, etc.)
|
|
3031
|
+
*/
|
|
3032
|
+
constructor(config) {
|
|
3033
|
+
this.#client = new BugBugApiClient(config);
|
|
3034
|
+
this.tests = new TestsModule(this.#client);
|
|
3035
|
+
this.suites = new SuitesModule(this.#client);
|
|
3036
|
+
this.profiles = new ProfilesModule(this.#client);
|
|
3037
|
+
this.config = new ConfigModule(this.#client);
|
|
3038
|
+
this.groups = new GroupsModule(this.#client);
|
|
3039
|
+
this.components = new ComponentsModule(this.#client);
|
|
3040
|
+
this.steps = new StepsModule(this.#client);
|
|
3041
|
+
this.stepRuns = new StepRunsModule(this.#client);
|
|
3042
|
+
this.project = new ProjectModule(this.#client);
|
|
3043
|
+
this.projectArtifacts = new ProjectArtifactsModule(this.#client);
|
|
3044
|
+
this.projects = new ProjectsModule(this.#client);
|
|
3045
|
+
this.variables = new VariablesModule(this.#client);
|
|
3046
|
+
this.visualRegression = new VisualRegressionModule(this.#client);
|
|
3047
|
+
this.auth = new AuthModule(this.#client);
|
|
3048
|
+
}
|
|
3049
|
+
/**
|
|
3050
|
+
* Get the current SDK configuration.
|
|
3051
|
+
*
|
|
3052
|
+
* @returns Readonly configuration object with all resolved defaults
|
|
3053
|
+
*
|
|
3054
|
+
* @example
|
|
3055
|
+
* ```typescript
|
|
3056
|
+
* const config = sdk.getConfig();
|
|
3057
|
+
* console.log(`API URL: ${config.apiUrl}`);
|
|
3058
|
+
* console.log(`Verbose: ${config.verbose}`);
|
|
3059
|
+
* ```
|
|
3060
|
+
*/
|
|
3061
|
+
getConfig = () => this.#client.getConfig();
|
|
3062
|
+
/**
|
|
3063
|
+
* Update SDK configuration at runtime.
|
|
3064
|
+
*
|
|
3065
|
+
* @param updates - Partial configuration updates to apply
|
|
3066
|
+
*
|
|
3067
|
+
* @example
|
|
3068
|
+
* ```typescript
|
|
3069
|
+
* sdk.updateConfig({
|
|
3070
|
+
* verbose: true,
|
|
3071
|
+
* timeout: 60000
|
|
3072
|
+
* });
|
|
3073
|
+
* ```
|
|
3074
|
+
*/
|
|
3075
|
+
updateConfig = (updates) => {
|
|
3076
|
+
this.#client.updateConfig(updates);
|
|
3077
|
+
};
|
|
3078
|
+
/**
|
|
3079
|
+
* Set the active project for subsequent project-scoped requests.
|
|
3080
|
+
* Shorthand for `updateConfig({ projectId })`.
|
|
3081
|
+
*
|
|
3082
|
+
* @param projectId - The project id to scope requests to.
|
|
3083
|
+
*
|
|
3084
|
+
* @example
|
|
3085
|
+
* ```typescript
|
|
3086
|
+
* sdk.setProject('00000000-0000-0000-0000-000000000001');
|
|
3087
|
+
* ```
|
|
3088
|
+
*/
|
|
3089
|
+
setProject = (projectId) => {
|
|
3090
|
+
this.updateConfig({ projectId });
|
|
3091
|
+
};
|
|
3092
|
+
/**
|
|
3093
|
+
* Test API connectivity and authentication.
|
|
3094
|
+
*
|
|
3095
|
+
* @param options - Request options including timeout and signal
|
|
3096
|
+
* @returns Connectivity test result with timing and status
|
|
3097
|
+
*
|
|
3098
|
+
* @example
|
|
3099
|
+
* ```typescript
|
|
3100
|
+
* const result = await sdk.testConnection();
|
|
3101
|
+
* if (result.connected) {
|
|
3102
|
+
* console.log(`Connected in ${result.responseTime}ms`);
|
|
3103
|
+
* } else {
|
|
3104
|
+
* console.error(`Connection failed: ${result.error}`);
|
|
3105
|
+
* }
|
|
3106
|
+
* ```
|
|
3107
|
+
*/
|
|
3108
|
+
testConnection = async (options = {}) => this.config.testConnectivity(options);
|
|
3109
|
+
};
|
|
3110
|
+
|
|
3111
|
+
// src/modules/visualRegression/visualRegression.constants.ts
|
|
3112
|
+
var VISUAL_REGRESSION_RESOLUTION_ACTIONS = [
|
|
3113
|
+
"acceptBaseline",
|
|
3114
|
+
"increaseMaxDifference"
|
|
3115
|
+
];
|
|
3116
|
+
var VISUAL_REGRESSION_RESOLUTION_TYPE_BY_ACTION = {
|
|
3117
|
+
acceptBaseline: "set_new_reference",
|
|
3118
|
+
increaseMaxDifference: "set_new_max_difference"
|
|
3119
|
+
};
|
|
3120
|
+
|
|
3121
|
+
// src/modules/auth/auth.types.ts
|
|
3122
|
+
var BUGBUG_CLI_OAUTH_CLIENT_ID = "bugbug-cli";
|
|
3123
|
+
var BUGBUG_MCP_OAUTH_CLIENT_ID = "bugbug-mcp";
|
|
3124
|
+
var BUGBUG_OAUTH_CODE_CHALLENGE_METHOD = "S256";
|
|
3125
|
+
|
|
3126
|
+
// src/modules/auth/auth.utils.ts
|
|
3127
|
+
import { randomBytes, randomUUID, createHash } from "crypto";
|
|
3128
|
+
var createOAuthState = () => randomUUID();
|
|
3129
|
+
var createOAuthCodeVerifier = () => randomBytes(48).toString("base64url");
|
|
3130
|
+
var createOAuthCodeChallenge = (verifier) => createHash("sha256").update(verifier).digest("base64url");
|
|
3131
|
+
|
|
3132
|
+
// src/modules/steps/steps.utils.ts
|
|
3133
|
+
var ASSERTION_STEP_TYPES = [
|
|
3134
|
+
"assert",
|
|
3135
|
+
"ifCondition"
|
|
3136
|
+
];
|
|
3137
|
+
var VISUAL_REGRESSION_STEP_TYPES = [
|
|
3138
|
+
"elementVisualRegression",
|
|
3139
|
+
"pageVisualRegression"
|
|
3140
|
+
];
|
|
3141
|
+
var isAssertionStepType = (stepType) => typeof stepType === "string" && ASSERTION_STEP_TYPES.includes(stepType);
|
|
3142
|
+
var isVisualRegressionStepType = (stepType) => typeof stepType === "string" && VISUAL_REGRESSION_STEP_TYPES.includes(stepType);
|
|
3143
|
+
|
|
3144
|
+
// ../api-schema/dist/schemas/common.js
|
|
3145
|
+
import { z } from "zod";
|
|
3146
|
+
var UuidSchema = z.string().min(1).describe("UUID");
|
|
3147
|
+
var DateTimeSchema = z.string().describe("ISO 8601 date-time string");
|
|
3148
|
+
var CursorSchema = z.string().describe("Opaque cursor returned as next_token.");
|
|
3149
|
+
var PageSizeSchema = z.number().int().min(1).max(100).describe("Number of results per page.");
|
|
3150
|
+
var StepTimingSecondsSchema = z.number().min(0).max(300).nullable().optional();
|
|
3151
|
+
|
|
3152
|
+
// ../api-schema/dist/schemas/enums.js
|
|
3153
|
+
import { z as z2 } from "zod";
|
|
3154
|
+
var DEBUG_ARTIFACT_TYPES = [
|
|
3155
|
+
"network_logs",
|
|
3156
|
+
"logs",
|
|
3157
|
+
"dom_snapshot"
|
|
3158
|
+
];
|
|
3159
|
+
var DebugArtifactByTypeSchema = z2.enum(DEBUG_ARTIFACT_TYPES);
|
|
3160
|
+
var RUN_MODES = [
|
|
3161
|
+
"server",
|
|
3162
|
+
"local",
|
|
3163
|
+
"recording"
|
|
3164
|
+
];
|
|
3165
|
+
var RunModeSchema = z2.enum(RUN_MODES).describe("Run execution mode: server, local, recording.");
|
|
3166
|
+
var WAITING_CONDITION_TYPES = [
|
|
3167
|
+
"documentComplete",
|
|
3168
|
+
"elementIsVisible",
|
|
3169
|
+
"elementIsNotCovered",
|
|
3170
|
+
"elementIsNotAnimating",
|
|
3171
|
+
"elementIsNotDisabled",
|
|
3172
|
+
"elementHasFocus",
|
|
3173
|
+
"networkIdle",
|
|
3174
|
+
"pageNavigationAfterExecution",
|
|
3175
|
+
"elementHasAttribute"
|
|
3176
|
+
];
|
|
3177
|
+
var WaitingConditionTypeSchema = z2.enum(WAITING_CONDITION_TYPES).describe("Waiting condition type evaluated by BugBug.");
|
|
3178
|
+
var StatusSchema = z2.enum([
|
|
3179
|
+
"initialized",
|
|
3180
|
+
"passed",
|
|
3181
|
+
"failed",
|
|
3182
|
+
"auto_retrying",
|
|
3183
|
+
"recording",
|
|
3184
|
+
"queued",
|
|
3185
|
+
"running",
|
|
3186
|
+
"stopped",
|
|
3187
|
+
"paused",
|
|
3188
|
+
"error",
|
|
3189
|
+
"skipped"
|
|
3190
|
+
]);
|
|
3191
|
+
var StepTypeSchema = z2.enum([
|
|
3192
|
+
"answerPrompt",
|
|
3193
|
+
"assert",
|
|
3194
|
+
"change",
|
|
3195
|
+
"clear",
|
|
3196
|
+
"click",
|
|
3197
|
+
"dblClick",
|
|
3198
|
+
"rightClick",
|
|
3199
|
+
"closeTab",
|
|
3200
|
+
"dragAndDrop",
|
|
3201
|
+
"elementVisualRegression",
|
|
3202
|
+
"execute",
|
|
3203
|
+
"goto",
|
|
3204
|
+
"goBack",
|
|
3205
|
+
"goForward",
|
|
3206
|
+
"hover",
|
|
3207
|
+
"ifCondition",
|
|
3208
|
+
"mouseDown",
|
|
3209
|
+
"mouseUp",
|
|
3210
|
+
"newTab",
|
|
3211
|
+
"pageVisualRegression",
|
|
3212
|
+
"pasteFromClipboard",
|
|
3213
|
+
"reloadPage",
|
|
3214
|
+
"scroll",
|
|
3215
|
+
"select",
|
|
3216
|
+
"setLocalVariable",
|
|
3217
|
+
"switchContext",
|
|
3218
|
+
"type",
|
|
3219
|
+
"uploadFile"
|
|
3220
|
+
]).describe("Available step types: answerPrompt, assert, change, clear, click, dblClick, rightClick, closeTab, dragAndDrop, elementVisualRegression, execute, goto, goBack, goForward, hover, ifCondition, mouseDown, mouseUp, newTab, pageVisualRegression, pasteFromClipboard, reloadPage, scroll, select, setLocalVariable, switchContext, type, uploadFile.");
|
|
3221
|
+
var AssertionTypeSchema = z2.enum([
|
|
3222
|
+
"contain",
|
|
3223
|
+
"equal",
|
|
3224
|
+
"greaterThan",
|
|
3225
|
+
"lessThan",
|
|
3226
|
+
"match",
|
|
3227
|
+
"notContain",
|
|
3228
|
+
"notEqual",
|
|
3229
|
+
"notMatch",
|
|
3230
|
+
"any"
|
|
3231
|
+
]).describe("Assertion operators supported by BugBug: contain, equal, greaterThan, lessThan, match, notContain, notEqual, notMatch, any.");
|
|
3232
|
+
var AssertionPropertySchema = z2.enum([
|
|
3233
|
+
"checked",
|
|
3234
|
+
"count",
|
|
3235
|
+
"customJavaScript",
|
|
3236
|
+
"exist",
|
|
3237
|
+
"notChecked",
|
|
3238
|
+
"notExist",
|
|
3239
|
+
"notVisible",
|
|
3240
|
+
"textContent",
|
|
3241
|
+
"hasAttribute",
|
|
3242
|
+
"hasNotAttribute",
|
|
3243
|
+
"value",
|
|
3244
|
+
"visible",
|
|
3245
|
+
"pageTitle",
|
|
3246
|
+
"pageShowsText",
|
|
3247
|
+
"pageDoesNotShowText",
|
|
3248
|
+
"pageUrlIs",
|
|
3249
|
+
"downloadStarted",
|
|
3250
|
+
"variableValue",
|
|
3251
|
+
"clipboardValue",
|
|
3252
|
+
"disabled",
|
|
3253
|
+
"notDisabled"
|
|
3254
|
+
]).describe("Assertion properties supported by BugBug, including element state, page-level checks, downloads, variables, clipboard, and disabled state.");
|
|
3255
|
+
var SelectorTypeSchema = z2.enum(["XPath", "customCSS", "customXPath", ""]).nullable().describe("Selector engines or strategies supported by BugBug: XPath, customCSS, customXPath.");
|
|
3256
|
+
var SelectorsRelationSchema = z2.enum(["descendant", "ancestor", "sibling"]).describe("How selectors inside a group are combined: descendant, ancestor, sibling.");
|
|
3257
|
+
var InteractionPositionSchema = z2.enum([
|
|
3258
|
+
"smart",
|
|
3259
|
+
"custom",
|
|
3260
|
+
"topLeft",
|
|
3261
|
+
"topCenter",
|
|
3262
|
+
"topRight",
|
|
3263
|
+
"middleLeft",
|
|
3264
|
+
"middleCenter",
|
|
3265
|
+
"middleRight",
|
|
3266
|
+
"bottomLeft",
|
|
3267
|
+
"bottomCenter",
|
|
3268
|
+
"bottomRight"
|
|
3269
|
+
]).describe("Element interaction position used by BugBug.");
|
|
3270
|
+
var DragAndDropTargetSchema = z2.enum(["coords", "element"]).describe("Drag-and-drop target mode.");
|
|
3271
|
+
var ScrollEdgeSchema = InteractionPositionSchema.exclude(["smart", "custom"]).nullable().describe("Element edge to scroll toward.");
|
|
3272
|
+
var ScrollDirectionSchema = z2.enum(["down", "up", "right", "left"]).nullable();
|
|
3273
|
+
var ScrollToSchema = z2.enum(["coords", "edge", "untilNextStepElementIsVisible", "elementIntoView"]).describe("Scroll target strategy.");
|
|
3274
|
+
var ScrollInsideSchema = z2.enum(["window", "element"]).describe("Whether to scroll inside the window or target element.");
|
|
3275
|
+
var SelectTypeSchema = z2.enum(["text", "index", "value"]).describe("How the option should be matched.");
|
|
3276
|
+
var ScreenSizeTypeSchema = z2.enum(["desktop", "mobile", "custom"]).describe("Screen size preset used when running or recording the test.");
|
|
3277
|
+
var TriggeredBySchema = z2.enum(["user", "api", "webhook", "scheduler", "github", "cli", "agent"]).describe("Origin that triggered the run.");
|
|
3278
|
+
|
|
3279
|
+
// ../api-schema/dist/schemas/selectors.js
|
|
3280
|
+
import { z as z3 } from "zod";
|
|
3281
|
+
var SelectorSchema = z3.object({
|
|
3282
|
+
id: UuidSchema.optional().describe("Selector UUID when persisted by the API."),
|
|
3283
|
+
selector: z3.string().nullable().describe("Raw xpath or css selector expression used to find an element."),
|
|
3284
|
+
type: SelectorTypeSchema.describe("Selector engine or strategy name used by BugBug."),
|
|
3285
|
+
isActive: z3.boolean().describe("Whether this selector is enabled."),
|
|
3286
|
+
step: UuidSchema.optional().describe("Owning step UUID when returned by the API.")
|
|
3287
|
+
}).passthrough();
|
|
3288
|
+
var SelectorsGroupSchema = z3.object({
|
|
3289
|
+
id: UuidSchema.optional().describe("Selectors group UUID when persisted by the API."),
|
|
3290
|
+
relation: SelectorsRelationSchema.describe("How selectors groups inside the preset are combined."),
|
|
3291
|
+
selectors: z3.array(SelectorSchema).min(1, "Selectors group must contain at least one selector.").describe("Selectors included in this group. Must contain at least one selector.")
|
|
3292
|
+
}).passthrough();
|
|
3293
|
+
var SelectorsPresetSchema = z3.object({
|
|
3294
|
+
id: UuidSchema.optional().describe("Selectors preset UUID when persisted by the API."),
|
|
3295
|
+
isCustom: z3.boolean().describe("Whether the preset was manually customized."),
|
|
3296
|
+
isActive: z3.boolean().describe("Whether the preset is enabled for the step."),
|
|
3297
|
+
selectorsGroups: z3.array(SelectorsGroupSchema).min(1, "Selectors preset must contain at least one selectors group.").describe("Selector groups used to locate the target element. Must contain at least one group when provided.")
|
|
3298
|
+
}).passthrough();
|
|
3299
|
+
|
|
3300
|
+
// ../api-schema/dist/schemas/steps.js
|
|
3301
|
+
import { z as z4 } from "zod";
|
|
3302
|
+
|
|
3303
|
+
// ../api-schema/dist/step-error-codes.js
|
|
3304
|
+
var STEP_ERROR_CODES = [
|
|
3305
|
+
"ELEMENT_DOES_NOT_EXIST",
|
|
3306
|
+
"ELEMENT_REMOVED",
|
|
3307
|
+
"SELECTOR_REQUIRED",
|
|
3308
|
+
"INVALID_ELEMENT_SELECTOR",
|
|
3309
|
+
"INTERACTION_POSITION_OUT_OF_VIEWPORT",
|
|
3310
|
+
"FRAME_DOES_NOT_EXIST",
|
|
3311
|
+
"FRAME_LOADS_TOO_LONG",
|
|
3312
|
+
"WINDOW_OR_TAB_DOES_NOT_EXIST",
|
|
3313
|
+
"TAB_CLOSED",
|
|
3314
|
+
"WINDOW_CLOSED",
|
|
3315
|
+
"WINDOW_MINIMIZED",
|
|
3316
|
+
"UNEXPECTED_WINDOW_STATE_CHANGE",
|
|
3317
|
+
"UNCHANGABLE_ELEMENT",
|
|
3318
|
+
"TYPED_TEXT_DIFFERENT_THAN_EXPECTED",
|
|
3319
|
+
"INVALID_FIELD_VALUE",
|
|
3320
|
+
"INVALID_OPTION_INDEX",
|
|
3321
|
+
"MISSING_OPTION_VALUE",
|
|
3322
|
+
"MISSING_OPTION_TEXT",
|
|
3323
|
+
"MISSING_OPTION_INDEX",
|
|
3324
|
+
"SELECT_OPTION_FAILED",
|
|
3325
|
+
"MULTIPLE_OPTIONS_WITH_VALUE",
|
|
3326
|
+
"MISSING_GOTO_STEP",
|
|
3327
|
+
"INVALID_URL",
|
|
3328
|
+
"PAGE_LOADING_ERROR",
|
|
3329
|
+
"ELEMENT_VISUAL_REGRESSION_FAILED",
|
|
3330
|
+
"PAGE_VISUAL_REGRESSION_FAILED",
|
|
3331
|
+
"SCREENSHOT_CAPTURE_ERROR",
|
|
3332
|
+
"REFERENCE_SCREENSHOT_NOT_FOUND",
|
|
3333
|
+
"SCREENSHOT_COMPARISON_ERROR",
|
|
3334
|
+
"CODE_EXECUTION_ERROR",
|
|
3335
|
+
"INVALID_EXECUTION_RESULT",
|
|
3336
|
+
"VARIABLE_DOES_NOT_EXIST",
|
|
3337
|
+
"VARIABLE_NESTING_LIMIT_EXCEEDED",
|
|
3338
|
+
"EVENT_DISPATCHED_ON_INVALID_ELEMENT",
|
|
3339
|
+
"EVENT_DISPATCH_FAILED",
|
|
3340
|
+
"FAILED_WAITING_CONDITIONS",
|
|
3341
|
+
"PROMPT_DOES_NOT_EXIST",
|
|
3342
|
+
"UNHANDLED_PROMPT",
|
|
3343
|
+
"NEXT_STEP_ELEMENT_REQUIRED",
|
|
3344
|
+
"NEXT_ACTIVE_STEP_WITH_ELEMENT_REQUIRED",
|
|
3345
|
+
"ASSERT_FAILED",
|
|
3346
|
+
"SCROLL_FAILED",
|
|
3347
|
+
"TIMEOUT",
|
|
3348
|
+
"SINGLE_TEST_TIME_EXCEEDED",
|
|
3349
|
+
"UNSUPPORTED_BROWSER",
|
|
3350
|
+
"DEBUGGER_DETACHED",
|
|
3351
|
+
"BLOCKED_BY_BROWSER_POLICY",
|
|
3352
|
+
"INVALID_CUSTOM_HEADERS",
|
|
3353
|
+
"INVALID_DATA_FORMAT",
|
|
3354
|
+
"RUNTIME_ERROR",
|
|
3355
|
+
"INTERNAL_ERROR",
|
|
3356
|
+
"INITIALIZATION_ERROR",
|
|
3357
|
+
"AUTHENTICATION_ERROR",
|
|
3358
|
+
"STEP_RUN_INITIALIZATION_ERROR",
|
|
3359
|
+
"UNRECOGNIZED_STEP_TYPE",
|
|
3360
|
+
"FRAME_IS_NOT_INITIALIZED",
|
|
3361
|
+
"INVALID_MOUSE_INPUT_PARAMS",
|
|
3362
|
+
"FILE_DOES_NOT_EXIST",
|
|
3363
|
+
"FILE_UPLOAD_ERROR",
|
|
3364
|
+
"VALUE_COMPUTING_ERROR",
|
|
3365
|
+
"WEBSOCKET_ERROR",
|
|
3366
|
+
"WEBSOCKET_SETUP_ERROR",
|
|
3367
|
+
"REQUEST_ERROR",
|
|
3368
|
+
"LOGS_UPLOAD_TIMEOUT",
|
|
3369
|
+
"EXTENSION_DOES_NOT_RESPONSE",
|
|
3370
|
+
"EXTENSION_DISCONNECTED",
|
|
3371
|
+
"EXTENSION_DISCONNECTED_ERROR",
|
|
3372
|
+
"TAKING_SCREENSHOT_TIMEOUT",
|
|
3373
|
+
"MISSING_STEP_SCREENSHOT",
|
|
3374
|
+
"MISSING_ELEMENT_SCREENSHOT"
|
|
3375
|
+
];
|
|
3376
|
+
|
|
3377
|
+
// ../api-schema/dist/schemas/steps.js
|
|
3378
|
+
var WaitingConditionSchema = z4.object({
|
|
3379
|
+
type: WaitingConditionTypeSchema,
|
|
3380
|
+
expected: z4.string().nullable().describe("Expected value for the waiting condition, if any.")
|
|
3381
|
+
}).passthrough();
|
|
3382
|
+
var StepErrorCodeSchema = z4.enum(STEP_ERROR_CODES);
|
|
3383
|
+
var StepSourceSchema = z4.enum(["api", "agent"]).describe("Creation source for public API step writes.");
|
|
3384
|
+
var StepBaseSchema = z4.object({
|
|
3385
|
+
id: UuidSchema.describe("Step UUID. Omit when creating a new step."),
|
|
3386
|
+
atIndex: z4.number().nullable().describe("Insert position within the group."),
|
|
3387
|
+
name: z4.string().nullable().optional().describe("Human-readable step name."),
|
|
3388
|
+
blockId: UuidSchema.nullable().optional().describe("Id of step representing a block start eg. ifCondition step"),
|
|
3389
|
+
groupId: UuidSchema.nullable().describe("Owning group UUID for the step."),
|
|
3390
|
+
originGroupId: UuidSchema.optional().describe("Original group UUID from the API response. Omit when creating a new step."),
|
|
3391
|
+
notes: z4.string().nullable().optional().describe("Optional step notes."),
|
|
3392
|
+
isActive: z4.boolean().optional().describe("Whether the step is enabled."),
|
|
3393
|
+
runTimeout: StepTimingSecondsSchema.describe("Maximum execution time in seconds before the step times out. If not defined, default project settings will be used."),
|
|
3394
|
+
sleep: StepTimingSecondsSchema.describe("Delay in seconds before running the step. If not defined, default project settings will be used."),
|
|
3395
|
+
continueOnFailure: z4.boolean().optional().describe("Whether the run should continue after a fail."),
|
|
3396
|
+
waitingConditions: z4.array(WaitingConditionSchema.extend({
|
|
3397
|
+
isActive: z4.boolean(),
|
|
3398
|
+
isOverridden: z4.boolean().default(true).describe("Required when overriding default waiting condition config."),
|
|
3399
|
+
timeout: z4.number().nullable().optional().default(null).describe("Custom waiting condition timeout")
|
|
3400
|
+
})).optional().describe("Optional waiting conditions evaluated around the step."),
|
|
3401
|
+
selectorsPresets: z4.array(SelectorsPresetSchema).optional().describe("Element selector presets required by element-targeting steps variants."),
|
|
3402
|
+
source: StepSourceSchema.optional().describe("Optional creation source. Direct API and SDK clients should use api; agent/MCP clients should use agent.")
|
|
3403
|
+
}).passthrough();
|
|
3404
|
+
var omitStepRuntimeFields = (schema) => schema.omit({
|
|
3405
|
+
id: true,
|
|
3406
|
+
originGroupId: true
|
|
3407
|
+
});
|
|
3408
|
+
var toStepPatchSchema = (schema) => {
|
|
3409
|
+
const createSchema = omitStepRuntimeFields(schema).omit({
|
|
3410
|
+
source: true
|
|
3411
|
+
});
|
|
3412
|
+
return createSchema.partial().extend({
|
|
3413
|
+
type: createSchema.shape.type
|
|
3414
|
+
});
|
|
3415
|
+
};
|
|
3416
|
+
var asNonEmptyDiscriminatedOptions = (schemas) => {
|
|
3417
|
+
const [first, ...rest] = schemas;
|
|
3418
|
+
if (!first) {
|
|
3419
|
+
throw new Error("Expected at least one step schema");
|
|
3420
|
+
}
|
|
3421
|
+
return [first, ...rest];
|
|
3422
|
+
};
|
|
3423
|
+
var AnswerPromptStepSchema = StepBaseSchema.extend({
|
|
3424
|
+
type: z4.literal("answerPrompt").describe("Respond to a browser prompt dialog."),
|
|
3425
|
+
value: z4.string().describe("Prompt response value.")
|
|
3426
|
+
});
|
|
3427
|
+
var AssertStepSchema = StepBaseSchema.extend({
|
|
3428
|
+
type: z4.literal("assert").describe("Create an assertion step."),
|
|
3429
|
+
selectorsPresets: z4.array(SelectorsPresetSchema).describe("Target selectors used by the element-related assertion."),
|
|
3430
|
+
assertionProperty: AssertionPropertySchema.describe("Assertion property to inspect, for example text or value."),
|
|
3431
|
+
assertionType: AssertionTypeSchema.describe("Assertion operator used by BugBug."),
|
|
3432
|
+
assertionExpectedValue: z4.string().nullable().describe("Expected assertion value when applicable."),
|
|
3433
|
+
assertionJavaScript: z4.string().nullable().describe("Custom JavaScript used by script-based assertions."),
|
|
3434
|
+
assertionVariableName: z4.string().nullable().describe("Variable name used by variable-based assertions.")
|
|
3435
|
+
});
|
|
3436
|
+
var PasteFromClipboardStepSchema = StepBaseSchema.extend({
|
|
3437
|
+
type: z4.literal("pasteFromClipboard").describe("Paste clipboard contents into the active element.")
|
|
3438
|
+
});
|
|
3439
|
+
var ChangeStepSchema = StepBaseSchema.extend({
|
|
3440
|
+
type: z4.literal("change").describe("Change the value of the targeted element."),
|
|
3441
|
+
value: z4.string().describe("Value to set on the target element.")
|
|
3442
|
+
});
|
|
3443
|
+
var ClearStepSchema = StepBaseSchema.extend({
|
|
3444
|
+
type: z4.literal("clear").describe("Clear the value of the targeted element.")
|
|
3445
|
+
});
|
|
3446
|
+
var ClickStepShape = {
|
|
3447
|
+
interactionPosition: InteractionPositionSchema.describe("Click position relative to the target element."),
|
|
3448
|
+
selectorsPresets: z4.array(SelectorsPresetSchema).describe("Target selectors used to locate the element.")
|
|
3449
|
+
};
|
|
3450
|
+
var ClickStepSchema = StepBaseSchema.extend({
|
|
3451
|
+
type: z4.literal("click").describe("Perform a click on the targeted element."),
|
|
3452
|
+
...ClickStepShape
|
|
3453
|
+
});
|
|
3454
|
+
var DoubleClickStepSchema = StepBaseSchema.extend({
|
|
3455
|
+
type: z4.literal("dblClick").describe("Perform a double click on the targeted element."),
|
|
3456
|
+
...ClickStepShape
|
|
3457
|
+
});
|
|
3458
|
+
var RightClickStepSchema = StepBaseSchema.extend({
|
|
3459
|
+
type: z4.literal("rightClick").describe("Perform a right click on the targeted element."),
|
|
3460
|
+
...ClickStepShape
|
|
3461
|
+
});
|
|
3462
|
+
var CloseTabStepSchema = StepBaseSchema.extend({
|
|
3463
|
+
type: z4.literal("closeTab").describe("Close the current browser tab.")
|
|
3464
|
+
});
|
|
3465
|
+
var DragAndDropStepSchema = StepBaseSchema.extend({
|
|
3466
|
+
type: z4.literal("dragAndDrop").describe("Drag from one element and drop onto another."),
|
|
3467
|
+
dndDragOn: DragAndDropTargetSchema.describe("Whether dragging starts from coordinates or an element."),
|
|
3468
|
+
interactionPosition: InteractionPositionSchema.describe("Drag start position relative to the target element."),
|
|
3469
|
+
dndDragX: z4.number().nullable().optional().describe("Drag start X coordinate when dragging by coordinates."),
|
|
3470
|
+
dndDragY: z4.number().nullable().optional().describe("Drag start Y coordinate when dragging by coordinates."),
|
|
3471
|
+
dndDropOn: DragAndDropTargetSchema.describe("Whether dropping targets coordinates or an element."),
|
|
3472
|
+
dndDropInteractionPosition: InteractionPositionSchema.describe("Drop position relative to the target element."),
|
|
3473
|
+
dndDropX: z4.number().nullable().optional().describe("Drop X coordinate when dropping by coordinates."),
|
|
3474
|
+
dndDropY: z4.number().nullable().optional().describe("Drop Y coordinate when dropping by coordinates."),
|
|
3475
|
+
dndDropSelectorsPresets: z4.array(SelectorsPresetSchema).optional().describe("Target selectors used to locate the drop element.")
|
|
3476
|
+
});
|
|
3477
|
+
var ExecuteStepSchema = StepBaseSchema.extend({
|
|
3478
|
+
type: z4.literal("execute").describe("Execute custom JavaScript in the page context."),
|
|
3479
|
+
code: z4.string().describe("JavaScript source code to execute.")
|
|
3480
|
+
});
|
|
3481
|
+
var GotoStepSchema = StepBaseSchema.extend({
|
|
3482
|
+
type: z4.literal("goto").describe("Navigate the browser to a URL."),
|
|
3483
|
+
url: z4.string().describe("Absolute URL to open.")
|
|
3484
|
+
});
|
|
3485
|
+
var HoverStepSchema = StepBaseSchema.extend({
|
|
3486
|
+
type: z4.literal("hover").describe("Hover over the targeted element."),
|
|
3487
|
+
interactionPosition: InteractionPositionSchema.describe("Hover position relative to the target element."),
|
|
3488
|
+
selectorsPresets: z4.array(SelectorsPresetSchema).describe("Target selectors used to locate the element.")
|
|
3489
|
+
});
|
|
3490
|
+
var MouseDownStepSchema = StepBaseSchema.extend({
|
|
3491
|
+
type: z4.literal("mouseDown").describe("Press the mouse button on the targeted element."),
|
|
3492
|
+
interactionPosition: InteractionPositionSchema.describe("Mouse-down position relative to the target element."),
|
|
3493
|
+
selectorsPresets: z4.array(SelectorsPresetSchema).describe("Target selectors used to locate the element.")
|
|
3494
|
+
});
|
|
3495
|
+
var MouseUpStepSchema = StepBaseSchema.extend({
|
|
3496
|
+
type: z4.literal("mouseUp").describe("Release the mouse button on the targeted element."),
|
|
3497
|
+
interactionPosition: InteractionPositionSchema.describe("Mouse-up position relative to the target element."),
|
|
3498
|
+
selectorsPresets: z4.array(SelectorsPresetSchema).describe("Target selectors used to locate the element.")
|
|
3499
|
+
});
|
|
3500
|
+
var NewTabStepSchema = StepBaseSchema.extend({
|
|
3501
|
+
type: z4.literal("newTab").describe("Open a new browser tab."),
|
|
3502
|
+
url: z4.string().nullable().optional().describe("URL to open in the new tab."),
|
|
3503
|
+
username: z4.string().nullable().optional().describe("Optional username for basic authentication."),
|
|
3504
|
+
password: z4.string().nullable().optional().describe("Optional password for basic authentication.")
|
|
3505
|
+
});
|
|
3506
|
+
var ReloadPageStepSchema = StepBaseSchema.extend({
|
|
3507
|
+
type: z4.literal("reloadPage").describe("Reload the current page.")
|
|
3508
|
+
});
|
|
3509
|
+
var ScrollStepSchema = StepBaseSchema.extend({
|
|
3510
|
+
type: z4.literal("scroll").describe("Scroll the page or target element."),
|
|
3511
|
+
scrollX: z4.number().nullable().optional().describe("Horizontal scroll coordinate."),
|
|
3512
|
+
scrollY: z4.number().nullable().optional().describe("Vertical scroll coordinate."),
|
|
3513
|
+
scrollEdge: ScrollEdgeSchema.optional(),
|
|
3514
|
+
scrollDirection: ScrollDirectionSchema.optional().describe("Scroll direction."),
|
|
3515
|
+
scrollTo: ScrollToSchema,
|
|
3516
|
+
scrollInside: ScrollInsideSchema
|
|
3517
|
+
});
|
|
3518
|
+
var SelectStepSchema = StepBaseSchema.extend({
|
|
3519
|
+
type: z4.literal("select").describe("Select an option in the targeted select element."),
|
|
3520
|
+
value: z4.string().describe("Option value or label to select."),
|
|
3521
|
+
selectType: SelectTypeSchema,
|
|
3522
|
+
selectIsMultiple: z4.boolean().describe("Whether the target select allows multiple values.")
|
|
3523
|
+
});
|
|
3524
|
+
var SetLocalVariableStepSchema = StepBaseSchema.extend({
|
|
3525
|
+
type: z4.literal("setLocalVariable").describe("Store a value in a local BugBug variable."),
|
|
3526
|
+
value: z4.string().optional().describe("Literal value to store when using the value source."),
|
|
3527
|
+
code: z4.string().nullable().optional().describe("JavaScript source used when evaluating a variable value."),
|
|
3528
|
+
localVariableName: z4.string().describe("Variable name to assign."),
|
|
3529
|
+
localVariableSource: z4.enum(["element", "value", "evaluate"]).describe("Source used to populate the variable.")
|
|
3530
|
+
});
|
|
3531
|
+
var SwitchContextStepSchema = StepBaseSchema.extend({
|
|
3532
|
+
type: z4.literal("switchContext").describe("Switch execution context to another browser tab."),
|
|
3533
|
+
tabNo: z4.number().describe("Tab index to activate.")
|
|
3534
|
+
});
|
|
3535
|
+
var TypeStepSchema = StepBaseSchema.extend({
|
|
3536
|
+
type: z4.literal("type").describe("Type text into the targeted element."),
|
|
3537
|
+
value: z4.string().describe("Text to type.")
|
|
3538
|
+
});
|
|
3539
|
+
var UploadStepSchema = StepBaseSchema.extend({
|
|
3540
|
+
type: z4.literal("uploadFile").describe("Upload a project artifact into the targeted file input."),
|
|
3541
|
+
projectArtifactId: UuidSchema.describe("Project artifact UUID to upload.")
|
|
3542
|
+
});
|
|
3543
|
+
var GoBackStepSchema = StepBaseSchema.extend({
|
|
3544
|
+
type: z4.literal("goBack").describe("Navigate the browser back in history.")
|
|
3545
|
+
});
|
|
3546
|
+
var GoForwardStepSchema = StepBaseSchema.extend({
|
|
3547
|
+
type: z4.literal("goForward").describe("Navigate the browser forward in history.")
|
|
3548
|
+
});
|
|
3549
|
+
var IfConditionStepSchema = StepBaseSchema.extend({
|
|
3550
|
+
type: z4.literal("ifCondition").describe("Branch execution based on an assertion condition."),
|
|
3551
|
+
selectorsPresets: z4.array(SelectorsPresetSchema).optional().describe("Target selectors used to locate the element for the condition."),
|
|
3552
|
+
assertionProperty: AssertionPropertySchema.describe("Assertion property to inspect for the branch condition."),
|
|
3553
|
+
assertionType: AssertionTypeSchema.describe("Assertion operator used to evaluate the branch condition."),
|
|
3554
|
+
assertionExpectedValue: z4.string().nullable().describe("Expected assertion value when applicable."),
|
|
3555
|
+
assertionJavaScript: z4.string().nullable().describe("Custom JavaScript used by script-based conditions."),
|
|
3556
|
+
assertionVariableName: z4.string().nullable().describe("Variable name used by variable-based conditions.")
|
|
3557
|
+
});
|
|
3558
|
+
var ElementVisualRegressionStepSchema = StepBaseSchema.extend({
|
|
3559
|
+
type: z4.literal("elementVisualRegression").describe("Perform a visual regression check on a targeted element."),
|
|
3560
|
+
selectorsPresets: z4.array(SelectorsPresetSchema).describe("Target selectors used to locate the element for comparison."),
|
|
3561
|
+
visualRegressionMaxDiff: z4.number().nullable().optional().describe("Maximum allowed pixel difference percentage (0\u2013100).")
|
|
3562
|
+
});
|
|
3563
|
+
var PageVisualRegressionStepSchema = StepBaseSchema.extend({
|
|
3564
|
+
type: z4.literal("pageVisualRegression").describe("Perform a visual regression check on the full page."),
|
|
3565
|
+
visualRegressionMaxDiff: z4.number().nullable().optional().describe("Maximum allowed pixel difference percentage (0\u2013100).")
|
|
3566
|
+
});
|
|
3567
|
+
var allStepsSchemas = [
|
|
3568
|
+
AnswerPromptStepSchema,
|
|
3569
|
+
AssertStepSchema,
|
|
3570
|
+
PasteFromClipboardStepSchema,
|
|
3571
|
+
ChangeStepSchema,
|
|
3572
|
+
ClearStepSchema,
|
|
3573
|
+
ClickStepSchema,
|
|
3574
|
+
DoubleClickStepSchema,
|
|
3575
|
+
RightClickStepSchema,
|
|
3576
|
+
CloseTabStepSchema,
|
|
3577
|
+
DragAndDropStepSchema,
|
|
3578
|
+
ElementVisualRegressionStepSchema,
|
|
3579
|
+
ExecuteStepSchema,
|
|
3580
|
+
GotoStepSchema,
|
|
3581
|
+
GoBackStepSchema,
|
|
3582
|
+
GoForwardStepSchema,
|
|
3583
|
+
HoverStepSchema,
|
|
3584
|
+
IfConditionStepSchema,
|
|
3585
|
+
MouseDownStepSchema,
|
|
3586
|
+
MouseUpStepSchema,
|
|
3587
|
+
NewTabStepSchema,
|
|
3588
|
+
PageVisualRegressionStepSchema,
|
|
3589
|
+
ReloadPageStepSchema,
|
|
3590
|
+
ScrollStepSchema,
|
|
3591
|
+
SelectStepSchema,
|
|
3592
|
+
SetLocalVariableStepSchema,
|
|
3593
|
+
SwitchContextStepSchema,
|
|
3594
|
+
TypeStepSchema,
|
|
3595
|
+
UploadStepSchema
|
|
3596
|
+
];
|
|
3597
|
+
var StepSchema = z4.discriminatedUnion("type", allStepsSchemas);
|
|
3598
|
+
var CREATE_STEP_SCHEMA_OPTIONS = asNonEmptyDiscriminatedOptions(allStepsSchemas.map((schema) => omitStepRuntimeFields(schema)));
|
|
3599
|
+
var UPDATE_STEP_SCHEMA_OPTIONS = asNonEmptyDiscriminatedOptions(allStepsSchemas.map((schema) => toStepPatchSchema(schema)));
|
|
3600
|
+
var StepCreateSchema = z4.discriminatedUnion("type", CREATE_STEP_SCHEMA_OPTIONS);
|
|
3601
|
+
var StepUpdateSchema = z4.discriminatedUnion("type", UPDATE_STEP_SCHEMA_OPTIONS);
|
|
3602
|
+
var StepRunSchema = z4.object({
|
|
3603
|
+
id: UuidSchema.describe("Step run UUID."),
|
|
3604
|
+
stepId: UuidSchema.nullable().optional().describe("Original step UUID associated with this run, if available."),
|
|
3605
|
+
type: z4.string().optional().describe("Executed step type."),
|
|
3606
|
+
status: StatusSchema.optional().describe("Execution status of the step run."),
|
|
3607
|
+
screenshotUrl: z4.string().optional().describe("Screenshot URL captured for this step run, when available."),
|
|
3608
|
+
errorCode: StepErrorCodeSchema.nullable().optional().describe("Structured BugBug error code for the failed step run, if any."),
|
|
3609
|
+
error: z4.string().nullable().optional().describe("Human-readable error message for the step run, if any."),
|
|
3610
|
+
duration: z4.string().nullable().optional().describe("Execution duration reported by the API."),
|
|
3611
|
+
waitingConditions: z4.array(WaitingConditionSchema.extend({
|
|
3612
|
+
isSuccess: z4.boolean().describe("Whether the condition passed."),
|
|
3613
|
+
isSoftSuccess: z4.boolean().optional().describe("Whether the condition passed softly with warnings."),
|
|
3614
|
+
isIgnored: z4.boolean().optional().describe("Whether the condition result is ignored."),
|
|
3615
|
+
current: z4.string().nullable().optional().describe("Observed value for the waiting condition, if any.")
|
|
3616
|
+
})).describe("Waiting conditions evaluated during the step run.")
|
|
3617
|
+
}).passthrough();
|
|
3618
|
+
|
|
3619
|
+
// ../api-schema/dist/schemas/tests.js
|
|
3620
|
+
import { z as z6 } from "zod";
|
|
3621
|
+
|
|
3622
|
+
// ../api-schema/dist/schemas/_paginated.js
|
|
3623
|
+
import { z as z5 } from "zod";
|
|
3624
|
+
var makePaginatedSchema = (itemSchema) => z5.object({
|
|
3625
|
+
count: z5.number().optional().describe("Total number of items matching the query."),
|
|
3626
|
+
nextToken: z5.string().nullable().optional().describe("Opaque cursor token for the next page, if one exists."),
|
|
3627
|
+
results: z5.array(itemSchema).describe("Items returned for the current page.")
|
|
3628
|
+
});
|
|
3629
|
+
|
|
3630
|
+
// ../api-schema/dist/schemas/tests.js
|
|
3631
|
+
var TestSchema = z6.object({
|
|
3632
|
+
id: UuidSchema.describe("Test UUID."),
|
|
3633
|
+
name: z6.string().optional().describe("Test name."),
|
|
3634
|
+
groups: z6.string().describe("Serialized group structure returned by the API."),
|
|
3635
|
+
notes: z6.string().nullable().describe("Optional test notes."),
|
|
3636
|
+
screenSizeType: ScreenSizeTypeSchema,
|
|
3637
|
+
webappUrl: z6.string().describe("BugBug web app URL for the test details page.")
|
|
3638
|
+
}).passthrough();
|
|
3639
|
+
var TestListItemSchema = z6.object({
|
|
3640
|
+
id: UuidSchema.describe("Test UUID."),
|
|
3641
|
+
name: z6.string().optional().describe("Test name.")
|
|
3642
|
+
}).passthrough();
|
|
3643
|
+
var TestsListSchema = makePaginatedSchema(TestListItemSchema);
|
|
3644
|
+
|
|
3645
|
+
// ../api-schema/dist/schemas/suites.js
|
|
3646
|
+
import { z as z7 } from "zod";
|
|
3647
|
+
var SuiteSchema = z7.object({
|
|
3648
|
+
id: UuidSchema.describe("Suite UUID."),
|
|
3649
|
+
name: z7.string().nullable().describe("Suite name."),
|
|
3650
|
+
testsCount: z7.number().describe("Number of tests included in the suite."),
|
|
3651
|
+
tests: z7.array(z7.record(z7.unknown())).describe("Raw test entries included in the suite response."),
|
|
3652
|
+
autoAddNewTests: z7.boolean().describe("Whether newly created tests are automatically added to the suite."),
|
|
3653
|
+
autoRetry: z7.number().describe("Number of automatic retries configured for failed tests in the suite."),
|
|
3654
|
+
notes: z7.string().nullable().describe("Optional suite notes."),
|
|
3655
|
+
runInParallel: z7.boolean().describe("Whether suite tests run in parallel."),
|
|
3656
|
+
runProfileId: z7.string().nullable().describe("Run profile UUID used by default for the suite, if configured.")
|
|
3657
|
+
}).passthrough();
|
|
3658
|
+
var SuiteDetailsSchema = SuiteSchema.extend({
|
|
3659
|
+
webappUrl: z7.string().describe("BugBug web app URL for the suite details page.")
|
|
3660
|
+
});
|
|
3661
|
+
var SuitesListSchema = makePaginatedSchema(SuiteSchema);
|
|
3662
|
+
|
|
3663
|
+
// ../api-schema/dist/schemas/profiles.js
|
|
3664
|
+
import { z as z8 } from "zod";
|
|
3665
|
+
var ProfileSchema = z8.object({
|
|
3666
|
+
id: UuidSchema.describe("Run profile UUID."),
|
|
3667
|
+
name: z8.string().describe("Run profile name."),
|
|
3668
|
+
isDefault: z8.boolean().optional().describe("Whether this is the default run profile.")
|
|
3669
|
+
}).passthrough();
|
|
3670
|
+
var ProfilesListSchema = makePaginatedSchema(ProfileSchema);
|
|
3671
|
+
|
|
3672
|
+
// ../api-schema/dist/schemas/projects.js
|
|
3673
|
+
import { z as z9 } from "zod";
|
|
3674
|
+
var ProjectSchema = z9.object({
|
|
3675
|
+
id: UuidSchema.describe("Project UUID."),
|
|
3676
|
+
name: z9.string().nullable().describe("Project name."),
|
|
3677
|
+
homepageUrl: z9.string().nullable().describe("Project homepage URL."),
|
|
3678
|
+
organizationId: z9.string().nullable().describe("Owning organization UUID."),
|
|
3679
|
+
organizationName: z9.string().describe("Owning organization name.")
|
|
3680
|
+
}).passthrough();
|
|
3681
|
+
var ProjectsListSchema = makePaginatedSchema(ProjectSchema);
|
|
3682
|
+
|
|
3683
|
+
// ../api-schema/dist/schemas/runs.js
|
|
3684
|
+
import { z as z10 } from "zod";
|
|
3685
|
+
var RunResultSchema = z10.object({
|
|
3686
|
+
id: UuidSchema.describe("Run UUID."),
|
|
3687
|
+
status: StatusSchema.describe("Current status of the run."),
|
|
3688
|
+
webappUrl: z10.string().describe("BugBug web app URL for the run details page."),
|
|
3689
|
+
modified: DateTimeSchema.optional().describe("Last modification timestamp reported by the API.")
|
|
3690
|
+
}).passthrough();
|
|
3691
|
+
var StepRunScreenshotSchema = z10.object({
|
|
3692
|
+
id: UuidSchema.describe("Step run UUID."),
|
|
3693
|
+
stepId: UuidSchema.nullable().describe("Original step UUID associated with the screenshot, if available."),
|
|
3694
|
+
screenshotUrl: z10.string().describe("Screenshot URL for the step run.")
|
|
3695
|
+
}).passthrough();
|
|
3696
|
+
var TestRunSchema = z10.object({
|
|
3697
|
+
id: UuidSchema.describe("Test run UUID."),
|
|
3698
|
+
name: z10.string().min(1).describe("Test run name."),
|
|
3699
|
+
queued: DateTimeSchema.nullable().describe("Queue timestamp, if the run waited before execution."),
|
|
3700
|
+
status: StatusSchema.describe("Current status of the test run."),
|
|
3701
|
+
sequence: z10.number().nullable().describe("Sequence number assigned by BugBug, if available."),
|
|
3702
|
+
webappUrl: z10.string().describe("BugBug web app URL for the test run."),
|
|
3703
|
+
errorCode: StepErrorCodeSchema.nullable().describe("Structured BugBug error code for the test run, if any."),
|
|
3704
|
+
duration: z10.string().nullable().describe("Execution duration reported by the API."),
|
|
3705
|
+
started: DateTimeSchema.nullable().describe("Run start timestamp, if available."),
|
|
3706
|
+
ended: DateTimeSchema.nullable().describe("Run end timestamp, if available."),
|
|
3707
|
+
stepsRuns: z10.string().describe("API reference or serialized value containing the step runs payload."),
|
|
3708
|
+
runProfileId: UuidSchema.nullable().describe("Run profile UUID used for this execution, if available."),
|
|
3709
|
+
testId: UuidSchema.nullable().describe("Original test UUID associated with this run, if available."),
|
|
3710
|
+
suiteRunId: UuidSchema.nullable().describe("Parent suite run UUID when the test run belongs to a suite run."),
|
|
3711
|
+
triggeredBy: TriggeredBySchema
|
|
3712
|
+
}).passthrough();
|
|
3713
|
+
var TestRunStateSchema = z10.object({
|
|
3714
|
+
id: UuidSchema.describe("Test run UUID."),
|
|
3715
|
+
modified: DateTimeSchema.describe("Last modification timestamp reported by the API."),
|
|
3716
|
+
status: StatusSchema.optional().describe("Current status of the test run."),
|
|
3717
|
+
webappUrl: z10.string().describe("BugBug web app URL for the test run."),
|
|
3718
|
+
duration: z10.string().nullable().optional().describe("Execution duration reported by the API."),
|
|
3719
|
+
started: DateTimeSchema.nullable().optional().describe("Run start timestamp, if available."),
|
|
3720
|
+
ended: DateTimeSchema.nullable().optional().describe("Run end timestamp, if available.")
|
|
3721
|
+
}).passthrough();
|
|
3722
|
+
var TestRunScreenshotsSchema = z10.object({
|
|
3723
|
+
id: UuidSchema.describe("Test run UUID."),
|
|
3724
|
+
stepsRuns: z10.array(StepRunScreenshotSchema).describe("Screenshots captured for step runs within the test run.")
|
|
3725
|
+
}).passthrough();
|
|
3726
|
+
var SuiteRunSchema = z10.object({
|
|
3727
|
+
id: UuidSchema.describe("Suite run UUID."),
|
|
3728
|
+
name: z10.string().min(1).describe("Suite run name."),
|
|
3729
|
+
queued: DateTimeSchema.nullable().describe("Queue timestamp, if the suite run waited before execution."),
|
|
3730
|
+
status: StatusSchema.describe("Current status of the suite run."),
|
|
3731
|
+
sequence: z10.number().nullable().describe("Sequence number assigned by BugBug, if available."),
|
|
3732
|
+
webappUrl: z10.string().describe("BugBug web app URL for the suite run."),
|
|
3733
|
+
errorCode: StepErrorCodeSchema.nullable().describe("Structured BugBug error code for the suite run, if any."),
|
|
3734
|
+
duration: z10.string().nullable().describe("Execution duration reported by the API."),
|
|
3735
|
+
started: DateTimeSchema.nullable().describe("Run start timestamp, if available."),
|
|
3736
|
+
ended: DateTimeSchema.nullable().describe("Run end timestamp, if available."),
|
|
3737
|
+
profileId: UuidSchema.describe("Run profile UUID used for the suite run."),
|
|
3738
|
+
profileName: z10.string().describe("Run profile name used for the suite run."),
|
|
3739
|
+
testRuns: z10.array(TestRunSchema).describe("Test runs included in the suite run."),
|
|
3740
|
+
triggeredBy: TriggeredBySchema
|
|
3741
|
+
}).passthrough();
|
|
3742
|
+
var SuiteRunStateSchema = z10.object({
|
|
3743
|
+
id: UuidSchema.describe("Suite run UUID."),
|
|
3744
|
+
modified: DateTimeSchema.describe("Last modification timestamp reported by the API."),
|
|
3745
|
+
status: StatusSchema.describe("Current status of the suite run."),
|
|
3746
|
+
webappUrl: z10.string().describe("BugBug web app URL for the suite run."),
|
|
3747
|
+
duration: z10.string().nullable().optional().describe("Execution duration reported by the API."),
|
|
3748
|
+
started: DateTimeSchema.nullable().optional().describe("Run start timestamp, if available."),
|
|
3749
|
+
ended: DateTimeSchema.nullable().optional().describe("Run end timestamp, if available.")
|
|
3750
|
+
}).passthrough();
|
|
3751
|
+
var SuiteRunScreenshotsSchema = z10.object({
|
|
3752
|
+
id: UuidSchema.describe("Suite run UUID."),
|
|
3753
|
+
testsRuns: z10.array(TestRunScreenshotsSchema).describe("Screenshot collections grouped by test run within the suite run.")
|
|
3754
|
+
}).passthrough();
|
|
3755
|
+
var RunVariableSchema = z10.object({
|
|
3756
|
+
key: z10.string().describe("Variable name."),
|
|
3757
|
+
value: z10.string().nullable().optional().describe("Variable value.")
|
|
3758
|
+
});
|
|
3759
|
+
|
|
3760
|
+
// src/index.ts
|
|
3761
|
+
function createBugBug(config) {
|
|
3762
|
+
return new BugBugSDK(config);
|
|
3763
|
+
}
|
|
3764
|
+
export {
|
|
3765
|
+
ASSERTION_STEP_TYPES,
|
|
3766
|
+
AuthModule,
|
|
3767
|
+
AuthenticationError,
|
|
3768
|
+
AuthorizationError,
|
|
3769
|
+
BUGBUG_CLI_OAUTH_CLIENT_ID,
|
|
3770
|
+
BUGBUG_MCP_OAUTH_CLIENT_ID,
|
|
3771
|
+
BUGBUG_OAUTH_CODE_CHALLENGE_METHOD,
|
|
3772
|
+
BugBugError,
|
|
3773
|
+
BugBugSDK,
|
|
3774
|
+
CancellationError,
|
|
3775
|
+
ComponentsModule,
|
|
3776
|
+
ConfigModule,
|
|
3777
|
+
ConfigurationError,
|
|
3778
|
+
CursorSchema,
|
|
3779
|
+
DebugArtifactByTypeSchema,
|
|
3780
|
+
GroupsModule,
|
|
3781
|
+
HttpError,
|
|
3782
|
+
InternalServerError,
|
|
3783
|
+
MissingProjectIdError,
|
|
3784
|
+
NetworkError,
|
|
3785
|
+
NotFoundError,
|
|
3786
|
+
PageSizeSchema,
|
|
3787
|
+
ProfilesModule,
|
|
3788
|
+
ProjectArtifactsModule,
|
|
3789
|
+
ProjectModule,
|
|
3790
|
+
ProjectsModule,
|
|
3791
|
+
RUN_MODES,
|
|
3792
|
+
RateLimitError,
|
|
3793
|
+
RunModeSchema,
|
|
3794
|
+
SDK_ERROR_CODES,
|
|
3795
|
+
STEP_ERROR_CODES,
|
|
3796
|
+
ScreenSizeTypeSchema,
|
|
3797
|
+
SelectorsPresetSchema,
|
|
3798
|
+
ServiceUnavailableError,
|
|
3799
|
+
StatusSchema,
|
|
3800
|
+
StepCreateSchema,
|
|
3801
|
+
StepErrorCodeSchema,
|
|
3802
|
+
StepRunsModule,
|
|
3803
|
+
StepSchema,
|
|
3804
|
+
StepTypeSchema,
|
|
3805
|
+
StepUpdateSchema,
|
|
3806
|
+
StepsModule,
|
|
3807
|
+
SuitesModule,
|
|
3808
|
+
TestsModule,
|
|
3809
|
+
TimeoutError,
|
|
3810
|
+
UnexpectedResponseError,
|
|
3811
|
+
UuidSchema,
|
|
3812
|
+
VISUAL_REGRESSION_RESOLUTION_ACTIONS,
|
|
3813
|
+
VISUAL_REGRESSION_RESOLUTION_TYPE_BY_ACTION,
|
|
3814
|
+
VISUAL_REGRESSION_STEP_TYPES,
|
|
3815
|
+
ValidationError,
|
|
3816
|
+
VariablesModule,
|
|
3817
|
+
VisualRegressionModule,
|
|
3818
|
+
createBugBug,
|
|
3819
|
+
createOAuthCodeChallenge,
|
|
3820
|
+
createOAuthCodeVerifier,
|
|
3821
|
+
createOAuthState,
|
|
3822
|
+
isAssertionStepType,
|
|
3823
|
+
isAuthenticationError,
|
|
3824
|
+
isAuthorizationError,
|
|
3825
|
+
isBugBugError,
|
|
3826
|
+
isCancellationError,
|
|
3827
|
+
isCompletedStatus,
|
|
3828
|
+
isConfigurationError,
|
|
3829
|
+
isEditingStatus,
|
|
3830
|
+
isFailedStatus,
|
|
3831
|
+
isHttpError,
|
|
3832
|
+
isInternalServerError,
|
|
3833
|
+
isMissingProjectIdError,
|
|
3834
|
+
isNetworkError,
|
|
3835
|
+
isNotFoundError,
|
|
3836
|
+
isPassedStatus,
|
|
3837
|
+
isRateLimitError,
|
|
3838
|
+
isRunningStatus,
|
|
3839
|
+
isServiceUnavailableError,
|
|
3840
|
+
isTimeoutError,
|
|
3841
|
+
isUnexpectedResponseError,
|
|
3842
|
+
isValidationError,
|
|
3843
|
+
isVisualRegressionStepType
|
|
3844
|
+
};
|