flakiness 0.211.0 → 0.213.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli/cli.js CHANGED
@@ -2,1476 +2,1760 @@
2
2
 
3
3
  // src/cli/cli.ts
4
4
  import { showReport } from "@flakiness/sdk";
5
- import { Command, Option } from "commander";
6
- import debug2 from "debug";
7
- import path6 from "path";
8
-
9
- // ../package.json
10
- var package_default = {
11
- name: "flakiness",
12
- version: "0.211.0",
13
- type: "module",
14
- private: true,
15
- scripts: {
16
- minor: "./version.mjs minor",
17
- patch: "./version.mjs patch",
18
- dev: "pnpm kubik --env-file=.env.dev -w $(find . -name build.mts) ./app.mts ./github_webhooks.mts",
19
- "dev+billing": "pnpm kubik --env-file=.env.dev+billing -w $(find . -name build.mts) ./app.mts ./stripe_webhooks.mts ./github_webhooks.mts",
20
- prod: "pnpm kubik --env-file=.env.prodlocal -w ./cli/build.mts ./server.mts ./web/build.mts ./experimental/build.mts ./landing/build.mts ./github_webhooks.mts",
21
- build: "pnpm kubik $(find . -name build.mts)",
22
- perf: "node --max-old-space-size=10240 --enable-source-maps --env-file=.env.prodlocal experimental/lib/perf_filter.js"
23
- },
24
- engines: {
25
- node: ">=24"
26
- },
27
- author: "Degu Labs, Inc",
28
- license: "Fair Source 100",
29
- devDependencies: {
30
- "@flakiness/playwright": "catalog:",
31
- "@playwright/test": "catalog:",
32
- "@types/node": "^22.19.3",
33
- esbuild: "^0.27.2",
34
- glob: "catalog:",
35
- kubik: "^0.24.0",
36
- "smee-client": "^5.0.0",
37
- tsx: "^4.21.0",
38
- typescript: "^5.9.3"
39
- }
40
- };
41
-
42
- // src/userSession.ts
43
- import fs from "fs/promises";
44
- import os from "os";
45
- import path from "path";
46
5
 
