@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/index.cjs
CHANGED
|
@@ -1595,6 +1595,8 @@ function credentialsFile() {
|
|
|
1595
1595
|
return (0, import_node_path.join)(base, "plune", "credentials.json");
|
|
1596
1596
|
}
|
|
1597
1597
|
function loadToken() {
|
|
1598
|
+
const fromEnv = process.env["PLUNE_TOKEN"]?.trim();
|
|
1599
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
1598
1600
|
try {
|
|
1599
1601
|
const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
|
|
1600
1602
|
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
@@ -1606,7 +1608,7 @@ function loadToken() {
|
|
|
1606
1608
|
// src/cli/commands/sync.ts
|
|
1607
1609
|
var NotLoggedInError = class extends Error {
|
|
1608
1610
|
constructor() {
|
|
1609
|
-
super('Not logged in. Run "plune login" first
|
|
1611
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1610
1612
|
this.name = "NotLoggedInError";
|
|
1611
1613
|
}
|
|
1612
1614
|
};
|
|
@@ -1862,7 +1864,7 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
1862
1864
|
// src/cli/commands/ingest.ts
|
|
1863
1865
|
var IngestNotLoggedInError = class extends Error {
|
|
1864
1866
|
constructor() {
|
|
1865
|
-
super('Not logged in. Run "plune login" first
|
|
1867
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1866
1868
|
this.name = "IngestNotLoggedInError";
|
|
1867
1869
|
}
|
|
1868
1870
|
};
|
package/dist/index.js
CHANGED
|
@@ -1542,6 +1542,8 @@ function credentialsFile() {
|
|
|
1542
1542
|
return join3(base, "plune", "credentials.json");
|
|
1543
1543
|
}
|
|
1544
1544
|
function loadToken() {
|
|
1545
|
+
const fromEnv = process.env["PLUNE_TOKEN"]?.trim();
|
|
1546
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
1545
1547
|
try {
|
|
1546
1548
|
const parsed = JSON.parse(readFileSync3(credentialsFile(), "utf-8"));
|
|
1547
1549
|
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
@@ -1553,7 +1555,7 @@ function loadToken() {
|
|
|
1553
1555
|
// src/cli/commands/sync.ts
|
|
1554
1556
|
var NotLoggedInError = class extends Error {
|
|
1555
1557
|
constructor() {
|
|
1556
|
-
super('Not logged in. Run "plune login" first
|
|
1558
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1557
1559
|
this.name = "NotLoggedInError";
|
|
1558
1560
|
}
|
|
1559
1561
|
};
|
|
@@ -1809,7 +1811,7 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
1809
1811
|
// src/cli/commands/ingest.ts
|
|
1810
1812
|
var IngestNotLoggedInError = class extends Error {
|
|
1811
1813
|
constructor() {
|
|
1812
|
-
super('Not logged in. Run "plune login" first
|
|
1814
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1813
1815
|
this.name = "IngestNotLoggedInError";
|
|
1814
1816
|
}
|
|
1815
1817
|
};
|
package/dist/reporter-core.cjs
CHANGED
|
@@ -35,9 +35,13 @@ __export(reporter_core_exports, {
|
|
|
35
35
|
UNSUPPORTED_VARS: () => UNSUPPORTED_VARS,
|
|
36
36
|
appendBatch: () => appendBatch,
|
|
37
37
|
createClient: () => createClient,
|
|
38
|
+
errorContextOf: () => errorContextOf,
|
|
39
|
+
failureOf: () => failureOf,
|
|
38
40
|
readEnv: () => readEnv,
|
|
41
|
+
repoRootOf: () => repoRootOf,
|
|
39
42
|
resultKey: () => resultKey,
|
|
40
|
-
startRun: () => startRun
|
|
43
|
+
startRun: () => startRun,
|
|
44
|
+
webLink: () => webLink
|
|
41
45
|
});
|
|
42
46
|
module.exports = __toCommonJS(reporter_core_exports);
|
|
43
47
|
|
|
@@ -60,6 +64,8 @@ function credentialsFile() {
|
|
|
60
64
|
return (0, import_node_path.join)(base, "plune", "credentials.json");
|
|
61
65
|
}
|
|
62
66
|
function loadToken() {
|
|
67
|
+
const fromEnv = process.env["PLUNE_TOKEN"]?.trim();
|
|
68
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
63
69
|
try {
|
|
64
70
|
const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
|
|
65
71
|
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
@@ -69,7 +75,39 @@ function loadToken() {
|
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
// src/reporter-core/client.ts
|
|
78
|
+
var import_node_http = require("http");
|
|
79
|
+
var import_node_https = require("https");
|
|
72
80
|
var RETRY_DELAYS_MS = [300, 900];
|
|
81
|
+
var IDLE_MS = 3e5;
|
|
82
|
+
function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
83
|
+
return new Promise((resolve2, reject) => {
|
|
84
|
+
const target = new URL(url);
|
|
85
|
+
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
86
|
+
const req = send(target, { method: init.method, headers: init.headers }, (res) => {
|
|
87
|
+
const chunks = [];
|
|
88
|
+
res.on("data", (chunk2) => chunks.push(chunk2));
|
|
89
|
+
res.on("error", reject);
|
|
90
|
+
res.on("end", () => {
|
|
91
|
+
const status = res.statusCode ?? 0;
|
|
92
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
93
|
+
resolve2({
|
|
94
|
+
ok: status >= 200 && status <= 299,
|
|
95
|
+
status,
|
|
96
|
+
headers: {
|
|
97
|
+
get: (name) => {
|
|
98
|
+
const value2 = res.headers[name.toLowerCase()];
|
|
99
|
+
return value2 === void 0 ? null : Array.isArray(value2) ? value2.join(", ") : value2;
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
json: async () => JSON.parse(text)
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
req.setTimeout(idleMs, () => req.destroy(new Error(`no answer from ${target.origin} in ${idleMs} ms`)));
|
|
107
|
+
req.on("error", reject);
|
|
108
|
+
req.end(init.body);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
73
111
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
74
112
|
async function detailOf(res) {
|
|
75
113
|
try {
|
|
@@ -93,7 +131,7 @@ function classify(status) {
|
|
|
93
131
|
return "other";
|
|
94
132
|
}
|
|
95
133
|
function createClient(opts) {
|
|
96
|
-
const doFetch = opts.fetchImpl ??
|
|
134
|
+
const doFetch = opts.fetchImpl ?? nodeTransport;
|
|
97
135
|
const wait = opts.wait ?? sleep;
|
|
98
136
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
99
137
|
const scrub = (text) => opts.token === "" ? text : text.split(opts.token).join("***");
|
|
@@ -236,6 +274,25 @@ function boundKey(key) {
|
|
|
236
274
|
function boundTitle(title) {
|
|
237
275
|
return title.length <= TITLE_MAX ? title : `${title.slice(0, TITLE_MAX - 1)}\u2026`;
|
|
238
276
|
}
|
|
277
|
+
var BATCH_BYTES = 8 * 1024 * 1024;
|
|
278
|
+
var ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ results: [] }));
|
|
279
|
+
function packed(results) {
|
|
280
|
+
const out = [];
|
|
281
|
+
let part = [];
|
|
282
|
+
let bytes = ENVELOPE_BYTES;
|
|
283
|
+
for (const result of results) {
|
|
284
|
+
const size = Buffer.byteLength(JSON.stringify(result));
|
|
285
|
+
if (part.length > 0 && bytes + 1 + size > BATCH_BYTES) {
|
|
286
|
+
out.push(part);
|
|
287
|
+
part = [];
|
|
288
|
+
bytes = ENVELOPE_BYTES;
|
|
289
|
+
}
|
|
290
|
+
bytes += (part.length > 0 ? 1 : 0) + size;
|
|
291
|
+
part.push(result);
|
|
292
|
+
}
|
|
293
|
+
if (part.length > 0) out.push(part);
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
239
296
|
function chunk(items, size) {
|
|
240
297
|
const out = [];
|
|
241
298
|
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
@@ -437,27 +494,32 @@ async function startRun(passed, declared) {
|
|
|
437
494
|
progress();
|
|
438
495
|
return;
|
|
439
496
|
}
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
497
|
+
for (const part of packed(submissions)) {
|
|
498
|
+
if (offline) {
|
|
499
|
+
defer(part);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const out = await client.post(`/v1/runs/${runId}/results`, {
|
|
503
|
+
results: part
|
|
504
|
+
});
|
|
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;
|
|
510
|
+
progress();
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (out.kind === "auth") {
|
|
514
|
+
log('plune: the API token was refused \u2014 run "plune login" with a fresh one.');
|
|
515
|
+
offline = true;
|
|
516
|
+
} else if (out.kind === "conflict") {
|
|
517
|
+
log(`plune: this run is closed \u2014 ${out.detail}`);
|
|
518
|
+
} else {
|
|
519
|
+
log(`plune: could not send results (${out.detail || out.kind}).`);
|
|
520
|
+
}
|
|
521
|
+
defer(part);
|
|
459
522
|
}
|
|
460
|
-
defer(submissions);
|
|
461
523
|
}
|
|
462
524
|
function progress() {
|
|
463
525
|
const total = expected?.length ?? 0;
|
|
@@ -473,9 +535,12 @@ async function startRun(passed, declared) {
|
|
|
473
535
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
474
536
|
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
475
537
|
if (stats.created > 0) parts.push(`${stats.created} added as cases (trusted source)`);
|
|
476
|
-
if (stats.deferred > 0) parts.push(`${stats.deferred} written to ${fallbackPath}`);
|
|
538
|
+
if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
|
|
477
539
|
if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
|
|
478
540
|
log(`plune: ${parts.join(" \xB7 ")}`);
|
|
541
|
+
if (stats.deferred > 0 && process.env["GITHUB_ACTIONS"] === "true") {
|
|
542
|
+
log(`::warning::${stats.deferred} result(s) were not delivered to Plune \u2014 see the reporting step's log.`);
|
|
543
|
+
}
|
|
479
544
|
}
|
|
480
545
|
return {
|
|
481
546
|
get runId() {
|
|
@@ -535,6 +600,122 @@ function resultKey(testId, retry) {
|
|
|
535
600
|
const plain = `${testId}#${retry}`;
|
|
536
601
|
return plain.length <= RESULT_KEY_MAX ? plain : `${sha256(testId).slice(0, HASH_CHARS)}#${retry}`;
|
|
537
602
|
}
|
|
603
|
+
|
|
604
|
+
// src/reporter-core/failure-detail.ts
|
|
605
|
+
var import_node_fs2 = require("fs");
|
|
606
|
+
var import_node_os2 = require("os");
|
|
607
|
+
var import_node_path2 = require("path");
|
|
608
|
+
var import_node_util = require("util");
|
|
609
|
+
function failureOf(attempt) {
|
|
610
|
+
const error = chosen(attempt);
|
|
611
|
+
const headline = error === void 0 ? void 0 : firstLine(error.text);
|
|
612
|
+
const steps = chainOf(attempt.steps);
|
|
613
|
+
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 });
|
|
615
|
+
const ciUrl = webLink(attempt.buildHref);
|
|
616
|
+
const failure = {
|
|
617
|
+
...headline !== void 0 ? { headline } : {},
|
|
618
|
+
...steps.length > 0 ? { steps } : {},
|
|
619
|
+
...location !== void 0 ? { location } : {},
|
|
620
|
+
...artifacts.length > 0 ? { artifacts } : {},
|
|
621
|
+
...ciUrl !== void 0 ? { ciUrl } : {}
|
|
622
|
+
};
|
|
623
|
+
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
624
|
+
}
|
|
625
|
+
function repoRootOf(dir, stopAt) {
|
|
626
|
+
for (let here = (0, import_node_path2.resolve)(dir); ; here = (0, import_node_path2.dirname)(here)) {
|
|
627
|
+
if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(here, ".git"))) return here;
|
|
628
|
+
if (here === stopAt || (0, import_node_path2.dirname)(here) === here) return void 0;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
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), "");
|
|
637
|
+
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);
|
|
641
|
+
}
|
|
642
|
+
var TEXT_LIMIT = 524288;
|
|
643
|
+
var ANY_HOME = /(?<=^|[\s'"`(=,[])(?:file:\/\/\/?)?(?:\/home\/|\/Users\/|[A-Za-z]:[\\/]Users[\\/])[^\\/\s'"`:*?<>|]+/g;
|
|
644
|
+
function pathIn(path3) {
|
|
645
|
+
const parts = path3.split(/[\\/]+/);
|
|
646
|
+
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
647
|
+
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");
|
|
650
|
+
}
|
|
651
|
+
function cut(text) {
|
|
652
|
+
if (Buffer.byteLength(text) <= TEXT_LIMIT) return text;
|
|
653
|
+
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));
|
|
656
|
+
let used = 0;
|
|
657
|
+
let head = 0;
|
|
658
|
+
let tail = lines.length;
|
|
659
|
+
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
660
|
+
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
661
|
+
return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
|
|
662
|
+
}
|
|
663
|
+
var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
664
|
+
var METADATA_TYPE = "application/plune.metadata+json";
|
|
665
|
+
var REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
666
|
+
var FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
667
|
+
var firstLine = (text) => (0, import_node_util.stripVTControlCharacters)(text).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
668
|
+
function chosen({ status, errors }) {
|
|
669
|
+
if (status === "timedOut") {
|
|
670
|
+
const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
|
|
671
|
+
if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
|
|
672
|
+
}
|
|
673
|
+
return errors[0];
|
|
674
|
+
}
|
|
675
|
+
function chainOf(steps) {
|
|
676
|
+
const chain = [];
|
|
677
|
+
for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
|
|
678
|
+
chain.push(failed.title);
|
|
679
|
+
}
|
|
680
|
+
return chain;
|
|
681
|
+
}
|
|
682
|
+
function placeOf(error, testFile) {
|
|
683
|
+
const own = normal(testFile);
|
|
684
|
+
let first;
|
|
685
|
+
for (const line of (0, import_node_util.stripVTControlCharacters)(error.text).split("\n")) {
|
|
686
|
+
const frame = FRAME.exec(line);
|
|
687
|
+
if (frame === null) continue;
|
|
688
|
+
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
689
|
+
if (normal(place.file) === own) return place;
|
|
690
|
+
if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
|
|
691
|
+
}
|
|
692
|
+
return error.location ?? first;
|
|
693
|
+
}
|
|
694
|
+
function fileOf(raw) {
|
|
695
|
+
if (!raw.startsWith("file://")) return raw;
|
|
696
|
+
const path3 = decodeURIComponent(raw.slice("file://".length));
|
|
697
|
+
return /^\/[A-Za-z]:\//.test(path3) ? path3.slice(1) : path3;
|
|
698
|
+
}
|
|
699
|
+
var normal = (path3) => import_node_path2.posix.normalize(path3.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
700
|
+
function fromRoot(place, root) {
|
|
701
|
+
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
702
|
+
const base = normal(root).replace(/\/+$/, "");
|
|
703
|
+
const file = normal(place.file);
|
|
704
|
+
if (!file.startsWith(`${base}/`)) return void 0;
|
|
705
|
+
const relative = file.slice(base.length + 1);
|
|
706
|
+
if (!REPO_RELATIVE.test(relative)) return void 0;
|
|
707
|
+
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
708
|
+
return column === void 0 ? { file: relative, line: place.line } : { file: relative, line: place.line, column };
|
|
709
|
+
}
|
|
710
|
+
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
|
+
}
|
|
718
|
+
}
|
|
538
719
|
// Annotate the CommonJS export names for ESM import in node:
|
|
539
720
|
0 && (module.exports = {
|
|
540
721
|
DEFAULT_FALLBACK_PATH,
|
|
@@ -542,7 +723,11 @@ function resultKey(testId, retry) {
|
|
|
542
723
|
UNSUPPORTED_VARS,
|
|
543
724
|
appendBatch,
|
|
544
725
|
createClient,
|
|
726
|
+
errorContextOf,
|
|
727
|
+
failureOf,
|
|
545
728
|
readEnv,
|
|
729
|
+
repoRootOf,
|
|
546
730
|
resultKey,
|
|
547
|
-
startRun
|
|
731
|
+
startRun,
|
|
732
|
+
webLink
|
|
548
733
|
});
|
package/dist/reporter-core.d.cts
CHANGED
|
@@ -103,10 +103,80 @@ interface PendingResult {
|
|
|
103
103
|
specRef?: string;
|
|
104
104
|
execution?: Execution;
|
|
105
105
|
errorContext?: string;
|
|
106
|
+
/** What failed and where, for a failed attempt (#790); a pass carries none. Built by `failureOf`. */
|
|
107
|
+
failure?: FailureDetail;
|
|
106
108
|
params?: Record<string, unknown>;
|
|
107
109
|
attachments?: Attachment[];
|
|
108
110
|
assertions?: AssertionRecord[];
|
|
109
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* What a failed attempt says about itself on the run page (#790, platform ADR-0002): every field
|
|
114
|
+
* optional — a field the runner could not supply is absent, never guessed. Nothing here is cut: the
|
|
115
|
+
* platform bounds the fields after its own cleaning, and a cut before that could halve a credential.
|
|
116
|
+
*/
|
|
117
|
+
interface FailureDetail {
|
|
118
|
+
/** The first non-empty line of the chosen error, without colour codes. */
|
|
119
|
+
headline?: string;
|
|
120
|
+
/** Declared steps (`test.step`), from the outermost to the one that failed. */
|
|
121
|
+
steps?: string[];
|
|
122
|
+
/** Where it failed, from the repository root, posix. */
|
|
123
|
+
location?: {
|
|
124
|
+
file: string;
|
|
125
|
+
line: number;
|
|
126
|
+
column?: number;
|
|
127
|
+
};
|
|
128
|
+
/** The files the runner kept for the attempt, by name — the files themselves stay in the CI run. */
|
|
129
|
+
artifacts?: {
|
|
130
|
+
name: string;
|
|
131
|
+
contentType?: string;
|
|
132
|
+
}[];
|
|
133
|
+
/** The CI run that produced the report, http(s) only. */
|
|
134
|
+
ciUrl?: string;
|
|
135
|
+
}
|
|
136
|
+
/** Where a runner says something happened: an absolute file, a line, maybe a column. */
|
|
137
|
+
interface RunnerLocation {
|
|
138
|
+
file: string;
|
|
139
|
+
line: number;
|
|
140
|
+
column?: number;
|
|
141
|
+
}
|
|
142
|
+
/** One error of an attempt, as the runner formats it for a report: the message, the code frame, the
|
|
143
|
+
* stack's ` at` lines — the form Playwright's JSON report already holds for every error. */
|
|
144
|
+
interface AttemptError {
|
|
145
|
+
text: string;
|
|
146
|
+
/** Where the runner says it was thrown. */
|
|
147
|
+
location?: RunnerLocation;
|
|
148
|
+
}
|
|
149
|
+
/** A step the test declared (`test.step`), with the declared steps inside it — nothing else. */
|
|
150
|
+
interface DeclaredStep {
|
|
151
|
+
title: string;
|
|
152
|
+
failed: boolean;
|
|
153
|
+
steps: DeclaredStep[];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* One failed attempt, as both roads reduce their runner's data to it before anything is derived —
|
|
157
|
+
* `plune run import` from the JSON report, the adapter from the reporter API (#790 ADR-0001). The same
|
|
158
|
+
* shape in, so the same detail and the same text out.
|
|
159
|
+
*/
|
|
160
|
+
interface FailedAttempt {
|
|
161
|
+
/** The runner's word: `timedOut` picks which error the headline comes from. */
|
|
162
|
+
status: string;
|
|
163
|
+
/** Every error of the attempt, in order. */
|
|
164
|
+
errors: AttemptError[];
|
|
165
|
+
/** The declared steps, outermost first — as the JSON report keeps them. */
|
|
166
|
+
steps: DeclaredStep[];
|
|
167
|
+
/** What the runner kept; `path` is what makes one a file. */
|
|
168
|
+
attachments: {
|
|
169
|
+
name: string;
|
|
170
|
+
contentType?: string;
|
|
171
|
+
path?: string;
|
|
172
|
+
}[];
|
|
173
|
+
/** `metadata.ci.buildHref` of the run — the CI run that produced the report. */
|
|
174
|
+
buildHref?: string;
|
|
175
|
+
/** The test's own file, absolute. */
|
|
176
|
+
testFile: string;
|
|
177
|
+
/** The repository's root on this machine, when one was found above the runner's root dir. */
|
|
178
|
+
repoRoot?: string;
|
|
179
|
+
}
|
|
110
180
|
/** What a run submission looks like once a key has resolved. Built by the core, never by a caller. */
|
|
111
181
|
interface ResultSubmission extends Omit<PendingResult, 'keys' | 'testCaseId'> {
|
|
112
182
|
testCaseId: string;
|
|
@@ -157,7 +227,11 @@ interface RunStats extends SubmitCounts {
|
|
|
157
227
|
* queue at capacity means "empty it and import again"; nothing else here means that.
|
|
158
228
|
*/
|
|
159
229
|
unoffered: number;
|
|
160
|
-
/**
|
|
230
|
+
/**
|
|
231
|
+
* Results not delivered, written to the fallback file instead of the platform (#790): a batch
|
|
232
|
+
* refused for any reason — its size, the network, the token, a closed run — or a result whose keys
|
|
233
|
+
* the platform could not be asked about. Apart from `rejected` and `unresolved`, which it did answer.
|
|
234
|
+
*/
|
|
161
235
|
deferred: number;
|
|
162
236
|
}
|
|
163
237
|
/** Everything the core needs to talk to a deployment. */
|
|
@@ -320,17 +394,30 @@ interface DeferredBatch {
|
|
|
320
394
|
declare function appendBatch(file: string, batch: DeferredBatch): void;
|
|
321
395
|
|
|
322
396
|
/**
|
|
323
|
-
* The
|
|
397
|
+
* The failure detail of one attempt (#790, ADR-0001 of the feature): what failed, the declared steps
|
|
398
|
+
* down to it, where, what the runner kept, and the CI run — derived from the attempt both roads
|
|
399
|
+
* reduce their data to, so `plune run import` and the adapter cannot disagree about it.
|
|
324
400
|
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
* of those a `catch` that re-derives what already happened here.
|
|
329
|
-
*
|
|
330
|
-
* It also does not log. Anything printed about a failure is printed by the session, which knows
|
|
331
|
-
* the context; keeping the token's only home in this file is what makes AC-12 structural rather
|
|
332
|
-
* than a rule someone has to remember.
|
|
401
|
+
* Pure: the repository root is found once by the caller (`repoRootOf`) and handed in. Nothing is cut
|
|
402
|
+
* and nothing is searched for credentials — the platform does both after its own cleaning, and a cut
|
|
403
|
+
* here could halve a credential it would then not recognise.
|
|
333
404
|
*/
|
|
405
|
+
declare function failureOf(attempt: FailedAttempt): FailureDetail | undefined;
|
|
406
|
+
/**
|
|
407
|
+
* The nearest folder with a `.git` above the runner's root dir — a folder, or the file a worktree
|
|
408
|
+
* keeps instead. Found once per run by the caller; `stopAt` bounds the walk for a test.
|
|
409
|
+
*/
|
|
410
|
+
declare function repoRootOf(dir: string, stopAt?: string): string | undefined;
|
|
411
|
+
/**
|
|
412
|
+
* `errorContext` for one attempt: every error in order, colour codes gone, the repository as relative
|
|
413
|
+
* paths and a home folder as `~` — any home folder when the root is unknown, since the report may come
|
|
414
|
+
* from another machine — and no longer than the CLI's transport limit, cut by whole lines. Nothing is
|
|
415
|
+
* searched for credentials: the platform does that before its own cut (ADR-0005).
|
|
416
|
+
*/
|
|
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. */
|
|
419
|
+
declare function webLink(href: string | undefined): string | undefined;
|
|
420
|
+
|
|
334
421
|
type ClientFailureKind =
|
|
335
422
|
/** The token was refused. Retrying sends the same token to the same door. */
|
|
336
423
|
'auth'
|
|
@@ -371,4 +458,4 @@ interface PlatformClient {
|
|
|
371
458
|
}
|
|
372
459
|
declare function createClient(opts: ClientOptions): PlatformClient;
|
|
373
460
|
|
|
374
|
-
export { type AssertionRecord, type Attachment, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type SubmitCounts, UNSUPPORTED_VARS, appendBatch, createClient, readEnv, resultKey, startRun };
|
|
461
|
+
export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, appendBatch, createClient, errorContextOf, failureOf, readEnv, repoRootOf, resultKey, startRun, webLink };
|
package/dist/reporter-core.d.ts
CHANGED
|
@@ -103,10 +103,80 @@ interface PendingResult {
|
|
|
103
103
|
specRef?: string;
|
|
104
104
|
execution?: Execution;
|
|
105
105
|
errorContext?: string;
|
|
106
|
+
/** What failed and where, for a failed attempt (#790); a pass carries none. Built by `failureOf`. */
|
|
107
|
+
failure?: FailureDetail;
|
|
106
108
|
params?: Record<string, unknown>;
|
|
107
109
|
attachments?: Attachment[];
|
|
108
110
|
assertions?: AssertionRecord[];
|
|
109
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* What a failed attempt says about itself on the run page (#790, platform ADR-0002): every field
|
|
114
|
+
* optional — a field the runner could not supply is absent, never guessed. Nothing here is cut: the
|
|
115
|
+
* platform bounds the fields after its own cleaning, and a cut before that could halve a credential.
|
|
116
|
+
*/
|
|
117
|
+
interface FailureDetail {
|
|
118
|
+
/** The first non-empty line of the chosen error, without colour codes. */
|
|
119
|
+
headline?: string;
|
|
120
|
+
/** Declared steps (`test.step`), from the outermost to the one that failed. */
|
|
121
|
+
steps?: string[];
|
|
122
|
+
/** Where it failed, from the repository root, posix. */
|
|
123
|
+
location?: {
|
|
124
|
+
file: string;
|
|
125
|
+
line: number;
|
|
126
|
+
column?: number;
|
|
127
|
+
};
|
|
128
|
+
/** The files the runner kept for the attempt, by name — the files themselves stay in the CI run. */
|
|
129
|
+
artifacts?: {
|
|
130
|
+
name: string;
|
|
131
|
+
contentType?: string;
|
|
132
|
+
}[];
|
|
133
|
+
/** The CI run that produced the report, http(s) only. */
|
|
134
|
+
ciUrl?: string;
|
|
135
|
+
}
|
|
136
|
+
/** Where a runner says something happened: an absolute file, a line, maybe a column. */
|
|
137
|
+
interface RunnerLocation {
|
|
138
|
+
file: string;
|
|
139
|
+
line: number;
|
|
140
|
+
column?: number;
|
|
141
|
+
}
|
|
142
|
+
/** One error of an attempt, as the runner formats it for a report: the message, the code frame, the
|
|
143
|
+
* stack's ` at` lines — the form Playwright's JSON report already holds for every error. */
|
|
144
|
+
interface AttemptError {
|
|
145
|
+
text: string;
|
|
146
|
+
/** Where the runner says it was thrown. */
|
|
147
|
+
location?: RunnerLocation;
|
|
148
|
+
}
|
|
149
|
+
/** A step the test declared (`test.step`), with the declared steps inside it — nothing else. */
|
|
150
|
+
interface DeclaredStep {
|
|
151
|
+
title: string;
|
|
152
|
+
failed: boolean;
|
|
153
|
+
steps: DeclaredStep[];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* One failed attempt, as both roads reduce their runner's data to it before anything is derived —
|
|
157
|
+
* `plune run import` from the JSON report, the adapter from the reporter API (#790 ADR-0001). The same
|
|
158
|
+
* shape in, so the same detail and the same text out.
|
|
159
|
+
*/
|
|
160
|
+
interface FailedAttempt {
|
|
161
|
+
/** The runner's word: `timedOut` picks which error the headline comes from. */
|
|
162
|
+
status: string;
|
|
163
|
+
/** Every error of the attempt, in order. */
|
|
164
|
+
errors: AttemptError[];
|
|
165
|
+
/** The declared steps, outermost first — as the JSON report keeps them. */
|
|
166
|
+
steps: DeclaredStep[];
|
|
167
|
+
/** What the runner kept; `path` is what makes one a file. */
|
|
168
|
+
attachments: {
|
|
169
|
+
name: string;
|
|
170
|
+
contentType?: string;
|
|
171
|
+
path?: string;
|
|
172
|
+
}[];
|
|
173
|
+
/** `metadata.ci.buildHref` of the run — the CI run that produced the report. */
|
|
174
|
+
buildHref?: string;
|
|
175
|
+
/** The test's own file, absolute. */
|
|
176
|
+
testFile: string;
|
|
177
|
+
/** The repository's root on this machine, when one was found above the runner's root dir. */
|
|
178
|
+
repoRoot?: string;
|
|
179
|
+
}
|
|
110
180
|
/** What a run submission looks like once a key has resolved. Built by the core, never by a caller. */
|
|
111
181
|
interface ResultSubmission extends Omit<PendingResult, 'keys' | 'testCaseId'> {
|
|
112
182
|
testCaseId: string;
|
|
@@ -157,7 +227,11 @@ interface RunStats extends SubmitCounts {
|
|
|
157
227
|
* queue at capacity means "empty it and import again"; nothing else here means that.
|
|
158
228
|
*/
|
|
159
229
|
unoffered: number;
|
|
160
|
-
/**
|
|
230
|
+
/**
|
|
231
|
+
* Results not delivered, written to the fallback file instead of the platform (#790): a batch
|
|
232
|
+
* refused for any reason — its size, the network, the token, a closed run — or a result whose keys
|
|
233
|
+
* the platform could not be asked about. Apart from `rejected` and `unresolved`, which it did answer.
|
|
234
|
+
*/
|
|
161
235
|
deferred: number;
|
|
162
236
|
}
|
|
163
237
|
/** Everything the core needs to talk to a deployment. */
|
|
@@ -320,17 +394,30 @@ interface DeferredBatch {
|
|
|
320
394
|
declare function appendBatch(file: string, batch: DeferredBatch): void;
|
|
321
395
|
|
|
322
396
|
/**
|
|
323
|
-
* The
|
|
397
|
+
* The failure detail of one attempt (#790, ADR-0001 of the feature): what failed, the declared steps
|
|
398
|
+
* down to it, where, what the runner kept, and the CI run — derived from the attempt both roads
|
|
399
|
+
* reduce their data to, so `plune run import` and the adapter cannot disagree about it.
|
|
324
400
|
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
* of those a `catch` that re-derives what already happened here.
|
|
329
|
-
*
|
|
330
|
-
* It also does not log. Anything printed about a failure is printed by the session, which knows
|
|
331
|
-
* the context; keeping the token's only home in this file is what makes AC-12 structural rather
|
|
332
|
-
* than a rule someone has to remember.
|
|
401
|
+
* Pure: the repository root is found once by the caller (`repoRootOf`) and handed in. Nothing is cut
|
|
402
|
+
* and nothing is searched for credentials — the platform does both after its own cleaning, and a cut
|
|
403
|
+
* here could halve a credential it would then not recognise.
|
|
333
404
|
*/
|
|
405
|
+
declare function failureOf(attempt: FailedAttempt): FailureDetail | undefined;
|
|
406
|
+
/**
|
|
407
|
+
* The nearest folder with a `.git` above the runner's root dir — a folder, or the file a worktree
|
|
408
|
+
* keeps instead. Found once per run by the caller; `stopAt` bounds the walk for a test.
|
|
409
|
+
*/
|
|
410
|
+
declare function repoRootOf(dir: string, stopAt?: string): string | undefined;
|
|
411
|
+
/**
|
|
412
|
+
* `errorContext` for one attempt: every error in order, colour codes gone, the repository as relative
|
|
413
|
+
* paths and a home folder as `~` — any home folder when the root is unknown, since the report may come
|
|
414
|
+
* from another machine — and no longer than the CLI's transport limit, cut by whole lines. Nothing is
|
|
415
|
+
* searched for credentials: the platform does that before its own cut (ADR-0005).
|
|
416
|
+
*/
|
|
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. */
|
|
419
|
+
declare function webLink(href: string | undefined): string | undefined;
|
|
420
|
+
|
|
334
421
|
type ClientFailureKind =
|
|
335
422
|
/** The token was refused. Retrying sends the same token to the same door. */
|
|
336
423
|
'auth'
|
|
@@ -371,4 +458,4 @@ interface PlatformClient {
|
|
|
371
458
|
}
|
|
372
459
|
declare function createClient(opts: ClientOptions): PlatformClient;
|
|
373
460
|
|
|
374
|
-
export { type AssertionRecord, type Attachment, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type SubmitCounts, UNSUPPORTED_VARS, appendBatch, createClient, readEnv, resultKey, startRun };
|
|
461
|
+
export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, appendBatch, createClient, errorContextOf, failureOf, readEnv, repoRootOf, resultKey, startRun, webLink };
|