@plune-ai/cli 0.14.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 +46 -32
- package/dist/reporter-core.cjs +45 -31
- package/dist/reporter-core.d.cts +6 -2
- package/dist/reporter-core.d.ts +6 -2
- package/dist/reporter-core.js +45 -31
- package/package.json +1 -1
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
|
-
|
|
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);
|
|
@@ -2625,15 +2629,18 @@ async function startRun(passed, declared) {
|
|
|
2625
2629
|
const out = await client.post(`/v1/runs/${runId}/results`, {
|
|
2626
2630
|
results: part
|
|
2627
2631
|
});
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
stats.
|
|
2631
|
-
stats.
|
|
2632
|
-
stats.
|
|
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;
|
|
2633
2638
|
progress();
|
|
2634
2639
|
continue;
|
|
2635
2640
|
}
|
|
2636
|
-
if (out.
|
|
2641
|
+
if (out.ok) {
|
|
2642
|
+
log("plune: could not send results (the answer carried no counts).");
|
|
2643
|
+
} else if (out.kind === "auth") {
|
|
2637
2644
|
log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
|
|
2638
2645
|
offline = true;
|
|
2639
2646
|
} else if (out.kind === "conflict") {
|
|
@@ -3164,12 +3171,13 @@ var init_junit = __esm({
|
|
|
3164
3171
|
});
|
|
3165
3172
|
|
|
3166
3173
|
// src/reporter-core/failure-detail.ts
|
|
3167
|
-
function failureOf(attempt) {
|
|
3174
|
+
function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
3168
3175
|
const error = chosen(attempt);
|
|
3169
|
-
const
|
|
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;
|
|
3170
3178
|
const steps = chainOf(attempt.steps);
|
|
3171
3179
|
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 &&
|
|
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 !== "");
|
|
3173
3181
|
const ciUrl = webLink(attempt.buildHref);
|
|
3174
3182
|
const failure = {
|
|
3175
3183
|
...headline !== void 0 ? { headline } : {},
|
|
@@ -3187,28 +3195,30 @@ function repoRootOf(dir, stopAt) {
|
|
|
3187
3195
|
}
|
|
3188
3196
|
}
|
|
3189
3197
|
function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
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), "");
|
|
3195
3205
|
const own = pathIn(home);
|
|
3196
|
-
if (own !== void 0) text2 = text2.replace(new RegExp(`${own.source}(?![\\w.-])`, own.flags), "~");
|
|
3197
|
-
if (
|
|
3198
|
-
return
|
|
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;
|
|
3199
3209
|
}
|
|
3200
3210
|
function pathIn(path14) {
|
|
3201
3211
|
const parts = path14.split(/[\\/]+/);
|
|
3202
3212
|
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
3203
3213
|
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
3204
|
-
const escaped = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]");
|
|
3214
|
+
const escaped = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
3205
3215
|
return new RegExp(`(?:file:\\/\\/\\/?)?${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
3206
3216
|
}
|
|
3207
3217
|
function cut(text2) {
|
|
3208
|
-
if (
|
|
3218
|
+
if (jsonBytes(text2) <= TEXT_LIMIT) return text2;
|
|
3209
3219
|
const lines = text2.split("\n");
|
|
3210
|
-
const size = lines.map((line) =>
|
|
3211
|
-
const budget = TEXT_LIMIT -
|
|
3220
|
+
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
3221
|
+
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
3212
3222
|
let used = 0;
|
|
3213
3223
|
let head = 0;
|
|
3214
3224
|
let tail = lines.length;
|
|
@@ -3234,6 +3244,7 @@ function placeOf(error, testFile) {
|
|
|
3234
3244
|
const own = normal(testFile);
|
|
3235
3245
|
let first;
|
|
3236
3246
|
for (const line of (0, import_node_util.stripVTControlCharacters)(error.text).split("\n")) {
|
|
3247
|
+
if (line.length > FRAME_MAX) continue;
|
|
3237
3248
|
const frame = FRAME.exec(line);
|
|
3238
3249
|
if (frame === null) continue;
|
|
3239
3250
|
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
@@ -3244,7 +3255,11 @@ function placeOf(error, testFile) {
|
|
|
3244
3255
|
}
|
|
3245
3256
|
function fileOf(raw) {
|
|
3246
3257
|
if (!raw.startsWith("file://")) return raw;
|
|
3247
|
-
|
|
3258
|
+
let path14 = raw.slice("file://".length);
|
|
3259
|
+
try {
|
|
3260
|
+
path14 = decodeURIComponent(path14);
|
|
3261
|
+
} catch {
|
|
3262
|
+
}
|
|
3248
3263
|
return /^\/[A-Za-z]:\//.test(path14) ? path14.slice(1) : path14;
|
|
3249
3264
|
}
|
|
3250
3265
|
function fromRoot(place, root) {
|
|
@@ -3258,15 +3273,9 @@ function fromRoot(place, root) {
|
|
|
3258
3273
|
return column === void 0 ? { file: relative2, line: place.line } : { file: relative2, line: place.line, column };
|
|
3259
3274
|
}
|
|
3260
3275
|
function webLink(href) {
|
|
3261
|
-
|
|
3262
|
-
try {
|
|
3263
|
-
const { protocol } = new URL(href);
|
|
3264
|
-
return protocol === "http:" || protocol === "https:" ? href : void 0;
|
|
3265
|
-
} catch {
|
|
3266
|
-
return void 0;
|
|
3267
|
-
}
|
|
3276
|
+
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
3268
3277
|
}
|
|
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;
|
|
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;
|
|
3270
3279
|
var init_failure_detail = __esm({
|
|
3271
3280
|
"src/reporter-core/failure-detail.ts"() {
|
|
3272
3281
|
"use strict";
|
|
@@ -3275,12 +3284,17 @@ var init_failure_detail = __esm({
|
|
|
3275
3284
|
import_node_os2 = require("os");
|
|
3276
3285
|
import_node_path2 = require("path");
|
|
3277
3286
|
import_node_util = require("util");
|
|
3287
|
+
PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
3278
3288
|
TEXT_LIMIT = 524288;
|
|
3279
|
-
|
|
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;
|
|
3280
3293
|
omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
3281
3294
|
METADATA_TYPE = "application/plune.metadata+json";
|
|
3282
3295
|
REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
3283
3296
|
FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
3297
|
+
FRAME_MAX = 8192;
|
|
3284
3298
|
firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
3285
3299
|
normal = (path14) => import_node_path2.posix.normalize(path14.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
3286
3300
|
}
|
package/dist/reporter-core.cjs
CHANGED
|
@@ -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
|
-
|
|
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);
|
|
@@ -502,15 +506,18 @@ async function startRun(passed, declared) {
|
|
|
502
506
|
const out = await client.post(`/v1/runs/${runId}/results`, {
|
|
503
507
|
results: part
|
|
504
508
|
});
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
stats.
|
|
508
|
-
stats.
|
|
509
|
-
stats.
|
|
509
|
+
const counts = out.ok ? out.body?.counts : void 0;
|
|
510
|
+
if (counts !== void 0) {
|
|
511
|
+
stats.accepted += counts.accepted ?? 0;
|
|
512
|
+
stats.duplicate += counts.duplicate ?? 0;
|
|
513
|
+
stats.conflict += counts.conflict ?? 0;
|
|
514
|
+
stats.rejected += counts.rejected ?? 0;
|
|
510
515
|
progress();
|
|
511
516
|
continue;
|
|
512
517
|
}
|
|
513
|
-
if (out.
|
|
518
|
+
if (out.ok) {
|
|
519
|
+
log("plune: could not send results (the answer carried no counts).");
|
|
520
|
+
} else if (out.kind === "auth") {
|
|
514
521
|
log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
|
|
515
522
|
offline = true;
|
|
516
523
|
} else if (out.kind === "conflict") {
|
|
@@ -606,12 +613,13 @@ var import_node_fs2 = require("fs");
|
|
|
606
613
|
var import_node_os2 = require("os");
|
|
607
614
|
var import_node_path2 = require("path");
|
|
608
615
|
var import_node_util = require("util");
|
|
609
|
-
function failureOf(attempt) {
|
|
616
|
+
function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
610
617
|
const error = chosen(attempt);
|
|
611
|
-
const
|
|
618
|
+
const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
|
|
619
|
+
const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
|
|
612
620
|
const steps = chainOf(attempt.steps);
|
|
613
621
|
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 &&
|
|
622
|
+
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
623
|
const ciUrl = webLink(attempt.buildHref);
|
|
616
624
|
const failure = {
|
|
617
625
|
...headline !== void 0 ? { headline } : {},
|
|
@@ -629,30 +637,36 @@ function repoRootOf(dir, stopAt) {
|
|
|
629
637
|
}
|
|
630
638
|
}
|
|
631
639
|
function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
640
|
+
const text = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
641
|
+
return cut(machineless(text, attempt.repoRoot, home));
|
|
642
|
+
}
|
|
643
|
+
function machineless(raw, repoRoot, home) {
|
|
644
|
+
let text = (0, import_node_util.stripVTControlCharacters)(raw).replaceAll("\x1B", "");
|
|
645
|
+
const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
|
|
646
|
+
if (root !== void 0) text = text.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
|
|
637
647
|
const own = pathIn(home);
|
|
638
|
-
if (own !== void 0) text = text.replace(new RegExp(`${own.source}(?![\\w.-])`, own.flags), "~");
|
|
639
|
-
if (
|
|
640
|
-
return
|
|
648
|
+
if (own !== void 0) text = text.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
|
|
649
|
+
if (repoRoot === void 0) text = text.replace(ANY_HOME, "~");
|
|
650
|
+
return text;
|
|
641
651
|
}
|
|
652
|
+
var PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
642
653
|
var TEXT_LIMIT = 524288;
|
|
643
|
-
var
|
|
654
|
+
var HEADLINE_LIMIT = 8 * 1024;
|
|
655
|
+
var jsonBytes = (text) => Buffer.byteLength(JSON.stringify(text)) - 2;
|
|
656
|
+
var ANY_HOME = /(?<=^|[\s'"`(=,[])(?:file:\/\/\/?)?(?:\/home\/|\/Users\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
|
|
644
657
|
function pathIn(path3) {
|
|
645
658
|
const parts = path3.split(/[\\/]+/);
|
|
646
659
|
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
647
660
|
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
648
|
-
const escaped = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]");
|
|
661
|
+
const escaped = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
649
662
|
return new RegExp(`(?:file:\\/\\/\\/?)?${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
650
663
|
}
|
|
664
|
+
var fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
651
665
|
function cut(text) {
|
|
652
|
-
if (
|
|
666
|
+
if (jsonBytes(text) <= TEXT_LIMIT) return text;
|
|
653
667
|
const lines = text.split("\n");
|
|
654
|
-
const size = lines.map((line) =>
|
|
655
|
-
const budget = TEXT_LIMIT -
|
|
668
|
+
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
669
|
+
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
656
670
|
let used = 0;
|
|
657
671
|
let head = 0;
|
|
658
672
|
let tail = lines.length;
|
|
@@ -664,6 +678,7 @@ var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
|
664
678
|
var METADATA_TYPE = "application/plune.metadata+json";
|
|
665
679
|
var REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
666
680
|
var FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
681
|
+
var FRAME_MAX = 8192;
|
|
667
682
|
var firstLine = (text) => (0, import_node_util.stripVTControlCharacters)(text).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
668
683
|
function chosen({ status, errors }) {
|
|
669
684
|
if (status === "timedOut") {
|
|
@@ -683,6 +698,7 @@ function placeOf(error, testFile) {
|
|
|
683
698
|
const own = normal(testFile);
|
|
684
699
|
let first;
|
|
685
700
|
for (const line of (0, import_node_util.stripVTControlCharacters)(error.text).split("\n")) {
|
|
701
|
+
if (line.length > FRAME_MAX) continue;
|
|
686
702
|
const frame = FRAME.exec(line);
|
|
687
703
|
if (frame === null) continue;
|
|
688
704
|
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
@@ -693,7 +709,11 @@ function placeOf(error, testFile) {
|
|
|
693
709
|
}
|
|
694
710
|
function fileOf(raw) {
|
|
695
711
|
if (!raw.startsWith("file://")) return raw;
|
|
696
|
-
|
|
712
|
+
let path3 = raw.slice("file://".length);
|
|
713
|
+
try {
|
|
714
|
+
path3 = decodeURIComponent(path3);
|
|
715
|
+
} catch {
|
|
716
|
+
}
|
|
697
717
|
return /^\/[A-Za-z]:\//.test(path3) ? path3.slice(1) : path3;
|
|
698
718
|
}
|
|
699
719
|
var normal = (path3) => import_node_path2.posix.normalize(path3.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
@@ -708,13 +728,7 @@ function fromRoot(place, root) {
|
|
|
708
728
|
return column === void 0 ? { file: relative, line: place.line } : { file: relative, line: place.line, column };
|
|
709
729
|
}
|
|
710
730
|
function webLink(href) {
|
|
711
|
-
|
|
712
|
-
try {
|
|
713
|
-
const { protocol } = new URL(href);
|
|
714
|
-
return protocol === "http:" || protocol === "https:" ? href : void 0;
|
|
715
|
-
} catch {
|
|
716
|
-
return void 0;
|
|
717
|
-
}
|
|
731
|
+
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
718
732
|
}
|
|
719
733
|
// Annotate the CommonJS export names for ESM import in node:
|
|
720
734
|
0 && (module.exports = {
|
package/dist/reporter-core.d.cts
CHANGED
|
@@ -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
|
-
/**
|
|
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 =
|
package/dist/reporter-core.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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 =
|
package/dist/reporter-core.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
|
@@ -455,15 +459,18 @@ async function startRun(passed, declared) {
|
|
|
455
459
|
const out = await client.post(`/v1/runs/${runId}/results`, {
|
|
456
460
|
results: part
|
|
457
461
|
});
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
stats.
|
|
461
|
-
stats.
|
|
462
|
-
stats.
|
|
462
|
+
const counts = out.ok ? out.body?.counts : void 0;
|
|
463
|
+
if (counts !== void 0) {
|
|
464
|
+
stats.accepted += counts.accepted ?? 0;
|
|
465
|
+
stats.duplicate += counts.duplicate ?? 0;
|
|
466
|
+
stats.conflict += counts.conflict ?? 0;
|
|
467
|
+
stats.rejected += counts.rejected ?? 0;
|
|
463
468
|
progress();
|
|
464
469
|
continue;
|
|
465
470
|
}
|
|
466
|
-
if (out.
|
|
471
|
+
if (out.ok) {
|
|
472
|
+
log("plune: could not send results (the answer carried no counts).");
|
|
473
|
+
} else if (out.kind === "auth") {
|
|
467
474
|
log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
|
|
468
475
|
offline = true;
|
|
469
476
|
} else if (out.kind === "conflict") {
|
|
@@ -559,12 +566,13 @@ import { existsSync as existsSync2 } from "fs";
|
|
|
559
566
|
import { homedir as homedir2 } from "os";
|
|
560
567
|
import { dirname as dirname3, join as join3, posix, resolve } from "path";
|
|
561
568
|
import { stripVTControlCharacters } from "util";
|
|
562
|
-
function failureOf(attempt) {
|
|
569
|
+
function failureOf(attempt, home = homedir2()) {
|
|
563
570
|
const error = chosen(attempt);
|
|
564
|
-
const
|
|
571
|
+
const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
|
|
572
|
+
const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
|
|
565
573
|
const steps = chainOf(attempt.steps);
|
|
566
574
|
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 &&
|
|
575
|
+
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
576
|
const ciUrl = webLink(attempt.buildHref);
|
|
569
577
|
const failure = {
|
|
570
578
|
...headline !== void 0 ? { headline } : {},
|
|
@@ -582,30 +590,36 @@ function repoRootOf(dir, stopAt) {
|
|
|
582
590
|
}
|
|
583
591
|
}
|
|
584
592
|
function errorContextOf(attempt, home = homedir2()) {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
593
|
+
const text = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
594
|
+
return cut(machineless(text, attempt.repoRoot, home));
|
|
595
|
+
}
|
|
596
|
+
function machineless(raw, repoRoot, home) {
|
|
597
|
+
let text = stripVTControlCharacters(raw).replaceAll("\x1B", "");
|
|
598
|
+
const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
|
|
599
|
+
if (root !== void 0) text = text.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
|
|
590
600
|
const own = pathIn(home);
|
|
591
|
-
if (own !== void 0) text = text.replace(new RegExp(`${own.source}(?![\\w.-])`, own.flags), "~");
|
|
592
|
-
if (
|
|
593
|
-
return
|
|
601
|
+
if (own !== void 0) text = text.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
|
|
602
|
+
if (repoRoot === void 0) text = text.replace(ANY_HOME, "~");
|
|
603
|
+
return text;
|
|
594
604
|
}
|
|
605
|
+
var PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
595
606
|
var TEXT_LIMIT = 524288;
|
|
596
|
-
var
|
|
607
|
+
var HEADLINE_LIMIT = 8 * 1024;
|
|
608
|
+
var jsonBytes = (text) => Buffer.byteLength(JSON.stringify(text)) - 2;
|
|
609
|
+
var ANY_HOME = /(?<=^|[\s'"`(=,[])(?:file:\/\/\/?)?(?:\/home\/|\/Users\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
|
|
597
610
|
function pathIn(path3) {
|
|
598
611
|
const parts = path3.split(/[\\/]+/);
|
|
599
612
|
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
600
613
|
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
601
|
-
const escaped = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]");
|
|
614
|
+
const escaped = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
602
615
|
return new RegExp(`(?:file:\\/\\/\\/?)?${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
603
616
|
}
|
|
617
|
+
var fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
604
618
|
function cut(text) {
|
|
605
|
-
if (
|
|
619
|
+
if (jsonBytes(text) <= TEXT_LIMIT) return text;
|
|
606
620
|
const lines = text.split("\n");
|
|
607
|
-
const size = lines.map((line) =>
|
|
608
|
-
const budget = TEXT_LIMIT -
|
|
621
|
+
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
622
|
+
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
609
623
|
let used = 0;
|
|
610
624
|
let head = 0;
|
|
611
625
|
let tail = lines.length;
|
|
@@ -617,6 +631,7 @@ var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
|
617
631
|
var METADATA_TYPE = "application/plune.metadata+json";
|
|
618
632
|
var REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
619
633
|
var FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
634
|
+
var FRAME_MAX = 8192;
|
|
620
635
|
var firstLine = (text) => stripVTControlCharacters(text).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
621
636
|
function chosen({ status, errors }) {
|
|
622
637
|
if (status === "timedOut") {
|
|
@@ -636,6 +651,7 @@ function placeOf(error, testFile) {
|
|
|
636
651
|
const own = normal(testFile);
|
|
637
652
|
let first;
|
|
638
653
|
for (const line of stripVTControlCharacters(error.text).split("\n")) {
|
|
654
|
+
if (line.length > FRAME_MAX) continue;
|
|
639
655
|
const frame = FRAME.exec(line);
|
|
640
656
|
if (frame === null) continue;
|
|
641
657
|
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
@@ -646,7 +662,11 @@ function placeOf(error, testFile) {
|
|
|
646
662
|
}
|
|
647
663
|
function fileOf(raw) {
|
|
648
664
|
if (!raw.startsWith("file://")) return raw;
|
|
649
|
-
|
|
665
|
+
let path3 = raw.slice("file://".length);
|
|
666
|
+
try {
|
|
667
|
+
path3 = decodeURIComponent(path3);
|
|
668
|
+
} catch {
|
|
669
|
+
}
|
|
650
670
|
return /^\/[A-Za-z]:\//.test(path3) ? path3.slice(1) : path3;
|
|
651
671
|
}
|
|
652
672
|
var normal = (path3) => posix.normalize(path3.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
@@ -661,13 +681,7 @@ function fromRoot(place, root) {
|
|
|
661
681
|
return column === void 0 ? { file: relative, line: place.line } : { file: relative, line: place.line, column };
|
|
662
682
|
}
|
|
663
683
|
function webLink(href) {
|
|
664
|
-
|
|
665
|
-
try {
|
|
666
|
-
const { protocol } = new URL(href);
|
|
667
|
-
return protocol === "http:" || protocol === "https:" ? href : void 0;
|
|
668
|
-
} catch {
|
|
669
|
-
return void 0;
|
|
670
|
-
}
|
|
684
|
+
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
671
685
|
}
|
|
672
686
|
export {
|
|
673
687
|
DEFAULT_FALLBACK_PATH,
|
package/package.json
CHANGED