@plune-ai/cli 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -512,7 +512,7 @@ var init_retry = __esm({
512
512
  "use strict";
513
513
  init_cjs_shims();
514
514
  init_errors2();
515
- defaultSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
515
+ defaultSleep = (ms) => new Promise((resolve8) => setTimeout(resolve8, ms));
516
516
  }
517
517
  });
518
518
 
@@ -2186,6 +2186,35 @@ var init_credentials = __esm({
2186
2186
  });
2187
2187
 
2188
2188
  // src/reporter-core/client.ts
2189
+ function nodeTransport(url, init, idleMs = IDLE_MS) {
2190
+ return new Promise((resolve8, reject) => {
2191
+ const target = new URL(url);
2192
+ const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
2193
+ const req = send(target, { method: init.method, headers: init.headers }, (res) => {
2194
+ const chunks = [];
2195
+ res.on("data", (chunk2) => chunks.push(chunk2));
2196
+ res.on("error", reject);
2197
+ res.on("end", () => {
2198
+ const status = res.statusCode ?? 0;
2199
+ const text2 = Buffer.concat(chunks).toString("utf8");
2200
+ resolve8({
2201
+ ok: status >= 200 && status <= 299,
2202
+ status,
2203
+ headers: {
2204
+ get: (name) => {
2205
+ const value2 = res.headers[name.toLowerCase()];
2206
+ return value2 === void 0 ? null : Array.isArray(value2) ? value2.join(", ") : value2;
2207
+ }
2208
+ },
2209
+ json: async () => JSON.parse(text2)
2210
+ });
2211
+ });
2212
+ });
2213
+ req.setTimeout(idleMs, () => req.destroy(new Error(`no answer from ${target.origin} in ${idleMs} ms`)));
2214
+ req.on("error", reject);
2215
+ req.end(init.body);
2216
+ });
2217
+ }
2189
2218
  async function detailOf(res) {
2190
2219
  try {
2191
2220
  const body = await res.json();
@@ -2208,7 +2237,7 @@ function classify3(status) {
2208
2237
  return "other";
2209
2238
  }
2210
2239
  function createClient(opts) {
2211
- const doFetch = opts.fetchImpl ?? fetch;
2240
+ const doFetch = opts.fetchImpl ?? nodeTransport;
2212
2241
  const wait = opts.wait ?? sleep;
2213
2242
  const base = opts.apiUrl.replace(/\/+$/, "");
2214
2243
  const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
@@ -2236,7 +2265,11 @@ function createClient(opts) {
2236
2265
  }
2237
2266
  if (res.ok) {
2238
2267
  if (res.status === 204) return { ok: true, status: res.status, body: void 0 };
2239
- return { ok: true, status: res.status, body: await res.json() };
2268
+ try {
2269
+ return { ok: true, status: res.status, body: await res.json() };
2270
+ } catch {
2271
+ return { ok: false, kind: "other", status: res.status, detail: "the answer was not JSON" };
2272
+ }
2240
2273
  }
2241
2274
  const detail = scrub(await detailOf(res));
2242
2275
  const kind = classify3(res.status);
@@ -2253,12 +2286,15 @@ function createClient(opts) {
2253
2286
  del: (path14) => send("DELETE", path14)
2254
2287
  };
2255
2288
  }
2256
- var RETRY_DELAYS_MS, sleep;
2289
+ var import_node_http, import_node_https, RETRY_DELAYS_MS, IDLE_MS, sleep;
2257
2290
  var init_client = __esm({
2258
2291
  "src/reporter-core/client.ts"() {
2259
2292
  "use strict";
2260
2293
  init_cjs_shims();
2294
+ import_node_http = require("http");
2295
+ import_node_https = require("https");
2261
2296
  RETRY_DELAYS_MS = [300, 900];
2297
+ IDLE_MS = 3e5;
2262
2298
  sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2263
2299
  }
2264
2300
  });
@@ -2367,6 +2403,23 @@ function boundKey(key) {
2367
2403
  function boundTitle(title) {
2368
2404
  return title.length <= TITLE_MAX ? title : `${title.slice(0, TITLE_MAX - 1)}\u2026`;
2369
2405
  }
2406
+ function packed(results) {
2407
+ const out = [];
2408
+ let part = [];
2409
+ let bytes = ENVELOPE_BYTES;
2410
+ for (const result of results) {
2411
+ const size = Buffer.byteLength(JSON.stringify(result));
2412
+ if (part.length > 0 && bytes + 1 + size > BATCH_BYTES) {
2413
+ out.push(part);
2414
+ part = [];
2415
+ bytes = ENVELOPE_BYTES;
2416
+ }
2417
+ bytes += (part.length > 0 ? 1 : 0) + size;
2418
+ part.push(result);
2419
+ }
2420
+ if (part.length > 0) out.push(part);
2421
+ return out;
2422
+ }
2370
2423
  function chunk(items, size) {
2371
2424
  const out = [];
2372
2425
  for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
@@ -2568,27 +2621,35 @@ async function startRun(passed, declared) {
2568
2621
  progress();
2569
2622
  return;
2570
2623
  }
2571
- const out = await client.post(
2572
- `/v1/runs/${runId}/results`,
2573
- { results: submissions }
2574
- );
2575
- if (out.ok) {
2576
- stats.accepted += out.body.counts.accepted ?? 0;
2577
- stats.duplicate += out.body.counts.duplicate ?? 0;
2578
- stats.conflict += out.body.counts.conflict ?? 0;
2579
- stats.rejected += out.body.counts.rejected ?? 0;
2580
- progress();
2581
- return;
2582
- }
2583
- if (out.kind === "auth") {
2584
- log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
2585
- offline = true;
2586
- } else if (out.kind === "conflict") {
2587
- log(`plune: this run is closed \u2014 ${out.detail}`);
2588
- } else {
2589
- log(`plune: could not send results (${out.detail || out.kind}).`);
2624
+ for (const part of packed(submissions)) {
2625
+ if (offline) {
2626
+ defer(part);
2627
+ continue;
2628
+ }
2629
+ const out = await client.post(`/v1/runs/${runId}/results`, {
2630
+ results: part
2631
+ });
2632
+ const counts = out.ok ? out.body?.counts : void 0;
2633
+ if (counts !== void 0) {
2634
+ stats.accepted += counts.accepted ?? 0;
2635
+ stats.duplicate += counts.duplicate ?? 0;
2636
+ stats.conflict += counts.conflict ?? 0;
2637
+ stats.rejected += counts.rejected ?? 0;
2638
+ progress();
2639
+ continue;
2640
+ }
2641
+ if (out.ok) {
2642
+ log("plune: could not send results (the answer carried no counts).");
2643
+ } else if (out.kind === "auth") {
2644
+ log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
2645
+ offline = true;
2646
+ } else if (out.kind === "conflict") {
2647
+ log(`plune: this run is closed \u2014 ${out.detail}`);
2648
+ } else {
2649
+ log(`plune: could not send results (${out.detail || out.kind}).`);
2650
+ }
2651
+ defer(part);
2590
2652
  }
2591
- defer(submissions);
2592
2653
  }
2593
2654
  function progress() {
2594
2655
  const total = expected?.length ?? 0;
@@ -2604,9 +2665,12 @@ async function startRun(passed, declared) {
2604
2665
  if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
2605
2666
  if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
2606
2667
  if (stats.created > 0) parts.push(`${stats.created} added as cases (trusted source)`);
2607
- if (stats.deferred > 0) parts.push(`${stats.deferred} written to ${fallbackPath}`);
2668
+ if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
2608
2669
  if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
2609
2670
  log(`plune: ${parts.join(" \xB7 ")}`);
2671
+ if (stats.deferred > 0 && process.env["GITHUB_ACTIONS"] === "true") {
2672
+ log(`::warning::${stats.deferred} result(s) were not delivered to Plune \u2014 see the reporting step's log.`);
2673
+ }
2610
2674
  }
2611
2675
  return {
2612
2676
  get runId() {
@@ -2652,7 +2716,7 @@ async function startRun(passed, declared) {
2652
2716
  }
2653
2717
  };
2654
2718
  }
2655
- var import_node_crypto2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX, KEY_MAX, TITLE_MAX;
2719
+ var import_node_crypto2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX, KEY_MAX, TITLE_MAX, BATCH_BYTES, ENVELOPE_BYTES;
2656
2720
  var init_session = __esm({
2657
2721
  "src/reporter-core/session.ts"() {
2658
2722
  "use strict";
@@ -2670,6 +2734,8 @@ var init_session = __esm({
2670
2734
  EXPECTED_MAX = 1e4;
2671
2735
  KEY_MAX = 1024;
2672
2736
  TITLE_MAX = 300;
2737
+ BATCH_BYTES = 8 * 1024 * 1024;
2738
+ ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ results: [] }));
2673
2739
  }
2674
2740
  });
2675
2741
 
@@ -2765,14 +2831,14 @@ async function handleRunExec(options) {
2765
2831
  if (command === void 0) throw new RunCommandError("Nothing to run \u2014 pass a command after `--`.");
2766
2832
  const run = await handleRunStart(options);
2767
2833
  const spawnImpl = options.spawnImpl ?? import_node_child_process.spawn;
2768
- const code = await new Promise((resolve7) => {
2834
+ const code = await new Promise((resolve8) => {
2769
2835
  const child = spawnImpl(command, args, {
2770
2836
  stdio: "inherit",
2771
2837
  shell: process.platform === "win32",
2772
2838
  env: { ...process.env, PLUNE_RUN: run.externalKey, PLUNE_PROCEED: "1" }
2773
2839
  });
2774
- child.on("close", (exitCode) => resolve7(exitCode ?? 1));
2775
- child.on("error", () => resolve7(1));
2840
+ child.on("close", (exitCode) => resolve8(exitCode ?? 1));
2841
+ child.on("error", () => resolve8(1));
2776
2842
  });
2777
2843
  await handleRunFinish(run.id, options);
2778
2844
  return code;
@@ -3104,21 +3170,170 @@ var init_junit = __esm({
3104
3170
  }
3105
3171
  });
3106
3172
 
3173
+ // src/reporter-core/failure-detail.ts
3174
+ function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
3175
+ const error = chosen(attempt);
3176
+ const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
3177
+ const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
3178
+ const steps = chainOf(attempt.steps);
3179
+ const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
3180
+ const artifacts = attempt.attachments.filter((a) => a.path !== void 0 && !a.name.startsWith("_") && a.contentType !== METADATA_TYPE).map(({ name, contentType }) => ({ name: fileNameOf(name), ...contentType ? { contentType } : {} })).filter((a) => a.name !== "");
3181
+ const ciUrl = webLink(attempt.buildHref);
3182
+ const failure = {
3183
+ ...headline !== void 0 ? { headline } : {},
3184
+ ...steps.length > 0 ? { steps } : {},
3185
+ ...location !== void 0 ? { location } : {},
3186
+ ...artifacts.length > 0 ? { artifacts } : {},
3187
+ ...ciUrl !== void 0 ? { ciUrl } : {}
3188
+ };
3189
+ return Object.keys(failure).length === 0 ? void 0 : failure;
3190
+ }
3191
+ function repoRootOf(dir, stopAt) {
3192
+ for (let here = (0, import_node_path2.resolve)(dir); ; here = (0, import_node_path2.dirname)(here)) {
3193
+ if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(here, ".git"))) return here;
3194
+ if (here === stopAt || (0, import_node_path2.dirname)(here) === here) return void 0;
3195
+ }
3196
+ }
3197
+ function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
3198
+ const text2 = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
3199
+ return cut(machineless(text2, attempt.repoRoot, home));
3200
+ }
3201
+ function machineless(raw, repoRoot, home) {
3202
+ let text2 = (0, import_node_util.stripVTControlCharacters)(raw).replaceAll("\x1B", "");
3203
+ const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
3204
+ if (root !== void 0) text2 = text2.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
3205
+ const own = pathIn(home);
3206
+ if (own !== void 0) text2 = text2.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
3207
+ if (repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
3208
+ return text2;
3209
+ }
3210
+ function pathIn(path14) {
3211
+ const parts = path14.split(/[\\/]+/);
3212
+ while (parts.length > 1 && parts.at(-1) === "") parts.pop();
3213
+ if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
3214
+ const escaped = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
3215
+ return new RegExp(`(?:file:\\/\\/\\/?)?${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
3216
+ }
3217
+ function cut(text2) {
3218
+ if (jsonBytes(text2) <= TEXT_LIMIT) return text2;
3219
+ const lines = text2.split("\n");
3220
+ const size = lines.map((line) => jsonBytes(line) + 2);
3221
+ const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
3222
+ let used = 0;
3223
+ let head = 0;
3224
+ let tail = lines.length;
3225
+ while (head < tail && used + size[head] <= budget / 2) used += size[head++];
3226
+ while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
3227
+ return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
3228
+ }
3229
+ function chosen({ status, errors }) {
3230
+ if (status === "timedOut") {
3231
+ const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
3232
+ if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
3233
+ }
3234
+ return errors[0];
3235
+ }
3236
+ function chainOf(steps) {
3237
+ const chain = [];
3238
+ for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
3239
+ chain.push(failed.title);
3240
+ }
3241
+ return chain;
3242
+ }
3243
+ function placeOf(error, testFile) {
3244
+ const own = normal(testFile);
3245
+ let first;
3246
+ for (const line of (0, import_node_util.stripVTControlCharacters)(error.text).split("\n")) {
3247
+ if (line.length > FRAME_MAX) continue;
3248
+ const frame = FRAME.exec(line);
3249
+ if (frame === null) continue;
3250
+ const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
3251
+ if (normal(place.file) === own) return place;
3252
+ if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
3253
+ }
3254
+ return error.location ?? first;
3255
+ }
3256
+ function fileOf(raw) {
3257
+ if (!raw.startsWith("file://")) return raw;
3258
+ let path14 = raw.slice("file://".length);
3259
+ try {
3260
+ path14 = decodeURIComponent(path14);
3261
+ } catch {
3262
+ }
3263
+ return /^\/[A-Za-z]:\//.test(path14) ? path14.slice(1) : path14;
3264
+ }
3265
+ function fromRoot(place, root) {
3266
+ if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
3267
+ const base = normal(root).replace(/\/+$/, "");
3268
+ const file = normal(place.file);
3269
+ if (!file.startsWith(`${base}/`)) return void 0;
3270
+ const relative2 = file.slice(base.length + 1);
3271
+ if (!REPO_RELATIVE.test(relative2)) return void 0;
3272
+ const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
3273
+ return column === void 0 ? { file: relative2, line: place.line } : { file: relative2, line: place.line, column };
3274
+ }
3275
+ function webLink(href) {
3276
+ return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
3277
+ }
3278
+ var import_node_fs2, import_node_os2, import_node_path2, import_node_util, PATH_START, TEXT_LIMIT, HEADLINE_LIMIT, jsonBytes, ANY_HOME, fileNameOf, omitted, METADATA_TYPE, REPO_RELATIVE, FRAME, FRAME_MAX, firstLine, normal;
3279
+ var init_failure_detail = __esm({
3280
+ "src/reporter-core/failure-detail.ts"() {
3281
+ "use strict";
3282
+ init_cjs_shims();
3283
+ import_node_fs2 = require("fs");
3284
+ import_node_os2 = require("os");
3285
+ import_node_path2 = require("path");
3286
+ import_node_util = require("util");
3287
+ PATH_START = "(?<![\\w.~%/\\\\-])";
3288
+ TEXT_LIMIT = 524288;
3289
+ HEADLINE_LIMIT = 8 * 1024;
3290
+ jsonBytes = (text2) => Buffer.byteLength(JSON.stringify(text2)) - 2;
3291
+ ANY_HOME = /(?<=^|[\s'"`(=,[])(?:file:\/\/\/?)?(?:\/home\/|\/Users\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
3292
+ fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
3293
+ omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
3294
+ METADATA_TYPE = "application/plune.metadata+json";
3295
+ REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
3296
+ FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
3297
+ FRAME_MAX = 8192;
3298
+ firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
3299
+ normal = (path14) => import_node_path2.posix.normalize(path14.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
3300
+ }
3301
+ });
3302
+
3107
3303
  // src/importers/playwright-json.ts
3108
3304
  function statedIdOf(test, title) {
3109
3305
  const annotation = test.annotations?.find((a) => a.type === "PluneId")?.description;
3110
3306
  if (annotation !== void 0 && annotation !== "") return annotation;
3111
3307
  return TOKEN.exec(title)?.[1];
3112
3308
  }
3113
- function errorTextOf2(result) {
3114
- return (result.errors ?? []).map((e) => e.stack ?? e.message ?? e.value ?? "").filter((text2) => text2 !== "").join("\n\n");
3309
+ function attemptOf(result, file, ctx) {
3310
+ const steps = (list2) => (list2 ?? []).map((s) => ({ title: s.title ?? "", failed: s.error !== void 0, steps: steps(s.steps) }));
3311
+ return {
3312
+ status: result.status ?? "unknown",
3313
+ errors: (result.errors ?? []).map(({ message, value: value2, location: at }) => ({
3314
+ text: message ?? value2 ?? "",
3315
+ ...typeof at?.file === "string" && typeof at.line === "number" ? { location: { file: at.file, line: at.line, ...typeof at.column === "number" ? { column: at.column } : {} } } : {}
3316
+ })),
3317
+ steps: steps(result.steps),
3318
+ attachments: (result.attachments ?? []).map(({ name, contentType, path: path14 }) => ({
3319
+ name: name ?? "",
3320
+ ...contentType !== void 0 ? { contentType } : {},
3321
+ ...path14 !== void 0 ? { path: path14 } : {}
3322
+ })),
3323
+ ...ctx.buildHref !== void 0 ? { buildHref: ctx.buildHref } : {},
3324
+ // Spec files are posix and relative to the root dir; the core reads either slash.
3325
+ testFile: ctx.rootDir === void 0 ? file : `${ctx.rootDir.replace(/[\\/]+$/, "")}/${file}`,
3326
+ ...ctx.repoRoot !== void 0 ? { repoRoot: ctx.repoRoot } : {}
3327
+ };
3115
3328
  }
3116
- function pendingFrom(spec, test, result, file, titles) {
3329
+ function pendingFrom(spec, test, result, file, titles, ctx) {
3117
3330
  const pathTitle = [file, ...titles].join("#");
3118
3331
  const seed = test.id ?? pathTitle;
3119
3332
  const stated = statedIdOf(test, spec.title ?? "");
3120
3333
  const expected = test.expectedStatus;
3121
- const errorContext = errorTextOf2(result);
3334
+ const attempt = attemptOf(result, file, ctx);
3335
+ const errorContext = errorContextOf(attempt);
3336
+ const failure = NOT_FAILED.has(attempt.status) ? void 0 : failureOf(attempt);
3122
3337
  const keys = [
3123
3338
  ...test.id !== void 0 ? [{ kind: "playwright-id", value: test.id }] : [],
3124
3339
  { kind: "path-title", value: pathTitle }
@@ -3146,19 +3361,20 @@ function pendingFrom(spec, test, result, file, titles) {
3146
3361
  ...result.workerIndex !== void 0 ? { worker: String(result.workerIndex) } : {}
3147
3362
  }
3148
3363
  } : {},
3149
- ...errorContext !== "" ? { errorContext } : {}
3364
+ ...errorContext !== "" ? { errorContext } : {},
3365
+ ...failure !== void 0 ? { failure } : {}
3150
3366
  };
3151
3367
  }
3152
- function walk(suite, describes, depth, out) {
3368
+ function walk(suite, describes, depth, ctx, out) {
3153
3369
  const here = depth === 0 ? [] : [...describes, suite.title ?? ""];
3154
3370
  for (const spec of suite.specs ?? []) {
3155
3371
  const file = spec.file ?? suite.file ?? suite.title ?? "";
3156
3372
  const titles = [...here, spec.title ?? ""];
3157
3373
  for (const test of spec.tests ?? []) {
3158
- for (const result of test.results ?? []) out.push(pendingFrom(spec, test, result, file, titles));
3374
+ for (const result of test.results ?? []) out.push(pendingFrom(spec, test, result, file, titles, ctx));
3159
3375
  }
3160
3376
  }
3161
- for (const child of suite.suites ?? []) walk(child, here, depth + 1, out);
3377
+ for (const child of suite.suites ?? []) walk(child, here, depth + 1, ctx, out);
3162
3378
  }
3163
3379
  function readPlaywrightJson(source, file) {
3164
3380
  let parsed;
@@ -3170,22 +3386,34 @@ function readPlaywrightJson(source, file) {
3170
3386
  if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.suites)) {
3171
3387
  throw new JsonReportError(file, 'no "suites" array \u2014 is this a Playwright JSON report?');
3172
3388
  }
3389
+ const { config, suites } = parsed;
3390
+ const rootDir = typeof config?.rootDir === "string" ? config.rootDir : void 0;
3391
+ const buildHref = config?.metadata?.ci?.buildHref;
3392
+ const repoRoot = rootDir !== void 0 && (0, import_node_fs3.existsSync)(rootDir) ? repoRootOf(rootDir) : void 0;
3393
+ const ctx = {
3394
+ ...rootDir !== void 0 ? { rootDir } : {},
3395
+ ...repoRoot !== void 0 ? { repoRoot } : {},
3396
+ ...typeof buildHref === "string" ? { buildHref } : {}
3397
+ };
3173
3398
  const out = [];
3174
- for (const suite of parsed.suites) walk(suite, [], 0, out);
3175
- return out;
3399
+ for (const suite of suites) walk(suite, [], 0, ctx, out);
3400
+ const ciUrl = webLink(ctx.buildHref);
3401
+ return ciUrl === void 0 ? { results: out } : { results: out, ciUrl };
3176
3402
  }
3177
3403
  function looksLikePlaywrightJson(source) {
3178
- const head = source.slice(0, 4096);
3179
- return head.trimStart().startsWith("{") && head.includes('"suites"');
3404
+ return /^\s*\{/.test(source) && source.includes('"suites"');
3180
3405
  }
3181
- var SOURCE2, TOKEN, JsonReportError;
3406
+ var import_node_fs3, SOURCE2, TOKEN, NOT_FAILED, JsonReportError;
3182
3407
  var init_playwright_json = __esm({
3183
3408
  "src/importers/playwright-json.ts"() {
3184
3409
  "use strict";
3185
3410
  init_cjs_shims();
3411
+ import_node_fs3 = require("fs");
3412
+ init_failure_detail();
3186
3413
  init_result_key();
3187
3414
  SOURCE2 = "playwright";
3188
3415
  TOKEN = /@P([A-Za-z0-9_-]+)/;
3416
+ NOT_FAILED = /* @__PURE__ */ new Set(["passed", "skipped"]);
3189
3417
  JsonReportError = class extends Error {
3190
3418
  constructor(file, detail) {
3191
3419
  super(`${file} \u2014 ${detail}`);
@@ -3216,11 +3444,9 @@ function detectFormat(source, file) {
3216
3444
  );
3217
3445
  }
3218
3446
  function readReport(source, file, format) {
3219
- const chosen = format ?? detectFormat(source, file);
3220
- return {
3221
- format: chosen,
3222
- results: chosen === "junit" ? readJUnit(source, file) : readPlaywrightJson(source, file)
3223
- };
3447
+ const chosen2 = format ?? detectFormat(source, file);
3448
+ if (chosen2 === "junit") return { format: chosen2, results: readJUnit(source, file) };
3449
+ return { format: chosen2, ...readPlaywrightJson(source, file) };
3224
3450
  }
3225
3451
  var IMPORT_FORMATS, UnknownFormatError;
3226
3452
  var init_importers = __esm({
@@ -3255,7 +3481,7 @@ async function handleRunImport(options) {
3255
3481
  } catch {
3256
3482
  throw new RunCommandError(`Cannot read ${options.file}.`);
3257
3483
  }
3258
- const { format, results } = readReport(source, options.file, options.format);
3484
+ const { format, results, ciUrl } = readReport(source, options.file, options.format);
3259
3485
  if (results.length === 0) {
3260
3486
  throw new RunCommandError(`No test cases in ${options.file} \u2014 it parsed, but there is nothing in it.`);
3261
3487
  }
@@ -3271,7 +3497,8 @@ async function handleRunImport(options) {
3271
3497
  ...options.key !== void 0 ? { externalKey: options.key } : {},
3272
3498
  ...options.create === true ? { offerDiscovered: true } : {},
3273
3499
  kind: "automated",
3274
- meta: { runner: format },
3500
+ // The CI run the report names, never this machine's: whoever imports may be another job (#790).
3501
+ meta: { runner: format, ...ciUrl !== void 0 ? { ciUrl } : {} },
3275
3502
  log: write
3276
3503
  },
3277
3504
  // The file is the complete list of what ran, so the platform can resolve every test in one
@@ -3312,7 +3539,9 @@ function describe(result, apiUrl) {
3312
3539
  // «Imported 555 result(s): 0 accepted, 0 already there, 0 unmatched» was a sentence whose two
3313
3540
  // halves disagreed, and readers stop at the verb — a run that reached nobody read as one that
3314
3541
  // worked (#622).
3315
- `Read ${result.parsed} result(s) from a ${result.format} report: ${result.accepted} accepted, ${result.duplicate} already there, ${result.unresolved} unmatched.`
3542
+ // `not delivered` always, zero included (#790): a number that only appears when it is bad is one
3543
+ // nobody learns to look for. The `[0-9]+ unmatched` the workflows read stays whole.
3544
+ `Read ${result.parsed} result(s) from a ${result.format} report: ${result.accepted} accepted, ${result.duplicate} already there, ${result.unresolved} unmatched, ${result.deferred} not delivered.`
3316
3545
  ];
3317
3546
  if (result.conflict > 0 || result.rejected > 0) {
3318
3547
  lines.push(`${result.conflict} conflicted and ${result.rejected} were refused.`);
@@ -4545,11 +4774,11 @@ module.exports = __toCommonJS(cli_exports);
4545
4774
  init_cjs_shims();
4546
4775
  init_api_url();
4547
4776
  var import_commander = require("commander");
4548
- var import_node_fs2 = require("fs");
4549
- var import_node_path2 = require("path");
4777
+ var import_node_fs4 = require("fs");
4778
+ var import_node_path3 = require("path");
4550
4779
  var import_node_url = require("url");
4551
- var __dirname = (0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
4552
- var pkg = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path2.join)(__dirname, "..", "package.json"), "utf-8"));
4780
+ var __dirname = (0, import_node_path3.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
4781
+ var pkg = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path3.join)(__dirname, "..", "package.json"), "utf-8"));
4553
4782
  function toFormat(s) {
4554
4783
  return s === "json" || s === "markdown" ? s : "console";
4555
4784
  }
@@ -4558,7 +4787,7 @@ function shouldColor(outputPath) {
4558
4787
  }
4559
4788
  function writeReport(text2, outputPath) {
4560
4789
  if (outputPath !== void 0) {
4561
- (0, import_node_fs2.writeFileSync)(outputPath, text2);
4790
+ (0, import_node_fs4.writeFileSync)(outputPath, text2);
4562
4791
  } else {
4563
4792
  process.stdout.write(text2 + "\n");
4564
4793
  }
@@ -4624,7 +4853,7 @@ function createProgram() {
4624
4853
  Promise.resolve().then(() => (init_errors(), errors_exports)),
4625
4854
  Promise.resolve().then(() => (init_errors2(), errors_exports2))
4626
4855
  ]);
4627
- loadEnv2(configPath !== void 0 ? (0, import_node_path2.dirname)((0, import_node_path2.resolve)(configPath)) : process.cwd());
4856
+ loadEnv2(configPath !== void 0 ? (0, import_node_path3.dirname)((0, import_node_path3.resolve)(configPath)) : process.cwd());
4628
4857
  try {
4629
4858
  const result = await handleRun2({
4630
4859
  dryRun: options.dryRun,
@@ -4755,7 +4984,8 @@ function createProgram() {
4755
4984
  Promise.resolve().then(() => (init_run_import(), run_import_exports)),
4756
4985
  Promise.resolve().then(() => (init_importers(), importers_exports))
4757
4986
  ]);
4758
- const format = options.format;
4987
+ const run = command.parent;
4988
+ const format = options.format ?? (run?.getOptionValueSource("format") === "cli" ? run.opts().format : void 0);
4759
4989
  if (format !== void 0 && !IMPORT_FORMATS2.includes(format)) {
4760
4990
  process.stderr.write(
4761
4991
  `Unknown --format "${format}". Use ${IMPORT_FORMATS2.join(" or ")}.
@@ -4788,7 +5018,7 @@ function createProgram() {
4788
5018
  Promise.resolve().then(() => (init_report(), report_exports)),
4789
5019
  Promise.resolve().then(() => (init_reporters(), reporters_exports))
4790
5020
  ]);
4791
- const cwd = globals.config !== void 0 ? (0, import_node_path2.dirname)((0, import_node_path2.resolve)(globals.config)) : process.cwd();
5021
+ const cwd = globals.config !== void 0 ? (0, import_node_path3.dirname)((0, import_node_path3.resolve)(globals.config)) : process.cwd();
4792
5022
  loadEnv2(cwd);
4793
5023
  try {
4794
5024
  const result = handleReport2({ cwd });
@@ -4886,7 +5116,7 @@ function createProgram() {
4886
5116
  const globals = command.optsWithGlobals();
4887
5117
  const verbose = globals.verbose === true;
4888
5118
  const { handleSync: handleSync2, reportSyncFailure: reportSyncFailure2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
4889
- const cwd = globals.config !== void 0 ? (0, import_node_path2.dirname)((0, import_node_path2.resolve)(globals.config)) : process.cwd();
5119
+ const cwd = globals.config !== void 0 ? (0, import_node_path3.dirname)((0, import_node_path3.resolve)(globals.config)) : process.cwd();
4890
5120
  try {
4891
5121
  const { id, url } = await handleSync2({
4892
5122
  cwd,
@@ -5004,7 +5234,7 @@ function isMainModule() {
5004
5234
  const entry = process.argv[1];
5005
5235
  if (entry === void 0) return false;
5006
5236
  try {
5007
- return (0, import_node_fs2.realpathSync)(entry) === (0, import_node_fs2.realpathSync)((0, import_node_url.fileURLToPath)(importMetaUrl));
5237
+ return (0, import_node_fs4.realpathSync)(entry) === (0, import_node_fs4.realpathSync)((0, import_node_url.fileURLToPath)(importMetaUrl));
5008
5238
  } catch {
5009
5239
  return false;
5010
5240
  }