@plune-ai/cli 0.7.0 → 0.9.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 +29 -0
- package/dist/cli.cjs +622 -36
- package/dist/reporter-core.cjs +68 -8
- package/dist/reporter-core.d.cts +39 -15
- package/dist/reporter-core.d.ts +39 -15
- package/dist/reporter-core.js +68 -8
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -2267,19 +2267,35 @@ function count(env, name) {
|
|
|
2267
2267
|
const n = Number(raw);
|
|
2268
2268
|
return Number.isInteger(n) && n > 0 ? n : void 0;
|
|
2269
2269
|
}
|
|
2270
|
+
function list(env, name) {
|
|
2271
|
+
const raw = value(env, name);
|
|
2272
|
+
if (raw === void 0) return void 0;
|
|
2273
|
+
const items = raw.split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
2274
|
+
return items.length === 0 ? void 0 : items;
|
|
2275
|
+
}
|
|
2270
2276
|
function readEnv(env = process.env) {
|
|
2271
2277
|
const externalKey = value(env, "PLUNE_RUN");
|
|
2272
2278
|
const token = value(env, "PLUNE_TOKEN");
|
|
2273
2279
|
const apiUrl = value(env, "PLUNE_API_URL");
|
|
2274
2280
|
const fallbackPath = value(env, "PLUNE_FALLBACK");
|
|
2275
2281
|
const batchSize = count(env, "PLUNE_BATCH_SIZE");
|
|
2282
|
+
const title = value(env, "PLUNE_RUN_TITLE");
|
|
2283
|
+
const environment = value(env, "PLUNE_ENV");
|
|
2284
|
+
const labels = list(env, "PLUNE_LABELS");
|
|
2285
|
+
const offerDiscovered = flag(env, "PLUNE_CREATE");
|
|
2276
2286
|
return {
|
|
2277
2287
|
config: {
|
|
2278
2288
|
...externalKey !== void 0 ? { externalKey } : {},
|
|
2279
2289
|
...token !== void 0 ? { token } : {},
|
|
2280
2290
|
...apiUrl !== void 0 ? { apiUrl } : {},
|
|
2281
2291
|
...fallbackPath !== void 0 ? { fallbackPath } : {},
|
|
2282
|
-
...batchSize !== void 0 ? { batchSize } : {}
|
|
2292
|
+
...batchSize !== void 0 ? { batchSize } : {},
|
|
2293
|
+
...title !== void 0 ? { title } : {},
|
|
2294
|
+
...environment !== void 0 ? { environment } : {},
|
|
2295
|
+
...labels !== void 0 ? { labels } : {},
|
|
2296
|
+
// Only when set: `false` here would outrank a config file that asked for it, and an unset
|
|
2297
|
+
// variable is not an instruction.
|
|
2298
|
+
...offerDiscovered ? { offerDiscovered } : {}
|
|
2283
2299
|
},
|
|
2284
2300
|
// Two names for two situations that end the same way. `SHARED_RUN` says other processes are
|
|
2285
2301
|
// reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
|
|
@@ -2293,12 +2309,7 @@ var init_env2 = __esm({
|
|
|
2293
2309
|
"src/reporter-core/env.ts"() {
|
|
2294
2310
|
"use strict";
|
|
2295
2311
|
init_cjs_shims();
|
|
2296
|
-
UNSUPPORTED_VARS = [
|
|
2297
|
-
"PLUNE_RUN_TITLE",
|
|
2298
|
-
"PLUNE_ENV",
|
|
2299
|
-
"PLUNE_LABELS",
|
|
2300
|
-
"PLUNE_GROUP"
|
|
2301
|
-
];
|
|
2312
|
+
UNSUPPORTED_VARS = ["PLUNE_GROUP"];
|
|
2302
2313
|
}
|
|
2303
2314
|
});
|
|
2304
2315
|
|
|
@@ -2331,6 +2342,14 @@ function chunk(items, size) {
|
|
|
2331
2342
|
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
2332
2343
|
return out;
|
|
2333
2344
|
}
|
|
2345
|
+
function warnIfDropped(sent, run, log) {
|
|
2346
|
+
const names = Object.keys(sent);
|
|
2347
|
+
if (names.length === 0) return;
|
|
2348
|
+
if (names.some((name) => run[name] != null)) return;
|
|
2349
|
+
log(
|
|
2350
|
+
`plune: this deployment stored none of ${names.join(", ")} \u2014 it is older than the fields. The run is otherwise reported in full.`
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2334
2353
|
async function startRun(passed, expected) {
|
|
2335
2354
|
const env = readEnv();
|
|
2336
2355
|
const stated = Object.fromEntries(Object.entries(passed).filter(([, v]) => v !== void 0));
|
|
@@ -2340,6 +2359,7 @@ async function startRun(passed, expected) {
|
|
|
2340
2359
|
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
2341
2360
|
const externalKey = cfg.externalKey ?? null;
|
|
2342
2361
|
const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
|
|
2362
|
+
const offerDiscovered = cfg.offerDiscovered ?? false;
|
|
2343
2363
|
const token = cfg.token ?? loadToken() ?? "";
|
|
2344
2364
|
const client = createClient({
|
|
2345
2365
|
apiUrl: resolveApiUrl(cfg.apiUrl),
|
|
@@ -2352,10 +2372,12 @@ async function startRun(passed, expected) {
|
|
|
2352
2372
|
conflict: 0,
|
|
2353
2373
|
rejected: 0,
|
|
2354
2374
|
unresolved: 0,
|
|
2375
|
+
offered: 0,
|
|
2355
2376
|
deferred: 0
|
|
2356
2377
|
};
|
|
2357
2378
|
const resolved = /* @__PURE__ */ new Map();
|
|
2358
2379
|
let buffer = [];
|
|
2380
|
+
const discoveries = /* @__PURE__ */ new Map();
|
|
2359
2381
|
let runId = null;
|
|
2360
2382
|
let joined = false;
|
|
2361
2383
|
let offline = token === "";
|
|
@@ -2398,16 +2420,23 @@ async function startRun(passed, expected) {
|
|
|
2398
2420
|
if (expected !== void 0 && expected.length > EXPECTED_MAX) {
|
|
2399
2421
|
log(`plune: ${expected.length} tests is more than the run configuration holds \u2014 reporting the first ${EXPECTED_MAX}.`);
|
|
2400
2422
|
}
|
|
2423
|
+
const described = {
|
|
2424
|
+
...cfg.title !== void 0 ? { title: cfg.title } : {},
|
|
2425
|
+
...cfg.environment !== void 0 ? { environment: cfg.environment } : {},
|
|
2426
|
+
...cfg.labels !== void 0 ? { labels: cfg.labels } : {}
|
|
2427
|
+
};
|
|
2401
2428
|
const start = await client.post("/v1/runs", {
|
|
2402
2429
|
schemaVersion: 2,
|
|
2403
2430
|
kind: cfg.kind ?? "automated",
|
|
2404
2431
|
...externalKey !== null ? { externalKey } : {},
|
|
2405
2432
|
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
2406
|
-
...configuration !== void 0 ? { configuration } : {}
|
|
2433
|
+
...configuration !== void 0 ? { configuration } : {},
|
|
2434
|
+
...described
|
|
2407
2435
|
});
|
|
2408
2436
|
if (start.ok) {
|
|
2409
2437
|
runId = start.body.run.id;
|
|
2410
2438
|
joined = start.status === 200;
|
|
2439
|
+
warnIfDropped(described, start.body.run, log);
|
|
2411
2440
|
} else {
|
|
2412
2441
|
log(`plune: could not start the run (${start.detail || start.kind}). Results will be written to ${fallbackPath}.`);
|
|
2413
2442
|
}
|
|
@@ -2422,6 +2451,32 @@ async function startRun(passed, expected) {
|
|
|
2422
2451
|
}
|
|
2423
2452
|
return unanswered ? { kind: "unanswered" } : { kind: "unmatched" };
|
|
2424
2453
|
}
|
|
2454
|
+
function remember(result) {
|
|
2455
|
+
if (!offerDiscovered) return;
|
|
2456
|
+
if (result.title === void 0 || result.specRef === void 0) return;
|
|
2457
|
+
if (result.keys.length === 0) return;
|
|
2458
|
+
discoveries.set(result.keys[0].value, {
|
|
2459
|
+
keys: result.keys,
|
|
2460
|
+
title: result.title,
|
|
2461
|
+
source: result.source,
|
|
2462
|
+
specRef: result.specRef,
|
|
2463
|
+
rawStatus: result.rawStatus
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
async function offer() {
|
|
2467
|
+
if (discoveries.size === 0 || offline || runId === null) return;
|
|
2468
|
+
for (const batch of chunk([...discoveries.values()], DISCOVER_MAX)) {
|
|
2469
|
+
const out = await client.post("/v1/review-items/discovered", {
|
|
2470
|
+
discovered: batch,
|
|
2471
|
+
runId
|
|
2472
|
+
});
|
|
2473
|
+
if (!out.ok) {
|
|
2474
|
+
log(`plune: could not offer ${batch.length} unknown tests for review (${out.detail || out.kind}).`);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
stats.offered += out.body.results.filter((r) => r.outcome === "queued").length;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2425
2480
|
async function flush() {
|
|
2426
2481
|
if (buffer.length === 0) return;
|
|
2427
2482
|
const pending = buffer;
|
|
@@ -2440,6 +2495,7 @@ async function startRun(passed, expected) {
|
|
|
2440
2495
|
submissions.push({ ...rest, testCaseId: match.testCaseId });
|
|
2441
2496
|
} else if (match.kind === "unmatched") {
|
|
2442
2497
|
stats.unresolved += 1;
|
|
2498
|
+
remember(result);
|
|
2443
2499
|
} else {
|
|
2444
2500
|
unanswered.push(result);
|
|
2445
2501
|
}
|
|
@@ -2473,6 +2529,7 @@ async function startRun(passed, expected) {
|
|
|
2473
2529
|
if (stats.conflict > 0) parts.push(`${stats.conflict} conflicting`);
|
|
2474
2530
|
if (stats.rejected > 0) parts.push(`${stats.rejected} rejected`);
|
|
2475
2531
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
2532
|
+
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
2476
2533
|
if (stats.deferred > 0) parts.push(`${stats.deferred} written to ${fallbackPath}`);
|
|
2477
2534
|
log(`plune: ${parts.join(" \xB7 ")}`);
|
|
2478
2535
|
}
|
|
@@ -2495,6 +2552,7 @@ async function startRun(passed, expected) {
|
|
|
2495
2552
|
if (done) return;
|
|
2496
2553
|
done = true;
|
|
2497
2554
|
await flush();
|
|
2555
|
+
await offer();
|
|
2498
2556
|
if (runId !== null && !offline) {
|
|
2499
2557
|
const out = await client.post(`/v1/runs/${runId}/events`, {
|
|
2500
2558
|
event: "finish",
|
|
@@ -2508,6 +2566,7 @@ async function startRun(passed, expected) {
|
|
|
2508
2566
|
if (done) return;
|
|
2509
2567
|
done = true;
|
|
2510
2568
|
await flush();
|
|
2569
|
+
await offer();
|
|
2511
2570
|
if (runId !== null) {
|
|
2512
2571
|
log(`plune: run ${runId} is still open \u2014 close it with "plune run finish ${runId}".`);
|
|
2513
2572
|
}
|
|
@@ -2515,7 +2574,7 @@ async function startRun(passed, expected) {
|
|
|
2515
2574
|
}
|
|
2516
2575
|
};
|
|
2517
2576
|
}
|
|
2518
|
-
var RESOLVE_CHUNK, BATCH_MAX, BATCH_DEFAULT, EXPECTED_MAX;
|
|
2577
|
+
var RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX;
|
|
2519
2578
|
var init_session = __esm({
|
|
2520
2579
|
"src/reporter-core/session.ts"() {
|
|
2521
2580
|
"use strict";
|
|
@@ -2527,6 +2586,7 @@ var init_session = __esm({
|
|
|
2527
2586
|
init_fallback();
|
|
2528
2587
|
RESOLVE_CHUNK = 500;
|
|
2529
2588
|
BATCH_MAX = 500;
|
|
2589
|
+
DISCOVER_MAX = 500;
|
|
2530
2590
|
BATCH_DEFAULT = 100;
|
|
2531
2591
|
EXPECTED_MAX = 1e4;
|
|
2532
2592
|
}
|
|
@@ -2689,6 +2749,502 @@ var init_run_lifecycle = __esm({
|
|
|
2689
2749
|
}
|
|
2690
2750
|
});
|
|
2691
2751
|
|
|
2752
|
+
// src/reporter-core/result-key.ts
|
|
2753
|
+
function resultKey(testId, retry) {
|
|
2754
|
+
const plain = `${testId}#${retry}`;
|
|
2755
|
+
return plain.length <= RESULT_KEY_MAX ? plain : `${sha256(testId).slice(0, HASH_CHARS)}#${retry}`;
|
|
2756
|
+
}
|
|
2757
|
+
var RESULT_KEY_MAX, HASH_CHARS;
|
|
2758
|
+
var init_result_key = __esm({
|
|
2759
|
+
"src/reporter-core/result-key.ts"() {
|
|
2760
|
+
"use strict";
|
|
2761
|
+
init_cjs_shims();
|
|
2762
|
+
init_hash();
|
|
2763
|
+
RESULT_KEY_MAX = 200;
|
|
2764
|
+
HASH_CHARS = 32;
|
|
2765
|
+
}
|
|
2766
|
+
});
|
|
2767
|
+
|
|
2768
|
+
// src/importers/xml.ts
|
|
2769
|
+
function decode(raw) {
|
|
2770
|
+
if (!raw.includes("&")) return raw;
|
|
2771
|
+
return raw.replace(/&(#[xX][0-9a-fA-F]+|#[0-9]+|[a-zA-Z][a-zA-Z0-9]*);/g, (whole, body) => {
|
|
2772
|
+
if (body.startsWith("#")) {
|
|
2773
|
+
const hex = body[1] === "x" || body[1] === "X";
|
|
2774
|
+
const code = Number.parseInt(hex ? body.slice(2) : body.slice(1), hex ? 16 : 10);
|
|
2775
|
+
try {
|
|
2776
|
+
return Number.isNaN(code) ? whole : String.fromCodePoint(code);
|
|
2777
|
+
} catch {
|
|
2778
|
+
return whole;
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
return NAMED[body] ?? whole;
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2784
|
+
function parseXml(source, file) {
|
|
2785
|
+
const root = { name: "#document", attrs: {}, children: [], text: "", line: 1 };
|
|
2786
|
+
const stack = [root];
|
|
2787
|
+
let i = 0;
|
|
2788
|
+
let line = 1;
|
|
2789
|
+
const advance = (to) => {
|
|
2790
|
+
for (let j = i; j < to; j++) if (source[j] === "\n") line += 1;
|
|
2791
|
+
i = to;
|
|
2792
|
+
};
|
|
2793
|
+
const top = () => stack[stack.length - 1];
|
|
2794
|
+
while (i < source.length) {
|
|
2795
|
+
const lt = source.indexOf("<", i);
|
|
2796
|
+
if (lt === -1) break;
|
|
2797
|
+
if (lt > i) {
|
|
2798
|
+
const between = source.slice(i, lt);
|
|
2799
|
+
if (between.trim() !== "") top().text += decode(between);
|
|
2800
|
+
advance(lt);
|
|
2801
|
+
}
|
|
2802
|
+
if (source.startsWith("<!--", i)) {
|
|
2803
|
+
const end = source.indexOf("-->", i + 4);
|
|
2804
|
+
if (end === -1) throw new XmlParseError(file, line, "a comment is never closed");
|
|
2805
|
+
advance(end + 3);
|
|
2806
|
+
continue;
|
|
2807
|
+
}
|
|
2808
|
+
if (source.startsWith("<![CDATA[", i)) {
|
|
2809
|
+
const end = source.indexOf("]]>", i + 9);
|
|
2810
|
+
if (end === -1) throw new XmlParseError(file, line, "a CDATA section is never closed");
|
|
2811
|
+
top().text += source.slice(i + 9, end);
|
|
2812
|
+
advance(end + 3);
|
|
2813
|
+
continue;
|
|
2814
|
+
}
|
|
2815
|
+
if (source.startsWith("<?", i)) {
|
|
2816
|
+
const end = source.indexOf("?>", i + 2);
|
|
2817
|
+
if (end === -1) throw new XmlParseError(file, line, "a processing instruction is never closed");
|
|
2818
|
+
advance(end + 2);
|
|
2819
|
+
continue;
|
|
2820
|
+
}
|
|
2821
|
+
if (source.startsWith("<!", i)) {
|
|
2822
|
+
const end = source.indexOf(">", i + 2);
|
|
2823
|
+
if (end === -1) throw new XmlParseError(file, line, "a declaration is never closed");
|
|
2824
|
+
advance(end + 1);
|
|
2825
|
+
continue;
|
|
2826
|
+
}
|
|
2827
|
+
if (source.startsWith("</", i)) {
|
|
2828
|
+
const end = source.indexOf(">", i + 2);
|
|
2829
|
+
if (end === -1) throw new XmlParseError(file, line, "a closing tag is never closed");
|
|
2830
|
+
const name2 = source.slice(i + 2, end).trim();
|
|
2831
|
+
const open = top();
|
|
2832
|
+
if (open === root) throw new XmlParseError(file, line, `</${name2}> closes nothing`);
|
|
2833
|
+
if (open.name !== name2) {
|
|
2834
|
+
throw new XmlParseError(file, line, `</${name2}> closes <${open.name}>, opened on line ${open.line}`);
|
|
2835
|
+
}
|
|
2836
|
+
stack.pop();
|
|
2837
|
+
advance(end + 1);
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
const startLine = line;
|
|
2841
|
+
let j = i + 1;
|
|
2842
|
+
const nameStart = j;
|
|
2843
|
+
while (j < source.length && !NAME_END.test(source[j])) j += 1;
|
|
2844
|
+
const name = source.slice(nameStart, j);
|
|
2845
|
+
if (name === "") throw new XmlParseError(file, startLine, "a tag with no name");
|
|
2846
|
+
const el = { name, attrs: {}, children: [], text: "", line: startLine };
|
|
2847
|
+
let selfClosing = false;
|
|
2848
|
+
for (; ; ) {
|
|
2849
|
+
while (j < source.length && SPACE.test(source[j])) j += 1;
|
|
2850
|
+
if (j >= source.length) throw new XmlParseError(file, startLine, `<${name}> is never closed`);
|
|
2851
|
+
if (source[j] === "/" && source[j + 1] === ">") {
|
|
2852
|
+
selfClosing = true;
|
|
2853
|
+
j += 2;
|
|
2854
|
+
break;
|
|
2855
|
+
}
|
|
2856
|
+
if (source[j] === ">") {
|
|
2857
|
+
j += 1;
|
|
2858
|
+
break;
|
|
2859
|
+
}
|
|
2860
|
+
const attrStart = j;
|
|
2861
|
+
while (j < source.length && !ATTR_NAME_END.test(source[j])) j += 1;
|
|
2862
|
+
const attr = source.slice(attrStart, j);
|
|
2863
|
+
if (attr === "") throw new XmlParseError(file, startLine, `<${name}> has an attribute with no name`);
|
|
2864
|
+
while (j < source.length && SPACE.test(source[j])) j += 1;
|
|
2865
|
+
if (source[j] !== "=") {
|
|
2866
|
+
throw new XmlParseError(file, startLine, `<${name}> attribute "${attr}" has no value`);
|
|
2867
|
+
}
|
|
2868
|
+
j += 1;
|
|
2869
|
+
while (j < source.length && SPACE.test(source[j])) j += 1;
|
|
2870
|
+
const quote = source[j];
|
|
2871
|
+
if (quote !== '"' && quote !== "'") {
|
|
2872
|
+
throw new XmlParseError(file, startLine, `<${name}> attribute "${attr}" is not quoted`);
|
|
2873
|
+
}
|
|
2874
|
+
const close = source.indexOf(quote, j + 1);
|
|
2875
|
+
if (close === -1) throw new XmlParseError(file, startLine, `<${name}> attribute "${attr}" is never closed`);
|
|
2876
|
+
el.attrs[attr] = decode(source.slice(j + 1, close));
|
|
2877
|
+
j = close + 1;
|
|
2878
|
+
}
|
|
2879
|
+
top().children.push(el);
|
|
2880
|
+
if (!selfClosing) stack.push(el);
|
|
2881
|
+
advance(j);
|
|
2882
|
+
}
|
|
2883
|
+
if (stack.length > 1) {
|
|
2884
|
+
const open = top();
|
|
2885
|
+
throw new XmlParseError(file, open.line, `<${open.name}> is never closed`);
|
|
2886
|
+
}
|
|
2887
|
+
if (root.children.length === 0) throw new XmlParseError(file, 1, "no elements \u2014 is this an XML report?");
|
|
2888
|
+
return root;
|
|
2889
|
+
}
|
|
2890
|
+
var XmlParseError, NAMED, NAME_END, ATTR_NAME_END, SPACE;
|
|
2891
|
+
var init_xml = __esm({
|
|
2892
|
+
"src/importers/xml.ts"() {
|
|
2893
|
+
"use strict";
|
|
2894
|
+
init_cjs_shims();
|
|
2895
|
+
XmlParseError = class extends Error {
|
|
2896
|
+
constructor(file, line, detail) {
|
|
2897
|
+
super(`${file}:${line} \u2014 ${detail}`);
|
|
2898
|
+
this.file = file;
|
|
2899
|
+
this.line = line;
|
|
2900
|
+
this.name = "XmlParseError";
|
|
2901
|
+
}
|
|
2902
|
+
file;
|
|
2903
|
+
line;
|
|
2904
|
+
};
|
|
2905
|
+
NAMED = {
|
|
2906
|
+
amp: "&",
|
|
2907
|
+
lt: "<",
|
|
2908
|
+
gt: ">",
|
|
2909
|
+
quot: '"',
|
|
2910
|
+
apos: "'"
|
|
2911
|
+
};
|
|
2912
|
+
NAME_END = /[\s/>]/;
|
|
2913
|
+
ATTR_NAME_END = /[\s=/>]/;
|
|
2914
|
+
SPACE = /\s/;
|
|
2915
|
+
}
|
|
2916
|
+
});
|
|
2917
|
+
|
|
2918
|
+
// src/importers/junit.ts
|
|
2919
|
+
function locate(el, suite, out) {
|
|
2920
|
+
for (const child of el.children) {
|
|
2921
|
+
if (child.name === "testcase") out.push({ test: child, suite });
|
|
2922
|
+
else locate(child, child.name === "testsuite" ? child : suite, out);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
function outcomeOf(test) {
|
|
2926
|
+
for (const name of ["error", "failure", "skipped"]) {
|
|
2927
|
+
const found = test.children.find((c) => c.name === name);
|
|
2928
|
+
if (found !== void 0) return { status: name, detail: found };
|
|
2929
|
+
}
|
|
2930
|
+
return { status: "pass", detail: void 0 };
|
|
2931
|
+
}
|
|
2932
|
+
function errorTextOf(detail) {
|
|
2933
|
+
if (detail === void 0) return "";
|
|
2934
|
+
const head = [detail.attrs["message"], detail.attrs["type"]].filter((s) => s !== void 0 && s !== "");
|
|
2935
|
+
return [...head, detail.text.trim()].filter((s) => s !== "").join("\n\n");
|
|
2936
|
+
}
|
|
2937
|
+
function keyOf(classname, name) {
|
|
2938
|
+
return { kind: "path-title", value: classname === "" ? name : `${classname}#${name}` };
|
|
2939
|
+
}
|
|
2940
|
+
function titleOf(classname, name) {
|
|
2941
|
+
if (classname === "") return name;
|
|
2942
|
+
if (classname === name || classname.endsWith(` ${name}`) || classname.endsWith(`\u203A${name}`)) return classname;
|
|
2943
|
+
return `${classname} \u203A ${name}`;
|
|
2944
|
+
}
|
|
2945
|
+
function specRefOf(test, suite, classname) {
|
|
2946
|
+
const file = test.attrs["file"] ?? suite?.attrs["file"] ?? "";
|
|
2947
|
+
if (file !== "") {
|
|
2948
|
+
const line = test.attrs["line"];
|
|
2949
|
+
return line !== void 0 && line !== "" ? `${file}:${line}` : file;
|
|
2950
|
+
}
|
|
2951
|
+
return classname === "" ? void 0 : classname;
|
|
2952
|
+
}
|
|
2953
|
+
function executionOf(test, suite) {
|
|
2954
|
+
const stamp = suite?.attrs["timestamp"];
|
|
2955
|
+
if (stamp === void 0 || stamp === "") return void 0;
|
|
2956
|
+
const started = new Date(stamp);
|
|
2957
|
+
if (Number.isNaN(started.getTime())) return void 0;
|
|
2958
|
+
const seconds = Number.parseFloat(test.attrs["time"] ?? "");
|
|
2959
|
+
const durationMs = Number.isFinite(seconds) && seconds >= 0 ? Math.round(seconds * 1e3) : 0;
|
|
2960
|
+
return {
|
|
2961
|
+
startedAt: started.toISOString(),
|
|
2962
|
+
finishedAt: new Date(started.getTime() + durationMs).toISOString(),
|
|
2963
|
+
durationMs,
|
|
2964
|
+
// The base format has no reruns. Surefire's `<rerunFailure>` does, and reading it is work for
|
|
2965
|
+
// the day somebody asks; reporting 0 says «first attempt», which is true of every case here.
|
|
2966
|
+
retry: 0
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
function readJUnit(source, file) {
|
|
2970
|
+
const doc = parseXml(source, file);
|
|
2971
|
+
const found = [];
|
|
2972
|
+
locate(doc, void 0, found);
|
|
2973
|
+
return found.map(({ test, suite }) => {
|
|
2974
|
+
const name = test.attrs["name"] ?? "";
|
|
2975
|
+
const classname = test.attrs["classname"] ?? test.attrs["class"] ?? "";
|
|
2976
|
+
const { status, detail } = outcomeOf(test);
|
|
2977
|
+
const key = keyOf(classname, name);
|
|
2978
|
+
const specRef = specRefOf(test, suite, classname);
|
|
2979
|
+
const errorContext = errorTextOf(detail);
|
|
2980
|
+
const execution = executionOf(test, suite);
|
|
2981
|
+
return {
|
|
2982
|
+
resultKey: resultKey(key.value, 0),
|
|
2983
|
+
keys: [key],
|
|
2984
|
+
source: SOURCE,
|
|
2985
|
+
rawStatus: status,
|
|
2986
|
+
title: titleOf(classname, name),
|
|
2987
|
+
...specRef !== void 0 ? { specRef } : {},
|
|
2988
|
+
...execution !== void 0 ? { execution } : {},
|
|
2989
|
+
...errorContext !== "" ? { errorContext } : {}
|
|
2990
|
+
};
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2993
|
+
function looksLikeJUnit(source) {
|
|
2994
|
+
return /<\s*testsuites?\b/.test(source.slice(0, 4096));
|
|
2995
|
+
}
|
|
2996
|
+
var SOURCE;
|
|
2997
|
+
var init_junit = __esm({
|
|
2998
|
+
"src/importers/junit.ts"() {
|
|
2999
|
+
"use strict";
|
|
3000
|
+
init_cjs_shims();
|
|
3001
|
+
init_result_key();
|
|
3002
|
+
init_xml();
|
|
3003
|
+
SOURCE = "junit";
|
|
3004
|
+
}
|
|
3005
|
+
});
|
|
3006
|
+
|
|
3007
|
+
// src/importers/playwright-json.ts
|
|
3008
|
+
function statedIdOf(test, title) {
|
|
3009
|
+
const annotation = test.annotations?.find((a) => a.type === "PluneId")?.description;
|
|
3010
|
+
if (annotation !== void 0 && annotation !== "") return annotation;
|
|
3011
|
+
return TOKEN.exec(title)?.[1];
|
|
3012
|
+
}
|
|
3013
|
+
function errorTextOf2(result) {
|
|
3014
|
+
return (result.errors ?? []).map((e) => e.stack ?? e.message ?? e.value ?? "").filter((text2) => text2 !== "").join("\n\n");
|
|
3015
|
+
}
|
|
3016
|
+
function pendingFrom(spec, test, result, file, titles) {
|
|
3017
|
+
const pathTitle = [file, ...titles].join("#");
|
|
3018
|
+
const seed = test.id ?? pathTitle;
|
|
3019
|
+
const stated = statedIdOf(test, spec.title ?? "");
|
|
3020
|
+
const expected = test.expectedStatus;
|
|
3021
|
+
const errorContext = errorTextOf2(result);
|
|
3022
|
+
const keys = [
|
|
3023
|
+
...test.id !== void 0 ? [{ kind: "playwright-id", value: test.id }] : [],
|
|
3024
|
+
{ kind: "path-title", value: pathTitle }
|
|
3025
|
+
];
|
|
3026
|
+
const started = result.startTime !== void 0 ? new Date(result.startTime) : void 0;
|
|
3027
|
+
const durationMs = typeof result.duration === "number" && result.duration >= 0 ? result.duration : 0;
|
|
3028
|
+
const retry = typeof result.retry === "number" && result.retry >= 0 ? result.retry : 0;
|
|
3029
|
+
return {
|
|
3030
|
+
resultKey: resultKey(seed, retry),
|
|
3031
|
+
...stated !== void 0 ? { testCaseId: stated } : {},
|
|
3032
|
+
keys,
|
|
3033
|
+
title: titles.join(" \u203A "),
|
|
3034
|
+
specRef: spec.line !== void 0 ? `${file}:${spec.line}` : file,
|
|
3035
|
+
source: SOURCE2,
|
|
3036
|
+
// Untouched, as everywhere: `timedOut` means what the project's map says it means, not what
|
|
3037
|
+
// this file decided.
|
|
3038
|
+
rawStatus: result.status ?? "unknown",
|
|
3039
|
+
...expected === "failed" || expected === "skipped" ? { expectedStatus: expected } : {},
|
|
3040
|
+
...started !== void 0 && !Number.isNaN(started.getTime()) ? {
|
|
3041
|
+
execution: {
|
|
3042
|
+
startedAt: started.toISOString(),
|
|
3043
|
+
finishedAt: new Date(started.getTime() + durationMs).toISOString(),
|
|
3044
|
+
durationMs,
|
|
3045
|
+
retry,
|
|
3046
|
+
...result.workerIndex !== void 0 ? { worker: String(result.workerIndex) } : {}
|
|
3047
|
+
}
|
|
3048
|
+
} : {},
|
|
3049
|
+
...errorContext !== "" ? { errorContext } : {}
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
function walk(suite, describes, depth, out) {
|
|
3053
|
+
const here = depth === 0 ? [] : [...describes, suite.title ?? ""];
|
|
3054
|
+
for (const spec of suite.specs ?? []) {
|
|
3055
|
+
const file = spec.file ?? suite.file ?? suite.title ?? "";
|
|
3056
|
+
const titles = [...here, spec.title ?? ""];
|
|
3057
|
+
for (const test of spec.tests ?? []) {
|
|
3058
|
+
for (const result of test.results ?? []) out.push(pendingFrom(spec, test, result, file, titles));
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
for (const child of suite.suites ?? []) walk(child, here, depth + 1, out);
|
|
3062
|
+
}
|
|
3063
|
+
function readPlaywrightJson(source, file) {
|
|
3064
|
+
let parsed;
|
|
3065
|
+
try {
|
|
3066
|
+
parsed = JSON.parse(source);
|
|
3067
|
+
} catch (err) {
|
|
3068
|
+
throw new JsonReportError(file, `not JSON \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
3069
|
+
}
|
|
3070
|
+
if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.suites)) {
|
|
3071
|
+
throw new JsonReportError(file, 'no "suites" array \u2014 is this a Playwright JSON report?');
|
|
3072
|
+
}
|
|
3073
|
+
const out = [];
|
|
3074
|
+
for (const suite of parsed.suites) walk(suite, [], 0, out);
|
|
3075
|
+
return out;
|
|
3076
|
+
}
|
|
3077
|
+
function looksLikePlaywrightJson(source) {
|
|
3078
|
+
const head = source.slice(0, 4096);
|
|
3079
|
+
return head.trimStart().startsWith("{") && head.includes('"suites"');
|
|
3080
|
+
}
|
|
3081
|
+
var SOURCE2, TOKEN, JsonReportError;
|
|
3082
|
+
var init_playwright_json = __esm({
|
|
3083
|
+
"src/importers/playwright-json.ts"() {
|
|
3084
|
+
"use strict";
|
|
3085
|
+
init_cjs_shims();
|
|
3086
|
+
init_result_key();
|
|
3087
|
+
SOURCE2 = "playwright";
|
|
3088
|
+
TOKEN = /@P([A-Za-z0-9_-]+)/;
|
|
3089
|
+
JsonReportError = class extends Error {
|
|
3090
|
+
constructor(file, detail) {
|
|
3091
|
+
super(`${file} \u2014 ${detail}`);
|
|
3092
|
+
this.file = file;
|
|
3093
|
+
this.name = "JsonReportError";
|
|
3094
|
+
}
|
|
3095
|
+
file;
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
});
|
|
3099
|
+
|
|
3100
|
+
// src/importers/index.ts
|
|
3101
|
+
var importers_exports = {};
|
|
3102
|
+
__export(importers_exports, {
|
|
3103
|
+
IMPORT_FORMATS: () => IMPORT_FORMATS,
|
|
3104
|
+
JsonReportError: () => JsonReportError,
|
|
3105
|
+
UnknownFormatError: () => UnknownFormatError,
|
|
3106
|
+
XmlParseError: () => XmlParseError,
|
|
3107
|
+
detectFormat: () => detectFormat,
|
|
3108
|
+
readReport: () => readReport
|
|
3109
|
+
});
|
|
3110
|
+
function detectFormat(source, file) {
|
|
3111
|
+
if (looksLikeJUnit(source)) return "junit";
|
|
3112
|
+
if (looksLikePlaywrightJson(source)) return "playwright-json";
|
|
3113
|
+
throw new UnknownFormatError(
|
|
3114
|
+
file,
|
|
3115
|
+
'no <testsuite> and no Playwright "suites" \u2014 this is not a report we can read.'
|
|
3116
|
+
);
|
|
3117
|
+
}
|
|
3118
|
+
function readReport(source, file, format) {
|
|
3119
|
+
const chosen = format ?? detectFormat(source, file);
|
|
3120
|
+
return {
|
|
3121
|
+
format: chosen,
|
|
3122
|
+
results: chosen === "junit" ? readJUnit(source, file) : readPlaywrightJson(source, file)
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
var IMPORT_FORMATS, UnknownFormatError;
|
|
3126
|
+
var init_importers = __esm({
|
|
3127
|
+
"src/importers/index.ts"() {
|
|
3128
|
+
"use strict";
|
|
3129
|
+
init_cjs_shims();
|
|
3130
|
+
init_junit();
|
|
3131
|
+
init_playwright_json();
|
|
3132
|
+
init_xml();
|
|
3133
|
+
init_playwright_json();
|
|
3134
|
+
IMPORT_FORMATS = ["junit", "playwright-json"];
|
|
3135
|
+
UnknownFormatError = class extends Error {
|
|
3136
|
+
constructor(file, detail) {
|
|
3137
|
+
super(`${file} \u2014 ${detail} Say which with --format ${IMPORT_FORMATS.join(" | ")}.`);
|
|
3138
|
+
this.name = "UnknownFormatError";
|
|
3139
|
+
}
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
});
|
|
3143
|
+
|
|
3144
|
+
// src/cli/commands/run-import.ts
|
|
3145
|
+
var run_import_exports = {};
|
|
3146
|
+
__export(run_import_exports, {
|
|
3147
|
+
handleRunImport: () => handleRunImport
|
|
3148
|
+
});
|
|
3149
|
+
async function handleRunImport(options) {
|
|
3150
|
+
const write = options.write ?? ((line) => void process.stdout.write(`${line}
|
|
3151
|
+
`));
|
|
3152
|
+
let source;
|
|
3153
|
+
try {
|
|
3154
|
+
source = fs8.readFileSync(options.file, "utf8");
|
|
3155
|
+
} catch {
|
|
3156
|
+
throw new RunCommandError(`Cannot read ${options.file}.`);
|
|
3157
|
+
}
|
|
3158
|
+
const { format, results } = readReport(source, options.file, options.format);
|
|
3159
|
+
if (results.length === 0) {
|
|
3160
|
+
throw new RunCommandError(`No test cases in ${options.file} \u2014 it parsed, but there is nothing in it.`);
|
|
3161
|
+
}
|
|
3162
|
+
const env = readEnv();
|
|
3163
|
+
const apiUrl = resolveApiUrl(options.apiUrl ?? env.config.apiUrl);
|
|
3164
|
+
const token = options.token ?? env.config.token ?? loadToken() ?? "";
|
|
3165
|
+
if (token === "") throw new NoTokenError();
|
|
3166
|
+
const session = await startRun(
|
|
3167
|
+
{
|
|
3168
|
+
apiUrl,
|
|
3169
|
+
token,
|
|
3170
|
+
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
|
|
3171
|
+
...options.key !== void 0 ? { externalKey: options.key } : {},
|
|
3172
|
+
...options.create === true ? { offerDiscovered: true } : {},
|
|
3173
|
+
kind: "automated",
|
|
3174
|
+
meta: { runner: format },
|
|
3175
|
+
log: write
|
|
3176
|
+
},
|
|
3177
|
+
// The file is the complete list of what ran, so the platform can resolve every test in one
|
|
3178
|
+
// request and derive what never ran from the same list (AC-09/AC-10) — the lazy path would be
|
|
3179
|
+
// a lookup per result.
|
|
3180
|
+
results.map((r) => r.keys)
|
|
3181
|
+
);
|
|
3182
|
+
for (const result2 of results) await session.add(result2);
|
|
3183
|
+
await session.finish();
|
|
3184
|
+
const stats = session.stats;
|
|
3185
|
+
const result = {
|
|
3186
|
+
runId: session.runId,
|
|
3187
|
+
format,
|
|
3188
|
+
parsed: results.length,
|
|
3189
|
+
accepted: stats.accepted,
|
|
3190
|
+
duplicate: stats.duplicate,
|
|
3191
|
+
conflict: stats.conflict,
|
|
3192
|
+
rejected: stats.rejected,
|
|
3193
|
+
unresolved: stats.unresolved,
|
|
3194
|
+
offered: stats.offered,
|
|
3195
|
+
deferred: stats.deferred,
|
|
3196
|
+
unlocatable: results.filter((r) => r.specRef === void 0).length,
|
|
3197
|
+
offering: options.create === true
|
|
3198
|
+
};
|
|
3199
|
+
for (const line of describe(result, apiUrl)) write(line);
|
|
3200
|
+
return result;
|
|
3201
|
+
}
|
|
3202
|
+
function runUrl2(apiUrl, id) {
|
|
3203
|
+
const web = dashboardUrl(apiUrl);
|
|
3204
|
+
return web === void 0 ? `${apiUrl}/v1/runs/${id}` : `${web}/runs/${id}`;
|
|
3205
|
+
}
|
|
3206
|
+
function describe(result, apiUrl) {
|
|
3207
|
+
const lines = [
|
|
3208
|
+
`Imported ${result.parsed} result(s) from a ${result.format} report: ${result.accepted} accepted, ${result.duplicate} already there, ${result.unresolved} unmatched.`
|
|
3209
|
+
];
|
|
3210
|
+
if (result.conflict > 0 || result.rejected > 0) {
|
|
3211
|
+
lines.push(`${result.conflict} conflicted and ${result.rejected} were refused.`);
|
|
3212
|
+
}
|
|
3213
|
+
if (result.deferred > 0) {
|
|
3214
|
+
lines.push(
|
|
3215
|
+
`${result.deferred} could not be sent and are in the fallback file \u2014 "plune run report" retries them.`
|
|
3216
|
+
);
|
|
3217
|
+
}
|
|
3218
|
+
if (result.offered > 0) {
|
|
3219
|
+
lines.push(`${result.offered} unknown test(s) offered to the review queue.`);
|
|
3220
|
+
} else if (result.unresolved > 0 && !result.offering) {
|
|
3221
|
+
lines.push("Nothing was offered to the review queue \u2014 pass --create to propose the unmatched tests.");
|
|
3222
|
+
} else if (result.unresolved > 0) {
|
|
3223
|
+
lines.push("Nothing new to offer \u2014 the unmatched tests are already in the review queue.");
|
|
3224
|
+
}
|
|
3225
|
+
if (result.unlocatable > 0) {
|
|
3226
|
+
lines.push(
|
|
3227
|
+
`${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.`
|
|
3228
|
+
);
|
|
3229
|
+
}
|
|
3230
|
+
if (result.runId !== null) lines.push(runUrl2(apiUrl, result.runId));
|
|
3231
|
+
return lines;
|
|
3232
|
+
}
|
|
3233
|
+
var fs8;
|
|
3234
|
+
var init_run_import = __esm({
|
|
3235
|
+
"src/cli/commands/run-import.ts"() {
|
|
3236
|
+
"use strict";
|
|
3237
|
+
init_cjs_shims();
|
|
3238
|
+
fs8 = __toESM(require("fs"), 1);
|
|
3239
|
+
init_session();
|
|
3240
|
+
init_importers();
|
|
3241
|
+
init_run_lifecycle();
|
|
3242
|
+
init_api_url();
|
|
3243
|
+
init_credentials();
|
|
3244
|
+
init_env2();
|
|
3245
|
+
}
|
|
3246
|
+
});
|
|
3247
|
+
|
|
2692
3248
|
// src/cli/commands/report.ts
|
|
2693
3249
|
var report_exports = {};
|
|
2694
3250
|
__export(report_exports, {
|
|
@@ -2705,7 +3261,7 @@ function handleReport(opts = {}) {
|
|
|
2705
3261
|
const file = path7.join(cwd, ".plune", "last-run.json");
|
|
2706
3262
|
let raw;
|
|
2707
3263
|
try {
|
|
2708
|
-
raw =
|
|
3264
|
+
raw = fs9.readFileSync(file, "utf8");
|
|
2709
3265
|
} catch {
|
|
2710
3266
|
throw new ReportNotFoundError('No saved run found. Run "plune run" first.');
|
|
2711
3267
|
}
|
|
@@ -2720,12 +3276,12 @@ function handleReport(opts = {}) {
|
|
|
2720
3276
|
}
|
|
2721
3277
|
return parsed;
|
|
2722
3278
|
}
|
|
2723
|
-
var
|
|
3279
|
+
var fs9, path7, ReportNotFoundError;
|
|
2724
3280
|
var init_report = __esm({
|
|
2725
3281
|
"src/cli/commands/report.ts"() {
|
|
2726
3282
|
"use strict";
|
|
2727
3283
|
init_cjs_shims();
|
|
2728
|
-
|
|
3284
|
+
fs9 = __toESM(require("fs"), 1);
|
|
2729
3285
|
path7 = __toESM(require("path"), 1);
|
|
2730
3286
|
ReportNotFoundError = class extends Error {
|
|
2731
3287
|
code = "NO_SAVED_RUN";
|
|
@@ -2800,7 +3356,7 @@ function isRunResult2(v) {
|
|
|
2800
3356
|
function loadRunResult(file, label) {
|
|
2801
3357
|
let raw;
|
|
2802
3358
|
try {
|
|
2803
|
-
raw =
|
|
3359
|
+
raw = fs10.readFileSync(file, "utf8");
|
|
2804
3360
|
} catch {
|
|
2805
3361
|
throw new DiffInputError(`Cannot read ${label} run file: ${file}`);
|
|
2806
3362
|
}
|
|
@@ -2820,12 +3376,12 @@ function handleDiff(opts) {
|
|
|
2820
3376
|
const current = loadRunResult(opts.currentPath, "current");
|
|
2821
3377
|
return diffRuns(baseline, current);
|
|
2822
3378
|
}
|
|
2823
|
-
var
|
|
3379
|
+
var fs10, DiffInputError;
|
|
2824
3380
|
var init_diff2 = __esm({
|
|
2825
3381
|
"src/cli/commands/diff.ts"() {
|
|
2826
3382
|
"use strict";
|
|
2827
3383
|
init_cjs_shims();
|
|
2828
|
-
|
|
3384
|
+
fs10 = __toESM(require("fs"), 1);
|
|
2829
3385
|
init_diff();
|
|
2830
3386
|
DiffInputError = class extends Error {
|
|
2831
3387
|
code = "DIFF_INPUT";
|
|
@@ -2951,7 +3507,7 @@ async function runInitWizard(cwd) {
|
|
|
2951
3507
|
return;
|
|
2952
3508
|
}
|
|
2953
3509
|
const outputPath = path8.join(cwd, "plune.yaml");
|
|
2954
|
-
if (
|
|
3510
|
+
if (fs11.existsSync(outputPath)) {
|
|
2955
3511
|
const overwrite = await clack.confirm({
|
|
2956
3512
|
message: "plune.yaml already exists. Overwrite?"
|
|
2957
3513
|
});
|
|
@@ -2990,17 +3546,17 @@ async function runInitWizard(cwd) {
|
|
|
2990
3546
|
`;
|
|
2991
3547
|
const spin = clack.spinner();
|
|
2992
3548
|
spin.start("Writing plune.yaml\u2026");
|
|
2993
|
-
|
|
3549
|
+
fs11.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
|
|
2994
3550
|
spin.stop("plune.yaml created!");
|
|
2995
3551
|
clack.outro("plune.yaml is ready.");
|
|
2996
3552
|
}
|
|
2997
|
-
var clack,
|
|
3553
|
+
var clack, fs11, path8, import_yaml2;
|
|
2998
3554
|
var init_wizard = __esm({
|
|
2999
3555
|
"src/config/init/wizard.ts"() {
|
|
3000
3556
|
"use strict";
|
|
3001
3557
|
init_cjs_shims();
|
|
3002
3558
|
clack = __toESM(require("@clack/prompts"), 1);
|
|
3003
|
-
|
|
3559
|
+
fs11 = __toESM(require("fs"), 1);
|
|
3004
3560
|
path8 = __toESM(require("path"), 1);
|
|
3005
3561
|
import_yaml2 = require("yaml");
|
|
3006
3562
|
init_errors();
|
|
@@ -3010,19 +3566,19 @@ var init_wizard = __esm({
|
|
|
3010
3566
|
// src/cli/templates/index.ts
|
|
3011
3567
|
function writeTemplateFile(cwd, rel, content, force) {
|
|
3012
3568
|
const target = path9.join(cwd, rel);
|
|
3013
|
-
if (!force &&
|
|
3569
|
+
if (!force && fs12.existsSync(target)) {
|
|
3014
3570
|
return "skipped";
|
|
3015
3571
|
}
|
|
3016
|
-
|
|
3017
|
-
|
|
3572
|
+
fs12.mkdirSync(path9.dirname(target), { recursive: true });
|
|
3573
|
+
fs12.writeFileSync(target, content, "utf8");
|
|
3018
3574
|
return "written";
|
|
3019
3575
|
}
|
|
3020
|
-
var
|
|
3576
|
+
var fs12, path9, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
|
|
3021
3577
|
var init_templates = __esm({
|
|
3022
3578
|
"src/cli/templates/index.ts"() {
|
|
3023
3579
|
"use strict";
|
|
3024
3580
|
init_cjs_shims();
|
|
3025
|
-
|
|
3581
|
+
fs12 = __toESM(require("fs"), 1);
|
|
3026
3582
|
path9 = __toESM(require("path"), 1);
|
|
3027
3583
|
ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
|
|
3028
3584
|
# .env is gitignored; .env.example is safe to commit.
|
|
@@ -3282,7 +3838,7 @@ function readRun(cwd, file) {
|
|
|
3282
3838
|
const target = file !== void 0 ? path10.resolve(cwd, file) : path10.join(cwd, ".plune", "last-run.json");
|
|
3283
3839
|
let raw;
|
|
3284
3840
|
try {
|
|
3285
|
-
raw =
|
|
3841
|
+
raw = fs13.readFileSync(target, "utf8");
|
|
3286
3842
|
} catch {
|
|
3287
3843
|
throw new SyncFileError(
|
|
3288
3844
|
file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
|
|
@@ -3346,12 +3902,12 @@ function reportSyncFailure(err, write) {
|
|
|
3346
3902
|
}
|
|
3347
3903
|
return null;
|
|
3348
3904
|
}
|
|
3349
|
-
var
|
|
3905
|
+
var fs13, path10, NotLoggedInError, TokenRejectedError2, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
3350
3906
|
var init_sync = __esm({
|
|
3351
3907
|
"src/cli/commands/sync.ts"() {
|
|
3352
3908
|
"use strict";
|
|
3353
3909
|
init_cjs_shims();
|
|
3354
|
-
|
|
3910
|
+
fs13 = __toESM(require("fs"), 1);
|
|
3355
3911
|
path10 = __toESM(require("path"), 1);
|
|
3356
3912
|
init_run_result_schema();
|
|
3357
3913
|
init_api_url();
|
|
@@ -3397,14 +3953,14 @@ var init_sync = __esm({
|
|
|
3397
3953
|
|
|
3398
3954
|
// src/cli/commands/cairn-artifact.ts
|
|
3399
3955
|
async function findLatestCairnRun(runsDir = "runs") {
|
|
3400
|
-
const entries = await
|
|
3956
|
+
const entries = await fs14.readdir(runsDir, { withFileTypes: true }).catch(() => null);
|
|
3401
3957
|
if (entries === null) throw new NoCairnRunsError(runsDir);
|
|
3402
3958
|
let newest;
|
|
3403
3959
|
for (const entry of entries) {
|
|
3404
3960
|
if (!entry.isDirectory()) continue;
|
|
3405
3961
|
const dir = path11.join(runsDir, entry.name);
|
|
3406
3962
|
try {
|
|
3407
|
-
const report = await
|
|
3963
|
+
const report = await fs14.lstat(path11.join(dir, "report.json"));
|
|
3408
3964
|
if (!report.isFile()) continue;
|
|
3409
3965
|
if (newest === void 0 || report.mtimeMs > newest.at) {
|
|
3410
3966
|
newest = { dir, at: report.mtimeMs };
|
|
@@ -3417,7 +3973,7 @@ async function findLatestCairnRun(runsDir = "runs") {
|
|
|
3417
3973
|
}
|
|
3418
3974
|
async function readJson(file) {
|
|
3419
3975
|
try {
|
|
3420
|
-
return JSON.parse(await
|
|
3976
|
+
return JSON.parse(await fs14.readFile(file, "utf8"));
|
|
3421
3977
|
} catch {
|
|
3422
3978
|
return void 0;
|
|
3423
3979
|
}
|
|
@@ -3431,8 +3987,8 @@ async function defaultReadEvidence(dir) {
|
|
|
3431
3987
|
});
|
|
3432
3988
|
}
|
|
3433
3989
|
function casesFromDesign(report) {
|
|
3434
|
-
const
|
|
3435
|
-
return
|
|
3990
|
+
const list2 = Array.isArray(report["testCases"]) ? report["testCases"] : [];
|
|
3991
|
+
return list2.map((raw) => {
|
|
3436
3992
|
const c = asRecord(raw);
|
|
3437
3993
|
const steps = (Array.isArray(c["steps"]) ? c["steps"] : []).map((s) => str(s)).filter((s) => s !== "");
|
|
3438
3994
|
return {
|
|
@@ -3447,8 +4003,8 @@ function casesFromDesign(report) {
|
|
|
3447
4003
|
});
|
|
3448
4004
|
}
|
|
3449
4005
|
function casesFromApi(report, verdicts) {
|
|
3450
|
-
const
|
|
3451
|
-
return
|
|
4006
|
+
const list2 = Array.isArray(report["cases"]) ? report["cases"] : [];
|
|
4007
|
+
return list2.map((raw) => {
|
|
3452
4008
|
const c = asRecord(raw);
|
|
3453
4009
|
const name = str(c["name"], "unnamed operation");
|
|
3454
4010
|
const request = `${str(c["method"], "GET")} ${str(c["path"], "/")}`;
|
|
@@ -3502,12 +4058,12 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
3502
4058
|
cases
|
|
3503
4059
|
};
|
|
3504
4060
|
}
|
|
3505
|
-
var
|
|
4061
|
+
var fs14, path11, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
|
|
3506
4062
|
var init_cairn_artifact = __esm({
|
|
3507
4063
|
"src/cli/commands/cairn-artifact.ts"() {
|
|
3508
4064
|
"use strict";
|
|
3509
4065
|
init_cjs_shims();
|
|
3510
|
-
|
|
4066
|
+
fs14 = __toESM(require("fs/promises"), 1);
|
|
3511
4067
|
path11 = __toESM(require("path"), 1);
|
|
3512
4068
|
SUPPORTED_ARTIFACT_VERSION = 1;
|
|
3513
4069
|
INGEST_SCHEMA_VERSION = 1;
|
|
@@ -3854,6 +4410,36 @@ function createProgram() {
|
|
|
3854
4410
|
await failRunCommand(err, verboseOf(command));
|
|
3855
4411
|
}
|
|
3856
4412
|
});
|
|
4413
|
+
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).action(
|
|
4414
|
+
async (file, options, command) => {
|
|
4415
|
+
const [{ handleRunImport: handleRunImport2 }, { IMPORT_FORMATS: IMPORT_FORMATS2, UnknownFormatError: UnknownFormatError2, XmlParseError: XmlParseError2, JsonReportError: JsonReportError2 }] = await Promise.all([
|
|
4416
|
+
Promise.resolve().then(() => (init_run_import(), run_import_exports)),
|
|
4417
|
+
Promise.resolve().then(() => (init_importers(), importers_exports))
|
|
4418
|
+
]);
|
|
4419
|
+
const format = options.format;
|
|
4420
|
+
if (format !== void 0 && !IMPORT_FORMATS2.includes(format)) {
|
|
4421
|
+
process.stderr.write(`Unknown --format "${format}". Use ${IMPORT_FORMATS2.join(" or ")}.
|
|
4422
|
+
`);
|
|
4423
|
+
process.exit(2);
|
|
4424
|
+
return;
|
|
4425
|
+
}
|
|
4426
|
+
try {
|
|
4427
|
+
await handleRunImport2({
|
|
4428
|
+
file,
|
|
4429
|
+
...format !== void 0 ? { format } : {},
|
|
4430
|
+
...options.key !== void 0 ? { key: options.key } : {},
|
|
4431
|
+
...options.create ? { create: true } : {}
|
|
4432
|
+
});
|
|
4433
|
+
} catch (err) {
|
|
4434
|
+
if (err instanceof UnknownFormatError2 || err instanceof XmlParseError2 || err instanceof JsonReportError2) {
|
|
4435
|
+
process.stderr.write(err.message + "\n");
|
|
4436
|
+
process.exit(2);
|
|
4437
|
+
return;
|
|
4438
|
+
}
|
|
4439
|
+
await failRunCommand(err, verboseOf(command));
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
);
|
|
3857
4443
|
program.command("report").description("Render the most recent run in a chosen format").option("--format <fmt>", "console | json | markdown", "console").option("-o, --output <path>", "Write the report to a file instead of stdout").action(async (options, command) => {
|
|
3858
4444
|
const globals = command.optsWithGlobals();
|
|
3859
4445
|
const [{ loadEnv: loadEnv2 }, { handleReport: handleReport2, ReportNotFoundError: ReportNotFoundError2 }, { renderReport: renderReport2 }] = await Promise.all([
|