@plune-ai/cli 0.20.0 → 0.22.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 +22 -1
- package/dist/cli.cjs +809 -659
- package/dist/reporter-core.cjs +86 -29
- package/dist/reporter-core.d.cts +16 -1
- package/dist/reporter-core.d.ts +16 -1
- package/dist/reporter-core.js +86 -29
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -2304,6 +2304,13 @@ var init_credentials = __esm({
|
|
|
2304
2304
|
});
|
|
2305
2305
|
|
|
2306
2306
|
// src/reporter-core/client.ts
|
|
2307
|
+
function parseRetryAfter(raw, now = Date.now()) {
|
|
2308
|
+
const text2 = raw?.trim();
|
|
2309
|
+
if (text2 === void 0 || text2 === "") return null;
|
|
2310
|
+
const seconds = /^\d+(\.\d+)?$/.test(text2) ? Number(text2) : (Date.parse(text2) - now) / 1e3;
|
|
2311
|
+
if (!Number.isFinite(seconds)) return null;
|
|
2312
|
+
return Math.min(Math.max(seconds, 0) * 1e3, RETRY_AFTER_CAP_MS);
|
|
2313
|
+
}
|
|
2307
2314
|
function nodeTransport(url, init, idleMs = IDLE_MS) {
|
|
2308
2315
|
return new Promise((resolve9, reject) => {
|
|
2309
2316
|
const target = new URL(url);
|
|
@@ -2371,12 +2378,6 @@ async function detailOf(res) {
|
|
|
2371
2378
|
return { detail: "" };
|
|
2372
2379
|
}
|
|
2373
2380
|
}
|
|
2374
|
-
function retryAfterMs2(res) {
|
|
2375
|
-
const raw = res.headers.get("retry-after");
|
|
2376
|
-
if (raw === null) return null;
|
|
2377
|
-
const seconds = Number(raw.trim());
|
|
2378
|
-
return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : null;
|
|
2379
|
-
}
|
|
2380
2381
|
function classify(status) {
|
|
2381
2382
|
if (status === 401 || status === 403) return "auth";
|
|
2382
2383
|
if (status === 409) return "conflict";
|
|
@@ -2387,15 +2388,34 @@ function classify(status) {
|
|
|
2387
2388
|
function createClient(opts) {
|
|
2388
2389
|
const doFetch = opts.fetchImpl ?? nodeTransport;
|
|
2389
2390
|
const wait = opts.wait ?? sleep;
|
|
2391
|
+
const uploadRetries = opts.uploadRetries === void 0 ? void 0 : Math.max(0, Math.floor(opts.uploadRetries));
|
|
2392
|
+
const random = opts.random ?? Math.random;
|
|
2393
|
+
const now = opts.now ?? Date.now;
|
|
2394
|
+
let pausedUntil = 0;
|
|
2395
|
+
const jittered = (ms) => Math.round(ms * (1 + random() * 0.5));
|
|
2396
|
+
async function backOff(attempt, said) {
|
|
2397
|
+
opts.onRetry?.();
|
|
2398
|
+
const ms = jittered(said ?? Math.min(BACKOFF_BASE_MS * 2 ** attempt, BACKOFF_CAP_MS));
|
|
2399
|
+
const until = now() + ms;
|
|
2400
|
+
pausedUntil = Math.max(pausedUntil, until);
|
|
2401
|
+
await wait(ms);
|
|
2402
|
+
return until;
|
|
2403
|
+
}
|
|
2404
|
+
async function respectPause(mine) {
|
|
2405
|
+
const left = pausedUntil - now();
|
|
2406
|
+
if (pausedUntil > mine && left > 0) await wait(left + Math.round(random() * 250));
|
|
2407
|
+
}
|
|
2390
2408
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
2391
2409
|
const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
|
|
2392
|
-
async function send(method, path17, payload) {
|
|
2410
|
+
async function send(method, path17, payload, retries = DEFAULT_RETRIES) {
|
|
2393
2411
|
let last = {
|
|
2394
2412
|
kind: "unavailable",
|
|
2395
2413
|
status: null,
|
|
2396
2414
|
detail: "no attempt was made"
|
|
2397
2415
|
};
|
|
2398
|
-
|
|
2416
|
+
let mine = 0;
|
|
2417
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
2418
|
+
await respectPause(mine);
|
|
2399
2419
|
let res;
|
|
2400
2420
|
try {
|
|
2401
2421
|
res = await doFetch(`${base}${path17}`, {
|
|
@@ -2408,7 +2428,8 @@ function createClient(opts) {
|
|
|
2408
2428
|
});
|
|
2409
2429
|
} catch (err) {
|
|
2410
2430
|
last = { kind: "unavailable", status: null, detail: scrub(String(err)) };
|
|
2411
|
-
if (attempt
|
|
2431
|
+
if (attempt >= Math.min(retries, DEFAULT_RETRIES)) break;
|
|
2432
|
+
mine = await backOff(attempt, null);
|
|
2412
2433
|
continue;
|
|
2413
2434
|
}
|
|
2414
2435
|
if (res.ok) {
|
|
@@ -2423,25 +2444,28 @@ function createClient(opts) {
|
|
|
2423
2444
|
const kind = classify(res.status);
|
|
2424
2445
|
last = { kind, status: res.status, detail: scrub(said.detail), ...said.code !== void 0 ? { code: said.code } : {} };
|
|
2425
2446
|
if (kind !== "unavailable") break;
|
|
2426
|
-
|
|
2447
|
+
const said429 = parseRetryAfter(res.headers.get("retry-after"), now());
|
|
2448
|
+
if (res.status === 429 && said429 === null) {
|
|
2427
2449
|
last.kind = "invalid";
|
|
2428
2450
|
last.detail = "quota exceeded (no Retry-After)";
|
|
2429
2451
|
if (said.detail) last.detail += ` \u2014 ${scrub(said.detail)}`;
|
|
2430
2452
|
break;
|
|
2431
2453
|
}
|
|
2432
|
-
if (attempt
|
|
2433
|
-
|
|
2434
|
-
}
|
|
2454
|
+
if (attempt >= (said429 === null ? Math.min(retries, DEFAULT_RETRIES) : retries)) break;
|
|
2455
|
+
mine = await backOff(attempt, said429);
|
|
2435
2456
|
}
|
|
2436
2457
|
return { ok: false, ...last };
|
|
2437
2458
|
}
|
|
2438
2459
|
return {
|
|
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 }),
|
|
2460
|
+
post: (path17, body, retries) => send("POST", path17, { body: JSON.stringify(body), type: "application/json" }, retries),
|
|
2461
|
+
upload: (path17, bytes, contentType) => send("POST", path17, { body: bytes, type: contentType }, uploadRetries ?? DEFAULT_RETRIES),
|
|
2441
2462
|
del: (path17) => send("DELETE", path17),
|
|
2442
2463
|
async put(url, headers, source) {
|
|
2464
|
+
const putRetries = uploadRetries ?? 3;
|
|
2443
2465
|
let last = { ok: false, status: null, code: "network" };
|
|
2444
|
-
|
|
2466
|
+
let mine = 0;
|
|
2467
|
+
for (let attempt = 0; attempt <= putRetries; attempt += 1) {
|
|
2468
|
+
await respectPause(mine);
|
|
2445
2469
|
try {
|
|
2446
2470
|
let status;
|
|
2447
2471
|
let retryAfter;
|
|
@@ -2456,21 +2480,21 @@ function createClient(opts) {
|
|
|
2456
2480
|
if (status >= 200 && status <= 299) return { ok: true };
|
|
2457
2481
|
last = { ok: false, status, code: `bucket_${status}` };
|
|
2458
2482
|
if (status < 500 && status !== 429) return last;
|
|
2459
|
-
const
|
|
2460
|
-
if (attempt
|
|
2461
|
-
|
|
2462
|
-
}
|
|
2483
|
+
const said = parseRetryAfter(retryAfter, now());
|
|
2484
|
+
if (attempt >= (said === null ? Math.min(putRetries, 3) : putRetries)) return last;
|
|
2485
|
+
mine = await backOff(attempt, said);
|
|
2463
2486
|
continue;
|
|
2464
2487
|
} catch (err) {
|
|
2465
2488
|
last = { ok: false, status: null, code: codeOfError(err) };
|
|
2466
2489
|
}
|
|
2467
|
-
if (attempt
|
|
2490
|
+
if (attempt >= Math.min(putRetries, 3)) return last;
|
|
2491
|
+
mine = await backOff(attempt, null);
|
|
2468
2492
|
}
|
|
2469
2493
|
return last;
|
|
2470
2494
|
}
|
|
2471
2495
|
};
|
|
2472
2496
|
}
|
|
2473
|
-
var import_node_fs2, import_node_stream, import_node_http, import_node_https,
|
|
2497
|
+
var import_node_fs2, import_node_stream, import_node_http, import_node_https, BACKOFF_BASE_MS, BACKOFF_CAP_MS, DEFAULT_RETRIES, RETRY_AFTER_CAP_MS, IDLE_MS, withoutLength, codeOfError, sleep;
|
|
2474
2498
|
var init_client = __esm({
|
|
2475
2499
|
"src/reporter-core/client.ts"() {
|
|
2476
2500
|
"use strict";
|
|
@@ -2479,9 +2503,11 @@ var init_client = __esm({
|
|
|
2479
2503
|
import_node_stream = require("stream");
|
|
2480
2504
|
import_node_http = require("http");
|
|
2481
2505
|
import_node_https = require("https");
|
|
2482
|
-
|
|
2506
|
+
BACKOFF_BASE_MS = 300;
|
|
2507
|
+
BACKOFF_CAP_MS = 1e4;
|
|
2508
|
+
DEFAULT_RETRIES = 2;
|
|
2509
|
+
RETRY_AFTER_CAP_MS = 6e4;
|
|
2483
2510
|
IDLE_MS = 3e5;
|
|
2484
|
-
PUT_DELAYS_MS = [500, 1500, 4e3];
|
|
2485
2511
|
withoutLength = (headers) => Object.fromEntries(Object.entries(headers).filter(([name]) => name.toLowerCase() !== "content-length"));
|
|
2486
2512
|
codeOfError = (err) => {
|
|
2487
2513
|
const code = err?.code;
|
|
@@ -2506,6 +2532,14 @@ function count(env, name) {
|
|
|
2506
2532
|
const n = Number(raw);
|
|
2507
2533
|
return Number.isInteger(n) && n > 0 ? n : void 0;
|
|
2508
2534
|
}
|
|
2535
|
+
function bounded(env, name, range, warnings) {
|
|
2536
|
+
const raw = value(env, name);
|
|
2537
|
+
if (raw === void 0) return void 0;
|
|
2538
|
+
const n = Number(raw);
|
|
2539
|
+
if (/^\d+$/.test(raw) && n >= range.min && n <= range.max) return n;
|
|
2540
|
+
warnings.push(`${name}=${JSON.stringify(raw.slice(0, 40))} is not a whole number from ${range.min} to ${range.max} \u2014 using ${range.default}.`);
|
|
2541
|
+
return void 0;
|
|
2542
|
+
}
|
|
2509
2543
|
function list(env, name) {
|
|
2510
2544
|
const raw = value(env, name);
|
|
2511
2545
|
if (raw === void 0) return void 0;
|
|
@@ -2518,6 +2552,9 @@ function readEnv(env = process.env) {
|
|
|
2518
2552
|
const apiUrl = value(env, "PLUNE_API_URL");
|
|
2519
2553
|
const fallbackPath = value(env, "PLUNE_FALLBACK");
|
|
2520
2554
|
const batchSize = count(env, "PLUNE_BATCH_SIZE");
|
|
2555
|
+
const warnings = [];
|
|
2556
|
+
const uploadConcurrency = bounded(env, "PLUNE_UPLOAD_CONCURRENCY", UPLOAD_CONCURRENCY, warnings);
|
|
2557
|
+
const uploadRetries = bounded(env, "PLUNE_UPLOAD_RETRIES", UPLOAD_RETRIES, warnings);
|
|
2521
2558
|
const title = value(env, "PLUNE_RUN_TITLE");
|
|
2522
2559
|
const environment = value(env, "PLUNE_ENV");
|
|
2523
2560
|
const labels = list(env, "PLUNE_LABELS");
|
|
@@ -2530,6 +2567,8 @@ function readEnv(env = process.env) {
|
|
|
2530
2567
|
...apiUrl !== void 0 ? { apiUrl } : {},
|
|
2531
2568
|
...fallbackPath !== void 0 ? { fallbackPath } : {},
|
|
2532
2569
|
...batchSize !== void 0 ? { batchSize } : {},
|
|
2570
|
+
...uploadConcurrency !== void 0 ? { uploadConcurrency } : {},
|
|
2571
|
+
...uploadRetries !== void 0 ? { uploadRetries } : {},
|
|
2533
2572
|
...title !== void 0 ? { title } : {},
|
|
2534
2573
|
...environment !== void 0 ? { environment } : {},
|
|
2535
2574
|
...labels !== void 0 ? { labels } : {},
|
|
@@ -2542,7 +2581,8 @@ function readEnv(env = process.env) {
|
|
|
2542
2581
|
// reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
|
|
2543
2582
|
// way this process must not be the one to call it finished.
|
|
2544
2583
|
keepOpen: flag(env, "PLUNE_SHARED_RUN") || flag(env, "PLUNE_PROCEED"),
|
|
2545
|
-
ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0)
|
|
2584
|
+
ignored: UNSUPPORTED_VARS.filter((name) => value(env, name) !== void 0),
|
|
2585
|
+
warnings
|
|
2546
2586
|
};
|
|
2547
2587
|
}
|
|
2548
2588
|
function defaultRunTitle(env = process.env, now = /* @__PURE__ */ new Date(), cwd = process.cwd()) {
|
|
@@ -2568,13 +2608,15 @@ function ciCommit(reported, env = process.env) {
|
|
|
2568
2608
|
...branch !== void 0 ? { branch } : {}
|
|
2569
2609
|
};
|
|
2570
2610
|
}
|
|
2571
|
-
var path6, UNSUPPORTED_VARS, PROVIDERS;
|
|
2611
|
+
var path6, UNSUPPORTED_VARS, UPLOAD_CONCURRENCY, UPLOAD_RETRIES, PROVIDERS;
|
|
2572
2612
|
var init_env2 = __esm({
|
|
2573
2613
|
"src/reporter-core/env.ts"() {
|
|
2574
2614
|
"use strict";
|
|
2575
2615
|
init_cjs_shims();
|
|
2576
2616
|
path6 = __toESM(require("path"), 1);
|
|
2577
2617
|
UNSUPPORTED_VARS = ["PLUNE_GROUP"];
|
|
2618
|
+
UPLOAD_CONCURRENCY = { min: 1, max: 32, default: 4 };
|
|
2619
|
+
UPLOAD_RETRIES = { min: 0, max: 20, default: 6 };
|
|
2578
2620
|
PROVIDERS = [
|
|
2579
2621
|
{ is: "GITHUB_ACTIONS", sha: "GITHUB_SHA", branch: ["GITHUB_HEAD_REF", "GITHUB_REF_NAME"] },
|
|
2580
2622
|
{ is: "GITLAB_CI", sha: "CI_COMMIT_SHA", branch: ["CI_COMMIT_REF_NAME"] },
|
|
@@ -2854,9 +2896,15 @@ async function startRun(passed, declared2) {
|
|
|
2854
2896
|
const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
|
|
2855
2897
|
const offerDiscovered = cfg.offerDiscovered ?? false;
|
|
2856
2898
|
const token = cfg.token ?? loadToken() ?? "";
|
|
2899
|
+
const uploadsAtOnce = Math.min(Math.max(Math.floor(cfg.uploadConcurrency ?? UPLOAD_CONCURRENCY.default), UPLOAD_CONCURRENCY.min), UPLOAD_CONCURRENCY.max);
|
|
2900
|
+
const uploadRetries = Math.min(Math.max(Math.floor(cfg.uploadRetries ?? UPLOAD_RETRIES.default), UPLOAD_RETRIES.min), UPLOAD_RETRIES.max);
|
|
2901
|
+
let retried = 0;
|
|
2902
|
+
let uploadMs = 0;
|
|
2857
2903
|
const client = createClient({
|
|
2858
2904
|
apiUrl: resolveApiUrl(cfg.apiUrl),
|
|
2859
2905
|
token,
|
|
2906
|
+
uploadRetries,
|
|
2907
|
+
onRetry: () => void (retried += 1),
|
|
2860
2908
|
...cfg.fetchImpl !== void 0 ? { fetchImpl: cfg.fetchImpl } : {}
|
|
2861
2909
|
});
|
|
2862
2910
|
const stats = {
|
|
@@ -2907,6 +2955,7 @@ async function startRun(passed, declared2) {
|
|
|
2907
2955
|
if (offline) {
|
|
2908
2956
|
log('plune: no API token \u2014 run "plune login" first. Results will be written to the fallback file.');
|
|
2909
2957
|
}
|
|
2958
|
+
for (const warning of env.warnings) log(`plune: ${warning}`);
|
|
2910
2959
|
if (env.ignored.length > 0) {
|
|
2911
2960
|
log(`plune: ignoring ${env.ignored.join(", ")} \u2014 a run has nowhere to store them yet.`);
|
|
2912
2961
|
}
|
|
@@ -3036,7 +3085,7 @@ async function startRun(passed, declared2) {
|
|
|
3036
3085
|
function ask(job, size) {
|
|
3037
3086
|
return client.post(`/v1/results/${encodeURIComponent(job.id)}/files/uploads`, {
|
|
3038
3087
|
files: [{ name: job.name, contentType: job.type, size }]
|
|
3039
|
-
});
|
|
3088
|
+
}, uploadRetries);
|
|
3040
3089
|
}
|
|
3041
3090
|
function failed(job, why) {
|
|
3042
3091
|
countsOf(job.kind).failed += 1;
|
|
@@ -3097,11 +3146,11 @@ async function startRun(passed, declared2) {
|
|
|
3097
3146
|
if (fresh === void 0) return failedToBucket(job, "bad_answer");
|
|
3098
3147
|
link = fresh;
|
|
3099
3148
|
}
|
|
3100
|
-
const done2 = await client.post(`/v1/files/${encodeURIComponent(link.id)}/complete`, {});
|
|
3149
|
+
const done2 = await client.post(`/v1/files/${encodeURIComponent(link.id)}/complete`, {}, uploadRetries);
|
|
3101
3150
|
if (!done2.ok) return failedToBucket(job, codeOf2(done2));
|
|
3102
3151
|
countsOf(job.kind).uploaded += 1;
|
|
3103
3152
|
}
|
|
3104
|
-
let free =
|
|
3153
|
+
let free = uploadsAtOnce;
|
|
3105
3154
|
const waiting = [];
|
|
3106
3155
|
async function gated(work) {
|
|
3107
3156
|
if (free === 0) await new Promise((resolve9) => waiting.push(resolve9));
|
|
@@ -3181,6 +3230,7 @@ async function startRun(passed, declared2) {
|
|
|
3181
3230
|
}
|
|
3182
3231
|
}
|
|
3183
3232
|
}
|
|
3233
|
+
const started = Date.now();
|
|
3184
3234
|
const byResult = /* @__PURE__ */ new Map();
|
|
3185
3235
|
for (const job of jobs) byResult.set(job.id, [...byResult.get(job.id) ?? [], job]);
|
|
3186
3236
|
await Promise.all(
|
|
@@ -3193,6 +3243,7 @@ async function startRun(passed, declared2) {
|
|
|
3193
3243
|
await Promise.all(work);
|
|
3194
3244
|
})
|
|
3195
3245
|
);
|
|
3246
|
+
uploadMs += Date.now() - started;
|
|
3196
3247
|
}
|
|
3197
3248
|
async function flush() {
|
|
3198
3249
|
if (buffer.length === 0) return;
|
|
@@ -3285,6 +3336,13 @@ async function startRun(passed, declared2) {
|
|
|
3285
3336
|
}
|
|
3286
3337
|
if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
|
|
3287
3338
|
if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
|
|
3339
|
+
const files = [stats.screenshots, stats.textFiles, stats.otherFiles];
|
|
3340
|
+
const total = (what) => files.reduce((n, counts) => n + counts[what], 0);
|
|
3341
|
+
if (total("uploaded") + total("skipped") + total("failed") > 0) {
|
|
3342
|
+
log(
|
|
3343
|
+
`plune: files \u2014 ${total("uploaded")} uploaded, ${total("skipped")} skipped, ${retried} retried, ${total("failed")} dropped, in ${(uploadMs / 1e3).toFixed(1)} s (${uploadsAtOnce} at once)`
|
|
3344
|
+
);
|
|
3345
|
+
}
|
|
3288
3346
|
log(`plune: ${parts.join(" \xB7 ")}`);
|
|
3289
3347
|
if (stats.deferred > 0 && process.env["GITHUB_ACTIONS"] === "true") {
|
|
3290
3348
|
log(`::warning::${stats.deferred} result(s) were not delivered to Plune \u2014 see the reporting step's log.`);
|
|
@@ -3339,7 +3397,7 @@ async function startRun(passed, declared2) {
|
|
|
3339
3397
|
}
|
|
3340
3398
|
};
|
|
3341
3399
|
}
|
|
3342
|
-
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,
|
|
3400
|
+
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, NAME_MAX, SKIPPED, BUCKET_FOUND, KINDS, codeOf2, BATCH_BYTES, ENVELOPE_BYTES;
|
|
3343
3401
|
var init_session = __esm({
|
|
3344
3402
|
"src/reporter-core/session.ts"() {
|
|
3345
3403
|
"use strict";
|
|
@@ -3357,7 +3415,7 @@ var init_session = __esm({
|
|
|
3357
3415
|
RESOLVE_CHUNK = 500;
|
|
3358
3416
|
BATCH_MAX = 500;
|
|
3359
3417
|
DISCOVER_MAX = 500;
|
|
3360
|
-
BATCH_DEFAULT =
|
|
3418
|
+
BATCH_DEFAULT = 500;
|
|
3361
3419
|
EXPECTED_MAX = 1e4;
|
|
3362
3420
|
KEY_MAX = 1024;
|
|
3363
3421
|
TITLE_MAX = 300;
|
|
@@ -3375,7 +3433,6 @@ var init_session = __esm({
|
|
|
3375
3433
|
".webp": "image/webp"
|
|
3376
3434
|
};
|
|
3377
3435
|
METADATA_TYPE2 = "application/plune.metadata+json";
|
|
3378
|
-
UPLOADS_AT_ONCE = 4;
|
|
3379
3436
|
NAME_MAX = 200;
|
|
3380
3437
|
SKIPPED = {
|
|
3381
3438
|
hidden: "its name starts with an underscore, which hides an attachment in Playwright's own reports too",
|
|
@@ -3644,160 +3701,631 @@ var init_run_lifecycle = __esm({
|
|
|
3644
3701
|
}
|
|
3645
3702
|
});
|
|
3646
3703
|
|
|
3647
|
-
// src/
|
|
3648
|
-
function
|
|
3649
|
-
|
|
3650
|
-
return plain.length <= RESULT_KEY_MAX ? plain : `${sha256(testId).slice(0, HASH_CHARS)}#${retry}`;
|
|
3704
|
+
// src/types/run-result-schema.ts
|
|
3705
|
+
function parseRunResult(value2) {
|
|
3706
|
+
return runResultSchema.parse(value2);
|
|
3651
3707
|
}
|
|
3652
|
-
var
|
|
3653
|
-
var
|
|
3654
|
-
"src/
|
|
3708
|
+
var import_zod3, RUN_RESULT_SCHEMA_VERSION, binaryVerdictSchema, assertionResultRecordSchema2, usageSchema, runErrorSchema, rowResultSchema, summarySchema, evalResultSchema, runResultSchema;
|
|
3709
|
+
var init_run_result_schema = __esm({
|
|
3710
|
+
"src/types/run-result-schema.ts"() {
|
|
3655
3711
|
"use strict";
|
|
3656
3712
|
init_cjs_shims();
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3713
|
+
import_zod3 = require("zod");
|
|
3714
|
+
init_results();
|
|
3715
|
+
RUN_RESULT_SCHEMA_VERSION = 1;
|
|
3716
|
+
binaryVerdictSchema = import_zod3.z.object({
|
|
3717
|
+
question: import_zod3.z.string().min(1),
|
|
3718
|
+
passed: import_zod3.z.boolean()
|
|
3719
|
+
});
|
|
3720
|
+
assertionResultRecordSchema2 = import_zod3.z.object({
|
|
3721
|
+
type: import_zod3.z.string(),
|
|
3722
|
+
passed: import_zod3.z.boolean(),
|
|
3723
|
+
score: import_zod3.z.number().optional(),
|
|
3724
|
+
reason: import_zod3.z.string().optional(),
|
|
3725
|
+
binaryVerdicts: import_zod3.z.array(binaryVerdictSchema).optional()
|
|
3726
|
+
});
|
|
3727
|
+
usageSchema = import_zod3.z.object({
|
|
3728
|
+
input_tokens: import_zod3.z.number(),
|
|
3729
|
+
output_tokens: import_zod3.z.number(),
|
|
3730
|
+
cost_usd: import_zod3.z.number()
|
|
3731
|
+
});
|
|
3732
|
+
runErrorSchema = import_zod3.z.object({
|
|
3733
|
+
code: import_zod3.z.string(),
|
|
3734
|
+
message: import_zod3.z.string()
|
|
3735
|
+
});
|
|
3736
|
+
rowResultSchema = import_zod3.z.object({
|
|
3737
|
+
vars: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()),
|
|
3738
|
+
output: import_zod3.z.string().nullable(),
|
|
3739
|
+
cached: import_zod3.z.boolean(),
|
|
3740
|
+
usage: usageSchema.optional(),
|
|
3741
|
+
latency_ms: import_zod3.z.number().optional(),
|
|
3742
|
+
error: runErrorSchema.optional(),
|
|
3743
|
+
assertions: import_zod3.z.array(assertionResultRecordSchema2),
|
|
3744
|
+
/** ADR 0045. Optional and additive — the same enum the platform validates on `POST /v1/runs`. */
|
|
3745
|
+
inconclusive: import_zod3.z.enum(ROW_INCONCLUSIVE).optional()
|
|
3746
|
+
});
|
|
3747
|
+
summarySchema = import_zod3.z.object({
|
|
3748
|
+
total: import_zod3.z.number(),
|
|
3749
|
+
passed: import_zod3.z.number(),
|
|
3750
|
+
failed: import_zod3.z.number(),
|
|
3751
|
+
errored: import_zod3.z.number(),
|
|
3752
|
+
/** plune#1245. Optional and additive — a RunResult written before them validates unchanged. */
|
|
3753
|
+
blocked: import_zod3.z.number().optional(),
|
|
3754
|
+
skipped: import_zod3.z.number().optional(),
|
|
3755
|
+
cost_usd: import_zod3.z.number(),
|
|
3756
|
+
duration_ms: import_zod3.z.number()
|
|
3757
|
+
});
|
|
3758
|
+
evalResultSchema = import_zod3.z.object({
|
|
3759
|
+
id: import_zod3.z.string(),
|
|
3760
|
+
tags: import_zod3.z.array(import_zod3.z.string()),
|
|
3761
|
+
rows: import_zod3.z.array(rowResultSchema),
|
|
3762
|
+
passed: import_zod3.z.boolean()
|
|
3763
|
+
});
|
|
3764
|
+
runResultSchema = import_zod3.z.object({
|
|
3765
|
+
schemaVersion: import_zod3.z.literal(RUN_RESULT_SCHEMA_VERSION),
|
|
3766
|
+
plune_version: import_zod3.z.string(),
|
|
3767
|
+
started_at: import_zod3.z.string(),
|
|
3768
|
+
finished_at: import_zod3.z.string(),
|
|
3769
|
+
config_hash: import_zod3.z.string(),
|
|
3770
|
+
summary: summarySchema,
|
|
3771
|
+
evals: import_zod3.z.array(evalResultSchema)
|
|
3772
|
+
});
|
|
3660
3773
|
}
|
|
3661
3774
|
});
|
|
3662
3775
|
|
|
3663
|
-
// src/
|
|
3664
|
-
|
|
3665
|
-
|
|
3776
|
+
// src/cli/commands/sync.ts
|
|
3777
|
+
var sync_exports = {};
|
|
3778
|
+
__export(sync_exports, {
|
|
3779
|
+
NotLoggedInError: () => NotLoggedInError,
|
|
3780
|
+
SyncFileError: () => SyncFileError,
|
|
3781
|
+
SyncHttpError: () => SyncHttpError,
|
|
3782
|
+
SyncNetworkError: () => SyncNetworkError,
|
|
3783
|
+
TokenRejectedError: () => TokenRejectedError,
|
|
3784
|
+
handleSync: () => handleSync,
|
|
3785
|
+
reportSyncFailure: () => reportSyncFailure
|
|
3786
|
+
});
|
|
3787
|
+
function readRun(cwd, file) {
|
|
3788
|
+
const target = file !== void 0 ? path9.resolve(cwd, file) : path9.join(cwd, ".plune", "last-run.json");
|
|
3789
|
+
let raw;
|
|
3666
3790
|
try {
|
|
3667
|
-
|
|
3791
|
+
raw = fs8.readFileSync(target, "utf8");
|
|
3668
3792
|
} catch {
|
|
3669
|
-
|
|
3793
|
+
throw new SyncFileError(
|
|
3794
|
+
file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
|
|
3795
|
+
);
|
|
3670
3796
|
}
|
|
3671
|
-
|
|
3672
|
-
function reportFolders(report) {
|
|
3673
|
-
return { lexical: path9.dirname(path9.resolve(report)), real: reportFolder(report) };
|
|
3674
|
-
}
|
|
3675
|
-
function within(folder, target) {
|
|
3676
|
-
const rel = path9.relative(folder, target);
|
|
3677
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel);
|
|
3678
|
-
}
|
|
3679
|
-
function resolveAttachment(folder, ref, lexical = folder) {
|
|
3680
|
-
if (/^[\\/]{2}/.test(ref) || ref.includes("\0")) return { why: "outside" };
|
|
3681
|
-
const face = path9.resolve(lexical, ref);
|
|
3682
|
-
if (!within(lexical, face) && !within(folder, face)) return { why: "outside" };
|
|
3683
|
-
let real;
|
|
3797
|
+
let json;
|
|
3684
3798
|
try {
|
|
3685
|
-
|
|
3799
|
+
json = JSON.parse(raw);
|
|
3686
3800
|
} catch {
|
|
3687
|
-
|
|
3801
|
+
throw new SyncFileError(`Run file is not valid JSON: ${target}`);
|
|
3688
3802
|
}
|
|
3689
|
-
if (!within(folder, real)) return { why: "outside" };
|
|
3690
3803
|
try {
|
|
3691
|
-
return
|
|
3804
|
+
return parseRunResult(json);
|
|
3692
3805
|
} catch {
|
|
3693
|
-
|
|
3806
|
+
throw new SyncFileError(`Run file does not match the RunResult schema: ${target}`);
|
|
3694
3807
|
}
|
|
3695
3808
|
}
|
|
3696
|
-
function
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
if (ref !== "") refs.add(ref);
|
|
3703
|
-
}
|
|
3809
|
+
async function safeErrorDetail(res) {
|
|
3810
|
+
try {
|
|
3811
|
+
const body = await res.json();
|
|
3812
|
+
return typeof body.error === "string" ? body.error : "";
|
|
3813
|
+
} catch {
|
|
3814
|
+
return "";
|
|
3704
3815
|
}
|
|
3705
|
-
return [...refs];
|
|
3706
3816
|
}
|
|
3707
|
-
function
|
|
3708
|
-
const
|
|
3709
|
-
const
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3817
|
+
async function handleSync(deps = {}) {
|
|
3818
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
3819
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
3820
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
3821
|
+
if (token === null) throw new NotLoggedInError();
|
|
3822
|
+
const run = readRun(cwd, deps.file);
|
|
3823
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
3824
|
+
const endpoint = `${apiUrl}/v1/runs`;
|
|
3825
|
+
let res;
|
|
3826
|
+
try {
|
|
3827
|
+
res = await doFetch(endpoint, {
|
|
3828
|
+
method: "POST",
|
|
3829
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
3830
|
+
body: JSON.stringify(run)
|
|
3831
|
+
});
|
|
3832
|
+
} catch {
|
|
3833
|
+
throw new SyncNetworkError(apiUrl);
|
|
3714
3834
|
}
|
|
3715
|
-
|
|
3716
|
-
const
|
|
3717
|
-
if (
|
|
3718
|
-
|
|
3719
|
-
continue;
|
|
3720
|
-
}
|
|
3721
|
-
const real = found.file;
|
|
3722
|
-
if (!files.some((f) => f.path === real)) {
|
|
3723
|
-
files.push({ name: printable(ref.split(/[\\/]/).pop() || path9.basename(real)), contentType: typeOfFile(real), path: real });
|
|
3724
|
-
}
|
|
3835
|
+
if (!res.ok) {
|
|
3836
|
+
const detail = await safeErrorDetail(res);
|
|
3837
|
+
if (res.status === 401) throw new TokenRejectedError();
|
|
3838
|
+
throw new SyncHttpError(res.status, detail);
|
|
3725
3839
|
}
|
|
3726
|
-
|
|
3840
|
+
const body = await res.json().catch(() => ({}));
|
|
3841
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
3842
|
+
const result = { id, url: `${endpoint}/${id}` };
|
|
3843
|
+
if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
|
|
3844
|
+
return result;
|
|
3727
3845
|
}
|
|
3728
|
-
|
|
3729
|
-
var init_attachments = __esm({
|
|
3730
|
-
"src/importers/attachments.ts"() {
|
|
3731
|
-
"use strict";
|
|
3732
|
-
init_cjs_shims();
|
|
3733
|
-
fs8 = __toESM(require("fs"), 1);
|
|
3734
|
-
path9 = __toESM(require("path"), 1);
|
|
3735
|
-
init_failure_detail();
|
|
3736
|
-
MARKER = /^[ \t]*\[\[ATTACHMENT\|([^\r\n]*?)\]\][ \t]*$/gm;
|
|
3737
|
-
TYPES = {
|
|
3738
|
-
".png": "image/png",
|
|
3739
|
-
".jpg": "image/jpeg",
|
|
3740
|
-
".jpeg": "image/jpeg",
|
|
3741
|
-
".webp": "image/webp",
|
|
3742
|
-
".gif": "image/gif",
|
|
3743
|
-
".svg": "image/svg+xml",
|
|
3744
|
-
".json": "application/json",
|
|
3745
|
-
".txt": "text/plain",
|
|
3746
|
-
".log": "text/plain",
|
|
3747
|
-
".md": "text/plain",
|
|
3748
|
-
".csv": "text/csv",
|
|
3749
|
-
".html": "text/html",
|
|
3750
|
-
".htm": "text/html",
|
|
3751
|
-
".xml": "application/xml",
|
|
3752
|
-
".pdf": "application/pdf",
|
|
3753
|
-
".zip": "application/zip",
|
|
3754
|
-
".webm": "video/webm",
|
|
3755
|
-
".mp4": "video/mp4",
|
|
3756
|
-
".har": "application/json"
|
|
3757
|
-
};
|
|
3758
|
-
typeOfFile = (file) => TYPES[path9.extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
3759
|
-
MARKERS_PER_TEST = 100;
|
|
3760
|
-
}
|
|
3761
|
-
});
|
|
3762
|
-
|
|
3763
|
-
// src/importers/allure.ts
|
|
3764
|
-
function isFolder(path17) {
|
|
3846
|
+
async function readVerdict(doFetch, url, token) {
|
|
3765
3847
|
try {
|
|
3766
|
-
|
|
3848
|
+
const res = await doFetch(url, { method: "GET", headers: { authorization: `Bearer ${token}` } });
|
|
3849
|
+
const body = await res.json().catch(() => void 0);
|
|
3850
|
+
return res.ok ? { statusEvent: body?.run?.statusEvent, counts: body?.counts, notRun: body?.notRun } : void 0;
|
|
3767
3851
|
} catch {
|
|
3768
|
-
return
|
|
3852
|
+
return void 0;
|
|
3769
3853
|
}
|
|
3770
3854
|
}
|
|
3771
|
-
function
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
} catch (err) {
|
|
3776
|
-
throw new AllureReportError(`${printable(file)} \u2014 cannot read \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
|
|
3777
|
-
}
|
|
3778
|
-
if (size > maxBytes) {
|
|
3779
|
-
throw new AllureReportError(`${printable(file)} \u2014 ${size} bytes, over the ${maxBytes} an Allure result may have; not read.`);
|
|
3780
|
-
}
|
|
3781
|
-
let parsed;
|
|
3782
|
-
try {
|
|
3783
|
-
parsed = JSON.parse(fs9.readFileSync(file, "utf8"));
|
|
3784
|
-
} catch (err) {
|
|
3785
|
-
throw new AllureReportError(`${printable(file)} \u2014 not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
|
|
3855
|
+
function reportSyncFailure(err, write) {
|
|
3856
|
+
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
|
|
3857
|
+
write(err.message + "\n");
|
|
3858
|
+
return 2;
|
|
3786
3859
|
}
|
|
3787
|
-
if (
|
|
3788
|
-
|
|
3860
|
+
if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
|
|
3861
|
+
write(err.message + "\n");
|
|
3862
|
+
return 1;
|
|
3789
3863
|
}
|
|
3790
|
-
return
|
|
3791
|
-
}
|
|
3792
|
-
function listOf(v) {
|
|
3793
|
-
const out = [];
|
|
3794
|
-
if (!Array.isArray(v)) return out;
|
|
3795
|
-
for (const x of v) if (typeof x === "object" && x !== null && !Array.isArray(x)) out.push(x);
|
|
3796
|
-
return out;
|
|
3864
|
+
return null;
|
|
3797
3865
|
}
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3866
|
+
var fs8, path9, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
3867
|
+
var init_sync = __esm({
|
|
3868
|
+
"src/cli/commands/sync.ts"() {
|
|
3869
|
+
"use strict";
|
|
3870
|
+
init_cjs_shims();
|
|
3871
|
+
fs8 = __toESM(require("fs"), 1);
|
|
3872
|
+
path9 = __toESM(require("path"), 1);
|
|
3873
|
+
init_run_result_schema();
|
|
3874
|
+
init_api_url();
|
|
3875
|
+
init_credentials();
|
|
3876
|
+
init_gate();
|
|
3877
|
+
NotLoggedInError = class extends Error {
|
|
3878
|
+
constructor() {
|
|
3879
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
3880
|
+
this.name = "NotLoggedInError";
|
|
3881
|
+
}
|
|
3882
|
+
};
|
|
3883
|
+
TokenRejectedError = class extends Error {
|
|
3884
|
+
constructor() {
|
|
3885
|
+
super(
|
|
3886
|
+
'Your API token was rejected (revoked or invalid). Run "plune login" with a fresh token.'
|
|
3887
|
+
);
|
|
3888
|
+
this.name = "TokenRejectedError";
|
|
3889
|
+
}
|
|
3890
|
+
};
|
|
3891
|
+
SyncFileError = class extends Error {
|
|
3892
|
+
constructor(message) {
|
|
3893
|
+
super(message);
|
|
3894
|
+
this.name = "SyncFileError";
|
|
3895
|
+
}
|
|
3896
|
+
};
|
|
3897
|
+
SyncNetworkError = class extends Error {
|
|
3898
|
+
constructor(apiUrl) {
|
|
3899
|
+
super(
|
|
3900
|
+
`Could not reach the Plune API at ${apiUrl}. Check your connection (or PLUNE_API_URL) and try again.`
|
|
3901
|
+
);
|
|
3902
|
+
this.name = "SyncNetworkError";
|
|
3903
|
+
}
|
|
3904
|
+
};
|
|
3905
|
+
SyncHttpError = class extends Error {
|
|
3906
|
+
constructor(status, detail) {
|
|
3907
|
+
super(`Sync failed \u2014 the server returned HTTP ${status}${detail ? ` (${detail})` : ""}.`);
|
|
3908
|
+
this.status = status;
|
|
3909
|
+
this.name = "SyncHttpError";
|
|
3910
|
+
}
|
|
3911
|
+
status;
|
|
3912
|
+
};
|
|
3913
|
+
}
|
|
3914
|
+
});
|
|
3915
|
+
|
|
3916
|
+
// src/cli/commands/plan.ts
|
|
3917
|
+
var plan_exports = {};
|
|
3918
|
+
__export(plan_exports, {
|
|
3919
|
+
RunRefError: () => RunRefError,
|
|
3920
|
+
formatGrep: () => formatGrep,
|
|
3921
|
+
grepFragment: () => grepFragment,
|
|
3922
|
+
handlePlanGrep: () => handlePlanGrep,
|
|
3923
|
+
handleRunGrep: () => handleRunGrep,
|
|
3924
|
+
runGrepSummary: () => runGrepSummary
|
|
3925
|
+
});
|
|
3926
|
+
function grepFragment(c) {
|
|
3927
|
+
const pathTitle = c.externalKeys.find((k) => k.kind === "path-title")?.value;
|
|
3928
|
+
const segments = pathTitle?.split("#") ?? [c.title];
|
|
3929
|
+
return (segments.length > 1 ? segments.slice(1) : segments).flatMap((s) => s.split(/ [>›] /)).map(escape).join(BOUNDARY);
|
|
3930
|
+
}
|
|
3931
|
+
function formatGrep(cases) {
|
|
3932
|
+
if (cases.length === 0) return "(?!)";
|
|
3933
|
+
return cases.map(grepFragment).join("|");
|
|
3934
|
+
}
|
|
3935
|
+
async function safeErrorDetail2(res) {
|
|
3936
|
+
try {
|
|
3937
|
+
const body = await res.json();
|
|
3938
|
+
return typeof body.error === "string" ? body.error : "";
|
|
3939
|
+
} catch {
|
|
3940
|
+
return "";
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
function platformGet(deps) {
|
|
3944
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
3945
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
3946
|
+
if (token === null) throw new NotLoggedInError();
|
|
3947
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
3948
|
+
return async (path17) => {
|
|
3949
|
+
let res;
|
|
3950
|
+
try {
|
|
3951
|
+
res = await doFetch(`${apiUrl}${path17}`, { headers: { authorization: `Bearer ${token}` } });
|
|
3952
|
+
} catch {
|
|
3953
|
+
throw new SyncNetworkError(apiUrl);
|
|
3954
|
+
}
|
|
3955
|
+
if (!res.ok) {
|
|
3956
|
+
const detail = await safeErrorDetail2(res);
|
|
3957
|
+
if (res.status === 401) throw new TokenRejectedError();
|
|
3958
|
+
throw new SyncHttpError(res.status, detail);
|
|
3959
|
+
}
|
|
3960
|
+
return await res.json();
|
|
3961
|
+
};
|
|
3962
|
+
}
|
|
3963
|
+
async function handlePlanGrep(deps) {
|
|
3964
|
+
const get = platformGet(deps);
|
|
3965
|
+
const { cases } = await get(
|
|
3966
|
+
`/v1/plans/${encodeURIComponent(deps.planId)}/cases`
|
|
3967
|
+
);
|
|
3968
|
+
return { pattern: formatGrep(cases), cases: cases.length, keyed: keyedOf(cases) };
|
|
3969
|
+
}
|
|
3970
|
+
async function handleRunGrep(deps) {
|
|
3971
|
+
const byKey = RUN_KEY.test(deps.run);
|
|
3972
|
+
if (!byKey && !RUN_ID.test(deps.run)) {
|
|
3973
|
+
throw new RunRefError(
|
|
3974
|
+
`${JSON.stringify(deps.run)} is not a run \u2014 pass its id (a UUID, as in the run page's address) or its key (rk_ and ten hex, the PLUNE_RUN Plune shows).`,
|
|
3975
|
+
2
|
|
3976
|
+
);
|
|
3977
|
+
}
|
|
3978
|
+
const get = platformGet(deps);
|
|
3979
|
+
let runId = deps.run;
|
|
3980
|
+
if (byKey) {
|
|
3981
|
+
const { runs } = await get(`/v1/runs?key=${deps.run}&limit=1`);
|
|
3982
|
+
const run = runs[0];
|
|
3983
|
+
if (run === void 0) {
|
|
3984
|
+
throw new RunRefError(
|
|
3985
|
+
`No run carries the key ${deps.run} \u2014 a key becomes a run's when the run is saved or launched in Plune.`,
|
|
3986
|
+
1
|
|
3987
|
+
);
|
|
3988
|
+
}
|
|
3989
|
+
runId = run.id;
|
|
3990
|
+
}
|
|
3991
|
+
const { cases } = await get(
|
|
3992
|
+
`/v1/runs/${encodeURIComponent(runId)}/cases${deps.failed === true ? "?status=failed,broken" : ""}`
|
|
3993
|
+
);
|
|
3994
|
+
const automated = cases.filter((c) => c.type === "automated");
|
|
3995
|
+
return {
|
|
3996
|
+
pattern: formatGrep(automated),
|
|
3997
|
+
cases: automated.length,
|
|
3998
|
+
keyed: keyedOf(automated),
|
|
3999
|
+
skipped: cases.length - automated.length
|
|
4000
|
+
};
|
|
4001
|
+
}
|
|
4002
|
+
function runGrepSummary(out, failed) {
|
|
4003
|
+
if (out.cases === 0) {
|
|
4004
|
+
const none = failed ? "no automated case failed or broke in the run" : "the run holds no automated case";
|
|
4005
|
+
return `plune: ${none} (${out.skipped} manual left out) \u2014 the pattern matches nothing.
|
|
4006
|
+
`;
|
|
4007
|
+
}
|
|
4008
|
+
const which = failed ? "that failed or broke in the run" : "of the run";
|
|
4009
|
+
return `plune: ${out.cases} automated case(s) ${which}, ${out.keyed} with a test key, the rest by title; ${out.skipped} manual left out.
|
|
4010
|
+
`;
|
|
4011
|
+
}
|
|
4012
|
+
var escape, BOUNDARY, keyedOf, RUN_ID, RUN_KEY, RunRefError;
|
|
4013
|
+
var init_plan = __esm({
|
|
4014
|
+
"src/cli/commands/plan.ts"() {
|
|
4015
|
+
"use strict";
|
|
4016
|
+
init_cjs_shims();
|
|
4017
|
+
init_api_url();
|
|
4018
|
+
init_credentials();
|
|
4019
|
+
init_sync();
|
|
4020
|
+
escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4021
|
+
BOUNDARY = "[ >#]+";
|
|
4022
|
+
keyedOf = (cases) => cases.filter((c) => c.externalKeys.some((k) => k.kind === "path-title")).length;
|
|
4023
|
+
RUN_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4024
|
+
RUN_KEY = /^rk_[0-9a-f]{10}$/;
|
|
4025
|
+
RunRefError = class extends Error {
|
|
4026
|
+
constructor(message, exitCode) {
|
|
4027
|
+
super(message);
|
|
4028
|
+
this.exitCode = exitCode;
|
|
4029
|
+
this.name = "RunRefError";
|
|
4030
|
+
}
|
|
4031
|
+
exitCode;
|
|
4032
|
+
};
|
|
4033
|
+
}
|
|
4034
|
+
});
|
|
4035
|
+
|
|
4036
|
+
// src/cli/commands/cases.ts
|
|
4037
|
+
var cases_exports = {};
|
|
4038
|
+
__export(cases_exports, {
|
|
4039
|
+
DEFAULT_CASES_FILE: () => DEFAULT_CASES_FILE,
|
|
4040
|
+
DirtyFileError: () => DirtyFileError,
|
|
4041
|
+
formatReport: () => formatReport,
|
|
4042
|
+
handlePull: () => handlePull,
|
|
4043
|
+
handlePush: () => handlePush,
|
|
4044
|
+
reportCasesFailure: () => reportCasesFailure
|
|
4045
|
+
});
|
|
4046
|
+
function gitDirty(file) {
|
|
4047
|
+
try {
|
|
4048
|
+
const out = (0, import_node_child_process2.execFileSync)("git", ["status", "--porcelain", "--", file], {
|
|
4049
|
+
cwd: path10.dirname(file),
|
|
4050
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
4051
|
+
encoding: "utf8"
|
|
4052
|
+
});
|
|
4053
|
+
return out.trim() !== "";
|
|
4054
|
+
} catch {
|
|
4055
|
+
return false;
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
async function safeErrorDetail3(res) {
|
|
4059
|
+
try {
|
|
4060
|
+
const body = await res.json();
|
|
4061
|
+
return typeof body.error === "string" ? body.error : "";
|
|
4062
|
+
} catch {
|
|
4063
|
+
return "";
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
function tokenOf(deps) {
|
|
4067
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
4068
|
+
if (token === null) throw new NotLoggedInError();
|
|
4069
|
+
return token;
|
|
4070
|
+
}
|
|
4071
|
+
async function handlePull(deps = {}) {
|
|
4072
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
4073
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4074
|
+
const token = tokenOf(deps);
|
|
4075
|
+
const file = path10.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
4076
|
+
if (fs9.existsSync(file) && !deps.force && (deps.isDirty ?? gitDirty)(file))
|
|
4077
|
+
throw new DirtyFileError(path10.relative(cwd, file) || file);
|
|
4078
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
4079
|
+
const endpoint = deps.suiteId !== void 0 ? `${apiUrl}/v1/suites/${encodeURIComponent(deps.suiteId)}/markdown` : `${apiUrl}/v1/test-cases/markdown`;
|
|
4080
|
+
let res;
|
|
4081
|
+
try {
|
|
4082
|
+
res = await doFetch(endpoint, { headers: { authorization: `Bearer ${token}` } });
|
|
4083
|
+
} catch {
|
|
4084
|
+
throw new SyncNetworkError(apiUrl);
|
|
4085
|
+
}
|
|
4086
|
+
if (!res.ok) {
|
|
4087
|
+
const detail = await safeErrorDetail3(res);
|
|
4088
|
+
if (res.status === 401) throw new TokenRejectedError();
|
|
4089
|
+
throw new SyncHttpError(res.status, detail);
|
|
4090
|
+
}
|
|
4091
|
+
const text2 = await res.text();
|
|
4092
|
+
fs9.mkdirSync(path10.dirname(file), { recursive: true });
|
|
4093
|
+
fs9.writeFileSync(file, text2, "utf8");
|
|
4094
|
+
return { file, cases: Number(res.headers.get("x-plune-cases") ?? "0") };
|
|
4095
|
+
}
|
|
4096
|
+
async function handlePush(deps = {}) {
|
|
4097
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
4098
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4099
|
+
const token = tokenOf(deps);
|
|
4100
|
+
const file = path10.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
4101
|
+
let text2;
|
|
4102
|
+
try {
|
|
4103
|
+
text2 = fs9.readFileSync(file, "utf8");
|
|
4104
|
+
} catch {
|
|
4105
|
+
throw new SyncFileError(
|
|
4106
|
+
`No document to push: ${file}. Run "plune pull" first (or pass a file).`
|
|
4107
|
+
);
|
|
4108
|
+
}
|
|
4109
|
+
if (text2.trim() === "") throw new SyncFileError(`The document is empty: ${file}`);
|
|
4110
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
4111
|
+
let res;
|
|
4112
|
+
try {
|
|
4113
|
+
res = await doFetch(`${apiUrl}/v1/test-cases/markdown${deps.dryRun ? "?dryRun=true" : ""}`, {
|
|
4114
|
+
method: "POST",
|
|
4115
|
+
headers: { "content-type": "text/markdown", authorization: `Bearer ${token}` },
|
|
4116
|
+
body: text2
|
|
4117
|
+
});
|
|
4118
|
+
} catch {
|
|
4119
|
+
throw new SyncNetworkError(apiUrl);
|
|
4120
|
+
}
|
|
4121
|
+
if (!res.ok) {
|
|
4122
|
+
const detail = await safeErrorDetail3(res);
|
|
4123
|
+
if (res.status === 401) throw new TokenRejectedError();
|
|
4124
|
+
throw new SyncHttpError(res.status, detail);
|
|
4125
|
+
}
|
|
4126
|
+
return await res.json();
|
|
4127
|
+
}
|
|
4128
|
+
function formatReport(report) {
|
|
4129
|
+
const verb = (past, future) => report.dryRun ? future : past;
|
|
4130
|
+
const lines = [
|
|
4131
|
+
`${report.dryRun ? "Dry run \u2014 nothing written." : "Pushed."} ${report.created.length} ${verb("created", "to create")} \xB7 ${report.updated.length} ${verb("updated", "to update")} \xB7 ${report.unchanged.length} unchanged \xB7 ${report.refused.length} refused` + (report.suites.created.length > 0 ? ` \xB7 ${report.suites.created.length} suite(s) ${verb("created", "to create")}` : "")
|
|
4132
|
+
];
|
|
4133
|
+
for (const r of report.refused)
|
|
4134
|
+
lines.push(` refused line ${r.line} ${r.title || "(no title)"} \u2014 ${r.error}`);
|
|
4135
|
+
for (const w of report.warnings)
|
|
4136
|
+
lines.push(` warning ${w.line !== void 0 ? `line ${w.line} ` : ""}${w.message}`);
|
|
4137
|
+
return lines.join("\n") + "\n";
|
|
4138
|
+
}
|
|
4139
|
+
function reportCasesFailure(err, write) {
|
|
4140
|
+
if (err instanceof DirtyFileError) {
|
|
4141
|
+
write(err.message + "\n");
|
|
4142
|
+
return 3;
|
|
4143
|
+
}
|
|
4144
|
+
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
|
|
4145
|
+
write(err.message + "\n");
|
|
4146
|
+
return 2;
|
|
4147
|
+
}
|
|
4148
|
+
if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
|
|
4149
|
+
write(err.message + "\n");
|
|
4150
|
+
return 1;
|
|
4151
|
+
}
|
|
4152
|
+
return null;
|
|
4153
|
+
}
|
|
4154
|
+
var import_node_child_process2, fs9, path10, DEFAULT_CASES_FILE, DirtyFileError;
|
|
4155
|
+
var init_cases = __esm({
|
|
4156
|
+
"src/cli/commands/cases.ts"() {
|
|
4157
|
+
"use strict";
|
|
4158
|
+
init_cjs_shims();
|
|
4159
|
+
import_node_child_process2 = require("child_process");
|
|
4160
|
+
fs9 = __toESM(require("fs"), 1);
|
|
4161
|
+
path10 = __toESM(require("path"), 1);
|
|
4162
|
+
init_api_url();
|
|
4163
|
+
init_credentials();
|
|
4164
|
+
init_sync();
|
|
4165
|
+
DEFAULT_CASES_FILE = path10.join("plune", "cases.md");
|
|
4166
|
+
DirtyFileError = class extends Error {
|
|
4167
|
+
constructor(file) {
|
|
4168
|
+
super(`${file} has uncommitted changes; commit them or pass --force to overwrite.`);
|
|
4169
|
+
this.name = "DirtyFileError";
|
|
4170
|
+
}
|
|
4171
|
+
};
|
|
4172
|
+
}
|
|
4173
|
+
});
|
|
4174
|
+
|
|
4175
|
+
// src/reporter-core/result-key.ts
|
|
4176
|
+
function resultKey(testId, retry) {
|
|
4177
|
+
const plain = `${testId}#${retry}`;
|
|
4178
|
+
return plain.length <= RESULT_KEY_MAX ? plain : `${sha256(testId).slice(0, HASH_CHARS)}#${retry}`;
|
|
4179
|
+
}
|
|
4180
|
+
var RESULT_KEY_MAX, HASH_CHARS;
|
|
4181
|
+
var init_result_key = __esm({
|
|
4182
|
+
"src/reporter-core/result-key.ts"() {
|
|
4183
|
+
"use strict";
|
|
4184
|
+
init_cjs_shims();
|
|
4185
|
+
init_hash();
|
|
4186
|
+
RESULT_KEY_MAX = 200;
|
|
4187
|
+
HASH_CHARS = 32;
|
|
4188
|
+
}
|
|
4189
|
+
});
|
|
4190
|
+
|
|
4191
|
+
// src/importers/attachments.ts
|
|
4192
|
+
function reportFolder(report) {
|
|
4193
|
+
const folder = path11.dirname(path11.resolve(report));
|
|
4194
|
+
try {
|
|
4195
|
+
return fs10.realpathSync(folder);
|
|
4196
|
+
} catch {
|
|
4197
|
+
return folder;
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
function reportFolders(report) {
|
|
4201
|
+
return { lexical: path11.dirname(path11.resolve(report)), real: reportFolder(report) };
|
|
4202
|
+
}
|
|
4203
|
+
function within(folder, target) {
|
|
4204
|
+
const rel = path11.relative(folder, target);
|
|
4205
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${path11.sep}`) && !path11.isAbsolute(rel);
|
|
4206
|
+
}
|
|
4207
|
+
function resolveAttachment(folder, ref, lexical = folder) {
|
|
4208
|
+
if (/^[\\/]{2}/.test(ref) || ref.includes("\0")) return { why: "outside" };
|
|
4209
|
+
const face = path11.resolve(lexical, ref);
|
|
4210
|
+
if (!within(lexical, face) && !within(folder, face)) return { why: "outside" };
|
|
4211
|
+
let real;
|
|
4212
|
+
try {
|
|
4213
|
+
real = fs10.realpathSync(path11.resolve(folder, ref));
|
|
4214
|
+
} catch {
|
|
4215
|
+
return { why: "absent" };
|
|
4216
|
+
}
|
|
4217
|
+
if (!within(folder, real)) return { why: "outside" };
|
|
4218
|
+
try {
|
|
4219
|
+
return fs10.statSync(real).isFile() ? { file: real } : { why: "not-a-file" };
|
|
4220
|
+
} catch {
|
|
4221
|
+
return { why: "absent" };
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
function markersOf(test) {
|
|
4225
|
+
const refs = /* @__PURE__ */ new Set();
|
|
4226
|
+
for (const stream of test.children) {
|
|
4227
|
+
if (stream.name !== "system-out" && stream.name !== "system-err") continue;
|
|
4228
|
+
for (const match of stream.text.matchAll(MARKER)) {
|
|
4229
|
+
const ref = match[1]?.trim() ?? "";
|
|
4230
|
+
if (ref !== "") refs.add(ref);
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
return [...refs];
|
|
4234
|
+
}
|
|
4235
|
+
function filesOfTest(test, folder, title, missing, options = {}) {
|
|
4236
|
+
const files = [];
|
|
4237
|
+
const all = markersOf(test);
|
|
4238
|
+
if (all.length > MARKERS_PER_TEST) {
|
|
4239
|
+
options.notes?.push(
|
|
4240
|
+
`plune: ${JSON.stringify(printable(title))}: ${all.length - MARKERS_PER_TEST} more markers ignored (at most ${MARKERS_PER_TEST} per test)`
|
|
4241
|
+
);
|
|
4242
|
+
}
|
|
4243
|
+
for (const ref of all.slice(0, MARKERS_PER_TEST)) {
|
|
4244
|
+
const found = resolveAttachment(folder, ref, options.lexical);
|
|
4245
|
+
if (!("file" in found)) {
|
|
4246
|
+
missing.push({ title, path: ref, ...options.resultKey !== void 0 ? { resultKey: options.resultKey } : {} });
|
|
4247
|
+
continue;
|
|
4248
|
+
}
|
|
4249
|
+
const real = found.file;
|
|
4250
|
+
if (!files.some((f) => f.path === real)) {
|
|
4251
|
+
files.push({ name: printable(ref.split(/[\\/]/).pop() || path11.basename(real)), contentType: typeOfFile(real), path: real });
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
return files;
|
|
4255
|
+
}
|
|
4256
|
+
var fs10, path11, MARKER, TYPES, typeOfFile, MARKERS_PER_TEST;
|
|
4257
|
+
var init_attachments = __esm({
|
|
4258
|
+
"src/importers/attachments.ts"() {
|
|
4259
|
+
"use strict";
|
|
4260
|
+
init_cjs_shims();
|
|
4261
|
+
fs10 = __toESM(require("fs"), 1);
|
|
4262
|
+
path11 = __toESM(require("path"), 1);
|
|
4263
|
+
init_failure_detail();
|
|
4264
|
+
MARKER = /^[ \t]*\[\[ATTACHMENT\|([^\r\n]*?)\]\][ \t]*$/gm;
|
|
4265
|
+
TYPES = {
|
|
4266
|
+
".png": "image/png",
|
|
4267
|
+
".jpg": "image/jpeg",
|
|
4268
|
+
".jpeg": "image/jpeg",
|
|
4269
|
+
".webp": "image/webp",
|
|
4270
|
+
".gif": "image/gif",
|
|
4271
|
+
".svg": "image/svg+xml",
|
|
4272
|
+
".json": "application/json",
|
|
4273
|
+
".txt": "text/plain",
|
|
4274
|
+
".log": "text/plain",
|
|
4275
|
+
".md": "text/plain",
|
|
4276
|
+
".csv": "text/csv",
|
|
4277
|
+
".html": "text/html",
|
|
4278
|
+
".htm": "text/html",
|
|
4279
|
+
".xml": "application/xml",
|
|
4280
|
+
".pdf": "application/pdf",
|
|
4281
|
+
".zip": "application/zip",
|
|
4282
|
+
".webm": "video/webm",
|
|
4283
|
+
".mp4": "video/mp4",
|
|
4284
|
+
".har": "application/json"
|
|
4285
|
+
};
|
|
4286
|
+
typeOfFile = (file) => TYPES[path11.extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
4287
|
+
MARKERS_PER_TEST = 100;
|
|
4288
|
+
}
|
|
4289
|
+
});
|
|
4290
|
+
|
|
4291
|
+
// src/importers/allure.ts
|
|
4292
|
+
function isFolder(path17) {
|
|
4293
|
+
try {
|
|
4294
|
+
return fs11.statSync(path17).isDirectory();
|
|
4295
|
+
} catch {
|
|
4296
|
+
return false;
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
function load(file, maxBytes) {
|
|
4300
|
+
let size;
|
|
4301
|
+
try {
|
|
4302
|
+
size = fs11.statSync(file).size;
|
|
4303
|
+
} catch (err) {
|
|
4304
|
+
throw new AllureReportError(`${printable(file)} \u2014 cannot read \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
|
|
4305
|
+
}
|
|
4306
|
+
if (size > maxBytes) {
|
|
4307
|
+
throw new AllureReportError(`${printable(file)} \u2014 ${size} bytes, over the ${maxBytes} an Allure result may have; not read.`);
|
|
4308
|
+
}
|
|
4309
|
+
let parsed;
|
|
4310
|
+
try {
|
|
4311
|
+
parsed = JSON.parse(fs11.readFileSync(file, "utf8"));
|
|
4312
|
+
} catch (err) {
|
|
4313
|
+
throw new AllureReportError(`${printable(file)} \u2014 not JSON \u2014 ${printable(err instanceof Error ? err.message : String(err))}`);
|
|
4314
|
+
}
|
|
4315
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4316
|
+
throw new AllureReportError(`${printable(file)} \u2014 not an Allure result: expected a JSON object.`);
|
|
4317
|
+
}
|
|
4318
|
+
return parsed;
|
|
4319
|
+
}
|
|
4320
|
+
function listOf(v) {
|
|
4321
|
+
const out = [];
|
|
4322
|
+
if (!Array.isArray(v)) return out;
|
|
4323
|
+
for (const x of v) if (typeof x === "object" && x !== null && !Array.isArray(x)) out.push(x);
|
|
4324
|
+
return out;
|
|
4325
|
+
}
|
|
4326
|
+
function keyOf(r, name) {
|
|
4327
|
+
const full = textOf(r.fullName);
|
|
4328
|
+
if (name.endsWith("]") && full.includes("#")) return `${full.split("#")[0]}#${name}`;
|
|
3801
4329
|
let base;
|
|
3802
4330
|
if (full.includes(" \u203A ")) base = full.split(" \u203A ").join("#");
|
|
3803
4331
|
else if (full.includes("#")) base = full;
|
|
@@ -3837,9 +4365,9 @@ function readAllure(dir, missing = [], notes = [], maxBytes = MAX_JSON_BYTES) {
|
|
|
3837
4365
|
`${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.`
|
|
3838
4366
|
);
|
|
3839
4367
|
}
|
|
3840
|
-
const { lexical, real: folder } = reportFolders(
|
|
3841
|
-
const tests = resultNames.map((n) => load(
|
|
3842
|
-
const containers = namesIn(dir, CONTAINER).map((n) => load(
|
|
4368
|
+
const { lexical, real: folder } = reportFolders(path12.join(dir, "_"));
|
|
4369
|
+
const tests = resultNames.map((n) => load(path12.join(dir, n), maxBytes));
|
|
4370
|
+
const containers = namesIn(dir, CONTAINER).map((n) => load(path12.join(dir, n), maxBytes));
|
|
3843
4371
|
const direct = /* @__PURE__ */ new Map();
|
|
3844
4372
|
for (const c of containers) {
|
|
3845
4373
|
for (const child of Array.isArray(c.children) ? c.children : []) {
|
|
@@ -3907,7 +4435,7 @@ function pendingOf(r, retry, ctx) {
|
|
|
3907
4435
|
}
|
|
3908
4436
|
if (files.some((f) => f.path === found.file)) return;
|
|
3909
4437
|
const type = textOf(att.type).trim();
|
|
3910
|
-
const name2 = printable(textOf(att.name) !== "" ? textOf(att.name) :
|
|
4438
|
+
const name2 = printable(textOf(att.name) !== "" ? textOf(att.name) : path12.basename(found.file));
|
|
3911
4439
|
files.push({ name: name2, contentType: type !== "" ? type : typeOfFile(found.file), path: found.file });
|
|
3912
4440
|
};
|
|
3913
4441
|
const holders = [];
|
|
@@ -3966,13 +4494,13 @@ function pendingOf(r, retry, ctx) {
|
|
|
3966
4494
|
...files.length > 0 ? { files } : {}
|
|
3967
4495
|
};
|
|
3968
4496
|
}
|
|
3969
|
-
var
|
|
4497
|
+
var fs11, path12, SOURCE, TOKEN, AllureReportError, RESULT, CONTAINER, MAX_JSON_BYTES, MAX_DEPTH, MAX_TIME, namesIn, timeOf, textOf, labelOf, FAILED, declared, stopOf, compareText;
|
|
3970
4498
|
var init_allure = __esm({
|
|
3971
4499
|
"src/importers/allure.ts"() {
|
|
3972
4500
|
"use strict";
|
|
3973
4501
|
init_cjs_shims();
|
|
3974
|
-
|
|
3975
|
-
|
|
4502
|
+
fs11 = __toESM(require("fs"), 1);
|
|
4503
|
+
path12 = __toESM(require("path"), 1);
|
|
3976
4504
|
init_failure_detail();
|
|
3977
4505
|
init_result_key();
|
|
3978
4506
|
init_attachments();
|
|
@@ -3989,7 +4517,7 @@ var init_allure = __esm({
|
|
|
3989
4517
|
MAX_JSON_BYTES = 64 * 1024 * 1024;
|
|
3990
4518
|
MAX_DEPTH = 256;
|
|
3991
4519
|
MAX_TIME = 864e13;
|
|
3992
|
-
namesIn = (dir, pattern) =>
|
|
4520
|
+
namesIn = (dir, pattern) => fs11.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile() && pattern.test(d.name)).map((d) => d.name).sort();
|
|
3993
4521
|
timeOf = (v) => typeof v === "number" && Math.abs(v) <= MAX_TIME ? v : void 0;
|
|
3994
4522
|
textOf = (v) => typeof v === "string" ? v : "";
|
|
3995
4523
|
labelOf = (r, name) => textOf(listOf(r.labels).find((l) => l.name === name)?.value);
|
|
@@ -4463,7 +4991,7 @@ async function handleRunImport(options) {
|
|
|
4463
4991
|
} else {
|
|
4464
4992
|
let source;
|
|
4465
4993
|
try {
|
|
4466
|
-
source =
|
|
4994
|
+
source = fs12.readFileSync(options.file, "utf8");
|
|
4467
4995
|
} catch {
|
|
4468
4996
|
throw new RunCommandError(`Cannot read ${options.file}.`);
|
|
4469
4997
|
}
|
|
@@ -4487,6 +5015,7 @@ async function handleRunImport(options) {
|
|
|
4487
5015
|
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
|
|
4488
5016
|
...options.key !== void 0 ? { externalKey: options.key } : {},
|
|
4489
5017
|
...options.create === true ? { offerDiscovered: true } : {},
|
|
5018
|
+
...options.uploadConcurrency !== void 0 ? { uploadConcurrency: options.uploadConcurrency } : {},
|
|
4490
5019
|
kind: "automated",
|
|
4491
5020
|
// The CI run the report names, never this machine's: whoever imports may be another job (#790).
|
|
4492
5021
|
// The commit and branch likewise, where the report names a commit; a report that names none —
|
|
@@ -4594,12 +5123,12 @@ function describe(result, apiUrl) {
|
|
|
4594
5123
|
if (result.runId !== null) lines.push(runUrl2(apiUrl, result.runId));
|
|
4595
5124
|
return lines;
|
|
4596
5125
|
}
|
|
4597
|
-
var
|
|
5126
|
+
var fs12, NEXT;
|
|
4598
5127
|
var init_run_import = __esm({
|
|
4599
5128
|
"src/cli/commands/run-import.ts"() {
|
|
4600
5129
|
"use strict";
|
|
4601
5130
|
init_cjs_shims();
|
|
4602
|
-
|
|
5131
|
+
fs12 = __toESM(require("fs"), 1);
|
|
4603
5132
|
init_session();
|
|
4604
5133
|
init_failure_detail();
|
|
4605
5134
|
init_importers();
|
|
@@ -4629,10 +5158,10 @@ function isRunResult(v) {
|
|
|
4629
5158
|
}
|
|
4630
5159
|
function handleReport(opts = {}) {
|
|
4631
5160
|
const cwd = opts.cwd ?? process.cwd();
|
|
4632
|
-
const file =
|
|
5161
|
+
const file = path13.join(cwd, ".plune", "last-run.json");
|
|
4633
5162
|
let raw;
|
|
4634
5163
|
try {
|
|
4635
|
-
raw =
|
|
5164
|
+
raw = fs13.readFileSync(file, "utf8");
|
|
4636
5165
|
} catch {
|
|
4637
5166
|
throw new ReportNotFoundError('No saved run found. Run "plune run" first.');
|
|
4638
5167
|
}
|
|
@@ -4647,13 +5176,13 @@ function handleReport(opts = {}) {
|
|
|
4647
5176
|
}
|
|
4648
5177
|
return parsed;
|
|
4649
5178
|
}
|
|
4650
|
-
var
|
|
5179
|
+
var fs13, path13, ReportNotFoundError;
|
|
4651
5180
|
var init_report = __esm({
|
|
4652
5181
|
"src/cli/commands/report.ts"() {
|
|
4653
5182
|
"use strict";
|
|
4654
5183
|
init_cjs_shims();
|
|
4655
|
-
|
|
4656
|
-
|
|
5184
|
+
fs13 = __toESM(require("fs"), 1);
|
|
5185
|
+
path13 = __toESM(require("path"), 1);
|
|
4657
5186
|
ReportNotFoundError = class extends Error {
|
|
4658
5187
|
code = "NO_SAVED_RUN";
|
|
4659
5188
|
constructor(message) {
|
|
@@ -4723,7 +5252,7 @@ function isRunResult2(v) {
|
|
|
4723
5252
|
function loadRunResult(file, label) {
|
|
4724
5253
|
let raw;
|
|
4725
5254
|
try {
|
|
4726
|
-
raw =
|
|
5255
|
+
raw = fs14.readFileSync(file, "utf8");
|
|
4727
5256
|
} catch {
|
|
4728
5257
|
throw new DiffInputError(`Cannot read ${label} run file: ${file}`);
|
|
4729
5258
|
}
|
|
@@ -4743,12 +5272,12 @@ function handleDiff(opts) {
|
|
|
4743
5272
|
const current = loadRunResult(opts.currentPath, "current");
|
|
4744
5273
|
return diffRuns(baseline, current);
|
|
4745
5274
|
}
|
|
4746
|
-
var
|
|
5275
|
+
var fs14, DiffInputError;
|
|
4747
5276
|
var init_diff2 = __esm({
|
|
4748
5277
|
"src/cli/commands/diff.ts"() {
|
|
4749
5278
|
"use strict";
|
|
4750
5279
|
init_cjs_shims();
|
|
4751
|
-
|
|
5280
|
+
fs14 = __toESM(require("fs"), 1);
|
|
4752
5281
|
init_diff();
|
|
4753
5282
|
DiffInputError = class extends Error {
|
|
4754
5283
|
code = "DIFF_INPUT";
|
|
@@ -4873,8 +5402,8 @@ async function runInitWizard(cwd) {
|
|
|
4873
5402
|
process.exit(1);
|
|
4874
5403
|
return;
|
|
4875
5404
|
}
|
|
4876
|
-
const outputPath =
|
|
4877
|
-
if (
|
|
5405
|
+
const outputPath = path14.join(cwd, "plune.yaml");
|
|
5406
|
+
if (fs15.existsSync(outputPath)) {
|
|
4878
5407
|
const overwrite = await clack.confirm({
|
|
4879
5408
|
message: "plune.yaml already exists. Overwrite?"
|
|
4880
5409
|
});
|
|
@@ -4913,18 +5442,18 @@ async function runInitWizard(cwd) {
|
|
|
4913
5442
|
`;
|
|
4914
5443
|
const spin = clack.spinner();
|
|
4915
5444
|
spin.start("Writing plune.yaml\u2026");
|
|
4916
|
-
|
|
5445
|
+
fs15.writeFileSync(outputPath, header + "\n" + (0, import_yaml2.stringify)(config), "utf8");
|
|
4917
5446
|
spin.stop("plune.yaml created!");
|
|
4918
5447
|
clack.outro("plune.yaml is ready.");
|
|
4919
5448
|
}
|
|
4920
|
-
var clack,
|
|
5449
|
+
var clack, fs15, path14, import_yaml2;
|
|
4921
5450
|
var init_wizard = __esm({
|
|
4922
5451
|
"src/config/init/wizard.ts"() {
|
|
4923
5452
|
"use strict";
|
|
4924
5453
|
init_cjs_shims();
|
|
4925
5454
|
clack = __toESM(require("@clack/prompts"), 1);
|
|
4926
|
-
|
|
4927
|
-
|
|
5455
|
+
fs15 = __toESM(require("fs"), 1);
|
|
5456
|
+
path14 = __toESM(require("path"), 1);
|
|
4928
5457
|
import_yaml2 = require("yaml");
|
|
4929
5458
|
init_errors();
|
|
4930
5459
|
}
|
|
@@ -4932,21 +5461,21 @@ var init_wizard = __esm({
|
|
|
4932
5461
|
|
|
4933
5462
|
// src/cli/templates/index.ts
|
|
4934
5463
|
function writeTemplateFile(cwd, rel, content, force) {
|
|
4935
|
-
const target =
|
|
4936
|
-
if (!force &&
|
|
5464
|
+
const target = path15.join(cwd, rel);
|
|
5465
|
+
if (!force && fs16.existsSync(target)) {
|
|
4937
5466
|
return "skipped";
|
|
4938
5467
|
}
|
|
4939
|
-
|
|
4940
|
-
|
|
5468
|
+
fs16.mkdirSync(path15.dirname(target), { recursive: true });
|
|
5469
|
+
fs16.writeFileSync(target, content, "utf8");
|
|
4941
5470
|
return "written";
|
|
4942
5471
|
}
|
|
4943
|
-
var
|
|
5472
|
+
var fs16, path15, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
|
|
4944
5473
|
var init_templates = __esm({
|
|
4945
5474
|
"src/cli/templates/index.ts"() {
|
|
4946
5475
|
"use strict";
|
|
4947
5476
|
init_cjs_shims();
|
|
4948
|
-
|
|
4949
|
-
|
|
5477
|
+
fs16 = __toESM(require("fs"), 1);
|
|
5478
|
+
path15 = __toESM(require("path"), 1);
|
|
4950
5479
|
ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
|
|
4951
5480
|
# .env is gitignored; .env.example is safe to commit.
|
|
4952
5481
|
# Plune reads whichever key matches your configured provider.
|
|
@@ -5032,7 +5561,7 @@ __export(login_exports, {
|
|
|
5032
5561
|
EmptyTokenError: () => EmptyTokenError,
|
|
5033
5562
|
LoginHttpError: () => LoginHttpError,
|
|
5034
5563
|
LoginNetworkError: () => LoginNetworkError,
|
|
5035
|
-
TokenRejectedError: () =>
|
|
5564
|
+
TokenRejectedError: () => TokenRejectedError2,
|
|
5036
5565
|
handleLogin: () => handleLogin,
|
|
5037
5566
|
reportLoginFailure: () => reportLoginFailure
|
|
5038
5567
|
});
|
|
@@ -5051,12 +5580,12 @@ async function handleLogin(deps) {
|
|
|
5051
5580
|
throw new LoginNetworkError(apiUrl);
|
|
5052
5581
|
}
|
|
5053
5582
|
await res.text().catch(() => "");
|
|
5054
|
-
if (res.status === 401) throw new
|
|
5583
|
+
if (res.status === 401) throw new TokenRejectedError2(apiUrl);
|
|
5055
5584
|
if (!res.ok) throw new LoginHttpError(res.status);
|
|
5056
5585
|
return { path: saveToken(token), verified: true };
|
|
5057
5586
|
}
|
|
5058
5587
|
function reportLoginFailure(err, write) {
|
|
5059
|
-
if (err instanceof EmptyTokenError || err instanceof
|
|
5588
|
+
if (err instanceof EmptyTokenError || err instanceof TokenRejectedError2) {
|
|
5060
5589
|
write(`${err.message}
|
|
5061
5590
|
`);
|
|
5062
5591
|
return 2;
|
|
@@ -5067,285 +5596,73 @@ function reportLoginFailure(err, write) {
|
|
|
5067
5596
|
return 1;
|
|
5068
5597
|
}
|
|
5069
5598
|
return null;
|
|
5070
|
-
}
|
|
5071
|
-
var EmptyTokenError,
|
|
5072
|
-
var init_login = __esm({
|
|
5073
|
-
"src/cli/commands/login.ts"() {
|
|
5074
|
-
"use strict";
|
|
5075
|
-
init_cjs_shims();
|
|
5076
|
-
init_api_url();
|
|
5077
|
-
init_credentials();
|
|
5078
|
-
EmptyTokenError = class extends Error {
|
|
5079
|
-
constructor(apiUrl = resolveApiUrl()) {
|
|
5080
|
-
super(
|
|
5081
|
-
`No API token provided. ${whereToGetAToken(apiUrl)} Then pass --token <token> or pipe it via stdin.`
|
|
5082
|
-
);
|
|
5083
|
-
this.name = "EmptyTokenError";
|
|
5084
|
-
}
|
|
5085
|
-
};
|
|
5086
|
-
TokenRejectedError = class extends Error {
|
|
5087
|
-
constructor(apiUrl = resolveApiUrl()) {
|
|
5088
|
-
super(`That API token was rejected (401). Nothing was saved. ${whereToGetAToken(apiUrl)}`);
|
|
5089
|
-
this.name = "TokenRejectedError";
|
|
5090
|
-
}
|
|
5091
|
-
};
|
|
5092
|
-
LoginNetworkError = class extends Error {
|
|
5093
|
-
constructor(apiUrl) {
|
|
5094
|
-
super(
|
|
5095
|
-
`Could not reach the Plune API at ${apiUrl}, so the token was not checked and NOT saved. Try again, or use --skip-verify to save it without checking.`
|
|
5096
|
-
);
|
|
5097
|
-
this.name = "LoginNetworkError";
|
|
5098
|
-
}
|
|
5099
|
-
};
|
|
5100
|
-
LoginHttpError = class extends Error {
|
|
5101
|
-
constructor(status) {
|
|
5102
|
-
super(
|
|
5103
|
-
`The Plune API returned ${status} while checking the token, so it was NOT saved. Try again.`
|
|
5104
|
-
);
|
|
5105
|
-
this.name = "LoginHttpError";
|
|
5106
|
-
}
|
|
5107
|
-
};
|
|
5108
|
-
}
|
|
5109
|
-
});
|
|
5110
|
-
|
|
5111
|
-
// src/cli/commands/logout.ts
|
|
5112
|
-
var logout_exports = {};
|
|
5113
|
-
__export(logout_exports, {
|
|
5114
|
-
handleLogout: () => handleLogout
|
|
5115
|
-
});
|
|
5116
|
-
function handleLogout() {
|
|
5117
|
-
return clearToken();
|
|
5118
|
-
}
|
|
5119
|
-
var init_logout = __esm({
|
|
5120
|
-
"src/cli/commands/logout.ts"() {
|
|
5121
|
-
"use strict";
|
|
5122
|
-
init_cjs_shims();
|
|
5123
|
-
init_credentials();
|
|
5124
|
-
}
|
|
5125
|
-
});
|
|
5126
|
-
|
|
5127
|
-
// src/types/run-result-schema.ts
|
|
5128
|
-
function parseRunResult(value2) {
|
|
5129
|
-
return runResultSchema.parse(value2);
|
|
5130
|
-
}
|
|
5131
|
-
var import_zod3, RUN_RESULT_SCHEMA_VERSION, binaryVerdictSchema, assertionResultRecordSchema2, usageSchema, runErrorSchema, rowResultSchema, summarySchema, evalResultSchema, runResultSchema;
|
|
5132
|
-
var init_run_result_schema = __esm({
|
|
5133
|
-
"src/types/run-result-schema.ts"() {
|
|
5134
|
-
"use strict";
|
|
5135
|
-
init_cjs_shims();
|
|
5136
|
-
import_zod3 = require("zod");
|
|
5137
|
-
init_results();
|
|
5138
|
-
RUN_RESULT_SCHEMA_VERSION = 1;
|
|
5139
|
-
binaryVerdictSchema = import_zod3.z.object({
|
|
5140
|
-
question: import_zod3.z.string().min(1),
|
|
5141
|
-
passed: import_zod3.z.boolean()
|
|
5142
|
-
});
|
|
5143
|
-
assertionResultRecordSchema2 = import_zod3.z.object({
|
|
5144
|
-
type: import_zod3.z.string(),
|
|
5145
|
-
passed: import_zod3.z.boolean(),
|
|
5146
|
-
score: import_zod3.z.number().optional(),
|
|
5147
|
-
reason: import_zod3.z.string().optional(),
|
|
5148
|
-
binaryVerdicts: import_zod3.z.array(binaryVerdictSchema).optional()
|
|
5149
|
-
});
|
|
5150
|
-
usageSchema = import_zod3.z.object({
|
|
5151
|
-
input_tokens: import_zod3.z.number(),
|
|
5152
|
-
output_tokens: import_zod3.z.number(),
|
|
5153
|
-
cost_usd: import_zod3.z.number()
|
|
5154
|
-
});
|
|
5155
|
-
runErrorSchema = import_zod3.z.object({
|
|
5156
|
-
code: import_zod3.z.string(),
|
|
5157
|
-
message: import_zod3.z.string()
|
|
5158
|
-
});
|
|
5159
|
-
rowResultSchema = import_zod3.z.object({
|
|
5160
|
-
vars: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.unknown()),
|
|
5161
|
-
output: import_zod3.z.string().nullable(),
|
|
5162
|
-
cached: import_zod3.z.boolean(),
|
|
5163
|
-
usage: usageSchema.optional(),
|
|
5164
|
-
latency_ms: import_zod3.z.number().optional(),
|
|
5165
|
-
error: runErrorSchema.optional(),
|
|
5166
|
-
assertions: import_zod3.z.array(assertionResultRecordSchema2),
|
|
5167
|
-
/** ADR 0045. Optional and additive — the same enum the platform validates on `POST /v1/runs`. */
|
|
5168
|
-
inconclusive: import_zod3.z.enum(ROW_INCONCLUSIVE).optional()
|
|
5169
|
-
});
|
|
5170
|
-
summarySchema = import_zod3.z.object({
|
|
5171
|
-
total: import_zod3.z.number(),
|
|
5172
|
-
passed: import_zod3.z.number(),
|
|
5173
|
-
failed: import_zod3.z.number(),
|
|
5174
|
-
errored: import_zod3.z.number(),
|
|
5175
|
-
/** plune#1245. Optional and additive — a RunResult written before them validates unchanged. */
|
|
5176
|
-
blocked: import_zod3.z.number().optional(),
|
|
5177
|
-
skipped: import_zod3.z.number().optional(),
|
|
5178
|
-
cost_usd: import_zod3.z.number(),
|
|
5179
|
-
duration_ms: import_zod3.z.number()
|
|
5180
|
-
});
|
|
5181
|
-
evalResultSchema = import_zod3.z.object({
|
|
5182
|
-
id: import_zod3.z.string(),
|
|
5183
|
-
tags: import_zod3.z.array(import_zod3.z.string()),
|
|
5184
|
-
rows: import_zod3.z.array(rowResultSchema),
|
|
5185
|
-
passed: import_zod3.z.boolean()
|
|
5186
|
-
});
|
|
5187
|
-
runResultSchema = import_zod3.z.object({
|
|
5188
|
-
schemaVersion: import_zod3.z.literal(RUN_RESULT_SCHEMA_VERSION),
|
|
5189
|
-
plune_version: import_zod3.z.string(),
|
|
5190
|
-
started_at: import_zod3.z.string(),
|
|
5191
|
-
finished_at: import_zod3.z.string(),
|
|
5192
|
-
config_hash: import_zod3.z.string(),
|
|
5193
|
-
summary: summarySchema,
|
|
5194
|
-
evals: import_zod3.z.array(evalResultSchema)
|
|
5195
|
-
});
|
|
5196
|
-
}
|
|
5197
|
-
});
|
|
5198
|
-
|
|
5199
|
-
// src/cli/commands/sync.ts
|
|
5200
|
-
var sync_exports = {};
|
|
5201
|
-
__export(sync_exports, {
|
|
5202
|
-
NotLoggedInError: () => NotLoggedInError,
|
|
5203
|
-
SyncFileError: () => SyncFileError,
|
|
5204
|
-
SyncHttpError: () => SyncHttpError,
|
|
5205
|
-
SyncNetworkError: () => SyncNetworkError,
|
|
5206
|
-
TokenRejectedError: () => TokenRejectedError2,
|
|
5207
|
-
handleSync: () => handleSync,
|
|
5208
|
-
reportSyncFailure: () => reportSyncFailure
|
|
5209
|
-
});
|
|
5210
|
-
function readRun(cwd, file) {
|
|
5211
|
-
const target = file !== void 0 ? path14.resolve(cwd, file) : path14.join(cwd, ".plune", "last-run.json");
|
|
5212
|
-
let raw;
|
|
5213
|
-
try {
|
|
5214
|
-
raw = fs15.readFileSync(target, "utf8");
|
|
5215
|
-
} catch {
|
|
5216
|
-
throw new SyncFileError(
|
|
5217
|
-
file !== void 0 ? `Run file not found: ${target}` : 'No run to sync. Run "plune run" first (or pass --file <path>).'
|
|
5218
|
-
);
|
|
5219
|
-
}
|
|
5220
|
-
let json;
|
|
5221
|
-
try {
|
|
5222
|
-
json = JSON.parse(raw);
|
|
5223
|
-
} catch {
|
|
5224
|
-
throw new SyncFileError(`Run file is not valid JSON: ${target}`);
|
|
5225
|
-
}
|
|
5226
|
-
try {
|
|
5227
|
-
return parseRunResult(json);
|
|
5228
|
-
} catch {
|
|
5229
|
-
throw new SyncFileError(`Run file does not match the RunResult schema: ${target}`);
|
|
5230
|
-
}
|
|
5231
|
-
}
|
|
5232
|
-
async function safeErrorDetail(res) {
|
|
5233
|
-
try {
|
|
5234
|
-
const body = await res.json();
|
|
5235
|
-
return typeof body.error === "string" ? body.error : "";
|
|
5236
|
-
} catch {
|
|
5237
|
-
return "";
|
|
5238
|
-
}
|
|
5239
|
-
}
|
|
5240
|
-
async function handleSync(deps = {}) {
|
|
5241
|
-
const cwd = deps.cwd ?? process.cwd();
|
|
5242
|
-
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
5243
|
-
const token = (deps.loadToken ?? loadToken)();
|
|
5244
|
-
if (token === null) throw new NotLoggedInError();
|
|
5245
|
-
const run = readRun(cwd, deps.file);
|
|
5246
|
-
const doFetch = deps.fetchImpl ?? fetch;
|
|
5247
|
-
const endpoint = `${apiUrl}/v1/runs`;
|
|
5248
|
-
let res;
|
|
5249
|
-
try {
|
|
5250
|
-
res = await doFetch(endpoint, {
|
|
5251
|
-
method: "POST",
|
|
5252
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
5253
|
-
body: JSON.stringify(run)
|
|
5254
|
-
});
|
|
5255
|
-
} catch {
|
|
5256
|
-
throw new SyncNetworkError(apiUrl);
|
|
5257
|
-
}
|
|
5258
|
-
if (!res.ok) {
|
|
5259
|
-
const detail = await safeErrorDetail(res);
|
|
5260
|
-
if (res.status === 401) throw new TokenRejectedError2();
|
|
5261
|
-
throw new SyncHttpError(res.status, detail);
|
|
5262
|
-
}
|
|
5263
|
-
const body = await res.json().catch(() => ({}));
|
|
5264
|
-
const id = typeof body.id === "string" ? body.id : "";
|
|
5265
|
-
const result = { id, url: `${endpoint}/${id}` };
|
|
5266
|
-
if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
|
|
5267
|
-
return result;
|
|
5268
|
-
}
|
|
5269
|
-
async function readVerdict(doFetch, url, token) {
|
|
5270
|
-
try {
|
|
5271
|
-
const res = await doFetch(url, { method: "GET", headers: { authorization: `Bearer ${token}` } });
|
|
5272
|
-
const body = await res.json().catch(() => void 0);
|
|
5273
|
-
return res.ok ? { statusEvent: body?.run?.statusEvent, counts: body?.counts, notRun: body?.notRun } : void 0;
|
|
5274
|
-
} catch {
|
|
5275
|
-
return void 0;
|
|
5276
|
-
}
|
|
5277
|
-
}
|
|
5278
|
-
function reportSyncFailure(err, write) {
|
|
5279
|
-
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError2 || err instanceof SyncFileError) {
|
|
5280
|
-
write(err.message + "\n");
|
|
5281
|
-
return 2;
|
|
5282
|
-
}
|
|
5283
|
-
if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
|
|
5284
|
-
write(err.message + "\n");
|
|
5285
|
-
return 1;
|
|
5286
|
-
}
|
|
5287
|
-
return null;
|
|
5288
|
-
}
|
|
5289
|
-
var fs15, path14, NotLoggedInError, TokenRejectedError2, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
5290
|
-
var init_sync = __esm({
|
|
5291
|
-
"src/cli/commands/sync.ts"() {
|
|
5292
|
-
"use strict";
|
|
5293
|
-
init_cjs_shims();
|
|
5294
|
-
fs15 = __toESM(require("fs"), 1);
|
|
5295
|
-
path14 = __toESM(require("path"), 1);
|
|
5296
|
-
init_run_result_schema();
|
|
5297
|
-
init_api_url();
|
|
5298
|
-
init_credentials();
|
|
5299
|
-
init_gate();
|
|
5300
|
-
NotLoggedInError = class extends Error {
|
|
5301
|
-
constructor() {
|
|
5302
|
-
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
5303
|
-
this.name = "NotLoggedInError";
|
|
5304
|
-
}
|
|
5305
|
-
};
|
|
5306
|
-
TokenRejectedError2 = class extends Error {
|
|
5307
|
-
constructor() {
|
|
5599
|
+
}
|
|
5600
|
+
var EmptyTokenError, TokenRejectedError2, LoginNetworkError, LoginHttpError;
|
|
5601
|
+
var init_login = __esm({
|
|
5602
|
+
"src/cli/commands/login.ts"() {
|
|
5603
|
+
"use strict";
|
|
5604
|
+
init_cjs_shims();
|
|
5605
|
+
init_api_url();
|
|
5606
|
+
init_credentials();
|
|
5607
|
+
EmptyTokenError = class extends Error {
|
|
5608
|
+
constructor(apiUrl = resolveApiUrl()) {
|
|
5308
5609
|
super(
|
|
5309
|
-
|
|
5610
|
+
`No API token provided. ${whereToGetAToken(apiUrl)} Then pass --token <token> or pipe it via stdin.`
|
|
5310
5611
|
);
|
|
5311
|
-
this.name = "
|
|
5612
|
+
this.name = "EmptyTokenError";
|
|
5312
5613
|
}
|
|
5313
5614
|
};
|
|
5314
|
-
|
|
5315
|
-
constructor(
|
|
5316
|
-
super(
|
|
5317
|
-
this.name = "
|
|
5615
|
+
TokenRejectedError2 = class extends Error {
|
|
5616
|
+
constructor(apiUrl = resolveApiUrl()) {
|
|
5617
|
+
super(`That API token was rejected (401). Nothing was saved. ${whereToGetAToken(apiUrl)}`);
|
|
5618
|
+
this.name = "TokenRejectedError";
|
|
5318
5619
|
}
|
|
5319
5620
|
};
|
|
5320
|
-
|
|
5621
|
+
LoginNetworkError = class extends Error {
|
|
5321
5622
|
constructor(apiUrl) {
|
|
5322
5623
|
super(
|
|
5323
|
-
`Could not reach the Plune API at ${apiUrl}
|
|
5624
|
+
`Could not reach the Plune API at ${apiUrl}, so the token was not checked and NOT saved. Try again, or use --skip-verify to save it without checking.`
|
|
5324
5625
|
);
|
|
5325
|
-
this.name = "
|
|
5626
|
+
this.name = "LoginNetworkError";
|
|
5326
5627
|
}
|
|
5327
5628
|
};
|
|
5328
|
-
|
|
5329
|
-
constructor(status
|
|
5330
|
-
super(
|
|
5331
|
-
|
|
5332
|
-
|
|
5629
|
+
LoginHttpError = class extends Error {
|
|
5630
|
+
constructor(status) {
|
|
5631
|
+
super(
|
|
5632
|
+
`The Plune API returned ${status} while checking the token, so it was NOT saved. Try again.`
|
|
5633
|
+
);
|
|
5634
|
+
this.name = "LoginHttpError";
|
|
5333
5635
|
}
|
|
5334
|
-
status;
|
|
5335
5636
|
};
|
|
5336
5637
|
}
|
|
5337
5638
|
});
|
|
5338
5639
|
|
|
5640
|
+
// src/cli/commands/logout.ts
|
|
5641
|
+
var logout_exports = {};
|
|
5642
|
+
__export(logout_exports, {
|
|
5643
|
+
handleLogout: () => handleLogout
|
|
5644
|
+
});
|
|
5645
|
+
function handleLogout() {
|
|
5646
|
+
return clearToken();
|
|
5647
|
+
}
|
|
5648
|
+
var init_logout = __esm({
|
|
5649
|
+
"src/cli/commands/logout.ts"() {
|
|
5650
|
+
"use strict";
|
|
5651
|
+
init_cjs_shims();
|
|
5652
|
+
init_credentials();
|
|
5653
|
+
}
|
|
5654
|
+
});
|
|
5655
|
+
|
|
5339
5656
|
// src/cli/commands/cairn-artifact.ts
|
|
5340
5657
|
async function findLatestCairnRun(runsDir = "runs") {
|
|
5341
|
-
const entries = await
|
|
5658
|
+
const entries = await fs17.readdir(runsDir, { withFileTypes: true }).catch(() => null);
|
|
5342
5659
|
if (entries === null) throw new NoCairnRunsError(runsDir);
|
|
5343
5660
|
let newest;
|
|
5344
5661
|
for (const entry of entries) {
|
|
5345
5662
|
if (!entry.isDirectory()) continue;
|
|
5346
|
-
const dir =
|
|
5663
|
+
const dir = path16.join(runsDir, entry.name);
|
|
5347
5664
|
try {
|
|
5348
|
-
const report = await
|
|
5665
|
+
const report = await fs17.lstat(path16.join(dir, "report.json"));
|
|
5349
5666
|
if (!report.isFile()) continue;
|
|
5350
5667
|
if (newest === void 0 || report.mtimeMs > newest.at) {
|
|
5351
5668
|
newest = { dir, at: report.mtimeMs };
|
|
@@ -5358,13 +5675,13 @@ async function findLatestCairnRun(runsDir = "runs") {
|
|
|
5358
5675
|
}
|
|
5359
5676
|
async function readJson(file) {
|
|
5360
5677
|
try {
|
|
5361
|
-
return JSON.parse(await
|
|
5678
|
+
return JSON.parse(await fs17.readFile(file, "utf8"));
|
|
5362
5679
|
} catch {
|
|
5363
5680
|
return void 0;
|
|
5364
5681
|
}
|
|
5365
5682
|
}
|
|
5366
5683
|
async function defaultReadEvidence(dir) {
|
|
5367
|
-
const raw = await readJson(
|
|
5684
|
+
const raw = await readJson(path16.join(dir, "api-evidence.json"));
|
|
5368
5685
|
if (!Array.isArray(raw)) return void 0;
|
|
5369
5686
|
return raw.map((r) => {
|
|
5370
5687
|
const rec = asRecord(r);
|
|
@@ -5407,7 +5724,7 @@ function casesFromApi(report, verdicts) {
|
|
|
5407
5724
|
});
|
|
5408
5725
|
}
|
|
5409
5726
|
async function readCairnRun(dir, deps = {}) {
|
|
5410
|
-
const raw = await readJson(
|
|
5727
|
+
const raw = await readJson(path16.join(dir, "report.json"));
|
|
5411
5728
|
if (raw === void 0) throw new CairnRunNotFoundError(dir);
|
|
5412
5729
|
const report = asRecord(raw);
|
|
5413
5730
|
if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
|
|
@@ -5437,20 +5754,20 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
5437
5754
|
return {
|
|
5438
5755
|
schemaVersion: INGEST_SCHEMA_VERSION,
|
|
5439
5756
|
run: {
|
|
5440
|
-
runId: str(report["runId"],
|
|
5757
|
+
runId: str(report["runId"], path16.basename(dir)),
|
|
5441
5758
|
mode,
|
|
5442
5759
|
target: str(report["url"], "unknown")
|
|
5443
5760
|
},
|
|
5444
5761
|
cases
|
|
5445
5762
|
};
|
|
5446
5763
|
}
|
|
5447
|
-
var
|
|
5764
|
+
var fs17, path16, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
|
|
5448
5765
|
var init_cairn_artifact = __esm({
|
|
5449
5766
|
"src/cli/commands/cairn-artifact.ts"() {
|
|
5450
5767
|
"use strict";
|
|
5451
5768
|
init_cjs_shims();
|
|
5452
|
-
|
|
5453
|
-
|
|
5769
|
+
fs17 = __toESM(require("fs/promises"), 1);
|
|
5770
|
+
path16 = __toESM(require("path"), 1);
|
|
5454
5771
|
SUPPORTED_ARTIFACT_VERSION = 1;
|
|
5455
5772
|
INGEST_SCHEMA_VERSION = 1;
|
|
5456
5773
|
NoCairnRunsError = class extends Error {
|
|
@@ -5505,7 +5822,7 @@ __export(ingest_exports, {
|
|
|
5505
5822
|
handleIngest: () => handleIngest,
|
|
5506
5823
|
reportIngestFailure: () => reportIngestFailure
|
|
5507
5824
|
});
|
|
5508
|
-
async function
|
|
5825
|
+
async function safeErrorDetail4(res) {
|
|
5509
5826
|
try {
|
|
5510
5827
|
const body = await res.json();
|
|
5511
5828
|
return typeof body.error === "string" ? body.error : "";
|
|
@@ -5532,7 +5849,7 @@ async function handleIngest(deps) {
|
|
|
5532
5849
|
throw new IngestNetworkError(apiUrl);
|
|
5533
5850
|
}
|
|
5534
5851
|
if (!res.ok) {
|
|
5535
|
-
const detail = await
|
|
5852
|
+
const detail = await safeErrorDetail4(res);
|
|
5536
5853
|
if (res.status === 401) throw new IngestTokenRejectedError();
|
|
5537
5854
|
throw new IngestHttpError(res.status, detail);
|
|
5538
5855
|
}
|
|
@@ -5604,207 +5921,6 @@ var init_ingest = __esm({
|
|
|
5604
5921
|
}
|
|
5605
5922
|
});
|
|
5606
5923
|
|
|
5607
|
-
// src/cli/commands/cases.ts
|
|
5608
|
-
var cases_exports = {};
|
|
5609
|
-
__export(cases_exports, {
|
|
5610
|
-
DEFAULT_CASES_FILE: () => DEFAULT_CASES_FILE,
|
|
5611
|
-
DirtyFileError: () => DirtyFileError,
|
|
5612
|
-
formatReport: () => formatReport,
|
|
5613
|
-
handlePull: () => handlePull,
|
|
5614
|
-
handlePush: () => handlePush,
|
|
5615
|
-
reportCasesFailure: () => reportCasesFailure
|
|
5616
|
-
});
|
|
5617
|
-
function gitDirty(file) {
|
|
5618
|
-
try {
|
|
5619
|
-
const out = (0, import_node_child_process2.execFileSync)("git", ["status", "--porcelain", "--", file], {
|
|
5620
|
-
cwd: path16.dirname(file),
|
|
5621
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
5622
|
-
encoding: "utf8"
|
|
5623
|
-
});
|
|
5624
|
-
return out.trim() !== "";
|
|
5625
|
-
} catch {
|
|
5626
|
-
return false;
|
|
5627
|
-
}
|
|
5628
|
-
}
|
|
5629
|
-
async function safeErrorDetail3(res) {
|
|
5630
|
-
try {
|
|
5631
|
-
const body = await res.json();
|
|
5632
|
-
return typeof body.error === "string" ? body.error : "";
|
|
5633
|
-
} catch {
|
|
5634
|
-
return "";
|
|
5635
|
-
}
|
|
5636
|
-
}
|
|
5637
|
-
function tokenOf(deps) {
|
|
5638
|
-
const token = (deps.loadToken ?? loadToken)();
|
|
5639
|
-
if (token === null) throw new NotLoggedInError();
|
|
5640
|
-
return token;
|
|
5641
|
-
}
|
|
5642
|
-
async function handlePull(deps = {}) {
|
|
5643
|
-
const cwd = deps.cwd ?? process.cwd();
|
|
5644
|
-
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
5645
|
-
const token = tokenOf(deps);
|
|
5646
|
-
const file = path16.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
5647
|
-
if (fs17.existsSync(file) && !deps.force && (deps.isDirty ?? gitDirty)(file))
|
|
5648
|
-
throw new DirtyFileError(path16.relative(cwd, file) || file);
|
|
5649
|
-
const doFetch = deps.fetchImpl ?? fetch;
|
|
5650
|
-
const endpoint = deps.suiteId !== void 0 ? `${apiUrl}/v1/suites/${encodeURIComponent(deps.suiteId)}/markdown` : `${apiUrl}/v1/test-cases/markdown`;
|
|
5651
|
-
let res;
|
|
5652
|
-
try {
|
|
5653
|
-
res = await doFetch(endpoint, { headers: { authorization: `Bearer ${token}` } });
|
|
5654
|
-
} catch {
|
|
5655
|
-
throw new SyncNetworkError(apiUrl);
|
|
5656
|
-
}
|
|
5657
|
-
if (!res.ok) {
|
|
5658
|
-
const detail = await safeErrorDetail3(res);
|
|
5659
|
-
if (res.status === 401) throw new TokenRejectedError2();
|
|
5660
|
-
throw new SyncHttpError(res.status, detail);
|
|
5661
|
-
}
|
|
5662
|
-
const text2 = await res.text();
|
|
5663
|
-
fs17.mkdirSync(path16.dirname(file), { recursive: true });
|
|
5664
|
-
fs17.writeFileSync(file, text2, "utf8");
|
|
5665
|
-
return { file, cases: Number(res.headers.get("x-plune-cases") ?? "0") };
|
|
5666
|
-
}
|
|
5667
|
-
async function handlePush(deps = {}) {
|
|
5668
|
-
const cwd = deps.cwd ?? process.cwd();
|
|
5669
|
-
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
5670
|
-
const token = tokenOf(deps);
|
|
5671
|
-
const file = path16.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
5672
|
-
let text2;
|
|
5673
|
-
try {
|
|
5674
|
-
text2 = fs17.readFileSync(file, "utf8");
|
|
5675
|
-
} catch {
|
|
5676
|
-
throw new SyncFileError(
|
|
5677
|
-
`No document to push: ${file}. Run "plune pull" first (or pass a file).`
|
|
5678
|
-
);
|
|
5679
|
-
}
|
|
5680
|
-
if (text2.trim() === "") throw new SyncFileError(`The document is empty: ${file}`);
|
|
5681
|
-
const doFetch = deps.fetchImpl ?? fetch;
|
|
5682
|
-
let res;
|
|
5683
|
-
try {
|
|
5684
|
-
res = await doFetch(`${apiUrl}/v1/test-cases/markdown${deps.dryRun ? "?dryRun=true" : ""}`, {
|
|
5685
|
-
method: "POST",
|
|
5686
|
-
headers: { "content-type": "text/markdown", authorization: `Bearer ${token}` },
|
|
5687
|
-
body: text2
|
|
5688
|
-
});
|
|
5689
|
-
} catch {
|
|
5690
|
-
throw new SyncNetworkError(apiUrl);
|
|
5691
|
-
}
|
|
5692
|
-
if (!res.ok) {
|
|
5693
|
-
const detail = await safeErrorDetail3(res);
|
|
5694
|
-
if (res.status === 401) throw new TokenRejectedError2();
|
|
5695
|
-
throw new SyncHttpError(res.status, detail);
|
|
5696
|
-
}
|
|
5697
|
-
return await res.json();
|
|
5698
|
-
}
|
|
5699
|
-
function formatReport(report) {
|
|
5700
|
-
const verb = (past, future) => report.dryRun ? future : past;
|
|
5701
|
-
const lines = [
|
|
5702
|
-
`${report.dryRun ? "Dry run \u2014 nothing written." : "Pushed."} ${report.created.length} ${verb("created", "to create")} \xB7 ${report.updated.length} ${verb("updated", "to update")} \xB7 ${report.unchanged.length} unchanged \xB7 ${report.refused.length} refused` + (report.suites.created.length > 0 ? ` \xB7 ${report.suites.created.length} suite(s) ${verb("created", "to create")}` : "")
|
|
5703
|
-
];
|
|
5704
|
-
for (const r of report.refused)
|
|
5705
|
-
lines.push(` refused line ${r.line} ${r.title || "(no title)"} \u2014 ${r.error}`);
|
|
5706
|
-
for (const w of report.warnings)
|
|
5707
|
-
lines.push(` warning ${w.line !== void 0 ? `line ${w.line} ` : ""}${w.message}`);
|
|
5708
|
-
return lines.join("\n") + "\n";
|
|
5709
|
-
}
|
|
5710
|
-
function reportCasesFailure(err, write) {
|
|
5711
|
-
if (err instanceof DirtyFileError) {
|
|
5712
|
-
write(err.message + "\n");
|
|
5713
|
-
return 3;
|
|
5714
|
-
}
|
|
5715
|
-
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError2 || err instanceof SyncFileError) {
|
|
5716
|
-
write(err.message + "\n");
|
|
5717
|
-
return 2;
|
|
5718
|
-
}
|
|
5719
|
-
if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
|
|
5720
|
-
write(err.message + "\n");
|
|
5721
|
-
return 1;
|
|
5722
|
-
}
|
|
5723
|
-
return null;
|
|
5724
|
-
}
|
|
5725
|
-
var import_node_child_process2, fs17, path16, DEFAULT_CASES_FILE, DirtyFileError;
|
|
5726
|
-
var init_cases = __esm({
|
|
5727
|
-
"src/cli/commands/cases.ts"() {
|
|
5728
|
-
"use strict";
|
|
5729
|
-
init_cjs_shims();
|
|
5730
|
-
import_node_child_process2 = require("child_process");
|
|
5731
|
-
fs17 = __toESM(require("fs"), 1);
|
|
5732
|
-
path16 = __toESM(require("path"), 1);
|
|
5733
|
-
init_api_url();
|
|
5734
|
-
init_credentials();
|
|
5735
|
-
init_sync();
|
|
5736
|
-
DEFAULT_CASES_FILE = path16.join("plune", "cases.md");
|
|
5737
|
-
DirtyFileError = class extends Error {
|
|
5738
|
-
constructor(file) {
|
|
5739
|
-
super(`${file} has uncommitted changes; commit them or pass --force to overwrite.`);
|
|
5740
|
-
this.name = "DirtyFileError";
|
|
5741
|
-
}
|
|
5742
|
-
};
|
|
5743
|
-
}
|
|
5744
|
-
});
|
|
5745
|
-
|
|
5746
|
-
// src/cli/commands/plan.ts
|
|
5747
|
-
var plan_exports = {};
|
|
5748
|
-
__export(plan_exports, {
|
|
5749
|
-
formatGrep: () => formatGrep,
|
|
5750
|
-
grepFragment: () => grepFragment,
|
|
5751
|
-
handlePlanGrep: () => handlePlanGrep
|
|
5752
|
-
});
|
|
5753
|
-
function grepFragment(c) {
|
|
5754
|
-
const pathTitle = c.externalKeys.find((k) => k.kind === "path-title")?.value;
|
|
5755
|
-
const segments = pathTitle?.split("#") ?? [c.title];
|
|
5756
|
-
return (segments.length > 1 ? segments.slice(1) : segments).flatMap((s) => s.split(/ [>›] /)).map(escape).join(BOUNDARY);
|
|
5757
|
-
}
|
|
5758
|
-
function formatGrep(cases) {
|
|
5759
|
-
if (cases.length === 0) return "(?!)";
|
|
5760
|
-
return cases.map(grepFragment).join("|");
|
|
5761
|
-
}
|
|
5762
|
-
async function safeErrorDetail4(res) {
|
|
5763
|
-
try {
|
|
5764
|
-
const body = await res.json();
|
|
5765
|
-
return typeof body.error === "string" ? body.error : "";
|
|
5766
|
-
} catch {
|
|
5767
|
-
return "";
|
|
5768
|
-
}
|
|
5769
|
-
}
|
|
5770
|
-
async function handlePlanGrep(deps) {
|
|
5771
|
-
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
5772
|
-
const token = (deps.loadToken ?? loadToken)();
|
|
5773
|
-
if (token === null) throw new NotLoggedInError();
|
|
5774
|
-
const doFetch = deps.fetchImpl ?? fetch;
|
|
5775
|
-
let res;
|
|
5776
|
-
try {
|
|
5777
|
-
res = await doFetch(`${apiUrl}/v1/plans/${encodeURIComponent(deps.planId)}/cases`, {
|
|
5778
|
-
headers: { authorization: `Bearer ${token}` }
|
|
5779
|
-
});
|
|
5780
|
-
} catch {
|
|
5781
|
-
throw new SyncNetworkError(apiUrl);
|
|
5782
|
-
}
|
|
5783
|
-
if (!res.ok) {
|
|
5784
|
-
const detail = await safeErrorDetail4(res);
|
|
5785
|
-
if (res.status === 401) throw new TokenRejectedError2();
|
|
5786
|
-
throw new SyncHttpError(res.status, detail);
|
|
5787
|
-
}
|
|
5788
|
-
const { cases } = await res.json();
|
|
5789
|
-
return {
|
|
5790
|
-
pattern: formatGrep(cases),
|
|
5791
|
-
cases: cases.length,
|
|
5792
|
-
keyed: cases.filter((c) => c.externalKeys.some((k) => k.kind === "path-title")).length
|
|
5793
|
-
};
|
|
5794
|
-
}
|
|
5795
|
-
var escape, BOUNDARY;
|
|
5796
|
-
var init_plan = __esm({
|
|
5797
|
-
"src/cli/commands/plan.ts"() {
|
|
5798
|
-
"use strict";
|
|
5799
|
-
init_cjs_shims();
|
|
5800
|
-
init_api_url();
|
|
5801
|
-
init_credentials();
|
|
5802
|
-
init_sync();
|
|
5803
|
-
escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5804
|
-
BOUNDARY = "[ >#]+";
|
|
5805
|
-
}
|
|
5806
|
-
});
|
|
5807
|
-
|
|
5808
5924
|
// src/cli.ts
|
|
5809
5925
|
var cli_exports = {};
|
|
5810
5926
|
__export(cli_exports, {
|
|
@@ -6014,6 +6130,32 @@ function createProgram() {
|
|
|
6014
6130
|
await failRunCommand(err, verboseOf(command));
|
|
6015
6131
|
}
|
|
6016
6132
|
});
|
|
6133
|
+
runCommand.command("grep").argument("<run>", "The run: its id, or its key (rk_\u2026) \u2014 the one PLUNE_RUN joins by").description(
|
|
6134
|
+
"Print the run's automated cases as one --grep pattern for the runner (GET /v1/runs/:id/cases)"
|
|
6135
|
+
).option("--failed", "Only the cases whose latest result failed or broke \u2014 a muted failure does not count", false).action(async (run, options, command) => {
|
|
6136
|
+
const verbose = verboseOf(command);
|
|
6137
|
+
const { handleRunGrep: handleRunGrep2, runGrepSummary: runGrepSummary2, RunRefError: RunRefError2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
6138
|
+
const { reportCasesFailure: reportCasesFailure2 } = await Promise.resolve().then(() => (init_cases(), cases_exports));
|
|
6139
|
+
try {
|
|
6140
|
+
const out = await handleRunGrep2({ run, failed: options.failed });
|
|
6141
|
+
process.stdout.write(out.pattern + "\n");
|
|
6142
|
+
process.stderr.write(runGrepSummary2(out, options.failed));
|
|
6143
|
+
} catch (err) {
|
|
6144
|
+
if (err instanceof RunRefError2) {
|
|
6145
|
+
process.stderr.write(`plune: ${err.message}
|
|
6146
|
+
`);
|
|
6147
|
+
process.exitCode = err.exitCode;
|
|
6148
|
+
return;
|
|
6149
|
+
}
|
|
6150
|
+
const code = reportCasesFailure2(err, (s) => process.stderr.write(s));
|
|
6151
|
+
if (code !== null) {
|
|
6152
|
+
maybeStack(err, verbose);
|
|
6153
|
+
process.exitCode = code;
|
|
6154
|
+
return;
|
|
6155
|
+
}
|
|
6156
|
+
failUnexpected(err, verbose);
|
|
6157
|
+
}
|
|
6158
|
+
});
|
|
6017
6159
|
runCommand.command("report").description("Send what the reporter could not \u2014 replays .plune/pending-results.jsonl").option("--file <path>", "The fallback file to replay").action(async (options, command) => {
|
|
6018
6160
|
try {
|
|
6019
6161
|
const { handleRunReport: handleRunReport2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
@@ -6026,6 +6168,13 @@ function createProgram() {
|
|
|
6026
6168
|
"--fail-on-missing-attachments",
|
|
6027
6169
|
"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",
|
|
6028
6170
|
false
|
|
6171
|
+
).option(
|
|
6172
|
+
"--upload-concurrency <n>",
|
|
6173
|
+
"Files uploaded at once, 1 to 32 (default 4; PLUNE_UPLOAD_CONCURRENCY sets it too). A 429 is retried PLUNE_UPLOAD_RETRIES times (default 6)",
|
|
6174
|
+
(v) => {
|
|
6175
|
+
if (!/^\d+$/.test(v) || Number(v) < 1 || Number(v) > 32) throw new import_commander.InvalidArgumentError("a whole number from 1 to 32.");
|
|
6176
|
+
return Number(v);
|
|
6177
|
+
}
|
|
6029
6178
|
).option("--fail-on-failures", FAIL_ON_FAILURES, false).action(
|
|
6030
6179
|
async (file, options, command) => {
|
|
6031
6180
|
const [
|
|
@@ -6051,6 +6200,7 @@ function createProgram() {
|
|
|
6051
6200
|
...format !== void 0 ? { format } : {},
|
|
6052
6201
|
...options.key !== void 0 ? { key: options.key } : {},
|
|
6053
6202
|
...options.create ? { create: true } : {},
|
|
6203
|
+
...options.uploadConcurrency !== void 0 ? { uploadConcurrency: options.uploadConcurrency } : {},
|
|
6054
6204
|
...options.failOnFailures ? { failOnFailures: true } : {}
|
|
6055
6205
|
});
|
|
6056
6206
|
if (options.failOnMissingAttachments && imported.missingAttachments.length > 0) process.exitCode = 1;
|