@plune-ai/cli 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/cli.cjs +365 -61
- package/dist/index.cjs +4 -2
- package/dist/index.js +4 -2
- package/dist/reporter-core.cjs +209 -24
- package/dist/reporter-core.d.cts +98 -11
- package/dist/reporter-core.d.ts +98 -11
- package/dist/reporter-core.js +204 -23
- package/package.json +1 -1
package/dist/reporter-core.js
CHANGED
|
@@ -17,6 +17,8 @@ function credentialsFile() {
|
|
|
17
17
|
return join(base, "plune", "credentials.json");
|
|
18
18
|
}
|
|
19
19
|
function loadToken() {
|
|
20
|
+
const fromEnv = process.env["PLUNE_TOKEN"]?.trim();
|
|
21
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
20
22
|
try {
|
|
21
23
|
const parsed = JSON.parse(readFileSync(credentialsFile(), "utf-8"));
|
|
22
24
|
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
@@ -26,7 +28,39 @@ function loadToken() {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
// src/reporter-core/client.ts
|
|
31
|
+
import { request as httpRequest } from "http";
|
|
32
|
+
import { request as httpsRequest } from "https";
|
|
29
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
|
+
}
|
|
30
64
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
31
65
|
async function detailOf(res) {
|
|
32
66
|
try {
|
|
@@ -50,7 +84,7 @@ function classify(status) {
|
|
|
50
84
|
return "other";
|
|
51
85
|
}
|
|
52
86
|
function createClient(opts) {
|
|
53
|
-
const doFetch = opts.fetchImpl ??
|
|
87
|
+
const doFetch = opts.fetchImpl ?? nodeTransport;
|
|
54
88
|
const wait = opts.wait ?? sleep;
|
|
55
89
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
56
90
|
const scrub = (text) => opts.token === "" ? text : text.split(opts.token).join("***");
|
|
@@ -193,6 +227,25 @@ function boundKey(key) {
|
|
|
193
227
|
function boundTitle(title) {
|
|
194
228
|
return title.length <= TITLE_MAX ? title : `${title.slice(0, TITLE_MAX - 1)}\u2026`;
|
|
195
229
|
}
|
|
230
|
+
var BATCH_BYTES = 8 * 1024 * 1024;
|
|
231
|
+
var ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ results: [] }));
|
|
232
|
+
function packed(results) {
|
|
233
|
+
const out = [];
|
|
234
|
+
let part = [];
|
|
235
|
+
let bytes = ENVELOPE_BYTES;
|
|
236
|
+
for (const result of results) {
|
|
237
|
+
const size = Buffer.byteLength(JSON.stringify(result));
|
|
238
|
+
if (part.length > 0 && bytes + 1 + size > BATCH_BYTES) {
|
|
239
|
+
out.push(part);
|
|
240
|
+
part = [];
|
|
241
|
+
bytes = ENVELOPE_BYTES;
|
|
242
|
+
}
|
|
243
|
+
bytes += (part.length > 0 ? 1 : 0) + size;
|
|
244
|
+
part.push(result);
|
|
245
|
+
}
|
|
246
|
+
if (part.length > 0) out.push(part);
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
196
249
|
function chunk(items, size) {
|
|
197
250
|
const out = [];
|
|
198
251
|
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
@@ -394,27 +447,32 @@ async function startRun(passed, declared) {
|
|
|
394
447
|
progress();
|
|
395
448
|
return;
|
|
396
449
|
}
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
450
|
+
for (const part of packed(submissions)) {
|
|
451
|
+
if (offline) {
|
|
452
|
+
defer(part);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const out = await client.post(`/v1/runs/${runId}/results`, {
|
|
456
|
+
results: part
|
|
457
|
+
});
|
|
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;
|
|
463
|
+
progress();
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (out.kind === "auth") {
|
|
467
|
+
log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
|
|
468
|
+
offline = true;
|
|
469
|
+
} else if (out.kind === "conflict") {
|
|
470
|
+
log(`plune: this run is closed \u2014 ${out.detail}`);
|
|
471
|
+
} else {
|
|
472
|
+
log(`plune: could not send results (${out.detail || out.kind}).`);
|
|
473
|
+
}
|
|
474
|
+
defer(part);
|
|
416
475
|
}
|
|
417
|
-
defer(submissions);
|
|
418
476
|
}
|
|
419
477
|
function progress() {
|
|
420
478
|
const total = expected?.length ?? 0;
|
|
@@ -430,9 +488,12 @@ async function startRun(passed, declared) {
|
|
|
430
488
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
431
489
|
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
432
490
|
if (stats.created > 0) parts.push(`${stats.created} added as cases (trusted source)`);
|
|
433
|
-
if (stats.deferred > 0) parts.push(`${stats.deferred} written to ${fallbackPath}`);
|
|
491
|
+
if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
|
|
434
492
|
if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
|
|
435
493
|
log(`plune: ${parts.join(" \xB7 ")}`);
|
|
494
|
+
if (stats.deferred > 0 && process.env["GITHUB_ACTIONS"] === "true") {
|
|
495
|
+
log(`::warning::${stats.deferred} result(s) were not delivered to Plune \u2014 see the reporting step's log.`);
|
|
496
|
+
}
|
|
436
497
|
}
|
|
437
498
|
return {
|
|
438
499
|
get runId() {
|
|
@@ -492,13 +553,133 @@ function resultKey(testId, retry) {
|
|
|
492
553
|
const plain = `${testId}#${retry}`;
|
|
493
554
|
return plain.length <= RESULT_KEY_MAX ? plain : `${sha256(testId).slice(0, HASH_CHARS)}#${retry}`;
|
|
494
555
|
}
|
|
556
|
+
|
|
557
|
+
// src/reporter-core/failure-detail.ts
|
|
558
|
+
import { existsSync as existsSync2 } from "fs";
|
|
559
|
+
import { homedir as homedir2 } from "os";
|
|
560
|
+
import { dirname as dirname3, join as join3, posix, resolve } from "path";
|
|
561
|
+
import { stripVTControlCharacters } from "util";
|
|
562
|
+
function failureOf(attempt) {
|
|
563
|
+
const error = chosen(attempt);
|
|
564
|
+
const headline = error === void 0 ? void 0 : firstLine(error.text);
|
|
565
|
+
const steps = chainOf(attempt.steps);
|
|
566
|
+
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 });
|
|
568
|
+
const ciUrl = webLink(attempt.buildHref);
|
|
569
|
+
const failure = {
|
|
570
|
+
...headline !== void 0 ? { headline } : {},
|
|
571
|
+
...steps.length > 0 ? { steps } : {},
|
|
572
|
+
...location !== void 0 ? { location } : {},
|
|
573
|
+
...artifacts.length > 0 ? { artifacts } : {},
|
|
574
|
+
...ciUrl !== void 0 ? { ciUrl } : {}
|
|
575
|
+
};
|
|
576
|
+
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
577
|
+
}
|
|
578
|
+
function repoRootOf(dir, stopAt) {
|
|
579
|
+
for (let here = resolve(dir); ; here = dirname3(here)) {
|
|
580
|
+
if (existsSync2(join3(here, ".git"))) return here;
|
|
581
|
+
if (here === stopAt || dirname3(here) === here) return void 0;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
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), "");
|
|
590
|
+
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);
|
|
594
|
+
}
|
|
595
|
+
var TEXT_LIMIT = 524288;
|
|
596
|
+
var ANY_HOME = /(?<=^|[\s'"`(=,[])(?:file:\/\/\/?)?(?:\/home\/|\/Users\/|[A-Za-z]:[\\/]Users[\\/])[^\\/\s'"`:*?<>|]+/g;
|
|
597
|
+
function pathIn(path3) {
|
|
598
|
+
const parts = path3.split(/[\\/]+/);
|
|
599
|
+
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
600
|
+
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");
|
|
603
|
+
}
|
|
604
|
+
function cut(text) {
|
|
605
|
+
if (Buffer.byteLength(text) <= TEXT_LIMIT) return text;
|
|
606
|
+
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));
|
|
609
|
+
let used = 0;
|
|
610
|
+
let head = 0;
|
|
611
|
+
let tail = lines.length;
|
|
612
|
+
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
613
|
+
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
614
|
+
return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
|
|
615
|
+
}
|
|
616
|
+
var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
617
|
+
var METADATA_TYPE = "application/plune.metadata+json";
|
|
618
|
+
var REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
619
|
+
var FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
620
|
+
var firstLine = (text) => stripVTControlCharacters(text).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
621
|
+
function chosen({ status, errors }) {
|
|
622
|
+
if (status === "timedOut") {
|
|
623
|
+
const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
|
|
624
|
+
if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
|
|
625
|
+
}
|
|
626
|
+
return errors[0];
|
|
627
|
+
}
|
|
628
|
+
function chainOf(steps) {
|
|
629
|
+
const chain = [];
|
|
630
|
+
for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
|
|
631
|
+
chain.push(failed.title);
|
|
632
|
+
}
|
|
633
|
+
return chain;
|
|
634
|
+
}
|
|
635
|
+
function placeOf(error, testFile) {
|
|
636
|
+
const own = normal(testFile);
|
|
637
|
+
let first;
|
|
638
|
+
for (const line of stripVTControlCharacters(error.text).split("\n")) {
|
|
639
|
+
const frame = FRAME.exec(line);
|
|
640
|
+
if (frame === null) continue;
|
|
641
|
+
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
642
|
+
if (normal(place.file) === own) return place;
|
|
643
|
+
if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
|
|
644
|
+
}
|
|
645
|
+
return error.location ?? first;
|
|
646
|
+
}
|
|
647
|
+
function fileOf(raw) {
|
|
648
|
+
if (!raw.startsWith("file://")) return raw;
|
|
649
|
+
const path3 = decodeURIComponent(raw.slice("file://".length));
|
|
650
|
+
return /^\/[A-Za-z]:\//.test(path3) ? path3.slice(1) : path3;
|
|
651
|
+
}
|
|
652
|
+
var normal = (path3) => posix.normalize(path3.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
653
|
+
function fromRoot(place, root) {
|
|
654
|
+
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
655
|
+
const base = normal(root).replace(/\/+$/, "");
|
|
656
|
+
const file = normal(place.file);
|
|
657
|
+
if (!file.startsWith(`${base}/`)) return void 0;
|
|
658
|
+
const relative = file.slice(base.length + 1);
|
|
659
|
+
if (!REPO_RELATIVE.test(relative)) return void 0;
|
|
660
|
+
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
661
|
+
return column === void 0 ? { file: relative, line: place.line } : { file: relative, line: place.line, column };
|
|
662
|
+
}
|
|
663
|
+
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
|
+
}
|
|
671
|
+
}
|
|
495
672
|
export {
|
|
496
673
|
DEFAULT_FALLBACK_PATH,
|
|
497
674
|
RESULT_KEY_MAX,
|
|
498
675
|
UNSUPPORTED_VARS,
|
|
499
676
|
appendBatch,
|
|
500
677
|
createClient,
|
|
678
|
+
errorContextOf,
|
|
679
|
+
failureOf,
|
|
501
680
|
readEnv,
|
|
681
|
+
repoRootOf,
|
|
502
682
|
resultKey,
|
|
503
|
-
startRun
|
|
683
|
+
startRun,
|
|
684
|
+
webLink
|
|
504
685
|
};
|
package/package.json
CHANGED