@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 +290 -60
- package/dist/reporter-core.cjs +222 -25
- package/dist/reporter-core.d.cts +102 -11
- package/dist/reporter-core.d.ts +102 -11
- package/dist/reporter-core.js +217 -24
- package/package.json +1 -1
package/dist/reporter-core.js
CHANGED
|
@@ -28,7 +28,39 @@ function loadToken() {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// src/reporter-core/client.ts
|
|
31
|
+
import { request as httpRequest } from "http";
|
|
32
|
+
import { request as httpsRequest } from "https";
|
|
31
33
|
var RETRY_DELAYS_MS = [300, 900];
|
|
34
|
+
var IDLE_MS = 3e5;
|
|
35
|
+
function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
36
|
+
return new Promise((resolve2, reject) => {
|
|
37
|
+
const target = new URL(url);
|
|
38
|
+
const send = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
39
|
+
const req = send(target, { method: init.method, headers: init.headers }, (res) => {
|
|
40
|
+
const chunks = [];
|
|
41
|
+
res.on("data", (chunk2) => chunks.push(chunk2));
|
|
42
|
+
res.on("error", reject);
|
|
43
|
+
res.on("end", () => {
|
|
44
|
+
const status = res.statusCode ?? 0;
|
|
45
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
46
|
+
resolve2({
|
|
47
|
+
ok: status >= 200 && status <= 299,
|
|
48
|
+
status,
|
|
49
|
+
headers: {
|
|
50
|
+
get: (name) => {
|
|
51
|
+
const value2 = res.headers[name.toLowerCase()];
|
|
52
|
+
return value2 === void 0 ? null : Array.isArray(value2) ? value2.join(", ") : value2;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
json: async () => JSON.parse(text)
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
req.setTimeout(idleMs, () => req.destroy(new Error(`no answer from ${target.origin} in ${idleMs} ms`)));
|
|
60
|
+
req.on("error", reject);
|
|
61
|
+
req.end(init.body);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
32
64
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
33
65
|
async function detailOf(res) {
|
|
34
66
|
try {
|
|
@@ -52,7 +84,7 @@ function classify(status) {
|
|
|
52
84
|
return "other";
|
|
53
85
|
}
|
|
54
86
|
function createClient(opts) {
|
|
55
|
-
const doFetch = opts.fetchImpl ??
|
|
87
|
+
const doFetch = opts.fetchImpl ?? nodeTransport;
|
|
56
88
|
const wait = opts.wait ?? sleep;
|
|
57
89
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
58
90
|
const scrub = (text) => opts.token === "" ? text : text.split(opts.token).join("***");
|
|
@@ -80,7 +112,11 @@ function createClient(opts) {
|
|
|
80
112
|
}
|
|
81
113
|
if (res.ok) {
|
|
82
114
|
if (res.status === 204) return { ok: true, status: res.status, body: void 0 };
|
|
83
|
-
|
|
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
|
+
}
|
|
84
120
|
}
|
|
85
121
|
const detail = scrub(await detailOf(res));
|
|
86
122
|
const kind = classify(res.status);
|
|
@@ -195,6 +231,25 @@ function boundKey(key) {
|
|
|
195
231
|
function boundTitle(title) {
|
|
196
232
|
return title.length <= TITLE_MAX ? title : `${title.slice(0, TITLE_MAX - 1)}\u2026`;
|
|
197
233
|
}
|
|
234
|
+
var BATCH_BYTES = 8 * 1024 * 1024;
|
|
235
|
+
var ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ results: [] }));
|
|
236
|
+
function packed(results) {
|
|
237
|
+
const out = [];
|
|
238
|
+
let part = [];
|
|
239
|
+
let bytes = ENVELOPE_BYTES;
|
|
240
|
+
for (const result of results) {
|
|
241
|
+
const size = Buffer.byteLength(JSON.stringify(result));
|
|
242
|
+
if (part.length > 0 && bytes + 1 + size > BATCH_BYTES) {
|
|
243
|
+
out.push(part);
|
|
244
|
+
part = [];
|
|
245
|
+
bytes = ENVELOPE_BYTES;
|
|
246
|
+
}
|
|
247
|
+
bytes += (part.length > 0 ? 1 : 0) + size;
|
|
248
|
+
part.push(result);
|
|
249
|
+
}
|
|
250
|
+
if (part.length > 0) out.push(part);
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
198
253
|
function chunk(items, size) {
|
|
199
254
|
const out = [];
|
|
200
255
|
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
@@ -396,27 +451,35 @@ async function startRun(passed, declared) {
|
|
|
396
451
|
progress();
|
|
397
452
|
return;
|
|
398
453
|
}
|
|
399
|
-
const
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
454
|
+
for (const part of packed(submissions)) {
|
|
455
|
+
if (offline) {
|
|
456
|
+
defer(part);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const out = await client.post(`/v1/runs/${runId}/results`, {
|
|
460
|
+
results: part
|
|
461
|
+
});
|
|
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;
|
|
468
|
+
progress();
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (out.ok) {
|
|
472
|
+
log("plune: could not send results (the answer carried no counts).");
|
|
473
|
+
} else if (out.kind === "auth") {
|
|
474
|
+
log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
|
|
475
|
+
offline = true;
|
|
476
|
+
} else if (out.kind === "conflict") {
|
|
477
|
+
log(`plune: this run is closed \u2014 ${out.detail}`);
|
|
478
|
+
} else {
|
|
479
|
+
log(`plune: could not send results (${out.detail || out.kind}).`);
|
|
480
|
+
}
|
|
481
|
+
defer(part);
|
|
418
482
|
}
|
|
419
|
-
defer(submissions);
|
|
420
483
|
}
|
|
421
484
|
function progress() {
|
|
422
485
|
const total = expected?.length ?? 0;
|
|
@@ -432,9 +495,12 @@ async function startRun(passed, declared) {
|
|
|
432
495
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
433
496
|
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
434
497
|
if (stats.created > 0) parts.push(`${stats.created} added as cases (trusted source)`);
|
|
435
|
-
if (stats.deferred > 0) parts.push(`${stats.deferred} written to ${fallbackPath}`);
|
|
498
|
+
if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
|
|
436
499
|
if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
|
|
437
500
|
log(`plune: ${parts.join(" \xB7 ")}`);
|
|
501
|
+
if (stats.deferred > 0 && process.env["GITHUB_ACTIONS"] === "true") {
|
|
502
|
+
log(`::warning::${stats.deferred} result(s) were not delivered to Plune \u2014 see the reporting step's log.`);
|
|
503
|
+
}
|
|
438
504
|
}
|
|
439
505
|
return {
|
|
440
506
|
get runId() {
|
|
@@ -494,13 +560,140 @@ function resultKey(testId, retry) {
|
|
|
494
560
|
const plain = `${testId}#${retry}`;
|
|
495
561
|
return plain.length <= RESULT_KEY_MAX ? plain : `${sha256(testId).slice(0, HASH_CHARS)}#${retry}`;
|
|
496
562
|
}
|
|
563
|
+
|
|
564
|
+
// src/reporter-core/failure-detail.ts
|
|
565
|
+
import { existsSync as existsSync2 } from "fs";
|
|
566
|
+
import { homedir as homedir2 } from "os";
|
|
567
|
+
import { dirname as dirname3, join as join3, posix, resolve } from "path";
|
|
568
|
+
import { stripVTControlCharacters } from "util";
|
|
569
|
+
function failureOf(attempt, home = homedir2()) {
|
|
570
|
+
const error = chosen(attempt);
|
|
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;
|
|
573
|
+
const steps = chainOf(attempt.steps);
|
|
574
|
+
const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
|
|
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 !== "");
|
|
576
|
+
const ciUrl = webLink(attempt.buildHref);
|
|
577
|
+
const failure = {
|
|
578
|
+
...headline !== void 0 ? { headline } : {},
|
|
579
|
+
...steps.length > 0 ? { steps } : {},
|
|
580
|
+
...location !== void 0 ? { location } : {},
|
|
581
|
+
...artifacts.length > 0 ? { artifacts } : {},
|
|
582
|
+
...ciUrl !== void 0 ? { ciUrl } : {}
|
|
583
|
+
};
|
|
584
|
+
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
585
|
+
}
|
|
586
|
+
function repoRootOf(dir, stopAt) {
|
|
587
|
+
for (let here = resolve(dir); ; here = dirname3(here)) {
|
|
588
|
+
if (existsSync2(join3(here, ".git"))) return here;
|
|
589
|
+
if (here === stopAt || dirname3(here) === here) return void 0;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
function errorContextOf(attempt, home = homedir2()) {
|
|
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), "");
|
|
600
|
+
const own = pathIn(home);
|
|
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;
|
|
604
|
+
}
|
|
605
|
+
var PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
606
|
+
var TEXT_LIMIT = 524288;
|
|
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;
|
|
610
|
+
function pathIn(path3) {
|
|
611
|
+
const parts = path3.split(/[\\/]+/);
|
|
612
|
+
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
613
|
+
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
614
|
+
const escaped = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
615
|
+
return new RegExp(`(?:file:\\/\\/\\/?)?${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
616
|
+
}
|
|
617
|
+
var fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
618
|
+
function cut(text) {
|
|
619
|
+
if (jsonBytes(text) <= TEXT_LIMIT) return text;
|
|
620
|
+
const lines = text.split("\n");
|
|
621
|
+
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
622
|
+
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
623
|
+
let used = 0;
|
|
624
|
+
let head = 0;
|
|
625
|
+
let tail = lines.length;
|
|
626
|
+
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
627
|
+
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
628
|
+
return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
|
|
629
|
+
}
|
|
630
|
+
var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
631
|
+
var METADATA_TYPE = "application/plune.metadata+json";
|
|
632
|
+
var REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
633
|
+
var FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
634
|
+
var FRAME_MAX = 8192;
|
|
635
|
+
var firstLine = (text) => stripVTControlCharacters(text).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
636
|
+
function chosen({ status, errors }) {
|
|
637
|
+
if (status === "timedOut") {
|
|
638
|
+
const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
|
|
639
|
+
if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
|
|
640
|
+
}
|
|
641
|
+
return errors[0];
|
|
642
|
+
}
|
|
643
|
+
function chainOf(steps) {
|
|
644
|
+
const chain = [];
|
|
645
|
+
for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
|
|
646
|
+
chain.push(failed.title);
|
|
647
|
+
}
|
|
648
|
+
return chain;
|
|
649
|
+
}
|
|
650
|
+
function placeOf(error, testFile) {
|
|
651
|
+
const own = normal(testFile);
|
|
652
|
+
let first;
|
|
653
|
+
for (const line of stripVTControlCharacters(error.text).split("\n")) {
|
|
654
|
+
if (line.length > FRAME_MAX) continue;
|
|
655
|
+
const frame = FRAME.exec(line);
|
|
656
|
+
if (frame === null) continue;
|
|
657
|
+
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
658
|
+
if (normal(place.file) === own) return place;
|
|
659
|
+
if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
|
|
660
|
+
}
|
|
661
|
+
return error.location ?? first;
|
|
662
|
+
}
|
|
663
|
+
function fileOf(raw) {
|
|
664
|
+
if (!raw.startsWith("file://")) return raw;
|
|
665
|
+
let path3 = raw.slice("file://".length);
|
|
666
|
+
try {
|
|
667
|
+
path3 = decodeURIComponent(path3);
|
|
668
|
+
} catch {
|
|
669
|
+
}
|
|
670
|
+
return /^\/[A-Za-z]:\//.test(path3) ? path3.slice(1) : path3;
|
|
671
|
+
}
|
|
672
|
+
var normal = (path3) => posix.normalize(path3.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
673
|
+
function fromRoot(place, root) {
|
|
674
|
+
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
675
|
+
const base = normal(root).replace(/\/+$/, "");
|
|
676
|
+
const file = normal(place.file);
|
|
677
|
+
if (!file.startsWith(`${base}/`)) return void 0;
|
|
678
|
+
const relative = file.slice(base.length + 1);
|
|
679
|
+
if (!REPO_RELATIVE.test(relative)) return void 0;
|
|
680
|
+
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
681
|
+
return column === void 0 ? { file: relative, line: place.line } : { file: relative, line: place.line, column };
|
|
682
|
+
}
|
|
683
|
+
function webLink(href) {
|
|
684
|
+
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
685
|
+
}
|
|
497
686
|
export {
|
|
498
687
|
DEFAULT_FALLBACK_PATH,
|
|
499
688
|
RESULT_KEY_MAX,
|
|
500
689
|
UNSUPPORTED_VARS,
|
|
501
690
|
appendBatch,
|
|
502
691
|
createClient,
|
|
692
|
+
errorContextOf,
|
|
693
|
+
failureOf,
|
|
503
694
|
readEnv,
|
|
695
|
+
repoRootOf,
|
|
504
696
|
resultKey,
|
|
505
|
-
startRun
|
|
697
|
+
startRun,
|
|
698
|
+
webLink
|
|
506
699
|
};
|
package/package.json
CHANGED