@plune-ai/cli 0.14.0 → 0.14.2

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
@@ -2265,7 +2265,11 @@ function createClient(opts) {
2265
2265
  }
2266
2266
  if (res.ok) {
2267
2267
  if (res.status === 204) return { ok: true, status: res.status, body: void 0 };
2268
- 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
+ }
2269
2273
  }
2270
2274
  const detail = scrub(await detailOf(res));
2271
2275
  const kind = classify3(res.status);
@@ -2497,11 +2501,13 @@ async function startRun(passed, declared) {
2497
2501
  "/v1/test-cases/resolve",
2498
2502
  { keys: part }
2499
2503
  );
2500
- if (!out.ok) {
2501
- log(`plune: could not look up test cases (${out.detail || out.kind}).`);
2504
+ if (!out.ok || !Array.isArray(out.body?.results)) {
2505
+ log(`plune: could not look up test cases (${out.ok ? "the answer had no results" : out.detail || out.kind}).`);
2502
2506
  return;
2503
2507
  }
2504
- for (const row of out.body.results) resolved.set(row.key.value, row.testCaseId);
2508
+ for (const row of out.body.results) {
2509
+ if (typeof row?.key?.value === "string") resolved.set(row.key.value, row.testCaseId);
2510
+ }
2505
2511
  }
2506
2512
  }
2507
2513
  if (!offline) {
@@ -2538,12 +2544,13 @@ async function startRun(passed, declared) {
2538
2544
  title: defaultRunTitle(),
2539
2545
  ...described
2540
2546
  });
2541
- if (start.ok) {
2547
+ if (start.ok && typeof start.body?.run?.id === "string") {
2542
2548
  runId = start.body.run.id;
2543
2549
  joined = start.status === 200;
2544
2550
  warnIfDropped(described, start.body.run, log);
2545
2551
  } else {
2546
- log(`plune: could not start the run (${start.detail || start.kind}). Results will be written to ${fallbackPath}.`);
2552
+ const why = start.ok ? "the answer had no run" : start.detail || start.kind;
2553
+ log(`plune: could not start the run (${why}). Results will be written to ${fallbackPath}.`);
2547
2554
  }
2548
2555
  }
2549
2556
  function matchFor(result) {
@@ -2578,15 +2585,15 @@ async function startRun(passed, declared) {
2578
2585
  discovered: batch,
2579
2586
  runId
2580
2587
  });
2581
- if (!out.ok) {
2588
+ if (!out.ok || !Array.isArray(out.body?.results)) {
2582
2589
  stats.unoffered = batches.slice(index).reduce((n, rest) => n + rest.length, 0);
2583
2590
  log(
2584
- `plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.detail || out.kind}).`
2591
+ `plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.ok ? "the answer had no results" : out.detail || out.kind}).`
2585
2592
  );
2586
2593
  return;
2587
2594
  }
2588
- stats.offered += out.body.results.filter((r) => r.outcome === "queued").length;
2589
- stats.created += out.body.results.filter((r) => r.outcome === "created").length;
2595
+ stats.offered += out.body.results.filter((r) => r?.outcome === "queued").length;
2596
+ stats.created += out.body.results.filter((r) => r?.outcome === "created").length;
2590
2597
  }
2591
2598
  }