47
- // ../shared/lib/common/typedHttp.js
48
- var TypedHTTP;
49
- ((TypedHTTP2) => {
50
- TypedHTTP2.StatusCodes = {
51
- Informational: {
52
- CONTINUE: 100,
53
- SWITCHING_PROTOCOLS: 101,
54
- PROCESSING: 102,
55
- EARLY_HINTS: 103
56
- },
57
- Success: {
58
- OK: 200,
59
- CREATED: 201,
60
- ACCEPTED: 202,
61
- NON_AUTHORITATIVE_INFORMATION: 203,
62
- NO_CONTENT: 204,
63
- RESET_CONTENT: 205,
64
- PARTIAL_CONTENT: 206,
65
- MULTI_STATUS: 207
66
- },
67
- Redirection: {
68
- MULTIPLE_CHOICES: 300,
69
- MOVED_PERMANENTLY: 301,
70
- MOVED_TEMPORARILY: 302,
71
- SEE_OTHER: 303,
72
- NOT_MODIFIED: 304,
73
- USE_PROXY: 305,
74
- TEMPORARY_REDIRECT: 307,
75
- PERMANENT_REDIRECT: 308
76
- },
77
- ClientErrors: {
78
- BAD_REQUEST: 400,
79
- UNAUTHORIZED: 401,
80
- PAYMENT_REQUIRED: 402,
81
- FORBIDDEN: 403,
82
- NOT_FOUND: 404,
83
- METHOD_NOT_ALLOWED: 405,
84
- NOT_ACCEPTABLE: 406,
85
- PROXY_AUTHENTICATION_REQUIRED: 407,
86
- REQUEST_TIMEOUT: 408,
87
- CONFLICT: 409,
88
- GONE: 410,
89
- LENGTH_REQUIRED: 411,
90
- PRECONDITION_FAILED: 412,
91
- REQUEST_TOO_LONG: 413,
92
- REQUEST_URI_TOO_LONG: 414,
93
- UNSUPPORTED_MEDIA_TYPE: 415,
94
- REQUESTED_RANGE_NOT_SATISFIABLE: 416,
95
- EXPECTATION_FAILED: 417,
96
- IM_A_TEAPOT: 418,
97
- INSUFFICIENT_SPACE_ON_RESOURCE: 419,
98
- METHOD_FAILURE: 420,
99
- MISDIRECTED_REQUEST: 421,
100
- UNPROCESSABLE_ENTITY: 422,
101
- LOCKED: 423,
102
- FAILED_DEPENDENCY: 424,
103
- UPGRADE_REQUIRED: 426,
104
- PRECONDITION_REQUIRED: 428,
105
- TOO_MANY_REQUESTS: 429,
106
- REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
107
- UNAVAILABLE_FOR_LEGAL_REASONS: 451
108
- },
109
- ServerErrors: {
110
- INTERNAL_SERVER_ERROR: 500,
111
- NOT_IMPLEMENTED: 501,
112
- BAD_GATEWAY: 502,
113
- SERVICE_UNAVAILABLE: 503,
114
- GATEWAY_TIMEOUT: 504,
115
- HTTP_VERSION_NOT_SUPPORTED: 505,
116
- INSUFFICIENT_STORAGE: 507,
117
- NETWORK_AUTHENTICATION_REQUIRED: 511
118
- }
119
- };
120
- const AllErrorCodes = {
121
- ...TypedHTTP2.StatusCodes.ClientErrors,
122
- ...TypedHTTP2.StatusCodes.ServerErrors
123
- };
124
- function assert2(value, code, message) {
125
- if (!value)
126
- throw HttpError.withCode(code, message);
127
- }
128
- TypedHTTP2.assert = assert2;
129
- class HttpError extends Error {
130
- constructor(status, message) {
131
- super(message);
132
- this.status = status;
6
+ // ../node_modules/.pnpm/vlq@2.0.4/node_modules/vlq/src/index.js
7
+ var char_to_integer = {};
8
+ var integer_to_char = {};
9
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".split("").forEach(function(char, i) {
10
+ char_to_integer[char] = i;
11
+ integer_to_char[i] = char;
12
+ });
13
+ function decode(string) {
14
+ let result = [];
15
+ let shift = 0;
16
+ let value = 0;
17
+ for (let i = 0; i < string.length; i += 1) {
18
+ let integer = char_to_integer[string[i]];
19
+ if (integer === void 0) {
20
+ throw new Error("Invalid character (" + string[i] + ")");
133
21
  }
134
- static withCode(code, message) {
135
- const statusCode = AllErrorCodes[code];
136
- const defaultMessage = code.split("_").map((word) => word.charAt(0) + word.slice(1).toLowerCase()).join(" ");
137
- return new HttpError(statusCode, message ?? defaultMessage);
22
+ const has_continuation_bit = integer & 32;
23
+ integer &= 31;
24
+ value += integer << shift;
25
+ if (has_continuation_bit) {
26
+ shift += 5;
27
+ } else {
28
+ const should_negate = value & 1;
29
+ value >>>= 1;
30
+ if (should_negate) {
31
+ result.push(value === 0 ? -2147483648 : -value);
32
+ } else {
33
+ result.push(value);
34
+ }
35
+ value = shift = 0;
138
36
  }
139
37
  }
140
- TypedHTTP2.HttpError = HttpError;
141
- const allHttpMethods = ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"];
142
- function isInformationalResponse(response) {
143
- return response.status >= 100 && response.status < 200;
38
+ return result;
39
+ }
40
+ function encode(value) {
41
+ if (typeof value === "number") {
42
+ return encode_integer(value);
144
43
  }
145
- TypedHTTP2.isInformationalResponse = isInformationalResponse;
146
- function isSuccessResponse(response) {
147
- return response.status >= 200 && response.status < 300;
44
+ let result = "";
45
+ for (let i = 0; i < value.length; i += 1) {
46
+ result += encode_integer(value[i]);
148
47
  }
149
- TypedHTTP2.isSuccessResponse = isSuccessResponse;
150
- function isRedirectResponse(response) {
151
- return response.status >= 300 && response.status < 400;
48
+ return result;
49
+ }
50
+ function encode_integer(num) {
51
+ let result = "";
52
+ if (num < 0) {
53
+ num = -num << 1 | 1;
54
+ } else {
55
+ num <<= 1;
152
56
  }
153
- TypedHTTP2.isRedirectResponse = isRedirectResponse;
154
- function isErrorResponse(response) {
155
- return response.status >= 400 && response.status < 600;
57
+ do {
58
+ let clamped = num & 31;
59
+ num >>>= 5;
60
+ if (num > 0) {
61
+ clamped |= 32;
62
+ }
63
+ result += integer_to_char[clamped];
64
+ } while (num > 0);
65
+ return result;
66
+ }
67
+
68
+ // ../server/lib/common/heap.js
69
+ var Heap = class _Heap {
70
+ constructor(_cmp, elements = []) {
71
+ this._cmp = _cmp;
72
+ this._heap = elements.map(([element, score]) => ({ element, score }));
73
+ for (let idx = this._heap.length - 1; idx >= 0; --idx)
74
+ this._down(idx);
156
75
  }
157
- TypedHTTP2.isErrorResponse = isErrorResponse;
158
- function info(status) {
159
- return { status };
76
+ static createMin(elements = []) {
77
+ return new _Heap((a, b) => a - b, elements);
160
78
  }
161
- TypedHTTP2.info = info;
162
- function ok(data, contentType, status) {
163
- return {
164
- status: status ?? TypedHTTP2.StatusCodes.Success.OK,
165
- contentType,
166
- data
167
- };
79
+ static createMax(elements = []) {
80
+ return new _Heap((a, b) => b - a, elements);
168
81
  }
169
- TypedHTTP2.ok = ok;
170
- function redirect(url, status = 302) {
171
- return { status, url };
82
+ _heap = [];
83
+ _up(idx) {
84
+ const e2 = this._heap[idx];
85
+ while (idx > 0) {
86
+ const parentIdx = idx - 1 >>> 1;
87
+ if (this._cmp(this._heap[parentIdx].score, e2.score) <= 0)
88
+ break;
89
+ this._heap[idx] = this._heap[parentIdx];
90
+ idx = parentIdx;
91
+ }
92
+ this._heap[idx] = e2;
172
93
  }
173
- TypedHTTP2.redirect = redirect;
174
- function error(message, status = TypedHTTP2.StatusCodes.ServerErrors.INTERNAL_SERVER_ERROR) {
175
- return { status, message };
94
+ _down(idx) {
95
+ const N = this._heap.length;
96
+ const e2 = this._heap[idx];
97
+ while (true) {
98
+ const leftIdx = idx * 2 + 1;
99
+ const rightIdx = idx * 2 + 2;
100
+ let smallestIdx;
101
+ if (leftIdx < N && rightIdx < N) {
102
+ smallestIdx = this._cmp(this._heap[leftIdx].score, this._heap[rightIdx].score) < 0 ? leftIdx : rightIdx;
103
+ } else if (leftIdx < N) {
104
+ smallestIdx = leftIdx;
105
+ } else if (rightIdx < N) {
106
+ smallestIdx = rightIdx;
107
+ } else {
108
+ break;
109
+ }
110
+ if (this._cmp(e2.score, this._heap[smallestIdx].score) < 0)
111
+ break;
112
+ this._heap[idx] = this._heap[smallestIdx];
113
+ idx = smallestIdx;
114
+ }
115
+ this._heap[idx] = e2;
176
116
  }
177
- TypedHTTP2.error = error;
178
- class Router {
179
- constructor(_resolveContext) {
180
- this._resolveContext = _resolveContext;
181
- }
182
- static create() {
183
- return new Router(async (e) => e.ctx);
184
- }
185
- rawMethod(method, route) {
186
- return {
187
- [method]: {
188
- method,
189
- input: route.input,
190
- etag: route.etag,
191
- resolveContext: this._resolveContext,
192
- handler: route.handler
193
- }
194
- };
195
- }
196
- get(route) {
197
- return this.rawMethod("GET", {
198
- ...route,
199
- handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result, "application/json"))
200
- });
201
- }
202
- post(route) {
203
- return this.rawMethod("POST", {
204
- ...route,
205
- handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result, "application/json"))
206
- });
207
- }
208
- use(resolveContext) {
209
- return new Router(async (options) => {
210
- const m = await this._resolveContext(options);
211
- return resolveContext({ ...options, ctx: m });
212
- });
213
- }
214
- }
215
- TypedHTTP2.Router = Router;
216
- function createClient(base, fetchCallback) {
217
- function buildUrl(method, path7, input, options) {
218
- const url = new URL(path7.join("/"), base);
219
- const signal = options?.signal;
220
- let body = void 0;
221
- if (method === "GET" && input)
222
- url.searchParams.set("input", JSON.stringify(input));
223
- else if (method !== "GET" && input)
224
- body = JSON.stringify(input);
225
- return {
226
- url,
227
- method,
228
- headers: body ? { "Content-Type": "application/json" } : void 0,
229
- body,
230
- signal
231
- };
232
- }
233
- function fetcher(method, path7, input, methodOptions) {
234
- const options = buildUrl(method, path7, input, methodOptions);
235
- return fetchCallback(options.url, {
236
- method: options.method,
237
- body: options.body,
238
- headers: options.headers,
239
- signal: options.signal
240
- }).then(async (response) => {
241
- if (response.status >= 200 && response.status < 300) {
242
- if (response.headers.get("content-type")?.includes("application/json")) {
243
- const text = await response.text();
244
- return text.length ? JSON.parse(text) : void 0;
245
- }
246
- return await response.arrayBuffer();
247
- }
248
- if (response.status >= 400 && response.status < 600) {
249
- const text = await response.text();
250
- if (text)
251
- throw new HttpError(response.status, `HTTP request failed with status ${response.status}: ${text}`);
252
- else
253
- throw new HttpError(response.status, `HTTP request failed with status ${response.status}`);
254
- }
255
- });
256
- }
257
- function createProxy(path7 = []) {
258
- return new Proxy({}, {
259
- get(target, prop) {
260
- if (typeof prop === "symbol")
261
- return void 0;
262
- if (allHttpMethods.includes(prop)) {
263
- const f = fetcher.bind(null, prop, path7);
264
- f.prepare = buildUrl.bind(null, prop, path7);
265
- return f;
266
- }
267
- const newPath = [...path7, prop];
268
- return createProxy(newPath);
269
- }
270
- });
271
- }
272
- return createProxy();
117
+ push(element, score) {
118
+ this._heap.push({ element, score });
119
+ this._up(this._heap.length - 1);
273
120
  }
274
- TypedHTTP2.createClient = createClient;
275
- })(TypedHTTP || (TypedHTTP = {}));
276
-
277
- // src/serverapi.ts
278
- import debug from "debug";
279
- var log = debug("fk:server_api");
280
- function createServerAPI(endpoint, options) {
281
- endpoint += "/api/";
282
- const fetcher = options?.auth ? (url, init) => fetch(url, {
283
- ...init,
284
- headers: {
285
- ...init.headers,
286
- "Authorization": `Bearer ${options.auth}`
287
- }
288
- }) : fetch;
289
- if (options?.retries)
290
- return TypedHTTP.createClient(endpoint, (url, init) => retryWithBackoff(() => fetcher(url, init), options.retries));
291
- return TypedHTTP.createClient(endpoint, fetcher);
292
- }
293
- async function retryWithBackoff(job, backoff = []) {
294
- for (const timeout of backoff) {
295
- try {
296
- return await job();
297
- } catch (e) {
298
- if (e instanceof AggregateError)
299
- console.error(`[flakiness.io err]`, log(e.errors[0]));
300
- else if (e instanceof Error)
301
- console.error(`[flakiness.io err]`, log(e));
302
- else
303
- console.error(`[flakiness.io err]`, e);
304
- await new Promise((x) => setTimeout(x, timeout));
305
- }
121
+ get size() {
122
+ return this._heap.length;
306
123
  }
307
- return await job();
308
- }
309
-
310
- // src/userSession.ts
311
- var CONFIG_DIR = (() => {
312
- const configDir = process.platform === "darwin" ? path.join(os.homedir(), "Library", "Application Support", "flakiness") : process.platform === "win32" ? path.join(os.homedir(), "AppData", "Roaming", "flakiness") : path.join(os.homedir(), ".config", "flakiness");
313
- return configDir;
314
- })();
315
- var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
316
- var UserSession = class _UserSession {
317
- constructor(_config) {
318
- this._config = _config;
319
- this.api = createServerAPI(this._config.endpoint, { auth: this._config.token });
124
+ peekEntry() {
125
+ return this._heap.length ? [this._heap[0].element, this._heap[0].score] : void 0;
320
126
  }
321
- static async load() {
322
- const data = await fs.readFile(CONFIG_PATH, "utf-8").catch((e) => void 0);
323
- if (!data)
127
+ popEntry() {
128
+ if (!this._heap.length)
324
129
  return void 0;
325
- const json = JSON.parse(data);
326
- return new _UserSession(json);
327
- }
328
- static async remove() {
329
- await fs.unlink(CONFIG_PATH).catch((e) => void 0);
330
- }
331
- api;
332
- endpoint() {
333
- return this._config.endpoint;
334
- }
335
- path() {
336
- return CONFIG_PATH;
337
- }
338
- sessionToken() {
339
- return this._config.token;
340
- }
341
- async save() {
342
- await fs.mkdir(CONFIG_DIR, { recursive: true });
343
- await fs.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
130
+ const entry = this._heap[0];
131
+ const last = this._heap.pop();
132
+ if (!this._heap.length)
133
+ return [entry.element, entry.score];
134
+ this._heap[0] = last;
135
+ this._down(0);
136
+ return [entry.element, entry.score];
344
137
  }
345
138
  };
346
139
 
347
- // src/cli/cmd-access.ts
348
- import chalk from "chalk";
349
-
350
- // src/authenticate.ts
351
- import { GithubOIDC } from "@flakiness/sdk";
352
- async function authenticate(options) {
353
- if (options.accessToken) {
354
- return {
355
- api: createServerAPI(options.endpoint, { auth: options.accessToken }),
356
- method: "access token",
357
- accessToken: options.accessToken,
358
- endpoint: options.endpoint
359
- };
140
+ // ../server/lib/common/sequence.js
141
+ var Sequence = class _Sequence {
142
+ constructor(_seek, length) {
143
+ this._seek = _seek;
144
+ this.length = length;
360
145
  }
361
- const session = await UserSession.load();
362
- if (session && session.endpoint() === options.endpoint) {
363
- return {
364
- api: session.api,
365
- method: "device OAuth",
366
- accessToken: session.sessionToken(),
367
- endpoint: session.endpoint()
368
- };
146
+ static fromList(a) {
147
+ return new _Sequence(
148
+ function(pos) {
149
+ return {
150
+ next() {
151
+ if (pos >= a.length)
152
+ return { done: true, value: void 0 };
153
+ return { done: false, value: a[pos++] };
154
+ }
155
+ };
156
+ },
157
+ a.length
158
+ );
369
159
  }
370
- const githubOIDC = GithubOIDC.initializeFromEnv();
371
- if (githubOIDC) {
372
- if (!options.flakinessProject)
373
- throw new Error("GitHub OIDC requires --project to be specified");
374
- const token = await githubOIDC.createFlakinessAccessToken(options.flakinessProject);
375
- return {
376
- api: createServerAPI(options.endpoint, { auth: token }),
377
- method: "GitHub OIDC",
378
- accessToken: token,
379
- endpoint: options.endpoint
380
- };
160
+ static chain(seqs) {
161
+ const leftsums = [];
162
+ let length = 0;
163
+ for (let i = 0; i < seqs.length; ++i) {
164
+ length += seqs[i].length;
165
+ leftsums.push(length);
166
+ }
167
+ return new _Sequence(
168
+ function(fromIdx) {
169
+ fromIdx = Math.max(0, Math.min(length, fromIdx));
170
+ let idx = _Sequence.fromList(leftsums).partitionPoint(((x) => x <= fromIdx));
171
+ if (idx >= seqs.length) {
172
+ return {
173
+ next: () => ({ done: true, value: void 0 })
174
+ };
175
+ }
176
+ let it = seqs[idx].seek(idx > 0 ? fromIdx - leftsums[idx - 1] : fromIdx);
177
+ return {
178
+ next() {
179
+ let result = it.next();
180
+ while (result.done && ++idx < seqs.length) {
181
+ it = seqs[idx].seek(0);
182
+ result = it.next();
183
+ }
184
+ return result.done ? result : { done: false, value: [result.value, idx] };
185
+ }
186
+ };
187
+ },
188
+ length
189
+ );
381
190
  }
382
- throw new Error('No authentication found. Use --access-token, run "flakiness auth login", or run in GitHub Actions with OIDC enabled.');
383
- }
384
-
385
- // src/cli/cmd-access.ts
386
- async function cmdAccess(options) {
387
- const [orgSlug, projectSlug] = options.flakinessProject.split("/");
388
- if (!orgSlug || !projectSlug)
389
- throw new Error(`Invalid project slug '${options.flakinessProject}'; expected format: org/project`);
390
- const auth2 = await authenticate({
391
- accessToken: options.accessToken,
392
- endpoint: options.endpoint,
393
- flakinessProject: options.flakinessProject
394
- });
395
- const result = await auth2.api.project.checkAccess.GET({ orgSlug, projectSlug });
396
- if (options.json) {
397
- console.log(JSON.stringify(result, null, 2));
398
- } else if (!options.quiet) {
399
- console.log(chalk.bold(`Project: `) + `${orgSlug}/${projectSlug}`);
400
- console.log(chalk.bold(`Auth: `) + (result.auth ?? chalk.dim("none")));
401
- console.log(chalk.bold(`Access: `) + (result.access ? chalk.green("granted") : chalk.red("denied")));
402
- if (result.permissions.length)
403
- console.log(chalk.bold(`Perms: `) + result.permissions.join(", "));
191
+ static merge(sequences, cmp) {
192
+ const length = sequences.reduce((acc, seq) => acc + seq.length, 0);
193
+ return new _Sequence(
194
+ function(fromIdx) {
195
+ fromIdx = Math.max(0, Math.min(length, fromIdx));
196
+ const offsets = quickAdvance(sequences, cmp, fromIdx);
197
+ const entries = [];
198
+ for (let i = 0; i < sequences.length; ++i) {
199
+ const seq = sequences[i];
200
+ const it = seq.seek(offsets[i]);
201
+ const itval = it.next();
202
+ if (!itval.done)
203
+ entries.push([it, itval.value]);
204
+ }
205
+ const heap = new Heap(cmp, entries);
206
+ return {
207
+ next() {
208
+ if (!heap.size)
209
+ return { done: true, value: void 0 };
210
+ ++fromIdx;
211
+ const [it, e2] = heap.popEntry();
212
+ const itval = it.next();
213
+ if (!itval.done)
214
+ heap.push(it, itval.value);
215
+ return { done: false, value: e2 };
216
+ }
217
+ };
218
+ },
219
+ length
220
+ );
404
221
  }
405
- if (!result.access)
406
- process.exitCode = 1;
407
- }
408
-
409
- // ../server/lib/common/knownClientIds.js
410
- var KNOWN_CLIENT_IDS = {
411
- OFFICIAL_WEB: "flakiness-io-official-cli",
412
- OFFICIAL_CLI: "flakiness-io-official-website"
413
- };
414
-
415
- // src/cli/cmd-auth-login.ts
416
- import open from "open";
417
- import os2 from "os";
418
-
419
- // src/cli/cmd-auth-logout.ts
420
- async function cmdAuthLogout() {
421
- const session = await UserSession.load();
422
- if (!session)
423
- return;
424
- const currentSession = await session.api.user.currentSession.GET().catch((e) => void 0);
425
- if (currentSession)
426
- await session.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId }).catch((e) => void 0);
427
- await UserSession.remove();
428
- }
429
-
430
- // src/cli/cmd-auth-whoami.ts
431
- import chalk2 from "chalk";
432
- async function cmdAuthWhoami() {
433
- const session = await UserSession.load();
434
- if (!session || !await printLoggedInUser(session)) {
435
- console.log('Not logged in. Run "flakiness auth login" first.');
436
- process.exit(1);
222
+ static EMPTY = new _Sequence(function* () {
223
+ }, 0);
224
+ seek(idx) {
225
+ const it = this._seek(idx);
226
+ it[Symbol.iterator] = () => it;
227
+ return it;
437
228
  }
438
- }
439
- async function printLoggedInUser(session) {
440
- try {
441
- const user = await session.api.user.whoami.GET();
442
- const superUser = user.isSuperUser ? " " + chalk2.red.bold("[SUPERUSER]") : "";
443
- console.log(`Logged in as ${chalk2.bold(user.userName)} (${user.userLogin})${superUser}`);
444
- console.log(`Endpoint: ${chalk2.cyan(session.endpoint())}`);
445
- return true;
446
- } catch (e) {
447
- return false;
229
+ get(idx) {
230
+ return this.seek(idx).next().value;
448
231
  }
449
- }
450
-
451
- // src/cli/cmd-auth-login.ts
452
- var DEFAULT_FLAKINESS_ENDPOINT = "https://flakiness.io";
453
- async function cmdAuthLogin(endpoint = DEFAULT_FLAKINESS_ENDPOINT) {
454
- await cmdAuthLogout();
455
- const api = createServerAPI(endpoint);
456
- const data = await api.deviceauth.createRequest.POST({
457
- clientId: KNOWN_CLIENT_IDS.OFFICIAL_CLI,
458
- name: os2.hostname()
459
- });
460
- await open(new URL(data.verificationUrl, endpoint).href);
461
- console.log(`Please navigate to ${new URL(data.verificationUrl, endpoint)}`);
462
- let token;
463
- while (Date.now() < data.deadline) {
464
- await new Promise((x) => setTimeout(x, 2e3));
465
- const result = await api.deviceauth.getToken.GET({ deviceCode: data.deviceCode }).catch((e) => void 0);
466
- if (!result) {
467
- console.error(`Authorization request was rejected.`);
468
- process.exit(1);
469
- }
470
- token = result.token;
471
- if (token)
472
- break;
232
+ map(mapper) {
233
+ const originalSeek = this._seek;
234
+ return new _Sequence(
235
+ function(idx) {
236
+ const it = originalSeek(idx);
237
+ return {
238
+ next() {
239
+ const next = it.next();
240
+ if (next.done)
241
+ return next;
242
+ return { done: false, value: mapper(next.value) };
243
+ }
244
+ };
245
+ },
246
+ this.length
247
+ );
473
248
  }
474
- if (!token) {
475
- console.log(`Failed to login.`);
476
- process.exit(1);
249
+ /** Number of elements in sequence that are <= comparator. Only works on sorted sequences. */
250
+ partitionPoint(predicate) {
251
+ let lo = 0, hi = this.length;
252
+ while (lo < hi) {
253
+ const mid = lo + hi >>> 1;
254
+ if (predicate(this.get(mid)))
255
+ lo = mid + 1;
256
+ else
257
+ hi = mid;
258
+ }
259
+ return lo;
477
260
  }
478
- const session = new UserSession({
479
- endpoint,
480
- token
481
- });
482
- if (await printLoggedInUser(session)) {
483
- await session.save();
484
- } else {
485
- console.error(`x Failed to login:`);
261
+ };
262
+ function quickAdvance(sequences, cmp, k) {
263
+ const offsets = new Map(sequences.map((s) => [s, 0]));
264
+ while (offsets.size && k > 0) {
265
+ const t2 = offsets.size;
266
+ const x = Math.max(Math.floor(k / t2 / 2), 1);
267
+ const entries = [];
268
+ for (const [seq, offset] of offsets) {
269
+ if (offset + x <= seq.length)
270
+ entries.push([seq, seq.get(offset + x - 1)]);
271
+ }
272
+ const heap = new Heap(cmp, entries);
273
+ while (heap.size && k > 0 && (x === 1 || k >= x * t2)) {
274
+ k -= x;
275
+ const [seq] = heap.popEntry();
276
+ const offset = offsets.get(seq) + x;
277
+ if (offset === seq.length)
278
+ offsets.delete(seq);
279
+ else
280
+ offsets.set(seq, offset);
281
+ if (offset + x <= seq.length)
282
+ heap.push(seq, seq.get(offset + x - 1));
283
+ }
486
284
  }
285
+ return sequences.map((seq) => offsets.get(seq) ?? seq.length);
487
286
  }
488
287
 
489
- // src/cli/cmd-convert.ts
490
- import { GitWorktree, writeReport } from "@flakiness/sdk";
491
- import fs3 from "fs/promises";
492
- import path3 from "path";
493
-
494
- // src/junit.ts
495
- import { ReportUtils } from "@flakiness/sdk";
496
- import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
497
- import assert from "assert";
498
- import fs2 from "fs";
499
- import mime from "mime";
500
- import path2 from "path";
501
- function getProperties(element) {
502
- const propertiesNodes = element.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "properties");
503
- if (!propertiesNodes.length)
504
- return [];
505
- const result = [];
506
- for (const propertiesNode of propertiesNodes) {
507
- const properties = propertiesNode.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "property");
508
- for (const property of properties) {
509
- const name = property.attributes["name"];
510
- const innerText = property.children.find((node) => node instanceof XmlText);
511
- const value = property.attributes["value"] ?? innerText?.text ?? "";
512
- result.push([name, value]);
288
+ // ../server/lib/common/ranges.js
289
+ var Ranges;
290
+ ((Ranges2) => {
291
+ Ranges2.EMPTY = [];
292
+ Ranges2.FULL = [-Infinity, Infinity];
293
+ function isFull(ranges) {
294
+ return ranges.length === 2 && Object.is(ranges[0], -Infinity) && Object.is(ranges[1], Infinity);
295
+ }
296
+ Ranges2.isFull = isFull;
297
+ function compress(ranges) {
298
+ if (!ranges.length)
299
+ return "";
300
+ if (isInfinite(ranges))
301
+ throw new Error("Compression of infinite ranges is not supported");
302
+ const prepared = [];
303
+ let last = ranges[0] - 1;
304
+ prepared.push(last);
305
+ for (let i = 0; i < ranges.length; i += 2) {
306
+ if (ranges[i] === ranges[i + 1]) {
307
+ prepared.push(-(ranges[i] - last));
308
+ } else {
309
+ prepared.push(ranges[i] - last);
310
+ prepared.push(ranges[i + 1] - ranges[i]);
311
+ }
312
+ last = ranges[i + 1];
513
313
  }
314
+ return encode(prepared);
514
315
  }
515
- return result;
516
- }
517
- function extractErrors(testcase) {
518
- const xmlErrors = testcase.children.filter((e) => e instanceof XmlElement).filter((element) => element.name === "error" || element.name === "failure");
519
- if (!xmlErrors.length)
520
- return void 0;
521
- const errors = [];
522
- for (const xmlErr of xmlErrors) {
523
- const message = [xmlErr.attributes["type"], xmlErr.attributes["message"]].filter((x) => !!x).join(" ");
524
- const xmlStackNodes = xmlErr.children.filter((child) => child instanceof XmlText);
525
- const stack = xmlStackNodes ? xmlStackNodes.map((node) => node.text).join("\n") : void 0;
526
- errors.push({
527
- message,
528
- stack
529
- });
316
+ Ranges2.compress = compress;
317
+ function decompress(compressed) {
318
+ if (!compressed.length)
319
+ return [];
320
+ const prepared = decode(compressed);
321
+ const result = [];
322
+ let last = prepared[0];
323
+ for (let i = 1; i < prepared.length; ++i) {
324
+ if (prepared[i] < 0) {
325
+ result.push(-prepared[i] + last);
326
+ result.push(-prepared[i] + last);
327
+ last -= prepared[i];
328
+ } else {
329
+ result.push(prepared[i] + last);
330
+ last += prepared[i];
331
+ }
332
+ }
333
+ return result;
530
334
  }
531
- return errors;
532
- }
533
- function extractStdout(testcase, stdio) {
534
- const xmlStdio = testcase.children.filter((e) => e instanceof XmlElement).filter((element) => element.name === stdio);
535
- if (!xmlStdio.length)
536
- return void 0;
537
- return xmlStdio.map((node) => node.children.filter((node2) => node2 instanceof XmlText)).flat().map((txtNode) => ({
538
- text: txtNode.text
539
- }));
540
- }
541
- async function parseAttachment(value) {
542
- let absolutePath = path2.resolve(process.cwd(), value);
543
- if (fs2.existsSync(absolutePath))
544
- return ReportUtils.createFileAttachment(mime.getType(absolutePath) ?? "image/png", absolutePath);
545
- return ReportUtils.createDataAttachment("text/plain", Buffer.from(value));
546
- }
547
- async function traverseJUnitReport(context, node) {
548
- const element = node;
549
- if (!(element instanceof XmlElement))
550
- return;
551
- let { currentEnv, currentEnvIndex, currentSuite, report, currentTimeMs, attachments } = context;
552
- if (element.attributes["timestamp"])
553
- currentTimeMs = new Date(element.attributes["timestamp"]).getTime();
554
- if (element.name === "testsuite") {
555
- const file = element.attributes["file"];
556
- const line = parseInt(element.attributes["line"], 10);
557
- const name = element.attributes["name"];
558
- const newSuite = {
559
- title: name ?? file,
560
- location: file && !isNaN(line) ? {
561
- file,
562
- line,
563
- column: 1
564
- } : void 0,
565
- type: name ? "suite" : file ? "file" : "anonymous suite",
566
- suites: [],
567
- tests: []
568
- };
569
- if (currentSuite) {
570
- currentSuite.suites ??= [];
571
- currentSuite.suites.push(newSuite);
335
+ Ranges2.decompress = decompress;
336
+ function toString(ranges) {
337
+ const tokens = [];
338
+ for (let i = 0; i < ranges.length - 1; i += 2) {
339
+ if (ranges[i] === ranges[i + 1])
340
+ tokens.push(ranges[i]);
341
+ else
342
+ tokens.push(`${ranges[i]}-${ranges[i + 1]}`);
343
+ }
344
+ if (!tokens.length)
345
+ return `[]`;
346
+ return `[ ` + tokens.join(", ") + ` ]`;
347
+ }
348
+ Ranges2.toString = toString;
349
+ function popInplace(ranges) {
350
+ if (isInfinite(ranges))
351
+ throw new Error("cannot pop from infinite ranges!");
352
+ const last = ranges.at(-1);
353
+ const prelast = ranges.at(-2);
354
+ if (last === void 0 || prelast === void 0)
355
+ return void 0;
356
+ if (last === prelast) {
357
+ ranges.pop();
358
+ ranges.pop();
572
359
  } else {
573
- report.suites ??= [];
574
- report.suites.push(newSuite);
360
+ ranges[ranges.length - 1] = last - 1;
575
361
  }
576
- currentSuite = newSuite;
577
- const userSuppliedData = getProperties(element);
578
- if (userSuppliedData.length) {
579
- currentEnv = structuredClone(currentEnv);
580
- currentEnv.userSuppliedData ??= {};
581
- for (const [key, value] of userSuppliedData)
582
- currentEnv.userSuppliedData[key] = value;
583
- currentEnvIndex = report.environments.push(currentEnv) - 1;
362
+ return last;
363
+ }
364
+ Ranges2.popInplace = popInplace;
365
+ function* iterate(ranges) {
366
+ if (isInfinite(ranges))
367
+ throw new Error("cannot iterate infinite ranges!");
368
+ for (let i = 0; i < ranges.length - 1; i += 2) {
369
+ for (let j = ranges[i]; j <= ranges[i + 1]; ++j)
370
+ yield j;
584
371
  }
585
- } else if (element.name === "testcase") {
586
- assert(currentSuite);
587
- const file = element.attributes["file"];
588
- const name = element.attributes["name"];
589
- const line = parseInt(element.attributes["line"], 10);
590
- const timeMs = parseFloat(element.attributes["time"]) * 1e3;
591
- const startTimestamp = currentTimeMs;
592
- const duration = timeMs;
593
- currentTimeMs += timeMs;
594
- const annotations = [];
595
- const attachments2 = [];
596
- for (const [key, value] of getProperties(element)) {
597
- if (key.toLowerCase().startsWith("attachment")) {
598
- if (context.ignoreAttachments)
599
- continue;
600
- const attachment = await parseAttachment(value);
601
- context.attachments.set(attachment.id, attachment);
602
- attachments2.push({
603
- id: attachment.id,
604
- contentType: attachment.contentType,
605
- //TODO: better default names for attachments?
606
- name: attachment.type === "file" ? path2.basename(attachment.path) : `attachment`
607
- });
608
- } else {
609
- annotations.push({
610
- type: key,
611
- description: value.length ? value : void 0
612
- });
372
+ }
373
+ Ranges2.iterate = iterate;
374
+ function toSortedList(ranges) {
375
+ if (isInfinite(ranges))
376
+ throw new Error("cannot convert infinite ranges!");
377
+ const list2 = [];
378
+ for (let i = 0; i < ranges.length - 1; i += 2) {
379
+ for (let j = ranges[i]; j <= ranges[i + 1]; ++j)
380
+ list2.push(j);
381
+ }
382
+ return list2;
383
+ }
384
+ Ranges2.toSortedList = toSortedList;
385
+ function toInt32Array(ranges) {
386
+ if (isInfinite(ranges))
387
+ throw new Error("cannot convert infinite ranges!");
388
+ const result = new Int32Array(cardinality(ranges));
389
+ let idx = 0;
390
+ for (let i = 0; i < ranges.length - 1; i += 2) {
391
+ for (let j = ranges[i]; j <= ranges[i + 1]; ++j)
392
+ result[idx++] = j;
393
+ }
394
+ return result;
395
+ }
396
+ Ranges2.toInt32Array = toInt32Array;
397
+ function fromList(x) {
398
+ for (let i = 0; i < x.length - 1; ++i) {
399
+ if (x[i] > x[i + 1]) {
400
+ x = x.toSorted((a, b) => a - b);
401
+ break;
613
402
  }
614
403
  }
615
- const childElements = element.children.filter((child) => child instanceof XmlElement);
616
- const xmlSkippedAnnotation = childElements.find((child) => child.name === "skipped");
617
- if (xmlSkippedAnnotation)
618
- annotations.push({ type: "skipped", description: xmlSkippedAnnotation.attributes["message"] });
619
- const expectedStatus = xmlSkippedAnnotation ? "skipped" : "passed";
620
- const errors = extractErrors(element);
621
- const test = {
622
- title: name,
623
- location: file && !isNaN(line) ? {
624
- file,
625
- line,
626
- column: 1
627
- } : void 0,
628
- attempts: [{
629
- environmentIdx: currentEnvIndex,
630
- expectedStatus,
631
- annotations,
632
- attachments: attachments2,
633
- startTimestamp,
634
- duration,
635
- status: xmlSkippedAnnotation ? "skipped" : errors ? "failed" : "passed",
636
- errors,
637
- stdout: extractStdout(element, "system-out"),
638
- stderr: extractStdout(element, "system-err")
639
- }]
640
- };
641
- currentSuite.tests ??= [];
642
- currentSuite.tests.push(test);
404
+ return fromSortedList(x);
643
405
  }
644
- context = { ...context, currentEnv, currentEnvIndex, currentSuite, currentTimeMs };
645
- for (const child of element.children)
646
- await traverseJUnitReport(context, child);
647
- }
648
- async function parseJUnit(xmls, options) {
649
- const report = {
650
- category: "junit",
651
- commitId: options.commitId,
652
- duration: options.runDuration,
653
- startTimestamp: options.runStartTimestamp,
654
- url: options.runUrl,
655
- environments: [options.defaultEnv],
656
- suites: [],
657
- unattributedErrors: []
658
- };
659
- const context = {
660
- currentEnv: options.defaultEnv,
661
- currentEnvIndex: 0,
662
- currentTimeMs: 0,
663
- report,
664
- currentSuite: void 0,
665
- attachments: /* @__PURE__ */ new Map(),
666
- ignoreAttachments: !!options.ignoreAttachments
667
- };
668
- for (const xml of xmls) {
669
- const doc = parseXml(xml);
670
- for (const element of doc.children)
671
- await traverseJUnitReport(context, element);
406
+ Ranges2.fromList = fromList;
407
+ function from(x) {
408
+ return [x, x];
672
409
  }
673
- return {
674
- report: ReportUtils.normalizeReport(report),
675
- attachments: Array.from(context.attachments.values())
676
- };
677
- }
678
-
679
- // src/cli/cmd-convert.ts
680
- async function cmdConvert(junitPath, options) {
681
- const fullPath = path3.resolve(junitPath);
682
- if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
683
- console.error(`Error: path ${fullPath} is not accessible`);
684
- process.exit(1);
410
+ Ranges2.from = from;
411
+ function fromSortedList(sorted) {
412
+ const ranges = [];
413
+ let rangeStart = 0;
414
+ for (let i = 1; i <= sorted.length; ++i) {
415
+ if (i < sorted.length && sorted[i] - sorted[i - 1] <= 1)
416
+ continue;
417
+ ranges.push(sorted[rangeStart], sorted[i - 1]);
418
+ rangeStart = i;
419
+ }
420
+ return ranges;
685
421
  }
686
- const stat = await fs3.stat(fullPath);
687
- let xmlContents = [];
688
- if (stat.isFile()) {
689
- const xmlContent = await fs3.readFile(fullPath, "utf-8");
690
- xmlContents.push(xmlContent);
691
- } else if (stat.isDirectory()) {
692
- const xmlFiles = await findXmlFiles(fullPath);
693
- if (xmlFiles.length === 0) {
694
- console.error(`Error: No XML files found in directory ${fullPath}`);
695
- process.exit(1);
422
+ Ranges2.fromSortedList = fromSortedList;
423
+ function isInfinite(ranges) {
424
+ return ranges.length > 0 && (Object.is(ranges[0], Infinity) || Object.is(ranges[ranges.length - 1], Infinity));
425
+ }
426
+ Ranges2.isInfinite = isInfinite;
427
+ function includes(ranges, e2) {
428
+ if (!ranges.length)
429
+ return false;
430
+ if (e2 < ranges[0] || ranges[ranges.length - 1] < e2)
431
+ return false;
432
+ if (ranges.length < 17) {
433
+ for (let i = 0; i < ranges.length - 1; i += 2) {
434
+ if (ranges[i] <= e2 && e2 <= ranges[i + 1])
435
+ return true;
436
+ }
437
+ return false;
696
438
  }
697
- console.log(`Found ${xmlFiles.length} XML files`);
698
- for (const xmlFile of xmlFiles) {
699
- const xmlContent = await fs3.readFile(xmlFile, "utf-8");
700
- xmlContents.push(xmlContent);
439
+ let lo = 0, hi = ranges.length;
440
+ while (lo < hi) {
441
+ const mid = lo + hi >>> 1;
442
+ if (ranges[mid] === e2)
443
+ return true;
444
+ if (ranges[mid] < e2)
445
+ lo = mid + 1;
446
+ else
447
+ hi = mid;
701
448
  }
702
- } else {
703
- console.error(`Error: ${fullPath} is neither a file nor a directory`);
704
- process.exit(1);
449
+ return (lo & 1) !== 0;
705
450
  }
706
- let commitId;
707
- if (options.commitId) {
708
- commitId = options.commitId;
709
- } else {
710
- try {
711
- const worktree = GitWorktree.create(process.cwd());
712
- commitId = worktree.headCommitId();
713
- } catch (e) {
714
- console.error("Failed to get git commit info. Please provide --commit-id option.");
715
- process.exit(1);
716
- }
451
+ Ranges2.includes = includes;
452
+ function cardinality(ranges) {
453
+ if (isInfinite(ranges))
454
+ return Infinity;
455
+ let sum = 0;
456
+ for (let i = 0; i < ranges.length - 1; i += 2)
457
+ sum += ranges[i + 1] - ranges[i] + 1;
458
+ return sum;
717
459
  }
718
- const { report, attachments } = await parseJUnit(xmlContents, {
719
- commitId,
720
- defaultEnv: { name: options.envName },
721
- runStartTimestamp: Date.now(),
722
- runDuration: 0
723
- });
724
- if (options.flakinessProject)
725
- report.flakinessProject = options.flakinessProject;
726
- await writeReport(report, attachments, options.outputDir);
727
- console.log(`\u2713 Saved to ${options.outputDir}`);
728
- }
729
- async function findXmlFiles(dir, result = []) {
730
- const entries = await fs3.readdir(dir, { withFileTypes: true });
731
- for (const entry of entries) {
732
- const fullPath = path3.join(dir, entry.name);
733
- if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
734
- result.push(fullPath);
735
- else if (entry.isDirectory())
736
- await findXmlFiles(fullPath, result);
460
+ Ranges2.cardinality = cardinality;
461
+ function offset(ranges, offset2) {
462
+ return ranges.map((x) => x + offset2);
737
463
  }
738
- return result;
739
- }
740
-
741
- // ../node_modules/.pnpm/vlq@2.0.4/node_modules/vlq/src/index.js
742
- var char_to_integer = {};
743
- var integer_to_char = {};
744
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".split("").forEach(function(char, i) {
745
- char_to_integer[char] = i;
746
- integer_to_char[i] = char;
747
- });
748
- function decode(string) {
749
- let result = [];
750
- let shift = 0;
751
- let value = 0;
752
- for (let i = 0; i < string.length; i += 1) {
753
- let integer = char_to_integer[string[i]];
754
- if (integer === void 0) {
755
- throw new Error("Invalid character (" + string[i] + ")");
756
- }
757
- const has_continuation_bit = integer & 32;
758
- integer &= 31;
759
- value += integer << shift;
760
- if (has_continuation_bit) {
761
- shift += 5;
762
- } else {
763
- const should_negate = value & 1;
764
- value >>>= 1;
765
- if (should_negate) {
766
- result.push(value === 0 ? -2147483648 : -value);
464
+ Ranges2.offset = offset;
465
+ function intersect(ranges1, ranges2) {
466
+ const ranges = [];
467
+ if (!ranges1.length || !ranges2.length)
468
+ return ranges;
469
+ if (ranges1[ranges1.length - 1] < ranges2[0] || ranges2[ranges2.length - 1] < ranges1[0])
470
+ return ranges;
471
+ let p1 = 0;
472
+ let p2 = 0;
473
+ while (p1 < ranges1.length - 1 && p2 < ranges2.length - 1) {
474
+ if (ranges1[p1 + 1] < ranges2[p2]) {
475
+ p1 += 2;
476
+ let offset2 = 1;
477
+ while (p1 + offset2 * 2 + 1 < ranges1.length && ranges1[p1 + offset2 * 2 + 1] < ranges2[p2])
478
+ offset2 <<= 1;
479
+ p1 += offset2 >> 1 << 1;
480
+ } else if (ranges2[p2 + 1] < ranges1[p1]) {
481
+ p2 += 2;
482
+ let offset2 = 1;
483
+ while (p2 + offset2 * 2 + 1 < ranges2.length && ranges2[p2 + offset2 * 2 + 1] < ranges1[p1])
484
+ offset2 <<= 1;
485
+ p2 += offset2 >> 1 << 1;
767
486
  } else {
768
- result.push(value);
487
+ const a1 = ranges1[p1], a2 = ranges1[p1 + 1];
488
+ const b1 = ranges2[p2], b2 = ranges2[p2 + 1];
489
+ ranges.push(Math.max(a1, b1), Math.min(a2, b2));
490
+ if (a2 < b2) {
491
+ p1 += 2;
492
+ } else if (a2 > b2) {
493
+ p2 += 2;
494
+ } else {
495
+ p1 += 2;
496
+ p2 += 2;
497
+ }
769
498
  }
770
- value = shift = 0;
771
- }
772
- }
773
- return result;
774
- }
775
- function encode(value) {
776
- if (typeof value === "number") {
777
- return encode_integer(value);
778
- }
779
- let result = "";
780
- for (let i = 0; i < value.length; i += 1) {
781
- result += encode_integer(value[i]);
782
- }
783
- return result;
784
- }
785
- function encode_integer(num) {
786
- let result = "";
787
- if (num < 0) {
788
- num = -num << 1 | 1;
789
- } else {
790
- num <<= 1;
791
- }
792
- do {
793
- let clamped = num & 31;
794
- num >>>= 5;
795
- if (num > 0) {
796
- clamped |= 32;
797
499
  }
798
- result += integer_to_char[clamped];
799
- } while (num > 0);
800
- return result;
801
- }
802
-
803
- // ../server/lib/common/heap.js
804
- var Heap = class _Heap {
805
- constructor(_cmp, elements = []) {
806
- this._cmp = _cmp;
807
- this._heap = elements.map(([element, score]) => ({ element, score }));
808
- for (let idx = this._heap.length - 1; idx >= 0; --idx)
809
- this._down(idx);
810
- }
811
- static createMin(elements = []) {
812
- return new _Heap((a, b) => a - b, elements);
813
- }
814
- static createMax(elements = []) {
815
- return new _Heap((a, b) => b - a, elements);
500
+ return ranges;
816
501
  }
817
- _heap = [];
818
- _up(idx) {
819
- const e = this._heap[idx];
820
- while (idx > 0) {
821
- const parentIdx = idx - 1 >>> 1;
822
- if (this._cmp(this._heap[parentIdx].score, e.score) <= 0)
502
+ Ranges2.intersect = intersect;
503
+ function capAt(ranges, cap) {
504
+ const result = [];
505
+ for (let i = 0; i < ranges.length; i += 2) {
506
+ const start = ranges[i];
507
+ const end = ranges[i + 1];
508
+ if (start > cap)
823
509
  break;
824
- this._heap[idx] = this._heap[parentIdx];
825
- idx = parentIdx;
510
+ if (end <= cap) {
511
+ result.push(start, end);
512
+ } else {
513
+ result.push(start, cap);
514
+ break;
515
+ }
826
516
  }
827
- this._heap[idx] = e;
517
+ return result;
828
518
  }
829
- _down(idx) {
830
- const N = this._heap.length;
831
- const e = this._heap[idx];
832
- while (true) {
833
- const leftIdx = idx * 2 + 1;
834
- const rightIdx = idx * 2 + 2;
835
- let smallestIdx;
836
- if (leftIdx < N && rightIdx < N) {
837
- smallestIdx = this._cmp(this._heap[leftIdx].score, this._heap[rightIdx].score) < 0 ? leftIdx : rightIdx;
838
- } else if (leftIdx < N) {
839
- smallestIdx = leftIdx;
840
- } else if (rightIdx < N) {
841
- smallestIdx = rightIdx;
519
+ Ranges2.capAt = capAt;
520
+ function isIntersecting(ranges1, ranges2) {
521
+ if (!ranges1.length || !ranges2.length)
522
+ return false;
523
+ if (ranges1[ranges1.length - 1] < ranges2[0] || ranges2[ranges2.length - 1] < ranges1[0])
524
+ return false;
525
+ let p1 = 0;
526
+ let p2 = 0;
527
+ while (p1 < ranges1.length - 1 && p2 < ranges2.length - 1) {
528
+ const a1 = ranges1[p1], a2 = ranges1[p1 + 1];
529
+ const b1 = ranges2[p2], b2 = ranges2[p2 + 1];
530
+ if (a2 < b1) {
531
+ p1 += 2;
532
+ let offset2 = 1;
533
+ while (p1 + offset2 * 2 + 1 < ranges1.length && ranges1[p1 + offset2 * 2 + 1] < ranges2[p2])
534
+ offset2 <<= 1;
535
+ p1 += offset2 >> 1 << 1;
536
+ } else if (b2 < a1) {
537
+ p2 += 2;
538
+ let offset2 = 1;
539
+ while (p2 + offset2 * 2 + 1 < ranges2.length && ranges2[p2 + offset2 * 2 + 1] < ranges1[p1])
540
+ offset2 <<= 1;
541
+ p2 += offset2 >> 1 << 1;
842
542
  } else {
843
- break;
543
+ return true;
844
544
  }
845
- if (this._cmp(e.score, this._heap[smallestIdx].score) < 0)
846
- break;
847
- this._heap[idx] = this._heap[smallestIdx];
848
- idx = smallestIdx;
849
545
  }
850
- this._heap[idx] = e;
546
+ return false;
851
547
  }
852
- push(element, score) {
853
- this._heap.push({ element, score });
854
- this._up(this._heap.length - 1);
548
+ Ranges2.isIntersecting = isIntersecting;
549
+ function complement(r) {
550
+ if (r.length === 0)
551
+ return [-Infinity, Infinity];
552
+ const result = [];
553
+ if (!Object.is(r[0], -Infinity))
554
+ result.push(-Infinity, r[0] - 1);
555
+ for (let i = 1; i < r.length - 2; i += 2)
556
+ result.push(r[i] + 1, r[i + 1] - 1);
557
+ if (!Object.is(r[r.length - 1], Infinity))
558
+ result.push(r[r.length - 1] + 1, Infinity);
559
+ return result;
855
560
  }
856
- get size() {
857
- return this._heap.length;
561
+ Ranges2.complement = complement;
562
+ function subtract(ranges1, ranges2) {
563
+ if (!ranges2.length)
564
+ return ranges1;
565
+ return intersect(ranges1, complement(ranges2));
858
566
  }
859
- peekEntry() {
860
- return this._heap.length ? [this._heap[0].element, this._heap[0].score] : void 0;
567
+ Ranges2.subtract = subtract;
568
+ function singleRange(from2, to) {
569
+ return [from2, to];
861
570
  }
862
- popEntry() {
863
- if (!this._heap.length)
864
- return void 0;
865
- const entry = this._heap[0];
866
- const last = this._heap.pop();
867
- if (!this._heap.length)
868
- return [entry.element, entry.score];
869
- this._heap[0] = last;
870
- this._down(0);
871
- return [entry.element, entry.score];
571
+ Ranges2.singleRange = singleRange;
572
+ function unionAll(ranges) {
573
+ let result = Ranges2.EMPTY;
574
+ for (const r of ranges)
575
+ result = union(result, r);
576
+ return result;
872
577
  }
873
- };
874
-
875
- // ../server/lib/common/sequence.js
876
- var Sequence = class _Sequence {
877
- constructor(_seek, length) {
878
- this._seek = _seek;
879
- this.length = length;
578
+ Ranges2.unionAll = unionAll;
579
+ function unionAll_2(rangesIterable) {
580
+ const ranges = Array.isArray(rangesIterable) ? rangesIterable : Array.from(rangesIterable);
581
+ if (ranges.length === 0)
582
+ return [];
583
+ if (ranges.length === 1)
584
+ return ranges[0];
585
+ if (ranges.length === 2)
586
+ return union(ranges[0], ranges[1]);
587
+ const seq = Sequence.merge(ranges.map((r) => intervalSequence(r)), (a, b) => a[0] - b[0]);
588
+ const result = [];
589
+ let last;
590
+ for (const interval of seq.seek(0)) {
591
+ if (!last || last[1] + 1 < interval[0]) {
592
+ result.push(interval);
593
+ last = interval;
594
+ continue;
595
+ }
596
+ if (last[1] < interval[1])
597
+ last[1] = interval[1];
598
+ }
599
+ return result.flat();
880
600
  }
881
- static fromList(a) {
882
- return new _Sequence(
883
- function(pos) {
884
- return {
885
- next() {
886
- if (pos >= a.length)
887
- return { done: true, value: void 0 };
888
- return { done: false, value: a[pos++] };
889
- }
890
- };
891
- },
892
- a.length
893
- );
601
+ Ranges2.unionAll_2 = unionAll_2;
602
+ function intersectAll(ranges) {
603
+ if (!ranges.length)
604
+ return Ranges2.EMPTY;
605
+ let result = Ranges2.FULL;
606
+ for (const range of ranges)
607
+ result = Ranges2.intersect(result, range);
608
+ return result;
894
609
  }
895
- static chain(seqs) {
896
- const leftsums = [];
610
+ Ranges2.intersectAll = intersectAll;
611
+ function domain(ranges) {
612
+ if (!ranges.length)
613
+ return void 0;
614
+ return { min: ranges[0], max: ranges[ranges.length - 1] };
615
+ }
616
+ Ranges2.domain = domain;
617
+ function union(ranges1, ranges2) {
618
+ if (!ranges1.length)
619
+ return ranges2;
620
+ if (!ranges2.length)
621
+ return ranges1;
622
+ if (ranges2[0] < ranges1[0])
623
+ [ranges1, ranges2] = [ranges2, ranges1];
624
+ const r = [ranges1[0], ranges1[1]];
625
+ let p1 = 2, p2 = 0;
626
+ while (p1 < ranges1.length - 1 && p2 < ranges2.length - 1) {
627
+ if (ranges1[p1] <= ranges2[p2]) {
628
+ if (r[r.length - 1] + 1 < ranges1[p1]) {
629
+ r.push(ranges1[p1], ranges1[p1 + 1]);
630
+ p1 += 2;
631
+ } else if (r[r.length - 1] < ranges1[p1 + 1]) {
632
+ r[r.length - 1] = ranges1[p1 + 1];
633
+ p1 += 2;
634
+ } else {
635
+ p1 += 2;
636
+ let offset2 = 1;
637
+ while (p1 + offset2 * 2 + 1 < ranges1.length && r[r.length - 1] >= ranges1[p1 + offset2 * 2 + 1])
638
+ offset2 <<= 1;
639
+ p1 += offset2 >> 1 << 1;
640
+ }
641
+ } else {
642
+ if (r[r.length - 1] + 1 < ranges2[p2]) {
643
+ r.push(ranges2[p2], ranges2[p2 + 1]);
644
+ p2 += 2;
645
+ } else if (r[r.length - 1] < ranges2[p2 + 1]) {
646
+ r[r.length - 1] = ranges2[p2 + 1];
647
+ p2 += 2;
648
+ } else {
649
+ p2 += 2;
650
+ let offset2 = 1;
651
+ while (p2 + offset2 * 2 + 1 < ranges2.length && r[r.length - 1] >= ranges2[p2 + offset2 * 2 + 1])
652
+ offset2 <<= 1;
653
+ p2 += offset2 >> 1 << 1;
654
+ }
655
+ }
656
+ }
657
+ while (p1 < ranges1.length - 1) {
658
+ if (r[r.length - 1] + 1 < ranges1[p1]) {
659
+ r.push(ranges1[p1], ranges1[p1 + 1]);
660
+ p1 += 2;
661
+ } else if (r[r.length - 1] < ranges1[p1 + 1]) {
662
+ r[r.length - 1] = ranges1[p1 + 1];
663
+ p1 += 2;
664
+ } else {
665
+ p1 += 2;
666
+ let offset2 = 1;
667
+ while (p1 + offset2 * 2 + 1 < ranges1.length && r[r.length - 1] >= ranges1[p1 + offset2 * 2 + 1])
668
+ offset2 <<= 1;
669
+ p1 += offset2 >> 1 << 1;
670
+ }
671
+ }
672
+ while (p2 < ranges2.length - 1) {
673
+ if (r[r.length - 1] + 1 < ranges2[p2]) {
674
+ r.push(ranges2[p2], ranges2[p2 + 1]);
675
+ p2 += 2;
676
+ } else if (r[r.length - 1] < ranges2[p2 + 1]) {
677
+ r[r.length - 1] = ranges2[p2 + 1];
678
+ p2 += 2;
679
+ } else {
680
+ p2 += 2;
681
+ let offset2 = 1;
682
+ while (p2 + offset2 * 2 + 1 < ranges2.length && r[r.length - 1] >= ranges2[p2 + offset2 * 2 + 1])
683
+ offset2 <<= 1;
684
+ p2 += offset2 >> 1 << 1;
685
+ }
686
+ }
687
+ return r;
688
+ }
689
+ Ranges2.union = union;
690
+ function intervalSequence(ranges) {
691
+ return new Sequence(function(idx) {
692
+ return {
693
+ next() {
694
+ if (idx * 2 >= ranges.length)
695
+ return { done: true, value: void 0 };
696
+ const value = [ranges[idx * 2], ranges[idx * 2 + 1]];
697
+ ++idx;
698
+ return { done: false, value };
699
+ }
700
+ };
701
+ }, ranges.length >>> 1);
702
+ }
703
+ Ranges2.intervalSequence = intervalSequence;
704
+ function sequence(ranges) {
897
705
  let length = 0;
898
- for (let i = 0; i < seqs.length; ++i) {
899
- length += seqs[i].length;
706
+ const leftsums = [];
707
+ for (let i = 0; i < ranges.length - 1; i += 2) {
708
+ length += ranges[i + 1] - ranges[i] + 1;
900
709
  leftsums.push(length);
901
710
  }
902
- return new _Sequence(
711
+ return new Sequence(
903
712
  function(fromIdx) {
904
713
  fromIdx = Math.max(0, Math.min(length, fromIdx));
905
- let idx = _Sequence.fromList(leftsums).partitionPoint(((x) => x <= fromIdx));
906
- if (idx >= seqs.length) {
907
- return {
908
- next: () => ({ done: true, value: void 0 })
909
- };
910
- }
911
- let it = seqs[idx].seek(idx > 0 ? fromIdx - leftsums[idx - 1] : fromIdx);
714
+ const idx = Sequence.fromList(leftsums).partitionPoint((x) => x <= fromIdx);
715
+ const intervals = Ranges2.intervalSequence(ranges);
716
+ const it = intervals.seek(idx);
717
+ const firstInterval = it.next();
718
+ if (firstInterval.done)
719
+ return { next: () => firstInterval };
720
+ let from2 = firstInterval.value[0] + fromIdx - (idx > 0 ? leftsums[idx - 1] : 0);
721
+ let to = firstInterval.value[1];
912
722
  return {
913
723
  next() {
914
- let result = it.next();
915
- while (result.done && ++idx < seqs.length) {
916
- it = seqs[idx].seek(0);
917
- result = it.next();
724
+ if (from2 > to) {
725
+ const interval = it.next();
726
+ if (interval.done)
727
+ return { done: true, value: void 0 };
728
+ from2 = interval.value[0];
729
+ to = interval.value[1];
918
730
  }
919
- return result.done ? result : { done: false, value: [result.value, idx] };
731
+ return { done: false, value: from2++ };
920
732
  }
921
733
  };
922
734
  },
923
735
  length
924
736
  );
925
737
  }
926
- static merge(sequences, cmp) {
927
- const length = sequences.reduce((acc, seq) => acc + seq.length, 0);
928
- return new _Sequence(
929
- function(fromIdx) {
930
- fromIdx = Math.max(0, Math.min(length, fromIdx));
931
- const offsets = quickAdvance(sequences, cmp, fromIdx);
932
- const entries = [];
933
- for (let i = 0; i < sequences.length; ++i) {
934
- const seq = sequences[i];
935
- const it = seq.seek(offsets[i]);
936
- const itval = it.next();
937
- if (!itval.done)
938
- entries.push([it, itval.value]);
939
- }
940
- const heap = new Heap(cmp, entries);
941
- return {
942
- next() {
943
- if (!heap.size)
944
- return { done: true, value: void 0 };
945
- ++fromIdx;
946
- const [it, e] = heap.popEntry();
947
- const itval = it.next();
948
- if (!itval.done)
949
- heap.push(it, itval.value);
950
- return { done: false, value: e };
951
- }
952
- };
953
- },
954
- length
955
- );
738
+ Ranges2.sequence = sequence;
739
+ })(Ranges || (Ranges = {}));
740
+
741
+ // ../server/lib/common/stats/testOutcomes.js
742
+ var TestOutcomes;
743
+ ((TestOutcomes2) => {
744
+ TestOutcomes2.EMPTY_TESTS = [];
745
+ function regressifyInplace(outcomes, previouslyUnhealthyTests) {
746
+ const newRegressions = Ranges.subtract(outcomes.unexpected, previouslyUnhealthyTests);
747
+ if (newRegressions.length) {
748
+ outcomes.regressed = Ranges.union(outcomes.regressed, newRegressions);
749
+ outcomes.unexpected = Ranges.intersect(outcomes.unexpected, previouslyUnhealthyTests);
750
+ }
956
751
  }
957
- static EMPTY = new _Sequence(function* () {
958
- }, 0);
959
- seek(idx) {
960
- const it = this._seek(idx);
961
- it[Symbol.iterator] = () => it;
962
- return it;
752
+ TestOutcomes2.regressifyInplace = regressifyInplace;
753
+ function regressify(outcomes, previouslyUnhealthyTests) {
754
+ const newRegressions = Ranges.subtract(outcomes.unexpected, previouslyUnhealthyTests);
755
+ return {
756
+ regressed: Ranges.union(outcomes.regressed, newRegressions),
757
+ unexpected: Ranges.intersect(outcomes.unexpected, previouslyUnhealthyTests),
758
+ expected: outcomes.expected,
759
+ flaked: outcomes.flaked,
760
+ skipped: outcomes.skipped
761
+ };
963
762
  }
964
- get(idx) {
965
- return this.seek(idx).next().value;
763
+ TestOutcomes2.regressify = regressify;
764
+ function includes(outcomes, testIdx) {
765
+ return Ranges.includes(outcomes.regressed, testIdx) || Ranges.includes(outcomes.unexpected, testIdx) || Ranges.includes(outcomes.flaked, testIdx) || Ranges.includes(outcomes.expected, testIdx) || Ranges.includes(outcomes.skipped, testIdx);
966
766
  }
967
- map(mapper) {
968
- const originalSeek = this._seek;
969
- return new _Sequence(
970
- function(idx) {
971
- const it = originalSeek(idx);
972
- return {
973
- next() {
974
- const next = it.next();
975
- if (next.done)
976
- return next;
977
- return { done: false, value: mapper(next.value) };
978
- }
979
- };
980
- },
981
- this.length
982
- );
767
+ TestOutcomes2.includes = includes;
768
+ function unhealthyTests(outcomes) {
769
+ return Ranges.unionAll([outcomes.regressed, outcomes.flaked, outcomes.unexpected]);
983
770
  }
984
- /** Number of elements in sequence that are <= comparator. Only works on sorted sequences. */
985
- partitionPoint(predicate) {
986
- let lo = 0, hi = this.length;
987
- while (lo < hi) {
988
- const mid = lo + hi >>> 1;
989
- if (predicate(this.get(mid)))
990
- lo = mid + 1;
991
- else
992
- hi = mid;
993
- }
994
- return lo;
771
+ TestOutcomes2.unhealthyTests = unhealthyTests;
772
+ function unhealthyTestsForAggregatedRun(commitOutcomes) {
773
+ return Ranges.complement(commitOutcomes.regressed);
995
774
  }
996
- };
997
- function quickAdvance(sequences, cmp, k) {
998
- const offsets = new Map(sequences.map((s) => [s, 0]));
999
- while (offsets.size && k > 0) {
1000
- const t = offsets.size;
1001
- const x = Math.max(Math.floor(k / t / 2), 1);
1002
- const entries = [];
1003
- for (const [seq, offset] of offsets) {
1004
- if (offset + x <= seq.length)
1005
- entries.push([seq, seq.get(offset + x - 1)]);
1006
- }
1007
- const heap = new Heap(cmp, entries);
1008
- while (heap.size && k > 0 && (x === 1 || k >= x * t)) {
1009
- k -= x;
1010
- const [seq] = heap.popEntry();
1011
- const offset = offsets.get(seq) + x;
1012
- if (offset === seq.length)
1013
- offsets.delete(seq);
1014
- else
1015
- offsets.set(seq, offset);
1016
- if (offset + x <= seq.length)
1017
- heap.push(seq, seq.get(offset + x - 1));
1018
- }
775
+ TestOutcomes2.unhealthyTestsForAggregatedRun = unhealthyTestsForAggregatedRun;
776
+ function newOutcomeCounts(outcomes) {
777
+ return {
778
+ regressed: outcomes ? Ranges.cardinality(outcomes.regressed) : 0,
779
+ expected: outcomes ? Ranges.cardinality(outcomes.expected) : 0,
780
+ unexpected: outcomes ? Ranges.cardinality(outcomes.unexpected) : 0,
781
+ skipped: outcomes ? Ranges.cardinality(outcomes.skipped) : 0,
782
+ flaked: outcomes ? Ranges.cardinality(outcomes.flaked) : 0
783
+ };
1019
784
  }
1020
- return sequences.map((seq) => offsets.get(seq) ?? seq.length);
1021
- }
1022
-
1023
- // ../server/lib/common/ranges.js
1024
- var Ranges;
1025
- ((Ranges2) => {
1026
- Ranges2.EMPTY = [];
1027
- Ranges2.FULL = [-Infinity, Infinity];
1028
- function isFull(ranges) {
1029
- return ranges.length === 2 && Object.is(ranges[0], -Infinity) && Object.is(ranges[1], Infinity);
785
+ TestOutcomes2.newOutcomeCounts = newOutcomeCounts;
786
+ function accumulateOutcomeCounts(acc, other) {
787
+ acc.regressed += other.regressed;
788
+ acc.expected += other.expected;
789
+ acc.unexpected += other.unexpected;
790
+ acc.skipped += other.skipped;
791
+ acc.flaked += other.flaked;
1030
792
  }
1031
- Ranges2.isFull = isFull;
1032
- function compress(ranges) {
1033
- if (!ranges.length)
1034
- return "";
1035
- if (isInfinite(ranges))
1036
- throw new Error("Compression of infinite ranges is not supported");
1037
- const prepared = [];
1038
- let last = ranges[0] - 1;
1039
- prepared.push(last);
1040
- for (let i = 0; i < ranges.length; i += 2) {
1041
- if (ranges[i] === ranges[i + 1]) {
1042
- prepared.push(-(ranges[i] - last));
1043
- } else {
1044
- prepared.push(ranges[i] - last);
1045
- prepared.push(ranges[i + 1] - ranges[i]);
1046
- }
1047
- last = ranges[i + 1];
1048
- }
1049
- return encode(prepared);
793
+ TestOutcomes2.accumulateOutcomeCounts = accumulateOutcomeCounts;
794
+ function cloneOutcomes(other) {
795
+ return {
796
+ regressed: other.regressed,
797
+ expected: other.expected,
798
+ unexpected: other.unexpected,
799
+ skipped: other.skipped,
800
+ flaked: other.flaked
801
+ };
1050
802
  }
1051
- Ranges2.compress = compress;
1052
- function decompress(compressed) {
1053
- if (!compressed.length)
1054
- return [];
1055
- const prepared = decode(compressed);
1056
- const result = [];
1057
- let last = prepared[0];
1058
- for (let i = 1; i < prepared.length; ++i) {
1059
- if (prepared[i] < 0) {
1060
- result.push(-prepared[i] + last);
1061
- result.push(-prepared[i] + last);
1062
- last -= prepared[i];
1063
- } else {
1064
- result.push(prepared[i] + last);
1065
- last += prepared[i];
1066
- }
1067
- }
1068
- return result;
803
+ TestOutcomes2.cloneOutcomes = cloneOutcomes;
804
+ function newOutcomes() {
805
+ return {
806
+ regressed: Ranges.EMPTY,
807
+ expected: Ranges.EMPTY,
808
+ unexpected: Ranges.EMPTY,
809
+ skipped: Ranges.EMPTY,
810
+ flaked: Ranges.EMPTY
811
+ };
1069
812
  }
1070
- Ranges2.decompress = decompress;
1071
- function toString(ranges) {
1072
- const tokens = [];
1073
- for (let i = 0; i < ranges.length - 1; i += 2) {
1074
- if (ranges[i] === ranges[i + 1])
1075
- tokens.push(ranges[i]);
1076
- else
1077
- tokens.push(`${ranges[i]}-${ranges[i + 1]}`);
1078
- }
1079
- if (!tokens.length)
1080
- return `[]`;
1081
- return `[ ` + tokens.join(", ") + ` ]`;
813
+ TestOutcomes2.newOutcomes = newOutcomes;
814
+ function getTestOutcome(outcomes, testIndex) {
815
+ if (Ranges.includes(outcomes.regressed, testIndex))
816
+ return "regressed";
817
+ if (Ranges.includes(outcomes.unexpected, testIndex))
818
+ return "unexpected";
819
+ if (Ranges.includes(outcomes.expected, testIndex))
820
+ return "expected";
821
+ if (Ranges.includes(outcomes.skipped, testIndex))
822
+ return "skipped";
823
+ if (Ranges.includes(outcomes.flaked, testIndex))
824
+ return "flaked";
825
+ }
826
+ TestOutcomes2.getTestOutcome = getTestOutcome;
827
+ function normalizeInplace(outcomes) {
828
+ let union2 = outcomes.regressed;
829
+ outcomes.unexpected = Ranges.subtract(outcomes.unexpected, union2);
830
+ union2 = Ranges.union(union2, outcomes.unexpected);
831
+ outcomes.flaked = Ranges.subtract(outcomes.flaked, union2);
832
+ union2 = Ranges.union(union2, outcomes.flaked);
833
+ outcomes.expected = Ranges.subtract(outcomes.expected, union2);
834
+ union2 = Ranges.union(union2, outcomes.expected);
835
+ outcomes.skipped = Ranges.subtract(outcomes.skipped, union2);
836
+ }
837
+ TestOutcomes2.normalizeInplace = normalizeInplace;
838
+ function addOverlapAsFlakyInplace(outcomes) {
839
+ const allExpected = Ranges.unionAll([outcomes.flaked, outcomes.expected, outcomes.skipped]);
840
+ const allUnexpected = Ranges.union(outcomes.unexpected, outcomes.regressed);
841
+ const unexpectedToFlaked = Ranges.intersect(allExpected, allUnexpected);
842
+ outcomes.regressed = Ranges.subtract(outcomes.regressed, unexpectedToFlaked);
843
+ outcomes.unexpected = Ranges.subtract(outcomes.unexpected, unexpectedToFlaked);
844
+ outcomes.expected = Ranges.subtract(outcomes.expected, unexpectedToFlaked);
845
+ outcomes.skipped = Ranges.subtract(outcomes.skipped, unexpectedToFlaked);
846
+ outcomes.flaked = Ranges.union(outcomes.flaked, unexpectedToFlaked);
847
+ }
848
+ TestOutcomes2.addOverlapAsFlakyInplace = addOverlapAsFlakyInplace;
849
+ function unionInplace(acc, other) {
850
+ acc.regressed = Ranges.union(acc.regressed, other.regressed);
851
+ acc.expected = Ranges.union(acc.expected, other.expected);
852
+ acc.unexpected = Ranges.union(acc.unexpected, other.unexpected);
853
+ acc.flaked = Ranges.union(acc.flaked, other.flaked);
854
+ acc.skipped = Ranges.union(acc.skipped, other.skipped);
855
+ return acc;
856
+ }
857
+ TestOutcomes2.unionInplace = unionInplace;
858
+ function union(one, another) {
859
+ return {
860
+ regressed: Ranges.union(one.regressed, another.regressed),
861
+ expected: Ranges.union(one.expected, another.expected),
862
+ unexpected: Ranges.union(one.unexpected, another.unexpected),
863
+ flaked: Ranges.union(one.flaked, another.flaked),
864
+ skipped: Ranges.union(one.skipped, another.skipped)
865
+ };
1082
866
  }
1083
- Ranges2.toString = toString;
1084
- function popInplace(ranges) {
1085
- if (isInfinite(ranges))
1086
- throw new Error("cannot pop from infinite ranges!");
1087
- const last = ranges.at(-1);
1088
- const prelast = ranges.at(-2);
1089
- if (last === void 0 || prelast === void 0)
1090
- return void 0;
1091
- if (last === prelast) {
1092
- ranges.pop();
1093
- ranges.pop();
1094
- } else {
1095
- ranges[ranges.length - 1] = last - 1;
1096
- }
1097
- return last;
867
+ TestOutcomes2.union = union;
868
+ function unionAll(outcomes) {
869
+ const acc = newOutcomes();
870
+ for (const outcome of outcomes)
871
+ unionInplace(acc, outcome);
872
+ return acc;
1098
873
  }
1099
- Ranges2.popInplace = popInplace;
1100
- function* iterate(ranges) {
1101
- if (isInfinite(ranges))
1102
- throw new Error("cannot iterate infinite ranges!");
1103
- for (let i = 0; i < ranges.length - 1; i += 2) {
1104
- for (let j = ranges[i]; j <= ranges[i + 1]; ++j)
1105
- yield j;
1106
- }
874
+ TestOutcomes2.unionAll = unionAll;
875
+ function intersectRanges(outcomes, ranges) {
876
+ return {
877
+ regressed: Ranges.intersect(outcomes.regressed, ranges),
878
+ unexpected: Ranges.intersect(outcomes.unexpected, ranges),
879
+ expected: Ranges.intersect(outcomes.expected, ranges),
880
+ skipped: Ranges.intersect(outcomes.skipped, ranges),
881
+ flaked: Ranges.intersect(outcomes.flaked, ranges)
882
+ };
1107
883
  }
1108
- Ranges2.iterate = iterate;
1109
- function toSortedList(ranges) {
1110
- if (isInfinite(ranges))
1111
- throw new Error("cannot convert infinite ranges!");
1112
- const list = [];
1113
- for (let i = 0; i < ranges.length - 1; i += 2) {
1114
- for (let j = ranges[i]; j <= ranges[i + 1]; ++j)
1115
- list.push(j);
1116
- }
1117
- return list;
884
+ TestOutcomes2.intersectRanges = intersectRanges;
885
+ function capAt(outcomes, cutoffTestIndex) {
886
+ return {
887
+ regressed: Ranges.capAt(outcomes.regressed, cutoffTestIndex),
888
+ unexpected: Ranges.capAt(outcomes.unexpected, cutoffTestIndex),
889
+ expected: Ranges.capAt(outcomes.expected, cutoffTestIndex),
890
+ skipped: Ranges.capAt(outcomes.skipped, cutoffTestIndex),
891
+ flaked: Ranges.capAt(outcomes.flaked, cutoffTestIndex)
892
+ };
1118
893
  }
1119
- Ranges2.toSortedList = toSortedList;
1120
- function toInt32Array(ranges) {
1121
- if (isInfinite(ranges))
1122
- throw new Error("cannot convert infinite ranges!");
1123
- const result = new Int32Array(cardinality(ranges));
1124
- let idx = 0;
1125
- for (let i = 0; i < ranges.length - 1; i += 2) {
1126
- for (let j = ranges[i]; j <= ranges[i + 1]; ++j)
1127
- result[idx++] = j;
1128
- }
1129
- return result;
894
+ TestOutcomes2.capAt = capAt;
895
+ function intersectRangesInplace(outcomes, ranges) {
896
+ outcomes.regressed = Ranges.intersect(outcomes.regressed, ranges);
897
+ outcomes.unexpected = Ranges.intersect(outcomes.unexpected, ranges);
898
+ outcomes.expected = Ranges.intersect(outcomes.expected, ranges);
899
+ outcomes.skipped = Ranges.intersect(outcomes.skipped, ranges);
900
+ outcomes.flaked = Ranges.intersect(outcomes.flaked, ranges);
901
+ }
902
+ TestOutcomes2.intersectRangesInplace = intersectRangesInplace;
903
+ function subtractRangesInplace(outcomes, ranges) {
904
+ outcomes.regressed = Ranges.subtract(outcomes.regressed, ranges);
905
+ outcomes.unexpected = Ranges.subtract(outcomes.unexpected, ranges);
906
+ outcomes.expected = Ranges.subtract(outcomes.expected, ranges);
907
+ outcomes.skipped = Ranges.subtract(outcomes.skipped, ranges);
908
+ outcomes.flaked = Ranges.subtract(outcomes.flaked, ranges);
909
+ }
910
+ TestOutcomes2.subtractRangesInplace = subtractRangesInplace;
911
+ function subtractRanges(outcomes, ranges) {
912
+ return {
913
+ regressed: Ranges.subtract(outcomes.regressed, ranges),
914
+ unexpected: Ranges.subtract(outcomes.unexpected, ranges),
915
+ expected: Ranges.subtract(outcomes.expected, ranges),
916
+ skipped: Ranges.subtract(outcomes.skipped, ranges),
917
+ flaked: Ranges.subtract(outcomes.flaked, ranges)
918
+ };
1130
919
  }
1131
- Ranges2.toInt32Array = toInt32Array;
1132
- function fromList(x) {
1133
- for (let i = 0; i < x.length - 1; ++i) {
1134
- if (x[i] > x[i + 1]) {
1135
- x = x.toSorted((a, b) => a - b);
1136
- break;
1137
- }
1138
- }
1139
- return fromSortedList(x);
920
+ TestOutcomes2.subtractRanges = subtractRanges;
921
+ function intersectWithOutcomes(a, b) {
922
+ return {
923
+ regressed: Ranges.intersect(a.regressed, b.regressed),
924
+ unexpected: Ranges.intersect(a.unexpected, b.unexpected),
925
+ expected: Ranges.intersect(a.expected, b.expected),
926
+ skipped: Ranges.intersect(a.skipped, b.skipped),
927
+ flaked: Ranges.intersect(a.flaked, b.flaked)
928
+ };
1140
929
  }
1141
- Ranges2.fromList = fromList;
1142
- function from(x) {
1143
- return [x, x];
930
+ TestOutcomes2.intersectWithOutcomes = intersectWithOutcomes;
931
+ function subtractOutcomes(a, b) {
932
+ return {
933
+ regressed: Ranges.subtract(a.regressed, b.regressed),
934
+ unexpected: Ranges.subtract(a.unexpected, b.unexpected),
935
+ expected: Ranges.subtract(a.expected, b.expected),
936
+ skipped: Ranges.subtract(a.skipped, b.skipped),
937
+ flaked: Ranges.subtract(a.flaked, b.flaked)
938
+ };
1144
939
  }
1145
- Ranges2.from = from;
1146
- function fromSortedList(sorted) {
1147
- const ranges = [];
1148
- let rangeStart = 0;
1149
- for (let i = 1; i <= sorted.length; ++i) {
1150
- if (i < sorted.length && sorted[i] - sorted[i - 1] <= 1)
1151
- continue;
1152
- ranges.push(sorted[rangeStart], sorted[i - 1]);
1153
- rangeStart = i;
1154
- }
1155
- return ranges;
940
+ TestOutcomes2.subtractOutcomes = subtractOutcomes;
941
+ function flatten(outcomes) {
942
+ return Ranges.unionAll([outcomes.expected, outcomes.flaked, outcomes.skipped, outcomes.unexpected, outcomes.regressed]);
1156
943
  }
1157
- Ranges2.fromSortedList = fromSortedList;
1158
- function isInfinite(ranges) {
1159
- return ranges.length > 0 && (Object.is(ranges[0], Infinity) || Object.is(ranges[ranges.length - 1], Infinity));
944
+ TestOutcomes2.flatten = flatten;
945
+ function isEmptyOutcomes(x) {
946
+ return x.regressed.length === 0 && x.expected.length === 0 && x.flaked.length === 0 && x.skipped.length === 0 && x.unexpected.length === 0;
1160
947
  }
1161
- Ranges2.isInfinite = isInfinite;
1162
- function includes(ranges, e) {
1163
- if (!ranges.length)
1164
- return false;
1165
- if (e < ranges[0] || ranges[ranges.length - 1] < e)
1166
- return false;
1167
- if (ranges.length < 17) {
1168
- for (let i = 0; i < ranges.length - 1; i += 2) {
1169
- if (ranges[i] <= e && e <= ranges[i + 1])
1170
- return true;
1171
- }
1172
- return false;
1173
- }
1174
- let lo = 0, hi = ranges.length;
1175
- while (lo < hi) {
1176
- const mid = lo + hi >>> 1;
1177
- if (ranges[mid] === e)
1178
- return true;
1179
- if (ranges[mid] < e)
1180
- lo = mid + 1;
1181
- else
1182
- hi = mid;
1183
- }
1184
- return (lo & 1) !== 0;
948
+ TestOutcomes2.isEmptyOutcomes = isEmptyOutcomes;
949
+ function isEmptyOutcomeCounts(x) {
950
+ return x.regressed === 0 && x.expected === 0 && x.flaked === 0 && x.skipped === 0 && x.unexpected === 0;
1185
951
  }
1186
- Ranges2.includes = includes;
1187
- function cardinality(ranges) {
1188
- if (isInfinite(ranges))
1189
- return Infinity;
1190
- let sum = 0;
1191
- for (let i = 0; i < ranges.length - 1; i += 2)
1192
- sum += ranges[i + 1] - ranges[i] + 1;
1193
- return sum;
952
+ TestOutcomes2.isEmptyOutcomeCounts = isEmptyOutcomeCounts;
953
+ function sumAllCounts(x) {
954
+ return x.regressed + x.expected + x.flaked + x.skipped + x.unexpected;
1194
955
  }
1195
- Ranges2.cardinality = cardinality;
1196
- function offset(ranges, offset2) {
1197
- return ranges.map((x) => x + offset2);
956
+ TestOutcomes2.sumAllCounts = sumAllCounts;
957
+ })(TestOutcomes || (TestOutcomes = {}));
958
+
959
+ // ../server/lib/common/wireTypes.js
960
+ var WireTypes;
961
+ ((WireTypes2) => {
962
+ WireTypes2.SORT_AXES = {
963
+ tests: [
964
+ "name",
965
+ "timeline_name",
966
+ "duration",
967
+ "outcome",
968
+ "duration_trend",
969
+ "flip_rate"
970
+ ],
971
+ timelines: [
972
+ "name",
973
+ "outcome",
974
+ "total_time",
975
+ "unexpected",
976
+ "expected",
977
+ "skipped",
978
+ "flaked",
979
+ "regressed"
980
+ ],
981
+ errors: [
982
+ "name",
983
+ "timelines",
984
+ "tests"
985
+ ],
986
+ annotations: [
987
+ "name",
988
+ "timelines",
989
+ "tests"
990
+ ],
991
+ tags: [
992
+ "name",
993
+ "timelines",
994
+ "tests"
995
+ ],
996
+ commits: [
997
+ "chrono",
998
+ "outcome",
999
+ "unexpected",
1000
+ "expected",
1001
+ "skipped",
1002
+ "flaked",
1003
+ "regressed"
1004
+ ],
1005
+ runs: [
1006
+ "id",
1007
+ "outcome",
1008
+ "chrono",
1009
+ "unexpected",
1010
+ "expected",
1011
+ "skipped",
1012
+ "flaked",
1013
+ "factual_duration",
1014
+ "regressed"
1015
+ ],
1016
+ pullRequests: [
1017
+ "created"
1018
+ ]
1019
+ };
1020
+ WireTypes2.EMPTY_OUTCOMES = TestOutcomes.newOutcomeCounts();
1021
+ })(WireTypes || (WireTypes = {}));
1022
+
1023
+ // src/cli/cli.ts
1024
+ import { Command, Option } from "commander";
1025
+ import debug2 from "debug";
1026
+ import path6 from "path";
1027
+
1028
+ // ../package.json
1029
+ var package_default = {
1030
+ name: "flakiness",
1031
+ version: "0.213.0",
1032
+ type: "module",
1033
+ private: true,
1034
+ scripts: {
1035
+ minor: "./version.mjs minor",
1036
+ patch: "./version.mjs patch",
1037
+ dev: "pnpm kubik --env-file=.env.dev -w $(find . -name build.mts) ./app.mts ./github_webhooks.mts",
1038
+ "dev+billing": "pnpm kubik --env-file=.env.dev+billing -w $(find . -name build.mts) ./app.mts ./stripe_webhooks.mts ./github_webhooks.mts",
1039
+ prod: "pnpm kubik --env-file=.env.prodlocal -w ./cli/build.mts ./server.mts ./web/build.mts ./experimental/build.mts ./landing/build.mts ./github_webhooks.mts",
1040
+ build: "pnpm kubik $(find . -name build.mts)",
1041
+ perf: "node --max-old-space-size=10240 --enable-source-maps --env-file=.env.prodlocal experimental/lib/perf_filter.js"
1042
+ },
1043
+ engines: {
1044
+ node: ">=24"
1045
+ },
1046
+ author: "Degu Labs, Inc",
1047
+ license: "Fair Source 100",
1048
+ devDependencies: {
1049
+ "@flakiness/playwright": "catalog:",
1050
+ "@playwright/test": "catalog:",
1051
+ "@types/node": "^22.19.3",
1052
+ esbuild: "^0.27.2",
1053
+ glob: "catalog:",
1054
+ kubik: "^0.24.0",
1055
+ "smee-client": "^5.0.0",
1056
+ tsx: "^4.21.0",
1057
+ typescript: "^5.9.3"
1198
1058
  }
1199
- Ranges2.offset = offset;
1200
- function intersect(ranges1, ranges2) {
1201
- const ranges = [];
1202
- if (!ranges1.length || !ranges2.length)
1203
- return ranges;
1204
- if (ranges1[ranges1.length - 1] < ranges2[0] || ranges2[ranges2.length - 1] < ranges1[0])
1205
- return ranges;
1206
- let p1 = 0;
1207
- let p2 = 0;
1208
- while (p1 < ranges1.length - 1 && p2 < ranges2.length - 1) {
1209
- if (ranges1[p1 + 1] < ranges2[p2]) {
1210
- p1 += 2;
1211
- let offset2 = 1;
1212
- while (p1 + offset2 * 2 + 1 < ranges1.length && ranges1[p1 + offset2 * 2 + 1] < ranges2[p2])
1213
- offset2 <<= 1;
1214
- p1 += offset2 >> 1 << 1;
1215
- } else if (ranges2[p2 + 1] < ranges1[p1]) {
1216
- p2 += 2;
1217
- let offset2 = 1;
1218
- while (p2 + offset2 * 2 + 1 < ranges2.length && ranges2[p2 + offset2 * 2 + 1] < ranges1[p1])
1219
- offset2 <<= 1;
1220
- p2 += offset2 >> 1 << 1;
1221
- } else {
1222
- const a1 = ranges1[p1], a2 = ranges1[p1 + 1];
1223
- const b1 = ranges2[p2], b2 = ranges2[p2 + 1];
1224
- ranges.push(Math.max(a1, b1), Math.min(a2, b2));
1225
- if (a2 < b2) {
1226
- p1 += 2;
1227
- } else if (a2 > b2) {
1228
- p2 += 2;
1229
- } else {
1230
- p1 += 2;
1231
- p2 += 2;
1232
- }
1233
- }
1059
+ };
1060
+
1061
+ // src/userSession.ts
1062
+ import fs from "fs/promises";
1063
+ import os from "os";
1064
+ import path from "path";
1065
+
1066
+ // ../shared/lib/common/typedHttp.js
1067
+ var TypedHTTP;
1068
+ ((TypedHTTP2) => {
1069
+ TypedHTTP2.StatusCodes = {
1070
+ Informational: {
1071
+ CONTINUE: 100,
1072
+ SWITCHING_PROTOCOLS: 101,
1073
+ PROCESSING: 102,
1074
+ EARLY_HINTS: 103
1075
+ },
1076
+ Success: {
1077
+ OK: 200,
1078
+ CREATED: 201,
1079
+ ACCEPTED: 202,
1080
+ NON_AUTHORITATIVE_INFORMATION: 203,
1081
+ NO_CONTENT: 204,
1082
+ RESET_CONTENT: 205,
1083
+ PARTIAL_CONTENT: 206,
1084
+ MULTI_STATUS: 207
1085
+ },
1086
+ Redirection: {
1087
+ MULTIPLE_CHOICES: 300,
1088
+ MOVED_PERMANENTLY: 301,
1089
+ MOVED_TEMPORARILY: 302,
1090
+ SEE_OTHER: 303,
1091
+ NOT_MODIFIED: 304,
1092
+ USE_PROXY: 305,
1093
+ TEMPORARY_REDIRECT: 307,
1094
+ PERMANENT_REDIRECT: 308
1095
+ },
1096
+ ClientErrors: {
1097
+ BAD_REQUEST: 400,
1098
+ UNAUTHORIZED: 401,
1099
+ PAYMENT_REQUIRED: 402,
1100
+ FORBIDDEN: 403,
1101
+ NOT_FOUND: 404,
1102
+ METHOD_NOT_ALLOWED: 405,
1103
+ NOT_ACCEPTABLE: 406,
1104
+ PROXY_AUTHENTICATION_REQUIRED: 407,
1105
+ REQUEST_TIMEOUT: 408,
1106
+ CONFLICT: 409,
1107
+ GONE: 410,
1108
+ LENGTH_REQUIRED: 411,
1109
+ PRECONDITION_FAILED: 412,
1110
+ REQUEST_TOO_LONG: 413,
1111
+ REQUEST_URI_TOO_LONG: 414,
1112
+ UNSUPPORTED_MEDIA_TYPE: 415,
1113
+ REQUESTED_RANGE_NOT_SATISFIABLE: 416,
1114
+ EXPECTATION_FAILED: 417,
1115
+ IM_A_TEAPOT: 418,
1116
+ INSUFFICIENT_SPACE_ON_RESOURCE: 419,
1117
+ METHOD_FAILURE: 420,
1118
+ MISDIRECTED_REQUEST: 421,
1119
+ UNPROCESSABLE_ENTITY: 422,
1120
+ LOCKED: 423,
1121
+ FAILED_DEPENDENCY: 424,
1122
+ UPGRADE_REQUIRED: 426,
1123
+ PRECONDITION_REQUIRED: 428,
1124
+ TOO_MANY_REQUESTS: 429,
1125
+ REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
1126
+ UNAVAILABLE_FOR_LEGAL_REASONS: 451
1127
+ },
1128
+ ServerErrors: {
1129
+ INTERNAL_SERVER_ERROR: 500,
1130
+ NOT_IMPLEMENTED: 501,
1131
+ BAD_GATEWAY: 502,
1132
+ SERVICE_UNAVAILABLE: 503,
1133
+ GATEWAY_TIMEOUT: 504,
1134
+ HTTP_VERSION_NOT_SUPPORTED: 505,
1135
+ INSUFFICIENT_STORAGE: 507,
1136
+ NETWORK_AUTHENTICATION_REQUIRED: 511
1234
1137
  }
1235
- return ranges;
1138
+ };
1139
+ const AllErrorCodes = {
1140
+ ...TypedHTTP2.StatusCodes.ClientErrors,
1141
+ ...TypedHTTP2.StatusCodes.ServerErrors
1142
+ };
1143
+ function assert2(value, code, message) {
1144
+ if (!value)
1145
+ throw HttpError.withCode(code, message);
1236
1146
  }
1237
- Ranges2.intersect = intersect;
1238
- function capAt(ranges, cap) {
1239
- const result = [];
1240
- for (let i = 0; i < ranges.length; i += 2) {
1241
- const start = ranges[i];
1242
- const end = ranges[i + 1];
1243
- if (start > cap)
1244
- break;
1245
- if (end <= cap) {
1246
- result.push(start, end);
1247
- } else {
1248
- result.push(start, cap);
1249
- break;
1250
- }
1147
+ TypedHTTP2.assert = assert2;
1148
+ class HttpError extends Error {
1149
+ constructor(status, message) {
1150
+ super(message);
1151
+ this.status = status;
1251
1152
  }
1252
- return result;
1253
- }
1254
- Ranges2.capAt = capAt;
1255
- function isIntersecting(ranges1, ranges2) {
1256
- if (!ranges1.length || !ranges2.length)
1257
- return false;
1258
- if (ranges1[ranges1.length - 1] < ranges2[0] || ranges2[ranges2.length - 1] < ranges1[0])
1259
- return false;
1260
- let p1 = 0;
1261
- let p2 = 0;
1262
- while (p1 < ranges1.length - 1 && p2 < ranges2.length - 1) {
1263
- const a1 = ranges1[p1], a2 = ranges1[p1 + 1];
1264
- const b1 = ranges2[p2], b2 = ranges2[p2 + 1];
1265
- if (a2 < b1) {
1266
- p1 += 2;
1267
- let offset2 = 1;
1268
- while (p1 + offset2 * 2 + 1 < ranges1.length && ranges1[p1 + offset2 * 2 + 1] < ranges2[p2])
1269
- offset2 <<= 1;
1270
- p1 += offset2 >> 1 << 1;
1271
- } else if (b2 < a1) {
1272
- p2 += 2;
1273
- let offset2 = 1;
1274
- while (p2 + offset2 * 2 + 1 < ranges2.length && ranges2[p2 + offset2 * 2 + 1] < ranges1[p1])
1275
- offset2 <<= 1;
1276
- p2 += offset2 >> 1 << 1;
1277
- } else {
1278
- return true;
1279
- }
1153
+ static withCode(code, message) {
1154
+ const statusCode = AllErrorCodes[code];
1155
+ const defaultMessage = code.split("_").map((word) => word.charAt(0) + word.slice(1).toLowerCase()).join(" ");
1156
+ return new HttpError(statusCode, message ?? defaultMessage);
1280
1157
  }
1281
- return false;
1282
1158
  }
1283
- Ranges2.isIntersecting = isIntersecting;
1284
- function complement(r) {
1285
- if (r.length === 0)
1286
- return [-Infinity, Infinity];
1287
- const result = [];
1288
- if (!Object.is(r[0], -Infinity))
1289
- result.push(-Infinity, r[0] - 1);
1290
- for (let i = 1; i < r.length - 2; i += 2)
1291
- result.push(r[i] + 1, r[i + 1] - 1);
1292
- if (!Object.is(r[r.length - 1], Infinity))
1293
- result.push(r[r.length - 1] + 1, Infinity);
1294
- return result;
1159
+ TypedHTTP2.HttpError = HttpError;
1160
+ const allHttpMethods = ["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"];
1161
+ function isInformationalResponse(response) {
1162
+ return response.status >= 100 && response.status < 200;
1295
1163
  }
1296
- Ranges2.complement = complement;
1297
- function subtract(ranges1, ranges2) {
1298
- if (!ranges2.length)
1299
- return ranges1;
1300
- return intersect(ranges1, complement(ranges2));
1164
+ TypedHTTP2.isInformationalResponse = isInformationalResponse;
1165
+ function isSuccessResponse(response) {
1166
+ return response.status >= 200 && response.status < 300;
1301
1167
  }
1302
- Ranges2.subtract = subtract;
1303
- function singleRange(from2, to) {
1304
- return [from2, to];
1168
+ TypedHTTP2.isSuccessResponse = isSuccessResponse;
1169
+ function isRedirectResponse(response) {
1170
+ return response.status >= 300 && response.status < 400;
1305
1171
  }
1306
- Ranges2.singleRange = singleRange;
1307
- function unionAll(ranges) {
1308
- let result = Ranges2.EMPTY;
1309
- for (const r of ranges)
1310
- result = union(result, r);
1311
- return result;
1172
+ TypedHTTP2.isRedirectResponse = isRedirectResponse;
1173
+ function isErrorResponse(response) {
1174
+ return response.status >= 400 && response.status < 600;
1312
1175
  }
1313
- Ranges2.unionAll = unionAll;
1314
- function unionAll_2(rangesIterable) {
1315
- const ranges = Array.isArray(rangesIterable) ? rangesIterable : Array.from(rangesIterable);
1316
- if (ranges.length === 0)
1317
- return [];
1318
- if (ranges.length === 1)
1319
- return ranges[0];
1320
- if (ranges.length === 2)
1321
- return union(ranges[0], ranges[1]);
1322
- const seq = Sequence.merge(ranges.map((r) => intervalSequence(r)), (a, b) => a[0] - b[0]);
1323
- const result = [];
1324
- let last;
1325
- for (const interval of seq.seek(0)) {
1326
- if (!last || last[1] + 1 < interval[0]) {
1327
- result.push(interval);
1328
- last = interval;
1329
- continue;
1330
- }
1331
- if (last[1] < interval[1])
1332
- last[1] = interval[1];
1333
- }
1334
- return result.flat();
1176
+ TypedHTTP2.isErrorResponse = isErrorResponse;
1177
+ function info(status) {
1178
+ return { status };
1335
1179
  }
1336
- Ranges2.unionAll_2 = unionAll_2;
1337
- function intersectAll(ranges) {
1338
- if (!ranges.length)
1339
- return Ranges2.EMPTY;
1340
- let result = Ranges2.FULL;
1341
- for (const range of ranges)
1342
- result = Ranges2.intersect(result, range);
1343
- return result;
1180
+ TypedHTTP2.info = info;
1181
+ function ok(data, contentType, status) {
1182
+ return {
1183
+ status: status ?? TypedHTTP2.StatusCodes.Success.OK,
1184
+ contentType,
1185
+ data
1186
+ };
1344
1187
  }
1345
- Ranges2.intersectAll = intersectAll;
1346
- function domain(ranges) {
1347
- if (!ranges.length)
1348
- return void 0;
1349
- return { min: ranges[0], max: ranges[ranges.length - 1] };
1188
+ TypedHTTP2.ok = ok;
1189
+ function redirect(url, status = 302) {
1190
+ return { status, url };
1350
1191
  }
1351
- Ranges2.domain = domain;
1352
- function union(ranges1, ranges2) {
1353
- if (!ranges1.length)
1354
- return ranges2;
1355
- if (!ranges2.length)
1356
- return ranges1;
1357
- if (ranges2[0] < ranges1[0])
1358
- [ranges1, ranges2] = [ranges2, ranges1];
1359
- const r = [ranges1[0], ranges1[1]];
1360
- let p1 = 2, p2 = 0;
1361
- while (p1 < ranges1.length - 1 && p2 < ranges2.length - 1) {
1362
- if (ranges1[p1] <= ranges2[p2]) {
1363
- if (r[r.length - 1] + 1 < ranges1[p1]) {
1364
- r.push(ranges1[p1], ranges1[p1 + 1]);
1365
- p1 += 2;
1366
- } else if (r[r.length - 1] < ranges1[p1 + 1]) {
1367
- r[r.length - 1] = ranges1[p1 + 1];
1368
- p1 += 2;
1369
- } else {
1370
- p1 += 2;
1371
- let offset2 = 1;
1372
- while (p1 + offset2 * 2 + 1 < ranges1.length && r[r.length - 1] >= ranges1[p1 + offset2 * 2 + 1])
1373
- offset2 <<= 1;
1374
- p1 += offset2 >> 1 << 1;
1375
- }
1376
- } else {
1377
- if (r[r.length - 1] + 1 < ranges2[p2]) {
1378
- r.push(ranges2[p2], ranges2[p2 + 1]);
1379
- p2 += 2;
1380
- } else if (r[r.length - 1] < ranges2[p2 + 1]) {
1381
- r[r.length - 1] = ranges2[p2 + 1];
1382
- p2 += 2;
1383
- } else {
1384
- p2 += 2;
1385
- let offset2 = 1;
1386
- while (p2 + offset2 * 2 + 1 < ranges2.length && r[r.length - 1] >= ranges2[p2 + offset2 * 2 + 1])
1387
- offset2 <<= 1;
1388
- p2 += offset2 >> 1 << 1;
1192
+ TypedHTTP2.redirect = redirect;
1193
+ function error(message, status = TypedHTTP2.StatusCodes.ServerErrors.INTERNAL_SERVER_ERROR) {
1194
+ return { status, message };
1195
+ }
1196
+ TypedHTTP2.error = error;
1197
+ class Router {
1198
+ constructor(_resolveContext) {
1199
+ this._resolveContext = _resolveContext;
1200
+ }
1201
+ static create() {
1202
+ return new Router(async (e2) => e2.ctx);
1203
+ }
1204
+ rawMethod(method, route) {
1205
+ return {
1206
+ [method]: {
1207
+ method,
1208
+ input: route.input,
1209
+ etag: route.etag,
1210
+ resolveContext: this._resolveContext,
1211
+ handler: route.handler
1389
1212
  }
1390
- }
1213
+ };
1391
1214
  }
1392
- while (p1 < ranges1.length - 1) {
1393
- if (r[r.length - 1] + 1 < ranges1[p1]) {
1394
- r.push(ranges1[p1], ranges1[p1 + 1]);
1395
- p1 += 2;
1396
- } else if (r[r.length - 1] < ranges1[p1 + 1]) {
1397
- r[r.length - 1] = ranges1[p1 + 1];
1398
- p1 += 2;
1399
- } else {
1400
- p1 += 2;
1401
- let offset2 = 1;
1402
- while (p1 + offset2 * 2 + 1 < ranges1.length && r[r.length - 1] >= ranges1[p1 + offset2 * 2 + 1])
1403
- offset2 <<= 1;
1404
- p1 += offset2 >> 1 << 1;
1405
- }
1215
+ get(route) {
1216
+ return this.rawMethod("GET", {
1217
+ ...route,
1218
+ handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result, "application/json"))
1219
+ });
1220
+ }
1221
+ post(route) {
1222
+ return this.rawMethod("POST", {
1223
+ ...route,
1224
+ handler: (...args) => Promise.resolve(route.handler(...args)).then((result) => TypedHTTP2.ok(result, "application/json"))
1225
+ });
1226
+ }
1227
+ use(resolveContext) {
1228
+ return new Router(async (options) => {
1229
+ const m = await this._resolveContext(options);
1230
+ return resolveContext({ ...options, ctx: m });
1231
+ });
1232
+ }
1233
+ }
1234
+ TypedHTTP2.Router = Router;
1235
+ function createClient(base, fetchCallback) {
1236
+ function buildUrl(method, path7, input, options) {
1237
+ const url = new URL(path7.join("/"), base);
1238
+ const signal = options?.signal;
1239
+ let body = void 0;
1240
+ if (method === "GET" && input)
1241
+ url.searchParams.set("input", JSON.stringify(input));
1242
+ else if (method !== "GET" && input)
1243
+ body = JSON.stringify(input);
1244
+ return {
1245
+ url,
1246
+ method,
1247
+ headers: body ? { "Content-Type": "application/json" } : void 0,
1248
+ body,
1249
+ signal
1250
+ };
1251
+ }
1252
+ function fetcher(method, path7, input, methodOptions) {
1253
+ const options = buildUrl(method, path7, input, methodOptions);
1254
+ return fetchCallback(options.url, {
1255
+ method: options.method,
1256
+ body: options.body,
1257
+ headers: options.headers,
1258
+ signal: options.signal
1259
+ }).then(async (response) => {
1260
+ if (response.status >= 200 && response.status < 300) {
1261
+ if (response.headers.get("content-type")?.includes("application/json")) {
1262
+ const text = await response.text();
1263
+ return text.length ? JSON.parse(text) : void 0;
1264
+ }
1265
+ return await response.arrayBuffer();
1266
+ }
1267
+ if (response.status >= 400 && response.status < 600) {
1268
+ const text = await response.text();
1269
+ if (text)
1270
+ throw new HttpError(response.status, `HTTP request failed with status ${response.status}: ${text}`);
1271
+ else
1272
+ throw new HttpError(response.status, `HTTP request failed with status ${response.status}`);
1273
+ }
1274
+ });
1275
+ }
1276
+ function createProxy(path7 = []) {
1277
+ return new Proxy({}, {
1278
+ get(target, prop) {
1279
+ if (typeof prop === "symbol")
1280
+ return void 0;
1281
+ if (allHttpMethods.includes(prop)) {
1282
+ const f = fetcher.bind(null, prop, path7);
1283
+ f.prepare = buildUrl.bind(null, prop, path7);
1284
+ return f;
1285
+ }
1286
+ const newPath = [...path7, prop];
1287
+ return createProxy(newPath);
1288
+ }
1289
+ });
1290
+ }
1291
+ return createProxy();
1292
+ }
1293
+ TypedHTTP2.createClient = createClient;
1294
+ })(TypedHTTP || (TypedHTTP = {}));
1295
+
1296
+ // src/serverapi.ts
1297
+ import debug from "debug";
1298
+ var log = debug("fk:server_api");
1299
+ function createServerAPI(endpoint, options) {
1300
+ endpoint += "/api/";
1301
+ const fetcher = options?.auth ? (url, init) => fetch(url, {
1302
+ ...init,
1303
+ headers: {
1304
+ ...init.headers,
1305
+ "Authorization": `Bearer ${options.auth}`
1306
+ }
1307
+ }) : fetch;
1308
+ if (options?.retries)
1309
+ return TypedHTTP.createClient(endpoint, (url, init) => retryWithBackoff(() => fetcher(url, init), options.retries));
1310
+ return TypedHTTP.createClient(endpoint, fetcher);
1311
+ }
1312
+ async function retryWithBackoff(job, backoff = []) {
1313
+ for (const timeout of backoff) {
1314
+ try {
1315
+ return await job();
1316
+ } catch (e2) {
1317
+ if (e2 instanceof AggregateError)
1318
+ console.error(`[flakiness.io err]`, log(e2.errors[0]));
1319
+ else if (e2 instanceof Error)
1320
+ console.error(`[flakiness.io err]`, log(e2));
1321
+ else
1322
+ console.error(`[flakiness.io err]`, e2);
1323
+ await new Promise((x) => setTimeout(x, timeout));
1324
+ }
1325
+ }
1326
+ return await job();
1327
+ }
1328
+
1329
+ // src/userSession.ts
1330
+ var CONFIG_DIR = (() => {
1331
+ const configDir = process.platform === "darwin" ? path.join(os.homedir(), "Library", "Application Support", "flakiness") : process.platform === "win32" ? path.join(os.homedir(), "AppData", "Roaming", "flakiness") : path.join(os.homedir(), ".config", "flakiness");
1332
+ return configDir;
1333
+ })();
1334
+ var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
1335
+ var UserSession = class _UserSession {
1336
+ constructor(_config) {
1337
+ this._config = _config;
1338
+ this.api = createServerAPI(this._config.endpoint, { auth: this._config.token });
1339
+ }
1340
+ static async load() {
1341
+ const data = await fs.readFile(CONFIG_PATH, "utf-8").catch((e2) => void 0);
1342
+ if (!data)
1343
+ return void 0;
1344
+ const json = JSON.parse(data);
1345
+ return new _UserSession(json);
1346
+ }
1347
+ static async remove() {
1348
+ await fs.unlink(CONFIG_PATH).catch((e2) => void 0);
1349
+ }
1350
+ api;
1351
+ endpoint() {
1352
+ return this._config.endpoint;
1353
+ }
1354
+ path() {
1355
+ return CONFIG_PATH;
1356
+ }
1357
+ sessionToken() {
1358
+ return this._config.token;
1359
+ }
1360
+ async save() {
1361
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
1362
+ await fs.writeFile(CONFIG_PATH, JSON.stringify(this._config, null, 2));
1363
+ }
1364
+ };
1365
+
1366
+ // src/cli/cmd-access.ts
1367
+ import chalk from "chalk";
1368
+
1369
+ // src/authenticate.ts
1370
+ import { GithubOIDC } from "@flakiness/sdk";
1371
+ async function authenticate(options) {
1372
+ if (options.accessToken) {
1373
+ return {
1374
+ api: createServerAPI(options.endpoint, { auth: options.accessToken }),
1375
+ method: "access token",
1376
+ accessToken: options.accessToken,
1377
+ endpoint: options.endpoint
1378
+ };
1379
+ }
1380
+ const session = await UserSession.load();
1381
+ if (session && session.endpoint() === options.endpoint) {
1382
+ return {
1383
+ api: session.api,
1384
+ method: "device OAuth",
1385
+ accessToken: session.sessionToken(),
1386
+ endpoint: session.endpoint()
1387
+ };
1388
+ }
1389
+ const githubOIDC = GithubOIDC.initializeFromEnv();
1390
+ if (githubOIDC) {
1391
+ if (!options.flakinessProject)
1392
+ throw new Error("GitHub OIDC requires --project to be specified");
1393
+ const token = await githubOIDC.createFlakinessAccessToken(options.flakinessProject);
1394
+ return {
1395
+ api: createServerAPI(options.endpoint, { auth: token }),
1396
+ method: "GitHub OIDC",
1397
+ accessToken: token,
1398
+ endpoint: options.endpoint
1399
+ };
1400
+ }
1401
+ throw new Error('No authentication found. Use --access-token, run "flakiness auth login", or run in GitHub Actions with OIDC enabled.');
1402
+ }
1403
+
1404
+ // src/cli/cmd-access.ts
1405
+ async function cmdAccess(options) {
1406
+ const [orgSlug, projectSlug] = options.flakinessProject.split("/");
1407
+ if (!orgSlug || !projectSlug)
1408
+ throw new Error(`Invalid project slug '${options.flakinessProject}'; expected format: org/project`);
1409
+ const auth2 = await authenticate({
1410
+ accessToken: options.accessToken,
1411
+ endpoint: options.endpoint,
1412
+ flakinessProject: options.flakinessProject
1413
+ });
1414
+ const result = await auth2.api.project.checkAccess.GET({ orgSlug, projectSlug });
1415
+ if (options.json) {
1416
+ console.log(JSON.stringify(result, null, 2));
1417
+ } else if (!options.quiet) {
1418
+ console.log(chalk.bold(`Project: `) + `${orgSlug}/${projectSlug}`);
1419
+ console.log(chalk.bold(`Auth: `) + (result.auth ?? chalk.dim("none")));
1420
+ console.log(chalk.bold(`Access: `) + (result.access ? chalk.green("granted") : chalk.red("denied")));
1421
+ if (result.permissions.length)
1422
+ console.log(chalk.bold(`Perms: `) + result.permissions.join(", "));
1423
+ }
1424
+ if (!result.access)
1425
+ process.exitCode = 1;
1426
+ }
1427
+
1428
+ // ../server/lib/common/knownClientIds.js
1429
+ var KNOWN_CLIENT_IDS = {
1430
+ OFFICIAL_WEB: "flakiness-io-official-cli",
1431
+ OFFICIAL_CLI: "flakiness-io-official-website"
1432
+ };
1433
+
1434
+ // src/cli/cmd-auth-login.ts
1435
+ import open from "open";
1436
+ import os2 from "os";
1437
+
1438
+ // src/cli/cmd-auth-logout.ts
1439
+ async function cmdAuthLogout() {
1440
+ const session = await UserSession.load();
1441
+ if (!session)
1442
+ return;
1443
+ const currentSession = await session.api.user.currentSession.GET().catch((e2) => void 0);
1444
+ if (currentSession)
1445
+ await session.api.user.logoutSession.POST({ sessionId: currentSession.sessionPublicId }).catch((e2) => void 0);
1446
+ await UserSession.remove();
1447
+ }
1448
+
1449
+ // src/cli/cmd-auth-whoami.ts
1450
+ import chalk2 from "chalk";
1451
+ async function cmdAuthWhoami() {
1452
+ const session = await UserSession.load();
1453
+ if (!session || !await printLoggedInUser(session)) {
1454
+ console.log('Not logged in. Run "flakiness auth login" first.');
1455
+ process.exit(1);
1456
+ }
1457
+ }
1458
+ async function printLoggedInUser(session) {
1459
+ try {
1460
+ const user = await session.api.user.whoami.GET();
1461
+ const superUser = user.isSuperUser ? " " + chalk2.red.bold("[SUPERUSER]") : "";
1462
+ console.log(`Logged in as ${chalk2.bold(user.userName)} (${user.userLogin})${superUser}`);
1463
+ console.log(`Endpoint: ${chalk2.cyan(session.endpoint())}`);
1464
+ return true;
1465
+ } catch (e2) {
1466
+ return false;
1467
+ }
1468
+ }
1469
+
1470
+ // src/cli/cmd-auth-login.ts
1471
+ var DEFAULT_FLAKINESS_ENDPOINT = "https://flakiness.io";
1472
+ async function cmdAuthLogin(endpoint = DEFAULT_FLAKINESS_ENDPOINT) {
1473
+ await cmdAuthLogout();
1474
+ const api = createServerAPI(endpoint);
1475
+ const data = await api.deviceauth.createRequest.POST({
1476
+ clientId: KNOWN_CLIENT_IDS.OFFICIAL_CLI,
1477
+ name: os2.hostname()
1478
+ });
1479
+ await open(new URL(data.verificationUrl, endpoint).href);
1480
+ console.log(`Please navigate to ${new URL(data.verificationUrl, endpoint)}`);
1481
+ let token;
1482
+ while (Date.now() < data.deadline) {
1483
+ await new Promise((x) => setTimeout(x, 2e3));
1484
+ const result = await api.deviceauth.getToken.GET({ deviceCode: data.deviceCode }).catch((e2) => void 0);
1485
+ if (!result) {
1486
+ console.error(`Authorization request was rejected.`);
1487
+ process.exit(1);
1488
+ }
1489
+ token = result.token;
1490
+ if (token)
1491
+ break;
1492
+ }
1493
+ if (!token) {
1494
+ console.log(`Failed to login.`);
1495
+ process.exit(1);
1496
+ }
1497
+ const session = new UserSession({
1498
+ endpoint,
1499
+ token
1500
+ });
1501
+ if (await printLoggedInUser(session)) {
1502
+ await session.save();
1503
+ } else {
1504
+ console.error(`x Failed to login:`);
1505
+ }
1506
+ }
1507
+
1508
+ // src/cli/cmd-convert.ts
1509
+ import { GitWorktree, writeReport } from "@flakiness/sdk";
1510
+ import fs3 from "fs/promises";
1511
+ import path3 from "path";
1512
+
1513
+ // src/junit.ts
1514
+ import { ReportUtils } from "@flakiness/sdk";
1515
+ import { parseXml, XmlElement, XmlText } from "@rgrove/parse-xml";
1516
+ import assert from "assert";
1517
+ import fs2 from "fs";
1518
+ import mime from "mime";
1519
+ import path2 from "path";
1520
+ function getProperties(element) {
1521
+ const propertiesNodes = element.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "properties");
1522
+ if (!propertiesNodes.length)
1523
+ return [];
1524
+ const result = [];
1525
+ for (const propertiesNode of propertiesNodes) {
1526
+ const properties = propertiesNode.children.filter((node) => node instanceof XmlElement).filter((node) => node.name === "property");
1527
+ for (const property of properties) {
1528
+ const name = property.attributes["name"];
1529
+ const innerText = property.children.find((node) => node instanceof XmlText);
1530
+ const value = property.attributes["value"] ?? innerText?.text ?? "";
1531
+ result.push([name, value]);
1532
+ }
1533
+ }
1534
+ return result;
1535
+ }
1536
+ function extractErrors(testcase) {
1537
+ const xmlErrors = testcase.children.filter((e2) => e2 instanceof XmlElement).filter((element) => element.name === "error" || element.name === "failure");
1538
+ if (!xmlErrors.length)
1539
+ return void 0;
1540
+ const errors = [];
1541
+ for (const xmlErr of xmlErrors) {
1542
+ const message = [xmlErr.attributes["type"], xmlErr.attributes["message"]].filter((x) => !!x).join(" ");
1543
+ const xmlStackNodes = xmlErr.children.filter((child) => child instanceof XmlText);
1544
+ const stack = xmlStackNodes ? xmlStackNodes.map((node) => node.text).join("\n") : void 0;
1545
+ errors.push({
1546
+ message,
1547
+ stack
1548
+ });
1549
+ }
1550
+ return errors;
1551
+ }
1552
+ function extractStdout(testcase, stdio) {
1553
+ const xmlStdio = testcase.children.filter((e2) => e2 instanceof XmlElement).filter((element) => element.name === stdio);
1554
+ if (!xmlStdio.length)
1555
+ return void 0;
1556
+ return xmlStdio.map((node) => node.children.filter((node2) => node2 instanceof XmlText)).flat().map((txtNode) => ({
1557
+ text: txtNode.text
1558
+ }));
1559
+ }
1560
+ async function parseAttachment(value) {
1561
+ let absolutePath = path2.resolve(process.cwd(), value);
1562
+ if (fs2.existsSync(absolutePath))
1563
+ return ReportUtils.createFileAttachment(mime.getType(absolutePath) ?? "image/png", absolutePath);
1564
+ return ReportUtils.createDataAttachment("text/plain", Buffer.from(value));
1565
+ }
1566
+ async function traverseJUnitReport(context, node) {
1567
+ const element = node;
1568
+ if (!(element instanceof XmlElement))
1569
+ return;
1570
+ let { currentEnv, currentEnvIndex, currentSuite, report, currentTimeMs, attachments } = context;
1571
+ if (element.attributes["timestamp"])
1572
+ currentTimeMs = new Date(element.attributes["timestamp"]).getTime();
1573
+ if (element.name === "testsuite") {
1574
+ const file = element.attributes["file"];
1575
+ const line = parseInt(element.attributes["line"], 10);
1576
+ const name = element.attributes["name"];
1577
+ const newSuite = {
1578
+ title: name ?? file,
1579
+ location: file && !isNaN(line) ? {
1580
+ file,
1581
+ line,
1582
+ column: 1
1583
+ } : void 0,
1584
+ type: name ? "suite" : file ? "file" : "anonymous suite",
1585
+ suites: [],
1586
+ tests: []
1587
+ };
1588
+ if (currentSuite) {
1589
+ currentSuite.suites ??= [];
1590
+ currentSuite.suites.push(newSuite);
1591
+ } else {
1592
+ report.suites ??= [];
1593
+ report.suites.push(newSuite);
1594
+ }
1595
+ currentSuite = newSuite;
1596
+ const userSuppliedData = getProperties(element);
1597
+ if (userSuppliedData.length) {
1598
+ currentEnv = structuredClone(currentEnv);
1599
+ currentEnv.userSuppliedData ??= {};
1600
+ for (const [key, value] of userSuppliedData)
1601
+ currentEnv.userSuppliedData[key] = value;
1602
+ currentEnvIndex = report.environments.push(currentEnv) - 1;
1406
1603
  }
1407
- while (p2 < ranges2.length - 1) {
1408
- if (r[r.length - 1] + 1 < ranges2[p2]) {
1409
- r.push(ranges2[p2], ranges2[p2 + 1]);
1410
- p2 += 2;
1411
- } else if (r[r.length - 1] < ranges2[p2 + 1]) {
1412
- r[r.length - 1] = ranges2[p2 + 1];
1413
- p2 += 2;
1604
+ } else if (element.name === "testcase") {
1605
+ assert(currentSuite);
1606
+ const file = element.attributes["file"];
1607
+ const name = element.attributes["name"];
1608
+ const line = parseInt(element.attributes["line"], 10);
1609
+ const timeMs = parseFloat(element.attributes["time"]) * 1e3;
1610
+ const startTimestamp = currentTimeMs;
1611
+ const duration = timeMs;
1612
+ currentTimeMs += timeMs;
1613
+ const annotations = [];
1614
+ const attachments2 = [];
1615
+ for (const [key, value] of getProperties(element)) {
1616
+ if (key.toLowerCase().startsWith("attachment")) {
1617
+ if (context.ignoreAttachments)
1618
+ continue;
1619
+ const attachment = await parseAttachment(value);
1620
+ context.attachments.set(attachment.id, attachment);
1621
+ attachments2.push({
1622
+ id: attachment.id,
1623
+ contentType: attachment.contentType,
1624
+ //TODO: better default names for attachments?
1625
+ name: attachment.type === "file" ? path2.basename(attachment.path) : `attachment`
1626
+ });
1414
1627
  } else {
1415
- p2 += 2;
1416
- let offset2 = 1;
1417
- while (p2 + offset2 * 2 + 1 < ranges2.length && r[r.length - 1] >= ranges2[p2 + offset2 * 2 + 1])
1418
- offset2 <<= 1;
1419
- p2 += offset2 >> 1 << 1;
1628
+ annotations.push({
1629
+ type: key,
1630
+ description: value.length ? value : void 0
1631
+ });
1420
1632
  }
1421
1633
  }
1422
- return r;
1634
+ const childElements = element.children.filter((child) => child instanceof XmlElement);
1635
+ const xmlSkippedAnnotation = childElements.find((child) => child.name === "skipped");
1636
+ if (xmlSkippedAnnotation)
1637
+ annotations.push({ type: "skipped", description: xmlSkippedAnnotation.attributes["message"] });
1638
+ const expectedStatus = xmlSkippedAnnotation ? "skipped" : "passed";
1639
+ const errors = extractErrors(element);
1640
+ const test = {
1641
+ title: name,
1642
+ location: file && !isNaN(line) ? {
1643
+ file,
1644
+ line,
1645
+ column: 1
1646
+ } : void 0,
1647
+ attempts: [{
1648
+ environmentIdx: currentEnvIndex,
1649
+ expectedStatus,
1650
+ annotations,
1651
+ attachments: attachments2,
1652
+ startTimestamp,
1653
+ duration,
1654
+ status: xmlSkippedAnnotation ? "skipped" : errors ? "failed" : "passed",
1655
+ errors,
1656
+ stdout: extractStdout(element, "system-out"),
1657
+ stderr: extractStdout(element, "system-err")
1658
+ }]
1659
+ };
1660
+ currentSuite.tests ??= [];
1661
+ currentSuite.tests.push(test);
1423
1662
  }
1424
- Ranges2.union = union;
1425
- function intervalSequence(ranges) {
1426
- return new Sequence(function(idx) {
1427
- return {
1428
- next() {
1429
- if (idx * 2 >= ranges.length)
1430
- return { done: true, value: void 0 };
1431
- const value = [ranges[idx * 2], ranges[idx * 2 + 1]];
1432
- ++idx;
1433
- return { done: false, value };
1434
- }
1435
- };
1436
- }, ranges.length >>> 1);
1663
+ context = { ...context, currentEnv, currentEnvIndex, currentSuite, currentTimeMs };
1664
+ for (const child of element.children)
1665
+ await traverseJUnitReport(context, child);
1666
+ }
1667
+ async function parseJUnit(xmls, options) {
1668
+ const report = {
1669
+ category: "junit",
1670
+ commitId: options.commitId,
1671
+ duration: options.runDuration,
1672
+ startTimestamp: options.runStartTimestamp,
1673
+ url: options.runUrl,
1674
+ environments: [options.defaultEnv],
1675
+ suites: [],
1676
+ unattributedErrors: []
1677
+ };
1678
+ const context = {
1679
+ currentEnv: options.defaultEnv,
1680
+ currentEnvIndex: 0,
1681
+ currentTimeMs: 0,
1682
+ report,
1683
+ currentSuite: void 0,
1684
+ attachments: /* @__PURE__ */ new Map(),
1685
+ ignoreAttachments: !!options.ignoreAttachments
1686
+ };
1687
+ for (const xml of xmls) {
1688
+ const doc = parseXml(xml);
1689
+ for (const element of doc.children)
1690
+ await traverseJUnitReport(context, element);
1437
1691
  }
1438
- Ranges2.intervalSequence = intervalSequence;
1439
- function sequence(ranges) {
1440
- let length = 0;
1441
- const leftsums = [];
1442
- for (let i = 0; i < ranges.length - 1; i += 2) {
1443
- length += ranges[i + 1] - ranges[i] + 1;
1444
- leftsums.push(length);
1692
+ return {
1693
+ report: ReportUtils.normalizeReport(report),
1694
+ attachments: Array.from(context.attachments.values())
1695
+ };
1696
+ }
1697
+
1698
+ // src/cli/cmd-convert.ts
1699
+ async function cmdConvert(junitPath, options) {
1700
+ const fullPath = path3.resolve(junitPath);
1701
+ if (!await fs3.access(fullPath, fs3.constants.F_OK).then(() => true).catch(() => false)) {
1702
+ console.error(`Error: path ${fullPath} is not accessible`);
1703
+ process.exit(1);
1704
+ }
1705
+ const stat = await fs3.stat(fullPath);
1706
+ let xmlContents = [];
1707
+ if (stat.isFile()) {
1708
+ const xmlContent = await fs3.readFile(fullPath, "utf-8");
1709
+ xmlContents.push(xmlContent);
1710
+ } else if (stat.isDirectory()) {
1711
+ const xmlFiles = await findXmlFiles(fullPath);
1712
+ if (xmlFiles.length === 0) {
1713
+ console.error(`Error: No XML files found in directory ${fullPath}`);
1714
+ process.exit(1);
1445
1715
  }
1446
- return new Sequence(
1447
- function(fromIdx) {
1448
- fromIdx = Math.max(0, Math.min(length, fromIdx));
1449
- const idx = Sequence.fromList(leftsums).partitionPoint((x) => x <= fromIdx);
1450
- const intervals = Ranges2.intervalSequence(ranges);
1451
- const it = intervals.seek(idx);
1452
- const firstInterval = it.next();
1453
- if (firstInterval.done)
1454
- return { next: () => firstInterval };
1455
- let from2 = firstInterval.value[0] + fromIdx - (idx > 0 ? leftsums[idx - 1] : 0);
1456
- let to = firstInterval.value[1];
1457
- return {
1458
- next() {
1459
- if (from2 > to) {
1460
- const interval = it.next();
1461
- if (interval.done)
1462
- return { done: true, value: void 0 };
1463
- from2 = interval.value[0];
1464
- to = interval.value[1];
1465
- }
1466
- return { done: false, value: from2++ };
1467
- }
1468
- };
1469
- },
1470
- length
1471
- );
1716
+ console.log(`Found ${xmlFiles.length} XML files`);
1717
+ for (const xmlFile of xmlFiles) {
1718
+ const xmlContent = await fs3.readFile(xmlFile, "utf-8");
1719
+ xmlContents.push(xmlContent);
1720
+ }
1721
+ } else {
1722
+ console.error(`Error: ${fullPath} is neither a file nor a directory`);
1723
+ process.exit(1);
1472
1724
  }
1473
- Ranges2.sequence = sequence;
1474
- })(Ranges || (Ranges = {}));
1725
+ let commitId;
1726
+ if (options.commitId) {
1727
+ commitId = options.commitId;
1728
+ } else {
1729
+ try {
1730
+ const worktree = GitWorktree.create(process.cwd());
1731
+ commitId = worktree.headCommitId();
1732
+ } catch (e2) {
1733
+ console.error("Failed to get git commit info. Please provide --commit-id option.");
1734
+ process.exit(1);
1735
+ }
1736
+ }
1737
+ const { report, attachments } = await parseJUnit(xmlContents, {
1738
+ commitId,
1739
+ defaultEnv: { name: options.envName },
1740
+ runStartTimestamp: Date.now(),
1741
+ runDuration: 0
1742
+ });
1743
+ if (options.flakinessProject)
1744
+ report.flakinessProject = options.flakinessProject;
1745
+ await writeReport(report, attachments, options.outputDir);
1746
+ console.log(`\u2713 Saved to ${options.outputDir}`);
1747
+ }
1748
+ async function findXmlFiles(dir, result = []) {
1749
+ const entries = await fs3.readdir(dir, { withFileTypes: true });
1750
+ for (const entry of entries) {
1751
+ const fullPath = path3.join(dir, entry.name);
1752
+ if (entry.isFile() && entry.name.toLowerCase().endsWith(".xml"))
1753
+ result.push(fullPath);
1754
+ else if (entry.isDirectory())
1755
+ await findXmlFiles(fullPath, result);
1756
+ }
1757
+ return result;
1758
+ }
1475
1759
 
1476
1760
  // src/cli/cmd-download.ts
1477
1761
  import fs4 from "fs";
@@ -1565,6 +1849,162 @@ async function downloadRun(api, project, runId) {
1565
1849
  await Promise.all(workerPromises);
1566
1850
  }
1567
1851
 
1852
+ // ../node_modules/.pnpm/xxhash-wasm@1.1.0/node_modules/xxhash-wasm/esm/xxhash-wasm.js
1853
+ var t = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 48, 8, 96, 3, 127, 127, 127, 1, 127, 96, 3, 127, 127, 127, 0, 96, 2, 127, 127, 0, 96, 1, 127, 1, 127, 96, 3, 127, 127, 126, 1, 126, 96, 3, 126, 127, 127, 1, 126, 96, 2, 127, 126, 0, 96, 1, 127, 1, 126, 3, 11, 10, 0, 0, 2, 1, 3, 4, 5, 6, 1, 7, 5, 3, 1, 0, 1, 7, 85, 9, 3, 109, 101, 109, 2, 0, 5, 120, 120, 104, 51, 50, 0, 0, 6, 105, 110, 105, 116, 51, 50, 0, 2, 8, 117, 112, 100, 97, 116, 101, 51, 50, 0, 3, 8, 100, 105, 103, 101, 115, 116, 51, 50, 0, 4, 5, 120, 120, 104, 54, 52, 0, 5, 6, 105, 110, 105, 116, 54, 52, 0, 7, 8, 117, 112, 100, 97, 116, 101, 54, 52, 0, 8, 8, 100, 105, 103, 101, 115, 116, 54, 52, 0, 9, 10, 251, 22, 10, 242, 1, 1, 4, 127, 32, 0, 32, 1, 106, 33, 3, 32, 1, 65, 16, 79, 4, 127, 32, 3, 65, 16, 107, 33, 6, 32, 2, 65, 168, 136, 141, 161, 2, 106, 33, 3, 32, 2, 65, 137, 235, 208, 208, 7, 107, 33, 4, 32, 2, 65, 207, 140, 162, 142, 6, 106, 33, 5, 3, 64, 32, 3, 32, 0, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 3, 32, 4, 32, 0, 65, 4, 106, 34, 0, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 4, 32, 2, 32, 0, 65, 4, 106, 34, 0, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 2, 32, 5, 32, 0, 65, 4, 106, 34, 0, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 5, 32, 6, 32, 0, 65, 4, 106, 34, 0, 79, 13, 0, 11, 32, 2, 65, 12, 119, 32, 5, 65, 18, 119, 106, 32, 4, 65, 7, 119, 106, 32, 3, 65, 1, 119, 106, 5, 32, 2, 65, 177, 207, 217, 178, 1, 106, 11, 32, 1, 106, 32, 0, 32, 1, 65, 15, 113, 16, 1, 11, 146, 1, 0, 32, 1, 32, 2, 106, 33, 2, 3, 64, 32, 1, 65, 4, 106, 32, 2, 75, 69, 4, 64, 32, 0, 32, 1, 40, 2, 0, 65, 189, 220, 202, 149, 124, 108, 106, 65, 17, 119, 65, 175, 214, 211, 190, 2, 108, 33, 0, 32, 1, 65, 4, 106, 33, 1, 12, 1, 11, 11, 3, 64, 32, 1, 32, 2, 79, 69, 4, 64, 32, 0, 32, 1, 45, 0, 0, 65, 177, 207, 217, 178, 1, 108, 106, 65, 11, 119, 65, 177, 243, 221, 241, 121, 108, 33, 0, 32, 1, 65, 1, 106, 33, 1, 12, 1, 11, 11, 32, 0, 32, 0, 65, 15, 118, 115, 65, 247, 148, 175, 175, 120, 108, 34, 0, 65, 13, 118, 32, 0, 115, 65, 189, 220, 202, 149, 124, 108, 34, 0, 65, 16, 118, 32, 0, 115, 11, 63, 0, 32, 0, 65, 8, 106, 32, 1, 65, 168, 136, 141, 161, 2, 106, 54, 2, 0, 32, 0, 65, 12, 106, 32, 1, 65, 137, 235, 208, 208, 7, 107, 54, 2, 0, 32, 0, 65, 16, 106, 32, 1, 54, 2, 0, 32, 0, 65, 20, 106, 32, 1, 65, 207, 140, 162, 142, 6, 106, 54, 2, 0, 11, 195, 4, 1, 6, 127, 32, 1, 32, 2, 106, 33, 6, 32, 0, 65, 24, 106, 33, 4, 32, 0, 65, 40, 106, 40, 2, 0, 33, 3, 32, 0, 32, 0, 40, 2, 0, 32, 2, 106, 54, 2, 0, 32, 0, 65, 4, 106, 34, 5, 32, 5, 40, 2, 0, 32, 2, 65, 16, 79, 32, 0, 40, 2, 0, 65, 16, 79, 114, 114, 54, 2, 0, 32, 2, 32, 3, 106, 65, 16, 73, 4, 64, 32, 3, 32, 4, 106, 32, 1, 32, 2, 252, 10, 0, 0, 32, 0, 65, 40, 106, 32, 2, 32, 3, 106, 54, 2, 0, 15, 11, 32, 3, 4, 64, 32, 3, 32, 4, 106, 32, 1, 65, 16, 32, 3, 107, 34, 2, 252, 10, 0, 0, 32, 0, 65, 8, 106, 34, 3, 32, 3, 40, 2, 0, 32, 4, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 54, 2, 0, 32, 0, 65, 12, 106, 34, 3, 32, 3, 40, 2, 0, 32, 4, 65, 4, 106, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 54, 2, 0, 32, 0, 65, 16, 106, 34, 3, 32, 3, 40, 2, 0, 32, 4, 65, 8, 106, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 54, 2, 0, 32, 0, 65, 20, 106, 34, 3, 32, 3, 40, 2, 0, 32, 4, 65, 12, 106, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 54, 2, 0, 32, 0, 65, 40, 106, 65, 0, 54, 2, 0, 32, 1, 32, 2, 106, 33, 1, 11, 32, 1, 32, 6, 65, 16, 107, 77, 4, 64, 32, 6, 65, 16, 107, 33, 8, 32, 0, 65, 8, 106, 40, 2, 0, 33, 2, 32, 0, 65, 12, 106, 40, 2, 0, 33, 3, 32, 0, 65, 16, 106, 40, 2, 0, 33, 5, 32, 0, 65, 20, 106, 40, 2, 0, 33, 7, 3, 64, 32, 2, 32, 1, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 2, 32, 3, 32, 1, 65, 4, 106, 34, 1, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 3, 32, 5, 32, 1, 65, 4, 106, 34, 1, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 5, 32, 7, 32, 1, 65, 4, 106, 34, 1, 40, 2, 0, 65, 247, 148, 175, 175, 120, 108, 106, 65, 13, 119, 65, 177, 243, 221, 241, 121, 108, 33, 7, 32, 8, 32, 1, 65, 4, 106, 34, 1, 79, 13, 0, 11, 32, 0, 65, 8, 106, 32, 2, 54, 2, 0, 32, 0, 65, 12, 106, 32, 3, 54, 2, 0, 32, 0, 65, 16, 106, 32, 5, 54, 2, 0, 32, 0, 65, 20, 106, 32, 7, 54, 2, 0, 11, 32, 1, 32, 6, 73, 4, 64, 32, 4, 32, 1, 32, 6, 32, 1, 107, 34, 1, 252, 10, 0, 0, 32, 0, 65, 40, 106, 32, 1, 54, 2, 0, 11, 11, 97, 1, 1, 127, 32, 0, 65, 16, 106, 40, 2, 0, 33, 1, 32, 0, 65, 4, 106, 40, 2, 0, 4, 127, 32, 1, 65, 12, 119, 32, 0, 65, 20, 106, 40, 2, 0, 65, 18, 119, 106, 32, 0, 65, 12, 106, 40, 2, 0, 65, 7, 119, 106, 32, 0, 65, 8, 106, 40, 2, 0, 65, 1, 119, 106, 5, 32, 1, 65, 177, 207, 217, 178, 1, 106, 11, 32, 0, 40, 2, 0, 106, 32, 0, 65, 24, 106, 32, 0, 65, 40, 106, 40, 2, 0, 16, 1, 11, 255, 3, 2, 3, 126, 1, 127, 32, 0, 32, 1, 106, 33, 6, 32, 1, 65, 32, 79, 4, 126, 32, 6, 65, 32, 107, 33, 6, 32, 2, 66, 214, 235, 130, 238, 234, 253, 137, 245, 224, 0, 124, 33, 3, 32, 2, 66, 177, 169, 172, 193, 173, 184, 212, 166, 61, 125, 33, 4, 32, 2, 66, 249, 234, 208, 208, 231, 201, 161, 228, 225, 0, 124, 33, 5, 3, 64, 32, 3, 32, 0, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 3, 32, 4, 32, 0, 65, 8, 106, 34, 0, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 4, 32, 2, 32, 0, 65, 8, 106, 34, 0, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 2, 32, 5, 32, 0, 65, 8, 106, 34, 0, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 5, 32, 6, 32, 0, 65, 8, 106, 34, 0, 79, 13, 0, 11, 32, 2, 66, 12, 137, 32, 5, 66, 18, 137, 124, 32, 4, 66, 7, 137, 124, 32, 3, 66, 1, 137, 124, 32, 3, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 4, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 2, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 5, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 5, 32, 2, 66, 197, 207, 217, 178, 241, 229, 186, 234, 39, 124, 11, 32, 1, 173, 124, 32, 0, 32, 1, 65, 31, 113, 16, 6, 11, 134, 2, 0, 32, 1, 32, 2, 106, 33, 2, 3, 64, 32, 2, 32, 1, 65, 8, 106, 79, 4, 64, 32, 1, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 32, 0, 133, 66, 27, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 33, 0, 32, 1, 65, 8, 106, 33, 1, 12, 1, 11, 11, 32, 1, 65, 4, 106, 32, 2, 77, 4, 64, 32, 0, 32, 1, 53, 2, 0, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 23, 137, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 249, 243, 221, 241, 153, 246, 153, 171, 22, 124, 33, 0, 32, 1, 65, 4, 106, 33, 1, 11, 3, 64, 32, 1, 32, 2, 73, 4, 64, 32, 0, 32, 1, 49, 0, 0, 66, 197, 207, 217, 178, 241, 229, 186, 234, 39, 126, 133, 66, 11, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 0, 32, 1, 65, 1, 106, 33, 1, 12, 1, 11, 11, 32, 0, 32, 0, 66, 33, 136, 133, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 34, 0, 32, 0, 66, 29, 136, 133, 66, 249, 243, 221, 241, 153, 246, 153, 171, 22, 126, 34, 0, 32, 0, 66, 32, 136, 133, 11, 77, 0, 32, 0, 65, 8, 106, 32, 1, 66, 214, 235, 130, 238, 234, 253, 137, 245, 224, 0, 124, 55, 3, 0, 32, 0, 65, 16, 106, 32, 1, 66, 177, 169, 172, 193, 173, 184, 212, 166, 61, 125, 55, 3, 0, 32, 0, 65, 24, 106, 32, 1, 55, 3, 0, 32, 0, 65, 32, 106, 32, 1, 66, 249, 234, 208, 208, 231, 201, 161, 228, 225, 0, 124, 55, 3, 0, 11, 244, 4, 2, 3, 127, 4, 126, 32, 1, 32, 2, 106, 33, 5, 32, 0, 65, 40, 106, 33, 4, 32, 0, 65, 200, 0, 106, 40, 2, 0, 33, 3, 32, 0, 32, 0, 41, 3, 0, 32, 2, 173, 124, 55, 3, 0, 32, 2, 32, 3, 106, 65, 32, 73, 4, 64, 32, 3, 32, 4, 106, 32, 1, 32, 2, 252, 10, 0, 0, 32, 0, 65, 200, 0, 106, 32, 2, 32, 3, 106, 54, 2, 0, 15, 11, 32, 3, 4, 64, 32, 3, 32, 4, 106, 32, 1, 65, 32, 32, 3, 107, 34, 2, 252, 10, 0, 0, 32, 0, 65, 8, 106, 34, 3, 32, 3, 41, 3, 0, 32, 4, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 55, 3, 0, 32, 0, 65, 16, 106, 34, 3, 32, 3, 41, 3, 0, 32, 4, 65, 8, 106, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 55, 3, 0, 32, 0, 65, 24, 106, 34, 3, 32, 3, 41, 3, 0, 32, 4, 65, 16, 106, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 55, 3, 0, 32, 0, 65, 32, 106, 34, 3, 32, 3, 41, 3, 0, 32, 4, 65, 24, 106, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 55, 3, 0, 32, 0, 65, 200, 0, 106, 65, 0, 54, 2, 0, 32, 1, 32, 2, 106, 33, 1, 11, 32, 1, 65, 32, 106, 32, 5, 77, 4, 64, 32, 5, 65, 32, 107, 33, 2, 32, 0, 65, 8, 106, 41, 3, 0, 33, 6, 32, 0, 65, 16, 106, 41, 3, 0, 33, 7, 32, 0, 65, 24, 106, 41, 3, 0, 33, 8, 32, 0, 65, 32, 106, 41, 3, 0, 33, 9, 3, 64, 32, 6, 32, 1, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 6, 32, 7, 32, 1, 65, 8, 106, 34, 1, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 7, 32, 8, 32, 1, 65, 8, 106, 34, 1, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 8, 32, 9, 32, 1, 65, 8, 106, 34, 1, 41, 3, 0, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 124, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 33, 9, 32, 2, 32, 1, 65, 8, 106, 34, 1, 79, 13, 0, 11, 32, 0, 65, 8, 106, 32, 6, 55, 3, 0, 32, 0, 65, 16, 106, 32, 7, 55, 3, 0, 32, 0, 65, 24, 106, 32, 8, 55, 3, 0, 32, 0, 65, 32, 106, 32, 9, 55, 3, 0, 11, 32, 1, 32, 5, 73, 4, 64, 32, 4, 32, 1, 32, 5, 32, 1, 107, 34, 1, 252, 10, 0, 0, 32, 0, 65, 200, 0, 106, 32, 1, 54, 2, 0, 11, 11, 188, 2, 1, 5, 126, 32, 0, 65, 24, 106, 41, 3, 0, 33, 1, 32, 0, 41, 3, 0, 34, 2, 66, 32, 90, 4, 126, 32, 0, 65, 8, 106, 41, 3, 0, 34, 3, 66, 1, 137, 32, 0, 65, 16, 106, 41, 3, 0, 34, 4, 66, 7, 137, 124, 32, 1, 66, 12, 137, 32, 0, 65, 32, 106, 41, 3, 0, 34, 5, 66, 18, 137, 124, 124, 32, 3, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 4, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 1, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 32, 5, 66, 207, 214, 211, 190, 210, 199, 171, 217, 66, 126, 66, 31, 137, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 133, 66, 135, 149, 175, 175, 152, 182, 222, 155, 158, 127, 126, 66, 157, 163, 181, 234, 131, 177, 141, 138, 250, 0, 125, 5, 32, 1, 66, 197, 207, 217, 178, 241, 229, 186, 234, 39, 124, 11, 32, 2, 124, 32, 0, 65, 40, 106, 32, 2, 66, 31, 131, 167, 16, 6, 11]);
1854
+ async function e() {
1855
+ return (function(t2) {
1856
+ const { exports: { mem: e2, xxh32: n, xxh64: r, init32: i, update32: a, digest32: o, init64: s, update64: u, digest64: c } } = t2;
1857
+ let h = new Uint8Array(e2.buffer);
1858
+ function g(t3, n2) {
1859
+ if (e2.buffer.byteLength < t3 + n2) {
1860
+ const r2 = Math.ceil((t3 + n2 - e2.buffer.byteLength) / 65536);
1861
+ e2.grow(r2), h = new Uint8Array(e2.buffer);
1862
+ }
1863
+ }
1864
+ function f(t3, e3, n2, r2, i2, a2) {
1865
+ g(t3);
1866
+ const o2 = new Uint8Array(t3);
1867
+ return h.set(o2), n2(0, e3), o2.set(h.subarray(0, t3)), { update(e4) {
1868
+ let n3;
1869
+ return h.set(o2), "string" == typeof e4 ? (g(3 * e4.length, t3), n3 = w.encodeInto(e4, h.subarray(t3)).written) : (g(e4.byteLength, t3), h.set(e4, t3), n3 = e4.byteLength), r2(0, t3, n3), o2.set(h.subarray(0, t3)), this;
1870
+ }, digest: () => (h.set(o2), a2(i2(0))) };
1871
+ }
1872
+ function y(t3) {
1873
+ return t3 >>> 0;
1874
+ }
1875
+ const b = 2n ** 64n - 1n;
1876
+ function d(t3) {
1877
+ return t3 & b;
1878
+ }
1879
+ const w = new TextEncoder(), l = 0, p = 0n;
1880
+ function x(t3, e3 = l) {
1881
+ return g(3 * t3.length, 0), y(n(0, w.encodeInto(t3, h).written, e3));
1882
+ }
1883
+ function L(t3, e3 = p) {
1884
+ return g(3 * t3.length, 0), d(r(0, w.encodeInto(t3, h).written, e3));
1885
+ }
1886
+ return { h32: x, h32ToString: (t3, e3 = l) => x(t3, e3).toString(16).padStart(8, "0"), h32Raw: (t3, e3 = l) => (g(t3.byteLength, 0), h.set(t3), y(n(0, t3.byteLength, e3))), create32: (t3 = l) => f(48, t3, i, a, o, y), h64: L, h64ToString: (t3, e3 = p) => L(t3, e3).toString(16).padStart(16, "0"), h64Raw: (t3, e3 = p) => (g(t3.byteLength, 0), h.set(t3), d(r(0, t3.byteLength, e3))), create64: (t3 = p) => f(88, t3, s, u, c, d) };
1887
+ })((await WebAssembly.instantiate(t)).instance);
1888
+ }
1889
+
1890
+ // ../shared/lib/common/utils.js
1891
+ var xxHasher = await e();
1892
+ function humanReadableMs(ms) {
1893
+ if (+ms < 1e-5)
1894
+ return `0 ms`;
1895
+ if (+ms < 1)
1896
+ return `${Math.round(+ms * 1e3)} \xB5s`;
1897
+ ms = Math.round(+ms);
1898
+ let seconds = +ms / 1e3;
1899
+ if (seconds < 1)
1900
+ return `${ms} ms`;
1901
+ if (seconds < 60)
1902
+ return `${seconds.toFixed(1)} sec`;
1903
+ seconds = Math.round(seconds);
1904
+ let minutes = seconds / 60 | 0;
1905
+ seconds = seconds % 60;
1906
+ if (minutes < 1)
1907
+ return `${seconds} sec`;
1908
+ let hours = minutes / 60 | 0;
1909
+ minutes = minutes % 60;
1910
+ if (hours < 1)
1911
+ return seconds !== 0 ? `${minutes} min ${seconds} sec` : `${minutes} min`;
1912
+ let days = hours / 24 | 0;
1913
+ if (days < 1)
1914
+ return `${hours} h ${minutes} min`;
1915
+ hours = hours % 24;
1916
+ let weeks = days / 7 | 0;
1917
+ if (weeks < 1)
1918
+ return `${days} days ${hours} h ${minutes} min`;
1919
+ days %= 7;
1920
+ return `${weeks} weeks ${days} days ${hours} h`;
1921
+ }
1922
+
1923
+ // src/cli/cmd-list-tests.ts
1924
+ async function cmdListTests(options) {
1925
+ const [orgSlug, projectSlug] = options.flakinessProject.split("/");
1926
+ if (!orgSlug || !projectSlug)
1927
+ throw new Error(`Invalid project slug '${options.flakinessProject}'; expected format: org/project`);
1928
+ const { api } = await authenticate({
1929
+ endpoint: options.endpoint,
1930
+ accessToken: options.accessToken,
1931
+ flakinessProject: options.flakinessProject
1932
+ });
1933
+ const result = await api.report.testStats.POST({
1934
+ orgSlug,
1935
+ projectSlug,
1936
+ pageOptions: {
1937
+ // CLI pages are 1-based while backend pages are 0-based.
1938
+ number: options.page - 1,
1939
+ size: options.pageSize
1940
+ },
1941
+ sortOptions: {
1942
+ axis: options.sort,
1943
+ direction: options.sortDir
1944
+ },
1945
+ historyBuckets: 10,
1946
+ fql: options.fql
1947
+ });
1948
+ const lines = [
1949
+ `# Tests for \`${orgSlug}/${projectSlug}\``,
1950
+ "",
1951
+ `Total tests: **${result.totalElements}**`,
1952
+ `Showing page: **${result.pageNumber + 1}/${Math.max(result.totalPages, 1)}** (page size ${result.pageSize})`,
1953
+ ""
1954
+ ];
1955
+ if (!result.elements.length) {
1956
+ lines.push("_No tests found on this page._");
1957
+ console.log(lines.join("\n"));
1958
+ return;
1959
+ }
1960
+ for (const [index, testStats] of result.elements.entries()) {
1961
+ const fullName = testStats.test.titles.join(" > ") || testStats.test.testId;
1962
+ const location = `${testStats.test.filePath}:${testStats.lineNumber}`;
1963
+ const error = extractError(testStats);
1964
+ const status = outcomeToStatus(testStats.outcome);
1965
+ const duration = humanReadableMs(testStats.durationMs);
1966
+ const flipRate = formatFlipRate(testStats.flipRate);
1967
+ lines.push(`### ${index + 1}. ${fullName}`);
1968
+ lines.push(`- Location: ${asInlineCode(location)}`);
1969
+ lines.push(`- Status: ${asInlineCode(status)}`);
1970
+ lines.push(`- Duration: ${asInlineCode(duration)}`);
1971
+ lines.push(`- Flip Rate: ${asInlineCode(flipRate)}`);
1972
+ lines.push(`- Error: ${error ? asInlineCode(error) : "None"}`);
1973
+ lines.push("");
1974
+ }
1975
+ console.log(lines.join("\n").trimEnd());
1976
+ }
1977
+ function extractError(testStats) {
1978
+ const texts = (testStats.errors ?? []).map((error) => error.message?.trim() || error.value?.trim()).filter(Boolean);
1979
+ if (!texts.length)
1980
+ return void 0;
1981
+ return texts.join(" | ");
1982
+ }
1983
+ function outcomeToStatus(outcome) {
1984
+ switch (outcome) {
1985
+ case "expected":
1986
+ return "passed";
1987
+ case "unexpected":
1988
+ return "failed";
1989
+ case "regressed":
1990
+ return "regressed";
1991
+ case "skipped":
1992
+ return "skipped";
1993
+ case "flaked":
1994
+ return "flaked";
1995
+ }
1996
+ }
1997
+ function formatFlipRate(flipRate) {
1998
+ if (flipRate === void 0 || !Number.isFinite(flipRate))
1999
+ return "n/a";
2000
+ return `${(flipRate * 100).toFixed(2)}%`;
2001
+ }
2002
+ function asInlineCode(text) {
2003
+ const normalized = text.replace(/\s+/g, " ").trim();
2004
+ const ticks = normalized.includes("`") ? "``" : "`";
2005
+ return `${ticks}${normalized}${ticks}`;
2006
+ }
2007
+
1568
2008
  // src/cli/cmd-upload.ts
1569
2009
  import { readReport, uploadReport } from "@flakiness/sdk";
1570
2010
  import chalk3 from "chalk";
@@ -1629,20 +2069,48 @@ var log2 = debug2("fk:cli");
1629
2069
  var loggedInEndpoint = await UserSession.load().then((session) => session?.endpoint());
1630
2070
  var optAccessToken = new Option("-t, --access-token <token>", "A read-write flakiness.io access token").env("FLAKINESS_ACCESS_TOKEN");
1631
2071
  var optEndpoint = new Option("-e, --endpoint <url>", "An endpoint where the service is deployed").default(loggedInEndpoint ?? DEFAULT_FLAKINESS_ENDPOINT).env("FLAKINESS_ENDPOINT");
1632
- var mustFlakinessProject = new Option("--project <org/project>", "Flakiness.io project slug (e.g. myorg/myproject)").env("FLAKINESS_PROJECT").makeOptionMandatory();
1633
- var optFlakinessProject = new Option("--project <org/project>", "Flakiness.io project slug (e.g. myorg/myproject)").env("FLAKINESS_PROJECT");
2072
+ var mustFlakinessProject = new Option("-p, --project <org/project>", "Flakiness.io project slug (e.g. myorg/myproject)").env("FLAKINESS_PROJECT").makeOptionMandatory();
2073
+ var optFlakinessProject = new Option("-p, --project <org/project>", "Flakiness.io project slug (e.g. myorg/myproject)").env("FLAKINESS_PROJECT");
1634
2074
  async function runCommand(callback) {
1635
2075
  try {
1636
2076
  await callback();
1637
- } catch (e) {
1638
- if (!(e instanceof Error))
1639
- throw e;
1640
- log2(e);
1641
- console.error(e.message);
2077
+ } catch (e2) {
2078
+ if (!(e2 instanceof Error))
2079
+ throw e2;
2080
+ log2(e2);
2081
+ console.error(e2.message);
1642
2082
  process.exit(1);
1643
2083
  }
1644
2084
  }
1645
2085
  var program = new Command().name("flakiness").description("Flakiness CLI tool").version(package_default.version);
2086
+ var list = program.command("list").description("Query test data");
2087
+ var optTestsSort = new Option("--sort <sort>", `A sort axis for tests`).choices([...WireTypes.SORT_AXES.tests]).default("outcome");
2088
+ var optTestsSortDir = new Option("--sort-dir <sort-dir>", "Sort direction for tests").choices(["asc", "desc"]).default("desc");
2089
+ var optTestsPage = new Option("--page <page>", "1-based page number").argParser((value) => {
2090
+ const parsed = parseInt(value, 10);
2091
+ if (isNaN(parsed) || parsed < 1)
2092
+ throw new Error("page must be a number >= 1");
2093
+ return parsed;
2094
+ }).default(1);
2095
+ var optTestsPageSize = new Option("--page-size <page-size>", "Number of tests per page").argParser((value) => {
2096
+ const parsed = parseInt(value, 10);
2097
+ if (isNaN(parsed) || parsed < 1 || parsed > 300)
2098
+ throw new Error("page-size must be a number in range [1, 300]");
2099
+ return parsed;
2100
+ }).default(20);
2101
+ var optTestsFQL = new Option("--fql <query>", "Filter Query Language (FQL) expression for tests. Learn about FQL: https://flakiness.io/docs/concepts/fql/");
2102
+ list.command("tests").description("query tests data").addOption(optTestsPage).addOption(optTestsPageSize).addOption(optTestsSort).addOption(optTestsSortDir).addOption(optTestsFQL).addOption(mustFlakinessProject).addOption(optEndpoint).addOption(optAccessToken).action(async (options) => runCommand(async () => {
2103
+ await cmdListTests({
2104
+ page: options.page,
2105
+ pageSize: options.pageSize,
2106
+ sort: options.sort,
2107
+ sortDir: options.sortDir,
2108
+ fql: options.fql,
2109
+ endpoint: options.endpoint,
2110
+ accessToken: options.accessToken,
2111
+ flakinessProject: options.project
2112
+ });
2113
+ }));
1646
2114
  var auth = program.command("auth").description("Manage Device OAuth");
1647
2115
  auth.command("login").description("Login to the Flakiness.io service").addOption(optEndpoint).action(async (options) => runCommand(async () => {
1648
2116
  await cmdAuthLogin(options.endpoint);