@plune-ai/cli 0.17.0 → 0.18.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 +119 -4
- package/dist/cli.cjs +933 -268
- package/dist/index.cjs +135 -65
- package/dist/index.d.cts +198 -1
- package/dist/index.d.ts +198 -1
- package/dist/index.js +131 -64
- package/dist/reporter-core.cjs +244 -40
- package/dist/reporter-core.d.cts +35 -0
- package/dist/reporter-core.d.ts +35 -0
- package/dist/reporter-core.js +240 -36
- package/package.json +2 -1
package/dist/cli.cjs
CHANGED
|
@@ -512,7 +512,7 @@ var init_retry = __esm({
|
|
|
512
512
|
"use strict";
|
|
513
513
|
init_cjs_shims();
|
|
514
514
|
init_errors2();
|
|
515
|
-
defaultSleep = (ms) => new Promise((
|
|
515
|
+
defaultSleep = (ms) => new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
516
516
|
}
|
|
517
517
|
});
|
|
518
518
|
|
|
@@ -874,7 +874,7 @@ var init_embedder = __esm({
|
|
|
874
874
|
throw new Error(
|
|
875
875
|
"The `semantic-similarity` and RAG assertions need local embeddings, which ship separately: install @huggingface/transformers to enable them. Every other assertion works without it."
|
|
876
876
|
);
|
|
877
|
-
}).then(({ pipeline }) =>
|
|
877
|
+
}).then(({ pipeline: pipeline2 }) => pipeline2("feature-extraction", MODEL_ID));
|
|
878
878
|
return this.extractor;
|
|
879
879
|
}
|
|
880
880
|
};
|
|
@@ -1621,6 +1621,76 @@ var init_concurrency = __esm({
|
|
|
1621
1621
|
}
|
|
1622
1622
|
});
|
|
1623
1623
|
|
|
1624
|
+
// src/types/test-case-schema.ts
|
|
1625
|
+
var import_zod2, TEST_CASE_SCHEMA_VERSION, externalKeySchema, manualStepSchema, assertionResultRecordSchema, manualCaseSchema, automatedCaseSchema, evalCaseSchema, testCaseBaseSchema, testCaseSchema;
|
|
1626
|
+
var init_test_case_schema = __esm({
|
|
1627
|
+
"src/types/test-case-schema.ts"() {
|
|
1628
|
+
"use strict";
|
|
1629
|
+
init_cjs_shims();
|
|
1630
|
+
import_zod2 = require("zod");
|
|
1631
|
+
TEST_CASE_SCHEMA_VERSION = 2;
|
|
1632
|
+
externalKeySchema = import_zod2.z.object({
|
|
1633
|
+
kind: import_zod2.z.enum(["playwright-id", "path-title", "allure-history", "cairn-stable", "eval-id", "qase", "testrail"]),
|
|
1634
|
+
value: import_zod2.z.string().min(1)
|
|
1635
|
+
});
|
|
1636
|
+
manualStepSchema = import_zod2.z.object({
|
|
1637
|
+
action: import_zod2.z.string().min(1),
|
|
1638
|
+
expected: import_zod2.z.string().min(1).optional()
|
|
1639
|
+
});
|
|
1640
|
+
assertionResultRecordSchema = import_zod2.z.object({
|
|
1641
|
+
type: import_zod2.z.string().min(1),
|
|
1642
|
+
passed: import_zod2.z.boolean(),
|
|
1643
|
+
score: import_zod2.z.number().optional(),
|
|
1644
|
+
reason: import_zod2.z.string().optional()
|
|
1645
|
+
});
|
|
1646
|
+
manualCaseSchema = import_zod2.z.object({
|
|
1647
|
+
type: import_zod2.z.literal("manual"),
|
|
1648
|
+
steps: import_zod2.z.array(manualStepSchema).min(1),
|
|
1649
|
+
expected: import_zod2.z.string().min(1).optional()
|
|
1650
|
+
});
|
|
1651
|
+
automatedCaseSchema = import_zod2.z.object({
|
|
1652
|
+
type: import_zod2.z.literal("automated"),
|
|
1653
|
+
specRef: import_zod2.z.string().min(1)
|
|
1654
|
+
});
|
|
1655
|
+
evalCaseSchema = import_zod2.z.object({
|
|
1656
|
+
type: import_zod2.z.literal("eval"),
|
|
1657
|
+
prompt: import_zod2.z.string().min(1),
|
|
1658
|
+
goldenSetId: import_zod2.z.string().min(1).optional(),
|
|
1659
|
+
assertions: import_zod2.z.array(assertionResultRecordSchema).min(1)
|
|
1660
|
+
});
|
|
1661
|
+
testCaseBaseSchema = import_zod2.z.object({
|
|
1662
|
+
schemaVersion: import_zod2.z.literal(TEST_CASE_SCHEMA_VERSION),
|
|
1663
|
+
id: import_zod2.z.string().min(1),
|
|
1664
|
+
externalKeys: import_zod2.z.array(externalKeySchema).optional(),
|
|
1665
|
+
/** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
|
|
1666
|
+
is_flaky: import_zod2.z.boolean().optional(),
|
|
1667
|
+
/** ADR 0045 / plune#1246 — optional, purely additive; schemaVersion stays 2. */
|
|
1668
|
+
is_muted: import_zod2.z.boolean().optional()
|
|
1669
|
+
});
|
|
1670
|
+
testCaseSchema = import_zod2.z.discriminatedUnion("type", [
|
|
1671
|
+
testCaseBaseSchema.merge(manualCaseSchema),
|
|
1672
|
+
testCaseBaseSchema.merge(automatedCaseSchema),
|
|
1673
|
+
testCaseBaseSchema.merge(evalCaseSchema)
|
|
1674
|
+
]);
|
|
1675
|
+
}
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
// src/types/results.ts
|
|
1679
|
+
function rowStatus(row) {
|
|
1680
|
+
if (row.inconclusive !== void 0) return row.inconclusive;
|
|
1681
|
+
if (row.error !== void 0) return "errored";
|
|
1682
|
+
return row.assertions.every((a) => a.passed) ? "passed" : "failed";
|
|
1683
|
+
}
|
|
1684
|
+
var ROW_INCONCLUSIVE;
|
|
1685
|
+
var init_results = __esm({
|
|
1686
|
+
"src/types/results.ts"() {
|
|
1687
|
+
"use strict";
|
|
1688
|
+
init_cjs_shims();
|
|
1689
|
+
init_test_case_schema();
|
|
1690
|
+
ROW_INCONCLUSIVE = ["blocked", "skipped"];
|
|
1691
|
+
}
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1624
1694
|
// src/orchestrator/run.ts
|
|
1625
1695
|
function resolvePrompt(template, vars) {
|
|
1626
1696
|
return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (_match, key) => {
|
|
@@ -1754,10 +1824,25 @@ function resolveTemplate(ev, baseDir) {
|
|
|
1754
1824
|
}
|
|
1755
1825
|
throw new RunConfigError(`Eval "${ev.id}" has neither prompt nor prompt_file`);
|
|
1756
1826
|
}
|
|
1757
|
-
function
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1827
|
+
function summarize(evals, durationMs) {
|
|
1828
|
+
const summary = {
|
|
1829
|
+
total: 0,
|
|
1830
|
+
passed: 0,
|
|
1831
|
+
failed: 0,
|
|
1832
|
+
errored: 0,
|
|
1833
|
+
blocked: 0,
|
|
1834
|
+
skipped: 0,
|
|
1835
|
+
cost_usd: 0,
|
|
1836
|
+
duration_ms: durationMs
|
|
1837
|
+
};
|
|
1838
|
+
for (const ev of evals) {
|
|
1839
|
+
for (const row of ev.rows) {
|
|
1840
|
+
summary.total += 1;
|
|
1841
|
+
summary[rowStatus(row)] += 1;
|
|
1842
|
+
summary.cost_usd += row.usage?.cost_usd ?? 0;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
return summary;
|
|
1761
1846
|
}
|
|
1762
1847
|
async function runOrchestration(config, options, deps) {
|
|
1763
1848
|
const started = deps.now();
|
|
@@ -1792,26 +1877,12 @@ async function runOrchestration(config, options, deps) {
|
|
|
1792
1877
|
noCache
|
|
1793
1878
|
})
|
|
1794
1879
|
);
|
|
1795
|
-
const passed = rowResults.every((r) =>
|
|
1880
|
+
const passed = rowResults.every((r) => rowStatus(r) === "passed");
|
|
1796
1881
|
evalResults.push({ id: ev.id, tags: ev.tags ?? [], rows: rowResults, passed });
|
|
1797
1882
|
if (options.bail === true && !passed) break;
|
|
1798
1883
|
}
|
|
1799
1884
|
const finished = deps.now();
|
|
1800
|
-
const summary =
|
|
1801
|
-
total: 0,
|
|
1802
|
-
passed: 0,
|
|
1803
|
-
failed: 0,
|
|
1804
|
-
errored: 0,
|
|
1805
|
-
cost_usd: 0,
|
|
1806
|
-
duration_ms: finished - started
|
|
1807
|
-
};
|
|
1808
|
-
for (const ev of evalResults) {
|
|
1809
|
-
for (const row of ev.rows) {
|
|
1810
|
-
summary.total += 1;
|
|
1811
|
-
summary[classify(row)] += 1;
|
|
1812
|
-
summary.cost_usd += row.usage?.cost_usd ?? 0;
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1885
|
+
const summary = summarize(evalResults, finished - started);
|
|
1815
1886
|
return {
|
|
1816
1887
|
schemaVersion: 1,
|
|
1817
1888
|
plune_version: PLUNE_VERSION,
|
|
@@ -1834,6 +1905,7 @@ var init_run = __esm({
|
|
|
1834
1905
|
init_judge();
|
|
1835
1906
|
init_concurrency();
|
|
1836
1907
|
init_errors2();
|
|
1908
|
+
init_results();
|
|
1837
1909
|
DEFAULT_MAX_TOKENS2 = 1024;
|
|
1838
1910
|
PLUNE_VERSION = "0.1.0";
|
|
1839
1911
|
RunConfigError = class extends Error {
|
|
@@ -2027,8 +2099,10 @@ function renderConsole(result, opts = {}) {
|
|
|
2027
2099
|
const passedTxt = s.green(`${summary.passed} passed`);
|
|
2028
2100
|
const failedTxt = summary.failed > 0 ? s.red(`${summary.failed} failed`) : `${summary.failed} failed`;
|
|
2029
2101
|
const erroredTxt = summary.errored > 0 ? s.yellow(`${summary.errored} errored`) : `${summary.errored} errored`;
|
|
2102
|
+
const blockedTxt = (summary.blocked ?? 0) > 0 ? ` \xB7 ${s.yellow(`${summary.blocked} blocked`)}` : "";
|
|
2103
|
+
const skippedTxt = (summary.skipped ?? 0) > 0 ? ` \xB7 ${s.dim(`${summary.skipped} skipped`)}` : "";
|
|
2030
2104
|
lines.push(
|
|
2031
|
-
`${passedTxt} \xB7 ${failedTxt} \xB7 ${erroredTxt} \xB7 ${summary.total} total \xB7 $${summary.cost_usd.toFixed(4)} \xB7 ${summary.duration_ms}ms`
|
|
2105
|
+
`${passedTxt} \xB7 ${failedTxt} \xB7 ${erroredTxt}${blockedTxt}${skippedTxt} \xB7 ${summary.total} total \xB7 $${summary.cost_usd.toFixed(4)} \xB7 ${summary.duration_ms}ms`
|
|
2032
2106
|
);
|
|
2033
2107
|
lines.push("");
|
|
2034
2108
|
for (const ev of result.evals) {
|
|
@@ -2037,17 +2111,21 @@ function renderConsole(result, opts = {}) {
|
|
|
2037
2111
|
lines.push(`${mark} ${s.bold(ev.id)}${tags}`);
|
|
2038
2112
|
let passedCount = 0;
|
|
2039
2113
|
for (const row of ev.rows) {
|
|
2114
|
+
const status = rowStatus(row);
|
|
2115
|
+
if (status === "passed") {
|
|
2116
|
+
passedCount += 1;
|
|
2117
|
+
continue;
|
|
2118
|
+
}
|
|
2119
|
+
lines.push(` ${s.dim("vars:")} ${JSON.stringify(row.vars)}`);
|
|
2120
|
+
if (status === "blocked" || status === "skipped") {
|
|
2121
|
+
lines.push(` ${s.yellow(status)} (not judged)`);
|
|
2122
|
+
continue;
|
|
2123
|
+
}
|
|
2040
2124
|
if (row.error !== void 0) {
|
|
2041
|
-
lines.push(` ${s.dim("vars:")} ${JSON.stringify(row.vars)}`);
|
|
2042
2125
|
lines.push(` ${s.red("error:")} ${row.error.message}`);
|
|
2043
2126
|
continue;
|
|
2044
2127
|
}
|
|
2045
2128
|
const failed = row.assertions.filter((a) => !a.passed);
|
|
2046
|
-
if (failed.length === 0) {
|
|
2047
|
-
passedCount += 1;
|
|
2048
|
-
continue;
|
|
2049
|
-
}
|
|
2050
|
-
lines.push(` ${s.dim("vars:")} ${JSON.stringify(row.vars)}`);
|
|
2051
2129
|
if (row.output !== null) {
|
|
2052
2130
|
lines.push(` ${s.dim("output:")} ${truncate(row.output, maxOut)}`);
|
|
2053
2131
|
}
|
|
@@ -2068,6 +2146,7 @@ var init_console = __esm({
|
|
|
2068
2146
|
"src/reporters/console.ts"() {
|
|
2069
2147
|
"use strict";
|
|
2070
2148
|
init_cjs_shims();
|
|
2149
|
+
init_results();
|
|
2071
2150
|
init_style();
|
|
2072
2151
|
init_helpers();
|
|
2073
2152
|
DEFAULT_MAX_OUTPUT = 500;
|
|
@@ -2086,11 +2165,6 @@ var init_json = __esm({
|
|
|
2086
2165
|
});
|
|
2087
2166
|
|
|
2088
2167
|
// src/reporters/markdown.ts
|
|
2089
|
-
function classify2(row) {
|
|
2090
|
-
if (row.error !== void 0) return "errored";
|
|
2091
|
-
if (row.assertions.some((a) => !a.passed)) return "failed";
|
|
2092
|
-
return "passed";
|
|
2093
|
-
}
|
|
2094
2168
|
function fence(content) {
|
|
2095
2169
|
const runs = content.match(new RegExp(TICK + "+", "g"));
|
|
2096
2170
|
const longest = runs ? runs.reduce((m, r) => Math.max(m, r.length), 0) : 0;
|
|
@@ -2111,18 +2185,22 @@ function renderMarkdown(result, opts = {}) {
|
|
|
2111
2185
|
out.push(`| Passed | ${summary.passed} |`);
|
|
2112
2186
|
out.push(`| Failed | ${summary.failed} |`);
|
|
2113
2187
|
out.push(`| Errored | ${summary.errored} |`);
|
|
2188
|
+
if ((summary.blocked ?? 0) > 0) out.push(`| Blocked | ${summary.blocked} |`);
|
|
2189
|
+
if ((summary.skipped ?? 0) > 0) out.push(`| Skipped | ${summary.skipped} |`);
|
|
2114
2190
|
out.push(`| Cost (USD) | ${summary.cost_usd.toFixed(4)} |`);
|
|
2115
2191
|
out.push(`| Duration (ms) | ${summary.duration_ms} |`);
|
|
2116
2192
|
out.push("");
|
|
2117
2193
|
const failures = [];
|
|
2194
|
+
let allPassed = true;
|
|
2118
2195
|
for (const ev of result.evals) {
|
|
2119
2196
|
for (const row of ev.rows) {
|
|
2120
|
-
const state =
|
|
2121
|
-
if (state !== "passed")
|
|
2197
|
+
const state = rowStatus(row);
|
|
2198
|
+
if (state !== "passed") allPassed = false;
|
|
2199
|
+
if (state === "failed" || state === "errored") failures.push({ id: ev.id, row, state });
|
|
2122
2200
|
}
|
|
2123
2201
|
}
|
|
2124
2202
|
if (failures.length === 0) {
|
|
2125
|
-
out.push("All evals passed.");
|
|
2203
|
+
if (allPassed) out.push("All evals passed.");
|
|
2126
2204
|
return out.join("\n");
|
|
2127
2205
|
}
|
|
2128
2206
|
out.push("## Failures");
|
|
@@ -2153,6 +2231,7 @@ var init_markdown = __esm({
|
|
|
2153
2231
|
"src/reporters/markdown.ts"() {
|
|
2154
2232
|
"use strict";
|
|
2155
2233
|
init_cjs_shims();
|
|
2234
|
+
init_results();
|
|
2156
2235
|
init_helpers();
|
|
2157
2236
|
DEFAULT_MAX_OUTPUT2 = 500;
|
|
2158
2237
|
TICK = String.fromCharCode(96);
|
|
@@ -2226,7 +2305,7 @@ var init_credentials = __esm({
|
|
|
2226
2305
|
|
|
2227
2306
|
// src/reporter-core/client.ts
|
|
2228
2307
|
function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
2229
|
-
return new Promise((
|
|
2308
|
+
return new Promise((resolve9, reject) => {
|
|
2230
2309
|
const target = new URL(url);
|
|
2231
2310
|
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
2232
2311
|
const req = send(target, { method: init.method, headers: init.headers }, (res) => {
|
|
@@ -2236,7 +2315,7 @@ function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
|
2236
2315
|
res.on("end", () => {
|
|
2237
2316
|
const status = res.statusCode ?? 0;
|
|
2238
2317
|
const text2 = Buffer.concat(chunks).toString("utf8");
|
|
2239
|
-
|
|
2318
|
+
resolve9({
|
|
2240
2319
|
ok: status >= 200 && status <= 299,
|
|
2241
2320
|
status,
|
|
2242
2321
|
headers: {
|
|
@@ -2254,12 +2333,42 @@ function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
|
2254
2333
|
req.end(init.body);
|
|
2255
2334
|
});
|
|
2256
2335
|
}
|
|
2336
|
+
function nodePut(url, headers, source, idleMs) {
|
|
2337
|
+
return new Promise((resolve9, reject) => {
|
|
2338
|
+
const target = new URL(url);
|
|
2339
|
+
const send = target.protocol === "https:" ? import_node_https.request : import_node_http.request;
|
|
2340
|
+
const length = "body" in source ? source.body.length : source.size;
|
|
2341
|
+
let answered;
|
|
2342
|
+
const settle = (err) => answered !== void 0 ? resolve9(answered) : reject(err);
|
|
2343
|
+
const req = send(
|
|
2344
|
+
target,
|
|
2345
|
+
{ method: "PUT", headers: { ...withoutLength(headers), "content-length": String(length) } },
|
|
2346
|
+
(res) => {
|
|
2347
|
+
const retry = res.headers["retry-after"];
|
|
2348
|
+
answered = { status: res.statusCode ?? 0, retryAfter: Array.isArray(retry) ? retry[0] ?? null : retry ?? null };
|
|
2349
|
+
res.on("error", settle);
|
|
2350
|
+
res.on("close", () => {
|
|
2351
|
+
resolve9(answered);
|
|
2352
|
+
req.destroy();
|
|
2353
|
+
});
|
|
2354
|
+
res.resume();
|
|
2355
|
+
}
|
|
2356
|
+
);
|
|
2357
|
+
req.setTimeout(idleMs, () => req.destroy(Object.assign(new Error("idle"), { code: "ETIMEDOUT" })));
|
|
2358
|
+
req.on("error", settle);
|
|
2359
|
+
if ("body" in source) req.end(source.body);
|
|
2360
|
+
else (0, import_node_stream.pipeline)((0, import_node_fs2.createReadStream)(source.path), req, (err) => err ? settle(err) : void 0);
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2257
2363
|
async function detailOf(res) {
|
|
2258
2364
|
try {
|
|
2259
2365
|
const body = await res.json();
|
|
2260
|
-
return
|
|
2366
|
+
return {
|
|
2367
|
+
detail: typeof body.error === "string" ? body.error : "",
|
|
2368
|
+
...typeof body.code === "string" && /^[a-z0-9_]{1,60}$/.test(body.code) ? { code: body.code } : {}
|
|
2369
|
+
};
|
|
2261
2370
|
} catch {
|
|
2262
|
-
return "";
|
|
2371
|
+
return { detail: "" };
|
|
2263
2372
|
}
|
|
2264
2373
|
}
|
|
2265
2374
|
function retryAfterMs2(res) {
|
|
@@ -2268,7 +2377,7 @@ function retryAfterMs2(res) {
|
|
|
2268
2377
|
const seconds = Number(raw.trim());
|
|
2269
2378
|
return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : null;
|
|
2270
2379
|
}
|
|
2271
|
-
function
|
|
2380
|
+
function classify(status) {
|
|
2272
2381
|
if (status === 401 || status === 403) return "auth";
|
|
2273
2382
|
if (status === 409) return "conflict";
|
|
2274
2383
|
if (status === 400) return "invalid";
|
|
@@ -2280,7 +2389,7 @@ function createClient(opts) {
|
|
|
2280
2389
|
const wait = opts.wait ?? sleep;
|
|
2281
2390
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
2282
2391
|
const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
|
|
2283
|
-
async function send(method,
|
|
2392
|
+
async function send(method, path17, payload) {
|
|
2284
2393
|
let last = {
|
|
2285
2394
|
kind: "unavailable",
|
|
2286
2395
|
status: null,
|
|
@@ -2289,7 +2398,7 @@ function createClient(opts) {
|
|
|
2289
2398
|
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt += 1) {
|
|
2290
2399
|
let res;
|
|
2291
2400
|
try {
|
|
2292
|
-
res = await doFetch(`${base}${
|
|
2401
|
+
res = await doFetch(`${base}${path17}`, {
|
|
2293
2402
|
method,
|
|
2294
2403
|
headers: { "content-type": payload?.type ?? "application/json", authorization: `Bearer ${opts.token}` },
|
|
2295
2404
|
// A DELETE carries none. Sending `"undefined"` as a body is what a naive
|
|
@@ -2310,10 +2419,16 @@ function createClient(opts) {
|
|
|
2310
2419
|
return { ok: false, kind: "other", status: res.status, detail: "the answer was not JSON" };
|
|
2311
2420
|
}
|
|
2312
2421
|
}
|
|
2313
|
-
const
|
|
2314
|
-
const kind =
|
|
2315
|
-
last = { kind, status: res.status, detail };
|
|
2422
|
+
const said = await detailOf(res);
|
|
2423
|
+
const kind = classify(res.status);
|
|
2424
|
+
last = { kind, status: res.status, detail: scrub(said.detail), ...said.code !== void 0 ? { code: said.code } : {} };
|
|
2316
2425
|
if (kind !== "unavailable") break;
|
|
2426
|
+
if (res.status === 429 && retryAfterMs2(res) === null) {
|
|
2427
|
+
last.kind = "invalid";
|
|
2428
|
+
last.detail = "quota exceeded (no Retry-After)";
|
|
2429
|
+
if (said.detail) last.detail += ` \u2014 ${scrub(said.detail)}`;
|
|
2430
|
+
break;
|
|
2431
|
+
}
|
|
2317
2432
|
if (attempt < RETRY_DELAYS_MS.length) {
|
|
2318
2433
|
await wait(retryAfterMs2(res) ?? RETRY_DELAYS_MS[attempt]);
|
|
2319
2434
|
}
|
|
@@ -2321,20 +2436,57 @@ function createClient(opts) {
|
|
|
2321
2436
|
return { ok: false, ...last };
|
|
2322
2437
|
}
|
|
2323
2438
|
return {
|
|
2324
|
-
post: (
|
|
2325
|
-
upload: (
|
|
2326
|
-
del: (
|
|
2439
|
+
post: (path17, body) => send("POST", path17, { body: JSON.stringify(body), type: "application/json" }),
|
|
2440
|
+
upload: (path17, bytes, contentType) => send("POST", path17, { body: bytes, type: contentType }),
|
|
2441
|
+
del: (path17) => send("DELETE", path17),
|
|
2442
|
+
async put(url, headers, source) {
|
|
2443
|
+
let last = { ok: false, status: null, code: "network" };
|
|
2444
|
+
for (let attempt = 0; attempt <= PUT_DELAYS_MS.length; attempt += 1) {
|
|
2445
|
+
try {
|
|
2446
|
+
let status;
|
|
2447
|
+
let retryAfter;
|
|
2448
|
+
if (opts.fetchImpl === void 0) ({ status, retryAfter } = await nodePut(url, headers, source, IDLE_MS));
|
|
2449
|
+
else {
|
|
2450
|
+
const body = "body" in source ? source.body : await (0, import_node_fs2.openAsBlob)(source.path);
|
|
2451
|
+
const res = await opts.fetchImpl(url, { method: "PUT", headers: withoutLength(headers), body });
|
|
2452
|
+
await res.arrayBuffer().catch(() => void 0);
|
|
2453
|
+
status = res.status;
|
|
2454
|
+
retryAfter = res.headers.get("retry-after");
|
|
2455
|
+
}
|
|
2456
|
+
if (status >= 200 && status <= 299) return { ok: true };
|
|
2457
|
+
last = { ok: false, status, code: `bucket_${status}` };
|
|
2458
|
+
if (status < 500 && status !== 429) return last;
|
|
2459
|
+
const seconds = retryAfter === null ? NaN : Number(retryAfter.trim());
|
|
2460
|
+
if (attempt < PUT_DELAYS_MS.length) {
|
|
2461
|
+
await wait(Number.isFinite(seconds) && seconds >= 0 ? Math.min(seconds, 60) * 1e3 : PUT_DELAYS_MS[attempt]);
|
|
2462
|
+
}
|
|
2463
|
+
continue;
|
|
2464
|
+
} catch (err) {
|
|
2465
|
+
last = { ok: false, status: null, code: codeOfError(err) };
|
|
2466
|
+
}
|
|
2467
|
+
if (attempt < PUT_DELAYS_MS.length) await wait(PUT_DELAYS_MS[attempt]);
|
|
2468
|
+
}
|
|
2469
|
+
return last;
|
|
2470
|
+
}
|
|
2327
2471
|
};
|
|
2328
2472
|
}
|
|
2329
|
-
var import_node_http, import_node_https, RETRY_DELAYS_MS, IDLE_MS, sleep;
|
|
2473
|
+
var import_node_fs2, import_node_stream, import_node_http, import_node_https, RETRY_DELAYS_MS, IDLE_MS, PUT_DELAYS_MS, withoutLength, codeOfError, sleep;
|
|
2330
2474
|
var init_client = __esm({
|
|
2331
2475
|
"src/reporter-core/client.ts"() {
|
|
2332
2476
|
"use strict";
|
|
2333
2477
|
init_cjs_shims();
|
|
2478
|
+
import_node_fs2 = require("fs");
|
|
2479
|
+
import_node_stream = require("stream");
|
|
2334
2480
|
import_node_http = require("http");
|
|
2335
2481
|
import_node_https = require("https");
|
|
2336
2482
|
RETRY_DELAYS_MS = [300, 900];
|
|
2337
2483
|
IDLE_MS = 3e5;
|
|
2484
|
+
PUT_DELAYS_MS = [500, 1500, 4e3];
|
|
2485
|
+
withoutLength = (headers) => Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== "content-length"));
|
|
2486
|
+
codeOfError = (err) => {
|
|
2487
|
+
const code = err?.code;
|
|
2488
|
+
return typeof code === "string" && /^[A-Z0-9_]{2,40}$/.test(code) ? code : "network";
|
|
2489
|
+
};
|
|
2338
2490
|
sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2339
2491
|
}
|
|
2340
2492
|
});
|
|
@@ -2476,7 +2628,7 @@ function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
|
2476
2628
|
}
|
|
2477
2629
|
function repoRootOf(dir, stopAt) {
|
|
2478
2630
|
for (let here = (0, import_node_path2.resolve)(dir); ; here = (0, import_node_path2.dirname)(here)) {
|
|
2479
|
-
if ((0,
|
|
2631
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path2.join)(here, ".git"))) return here;
|
|
2480
2632
|
if (here === stopAt || (0, import_node_path2.dirname)(here) === here) return void 0;
|
|
2481
2633
|
}
|
|
2482
2634
|
}
|
|
@@ -2496,12 +2648,12 @@ function machineless(raw, repoRoot, home) {
|
|
|
2496
2648
|
if (repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
|
|
2497
2649
|
return text2;
|
|
2498
2650
|
}
|
|
2499
|
-
function pathIn(
|
|
2500
|
-
const parts =
|
|
2651
|
+
function pathIn(path17) {
|
|
2652
|
+
const parts = path17.split(/[\\/]+/);
|
|
2501
2653
|
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
2502
2654
|
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
2503
2655
|
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
2504
|
-
const escaped = /^\/(?!\/)/.test(
|
|
2656
|
+
const escaped = /^\/(?!\/)/.test(path17) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
2505
2657
|
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
2506
2658
|
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
2507
2659
|
}
|
|
@@ -2546,35 +2698,36 @@ function placeOf(error, testFile) {
|
|
|
2546
2698
|
}
|
|
2547
2699
|
function fileOf(raw) {
|
|
2548
2700
|
if (!raw.startsWith("file://")) return raw;
|
|
2549
|
-
let
|
|
2701
|
+
let path17 = raw.slice("file://".length);
|
|
2550
2702
|
try {
|
|
2551
|
-
|
|
2703
|
+
path17 = decodeURIComponent(path17);
|
|
2552
2704
|
} catch {
|
|
2553
2705
|
}
|
|
2554
|
-
return /^\/[A-Za-z]:\//.test(
|
|
2706
|
+
return /^\/[A-Za-z]:\//.test(path17) ? path17.slice(1) : path17;
|
|
2555
2707
|
}
|
|
2556
2708
|
function fromRoot(place, root) {
|
|
2557
2709
|
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
2558
2710
|
const base = normal(root).replace(/\/+$/, "");
|
|
2559
2711
|
const file = normal(place.file);
|
|
2560
2712
|
if (!file.startsWith(`${base}/`)) return void 0;
|
|
2561
|
-
const
|
|
2562
|
-
if (!REPO_RELATIVE.test(
|
|
2713
|
+
const relative3 = file.slice(base.length + 1);
|
|
2714
|
+
if (!REPO_RELATIVE.test(relative3)) return void 0;
|
|
2563
2715
|
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
2564
|
-
return column === void 0 ? { file:
|
|
2716
|
+
return column === void 0 ? { file: relative3, line: place.line } : { file: relative3, line: place.line, column };
|
|
2565
2717
|
}
|
|
2566
2718
|
function webLink(href) {
|
|
2567
2719
|
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
2568
2720
|
}
|
|
2569
|
-
var
|
|
2721
|
+
var import_node_fs3, import_node_os2, import_node_path2, import_node_util, printable, mediaTypeOf, TEXT_TYPES, isTextType, PATH_START, TEXT_LIMIT, HEADLINE_LIMIT, jsonBytes, ANY_HOME, fileNameOf, omitted, METADATA_TYPE, REPO_RELATIVE, FRAME, FRAME_MAX, firstLine, normal;
|
|
2570
2722
|
var init_failure_detail = __esm({
|
|
2571
2723
|
"src/reporter-core/failure-detail.ts"() {
|
|
2572
2724
|
"use strict";
|
|
2573
2725
|
init_cjs_shims();
|
|
2574
|
-
|
|
2726
|
+
import_node_fs3 = require("fs");
|
|
2575
2727
|
import_node_os2 = require("os");
|
|
2576
2728
|
import_node_path2 = require("path");
|
|
2577
2729
|
import_node_util = require("util");
|
|
2730
|
+
printable = (text2) => text2.replace(/[\u0000-\u001f\u007f-\u009f]/g, "?");
|
|
2578
2731
|
mediaTypeOf = (contentType) => contentType?.split(";")[0]?.trim().toLowerCase() || void 0;
|
|
2579
2732
|
TEXT_TYPES = /* @__PURE__ */ new Set(["application/json", "text/plain"]);
|
|
2580
2733
|
isTextType = (contentType) => TEXT_TYPES.has(mediaTypeOf(contentType) ?? "");
|
|
@@ -2590,7 +2743,7 @@ var init_failure_detail = __esm({
|
|
|
2590
2743
|
FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
2591
2744
|
FRAME_MAX = 8192;
|
|
2592
2745
|
firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
2593
|
-
normal = (
|
|
2746
|
+
normal = (path17) => import_node_path2.posix.normalize(path17.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
2594
2747
|
}
|
|
2595
2748
|
});
|
|
2596
2749
|
|
|
@@ -2610,7 +2763,13 @@ function nameOf(file) {
|
|
|
2610
2763
|
}
|
|
2611
2764
|
function kindOf(type) {
|
|
2612
2765
|
if (type.startsWith("image/")) return "screenshot";
|
|
2613
|
-
return TEXT_TYPES.has(type) ? "text" :
|
|
2766
|
+
return TEXT_TYPES.has(type) ? "text" : "file";
|
|
2767
|
+
}
|
|
2768
|
+
function linkOf(item) {
|
|
2769
|
+
const link = item;
|
|
2770
|
+
if (typeof link?.id !== "string" || typeof link.url !== "string") return void 0;
|
|
2771
|
+
const headers = typeof link.headers === "object" && link.headers !== null ? link.headers : {};
|
|
2772
|
+
return { id: link.id, url: link.url, headers };
|
|
2614
2773
|
}
|
|
2615
2774
|
function textTypeOf(contentType, type) {
|
|
2616
2775
|
const charset = /;\s*charset\s*=\s*"?([^\s";]+)"?/i.exec(contentType)?.[1];
|
|
@@ -2683,7 +2842,7 @@ function warnIfDropped(sent, run, log) {
|
|
|
2683
2842
|
`plune: this deployment stored none of ${names.join(", ")} \u2014 it is older than the fields. The run is otherwise reported in full.`
|
|
2684
2843
|
);
|
|
2685
2844
|
}
|
|
2686
|
-
async function startRun(passed,
|
|
2845
|
+
async function startRun(passed, declared2) {
|
|
2687
2846
|
const env = readEnv();
|
|
2688
2847
|
const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
|
|
2689
2848
|
const cfg = { ...env.config, ...stated };
|
|
@@ -2711,7 +2870,8 @@ async function startRun(passed, declared) {
|
|
|
2711
2870
|
unoffered: 0,
|
|
2712
2871
|
deferred: 0,
|
|
2713
2872
|
screenshots: { uploaded: 0, skipped: 0, failed: 0 },
|
|
2714
|
-
textFiles: { uploaded: 0, skipped: 0, failed: 0 }
|
|
2873
|
+
textFiles: { uploaded: 0, skipped: 0, failed: 0 },
|
|
2874
|
+
otherFiles: { uploaded: 0, skipped: 0, failed: 0 }
|
|
2715
2875
|
};
|
|
2716
2876
|
const resolved = /* @__PURE__ */ new Map();
|
|
2717
2877
|
let buffer = [];
|
|
@@ -2722,7 +2882,13 @@ async function startRun(passed, declared) {
|
|
|
2722
2882
|
let done = false;
|
|
2723
2883
|
const shortened = /* @__PURE__ */ new Set();
|
|
2724
2884
|
const skipsSaid = /* @__PURE__ */ new Set();
|
|
2725
|
-
const countsOf = (kind) => kind === "text" ? stats.textFiles : stats.screenshots;
|
|
2885
|
+
const countsOf = (kind) => kind === "text" ? stats.textFiles : kind === "file" ? stats.otherFiles : stats.screenshots;
|
|
2886
|
+
let bucketSeen = false;
|
|
2887
|
+
let noStorage = 0;
|
|
2888
|
+
let uploadsMissing = false;
|
|
2889
|
+
const failedSaid = /* @__PURE__ */ new Set();
|
|
2890
|
+
const codesSaid = /* @__PURE__ */ new Set();
|
|
2891
|
+
const rooms = /* @__PURE__ */ new Map();
|
|
2726
2892
|
function bound(keys) {
|
|
2727
2893
|
return keys.map((key) => {
|
|
2728
2894
|
const fit = boundKey(key);
|
|
@@ -2730,14 +2896,14 @@ async function startRun(passed, declared) {
|
|
|
2730
2896
|
shortened.add(key.value);
|
|
2731
2897
|
if (shortened.size === 1) {
|
|
2732
2898
|
log(
|
|
2733
|
-
`plune: a test name longer than ${KEY_MAX} characters was shortened to fit its key \u2014
|
|
2899
|
+
`plune: a test name longer than ${KEY_MAX} characters was shortened to fit its key \u2014 ${JSON.stringify(printable(key.value.slice(0, 80)))}\u2026 Give the test a shorter name; until then this is its identity.`
|
|
2734
2900
|
);
|
|
2735
2901
|
}
|
|
2736
2902
|
}
|
|
2737
2903
|
return fit;
|
|
2738
2904
|
});
|
|
2739
2905
|
}
|
|
2740
|
-
const expected =
|
|
2906
|
+
const expected = declared2?.map(bound);
|
|
2741
2907
|
if (offline) {
|
|
2742
2908
|
log('plune: no API token \u2014 run "plune login" first. Results will be written to the fallback file.');
|
|
2743
2909
|
}
|
|
@@ -2860,20 +3026,49 @@ async function startRun(passed, declared) {
|
|
|
2860
3026
|
}
|
|
2861
3027
|
function skip(kind, name, why) {
|
|
2862
3028
|
countsOf(kind).skipped += 1;
|
|
2863
|
-
if (
|
|
2864
|
-
|
|
2865
|
-
|
|
3029
|
+
if (why === "noStorage") noStorage += 1;
|
|
3030
|
+
const said = why === "noStorage" ? why : `${kind}/${why}`;
|
|
3031
|
+
if (skipsSaid.has(said)) return;
|
|
3032
|
+
skipsSaid.add(said);
|
|
3033
|
+
if (why === "noStorage") log(`plune: "${name}" not stored \u2014 ${SKIPPED[why]}.`);
|
|
3034
|
+
else log(`plune: ${KINDS[kind].label} "${name}" not uploaded \u2014 ${SKIPPED[why]}.`);
|
|
3035
|
+
}
|
|
3036
|
+
function ask(job, size) {
|
|
3037
|
+
return client.post(`/v1/results/${encodeURIComponent(job.id)}/files/uploads`, {
|
|
3038
|
+
files: [{ name: job.name, contentType: job.type, size }]
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
function failed(job, why) {
|
|
3042
|
+
countsOf(job.kind).failed += 1;
|
|
3043
|
+
if (!failedSaid.has(job.kind)) {
|
|
3044
|
+
failedSaid.add(job.kind);
|
|
3045
|
+
log(`plune: could not upload a ${KINDS[job.kind].label} (${why}) \u2014 its result is reported without it.`);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
function failedToBucket(job, code) {
|
|
3049
|
+
countsOf(job.kind).failed += 1;
|
|
3050
|
+
if (codesSaid.has(code)) return;
|
|
3051
|
+
codesSaid.add(code);
|
|
3052
|
+
log(`plune: "${job.name}" not uploaded to your bucket (${code}) \u2014 its result is reported without it.`);
|
|
3053
|
+
}
|
|
3054
|
+
function refusedHere(job, size) {
|
|
3055
|
+
if (job.kind === "screenshot") {
|
|
3056
|
+
if (!SCREENSHOT_TYPES.has(job.type)) return "type";
|
|
3057
|
+
if (size > SCREENSHOT_BYTES) return "size";
|
|
3058
|
+
}
|
|
3059
|
+
if (job.kind === "text" && size > TEXT_BYTES) return "textSize";
|
|
3060
|
+
return void 0;
|
|
2866
3061
|
}
|
|
2867
|
-
async function
|
|
3062
|
+
async function uploadDirect(job, size) {
|
|
2868
3063
|
const { kind } = job;
|
|
2869
|
-
|
|
3064
|
+
if (kind === "file") return skip(kind, job.name, "noStorage");
|
|
3065
|
+
const { bytes: ceiling, size: tooBig } = KINDS[kind];
|
|
3066
|
+
if (size > ceiling) return skip(kind, job.name, tooBig);
|
|
2870
3067
|
let bytes;
|
|
2871
3068
|
if (job.body !== void 0) bytes = job.body;
|
|
2872
|
-
else if (job.path === void 0) return skip(kind, job.name, "missing");
|
|
2873
3069
|
else {
|
|
2874
3070
|
try {
|
|
2875
|
-
|
|
2876
|
-
bytes = await import_node_fs3.promises.readFile(job.path);
|
|
3071
|
+
bytes = await import_node_fs4.promises.readFile(job.path);
|
|
2877
3072
|
} catch {
|
|
2878
3073
|
return skip(kind, job.name, "missing");
|
|
2879
3074
|
}
|
|
@@ -2885,32 +3080,96 @@ async function startRun(passed, declared) {
|
|
|
2885
3080
|
bytes,
|
|
2886
3081
|
job.type
|
|
2887
3082
|
);
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
3083
|
+
if (out.ok) countsOf(kind).uploaded += 1;
|
|
3084
|
+
else failed(job, out.detail || out.kind);
|
|
3085
|
+
}
|
|
3086
|
+
async function uploadToBucket(job, size, first) {
|
|
3087
|
+
const source = job.body !== void 0 ? { body: job.body } : { path: job.path, size };
|
|
3088
|
+
let link = first;
|
|
3089
|
+
for (let renewed = false; ; ) {
|
|
3090
|
+
const put = await client.put(link.url, link.headers, source);
|
|
3091
|
+
if (put.ok) break;
|
|
3092
|
+
if (put.status !== 403 || renewed) return failedToBucket(job, put.code);
|
|
3093
|
+
renewed = true;
|
|
3094
|
+
const again = await ask(job, size);
|
|
3095
|
+
if (!again.ok) return failedToBucket(job, codeOf2(again));
|
|
3096
|
+
const fresh = linkOf(again.body?.files?.[0]);
|
|
3097
|
+
if (fresh === void 0) return failedToBucket(job, "bad_answer");
|
|
3098
|
+
link = fresh;
|
|
3099
|
+
}
|
|
3100
|
+
const done2 = await client.post(`/v1/files/${encodeURIComponent(link.id)}/complete`, {});
|
|
3101
|
+
if (!done2.ok) return failedToBucket(job, codeOf2(done2));
|
|
3102
|
+
countsOf(job.kind).uploaded += 1;
|
|
3103
|
+
}
|
|
3104
|
+
let free = UPLOADS_AT_ONCE;
|
|
3105
|
+
const waiting = [];
|
|
3106
|
+
async function gated(work) {
|
|
3107
|
+
if (free === 0) await new Promise((resolve9) => waiting.push(resolve9));
|
|
3108
|
+
else free -= 1;
|
|
3109
|
+
try {
|
|
3110
|
+
return await work();
|
|
3111
|
+
} finally {
|
|
3112
|
+
const next = waiting.shift();
|
|
3113
|
+
if (next !== void 0) next();
|
|
3114
|
+
else free += 1;
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
async function plan(job) {
|
|
3118
|
+
const { kind } = job;
|
|
3119
|
+
let size;
|
|
3120
|
+
if (job.body !== void 0) size = job.body.length;
|
|
3121
|
+
else if (job.path === void 0) return void skip(kind, job.name, "missing");
|
|
3122
|
+
else {
|
|
3123
|
+
try {
|
|
3124
|
+
size = (await import_node_fs4.promises.stat(job.path)).size;
|
|
3125
|
+
} catch {
|
|
3126
|
+
return void skip(kind, job.name, "missing");
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
if (size === 0) return void skip(kind, job.name, "empty");
|
|
3130
|
+
const direct = () => {
|
|
3131
|
+
if (kind === "file") return void skip(kind, job.name, "noStorage");
|
|
3132
|
+
const roomKey = `${job.id}/${kind}`;
|
|
3133
|
+
const left = rooms.get(roomKey) ?? KINDS[kind].room;
|
|
3134
|
+
if (left === 0) return void skip(kind, job.name, KINDS[kind].limit);
|
|
3135
|
+
rooms.set(roomKey, left - 1);
|
|
3136
|
+
return () => uploadDirect(job, size);
|
|
3137
|
+
};
|
|
3138
|
+
if (uploadsMissing) return direct();
|
|
3139
|
+
const out = await gated(() => ask(job, size));
|
|
3140
|
+
if (!out.ok) {
|
|
3141
|
+
if (out.status === 404 && out.detail === "" && out.code === void 0) {
|
|
3142
|
+
uploadsMissing = true;
|
|
3143
|
+
return direct();
|
|
3144
|
+
}
|
|
3145
|
+
if (out.code !== void 0) failedToBucket(job, out.code);
|
|
3146
|
+
else failed(job, out.detail || out.kind);
|
|
3147
|
+
return void 0;
|
|
2892
3148
|
}
|
|
2893
|
-
|
|
2894
|
-
if (
|
|
2895
|
-
|
|
3149
|
+
const item = out.body?.files?.[0];
|
|
3150
|
+
if (item?.skipped === "no_storage") return void skip(kind, job.name, refusedHere(job, size) ?? "noStorage");
|
|
3151
|
+
if (item?.direct === true) return direct();
|
|
3152
|
+
const link = linkOf(item);
|
|
3153
|
+
if (link === void 0) return void failed(job, "bad_answer");
|
|
3154
|
+
if (!bucketSeen) {
|
|
3155
|
+
bucketSeen = true;
|
|
3156
|
+
log(BUCKET_FOUND);
|
|
2896
3157
|
}
|
|
3158
|
+
return () => uploadToBucket(job, size, link);
|
|
2897
3159
|
}
|
|
2898
3160
|
async function upload(items, filesByKey) {
|
|
2899
3161
|
if (filesByKey.size === 0 || !Array.isArray(items)) return;
|
|
2900
3162
|
const jobs = [];
|
|
2901
3163
|
for (const item of items) {
|
|
2902
3164
|
if (item?.status !== "accepted" || typeof item.id !== "string" || typeof item.resultKey !== "string") continue;
|
|
2903
|
-
const room = { screenshot: KINDS.screenshot.room, text: KINDS.text.room };
|
|
2904
3165
|
for (const file of filesByKey.get(item.resultKey) ?? []) {
|
|
2905
3166
|
const type = mediaTypeOf(file.contentType) ?? (file.path === void 0 ? void 0 : TYPE_BY_EXTENSION[(0, import_node_path3.extname)(file.path).toLowerCase()]);
|
|
2906
|
-
|
|
2907
|
-
|
|
3167
|
+
if (type === void 0 || type === METADATA_TYPE2) continue;
|
|
3168
|
+
const kind = kindOf(type);
|
|
3169
|
+
if (kind !== "text" && file.path === void 0) continue;
|
|
2908
3170
|
const name = nameOf(file);
|
|
2909
3171
|
if (file.name.startsWith("_")) skip(kind, name, "hidden");
|
|
2910
|
-
else if (kind === "screenshot" && !SCREENSHOT_TYPES.has(type)) skip(kind, name, "type");
|
|
2911
|
-
else if (room[kind] === 0) skip(kind, name, KINDS[kind].limit);
|
|
2912
3172
|
else {
|
|
2913
|
-
room[kind] -= 1;
|
|
2914
3173
|
jobs.push({
|
|
2915
3174
|
id: item.id,
|
|
2916
3175
|
kind,
|
|
@@ -2922,10 +3181,16 @@ async function startRun(passed, declared) {
|
|
|
2922
3181
|
}
|
|
2923
3182
|
}
|
|
2924
3183
|
}
|
|
2925
|
-
|
|
3184
|
+
const byResult = /* @__PURE__ */ new Map();
|
|
3185
|
+
for (const job of jobs) byResult.set(job.id, [...byResult.get(job.id) ?? [], job]);
|
|
2926
3186
|
await Promise.all(
|
|
2927
|
-
|
|
2928
|
-
|
|
3187
|
+
[...byResult.values()].map(async (group) => {
|
|
3188
|
+
const work = [];
|
|
3189
|
+
for (const job of group) {
|
|
3190
|
+
const act = await plan(job);
|
|
3191
|
+
if (act !== void 0) work.push(gated(act));
|
|
3192
|
+
}
|
|
3193
|
+
await Promise.all(work);
|
|
2929
3194
|
})
|
|
2930
3195
|
);
|
|
2931
3196
|
}
|
|
@@ -3003,8 +3268,20 @@ async function startRun(passed, declared) {
|
|
|
3003
3268
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
3004
3269
|
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
3005
3270
|
if (stats.created > 0) parts.push(`${stats.created} added as cases (trusted source)`);
|
|
3006
|
-
|
|
3007
|
-
|
|
3271
|
+
const kinds = [stats.screenshots, stats.textFiles, stats.otherFiles];
|
|
3272
|
+
const sum = (what) => kinds.reduce((n, counts) => n + counts[what], 0);
|
|
3273
|
+
if (bucketSeen) {
|
|
3274
|
+
const other = sum("skipped") - noStorage;
|
|
3275
|
+
parts.push(
|
|
3276
|
+
`${sum("uploaded")} file(s) uploaded to your bucket` + (noStorage > 0 ? `, ${noStorage} skipped (no bucket connected)` : "") + (other > 0 ? `, ${other} skipped` : "") + (sum("failed") > 0 ? `, ${sum("failed")} failed to upload` : "")
|
|
3277
|
+
);
|
|
3278
|
+
} else {
|
|
3279
|
+
for (const said of [tally(stats.screenshots, "screenshot"), tally(stats.textFiles, "text file")]) {
|
|
3280
|
+
if (said !== void 0) parts.push(said);
|
|
3281
|
+
}
|
|
3282
|
+
const other = stats.otherFiles;
|
|
3283
|
+
if (other.skipped > 0) parts.push(`${other.skipped} file(s) not stored (no bucket connected)`);
|
|
3284
|
+
if (other.failed > 0) parts.push(`${other.failed} file(s) failed to upload`);
|
|
3008
3285
|
}
|
|
3009
3286
|
if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
|
|
3010
3287
|
if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
|
|
@@ -3057,13 +3334,13 @@ async function startRun(passed, declared) {
|
|
|
3057
3334
|
}
|
|
3058
3335
|
};
|
|
3059
3336
|
}
|
|
3060
|
-
var import_node_crypto2,
|
|
3337
|
+
var import_node_crypto2, import_node_fs4, import_node_path3, import_node_util2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX, KEY_MAX, TITLE_MAX, COUNTED, SCREENSHOT_TYPES, SCREENSHOT_BYTES, SCREENSHOTS_PER_RESULT, TEXT_BYTES, TEXTS_PER_RESULT, UTF8, TYPE_BY_EXTENSION, METADATA_TYPE2, UPLOADS_AT_ONCE, NAME_MAX, SKIPPED, BUCKET_FOUND, KINDS, codeOf2, BATCH_BYTES, ENVELOPE_BYTES;
|
|
3061
3338
|
var init_session = __esm({
|
|
3062
3339
|
"src/reporter-core/session.ts"() {
|
|
3063
3340
|
"use strict";
|
|
3064
3341
|
init_cjs_shims();
|
|
3065
3342
|
import_node_crypto2 = require("crypto");
|
|
3066
|
-
|
|
3343
|
+
import_node_fs4 = require("fs");
|
|
3067
3344
|
import_node_path3 = require("path");
|
|
3068
3345
|
import_node_util2 = require("util");
|
|
3069
3346
|
init_api_url();
|
|
@@ -3092,6 +3369,7 @@ var init_session = __esm({
|
|
|
3092
3369
|
".jpeg": "image/jpeg",
|
|
3093
3370
|
".webp": "image/webp"
|
|
3094
3371
|
};
|
|
3372
|
+
METADATA_TYPE2 = "application/plune.metadata+json";
|
|
3095
3373
|
UPLOADS_AT_ONCE = 4;
|
|
3096
3374
|
NAME_MAX = 200;
|
|
3097
3375
|
SKIPPED = {
|
|
@@ -3104,12 +3382,17 @@ var init_session = __esm({
|
|
|
3104
3382
|
textSize: "Plune takes a text file of at most 512 KiB",
|
|
3105
3383
|
empty: "the file is empty",
|
|
3106
3384
|
binary: "the file holds a NUL byte, and Plune takes UTF-8 text without one",
|
|
3107
|
-
encoding: "Plune takes UTF-8 text only"
|
|
3385
|
+
encoding: "Plune takes UTF-8 text only",
|
|
3386
|
+
noStorage: "no storage bucket is connected to this project"
|
|
3108
3387
|
};
|
|
3388
|
+
BUCKET_FOUND = "plune: storage bucket found for this project \u2014 screenshots, text, trace and video go straight to it over short-lived links.";
|
|
3109
3389
|
KINDS = {
|
|
3110
3390
|
screenshot: { label: "screenshot", bytes: SCREENSHOT_BYTES, room: SCREENSHOTS_PER_RESULT, limit: "limit", size: "size" },
|
|
3111
|
-
text: { label: "text file", bytes: TEXT_BYTES, room: TEXTS_PER_RESULT, limit: "textLimit", size: "textSize" }
|
|
3391
|
+
text: { label: "text file", bytes: TEXT_BYTES, room: TEXTS_PER_RESULT, limit: "textLimit", size: "textSize" },
|
|
3392
|
+
// Only the label is used: a file of this kind has no home in the database, so no ceiling of its own is known here.
|
|
3393
|
+
file: { label: "file", bytes: 0, room: 0, limit: "limit", size: "size" }
|
|
3112
3394
|
};
|
|
3395
|
+
codeOf2 = (out) => out.code ?? (out.status === null ? "network" : `http_${out.status}`);
|
|
3113
3396
|
BATCH_BYTES = 8 * 1024 * 1024;
|
|
3114
3397
|
ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ results: [] }));
|
|
3115
3398
|
}
|
|
@@ -3200,21 +3483,21 @@ async function handleRunDelete(id, options = {}) {
|
|
|
3200
3483
|
);
|
|
3201
3484
|
}
|
|
3202
3485
|
write(`Run ${id} deleted, with its results and the review-queue entries it produced.`);
|
|
3203
|
-
write("Recoverable for
|
|
3486
|
+
write("Recoverable for 30 days \u2014 an owner of the project can put it back from Settings \u2192 Bin.");
|
|
3204
3487
|
}
|
|
3205
3488
|
async function handleRunExec(options) {
|
|
3206
3489
|
const [command, ...args] = options.argv;
|
|
3207
3490
|
if (command === void 0) throw new RunCommandError("Nothing to run \u2014 pass a command after `--`.");
|
|
3208
3491
|
const run = await handleRunStart(options);
|
|
3209
3492
|
const spawnImpl = options.spawnImpl ?? import_node_child_process.spawn;
|
|
3210
|
-
const code = await new Promise((
|
|
3493
|
+
const code = await new Promise((resolve9) => {
|
|
3211
3494
|
const child = spawnImpl(command, args, {
|
|
3212
3495
|
stdio: "inherit",
|
|
3213
3496
|
shell: process.platform === "win32",
|
|
3214
3497
|
env: { ...process.env, PLUNE_RUN: run.externalKey, PLUNE_PROCEED: "1" }
|
|
3215
3498
|
});
|
|
3216
|
-
child.on("close", (exitCode) =>
|
|
3217
|
-
child.on("error", () =>
|
|
3499
|
+
child.on("close", (exitCode) => resolve9(exitCode ?? 1));
|
|
3500
|
+
child.on("error", () => resolve9(1));
|
|
3218
3501
|
});
|
|
3219
3502
|
await handleRunFinish(run.id, options);
|
|
3220
3503
|
return code;
|
|
@@ -3345,6 +3628,346 @@ var init_result_key = __esm({
|
|
|
3345
3628
|
}
|
|
3346
3629
|
});
|
|
3347
3630
|
|
|
3631
|
+
// src/importers/attachments.ts
|
|
3632
|
+
function reportFolder(report) {
|
|
3633
|
+
const folder = path9.dirname(path9.resolve(report));
|
|
3634
|
+
try {
|
|
3635
|
+
return fs8.realpathSync(folder);
|
|
3636
|
+
} catch {
|
|
3637
|
+
return folder;
|
|
3638
|
+
}
|
|
3639
|
+
}
|
|
3640
|
+
function reportFolders(report) {
|
|
3641
|
+
return { lexical: path9.dirname(path9.resolve(report)), real: reportFolder(report) };
|
|
3642
|
+
}
|
|
3643
|
+
function within(folder, target) {
|
|
3644
|
+
const rel = path9.relative(folder, target);
|
|
3645
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel);
|
|
3646
|
+
}
|
|
3647
|
+
function resolveAttachment(folder, ref, lexical = folder) {
|
|
3648
|
+
if (/^[\\/]{2}/.test(ref) || ref.includes("\0")) return { why: "outside" };
|
|
3649
|
+
const face = path9.resolve(lexical, ref);
|
|
3650
|
+
if (!within(lexical, face) && !within(folder, face)) return { why: "outside" };
|
|
3651
|
+
let real;
|
|
3652
|
+
try {
|
|
3653
|
+
real = fs8.realpathSync(path9.resolve(folder, ref));
|
|
3654
|
+
} catch {
|
|
3655
|
+
return { why: "absent" };
|
|
3656
|
+
}
|
|
3657
|
+
if (!within(folder, real)) return { why: "outside" };
|
|
3658
|
+
try {
|
|
3659
|
+
return fs8.statSync(real).isFile() ? { file: real } : { why: "not-a-file" };
|
|
3660
|
+
} catch {
|
|
3661
|
+
return { why: "absent" };
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
function markersOf(test) {
|
|
3665
|
+
const refs = /* @__PURE__ */ new Set();
|
|
3666
|
+
for (const stream of test.children) {
|
|
3667
|
+
if (stream.name !== "system-out" && stream.name !== "system-err") continue;
|
|
3668
|
+
for (const match of stream.text.matchAll(MARKER)) {
|
|
3669
|
+
const ref = match[1]?.trim() ?? "";
|
|
3670
|
+
if (ref !== "") refs.add(ref);
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
return [...refs];
|
|
3674
|
+
}
|
|
3675
|
+
function filesOfTest(test, folder, title, missing, options = {}) {
|
|
3676
|
+
const files = [];
|
|
3677
|
+
const all = markersOf(test);
|
|
3678
|
+
if (all.length > MARKERS_PER_TEST) {
|
|
3679
|
+
options.notes?.push(
|
|
3680
|
+
`plune: ${JSON.stringify(printable(title))}: ${all.length - MARKERS_PER_TEST} more markers ignored (at most ${MARKERS_PER_TEST} per test)`
|
|
3681
|
+
);
|
|
3682
|
+
}
|
|
3683
|
+
for (const ref of all.slice(0, MARKERS_PER_TEST)) {
|
|
3684
|
+
const found = resolveAttachment(folder, ref, options.lexical);
|
|
3685
|
+
if (!("file" in found)) {
|
|
3686
|
+
missing.push({ title, path: ref, ...options.resultKey !== void 0 ? { resultKey: options.resultKey } : {} });
|
|
3687
|
+
continue;
|
|
3688
|
+
}
|
|
3689
|
+
const real = found.file;
|
|
3690
|
+
if (!files.some((f) => f.path === real)) {
|
|
3691
|
+
files.push({ name: printable(ref.split(/[\\/]/).pop() || path9.basename(real)), contentType: typeOfFile(real), path: real });
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
return files;
|
|
3695
|
+
}
|
|
3696
|
+
var fs8, path9, MARKER, TYPES, typeOfFile, MARKERS_PER_TEST;
|
|
3697
|
+
var init_attachments = __esm({
|
|
3698
|
+
"src/importers/attachments.ts"() {
|
|
3699
|
+
"use strict";
|
|
3700
|
+
init_cjs_shims();
|
|
3701
|
+
fs8 = __toESM(require("fs"), 1);
|
|
3702
|
+
path9 = __toESM(require("path"), 1);
|
|
3703
|
+
init_failure_detail();
|
|
3704
|
+
MARKER = /^[ \t]*\[\[ATTACHMENT\|([^\r\n]*?)\]\][ \t]*$/gm;
|
|
3705
|
+
TYPES = {
|
|
3706
|
+
".png": "image/png",
|
|
3707
|
+
".jpg": "image/jpeg",
|
|
3708
|
+
".jpeg": "image/jpeg",
|
|
3709
|
+
".webp": "image/webp",
|
|
3710
|
+
".gif": "image/gif",
|
|
3711
|
+
".svg": "image/svg+xml",
|
|
3712
|
+
".json": "application/json",
|
|
3713
|
+
".txt": "text/plain",
|
|
3714
|
+
".log": "text/plain",
|
|
3715
|
+
".md": "text/plain",
|
|
3716
|
+
".csv": "text/csv",
|
|
3717
|
+
".html": "text/html",
|
|
3718
|
+
".htm": "text/html",
|
|
3719
|
+
".xml": "application/xml",
|
|
3720
|
+
".pdf": "application/pdf",
|
|
3721
|
+
".zip": "application/zip",
|
|
3722
|
+
".webm": "video/webm",
|
|
3723
|
+
".mp4": "video/mp4",
|
|
3724
|
+
".har": "application/json"
|
|
3725
|
+
};
|
|
3726
|
+
typeOfFile = (file) => TYPES[path9.extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
3727
|
+
MARKERS_PER_TEST = 100;
|
|
3728
|
+
}
|
|
3729
|
+
});
|
|
3730
|
+
|
|
3731
|
+
// src/importers/allure.ts
|
|
3732
|
+
function isFolder(path17) {
|
|
3733
|
+
try {
|
|
3734
|
+
return fs9.statSync(path17).isDirectory();
|
|
3735
|
+
} catch {
|
|
3736
|
+
return false;
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
function load(file, maxBytes) {
|
|
3740
|
+
let size;
|
|
3741
|
+
try {
|
|
3742
|
+
size = fs9.statSync(file).size;
|
|
3743
|
+
} catch (err) {
|
|
3744
|
+
throw new AllureReportError(`${printable(file)} \u2014 cannot read \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
|
|
3745
|
+
}
|
|
3746
|
+
if (size > maxBytes) {
|
|
3747
|
+
throw new AllureReportError(`${printable(file)} \u2014 ${size} bytes, over the ${maxBytes} an Allure result may have; not read.`);
|
|
3748
|
+
}
|
|
3749
|
+
let parsed;
|
|
3750
|
+
try {
|
|
3751
|
+
parsed = JSON.parse(fs9.readFileSync(file, "utf8"));
|
|
3752
|
+
} catch (err) {
|
|
3753
|
+
throw new AllureReportError(`${printable(file)} \u2014 not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
|
|
3754
|
+
}
|
|
3755
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
3756
|
+
throw new AllureReportError(`${printable(file)} \u2014 not an Allure result: expected a JSON object.`);
|
|
3757
|
+
}
|
|
3758
|
+
return parsed;
|
|
3759
|
+
}
|
|
3760
|
+
function listOf(v) {
|
|
3761
|
+
const out = [];
|
|
3762
|
+
if (!Array.isArray(v)) return out;
|
|
3763
|
+
for (const x of v) if (typeof x === "object" && x !== null && !Array.isArray(x)) out.push(x);
|
|
3764
|
+
return out;
|
|
3765
|
+
}
|
|
3766
|
+
function keyOf(r, name) {
|
|
3767
|
+
const full = textOf(r.fullName);
|
|
3768
|
+
if (name.endsWith("]") && full.includes("#")) return `${full.split("#")[0]}#${name}`;
|
|
3769
|
+
let base;
|
|
3770
|
+
if (full.includes(" \u203A ")) base = full.split(" \u203A ").join("#");
|
|
3771
|
+
else if (full.includes("#")) base = full;
|
|
3772
|
+
else if (labelOf(r, "package") !== "") base = `${labelOf(r, "package")}#${name}`;
|
|
3773
|
+
else base = full !== "" ? full : name;
|
|
3774
|
+
const params = listOf(r.parameters).map((p) => textOf(p.value));
|
|
3775
|
+
return params.length > 0 && !base.endsWith("]") ? `${base}[${params.join("-")}]` : base;
|
|
3776
|
+
}
|
|
3777
|
+
function labelGroup(r) {
|
|
3778
|
+
const out = [];
|
|
3779
|
+
for (const l of ["parentSuite", "suite", "subSuite"]) {
|
|
3780
|
+
const v = labelOf(r, l);
|
|
3781
|
+
if (v !== "" && out.at(-1) !== v) out.push(v);
|
|
3782
|
+
}
|
|
3783
|
+
return out;
|
|
3784
|
+
}
|
|
3785
|
+
function attachmentsOf(holder, out) {
|
|
3786
|
+
let deep = false;
|
|
3787
|
+
const todo = [{ h: holder, depth: 0 }];
|
|
3788
|
+
while (todo.length > 0) {
|
|
3789
|
+
const { h, depth } = todo.pop();
|
|
3790
|
+
for (const a of listOf(h.attachments)) out.push(a);
|
|
3791
|
+
const steps = listOf(h.steps);
|
|
3792
|
+
if (depth >= MAX_DEPTH) {
|
|
3793
|
+
deep ||= steps.length > 0;
|
|
3794
|
+
continue;
|
|
3795
|
+
}
|
|
3796
|
+
for (let i = steps.length - 1; i >= 0; i--) todo.push({ h: steps[i], depth: depth + 1 });
|
|
3797
|
+
}
|
|
3798
|
+
return deep;
|
|
3799
|
+
}
|
|
3800
|
+
function readAllure(dir, missing = [], notes = [], maxBytes = MAX_JSON_BYTES) {
|
|
3801
|
+
if (!isFolder(dir)) throw new AllureReportError(`${printable(dir)} \u2014 not a folder.`);
|
|
3802
|
+
const resultNames = namesIn(dir, RESULT);
|
|
3803
|
+
if (resultNames.length === 0) {
|
|
3804
|
+
throw new AllureReportError(
|
|
3805
|
+
`${printable(dir)} \u2014 no *-result.json files in it, so there is no Allure report to read. Point at the folder an Allure adapter wrote (pytest --alluredir, allure-results), or at a JUnit or Playwright JSON file.`
|
|
3806
|
+
);
|
|
3807
|
+
}
|
|
3808
|
+
const { lexical, real: folder } = reportFolders(path10.join(dir, "_"));
|
|
3809
|
+
const tests = resultNames.map((n) => load(path10.join(dir, n), maxBytes));
|
|
3810
|
+
const containers = namesIn(dir, CONTAINER).map((n) => load(path10.join(dir, n), maxBytes));
|
|
3811
|
+
const direct = /* @__PURE__ */ new Map();
|
|
3812
|
+
for (const c of containers) {
|
|
3813
|
+
for (const child of Array.isArray(c.children) ? c.children : []) {
|
|
3814
|
+
if (typeof child !== "string") continue;
|
|
3815
|
+
const list2 = direct.get(child);
|
|
3816
|
+
if (list2 === void 0) direct.set(child, [c]);
|
|
3817
|
+
else list2.push(c);
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
const around = (uuid) => {
|
|
3821
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3822
|
+
const todo = [uuid];
|
|
3823
|
+
while (todo.length > 0) {
|
|
3824
|
+
for (const c of direct.get(todo.pop()) ?? []) {
|
|
3825
|
+
if (seen.has(c)) continue;
|
|
3826
|
+
seen.add(c);
|
|
3827
|
+
if (typeof c.uuid === "string") todo.push(c.uuid);
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
return [...seen];
|
|
3831
|
+
};
|
|
3832
|
+
const ctx = { folder, lexical, missing, notes, around, direct };
|
|
3833
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3834
|
+
tests.forEach((r, i) => {
|
|
3835
|
+
const id = textOf(r.historyId);
|
|
3836
|
+
const g = id === "" ? `\0${i}` : id;
|
|
3837
|
+
const list2 = groups.get(g);
|
|
3838
|
+
if (list2 === void 0) groups.set(g, [r]);
|
|
3839
|
+
else list2.push(r);
|
|
3840
|
+
});
|
|
3841
|
+
const out = [];
|
|
3842
|
+
for (const attempts of groups.values()) {
|
|
3843
|
+
const ordered = attempts.map((r, i) => ({ r, i })).sort(
|
|
3844
|
+
(a, b) => stopOf(a.r) - stopOf(b.r) || (timeOf(a.r.start) ?? 0) - (timeOf(b.r.start) ?? 0) || compareText(textOf(a.r.uuid), textOf(b.r.uuid)) || a.i - b.i
|
|
3845
|
+
).map((x) => x.r);
|
|
3846
|
+
ordered.forEach((r, retry) => out.push(pendingOf(r, retry, ctx)));
|
|
3847
|
+
}
|
|
3848
|
+
return out;
|
|
3849
|
+
}
|
|
3850
|
+
function pendingOf(r, retry, ctx) {
|
|
3851
|
+
const name = textOf(r.name);
|
|
3852
|
+
const uuid = textOf(r.uuid);
|
|
3853
|
+
const key = keyOf(r, name);
|
|
3854
|
+
const ancestors = uuid === "" ? [] : ctx.around(uuid);
|
|
3855
|
+
const suite = (uuid === "" ? [] : ctx.direct.get(uuid) ?? []).filter((c) => textOf(c.name) !== "" && c.name !== name).sort((a, b) => (Array.isArray(b.children) ? b.children.length : 0) - (Array.isArray(a.children) ? a.children.length : 0))[0];
|
|
3856
|
+
const group = suite !== void 0 ? [textOf(suite.name)] : labelGroup(r);
|
|
3857
|
+
const title = [...group, name].join(" \u203A ");
|
|
3858
|
+
const safeTitle = printable(title);
|
|
3859
|
+
const rkey = resultKey(key, retry);
|
|
3860
|
+
const files = [];
|
|
3861
|
+
const announced = /* @__PURE__ */ new Set();
|
|
3862
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
3863
|
+
const announce = (att) => {
|
|
3864
|
+
const source = textOf(att.source);
|
|
3865
|
+
if (source === "" || announced.has(source) || ignored.has(source)) return;
|
|
3866
|
+
if (announced.size >= MARKERS_PER_TEST) {
|
|
3867
|
+
ignored.add(source);
|
|
3868
|
+
return;
|
|
3869
|
+
}
|
|
3870
|
+
announced.add(source);
|
|
3871
|
+
const found = resolveAttachment(ctx.folder, source, ctx.lexical);
|
|
3872
|
+
if (!("file" in found)) {
|
|
3873
|
+
ctx.missing.push({ title: safeTitle, resultKey: rkey, path: source });
|
|
3874
|
+
return;
|
|
3875
|
+
}
|
|
3876
|
+
if (files.some((f) => f.path === found.file)) return;
|
|
3877
|
+
const type = textOf(att.type).trim();
|
|
3878
|
+
const name2 = printable(textOf(att.name) !== "" ? textOf(att.name) : path10.basename(found.file));
|
|
3879
|
+
files.push({ name: name2, contentType: type !== "" ? type : typeOfFile(found.file), path: found.file });
|
|
3880
|
+
};
|
|
3881
|
+
const holders = [];
|
|
3882
|
+
for (const c of [...ancestors].reverse()) for (const h of listOf(c.befores)) holders.push(h);
|
|
3883
|
+
holders.push(r);
|
|
3884
|
+
for (const c of ancestors) for (const h of listOf(c.afters)) holders.push(h);
|
|
3885
|
+
const atts = [];
|
|
3886
|
+
let deep = false;
|
|
3887
|
+
for (const h of holders) deep = attachmentsOf(h, atts) || deep;
|
|
3888
|
+
atts.forEach(announce);
|
|
3889
|
+
if (ignored.size > 0) {
|
|
3890
|
+
ctx.notes.push(`plune: ${JSON.stringify(safeTitle)}: ${ignored.size} more markers ignored (at most ${MARKERS_PER_TEST} per test)`);
|
|
3891
|
+
}
|
|
3892
|
+
if (deep) ctx.notes.push(`plune: ${JSON.stringify(safeTitle)}: steps nested deeper than ${MAX_DEPTH} are ignored`);
|
|
3893
|
+
const status = textOf(r.status) !== "" ? textOf(r.status) : "unknown";
|
|
3894
|
+
const details = typeof r.statusDetails === "object" && r.statusDetails !== null ? r.statusDetails : {};
|
|
3895
|
+
const message = textOf(details.message);
|
|
3896
|
+
const trace = textOf(details.trace);
|
|
3897
|
+
const attempt = {
|
|
3898
|
+
status,
|
|
3899
|
+
errors: [{ text: [message, trace].filter((s) => s !== "").join("\n\n") }],
|
|
3900
|
+
steps: declared(r.steps),
|
|
3901
|
+
attachments: files.map(({ name: n, contentType, path: p }) => ({
|
|
3902
|
+
name: n,
|
|
3903
|
+
...contentType !== void 0 ? { contentType } : {},
|
|
3904
|
+
...p !== void 0 ? { path: p } : {}
|
|
3905
|
+
})),
|
|
3906
|
+
testFile: ""
|
|
3907
|
+
};
|
|
3908
|
+
const errorContext = errorContextOf(attempt);
|
|
3909
|
+
const failure = FAILED.has(status) ? failureOf(attempt) : void 0;
|
|
3910
|
+
const pkg2 = labelOf(r, "package");
|
|
3911
|
+
const where = (key.includes("#") ? key.split("#")[0] : pkg2)?.replace(/^[A-Za-z][A-Za-z0-9_-]+:(?![\\/])/, "");
|
|
3912
|
+
const stated = TOKEN.exec(name)?.[1];
|
|
3913
|
+
const started = timeOf(r.start);
|
|
3914
|
+
const stopped = timeOf(r.stop);
|
|
3915
|
+
const durationMs = started !== void 0 && stopped !== void 0 ? Math.max(0, stopped - started) : 0;
|
|
3916
|
+
return {
|
|
3917
|
+
resultKey: rkey,
|
|
3918
|
+
...stated !== void 0 ? { testCaseId: stated } : {},
|
|
3919
|
+
keys: [{ kind: "path-title", value: key }],
|
|
3920
|
+
source: SOURCE,
|
|
3921
|
+
rawStatus: status,
|
|
3922
|
+
title,
|
|
3923
|
+
...where !== void 0 && where !== "" ? { specRef: where } : {},
|
|
3924
|
+
...started !== void 0 ? {
|
|
3925
|
+
execution: {
|
|
3926
|
+
startedAt: new Date(started).toISOString(),
|
|
3927
|
+
finishedAt: new Date(started + durationMs).toISOString(),
|
|
3928
|
+
durationMs,
|
|
3929
|
+
retry
|
|
3930
|
+
}
|
|
3931
|
+
} : {},
|
|
3932
|
+
...errorContext !== "" ? { errorContext } : {},
|
|
3933
|
+
...failure !== void 0 ? { failure } : {},
|
|
3934
|
+
...files.length > 0 ? { files } : {}
|
|
3935
|
+
};
|
|
3936
|
+
}
|
|
3937
|
+
var fs9, path10, SOURCE, TOKEN, AllureReportError, RESULT, CONTAINER, MAX_JSON_BYTES, MAX_DEPTH, MAX_TIME, namesIn, timeOf, textOf, labelOf, FAILED, declared, stopOf, compareText;
|
|
3938
|
+
var init_allure = __esm({
|
|
3939
|
+
"src/importers/allure.ts"() {
|
|
3940
|
+
"use strict";
|
|
3941
|
+
init_cjs_shims();
|
|
3942
|
+
fs9 = __toESM(require("fs"), 1);
|
|
3943
|
+
path10 = __toESM(require("path"), 1);
|
|
3944
|
+
init_failure_detail();
|
|
3945
|
+
init_result_key();
|
|
3946
|
+
init_attachments();
|
|
3947
|
+
SOURCE = "allure";
|
|
3948
|
+
TOKEN = /@P([A-Za-z0-9_-]+)/;
|
|
3949
|
+
AllureReportError = class extends Error {
|
|
3950
|
+
constructor(message) {
|
|
3951
|
+
super(message);
|
|
3952
|
+
this.name = "AllureReportError";
|
|
3953
|
+
}
|
|
3954
|
+
};
|
|
3955
|
+
RESULT = /-result\.json$/;
|
|
3956
|
+
CONTAINER = /-container\.json$/;
|
|
3957
|
+
MAX_JSON_BYTES = 64 * 1024 * 1024;
|
|
3958
|
+
MAX_DEPTH = 256;
|
|
3959
|
+
MAX_TIME = 864e13;
|
|
3960
|
+
namesIn = (dir, pattern) => fs9.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile() && pattern.test(d.name)).map((d) => d.name).sort();
|
|
3961
|
+
timeOf = (v) => typeof v === "number" && Math.abs(v) <= MAX_TIME ? v : void 0;
|
|
3962
|
+
textOf = (v) => typeof v === "string" ? v : "";
|
|
3963
|
+
labelOf = (r, name) => textOf(listOf(r.labels).find((l) => l.name === name)?.value);
|
|
3964
|
+
FAILED = /* @__PURE__ */ new Set(["failed", "broken"]);
|
|
3965
|
+
declared = (steps, depth = 0) => depth >= MAX_DEPTH ? [] : listOf(steps).map((s) => ({ title: textOf(s.name), failed: FAILED.has(textOf(s.status)), steps: declared(s.steps, depth + 1) }));
|
|
3966
|
+
stopOf = (r) => timeOf(r.stop) ?? timeOf(r.start) ?? 0;
|
|
3967
|
+
compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
3968
|
+
}
|
|
3969
|
+
});
|
|
3970
|
+
|
|
3348
3971
|
// src/importers/xml.ts
|
|
3349
3972
|
function decode(raw) {
|
|
3350
3973
|
if (!raw.includes("&")) return raw;
|
|
@@ -3409,9 +4032,9 @@ function parseXml(source, file) {
|
|
|
3409
4032
|
if (end === -1) throw new XmlParseError(file, line, "a closing tag is never closed");
|
|
3410
4033
|
const name2 = source.slice(i + 2, end).trim();
|
|
3411
4034
|
const open = top();
|
|
3412
|
-
if (open === root) throw new XmlParseError(file, line, `</${name2}> closes nothing`);
|
|
4035
|
+
if (open === root) throw new XmlParseError(file, line, `</${printable(name2)}> closes nothing`);
|
|
3413
4036
|
if (open.name !== name2) {
|
|
3414
|
-
throw new XmlParseError(file, line, `</${name2}> closes <${open.name}>, opened on line ${open.line}`);
|
|
4037
|
+
throw new XmlParseError(file, line, `</${printable(name2)}> closes <${printable(open.name)}>, opened on line ${open.line}`);
|
|
3415
4038
|
}
|
|
3416
4039
|
stack.pop();
|
|
3417
4040
|
advance(end + 1);
|
|
@@ -3427,7 +4050,7 @@ function parseXml(source, file) {
|
|
|
3427
4050
|
let selfClosing = false;
|
|
3428
4051
|
for (; ; ) {
|
|
3429
4052
|
while (j < source.length && SPACE.test(source[j])) j += 1;
|
|
3430
|
-
if (j >= source.length) throw new XmlParseError(file, startLine, `<${name}> is never closed`);
|
|
4053
|
+
if (j >= source.length) throw new XmlParseError(file, startLine, `<${printable(name)}> is never closed`);
|
|
3431
4054
|
if (source[j] === "/" && source[j + 1] === ">") {
|
|
3432
4055
|
selfClosing = true;
|
|
3433
4056
|
j += 2;
|
|
@@ -3440,19 +4063,19 @@ function parseXml(source, file) {
|
|
|
3440
4063
|
const attrStart = j;
|
|
3441
4064
|
while (j < source.length && !ATTR_NAME_END.test(source[j])) j += 1;
|
|
3442
4065
|
const attr = source.slice(attrStart, j);
|
|
3443
|
-
if (attr === "") throw new XmlParseError(file, startLine, `<${name}> has an attribute with no name`);
|
|
4066
|
+
if (attr === "") throw new XmlParseError(file, startLine, `<${printable(name)}> has an attribute with no name`);
|
|
3444
4067
|
while (j < source.length && SPACE.test(source[j])) j += 1;
|
|
3445
4068
|
if (source[j] !== "=") {
|
|
3446
|
-
throw new XmlParseError(file, startLine, `<${name}> attribute "${attr}" has no value`);
|
|
4069
|
+
throw new XmlParseError(file, startLine, `<${printable(name)}> attribute "${printable(attr)}" has no value`);
|
|
3447
4070
|
}
|
|
3448
4071
|
j += 1;
|
|
3449
4072
|
while (j < source.length && SPACE.test(source[j])) j += 1;
|
|
3450
4073
|
const quote = source[j];
|
|
3451
4074
|
if (quote !== '"' && quote !== "'") {
|
|
3452
|
-
throw new XmlParseError(file, startLine, `<${name}> attribute "${attr}" is not quoted`);
|
|
4075
|
+
throw new XmlParseError(file, startLine, `<${printable(name)}> attribute "${printable(attr)}" is not quoted`);
|
|
3453
4076
|
}
|
|
3454
4077
|
const close = source.indexOf(quote, j + 1);
|
|
3455
|
-
if (close === -1) throw new XmlParseError(file, startLine, `<${name}> attribute "${attr}" is never closed`);
|
|
4078
|
+
if (close === -1) throw new XmlParseError(file, startLine, `<${printable(name)}> attribute "${printable(attr)}" is never closed`);
|
|
3456
4079
|
el.attrs[attr] = decode(source.slice(j + 1, close));
|
|
3457
4080
|
j = close + 1;
|
|
3458
4081
|
}
|
|
@@ -3462,7 +4085,7 @@ function parseXml(source, file) {
|
|
|
3462
4085
|
}
|
|
3463
4086
|
if (stack.length > 1) {
|
|
3464
4087
|
const open = top();
|
|
3465
|
-
throw new XmlParseError(file, open.line, `<${open.name}> is never closed`);
|
|
4088
|
+
throw new XmlParseError(file, open.line, `<${printable(open.name)}> is never closed`);
|
|
3466
4089
|
}
|
|
3467
4090
|
if (root.children.length === 0) throw new XmlParseError(file, 1, "no elements \u2014 is this an XML report?");
|
|
3468
4091
|
return root;
|
|
@@ -3472,6 +4095,7 @@ var init_xml = __esm({
|
|
|
3472
4095
|
"src/importers/xml.ts"() {
|
|
3473
4096
|
"use strict";
|
|
3474
4097
|
init_cjs_shims();
|
|
4098
|
+
init_failure_detail();
|
|
3475
4099
|
XmlParseError = class extends Error {
|
|
3476
4100
|
constructor(file, line, detail) {
|
|
3477
4101
|
super(`${file}:${line} \u2014 ${detail}`);
|
|
@@ -3518,7 +4142,7 @@ function playwrightOf(classname, name) {
|
|
|
3518
4142
|
if (!SPEC_FILE.test(classname)) return void 0;
|
|
3519
4143
|
return { file: classname.replaceAll("\\", "/"), titles: name.split(" \u203A ") };
|
|
3520
4144
|
}
|
|
3521
|
-
function
|
|
4145
|
+
function keyOf2(classname, name, playwright) {
|
|
3522
4146
|
if (playwright !== void 0) return { kind: "path-title", value: [playwright.file, ...playwright.titles].join("#") };
|
|
3523
4147
|
return { kind: "path-title", value: classname === "" ? name : `${classname}#${name}` };
|
|
3524
4148
|
}
|
|
@@ -3551,8 +4175,9 @@ function executionOf(test, suite) {
|
|
|
3551
4175
|
retry: 0
|
|
3552
4176
|
};
|
|
3553
4177
|
}
|
|
3554
|
-
function readJUnit(source, file) {
|
|
4178
|
+
function readJUnit(source, file, missing = [], notes = []) {
|
|
3555
4179
|
const doc = parseXml(source, file);
|
|
4180
|
+
const { lexical, real: folder } = reportFolders(file);
|
|
3556
4181
|
const found = [];
|
|
3557
4182
|
locate(doc, void 0, found);
|
|
3558
4183
|
return found.map(({ test, suite }) => {
|
|
@@ -3560,36 +4185,41 @@ function readJUnit(source, file) {
|
|
|
3560
4185
|
const classname = test.attrs["classname"] ?? test.attrs["class"] ?? "";
|
|
3561
4186
|
const { status, detail } = outcomeOf(test);
|
|
3562
4187
|
const playwright = playwrightOf(classname, name);
|
|
3563
|
-
const key =
|
|
3564
|
-
const stated =
|
|
4188
|
+
const key = keyOf2(classname, name, playwright);
|
|
4189
|
+
const stated = TOKEN2.exec(playwright?.titles.at(-1) ?? name)?.[1];
|
|
3565
4190
|
const specRef = specRefOf(test, suite, classname);
|
|
3566
4191
|
const errorContext = errorTextOf(detail);
|
|
3567
4192
|
const execution = executionOf(test, suite);
|
|
4193
|
+
const title = titleOf(classname, name);
|
|
4194
|
+
const rkey = resultKey(key.value, 0);
|
|
4195
|
+
const files = filesOfTest(test, folder, title, missing, { lexical, resultKey: rkey, notes });
|
|
3568
4196
|
return {
|
|
3569
|
-
resultKey:
|
|
4197
|
+
resultKey: rkey,
|
|
3570
4198
|
...stated !== void 0 ? { testCaseId: stated } : {},
|
|
3571
4199
|
keys: [key],
|
|
3572
|
-
source:
|
|
4200
|
+
source: SOURCE2,
|
|
3573
4201
|
rawStatus: status,
|
|
3574
|
-
title
|
|
4202
|
+
title,
|
|
3575
4203
|
...specRef !== void 0 ? { specRef } : {},
|
|
3576
4204
|
...execution !== void 0 ? { execution } : {},
|
|
3577
|
-
...errorContext !== "" ? { errorContext } : {}
|
|
4205
|
+
...errorContext !== "" ? { errorContext } : {},
|
|
4206
|
+
...files.length > 0 ? { files } : {}
|
|
3578
4207
|
};
|
|
3579
4208
|
});
|
|
3580
4209
|
}
|
|
3581
4210
|
function looksLikeJUnit(source) {
|
|
3582
4211
|
return /<\s*testsuites?\b/.test(source.slice(0, 4096));
|
|
3583
4212
|
}
|
|
3584
|
-
var
|
|
4213
|
+
var SOURCE2, TOKEN2, SPEC_FILE;
|
|
3585
4214
|
var init_junit = __esm({
|
|
3586
4215
|
"src/importers/junit.ts"() {
|
|
3587
4216
|
"use strict";
|
|
3588
4217
|
init_cjs_shims();
|
|
3589
4218
|
init_result_key();
|
|
4219
|
+
init_attachments();
|
|
3590
4220
|
init_xml();
|
|
3591
|
-
|
|
3592
|
-
|
|
4221
|
+
SOURCE2 = "junit";
|
|
4222
|
+
TOKEN2 = /@P([A-Za-z0-9_-]+)/;
|
|
3593
4223
|
SPEC_FILE = /\.(?:spec|test)\.[cm]?[jt]sx?$/;
|
|
3594
4224
|
}
|
|
3595
4225
|
});
|
|
@@ -3598,7 +4228,7 @@ var init_junit = __esm({
|
|
|
3598
4228
|
function statedIdOf(test, title) {
|
|
3599
4229
|
const annotation = test.annotations?.find((a) => a.type === "PluneId")?.description;
|
|
3600
4230
|
if (annotation !== void 0 && annotation !== "") return annotation;
|
|
3601
|
-
return
|
|
4231
|
+
return TOKEN3.exec(title)?.[1];
|
|
3602
4232
|
}
|
|
3603
4233
|
function attemptOf(result, file, ctx) {
|
|
3604
4234
|
const steps = (list2) => (list2 ?? []).map((s) => ({ title: s.title ?? "", failed: s.error !== void 0, steps: steps(s.steps) }));
|
|
@@ -3609,10 +4239,10 @@ function attemptOf(result, file, ctx) {
|
|
|
3609
4239
|
...typeof at?.file === "string" && typeof at.line === "number" ? { location: { file: at.file, line: at.line, ...typeof at.column === "number" ? { column: at.column } : {} } } : {}
|
|
3610
4240
|
})),
|
|
3611
4241
|
steps: steps(result.steps),
|
|
3612
|
-
attachments: (result.attachments ?? []).map(({ name, contentType, path:
|
|
4242
|
+
attachments: (result.attachments ?? []).map(({ name, contentType, path: path17, body }) => ({
|
|
3613
4243
|
name: name ?? "",
|
|
3614
4244
|
...contentType !== void 0 ? { contentType } : {},
|
|
3615
|
-
...
|
|
4245
|
+
...path17 !== void 0 ? { path: path17 } : {},
|
|
3616
4246
|
// Decoded only where the platform would take it (plune-ai/plune#928): a report of screenshots kept as
|
|
3617
4247
|
// bodies is megabytes of base64 that nothing here will read.
|
|
3618
4248
|
...typeof body === "string" && isTextType(contentType) ? { body: Buffer.from(body, "base64") } : {}
|
|
@@ -3645,7 +4275,7 @@ function pendingFrom(spec, test, result, file, titles, ctx) {
|
|
|
3645
4275
|
keys,
|
|
3646
4276
|
title: titles.join(" \u203A "),
|
|
3647
4277
|
specRef: spec.line !== void 0 ? `${file}:${spec.line}` : file,
|
|
3648
|
-
source:
|
|
4278
|
+
source: SOURCE3,
|
|
3649
4279
|
// Untouched, as everywhere: `timedOut` means what the project's map says it means, not what
|
|
3650
4280
|
// this file decided.
|
|
3651
4281
|
rawStatus: result.status ?? "unknown",
|
|
@@ -3680,7 +4310,7 @@ function readPlaywrightJson(source, file) {
|
|
|
3680
4310
|
try {
|
|
3681
4311
|
parsed = JSON.parse(source);
|
|
3682
4312
|
} catch (err) {
|
|
3683
|
-
throw new JsonReportError(file, `not JSON \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
4313
|
+
throw new JsonReportError(file, `not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
|
|
3684
4314
|
}
|
|
3685
4315
|
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.suites)) {
|
|
3686
4316
|
throw new JsonReportError(file, 'no "suites" array \u2014 is this a Playwright JSON report?');
|
|
@@ -3688,7 +4318,7 @@ function readPlaywrightJson(source, file) {
|
|
|
3688
4318
|
const { config, suites } = parsed;
|
|
3689
4319
|
const rootDir = typeof config?.rootDir === "string" ? config.rootDir : void 0;
|
|
3690
4320
|
const buildHref = config?.metadata?.ci?.buildHref;
|
|
3691
|
-
const repoRoot = rootDir !== void 0 && (0,
|
|
4321
|
+
const repoRoot = rootDir !== void 0 && (0, import_node_fs5.existsSync)(rootDir) ? repoRootOf(rootDir) : void 0;
|
|
3692
4322
|
const ctx = {
|
|
3693
4323
|
...rootDir !== void 0 ? { rootDir } : {},
|
|
3694
4324
|
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
@@ -3707,20 +4337,20 @@ function readPlaywrightJson(source, file) {
|
|
|
3707
4337
|
function looksLikePlaywrightJson(source) {
|
|
3708
4338
|
return /^\s*\{/.test(source) && source.includes('"suites"');
|
|
3709
4339
|
}
|
|
3710
|
-
var
|
|
4340
|
+
var import_node_fs5, SOURCE3, TOKEN3, NOT_FAILED, JsonReportError;
|
|
3711
4341
|
var init_playwright_json = __esm({
|
|
3712
4342
|
"src/importers/playwright-json.ts"() {
|
|
3713
4343
|
"use strict";
|
|
3714
4344
|
init_cjs_shims();
|
|
3715
|
-
|
|
4345
|
+
import_node_fs5 = require("fs");
|
|
3716
4346
|
init_failure_detail();
|
|
3717
4347
|
init_result_key();
|
|
3718
|
-
|
|
3719
|
-
|
|
4348
|
+
SOURCE3 = "playwright";
|
|
4349
|
+
TOKEN3 = /@P([A-Za-z0-9_-]+)/;
|
|
3720
4350
|
NOT_FAILED = /* @__PURE__ */ new Set(["passed", "skipped"]);
|
|
3721
4351
|
JsonReportError = class extends Error {
|
|
3722
4352
|
constructor(file, detail) {
|
|
3723
|
-
super(`${file} \u2014 ${detail}`);
|
|
4353
|
+
super(`${printable(file)} \u2014 ${detail}`);
|
|
3724
4354
|
this.file = file;
|
|
3725
4355
|
this.name = "JsonReportError";
|
|
3726
4356
|
}
|
|
@@ -3732,11 +4362,14 @@ var init_playwright_json = __esm({
|
|
|
3732
4362
|
// src/importers/index.ts
|
|
3733
4363
|
var importers_exports = {};
|
|
3734
4364
|
__export(importers_exports, {
|
|
4365
|
+
AllureReportError: () => AllureReportError,
|
|
3735
4366
|
IMPORT_FORMATS: () => IMPORT_FORMATS,
|
|
3736
4367
|
JsonReportError: () => JsonReportError,
|
|
3737
4368
|
UnknownFormatError: () => UnknownFormatError,
|
|
3738
4369
|
XmlParseError: () => XmlParseError,
|
|
3739
4370
|
detectFormat: () => detectFormat,
|
|
4371
|
+
isFolder: () => isFolder,
|
|
4372
|
+
readFolder: () => readFolder,
|
|
3740
4373
|
readReport: () => readReport
|
|
3741
4374
|
});
|
|
3742
4375
|
function detectFormat(source, file) {
|
|
@@ -3747,21 +4380,34 @@ function detectFormat(source, file) {
|
|
|
3747
4380
|
'no <testsuite> and no Playwright "suites" \u2014 this is not a report we can read.'
|
|
3748
4381
|
);
|
|
3749
4382
|
}
|
|
4383
|
+
function readFolder(dir, format) {
|
|
4384
|
+
if (format !== void 0 && format !== "allure") {
|
|
4385
|
+
throw new UnknownFormatError(dir, `this is a folder, and only allure-results is read from one, not ${format}.`);
|
|
4386
|
+
}
|
|
4387
|
+
const missing = [];
|
|
4388
|
+
const notes = [];
|
|
4389
|
+
return { format: "allure", results: readAllure(dir, missing, notes), missing, notes };
|
|
4390
|
+
}
|
|
3750
4391
|
function readReport(source, file, format) {
|
|
3751
4392
|
const chosen2 = format ?? detectFormat(source, file);
|
|
3752
|
-
|
|
3753
|
-
|
|
4393
|
+
const missing = [];
|
|
4394
|
+
if (chosen2 === "allure") throw new UnknownFormatError(file, "allure-results is a folder, and this is a file.");
|
|
4395
|
+
const notes = [];
|
|
4396
|
+
if (chosen2 === "junit") return { format: chosen2, results: readJUnit(source, file, missing, notes), missing, notes };
|
|
4397
|
+
return { format: chosen2, ...readPlaywrightJson(source, file), missing, notes };
|
|
3754
4398
|
}
|
|
3755
4399
|
var IMPORT_FORMATS, UnknownFormatError;
|
|
3756
4400
|
var init_importers = __esm({
|
|
3757
4401
|
"src/importers/index.ts"() {
|
|
3758
4402
|
"use strict";
|
|
3759
4403
|
init_cjs_shims();
|
|
4404
|
+
init_allure();
|
|
3760
4405
|
init_junit();
|
|
3761
4406
|
init_playwright_json();
|
|
3762
4407
|
init_xml();
|
|
3763
4408
|
init_playwright_json();
|
|
3764
|
-
|
|
4409
|
+
init_allure();
|
|
4410
|
+
IMPORT_FORMATS = ["junit", "playwright-json", "allure"];
|
|
3765
4411
|
UnknownFormatError = class extends Error {
|
|
3766
4412
|
constructor(file, detail) {
|
|
3767
4413
|
super(`${file} \u2014 ${detail} Say which with --format ${IMPORT_FORMATS.join(" | ")}.`);
|
|
@@ -3779,16 +4425,25 @@ __export(run_import_exports, {
|
|
|
3779
4425
|
async function handleRunImport(options) {
|
|
3780
4426
|
const write = options.write ?? ((line) => void process.stdout.write(`${line}
|
|
3781
4427
|
`));
|
|
3782
|
-
let
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
}
|
|
3786
|
-
|
|
4428
|
+
let read;
|
|
4429
|
+
if (isFolder(options.file)) {
|
|
4430
|
+
read = readFolder(options.file, options.format);
|
|
4431
|
+
} else {
|
|
4432
|
+
let source;
|
|
4433
|
+
try {
|
|
4434
|
+
source = fs10.readFileSync(options.file, "utf8");
|
|
4435
|
+
} catch {
|
|
4436
|
+
throw new RunCommandError(`Cannot read ${options.file}.`);
|
|
4437
|
+
}
|
|
4438
|
+
read = readReport(source, options.file, options.format);
|
|
3787
4439
|
}
|
|
3788
|
-
const { format, results, ciUrl, ci } =
|
|
4440
|
+
const { format, results, missing, notes, ciUrl, ci } = read;
|
|
3789
4441
|
if (results.length === 0) {
|
|
3790
4442
|
throw new RunCommandError(`No test cases in ${options.file} \u2014 it parsed, but there is nothing in it.`);
|
|
3791
4443
|
}
|
|
4444
|
+
for (const m of missing) write(`plune: attachment not found \u2014 ${JSON.stringify(printable(m.title))}: ${printable(m.path)}`);
|
|
4445
|
+
for (const note of notes) write(note);
|
|
4446
|
+
const announced = (format === "junit" || format === "allure") && (missing.length > 0 || results.some((r) => (r.files?.length ?? 0) > 0));
|
|
3792
4447
|
const env = readEnv();
|
|
3793
4448
|
const apiUrl = resolveApiUrl(options.apiUrl ?? env.config.apiUrl);
|
|
3794
4449
|
const token = options.token ?? env.config.token ?? loadToken() ?? "";
|
|
@@ -3833,6 +4488,13 @@ async function handleRunImport(options) {
|
|
|
3833
4488
|
deferred: stats.deferred,
|
|
3834
4489
|
screenshots: { ...stats.screenshots },
|
|
3835
4490
|
textFiles: { ...stats.textFiles },
|
|
4491
|
+
missingAttachments: missing,
|
|
4492
|
+
filesUploaded: stats.screenshots.uploaded + stats.textFiles.uploaded + stats.otherFiles.uploaded,
|
|
4493
|
+
announced,
|
|
4494
|
+
filesNotSent: Math.max(
|
|
4495
|
+
0,
|
|
4496
|
+
results.reduce((n, r) => n + (r.files?.length ?? 0), 0) - [stats.screenshots, stats.textFiles, stats.otherFiles].reduce((n, c) => n + c.uploaded + c.skipped + c.failed, 0)
|
|
4497
|
+
),
|
|
3836
4498
|
unlocatable: results.filter((r) => r.specRef === void 0).length,
|
|
3837
4499
|
offering: options.create === true
|
|
3838
4500
|
};
|
|
@@ -3851,21 +4513,11 @@ function describe(result, apiUrl) {
|
|
|
3851
4513
|
// worked (#622).
|
|
3852
4514
|
// `not delivered` always, zero included (#790): a number that only appears when it is bad is one
|
|
3853
4515
|
// nobody learns to look for. The `[0-9]+ unmatched` the workflows read stays whole.
|
|
3854
|
-
`Read ${result.parsed} result(s) from a ${result.format} report: ${result.accepted} accepted, ${result.duplicate} already there, ${result.unresolved} unmatched, ${result.deferred} not delivered.`
|
|
4516
|
+
`Read ${result.parsed} result(s) from ${result.format === "allure" ? "an" : "a"} ${result.format} report: ${result.accepted} accepted, ${result.duplicate} already there, ${result.unresolved} unmatched, ${result.deferred} not delivered.`
|
|
3855
4517
|
];
|
|
3856
4518
|
if (result.conflict > 0 || result.rejected > 0) {
|
|
3857
4519
|
lines.push(`${result.conflict} conflicted and ${result.rejected} were refused.`);
|
|
3858
4520
|
}
|
|
3859
|
-
for (const [counts, noun] of [
|
|
3860
|
-
[result.screenshots, "screenshot"],
|
|
3861
|
-
[result.textFiles, "text file"]
|
|
3862
|
-
]) {
|
|
3863
|
-
if (counts.uploaded + counts.skipped + counts.failed > 0) {
|
|
3864
|
-
lines.push(
|
|
3865
|
-
`${counts.uploaded} ${noun}(s) uploaded to their results` + (counts.skipped > 0 ? `, ${counts.skipped} skipped` : "") + (counts.failed > 0 ? `, ${counts.failed} could not be uploaded` : "") + "."
|
|
3866
|
-
);
|
|
3867
|
-
}
|
|
3868
|
-
}
|
|
3869
4521
|
if (result.deferred > 0) {
|
|
3870
4522
|
lines.push(
|
|
3871
4523
|
`${result.deferred} could not be sent and are in the fallback file \u2014 "plune run report" retries them.`
|
|
@@ -3893,16 +4545,22 @@ function describe(result, apiUrl) {
|
|
|
3893
4545
|
`${result.unlocatable} test(s) carry no file or class in the report, so they cannot be offered for review \u2014 a reviewer would have nowhere to look.`
|
|
3894
4546
|
);
|
|
3895
4547
|
}
|
|
4548
|
+
if (result.announced) {
|
|
4549
|
+
lines.push(
|
|
4550
|
+
`Imported ${result.accepted} results \xB7 ${result.filesUploaded} files uploaded \xB7 ${result.missingAttachments.length} files not found` + (result.filesNotSent > 0 ? ` \xB7 ${result.filesNotSent} files not sent (their result was not stored)` : "")
|
|
4551
|
+
);
|
|
4552
|
+
}
|
|
3896
4553
|
if (result.runId !== null) lines.push(runUrl2(apiUrl, result.runId));
|
|
3897
4554
|
return lines;
|
|
3898
4555
|
}
|
|
3899
|
-
var
|
|
4556
|
+
var fs10, NEXT;
|
|
3900
4557
|
var init_run_import = __esm({
|
|
3901
4558
|
"src/cli/commands/run-import.ts"() {
|
|
3902
4559
|
"use strict";
|
|
3903
4560
|
init_cjs_shims();
|
|
3904
|
-
|
|
4561
|
+
fs10 = __toESM(require("fs"), 1);
|
|
3905
4562
|
init_session();
|
|
4563
|
+
init_failure_detail();
|
|
3906
4564
|
init_importers();
|
|
3907
4565
|
init_run_lifecycle();
|
|
3908
4566
|
init_api_url();
|
|
@@ -3929,10 +4587,10 @@ function isRunResult(v) {
|
|
|
3929
4587
|
}
|
|
3930
4588
|
function handleReport(opts = {}) {
|
|
3931
4589
|
const cwd = opts.cwd ?? process.cwd();
|
|
3932
|
-
const file =
|
|
4590
|
+
const file = path11.join(cwd, ".plune", "last-run.json");
|
|
3933
4591
|
let raw;
|
|
3934
4592
|
try {
|
|
3935
|
-
raw =
|
|
4593
|
+
raw = fs11.readFileSync(file, "utf8");
|
|
3936
4594
|
} catch {
|
|
3937
4595
|
throw new ReportNotFoundError('No saved run found. Run "plune run" first.');
|
|
3938
4596
|
}
|
|
@@ -3947,13 +4605,13 @@ function handleReport(opts = {}) {
|
|
|
3947
4605
|
}
|
|
3948
4606
|
return parsed;
|
|
3949
4607
|
}
|
|
3950
|
-
var
|
|
4608
|
+
var fs11, path11, ReportNotFoundError;
|
|
3951
4609
|
var init_report = __esm({
|
|
3952
4610
|
"src/cli/commands/report.ts"() {
|
|
3953
4611
|
"use strict";
|
|
3954
4612
|
init_cjs_shims();
|
|
3955
|
-
|
|
3956
|
-
|
|
4613
|
+
fs11 = __toESM(require("fs"), 1);
|
|
4614
|
+
path11 = __toESM(require("path"), 1);
|
|
3957
4615
|
ReportNotFoundError = class extends Error {
|
|
3958
4616
|
code = "NO_SAVED_RUN";
|
|
3959
4617
|
constructor(message) {
|
|
@@ -3965,18 +4623,13 @@ var init_report = __esm({
|
|
|
3965
4623
|
});
|
|
3966
4624
|
|
|
3967
4625
|
// src/diff/diff.ts
|
|
3968
|
-
function rowStatus(r) {
|
|
3969
|
-
if (r.error !== void 0) return "errored";
|
|
3970
|
-
if (r.assertions.some((a) => !a.passed)) return "failed";
|
|
3971
|
-
return "passed";
|
|
3972
|
-
}
|
|
3973
4626
|
function evalStatus(e) {
|
|
3974
4627
|
const statuses = e.rows.map(rowStatus);
|
|
3975
4628
|
if (statuses.includes("failed")) return "failed";
|
|
3976
4629
|
if (statuses.includes("errored")) return "errored";
|
|
3977
4630
|
return "passed";
|
|
3978
4631
|
}
|
|
3979
|
-
function
|
|
4632
|
+
function classify2(baseline, current) {
|
|
3980
4633
|
if (current === "errored") return "errored";
|
|
3981
4634
|
if (baseline === "absent") return current === "passed" ? "new-pass" : "new-fail";
|
|
3982
4635
|
if (current === "absent") return "removed";
|
|
@@ -3990,7 +4643,7 @@ function diffRuns(baseline, current) {
|
|
|
3990
4643
|
const evals = ids.map((id) => {
|
|
3991
4644
|
const baselineP = baseMap.get(id) ?? "absent";
|
|
3992
4645
|
const currentP = curMap.get(id) ?? "absent";
|
|
3993
|
-
return { id, status:
|
|
4646
|
+
return { id, status: classify2(baselineP, currentP), baseline: baselineP, current: currentP };
|
|
3994
4647
|
});
|
|
3995
4648
|
const count2 = (s) => evals.filter((e) => e.status === s).length;
|
|
3996
4649
|
const summary = {
|
|
@@ -4010,6 +4663,7 @@ var init_diff = __esm({
|
|
|
4010
4663
|
"src/diff/diff.ts"() {
|
|
4011
4664
|
"use strict";
|
|
4012
4665
|
init_cjs_shims();
|
|
4666
|
+
init_results();
|
|
4013
4667
|
}
|
|
4014
4668
|
});
|
|
4015
4669
|
|
|
@@ -4027,7 +4681,7 @@ function isRunResult2(v) {
|
|
|
4027
4681
|
function loadRunResult(file, label) {
|
|
4028
4682
|
let raw;
|
|
4029
4683
|
try {
|
|
4030
|
-
raw =
|
|
4684
|
+
raw = fs12.readFileSync(file, "utf8");
|
|
4031
4685
|
} catch {
|
|
4032
4686
|
throw new DiffInputError(`Cannot read ${label} run file: ${file}`);
|
|
4033
4687
|
}
|
|
@@ -4047,12 +4701,12 @@ function handleDiff(opts) {
|
|
|
4047
4701
|
const current = loadRunResult(opts.currentPath, "current");
|
|
4048
4702
|
return diffRuns(baseline, current);
|
|
4049
4703
|
}
|
|
4050
|
-
var
|
|
4704
|
+
var fs12, DiffInputError;
|
|
4051
4705
|
var init_diff2 = __esm({
|
|
4052
4706
|
"src/cli/commands/diff.ts"() {
|
|
4053
4707
|
"use strict";
|
|
4054
4708
|
init_cjs_shims();
|
|
4055
|
-
|
|
4709
|
+
fs12 = __toESM(require("fs"), 1);
|
|
4056
4710
|
init_diff();
|
|
4057
4711
|
DiffInputError = class extends Error {
|
|
4058
4712
|
code = "DIFF_INPUT";
|
|
@@ -4177,8 +4831,8 @@ async function runInitWizard(cwd) {
|
|
|
4177
4831
|
process.exit(1);
|
|
4178
4832
|
return;
|
|
4179
4833
|
}
|
|
4180
|
-
const outputPath =
|
|
4181
|
-
if (
|
|
4834
|
+
const outputPath = path12.join(cwd, "plune.yaml");
|
|
4835
|
+
if (fs13.existsSync(outputPath)) {
|
|
4182
4836
|
const overwrite = await clack.confirm({
|
|
4183
4837
|
message: "plune.yaml already exists. Overwrite?"
|
|
4184
4838
|
});
|
|
@@ -4217,18 +4871,18 @@ async function runInitWizard(cwd) {
|
|
|
4217
4871
|
`;
|
|
4218
4872
|
const spin = clack.spinner();
|
|
4219
4873
|
spin.start("Writing plune.yaml\u2026");
|
|
4220
|
-
|
|
4874
|
+
fs13.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
|
|
4221
4875
|
spin.stop("plune.yaml created!");
|
|
4222
4876
|
clack.outro("plune.yaml is ready.");
|
|
4223
4877
|
}
|
|
4224
|
-
var clack,
|
|
4878
|
+
var clack, fs13, path12, import_yaml2;
|
|
4225
4879
|
var init_wizard = __esm({
|
|
4226
4880
|
"src/config/init/wizard.ts"() {
|
|
4227
4881
|
"use strict";
|
|
4228
4882
|
init_cjs_shims();
|
|
4229
4883
|
clack = __toESM(require("@clack/prompts"), 1);
|
|
4230
|
-
|
|
4231
|
-
|
|
4884
|
+
fs13 = __toESM(require("fs"), 1);
|
|
4885
|
+
path12 = __toESM(require("path"), 1);
|
|
4232
4886
|
import_yaml2 = require("yaml");
|
|
4233
4887
|
init_errors();
|
|
4234
4888
|
}
|
|
@@ -4236,21 +4890,21 @@ var init_wizard = __esm({
|
|
|
4236
4890
|
|
|
4237
4891
|
// src/cli/templates/index.ts
|
|
4238
4892
|
function writeTemplateFile(cwd, rel, content, force) {
|
|
4239
|
-
const target =
|
|
4240
|
-
if (!force &&
|
|
4893
|
+
const target = path13.join(cwd, rel);
|
|
4894
|
+
if (!force && fs14.existsSync(target)) {
|
|
4241
4895
|
return "skipped";
|
|
4242
4896
|
}
|
|
4243
|
-
|
|
4244
|
-
|
|
4897
|
+
fs14.mkdirSync(path13.dirname(target), { recursive: true });
|
|
4898
|
+
fs14.writeFileSync(target, content, "utf8");
|
|
4245
4899
|
return "written";
|
|
4246
4900
|
}
|
|
4247
|
-
var
|
|
4901
|
+
var fs14, path13, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
|
|
4248
4902
|
var init_templates = __esm({
|
|
4249
4903
|
"src/cli/templates/index.ts"() {
|
|
4250
4904
|
"use strict";
|
|
4251
4905
|
init_cjs_shims();
|
|
4252
|
-
|
|
4253
|
-
|
|
4906
|
+
fs14 = __toESM(require("fs"), 1);
|
|
4907
|
+
path13 = __toESM(require("path"), 1);
|
|
4254
4908
|
ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
|
|
4255
4909
|
# .env is gitignored; .env.example is safe to commit.
|
|
4256
4910
|
# Plune reads whichever key matches your configured provider.
|
|
@@ -4432,64 +5086,70 @@ var init_logout = __esm({
|
|
|
4432
5086
|
function parseRunResult(value2) {
|
|
4433
5087
|
return runResultSchema.parse(value2);
|
|
4434
5088
|
}
|
|
4435
|
-
var
|
|
5089
|
+
var import_zod3, RUN_RESULT_SCHEMA_VERSION, binaryVerdictSchema, assertionResultRecordSchema2, usageSchema, runErrorSchema, rowResultSchema, summarySchema, evalResultSchema, runResultSchema;
|
|
4436
5090
|
var init_run_result_schema = __esm({
|
|
4437
5091
|
"src/types/run-result-schema.ts"() {
|
|
4438
5092
|
"use strict";
|
|
4439
5093
|
init_cjs_shims();
|
|
4440
|
-
|
|
5094
|
+
import_zod3 = require("zod");
|
|
5095
|
+
init_results();
|
|
4441
5096
|
RUN_RESULT_SCHEMA_VERSION = 1;
|
|
4442
|
-
binaryVerdictSchema =
|
|
4443
|
-
question:
|
|
4444
|
-
passed:
|
|
5097
|
+
binaryVerdictSchema = import_zod3.z.object({
|
|
5098
|
+
question: import_zod3.z.string().min(1),
|
|
5099
|
+
passed: import_zod3.z.boolean()
|
|
4445
5100
|
});
|
|
4446
|
-
|
|
4447
|
-
type:
|
|
4448
|
-
passed:
|
|
4449
|
-
score:
|
|
4450
|
-
reason:
|
|
4451
|
-
binaryVerdicts:
|
|
5101
|
+
assertionResultRecordSchema2 = import_zod3.z.object({
|
|
5102
|
+
type: import_zod3.z.string(),
|
|
5103
|
+
passed: import_zod3.z.boolean(),
|
|
5104
|
+
score: import_zod3.z.number().optional(),
|
|
5105
|
+
reason: import_zod3.z.string().optional(),
|
|
5106
|
+
binaryVerdicts: import_zod3.z.array(binaryVerdictSchema).optional()
|
|
4452
5107
|
});
|
|
4453
|
-
usageSchema =
|
|
4454
|
-
input_tokens:
|
|
4455
|
-
output_tokens:
|
|
4456
|
-
cost_usd:
|
|
5108
|
+
usageSchema = import_zod3.z.object({
|
|
5109
|
+
input_tokens: import_zod3.z.number(),
|
|
5110
|
+
output_tokens: import_zod3.z.number(),
|
|
5111
|
+
cost_usd: import_zod3.z.number()
|
|
4457
5112
|
});
|
|
4458
|
-
runErrorSchema =
|
|
4459
|
-
code:
|
|
4460
|
-
message:
|
|
5113
|
+
runErrorSchema = import_zod3.z.object({
|
|
5114
|
+
code: import_zod3.z.string(),
|
|
5115
|
+
message: import_zod3.z.string()
|
|
4461
5116
|
});
|
|
4462
|
-
rowResultSchema =
|
|
4463
|
-
vars:
|
|
4464
|
-
output:
|
|
4465
|
-
cached:
|
|
5117
|
+
rowResultSchema = import_zod3.z.object({
|
|
5118
|
+
vars: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()),
|
|
5119
|
+
output: import_zod3.z.string().nullable(),
|
|
5120
|
+
cached: import_zod3.z.boolean(),
|
|
4466
5121
|
usage: usageSchema.optional(),
|
|
4467
|
-
latency_ms:
|
|
5122
|
+
latency_ms: import_zod3.z.number().optional(),
|
|
4468
5123
|
error: runErrorSchema.optional(),
|
|
4469
|
-
assertions:
|
|
5124
|
+
assertions: import_zod3.z.array(assertionResultRecordSchema2),
|
|
5125
|
+
/** ADR 0045. Optional and additive — the same enum the platform validates on `POST /v1/runs`. */
|
|
5126
|
+
inconclusive: import_zod3.z.enum(ROW_INCONCLUSIVE).optional()
|
|
4470
5127
|
});
|
|
4471
|
-
summarySchema =
|
|
4472
|
-
total:
|
|
4473
|
-
passed:
|
|
4474
|
-
failed:
|
|
4475
|
-
errored:
|
|
4476
|
-
|
|
4477
|
-
|
|
5128
|
+
summarySchema = import_zod3.z.object({
|
|
5129
|
+
total: import_zod3.z.number(),
|
|
5130
|
+
passed: import_zod3.z.number(),
|
|
5131
|
+
failed: import_zod3.z.number(),
|
|
5132
|
+
errored: import_zod3.z.number(),
|
|
5133
|
+
/** plune#1245. Optional and additive — a RunResult written before them validates unchanged. */
|
|
5134
|
+
blocked: import_zod3.z.number().optional(),
|
|
5135
|
+
skipped: import_zod3.z.number().optional(),
|
|
5136
|
+
cost_usd: import_zod3.z.number(),
|
|
5137
|
+
duration_ms: import_zod3.z.number()
|
|
4478
5138
|
});
|
|
4479
|
-
evalResultSchema =
|
|
4480
|
-
id:
|
|
4481
|
-
tags:
|
|
4482
|
-
rows:
|
|
4483
|
-
passed:
|
|
5139
|
+
evalResultSchema = import_zod3.z.object({
|
|
5140
|
+
id: import_zod3.z.string(),
|
|
5141
|
+
tags: import_zod3.z.array(import_zod3.z.string()),
|
|
5142
|
+
rows: import_zod3.z.array(rowResultSchema),
|
|
5143
|
+
passed: import_zod3.z.boolean()
|
|
4484
5144
|
});
|
|
4485
|
-
runResultSchema =
|
|
4486
|
-
schemaVersion:
|
|
4487
|
-
plune_version:
|
|
4488
|
-
started_at:
|
|
4489
|
-
finished_at:
|
|
4490
|
-
config_hash:
|
|
5145
|
+
runResultSchema = import_zod3.z.object({
|
|
5146
|
+
schemaVersion: import_zod3.z.literal(RUN_RESULT_SCHEMA_VERSION),
|
|
5147
|
+
plune_version: import_zod3.z.string(),
|
|
5148
|
+
started_at: import_zod3.z.string(),
|
|
5149
|
+
finished_at: import_zod3.z.string(),
|
|
5150
|
+
config_hash: import_zod3.z.string(),
|
|
4491
5151
|
summary: summarySchema,
|
|
4492
|
-
evals:
|
|
5152
|
+
evals: import_zod3.z.array(evalResultSchema)
|
|
4493
5153
|
});
|
|
4494
5154
|
}
|
|
4495
5155
|
});
|
|
@@ -4506,10 +5166,10 @@ __export(sync_exports, {
|
|
|
4506
5166
|
reportSyncFailure: () => reportSyncFailure
|
|
4507
5167
|
});
|
|
4508
5168
|
function readRun(cwd, file) {
|
|
4509
|
-
const target = file !== void 0 ?
|
|
5169
|
+
const target = file !== void 0 ? path14.resolve(cwd, file) : path14.join(cwd, ".plune", "last-run.json");
|
|
4510
5170
|
let raw;
|
|
4511
5171
|
try {
|
|
4512
|
-
raw =
|
|
5172
|
+
raw = fs15.readFileSync(target, "utf8");
|
|
4513
5173
|
} catch {
|
|
4514
5174
|
throw new SyncFileError(
|
|
4515
5175
|
file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
|
|
@@ -4573,13 +5233,13 @@ function reportSyncFailure(err, write) {
|
|
|
4573
5233
|
}
|
|
4574
5234
|
return null;
|
|
4575
5235
|
}
|
|
4576
|
-
var
|
|
5236
|
+
var fs15, path14, NotLoggedInError, TokenRejectedError2, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
4577
5237
|
var init_sync = __esm({
|
|
4578
5238
|
"src/cli/commands/sync.ts"() {
|
|
4579
5239
|
"use strict";
|
|
4580
5240
|
init_cjs_shims();
|
|
4581
|
-
|
|
4582
|
-
|
|
5241
|
+
fs15 = __toESM(require("fs"), 1);
|
|
5242
|
+
path14 = __toESM(require("path"), 1);
|
|
4583
5243
|
init_run_result_schema();
|
|
4584
5244
|
init_api_url();
|
|
4585
5245
|
init_credentials();
|
|
@@ -4624,14 +5284,14 @@ var init_sync = __esm({
|
|
|
4624
5284
|
|
|
4625
5285
|
// src/cli/commands/cairn-artifact.ts
|
|
4626
5286
|
async function findLatestCairnRun(runsDir = "runs") {
|
|
4627
|
-
const entries = await
|
|
5287
|
+
const entries = await fs16.readdir(runsDir, { withFileTypes: true }).catch(() => null);
|
|
4628
5288
|
if (entries === null) throw new NoCairnRunsError(runsDir);
|
|
4629
5289
|
let newest;
|
|
4630
5290
|
for (const entry of entries) {
|
|
4631
5291
|
if (!entry.isDirectory()) continue;
|
|
4632
|
-
const dir =
|
|
5292
|
+
const dir = path15.join(runsDir, entry.name);
|
|
4633
5293
|
try {
|
|
4634
|
-
const report = await
|
|
5294
|
+
const report = await fs16.lstat(path15.join(dir, "report.json"));
|
|
4635
5295
|
if (!report.isFile()) continue;
|
|
4636
5296
|
if (newest === void 0 || report.mtimeMs > newest.at) {
|
|
4637
5297
|
newest = { dir, at: report.mtimeMs };
|
|
@@ -4644,13 +5304,13 @@ async function findLatestCairnRun(runsDir = "runs") {
|
|
|
4644
5304
|
}
|
|
4645
5305
|
async function readJson(file) {
|
|
4646
5306
|
try {
|
|
4647
|
-
return JSON.parse(await
|
|
5307
|
+
return JSON.parse(await fs16.readFile(file, "utf8"));
|
|
4648
5308
|
} catch {
|
|
4649
5309
|
return void 0;
|
|
4650
5310
|
}
|
|
4651
5311
|
}
|
|
4652
5312
|
async function defaultReadEvidence(dir) {
|
|
4653
|
-
const raw = await readJson(
|
|
5313
|
+
const raw = await readJson(path15.join(dir, "api-evidence.json"));
|
|
4654
5314
|
if (!Array.isArray(raw)) return void 0;
|
|
4655
5315
|
return raw.map((r) => {
|
|
4656
5316
|
const rec = asRecord(r);
|
|
@@ -4692,7 +5352,7 @@ function casesFromApi(report, verdicts) {
|
|
|
4692
5352
|
});
|
|
4693
5353
|
}
|
|
4694
5354
|
async function readCairnRun(dir, deps = {}) {
|
|
4695
|
-
const raw = await readJson(
|
|
5355
|
+
const raw = await readJson(path15.join(dir, "report.json"));
|
|
4696
5356
|
if (raw === void 0) throw new CairnRunNotFoundError(dir);
|
|
4697
5357
|
const report = asRecord(raw);
|
|
4698
5358
|
if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
|
|
@@ -4722,20 +5382,20 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
4722
5382
|
return {
|
|
4723
5383
|
schemaVersion: INGEST_SCHEMA_VERSION,
|
|
4724
5384
|
run: {
|
|
4725
|
-
runId: str(report["runId"],
|
|
5385
|
+
runId: str(report["runId"], path15.basename(dir)),
|
|
4726
5386
|
mode,
|
|
4727
5387
|
target: str(report["url"], "unknown")
|
|
4728
5388
|
},
|
|
4729
5389
|
cases
|
|
4730
5390
|
};
|
|
4731
5391
|
}
|
|
4732
|
-
var
|
|
5392
|
+
var fs16, path15, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
|
|
4733
5393
|
var init_cairn_artifact = __esm({
|
|
4734
5394
|
"src/cli/commands/cairn-artifact.ts"() {
|
|
4735
5395
|
"use strict";
|
|
4736
5396
|
init_cjs_shims();
|
|
4737
|
-
|
|
4738
|
-
|
|
5397
|
+
fs16 = __toESM(require("fs/promises"), 1);
|
|
5398
|
+
path15 = __toESM(require("path"), 1);
|
|
4739
5399
|
SUPPORTED_ARTIFACT_VERSION = 1;
|
|
4740
5400
|
INGEST_SCHEMA_VERSION = 1;
|
|
4741
5401
|
NoCairnRunsError = class extends Error {
|
|
@@ -4902,7 +5562,7 @@ __export(cases_exports, {
|
|
|
4902
5562
|
function gitDirty(file) {
|
|
4903
5563
|
try {
|
|
4904
5564
|
const out = (0, import_node_child_process2.execFileSync)("git", ["status", "--porcelain", "--", file], {
|
|
4905
|
-
cwd:
|
|
5565
|
+
cwd: path16.dirname(file),
|
|
4906
5566
|
stdio: ["ignore", "pipe", "ignore"],
|
|
4907
5567
|
encoding: "utf8"
|
|
4908
5568
|
});
|
|
@@ -4928,9 +5588,9 @@ async function handlePull(deps = {}) {
|
|
|
4928
5588
|
const cwd = deps.cwd ?? process.cwd();
|
|
4929
5589
|
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4930
5590
|
const token = tokenOf(deps);
|
|
4931
|
-
const file =
|
|
4932
|
-
if (
|
|
4933
|
-
throw new DirtyFileError(
|
|
5591
|
+
const file = path16.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
5592
|
+
if (fs17.existsSync(file) && !deps.force && (deps.isDirty ?? gitDirty)(file))
|
|
5593
|
+
throw new DirtyFileError(path16.relative(cwd, file) || file);
|
|
4934
5594
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
4935
5595
|
const endpoint = deps.suiteId !== void 0 ? `${apiUrl}/v1/suites/${encodeURIComponent(deps.suiteId)}/markdown` : `${apiUrl}/v1/test-cases/markdown`;
|
|
4936
5596
|
let res;
|
|
@@ -4945,18 +5605,18 @@ async function handlePull(deps = {}) {
|
|
|
4945
5605
|
throw new SyncHttpError(res.status, detail);
|
|
4946
5606
|
}
|
|
4947
5607
|
const text2 = await res.text();
|
|
4948
|
-
|
|
4949
|
-
|
|
5608
|
+
fs17.mkdirSync(path16.dirname(file), { recursive: true });
|
|
5609
|
+
fs17.writeFileSync(file, text2, "utf8");
|
|
4950
5610
|
return { file, cases: Number(res.headers.get("x-plune-cases") ?? "0") };
|
|
4951
5611
|
}
|
|
4952
5612
|
async function handlePush(deps = {}) {
|
|
4953
5613
|
const cwd = deps.cwd ?? process.cwd();
|
|
4954
5614
|
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4955
5615
|
const token = tokenOf(deps);
|
|
4956
|
-
const file =
|
|
5616
|
+
const file = path16.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
4957
5617
|
let text2;
|
|
4958
5618
|
try {
|
|
4959
|
-
text2 =
|
|
5619
|
+
text2 = fs17.readFileSync(file, "utf8");
|
|
4960
5620
|
} catch {
|
|
4961
5621
|
throw new SyncFileError(
|
|
4962
5622
|
`No document to push: ${file}. Run "plune pull" first (or pass a file).`
|
|
@@ -5007,18 +5667,18 @@ function reportCasesFailure(err, write) {
|
|
|
5007
5667
|
}
|
|
5008
5668
|
return null;
|
|
5009
5669
|
}
|
|
5010
|
-
var import_node_child_process2,
|
|
5670
|
+
var import_node_child_process2, fs17, path16, DEFAULT_CASES_FILE, DirtyFileError;
|
|
5011
5671
|
var init_cases = __esm({
|
|
5012
5672
|
"src/cli/commands/cases.ts"() {
|
|
5013
5673
|
"use strict";
|
|
5014
5674
|
init_cjs_shims();
|
|
5015
5675
|
import_node_child_process2 = require("child_process");
|
|
5016
|
-
|
|
5017
|
-
|
|
5676
|
+
fs17 = __toESM(require("fs"), 1);
|
|
5677
|
+
path16 = __toESM(require("path"), 1);
|
|
5018
5678
|
init_api_url();
|
|
5019
5679
|
init_credentials();
|
|
5020
5680
|
init_sync();
|
|
5021
|
-
DEFAULT_CASES_FILE =
|
|
5681
|
+
DEFAULT_CASES_FILE = path16.join("plune", "cases.md");
|
|
5022
5682
|
DirtyFileError = class extends Error {
|
|
5023
5683
|
constructor(file) {
|
|
5024
5684
|
super(`${file} has uncommitted changes; commit them or pass --force to overwrite.`);
|
|
@@ -5099,11 +5759,11 @@ module.exports = __toCommonJS(cli_exports);
|
|
|
5099
5759
|
init_cjs_shims();
|
|
5100
5760
|
init_api_url();
|
|
5101
5761
|
var import_commander = require("commander");
|
|
5102
|
-
var
|
|
5762
|
+
var import_node_fs6 = require("fs");
|
|
5103
5763
|
var import_node_path4 = require("path");
|
|
5104
5764
|
var import_node_url = require("url");
|
|
5105
5765
|
var __dirname = (0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
5106
|
-
var pkg = JSON.parse((0,
|
|
5766
|
+
var pkg = JSON.parse((0, import_node_fs6.readFileSync)((0, import_node_path4.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
5107
5767
|
function toFormat(s) {
|
|
5108
5768
|
return s === "json" || s === "markdown" ? s : "console";
|
|
5109
5769
|
}
|
|
@@ -5112,7 +5772,7 @@ function shouldColor(outputPath) {
|
|
|
5112
5772
|
}
|
|
5113
5773
|
function writeReport(text2, outputPath) {
|
|
5114
5774
|
if (outputPath !== void 0) {
|
|
5115
|
-
(0,
|
|
5775
|
+
(0, import_node_fs6.writeFileSync)(outputPath, text2);
|
|
5116
5776
|
} else {
|
|
5117
5777
|
process.stdout.write(text2 + "\n");
|
|
5118
5778
|
}
|
|
@@ -5287,7 +5947,7 @@ function createProgram() {
|
|
|
5287
5947
|
runCommand.command("delete").argument(
|
|
5288
5948
|
"<id>",
|
|
5289
5949
|
"The run id \u2014 the one `plune run import` printed, or the one in the dashboard URL"
|
|
5290
|
-
).description("Delete a run and everything it produced \u2014 recoverable for
|
|
5950
|
+
).description("Delete a run and everything it produced \u2014 recoverable for 30 days").action(async (id, _options, command) => {
|
|
5291
5951
|
try {
|
|
5292
5952
|
const { handleRunDelete: handleRunDelete2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
5293
5953
|
await handleRunDelete2(id);
|
|
@@ -5303,11 +5963,15 @@ function createProgram() {
|
|
|
5303
5963
|
await failRunCommand(err, verboseOf(command));
|
|
5304
5964
|
}
|
|
5305
5965
|
});
|
|
5306
|
-
runCommand.command("import").argument("<file>", "A JUnit XML or Playwright JSON report").description("Turn a report another runner wrote into a run in Plune \u2014 no provider key needed").option("--format <fmt>", "junit | playwright-json; detected from the file when omitted").option("--key <externalKey>", "The key several jobs share so their reports land in one run").option("--create", "Offer tests nothing matched to the review queue", false).
|
|
5966
|
+
runCommand.command("import").argument("<file>", "A JUnit XML or Playwright JSON report, or an Allure allure-results folder").description("Turn a report another runner wrote into a run in Plune \u2014 no provider key needed").option("--format <fmt>", "junit | playwright-json | allure; detected from the file (a folder is allure) when omitted").option("--key <externalKey>", "The key several jobs share so their reports land in one run").option("--create", "Offer tests nothing matched to the review queue", false).option(
|
|
5967
|
+
"--fail-on-missing-attachments",
|
|
5968
|
+
"Exit 1 when a file the report names ([[ATTACHMENT|path]] in JUnit, a source in allure-results) is not there; the results are sent either way",
|
|
5969
|
+
false
|
|
5970
|
+
).action(
|
|
5307
5971
|
async (file, options, command) => {
|
|
5308
5972
|
const [
|
|
5309
5973
|
{ handleRunImport: handleRunImport2 },
|
|
5310
|
-
{ IMPORT_FORMATS: IMPORT_FORMATS2, UnknownFormatError: UnknownFormatError2, XmlParseError: XmlParseError2, JsonReportError: JsonReportError2 }
|
|
5974
|
+
{ IMPORT_FORMATS: IMPORT_FORMATS2, UnknownFormatError: UnknownFormatError2, XmlParseError: XmlParseError2, JsonReportError: JsonReportError2, AllureReportError: AllureReportError2 }
|
|
5311
5975
|
] = await Promise.all([
|
|
5312
5976
|
Promise.resolve().then(() => (init_run_import(), run_import_exports)),
|
|
5313
5977
|
Promise.resolve().then(() => (init_importers(), importers_exports))
|
|
@@ -5323,14 +5987,15 @@ function createProgram() {
|
|
|
5323
5987
|
return;
|
|
5324
5988
|
}
|
|
5325
5989
|
try {
|
|
5326
|
-
await handleRunImport2({
|
|
5990
|
+
const imported = await handleRunImport2({
|
|
5327
5991
|
file,
|
|
5328
5992
|
...format !== void 0 ? { format } : {},
|
|
5329
5993
|
...options.key !== void 0 ? { key: options.key } : {},
|
|
5330
5994
|
...options.create ? { create: true } : {}
|
|
5331
5995
|
});
|
|
5996
|
+
if (options.failOnMissingAttachments && imported.missingAttachments.length > 0) process.exitCode = 1;
|
|
5332
5997
|
} catch (err) {
|
|
5333
|
-
if (err instanceof UnknownFormatError2 || err instanceof XmlParseError2 || err instanceof JsonReportError2) {
|
|
5998
|
+
if (err instanceof UnknownFormatError2 || err instanceof XmlParseError2 || err instanceof JsonReportError2 || err instanceof AllureReportError2) {
|
|
5334
5999
|
process.stderr.write(err.message + "\n");
|
|
5335
6000
|
process.exit(2);
|
|
5336
6001
|
return;
|
|
@@ -5409,10 +6074,10 @@ function createProgram() {
|
|
|
5409
6074
|
const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
5410
6075
|
try {
|
|
5411
6076
|
const token = options.token ?? await readTokenFromStdin();
|
|
5412
|
-
const { path:
|
|
6077
|
+
const { path: path17, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
|
|
5413
6078
|
process.stdout.write(
|
|
5414
|
-
verified ? `Logged in. Token checked against the API and saved to ${
|
|
5415
|
-
` : `Token saved to ${
|
|
6079
|
+
verified ? `Logged in. Token checked against the API and saved to ${path17}
|
|
6080
|
+
` : `Token saved to ${path17} WITHOUT being checked (--skip-verify).
|
|
5416
6081
|
`
|
|
5417
6082
|
);
|
|
5418
6083
|
} catch (err) {
|
|
@@ -5429,9 +6094,9 @@ function createProgram() {
|
|
|
5429
6094
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
5430
6095
|
const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
5431
6096
|
try {
|
|
5432
|
-
const { removed, path:
|
|
6097
|
+
const { removed, path: path17 } = handleLogout2();
|
|
5433
6098
|
process.stdout.write(
|
|
5434
|
-
removed ? `Logged out. Removed ${
|
|
6099
|
+
removed ? `Logged out. Removed ${path17}
|
|
5435
6100
|
` : "Not logged in \u2014 nothing to remove.\n"
|
|
5436
6101
|
);
|
|
5437
6102
|
} catch (err) {
|
|
@@ -5560,7 +6225,7 @@ function isMainModule() {
|
|
|
5560
6225
|
const entry = process.argv[1];
|
|
5561
6226
|
if (entry === void 0) return false;
|
|
5562
6227
|
try {
|
|
5563
|
-
return (0,
|
|
6228
|
+
return (0, import_node_fs6.realpathSync)(entry) === (0, import_node_fs6.realpathSync)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
5564
6229
|
} catch {
|
|
5565
6230
|
return false;
|
|
5566
6231
|
}
|