2592
2599
  async function flush() {
@@ -2625,15 +2632,16 @@ async function startRun(passed, declared) {
2625
2632
  const out = await client.post(`/v1/runs/${runId}/results`, {
2626
2633
  results: part
2627
2634
  });
2628
- if (out.ok) {
2629
- stats.accepted += out.body.counts.accepted ?? 0;
2630
- stats.duplicate += out.body.counts.duplicate ?? 0;
2631
- stats.conflict += out.body.counts.conflict ?? 0;
2632
- stats.rejected += out.body.counts.rejected ?? 0;
2635
+ const counts = out.ok ? out.body?.counts : void 0;
2636
+ const told = COUNTED.filter((k) => typeof counts?.[k] === "number");
2637
+ if (told.length > 0) {
2638
+ for (const k of told) stats[k] += counts[k];
2633
2639
  progress();
2634
2640
  continue;
2635
2641
  }
2636
- if (out.kind === "auth") {
2642
+ if (out.ok) {
2643
+ log("plune: could not send results (the answer carried no counts).");
2644
+ } else if (out.kind === "auth") {
2637
2645
  log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
2638
2646
  offline = true;
2639
2647
  } else if (out.kind === "conflict") {
@@ -2691,7 +2699,7 @@ async function startRun(passed, declared) {
2691
2699
  ...reason !== void 0 ? { reason } : {}
2692
2700
  });
2693
2701
  if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
2694
- else if (typeof out.body.detached === "number" && out.body.detached > 0) {
2702
+ else if (typeof out.body?.detached === "number" && out.body.detached > 0) {
2695
2703
  log(`plune: ${out.body.detached} test case(s) marked detached \u2014 this full run no longer reports them.`);
2696
2704
  }
2697
2705
  }
@@ -2709,7 +2717,7 @@ async function startRun(passed, declared) {
2709
2717
  }
2710
2718
  };
2711
2719
  }
2712
- var import_node_crypto2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX, KEY_MAX, TITLE_MAX, BATCH_BYTES, ENVELOPE_BYTES;
2720
+ var import_node_crypto2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX, KEY_MAX, TITLE_MAX, COUNTED, BATCH_BYTES, ENVELOPE_BYTES;
2713
2721
  var init_session = __esm({
2714
2722
  "src/reporter-core/session.ts"() {
2715
2723
  "use strict";
@@ -2727,6 +2735,7 @@ var init_session = __esm({
2727
2735
  EXPECTED_MAX = 1e4;
2728
2736
  KEY_MAX = 1024;
2729
2737
  TITLE_MAX = 300;
2738
+ COUNTED = ["accepted", "duplicate", "conflict", "rejected"];
2730
2739
  BATCH_BYTES = 8 * 1024 * 1024;
2731
2740
  ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ results: [] }));
2732
2741
  }
@@ -3164,12 +3173,13 @@ var init_junit = __esm({
3164
3173
  });
3165
3174
 
3166
3175
  // src/reporter-core/failure-detail.ts
3167
- function failureOf(attempt) {
3176
+ function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
3168
3177
  const error = chosen(attempt);
3169
- const headline = error === void 0 ? void 0 : firstLine(error.text);
3178
+ const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
3179
+ const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
3170
3180
  const steps = chainOf(attempt.steps);
3171
3181
  const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
3172
- const artifacts = attempt.attachments.filter((a) => a.path !== void 0 && a.name !== "" && !a.name.startsWith("_") && a.contentType !== METADATA_TYPE).map(({ name, contentType }) => contentType === void 0 ? { name } : { name, contentType });
3182
+ 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 !== "");
3173
3183
  const ciUrl = webLink(attempt.buildHref);
3174
3184
  const failure = {
3175
3185
  ...headline !== void 0 ? { headline } : {},
@@ -3187,28 +3197,32 @@ function repoRootOf(dir, stopAt) {
3187
3197
  }
3188
3198
  }
3189
3199
  function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
3190
- let text2 = (0, import_node_util.stripVTControlCharacters)(
3191
- attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n")
3192
- ).replaceAll("\x1B", "");
3193
- const root = attempt.repoRoot === void 0 ? void 0 : pathIn(attempt.repoRoot);
3194
- if (root !== void 0) text2 = text2.replace(new RegExp(`${root.source}[\\\\/]`, root.flags), "");
3200
+ const text2 = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
3201
+ return cut(machineless(text2, attempt.repoRoot, home));
3202
+ }
3203
+ function machineless(raw, repoRoot, home) {
3204
+ let text2 = (0, import_node_util.stripVTControlCharacters)(raw).replaceAll("\x1B", "");
3205
+ const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
3206
+ if (root !== void 0) text2 = text2.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
3195
3207
  const own = pathIn(home);
3196
- if (own !== void 0) text2 = text2.replace(new RegExp(`${own.source}(?![\\w.-])`, own.flags), "~");
3197
- if (attempt.repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
3198
- return cut(text2);
3208
+ if (own !== void 0) text2 = text2.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
3209
+ if (repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
3210
+ return text2;
3199
3211
  }
3200
3212
  function pathIn(path14) {
3201
3213
  const parts = path14.split(/[\\/]+/);
3202
3214
  while (parts.length > 1 && parts.at(-1) === "") parts.pop();
3203
3215
  if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
3204
- const escaped = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]");
3205
- return new RegExp(`(?:file:\\/\\/\\/?)?${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
3216
+ const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
3217
+ const escaped = /^\/(?!\/)/.test(path14) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
3218
+ const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
3219
+ return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
3206
3220
  }
3207
3221
  function cut(text2) {
3208
- if (Buffer.byteLength(text2) <= TEXT_LIMIT) return text2;
3222
+ if (jsonBytes(text2) <= TEXT_LIMIT) return text2;
3209
3223
  const lines = text2.split("\n");
3210
- const size = lines.map((line) => Buffer.byteLength(line) + 1);
3211
- const budget = TEXT_LIMIT - Buffer.byteLength(omitted(lines.length));
3224
+ const size = lines.map((line) => jsonBytes(line) + 2);
3225
+ const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
3212
3226
  let used = 0;
3213
3227
  let head = 0;
3214
3228
  let tail = lines.length;
@@ -3234,6 +3248,7 @@ function placeOf(error, testFile) {
3234
3248
  const own = normal(testFile);
3235
3249
  let first;
3236
3250
  for (const line of (0, import_node_util.stripVTControlCharacters)(error.text).split("\n")) {
3251
+ if (line.length > FRAME_MAX) continue;
3237
3252
  const frame = FRAME.exec(line);
3238
3253
  if (frame === null) continue;
3239
3254
  const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
@@ -3244,7 +3259,11 @@ function placeOf(error, testFile) {
3244
3259
  }
3245
3260
  function fileOf(raw) {
3246
3261
  if (!raw.startsWith("file://")) return raw;
3247
- const path14 = decodeURIComponent(raw.slice("file://".length));
3262
+ let path14 = raw.slice("file://".length);
3263
+ try {
3264
+ path14 = decodeURIComponent(path14);
3265
+ } catch {
3266
+ }
3248
3267
  return /^\/[A-Za-z]:\//.test(path14) ? path14.slice(1) : path14;
3249
3268
  }
3250
3269
  function fromRoot(place, root) {
@@ -3258,15 +3277,9 @@ function fromRoot(place, root) {
3258
3277
  return column === void 0 ? { file: relative2, line: place.line } : { file: relative2, line: place.line, column };
3259
3278
  }
3260
3279
  function webLink(href) {
3261
- if (href === void 0) return void 0;
3262
- try {
3263
- const { protocol } = new URL(href);
3264
- return protocol === "http:" || protocol === "https:" ? href : void 0;
3265
- } catch {
3266
- return void 0;
3267
- }
3280
+ return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
3268
3281
  }
3269
- var import_node_fs2, import_node_os2, import_node_path2, import_node_util, TEXT_LIMIT, ANY_HOME, omitted, METADATA_TYPE, REPO_RELATIVE, FRAME, firstLine, normal;
3282
+ 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;
3270
3283
  var init_failure_detail = __esm({
3271
3284
  "src/reporter-core/failure-detail.ts"() {
3272
3285
  "use strict";
@@ -3275,12 +3288,17 @@ var init_failure_detail = __esm({
3275
3288
  import_node_os2 = require("os");
3276
3289
  import_node_path2 = require("path");
3277
3290
  import_node_util = require("util");
3291
+ PATH_START = "(?<![\\w.~%/\\\\-])";
3278
3292
  TEXT_LIMIT = 524288;
3279
- ANY_HOME = /(?<=^|[\s'"`(=,[])(?:file:\/\/\/?)?(?:\/home\/|\/Users\/|[A-Za-z]:[\\/]Users[\\/])[^\\/\s'"`:*?<>|]+/g;
3293
+ HEADLINE_LIMIT = 8 * 1024;
3294
+ jsonBytes = (text2) => Buffer.byteLength(JSON.stringify(text2)) - 2;
3295
+ ANY_HOME = /(?<=^|[\s'"`(=,[])(?:(?:file|webpack|webpack-internal):(?:\\*\/){2,3})?(?:\\*\/home\\*\/|\\*\/Users\\*\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
3296
+ fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
3280
3297
  omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
3281
3298
  METADATA_TYPE = "application/plune.metadata+json";
3282
3299
  REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
3283
3300
  FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
3301
+ FRAME_MAX = 8192;
3284
3302
  firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
3285
3303
  normal = (path14) => import_node_path2.posix.normalize(path14.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
3286
3304
  }
@@ -159,7 +159,11 @@ function createClient(opts) {
159
159
  }
160
160
  if (res.ok) {
161
161
  if (res.status === 204) return { ok: true, status: res.status, body: void 0 };
162
- return { ok: true, status: res.status, body: await res.json() };
162
+ try {
163
+ return { ok: true, status: res.status, body: await res.json() };
164
+ } catch {
165
+ return { ok: false, kind: "other", status: res.status, detail: "the answer was not JSON" };
166
+ }
163
167
  }
164
168
  const detail = scrub(await detailOf(res));
165
169
  const kind = classify(res.status);
@@ -264,6 +268,7 @@ var BATCH_DEFAULT = 100;
264
268
  var EXPECTED_MAX = 1e4;
265
269
  var KEY_MAX = 1024;
266
270
  var TITLE_MAX = 300;
271
+ var COUNTED = ["accepted", "duplicate", "conflict", "rejected"];
267
272
  function boundKey(key) {
268
273
  if (key.value.length <= KEY_MAX) return key;
269
274
  const digest = (0, import_node_crypto.createHash)("sha256").update(key.value).digest("hex").slice(0, 16);
@@ -374,11 +379,13 @@ async function startRun(passed, declared) {
374
379
  "/v1/test-cases/resolve",
375
380
  { keys: part }
376
381
  );
377
- if (!out.ok) {
378
- log(`plune: could not look up test cases (${out.detail || out.kind}).`);
382
+ if (!out.ok || !Array.isArray(out.body?.results)) {
383
+ log(`plune: could not look up test cases (${out.ok ? "the answer had no results" : out.detail || out.kind}).`);
379
384
  return;
380
385
  }
381
- for (const row of out.body.results) resolved.set(row.key.value, row.testCaseId);
386
+ for (const row of out.body.results) {
387
+ if (typeof row?.key?.value === "string") resolved.set(row.key.value, row.testCaseId);
388
+ }
382
389
  }
383
390
  }
384
391
  if (!offline) {
@@ -415,12 +422,13 @@ async function startRun(passed, declared) {
415
422
  title: defaultRunTitle(),
416
423
  ...described
417
424
  });
418
- if (start.ok) {
425
+ if (start.ok && typeof start.body?.run?.id === "string") {
419
426
  runId = start.body.run.id;
420
427
  joined = start.status === 200;
421
428
  warnIfDropped(described, start.body.run, log);
422
429
  } else {
423
- log(`plune: could not start the run (${start.detail || start.kind}). Results will be written to ${fallbackPath}.`);
430
+ const why = start.ok ? "the answer had no run" : start.detail || start.kind;
431
+ log(`plune: could not start the run (${why}). Results will be written to ${fallbackPath}.`);
424
432
  }
425
433
  }
426
434
  function matchFor(result) {
@@ -455,15 +463,15 @@ async function startRun(passed, declared) {
455
463
  discovered: batch,
456
464
  runId
457
465
  });
458
- if (!out.ok) {
466
+ if (!out.ok || !Array.isArray(out.body?.results)) {
459
467
  stats.unoffered = batches.slice(index).reduce((n, rest) => n + rest.length, 0);
460
468
  log(
461
- `plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.detail || out.kind}).`
469
+ `plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.ok ? "the answer had no results" : out.detail || out.kind}).`
462
470
  );
463
471
  return;
464
472
  }
465
- stats.offered += out.body.results.filter((r) => r.outcome === "queued").length;
466
- stats.created += out.body.results.filter((r) => r.outcome === "created").length;
473
+ stats.offered += out.body.results.filter((r) => r?.outcome === "queued").length;
474
+ stats.created += out.body.results.filter((r) => r?.outcome === "created").length;
467
475
  }
468
476
  }
469
477
  async function flush() {
@@ -502,15 +510,16 @@ async function startRun(passed, declared) {
502
510
  const out = await client.post(`/v1/runs/${runId}/results`, {
503
511
  results: part
504
512
  });
505
- if (out.ok) {
506
- stats.accepted += out.body.counts.accepted ?? 0;
507
- stats.duplicate += out.body.counts.duplicate ?? 0;
508
- stats.conflict += out.body.counts.conflict ?? 0;
509
- stats.rejected += out.body.counts.rejected ?? 0;
513
+ const counts = out.ok ? out.body?.counts : void 0;
514
+ const told = COUNTED.filter((k) => typeof counts?.[k] === "number");
515
+ if (told.length > 0) {
516
+ for (const k of told) stats[k] += counts[k];
510
517
  progress();
511
518
  continue;
512
519
  }
513
- if (out.kind === "auth") {
520
+ if (out.ok) {
521
+ log("plune: could not send results (the answer carried no counts).");
522
+ } else if (out.kind === "auth") {
514
523
  log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
515
524
  offline = true;
516
525
  } else if (out.kind === "conflict") {
@@ -568,7 +577,7 @@ async function startRun(passed, declared) {
568
577
  ...reason !== void 0 ? { reason } : {}
569
578
  });
570
579
  if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
571
- else if (typeof out.body.detached === "number" && out.body.detached > 0) {
580
+ else if (typeof out.body?.detached === "number" && out.body.detached > 0) {
572
581
  log(`plune: ${out.body.detached} test case(s) marked detached \u2014 this full run no longer reports them.`);
573
582
  }
574
583
  }
@@ -606,12 +615,13 @@ var import_node_fs2 = require("fs");
606
615
  var import_node_os2 = require("os");
607
616
  var import_node_path2 = require("path");
608
617
  var import_node_util = require("util");
609
- function failureOf(attempt) {
618
+ function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
610
619
  const error = chosen(attempt);
611
- const headline = error === void 0 ? void 0 : firstLine(error.text);
620
+ const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
621
+ const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
612
622
  const steps = chainOf(attempt.steps);
613
623
  const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
614
- const artifacts = attempt.attachments.filter((a) => a.path !== void 0 && a.name !== "" && !a.name.startsWith("_") && a.contentType !== METADATA_TYPE).map(({ name, contentType }) => contentType === void 0 ? { name } : { name, contentType });
624
+ 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 !== "");
615
625
  const ciUrl = webLink(attempt.buildHref);
616
626
  const failure = {
617
627
  ...headline !== void 0 ? { headline } : {},
@@ -629,30 +639,38 @@ function repoRootOf(dir, stopAt) {
629
639
  }
630
640
  }
631
641
  function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
632
- let text = (0, import_node_util.stripVTControlCharacters)(
633
- attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n")
634
- ).replaceAll("\x1B", "");
635
- const root = attempt.repoRoot === void 0 ? void 0 : pathIn(attempt.repoRoot);
636
- if (root !== void 0) text = text.replace(new RegExp(`${root.source}[\\\\/]`, root.flags), "");
642
+ const text = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
643
+ return cut(machineless(text, attempt.repoRoot, home));
644
+ }
645
+ function machineless(raw, repoRoot, home) {
646
+ let text = (0, import_node_util.stripVTControlCharacters)(raw).replaceAll("\x1B", "");
647
+ const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
648
+ if (root !== void 0) text = text.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
637
649
  const own = pathIn(home);
638
- if (own !== void 0) text = text.replace(new RegExp(`${own.source}(?![\\w.-])`, own.flags), "~");
639
- if (attempt.repoRoot === void 0) text = text.replace(ANY_HOME, "~");
640
- return cut(text);
650
+ if (own !== void 0) text = text.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
651
+ if (repoRoot === void 0) text = text.replace(ANY_HOME, "~");
652
+ return text;
641
653
  }
654
+ var PATH_START = "(?<![\\w.~%/\\\\-])";
642
655
  var TEXT_LIMIT = 524288;
643
- var ANY_HOME = /(?<=^|[\s'"`(=,[])(?:file:\/\/\/?)?(?:\/home\/|\/Users\/|[A-Za-z]:[\\/]Users[\\/])[^\\/\s'"`:*?<>|]+/g;
656
+ var HEADLINE_LIMIT = 8 * 1024;
657
+ var jsonBytes = (text) => Buffer.byteLength(JSON.stringify(text)) - 2;
658
+ var ANY_HOME = /(?<=^|[\s'"`(=,[])(?:(?:file|webpack|webpack-internal):(?:\\*\/){2,3})?(?:\\*\/home\\*\/|\\*\/Users\\*\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
644
659
  function pathIn(path3) {
645
660
  const parts = path3.split(/[\\/]+/);
646
661
  while (parts.length > 1 && parts.at(-1) === "") parts.pop();
647
662
  if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
648
- const escaped = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]");
649
- return new RegExp(`(?:file:\\/\\/\\/?)?${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
663
+ const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
664
+ const escaped = /^\/(?!\/)/.test(path3) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
665
+ const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
666
+ return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
650
667
  }
668
+ var fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
651
669
  function cut(text) {
652
- if (Buffer.byteLength(text) <= TEXT_LIMIT) return text;
670
+ if (jsonBytes(text) <= TEXT_LIMIT) return text;
653
671
  const lines = text.split("\n");
654
- const size = lines.map((line) => Buffer.byteLength(line) + 1);
655
- const budget = TEXT_LIMIT - Buffer.byteLength(omitted(lines.length));
672
+ const size = lines.map((line) => jsonBytes(line) + 2);
673
+ const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
656
674
  let used = 0;
657
675
  let head = 0;
658
676
  let tail = lines.length;
@@ -664,6 +682,7 @@ var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
664
682
  var METADATA_TYPE = "application/plune.metadata+json";
665
683
  var REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
666
684
  var FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
685
+ var FRAME_MAX = 8192;
667
686
  var firstLine = (text) => (0, import_node_util.stripVTControlCharacters)(text).split("\n").map((line) => line.trim()).find((line) => line !== "");
668
687
  function chosen({ status, errors }) {
669
688
  if (status === "timedOut") {
@@ -683,6 +702,7 @@ function placeOf(error, testFile) {
683
702
  const own = normal(testFile);
684
703
  let first;
685
704
  for (const line of (0, import_node_util.stripVTControlCharacters)(error.text).split("\n")) {
705
+ if (line.length > FRAME_MAX) continue;
686
706
  const frame = FRAME.exec(line);
687
707
  if (frame === null) continue;
688
708
  const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
@@ -693,7 +713,11 @@ function placeOf(error, testFile) {
693
713
  }
694
714
  function fileOf(raw) {
695
715
  if (!raw.startsWith("file://")) return raw;
696
- const path3 = decodeURIComponent(raw.slice("file://".length));
716
+ let path3 = raw.slice("file://".length);
717
+ try {
718
+ path3 = decodeURIComponent(path3);
719
+ } catch {
720
+ }
697
721
  return /^\/[A-Za-z]:\//.test(path3) ? path3.slice(1) : path3;
698
722
  }
699
723
  var normal = (path3) => import_node_path2.posix.normalize(path3.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
@@ -708,13 +732,7 @@ function fromRoot(place, root) {
708
732
  return column === void 0 ? { file: relative, line: place.line } : { file: relative, line: place.line, column };
709
733
  }
710
734
  function webLink(href) {
711
- if (href === void 0) return void 0;
712
- try {
713
- const { protocol } = new URL(href);
714
- return protocol === "http:" || protocol === "https:" ? href : void 0;
715
- } catch {
716
- return void 0;
717
- }
735
+ return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
718
736
  }
719
737
  // Annotate the CommonJS export names for ESM import in node:
720
738
  0 && (module.exports = {
@@ -402,7 +402,7 @@ declare function appendBatch(file: string, batch: DeferredBatch): void;
402
402
  * and nothing is searched for credentials — the platform does both after its own cleaning, and a cut
403
403
  * here could halve a credential it would then not recognise.
404
404
  */
405
- declare function failureOf(attempt: FailedAttempt): FailureDetail | undefined;
405
+ declare function failureOf(attempt: FailedAttempt, home?: string): FailureDetail | undefined;
406
406
  /**
407
407
  * The nearest folder with a `.git` above the runner's root dir — a folder, or the file a worktree
408
408
  * keeps instead. Found once per run by the caller; `stopAt` bounds the walk for a test.
@@ -415,7 +415,11 @@ declare function repoRootOf(dir: string, stopAt?: string): string | undefined;
415
415
  * searched for credentials: the platform does that before its own cut (ADR-0005).
416
416
  */
417
417
  declare function errorContextOf(attempt: FailedAttempt, home?: string): string;
418
- /** A link only for an address a browser opens as a page — never `file:`, `javascript:` or half a URL. */
418
+ /**
419
+ * A link only for an address a browser opens as a page — never `file:`, `javascript:` or half a URL.
420
+ * As the platform checks it: `new URL` alone mends `https:/host` and `https:host` into https, and a
421
+ * link the platform refuses costs the run's start or the whole batch.
422
+ */
419
423
  declare function webLink(href: string | undefined): string | undefined;
420
424
 
421
425
  type ClientFailureKind =
@@ -402,7 +402,7 @@ declare function appendBatch(file: string, batch: DeferredBatch): void;
402
402
  * and nothing is searched for credentials — the platform does both after its own cleaning, and a cut
403
403
  * here could halve a credential it would then not recognise.
404
404
  */
405
- declare function failureOf(attempt: FailedAttempt): FailureDetail | undefined;
405
+ declare function failureOf(attempt: FailedAttempt, home?: string): FailureDetail | undefined;
406
406
  /**
407
407
  * The nearest folder with a `.git` above the runner's root dir — a folder, or the file a worktree
408
408
  * keeps instead. Found once per run by the caller; `stopAt` bounds the walk for a test.
@@ -415,7 +415,11 @@ declare function repoRootOf(dir: string, stopAt?: string): string | undefined;
415
415
  * searched for credentials: the platform does that before its own cut (ADR-0005).
416
416
  */
417
417
  declare function errorContextOf(attempt: FailedAttempt, home?: string): string;
418
- /** A link only for an address a browser opens as a page — never `file:`, `javascript:` or half a URL. */
418
+ /**
419
+ * A link only for an address a browser opens as a page — never `file:`, `javascript:` or half a URL.
420
+ * As the platform checks it: `new URL` alone mends `https:/host` and `https:host` into https, and a
421
+ * link the platform refuses costs the run's start or the whole batch.
422
+ */
419
423
  declare function webLink(href: string | undefined): string | undefined;
420
424
 
421
425
  type ClientFailureKind =
@@ -112,7 +112,11 @@ function createClient(opts) {
112
112
  }
113
113
  if (res.ok) {
114
114
  if (res.status === 204) return { ok: true, status: res.status, body: void 0 };
115
- return { ok: true, status: res.status, body: await res.json() };
115
+ try {
116
+ return { ok: true, status: res.status, body: await res.json() };
117
+ } catch {
118
+ return { ok: false, kind: "other", status: res.status, detail: "the answer was not JSON" };
119
+ }
116
120
  }
117
121
  const detail = scrub(await detailOf(res));
118
122
  const kind = classify(res.status);
@@ -217,6 +221,7 @@ var BATCH_DEFAULT = 100;
217
221
  var EXPECTED_MAX = 1e4;
218
222
  var KEY_MAX = 1024;
219
223
  var TITLE_MAX = 300;
224
+ var COUNTED = ["accepted", "duplicate", "conflict", "rejected"];
220
225
  function boundKey(key) {
221
226
  if (key.value.length <= KEY_MAX) return key;
222
227
  const digest = createHash("sha256").update(key.value).digest("hex").slice(0, 16);
@@ -327,11 +332,13 @@ async function startRun(passed, declared) {
327
332
  "/v1/test-cases/resolve",
328
333
  { keys: part }
329
334
  );
330
- if (!out.ok) {
331
- log(`plune: could not look up test cases (${out.detail || out.kind}).`);
335
+ if (!out.ok || !Array.isArray(out.body?.results)) {
336
+ log(`plune: could not look up test cases (${out.ok ? "the answer had no results" : out.detail || out.kind}).`);
332
337
  return;
333
338
  }
334
- for (const row of out.body.results) resolved.set(row.key.value, row.testCaseId);
339
+ for (const row of out.body.results) {
340
+ if (typeof row?.key?.value === "string") resolved.set(row.key.value, row.testCaseId);
341
+ }
335
342
  }
336
343
  }
337
344
  if (!offline) {
@@ -368,12 +375,13 @@ async function startRun(passed, declared) {
368
375
  title: defaultRunTitle(),
369
376
  ...described
370
377
  });
371
- if (start.ok) {
378
+ if (start.ok && typeof start.body?.run?.id === "string") {
372
379
  runId = start.body.run.id;
373
380
  joined = start.status === 200;
374
381
  warnIfDropped(described, start.body.run, log);
375
382
  } else {
376
- log(`plune: could not start the run (${start.detail || start.kind}). Results will be written to ${fallbackPath}.`);
383
+ const why = start.ok ? "the answer had no run" : start.detail || start.kind;
384
+ log(`plune: could not start the run (${why}). Results will be written to ${fallbackPath}.`);
377
385
  }
378
386
  }
379
387
  function matchFor(result) {
@@ -408,15 +416,15 @@ async function startRun(passed, declared) {
408
416
  discovered: batch,
409
417
  runId
410
418
  });
411
- if (!out.ok) {
419
+ if (!out.ok || !Array.isArray(out.body?.results)) {
412
420
  stats.unoffered = batches.slice(index).reduce((n, rest) => n + rest.length, 0);
413
421
  log(
414
- `plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.detail || out.kind}).`
422
+ `plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.ok ? "the answer had no results" : out.detail || out.kind}).`
415
423
  );
416
424
  return;
417
425
  }
418
- stats.offered += out.body.results.filter((r) => r.outcome === "queued").length;
419
- stats.created += out.body.results.filter((r) => r.outcome === "created").length;
426
+ stats.offered += out.body.results.filter((r) => r?.outcome === "queued").length;
427
+ stats.created += out.body.results.filter((r) => r?.outcome === "created").length;
420
428
  }
421
429
  }
422
430
  async function flush() {
@@ -455,15 +463,16 @@ async function startRun(passed, declared) {
455
463
  const out = await client.post(`/v1/runs/${runId}/results`, {
456
464
  results: part
457
465
  });
458
- if (out.ok) {
459
- stats.accepted += out.body.counts.accepted ?? 0;
460
- stats.duplicate += out.body.counts.duplicate ?? 0;
461
- stats.conflict += out.body.counts.conflict ?? 0;
462
- stats.rejected += out.body.counts.rejected ?? 0;
466
+ const counts = out.ok ? out.body?.counts : void 0;
467
+ const told = COUNTED.filter((k) => typeof counts?.[k] === "number");
468
+ if (told.length > 0) {
469
+ for (const k of told) stats[k] += counts[k];
463
470
  progress();
464
471
  continue;
465
472
  }
466
- if (out.kind === "auth") {
473
+ if (out.ok) {
474
+ log("plune: could not send results (the answer carried no counts).");
475
+ } else if (out.kind === "auth") {
467
476
  log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
468
477
  offline = true;
469
478
  } else if (out.kind === "conflict") {
@@ -521,7 +530,7 @@ async function startRun(passed, declared) {
521
530
  ...reason !== void 0 ? { reason } : {}
522
531
  });
523
532
  if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
524
- else if (typeof out.body.detached === "number" && out.body.detached > 0) {
533
+ else if (typeof out.body?.detached === "number" && out.body.detached > 0) {
525
534
  log(`plune: ${out.body.detached} test case(s) marked detached \u2014 this full run no longer reports them.`);
526
535
  }
527
536
  }
@@ -559,12 +568,13 @@ import { existsSync as existsSync2 } from "fs";
559
568
  import { homedir as homedir2 } from "os";
560
569
  import { dirname as dirname3, join as join3, posix, resolve } from "path";
561
570
  import { stripVTControlCharacters } from "util";
562
- function failureOf(attempt) {
571
+ function failureOf(attempt, home = homedir2()) {
563
572
  const error = chosen(attempt);
564
- const headline = error === void 0 ? void 0 : firstLine(error.text);
573
+ const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
574
+ const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
565
575
  const steps = chainOf(attempt.steps);
566
576
  const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
567
- const artifacts = attempt.attachments.filter((a) => a.path !== void 0 && a.name !== "" && !a.name.startsWith("_") && a.contentType !== METADATA_TYPE).map(({ name, contentType }) => contentType === void 0 ? { name } : { name, contentType });
577
+ 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 !== "");
568
578
  const ciUrl = webLink(attempt.buildHref);
569
579
  const failure = {
570
580
  ...headline !== void 0 ? { headline } : {},
@@ -582,30 +592,38 @@ function repoRootOf(dir, stopAt) {
582
592
  }
583
593
  }
584
594
  function errorContextOf(attempt, home = homedir2()) {
585
- let text = stripVTControlCharacters(
586
- attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n")
587
- ).replaceAll("\x1B", "");
588
- const root = attempt.repoRoot === void 0 ? void 0 : pathIn(attempt.repoRoot);
589
- if (root !== void 0) text = text.replace(new RegExp(`${root.source}[\\\\/]`, root.flags), "");
595
+ const text = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
596
+ return cut(machineless(text, attempt.repoRoot, home));
597
+ }
598
+ function machineless(raw, repoRoot, home) {
599
+ let text = stripVTControlCharacters(raw).replaceAll("\x1B", "");
600
+ const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
601
+ if (root !== void 0) text = text.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
590
602
  const own = pathIn(home);
591
- if (own !== void 0) text = text.replace(new RegExp(`${own.source}(?![\\w.-])`, own.flags), "~");
592
- if (attempt.repoRoot === void 0) text = text.replace(ANY_HOME, "~");
593
- return cut(text);
603
+ if (own !== void 0) text = text.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
604
+ if (repoRoot === void 0) text = text.replace(ANY_HOME, "~");
605
+ return text;
594
606
  }
607
+ var PATH_START = "(?<![\\w.~%/\\\\-])";
595
608
  var TEXT_LIMIT = 524288;
596
- var ANY_HOME = /(?<=^|[\s'"`(=,[])(?:file:\/\/\/?)?(?:\/home\/|\/Users\/|[A-Za-z]:[\\/]Users[\\/])[^\\/\s'"`:*?<>|]+/g;
609
+ var HEADLINE_LIMIT = 8 * 1024;
610
+ var jsonBytes = (text) => Buffer.byteLength(JSON.stringify(text)) - 2;
611
+ var ANY_HOME = /(?<=^|[\s'"`(=,[])(?:(?:file|webpack|webpack-internal):(?:\\*\/){2,3})?(?:\\*\/home\\*\/|\\*\/Users\\*\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
597
612
  function pathIn(path3) {
598
613
  const parts = path3.split(/[\\/]+/);
599
614
  while (parts.length > 1 && parts.at(-1) === "") parts.pop();
600
615
  if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
601
- const escaped = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]");
602
- return new RegExp(`(?:file:\\/\\/\\/?)?${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
616
+ const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
617
+ const escaped = /^\/(?!\/)/.test(path3) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
618
+ const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
619
+ return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
603
620
  }
621
+ var fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
604
622
  function cut(text) {
605
- if (Buffer.byteLength(text) <= TEXT_LIMIT) return text;
623
+ if (jsonBytes(text) <= TEXT_LIMIT) return text;
606
624
  const lines = text.split("\n");
607
- const size = lines.map((line) => Buffer.byteLength(line) + 1);
608
- const budget = TEXT_LIMIT - Buffer.byteLength(omitted(lines.length));
625
+ const size = lines.map((line) => jsonBytes(line) + 2);
626
+ const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
609
627
  let used = 0;
610
628
  let head = 0;
611
629
  let tail = lines.length;
@@ -617,6 +635,7 @@ var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
617
635
  var METADATA_TYPE = "application/plune.metadata+json";
618
636
  var REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
619
637
  var FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
638
+ var FRAME_MAX = 8192;
620
639
  var firstLine = (text) => stripVTControlCharacters(text).split("\n").map((line) => line.trim()).find((line) => line !== "");
621
640
  function chosen({ status, errors }) {
622
641
  if (status === "timedOut") {
@@ -636,6 +655,7 @@ function placeOf(error, testFile) {
636
655
  const own = normal(testFile);
637
656
  let first;
638
657
  for (const line of stripVTControlCharacters(error.text).split("\n")) {
658
+ if (line.length > FRAME_MAX) continue;
639
659
  const frame = FRAME.exec(line);
640
660
  if (frame === null) continue;
641
661
  const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
@@ -646,7 +666,11 @@ function placeOf(error, testFile) {
646
666
  }
647
667
  function fileOf(raw) {
648
668
  if (!raw.startsWith("file://")) return raw;
649
- const path3 = decodeURIComponent(raw.slice("file://".length));
669
+ let path3 = raw.slice("file://".length);
670
+ try {
671
+ path3 = decodeURIComponent(path3);
672
+ } catch {
673
+ }
650
674
  return /^\/[A-Za-z]:\//.test(path3) ? path3.slice(1) : path3;
651
675
  }
652
676
  var normal = (path3) => posix.normalize(path3.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
@@ -661,13 +685,7 @@ function fromRoot(place, root) {
661
685
  return column === void 0 ? { file: relative, line: place.line } : { file: relative, line: place.line, column };
662
686
  }
663
687
  function webLink(href) {
664
- if (href === void 0) return void 0;
665
- try {
666
- const { protocol } = new URL(href);
667
- return protocol === "http:" || protocol === "https:" ? href : void 0;
668
- } catch {
669
- return void 0;
670
- }
688
+ return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
671
689
  }
672
690
  export {
673
691
  DEFAULT_FALLBACK_PATH,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plune-ai/cli",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "AI-powered assertion testing for LLM apps — write assertions, run them against any provider, get a pass/fail report (local, CI, or diff).",
5
5
  "keywords": [
6
6
  "ai",