@plune-ai/cli 0.14.3 → 0.16.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 +25 -3
- package/dist/cli.cjs +359 -107
- package/dist/index.cjs +34 -3
- package/dist/index.js +34 -3
- package/dist/reporter-core.cjs +161 -16
- package/dist/reporter-core.d.cts +75 -4
- package/dist/reporter-core.d.ts +75 -4
- package/dist/reporter-core.js +154 -11
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -75,8 +75,8 @@ var env_exports = {};
|
|
|
75
75
|
__export(env_exports, {
|
|
76
76
|
loadEnv: () => loadEnv
|
|
77
77
|
});
|
|
78
|
-
function loadEnv(
|
|
79
|
-
(0, import_dotenv.config)({ path: path.join(
|
|
78
|
+
function loadEnv(dir) {
|
|
79
|
+
(0, import_dotenv.config)({ path: path.join(dir, ".env"), override: false });
|
|
80
80
|
}
|
|
81
81
|
var import_dotenv, path;
|
|
82
82
|
var init_env = __esm({
|
|
@@ -523,11 +523,25 @@ var init_prices = __esm({
|
|
|
523
523
|
"use strict";
|
|
524
524
|
init_cjs_shims();
|
|
525
525
|
PRICE_TABLE = {
|
|
526
|
-
// Anthropic
|
|
526
|
+
// Anthropic, current models. Source: platform.claude.com/docs/en/about-claude/pricing — base
|
|
527
|
+
// input and output tokens per million, read on 2026-09-29, kept here per thousand.
|
|
528
|
+
"claude-fable-5-1": { input_per_1k_usd: 0.01, output_per_1k_usd: 0.05 },
|
|
529
|
+
"claude-opus-5-5": { input_per_1k_usd: 4e-3, output_per_1k_usd: 0.02 },
|
|
530
|
+
"claude-sonnet-5-5": { input_per_1k_usd: 2e-3, output_per_1k_usd: 0.01 },
|
|
531
|
+
"claude-haiku-4-5": { input_per_1k_usd: 1e-3, output_per_1k_usd: 5e-3 },
|
|
532
|
+
// The dated id of the same model; a config may name either.
|
|
533
|
+
"claude-haiku-4-5-20251001": { input_per_1k_usd: 1e-3, output_per_1k_usd: 5e-3 },
|
|
534
|
+
// Anthropic, older ids: unchanged, so a config that still names one prices as it did. Anthropic has
|
|
535
|
+
// retired these models — a new project should not start on them.
|
|
527
536
|
"claude-3-5-haiku-latest": { input_per_1k_usd: 8e-4, output_per_1k_usd: 4e-3 },
|
|
528
537
|
"claude-3-5-sonnet-latest": { input_per_1k_usd: 3e-3, output_per_1k_usd: 0.015 },
|
|
529
538
|
"claude-3-opus-latest": { input_per_1k_usd: 0.015, output_per_1k_usd: 0.075 },
|
|
530
|
-
// OpenAI
|
|
539
|
+
// OpenAI, current models. Source: developers.openai.com/api/docs/pricing — standard tier, the
|
|
540
|
+
// short-context rates (a long-context request costs more), read on 2026-09-29.
|
|
541
|
+
"gpt-6-astra": { input_per_1k_usd: 0.01, output_per_1k_usd: 0.05 },
|
|
542
|
+
"gpt-6.1-sol": { input_per_1k_usd: 2e-3, output_per_1k_usd: 0.01 },
|
|
543
|
+
"gpt-6-luna": { input_per_1k_usd: 1e-4, output_per_1k_usd: 5e-4 },
|
|
544
|
+
// OpenAI, older ids: unchanged (the same rates the page above still lists for both).
|
|
531
545
|
"gpt-4o": { input_per_1k_usd: 25e-4, output_per_1k_usd: 0.01 },
|
|
532
546
|
"gpt-4o-mini": { input_per_1k_usd: 15e-5, output_per_1k_usd: 6e-4 },
|
|
533
547
|
// OpenAI via OpenRouter (namespaced ids). OpenRouter passes OpenAI list price through for
|
|
@@ -776,6 +790,25 @@ var init_providers = __esm({
|
|
|
776
790
|
}
|
|
777
791
|
});
|
|
778
792
|
|
|
793
|
+
// src/providers/estimate.ts
|
|
794
|
+
function makeEstimateProvider(config, pricing) {
|
|
795
|
+
return {
|
|
796
|
+
complete: () => Promise.reject(
|
|
797
|
+
new Error("The --dry-run provider only estimates cost; it never calls a model.")
|
|
798
|
+
),
|
|
799
|
+
estimateCost: (usage, reportedCostUsd) => ({
|
|
800
|
+
cost_usd: resolveCost(usage, config.model, reportedCostUsd, pricing)
|
|
801
|
+
})
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
var init_estimate = __esm({
|
|
805
|
+
"src/providers/estimate.ts"() {
|
|
806
|
+
"use strict";
|
|
807
|
+
init_cjs_shims();
|
|
808
|
+
init_cost();
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
|
|
779
812
|
// src/providers/mock.ts
|
|
780
813
|
function makeMockProvider() {
|
|
781
814
|
return {
|
|
@@ -1878,11 +1911,16 @@ __export(run_exports, {
|
|
|
1878
1911
|
function isMockMode(env) {
|
|
1879
1912
|
return env["PLUNE_MOCK_PROVIDER"] === "1";
|
|
1880
1913
|
}
|
|
1914
|
+
function resolveProviderFor(config, dryRun) {
|
|
1915
|
+
if (isMockMode(process.env)) return () => makeMockProvider();
|
|
1916
|
+
if (dryRun) return (cfg) => makeEstimateProvider(cfg, config.pricing);
|
|
1917
|
+
return (cfg) => getProvider(cfg, process.env, config.pricing);
|
|
1918
|
+
}
|
|
1881
1919
|
function buildRealDeps(config, baseDir, dryRun) {
|
|
1882
1920
|
const dir = path5.join(baseDir, ".plune");
|
|
1883
1921
|
fs5.mkdirSync(dir, { recursive: true });
|
|
1884
1922
|
return {
|
|
1885
|
-
resolveProvider:
|
|
1923
|
+
resolveProvider: resolveProviderFor(config, dryRun),
|
|
1886
1924
|
embedder: getDefaultEmbedder(),
|
|
1887
1925
|
cache: dryRun ? NOOP_CACHE : openCache(path5.join(dir, "cache.db")),
|
|
1888
1926
|
now: Date.now,
|
|
@@ -1929,6 +1967,7 @@ var init_run2 = __esm({
|
|
|
1929
1967
|
path5 = __toESM(require("path"), 1);
|
|
1930
1968
|
init_loader();
|
|
1931
1969
|
init_providers();
|
|
1970
|
+
init_estimate();
|
|
1932
1971
|
init_mock();
|
|
1933
1972
|
init_embeddings();
|
|
1934
1973
|
init_cache2();
|
|
@@ -2241,7 +2280,7 @@ function createClient(opts) {
|
|
|
2241
2280
|
const wait = opts.wait ?? sleep;
|
|
2242
2281
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
2243
2282
|
const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
|
|
2244
|
-
async function send(method,
|
|
2283
|
+
async function send(method, path15, payload) {
|
|
2245
2284
|
let last = {
|
|
2246
2285
|
kind: "unavailable",
|
|
2247
2286
|
status: null,
|
|
@@ -2250,13 +2289,13 @@ function createClient(opts) {
|
|
|
2250
2289
|
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt += 1) {
|
|
2251
2290
|
let res;
|
|
2252
2291
|
try {
|
|
2253
|
-
res = await doFetch(`${base}${
|
|
2292
|
+
res = await doFetch(`${base}${path15}`, {
|
|
2254
2293
|
method,
|
|
2255
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${opts.token}` },
|
|
2294
|
+
headers: { "content-type": payload?.type ?? "application/json", authorization: `Bearer ${opts.token}` },
|
|
2256
2295
|
// A DELETE carries none. Sending `"undefined"` as a body is what a naive
|
|
2257
2296
|
// `JSON.stringify(body)` would do, and some proxies answer that with a 400 nobody can
|
|
2258
2297
|
// explain.
|
|
2259
|
-
...
|
|
2298
|
+
...payload === void 0 ? {} : { body: payload.body }
|
|
2260
2299
|
});
|
|
2261
2300
|
} catch (err) {
|
|
2262
2301
|
last = { kind: "unavailable", status: null, detail: scrub(String(err)) };
|
|
@@ -2282,8 +2321,9 @@ function createClient(opts) {
|
|
|
2282
2321
|
return { ok: false, ...last };
|
|
2283
2322
|
}
|
|
2284
2323
|
return {
|
|
2285
|
-
post: (
|
|
2286
|
-
|
|
2324
|
+
post: (path15, body) => send("POST", path15, { body: JSON.stringify(body), type: "application/json" }),
|
|
2325
|
+
upload: (path15, bytes, contentType) => send("POST", path15, { body: bytes, type: contentType }),
|
|
2326
|
+
del: (path15) => send("DELETE", path15)
|
|
2287
2327
|
};
|
|
2288
2328
|
}
|
|
2289
2329
|
var import_node_http, import_node_https, RETRY_DELAYS_MS, IDLE_MS, sleep;
|
|
@@ -2359,13 +2399,35 @@ function defaultRunTitle(env = process.env, now = /* @__PURE__ */ new Date(), cw
|
|
|
2359
2399
|
const where = flag(env, "CI") || flag(env, "GITHUB_ACTIONS") ? "ci" : "local";
|
|
2360
2400
|
return `${path6.basename(cwd)} \xB7 ${stamp} \xB7 ${where}`;
|
|
2361
2401
|
}
|
|
2362
|
-
|
|
2402
|
+
function ciCommit(reported, env = process.env) {
|
|
2403
|
+
const said = typeof reported === "object" && reported !== null ? reported : {};
|
|
2404
|
+
const text2 = (raw) => typeof raw === "string" && raw.trim() !== "" ? raw.trim() : void 0;
|
|
2405
|
+
const provider = PROVIDERS.find((p) => value(env, p.is) !== void 0);
|
|
2406
|
+
const job = {
|
|
2407
|
+
sha: provider === void 0 ? void 0 : value(env, provider.sha),
|
|
2408
|
+
branch: provider?.branch.map((name) => value(env, name)).find((v) => v !== void 0)
|
|
2409
|
+
};
|
|
2410
|
+
const sha = text2(said["commitHash"]);
|
|
2411
|
+
const sameCommit = sha === void 0 || sha === job.sha;
|
|
2412
|
+
const commit = sha ?? job.sha;
|
|
2413
|
+
const branch = text2(said["branch"]) ?? (sameCommit ? job.branch : void 0);
|
|
2414
|
+
return {
|
|
2415
|
+
...commit !== void 0 ? { sha: commit } : {},
|
|
2416
|
+
...branch !== void 0 ? { branch } : {}
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
var path6, UNSUPPORTED_VARS, PROVIDERS;
|
|
2363
2420
|
var init_env2 = __esm({
|
|
2364
2421
|
"src/reporter-core/env.ts"() {
|
|
2365
2422
|
"use strict";
|
|
2366
2423
|
init_cjs_shims();
|
|
2367
2424
|
path6 = __toESM(require("path"), 1);
|
|
2368
2425
|
UNSUPPORTED_VARS = ["PLUNE_GROUP"];
|
|
2426
|
+
PROVIDERS = [
|
|
2427
|
+
{ is: "GITHUB_ACTIONS", sha: "GITHUB_SHA", branch: ["GITHUB_HEAD_REF", "GITHUB_REF_NAME"] },
|
|
2428
|
+
{ is: "GITLAB_CI", sha: "CI_COMMIT_SHA", branch: ["CI_COMMIT_REF_NAME"] },
|
|
2429
|
+
{ is: "JENKINS_URL", sha: "GIT_COMMIT", branch: ["GIT_BRANCH"] }
|
|
2430
|
+
];
|
|
2369
2431
|
}
|
|
2370
2432
|
});
|
|
2371
2433
|
|
|
@@ -2376,6 +2438,8 @@ function appendBatch(file, batch) {
|
|
|
2376
2438
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2377
2439
|
runId: batch.runId,
|
|
2378
2440
|
externalKey: batch.externalKey,
|
|
2441
|
+
...batch.session !== void 0 ? { session: batch.session } : {},
|
|
2442
|
+
...batch.meta !== void 0 ? { meta: batch.meta } : {},
|
|
2379
2443
|
results: batch.results
|
|
2380
2444
|
};
|
|
2381
2445
|
fs6.appendFileSync(file, `${JSON.stringify(line)}
|
|
@@ -2393,6 +2457,19 @@ var init_fallback = __esm({
|
|
|
2393
2457
|
});
|
|
2394
2458
|
|
|
2395
2459
|
// src/reporter-core/session.ts
|
|
2460
|
+
function nameOf(file) {
|
|
2461
|
+
const own = file.name.split(/[\\/]/).pop()?.trim() || (0, import_node_path2.basename)(file.path);
|
|
2462
|
+
const fileExt = (0, import_node_path2.extname)(file.path);
|
|
2463
|
+
const ownExt = (0, import_node_path2.extname)(own);
|
|
2464
|
+
const typed = ownExt !== "" && (ownExt.toLowerCase() === fileExt.toLowerCase() || TYPE_BY_EXTENSION[ownExt.toLowerCase()] !== void 0);
|
|
2465
|
+
const ext = typed ? ownExt : fileExt;
|
|
2466
|
+
let stem = typed ? own.slice(0, -ownExt.length) : own;
|
|
2467
|
+
if (stem.length + ext.length > NAME_MAX) {
|
|
2468
|
+
stem = stem.slice(0, Math.max(0, NAME_MAX - ext.length));
|
|
2469
|
+
if (/[\uD800-\uDBFF]$/.test(stem)) stem = stem.slice(0, -1);
|
|
2470
|
+
}
|
|
2471
|
+
return `${stem}${ext}`;
|
|
2472
|
+
}
|
|
2396
2473
|
function boundKey(key) {
|
|
2397
2474
|
if (key.value.length <= KEY_MAX) return key;
|
|
2398
2475
|
const digest = (0, import_node_crypto2.createHash)("sha256").update(key.value).digest("hex").slice(0, 16);
|
|
@@ -2400,6 +2477,12 @@ function boundKey(key) {
|
|
|
2400
2477
|
if (/[\uD800-\uDBFF]$/.test(head)) head = head.slice(0, -1);
|
|
2401
2478
|
return { ...key, value: `${head}#${digest}` };
|
|
2402
2479
|
}
|
|
2480
|
+
function whyUnoffered(out) {
|
|
2481
|
+
if (out.ok || out.status !== 429) return "other";
|
|
2482
|
+
if (/review queue quota reached/i.test(out.detail)) return "queue";
|
|
2483
|
+
if (/test case quota reached/i.test(out.detail)) return "cases";
|
|
2484
|
+
return "other";
|
|
2485
|
+
}
|
|
2403
2486
|
function boundTitle(title) {
|
|
2404
2487
|
return title.length <= TITLE_MAX ? title : `${title.slice(0, TITLE_MAX - 1)}\u2026`;
|
|
2405
2488
|
}
|
|
@@ -2441,6 +2524,7 @@ async function startRun(passed, declared) {
|
|
|
2441
2524
|
`));
|
|
2442
2525
|
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
2443
2526
|
const externalKey = cfg.externalKey ?? null;
|
|
2527
|
+
const sessionMarker = (0, import_node_crypto2.randomUUID)();
|
|
2444
2528
|
const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
|
|
2445
2529
|
const offerDiscovered = cfg.offerDiscovered ?? false;
|
|
2446
2530
|
const token = cfg.token ?? loadToken() ?? "";
|
|
@@ -2458,7 +2542,8 @@ async function startRun(passed, declared) {
|
|
|
2458
2542
|
offered: 0,
|
|
2459
2543
|
created: 0,
|
|
2460
2544
|
unoffered: 0,
|
|
2461
|
-
deferred: 0
|
|
2545
|
+
deferred: 0,
|
|
2546
|
+
screenshots: { uploaded: 0, skipped: 0, failed: 0 }
|
|
2462
2547
|
};
|
|
2463
2548
|
const resolved = /* @__PURE__ */ new Map();
|
|
2464
2549
|
let buffer = [];
|
|
@@ -2468,6 +2553,7 @@ async function startRun(passed, declared) {
|
|
|
2468
2553
|
let offline = token === "";
|
|
2469
2554
|
let done = false;
|
|
2470
2555
|
const shortened = /* @__PURE__ */ new Set();
|
|
2556
|
+
const skipsSaid = /* @__PURE__ */ new Set();
|
|
2471
2557
|
function bound(keys) {
|
|
2472
2558
|
return keys.map((key) => {
|
|
2473
2559
|
const fit = boundKey(key);
|
|
@@ -2490,7 +2576,13 @@ async function startRun(passed, declared) {
|
|
|
2490
2576
|
log(`plune: ignoring ${env.ignored.join(", ")} \u2014 a run has nowhere to store them yet.`);
|
|
2491
2577
|
}
|
|
2492
2578
|
function defer(results) {
|
|
2493
|
-
appendBatch(fallbackPath, {
|
|
2579
|
+
appendBatch(fallbackPath, {
|
|
2580
|
+
runId,
|
|
2581
|
+
externalKey,
|
|
2582
|
+
session: sessionMarker,
|
|
2583
|
+
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
2584
|
+
results
|
|
2585
|
+
});
|
|
2494
2586
|
stats.deferred += results.length;
|
|
2495
2587
|
}
|
|
2496
2588
|
async function resolveKeys(values) {
|
|
@@ -2587,6 +2679,7 @@ async function startRun(passed, declared) {
|
|
|
2587
2679
|
});
|
|
2588
2680
|
if (!out.ok || !Array.isArray(out.body?.results)) {
|
|
2589
2681
|
stats.unoffered = batches.slice(index).reduce((n, rest) => n + rest.length, 0);
|
|
2682
|
+
stats.unofferedWhy = whyUnoffered(out);
|
|
2590
2683
|
log(
|
|
2591
2684
|
`plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.ok ? "the answer had no results" : out.detail || out.kind}).`
|
|
2592
2685
|
);
|
|
@@ -2596,6 +2689,60 @@ async function startRun(passed, declared) {
|
|
|
2596
2689
|
stats.created += out.body.results.filter((r) => r?.outcome === "created").length;
|
|
2597
2690
|
}
|
|
2598
2691
|
}
|
|
2692
|
+
function skip(name, why) {
|
|
2693
|
+
stats.screenshots.skipped += 1;
|
|
2694
|
+
if (skipsSaid.has(why)) return;
|
|
2695
|
+
skipsSaid.add(why);
|
|
2696
|
+
log(`plune: screenshot "${name}" not uploaded \u2014 ${SKIPPED[why]}.`);
|
|
2697
|
+
}
|
|
2698
|
+
async function uploadOne(shot) {
|
|
2699
|
+
let bytes;
|
|
2700
|
+
try {
|
|
2701
|
+
if ((await import_node_fs2.promises.stat(shot.path)).size > SCREENSHOT_BYTES) return skip(shot.name, "size");
|
|
2702
|
+
bytes = await import_node_fs2.promises.readFile(shot.path);
|
|
2703
|
+
} catch {
|
|
2704
|
+
return skip(shot.name, "missing");
|
|
2705
|
+
}
|
|
2706
|
+
const out = await client.upload(
|
|
2707
|
+
`/v1/results/${encodeURIComponent(shot.id)}/files?name=${encodeURIComponent(shot.name)}`,
|
|
2708
|
+
bytes,
|
|
2709
|
+
shot.type
|
|
2710
|
+
);
|
|
2711
|
+
if (out.ok) {
|
|
2712
|
+
stats.screenshots.uploaded += 1;
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
stats.screenshots.failed += 1;
|
|
2716
|
+
if (stats.screenshots.failed === 1) {
|
|
2717
|
+
log(`plune: could not upload a screenshot (${out.detail || out.kind}) \u2014 its result is reported without it.`);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
async function upload(items, filesByKey) {
|
|
2721
|
+
if (filesByKey.size === 0 || !Array.isArray(items)) return;
|
|
2722
|
+
const shots = [];
|
|
2723
|
+
for (const item of items) {
|
|
2724
|
+
if (item?.status !== "accepted" || typeof item.id !== "string" || typeof item.resultKey !== "string") continue;
|
|
2725
|
+
let room = SCREENSHOTS_PER_RESULT;
|
|
2726
|
+
for (const file of filesByKey.get(item.resultKey) ?? []) {
|
|
2727
|
+
const type = file.contentType?.split(";")[0]?.trim().toLowerCase() || TYPE_BY_EXTENSION[(0, import_node_path2.extname)(file.path).toLowerCase()];
|
|
2728
|
+
if (!type?.startsWith("image/")) continue;
|
|
2729
|
+
const name = nameOf(file);
|
|
2730
|
+
if (file.name.startsWith("_")) skip(name, "hidden");
|
|
2731
|
+
else if (!SCREENSHOT_TYPES.has(type)) skip(name, "type");
|
|
2732
|
+
else if (room === 0) skip(name, "limit");
|
|
2733
|
+
else {
|
|
2734
|
+
room -= 1;
|
|
2735
|
+
shots.push({ id: item.id, name, type, path: file.path });
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
let next = 0;
|
|
2740
|
+
await Promise.all(
|
|
2741
|
+
Array.from({ length: Math.min(UPLOADS_AT_ONCE, shots.length) }, async () => {
|
|
2742
|
+
while (next < shots.length) await uploadOne(shots[next++]);
|
|
2743
|
+
})
|
|
2744
|
+
);
|
|
2745
|
+
}
|
|
2599
2746
|
async function flush() {
|
|
2600
2747
|
if (buffer.length === 0) return;
|
|
2601
2748
|
const pending = buffer;
|
|
@@ -2607,11 +2754,13 @@ async function startRun(passed, declared) {
|
|
|
2607
2754
|
await resolveKeys(pending.flatMap((p) => p.keys));
|
|
2608
2755
|
const submissions = [];
|
|
2609
2756
|
const unanswered = [];
|
|
2757
|
+
const filesByKey = /* @__PURE__ */ new Map();
|
|
2610
2758
|
for (const result of pending) {
|
|
2611
2759
|
const match = matchFor(result);
|
|
2612
2760
|
if (match.kind === "found") {
|
|
2613
|
-
const { keys: _keys, ...rest } = result;
|
|
2761
|
+
const { keys: _keys, files, ...rest } = result;
|
|
2614
2762
|
submissions.push({ ...rest, testCaseId: match.testCaseId });
|
|
2763
|
+
if (files !== void 0) filesByKey.set(result.resultKey, files);
|
|
2615
2764
|
} else if (match.kind === "unmatched") {
|
|
2616
2765
|
stats.unresolved += 1;
|
|
2617
2766
|
remember(result);
|
|
@@ -2629,14 +2778,16 @@ async function startRun(passed, declared) {
|
|
|
2629
2778
|
defer(part);
|
|
2630
2779
|
continue;
|
|
2631
2780
|
}
|
|
2632
|
-
const out = await client.post(
|
|
2633
|
-
results
|
|
2634
|
-
|
|
2781
|
+
const out = await client.post(
|
|
2782
|
+
`/v1/runs/${runId}/results`,
|
|
2783
|
+
{ results: part }
|
|
2784
|
+
);
|
|
2635
2785
|
const counts = out.ok ? out.body?.counts : void 0;
|
|
2636
2786
|
const told = COUNTED.filter((k) => typeof counts?.[k] === "number");
|
|
2637
2787
|
if (told.length > 0) {
|
|
2638
2788
|
for (const k of told) stats[k] += counts[k];
|
|
2639
2789
|
progress();
|
|
2790
|
+
await upload(out.ok ? out.body?.items : void 0, filesByKey);
|
|
2640
2791
|
continue;
|
|
2641
2792
|
}
|
|
2642
2793
|
if (out.ok) {
|
|
@@ -2666,6 +2817,12 @@ async function startRun(passed, declared) {
|
|
|
2666
2817
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
2667
2818
|
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
2668
2819
|
if (stats.created > 0) parts.push(`${stats.created} added as cases (trusted source)`);
|
|
2820
|
+
const shots = stats.screenshots;
|
|
2821
|
+
if (shots.uploaded + shots.skipped + shots.failed > 0) {
|
|
2822
|
+
parts.push(
|
|
2823
|
+
`${shots.uploaded} screenshot(s) uploaded` + (shots.skipped > 0 ? `, ${shots.skipped} skipped` : "") + (shots.failed > 0 ? `, ${shots.failed} failed to upload` : "")
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2669
2826
|
if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
|
|
2670
2827
|
if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
|
|
2671
2828
|
log(`plune: ${parts.join(" \xB7 ")}`);
|
|
@@ -2717,12 +2874,14 @@ async function startRun(passed, declared) {
|
|
|
2717
2874
|
}
|
|
2718
2875
|
};
|
|
2719
2876
|
}
|
|
2720
|
-
var import_node_crypto2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX, KEY_MAX, TITLE_MAX, COUNTED, BATCH_BYTES, ENVELOPE_BYTES;
|
|
2877
|
+
var import_node_crypto2, import_node_fs2, import_node_path2, RESOLVE_CHUNK, BATCH_MAX, DISCOVER_MAX, BATCH_DEFAULT, EXPECTED_MAX, KEY_MAX, TITLE_MAX, COUNTED, SCREENSHOT_TYPES, SCREENSHOT_BYTES, SCREENSHOTS_PER_RESULT, TYPE_BY_EXTENSION, UPLOADS_AT_ONCE, NAME_MAX, SKIPPED, BATCH_BYTES, ENVELOPE_BYTES;
|
|
2721
2878
|
var init_session = __esm({
|
|
2722
2879
|
"src/reporter-core/session.ts"() {
|
|
2723
2880
|
"use strict";
|
|
2724
2881
|
init_cjs_shims();
|
|
2725
2882
|
import_node_crypto2 = require("crypto");
|
|
2883
|
+
import_node_fs2 = require("fs");
|
|
2884
|
+
import_node_path2 = require("path");
|
|
2726
2885
|
init_api_url();
|
|
2727
2886
|
init_credentials();
|
|
2728
2887
|
init_client();
|
|
@@ -2736,6 +2895,24 @@ var init_session = __esm({
|
|
|
2736
2895
|
KEY_MAX = 1024;
|
|
2737
2896
|
TITLE_MAX = 300;
|
|
2738
2897
|
COUNTED = ["accepted", "duplicate", "conflict", "rejected"];
|
|
2898
|
+
SCREENSHOT_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
2899
|
+
SCREENSHOT_BYTES = 2 * 1024 * 1024;
|
|
2900
|
+
SCREENSHOTS_PER_RESULT = 20;
|
|
2901
|
+
TYPE_BY_EXTENSION = {
|
|
2902
|
+
".png": "image/png",
|
|
2903
|
+
".jpg": "image/jpeg",
|
|
2904
|
+
".jpeg": "image/jpeg",
|
|
2905
|
+
".webp": "image/webp"
|
|
2906
|
+
};
|
|
2907
|
+
UPLOADS_AT_ONCE = 4;
|
|
2908
|
+
NAME_MAX = 200;
|
|
2909
|
+
SKIPPED = {
|
|
2910
|
+
hidden: "its name starts with an underscore, which hides an attachment in Playwright's own reports too",
|
|
2911
|
+
type: "Plune takes PNG, JPEG and WebP",
|
|
2912
|
+
limit: `a result holds at most ${SCREENSHOTS_PER_RESULT}`,
|
|
2913
|
+
missing: "the file cannot be read on this machine",
|
|
2914
|
+
size: "Plune takes a file of at most 2 MB"
|
|
2915
|
+
};
|
|
2739
2916
|
BATCH_BYTES = 8 * 1024 * 1024;
|
|
2740
2917
|
ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ results: [] }));
|
|
2741
2918
|
}
|
|
@@ -2859,23 +3036,42 @@ async function handleRunReport(options = {}) {
|
|
|
2859
3036
|
if (lines.length === 0) throw new RunCommandError(`Nothing to send \u2014 ${file} is empty.`);
|
|
2860
3037
|
const { client } = connect(options);
|
|
2861
3038
|
const result = { batches: lines.length, sent: 0, failed: 0 };
|
|
3039
|
+
const groups = [];
|
|
3040
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
2862
3041
|
for (const line of lines) {
|
|
2863
|
-
const
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
3042
|
+
const group = line.session === void 0 ? void 0 : bySession.get(line.session);
|
|
3043
|
+
if (group !== void 0) {
|
|
3044
|
+
group.push(line);
|
|
3045
|
+
continue;
|
|
3046
|
+
}
|
|
3047
|
+
const alone = [line];
|
|
3048
|
+
groups.push(alone);
|
|
3049
|
+
if (line.session !== void 0) bySession.set(line.session, alone);
|
|
3050
|
+
}
|
|
3051
|
+
for (const group of groups) {
|
|
3052
|
+
for (const line of group) {
|
|
3053
|
+
const ready = line.results.filter((r) => !isPending(r));
|
|
3054
|
+
if (line.runId !== null && ready.length > 0) {
|
|
3055
|
+
const out = await client.post(`/v1/runs/${line.runId}/results`, { results: ready });
|
|
3056
|
+
if (out.ok) result.sent += ready.length;
|
|
3057
|
+
else {
|
|
3058
|
+
result.failed += ready.length;
|
|
3059
|
+
write(
|
|
3060
|
+
`Could not send ${ready.length} result(s) to run ${line.runId} \u2014 ${out.detail || out.kind}.`
|
|
3061
|
+
);
|
|
3062
|
+
}
|
|
2871
3063
|
}
|
|
2872
3064
|
}
|
|
3065
|
+
const pending = group.flatMap((line) => line.results.filter(isPending));
|
|
2873
3066
|
if (pending.length > 0) {
|
|
3067
|
+
const first = group[0];
|
|
2874
3068
|
const session = await startRun({
|
|
2875
3069
|
...options.apiUrl !== void 0 ? { apiUrl: options.apiUrl } : {},
|
|
2876
3070
|
...options.token !== void 0 ? { token: options.token } : {},
|
|
2877
3071
|
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
|
|
2878
|
-
...
|
|
3072
|
+
...first.externalKey !== null ? { externalKey: first.externalKey } : {},
|
|
3073
|
+
// Where the run came from, as the reporter recorded it (plune#927) — never this machine's own.
|
|
3074
|
+
...first.meta !== void 0 ? { meta: first.meta } : {},
|
|
2879
3075
|
// The replay does not know whether the run is complete, so it must not say that it is.
|
|
2880
3076
|
log: write
|
|
2881
3077
|
});
|
|
@@ -2886,15 +3082,34 @@ async function handleRunReport(options = {}) {
|
|
|
2886
3082
|
}
|
|
2887
3083
|
}
|
|
2888
3084
|
write(`Replayed ${result.batches} batch(es) from ${file}: ${result.sent} sent, ${result.failed} not.`);
|
|
3085
|
+
if (result.failed === 0) {
|
|
3086
|
+
const sentAs = sentName(file, (options.now ?? Date.now)());
|
|
3087
|
+
try {
|
|
3088
|
+
fs7.renameSync(file, sentAs);
|
|
3089
|
+
write(
|
|
3090
|
+
`Every result was sent \u2014 ${file} is now ${sentAs}, so a later replay does not send them twice.`
|
|
3091
|
+
);
|
|
3092
|
+
} catch (err) {
|
|
3093
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
3094
|
+
write(
|
|
3095
|
+
`Every result was sent, but ${file} could not be renamed (${why}) \u2014 take it away by hand, or a later replay sends them again.`
|
|
3096
|
+
);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
2889
3099
|
return result;
|
|
2890
3100
|
}
|
|
2891
|
-
|
|
3101
|
+
function sentName(file, at) {
|
|
3102
|
+
const { dir, name, ext } = path8.parse(file);
|
|
3103
|
+
return path8.join(dir, `${name}.${new Date(at).toISOString().replace(/[:.]/g, "-")}.sent${ext}`);
|
|
3104
|
+
}
|
|
3105
|
+
var import_node_child_process, fs7, path8, NoTokenError, RunCommandError, isPending;
|
|
2892
3106
|
var init_run_lifecycle = __esm({
|
|
2893
3107
|
"src/cli/commands/run-lifecycle.ts"() {
|
|
2894
3108
|
"use strict";
|
|
2895
3109
|
init_cjs_shims();
|
|
2896
3110
|
import_node_child_process = require("child_process");
|
|
2897
3111
|
fs7 = __toESM(require("fs"), 1);
|
|
3112
|
+
path8 = __toESM(require("path"), 1);
|
|
2898
3113
|
init_api_url();
|
|
2899
3114
|
init_credentials();
|
|
2900
3115
|
init_client();
|
|
@@ -3102,7 +3317,12 @@ function errorTextOf(detail) {
|
|
|
3102
3317
|
const head = [detail.attrs["message"], detail.attrs["type"]].filter((s) => s !== void 0 && s !== "");
|
|
3103
3318
|
return [...head, detail.text.trim()].filter((s) => s !== "").join("\n\n");
|
|
3104
3319
|
}
|
|
3105
|
-
function
|
|
3320
|
+
function playwrightOf(classname, name) {
|
|
3321
|
+
if (!SPEC_FILE.test(classname)) return void 0;
|
|
3322
|
+
return { file: classname.replaceAll("\\", "/"), titles: name.split(" \u203A ") };
|
|
3323
|
+
}
|
|
3324
|
+
function keyOf(classname, name, playwright) {
|
|
3325
|
+
if (playwright !== void 0) return { kind: "path-title", value: [playwright.file, ...playwright.titles].join("#") };
|
|
3106
3326
|
return { kind: "path-title", value: classname === "" ? name : `${classname}#${name}` };
|
|
3107
3327
|
}
|
|
3108
3328
|
function titleOf(classname, name) {
|
|
@@ -3142,12 +3362,15 @@ function readJUnit(source, file) {
|
|
|
3142
3362
|
const name = test.attrs["name"] ?? "";
|
|
3143
3363
|
const classname = test.attrs["classname"] ?? test.attrs["class"] ?? "";
|
|
3144
3364
|
const { status, detail } = outcomeOf(test);
|
|
3145
|
-
const
|
|
3365
|
+
const playwright = playwrightOf(classname, name);
|
|
3366
|
+
const key = keyOf(classname, name, playwright);
|
|
3367
|
+
const stated = TOKEN.exec(playwright?.titles.at(-1) ?? name)?.[1];
|
|
3146
3368
|
const specRef = specRefOf(test, suite, classname);
|
|
3147
3369
|
const errorContext = errorTextOf(detail);
|
|
3148
3370
|
const execution = executionOf(test, suite);
|
|
3149
3371
|
return {
|
|
3150
3372
|
resultKey: resultKey(key.value, 0),
|
|
3373
|
+
...stated !== void 0 ? { testCaseId: stated } : {},
|
|
3151
3374
|
keys: [key],
|
|
3152
3375
|
source: SOURCE,
|
|
3153
3376
|
rawStatus: status,
|
|
@@ -3161,7 +3384,7 @@ function readJUnit(source, file) {
|
|
|
3161
3384
|
function looksLikeJUnit(source) {
|
|
3162
3385
|
return /<\s*testsuites?\b/.test(source.slice(0, 4096));
|
|
3163
3386
|
}
|
|
3164
|
-
var SOURCE;
|
|
3387
|
+
var SOURCE, TOKEN, SPEC_FILE;
|
|
3165
3388
|
var init_junit = __esm({
|
|
3166
3389
|
"src/importers/junit.ts"() {
|
|
3167
3390
|
"use strict";
|
|
@@ -3169,6 +3392,8 @@ var init_junit = __esm({
|
|
|
3169
3392
|
init_result_key();
|
|
3170
3393
|
init_xml();
|
|
3171
3394
|
SOURCE = "junit";
|
|
3395
|
+
TOKEN = /@P([A-Za-z0-9_-]+)/;
|
|
3396
|
+
SPEC_FILE = /\.(?:spec|test)\.[cm]?[jt]sx?$/;
|
|
3172
3397
|
}
|
|
3173
3398
|
});
|
|
3174
3399
|
|
|
@@ -3191,15 +3416,18 @@ function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
|
3191
3416
|
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
3192
3417
|
}
|
|
3193
3418
|
function repoRootOf(dir, stopAt) {
|
|
3194
|
-
for (let here = (0,
|
|
3195
|
-
if ((0,
|
|
3196
|
-
if (here === stopAt || (0,
|
|
3419
|
+
for (let here = (0, import_node_path3.resolve)(dir); ; here = (0, import_node_path3.dirname)(here)) {
|
|
3420
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(here, ".git"))) return here;
|
|
3421
|
+
if (here === stopAt || (0, import_node_path3.dirname)(here) === here) return void 0;
|
|
3197
3422
|
}
|
|
3198
3423
|
}
|
|
3199
3424
|
function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
3200
3425
|
const text2 = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
3201
3426
|
return cut(machineless(text2, attempt.repoRoot, home));
|
|
3202
3427
|
}
|
|
3428
|
+
function filesOf(attempt) {
|
|
3429
|
+
return attempt.attachments.filter((a) => a.path !== void 0);
|
|
3430
|
+
}
|
|
3203
3431
|
function machineless(raw, repoRoot, home) {
|
|
3204
3432
|
let text2 = (0, import_node_util.stripVTControlCharacters)(raw).replaceAll("\x1B", "");
|
|
3205
3433
|
const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
|
|
@@ -3209,12 +3437,12 @@ function machineless(raw, repoRoot, home) {
|
|
|
3209
3437
|
if (repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
|
|
3210
3438
|
return text2;
|
|
3211
3439
|
}
|
|
3212
|
-
function pathIn(
|
|
3213
|
-
const parts =
|
|
3440
|
+
function pathIn(path15) {
|
|
3441
|
+
const parts = path15.split(/[\\/]+/);
|
|
3214
3442
|
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
3215
3443
|
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
3216
3444
|
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
3217
|
-
const escaped = /^\/(?!\/)/.test(
|
|
3445
|
+
const escaped = /^\/(?!\/)/.test(path15) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
3218
3446
|
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
3219
3447
|
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
3220
3448
|
}
|
|
@@ -3259,12 +3487,12 @@ function placeOf(error, testFile) {
|
|
|
3259
3487
|
}
|
|
3260
3488
|
function fileOf(raw) {
|
|
3261
3489
|
if (!raw.startsWith("file://")) return raw;
|
|
3262
|
-
let
|
|
3490
|
+
let path15 = raw.slice("file://".length);
|
|
3263
3491
|
try {
|
|
3264
|
-
|
|
3492
|
+
path15 = decodeURIComponent(path15);
|
|
3265
3493
|
} catch {
|
|
3266
3494
|
}
|
|
3267
|
-
return /^\/[A-Za-z]:\//.test(
|
|
3495
|
+
return /^\/[A-Za-z]:\//.test(path15) ? path15.slice(1) : path15;
|
|
3268
3496
|
}
|
|
3269
3497
|
function fromRoot(place, root) {
|
|
3270
3498
|
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
@@ -3279,14 +3507,14 @@ function fromRoot(place, root) {
|
|
|
3279
3507
|
function webLink(href) {
|
|
3280
3508
|
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
3281
3509
|
}
|
|
3282
|
-
var
|
|
3510
|
+
var import_node_fs3, import_node_os2, import_node_path3, import_node_util, PATH_START, TEXT_LIMIT, HEADLINE_LIMIT, jsonBytes, ANY_HOME, fileNameOf, omitted, METADATA_TYPE, REPO_RELATIVE, FRAME, FRAME_MAX, firstLine, normal;
|
|
3283
3511
|
var init_failure_detail = __esm({
|
|
3284
3512
|
"src/reporter-core/failure-detail.ts"() {
|
|
3285
3513
|
"use strict";
|
|
3286
3514
|
init_cjs_shims();
|
|
3287
|
-
|
|
3515
|
+
import_node_fs3 = require("fs");
|
|
3288
3516
|
import_node_os2 = require("os");
|
|
3289
|
-
|
|
3517
|
+
import_node_path3 = require("path");
|
|
3290
3518
|
import_node_util = require("util");
|
|
3291
3519
|
PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
3292
3520
|
TEXT_LIMIT = 524288;
|
|
@@ -3300,7 +3528,7 @@ var init_failure_detail = __esm({
|
|
|
3300
3528
|
FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
3301
3529
|
FRAME_MAX = 8192;
|
|
3302
3530
|
firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
3303
|
-
normal = (
|
|
3531
|
+
normal = (path15) => import_node_path3.posix.normalize(path15.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
3304
3532
|
}
|
|
3305
3533
|
});
|
|
3306
3534
|
|
|
@@ -3308,7 +3536,7 @@ var init_failure_detail = __esm({
|
|
|
3308
3536
|
function statedIdOf(test, title) {
|
|
3309
3537
|
const annotation = test.annotations?.find((a) => a.type === "PluneId")?.description;
|
|
3310
3538
|
if (annotation !== void 0 && annotation !== "") return annotation;
|
|
3311
|
-
return
|
|
3539
|
+
return TOKEN2.exec(title)?.[1];
|
|
3312
3540
|
}
|
|
3313
3541
|
function attemptOf(result, file, ctx) {
|
|
3314
3542
|
const steps = (list2) => (list2 ?? []).map((s) => ({ title: s.title ?? "", failed: s.error !== void 0, steps: steps(s.steps) }));
|
|
@@ -3319,10 +3547,10 @@ function attemptOf(result, file, ctx) {
|
|
|
3319
3547
|
...typeof at?.file === "string" && typeof at.line === "number" ? { location: { file: at.file, line: at.line, ...typeof at.column === "number" ? { column: at.column } : {} } } : {}
|
|
3320
3548
|
})),
|
|
3321
3549
|
steps: steps(result.steps),
|
|
3322
|
-
attachments: (result.attachments ?? []).map(({ name, contentType, path:
|
|
3550
|
+
attachments: (result.attachments ?? []).map(({ name, contentType, path: path15 }) => ({
|
|
3323
3551
|
name: name ?? "",
|
|
3324
3552
|
...contentType !== void 0 ? { contentType } : {},
|
|
3325
|
-
...
|
|
3553
|
+
...path15 !== void 0 ? { path: path15 } : {}
|
|
3326
3554
|
})),
|
|
3327
3555
|
...ctx.buildHref !== void 0 ? { buildHref: ctx.buildHref } : {},
|
|
3328
3556
|
// Spec files are posix and relative to the root dir; the core reads either slash.
|
|
@@ -3338,6 +3566,7 @@ function pendingFrom(spec, test, result, file, titles, ctx) {
|
|
|
3338
3566
|
const attempt = attemptOf(result, file, ctx);
|
|
3339
3567
|
const errorContext = errorContextOf(attempt);
|
|
3340
3568
|
const failure = NOT_FAILED.has(attempt.status) ? void 0 : failureOf(attempt);
|
|
3569
|
+
const files = filesOf(attempt);
|
|
3341
3570
|
const keys = [
|
|
3342
3571
|
...test.id !== void 0 ? [{ kind: "playwright-id", value: test.id }] : [],
|
|
3343
3572
|
{ kind: "path-title", value: pathTitle }
|
|
@@ -3366,7 +3595,8 @@ function pendingFrom(spec, test, result, file, titles, ctx) {
|
|
|
3366
3595
|
}
|
|
3367
3596
|
} : {},
|
|
3368
3597
|
...errorContext !== "" ? { errorContext } : {},
|
|
3369
|
-
...failure !== void 0 ? { failure } : {}
|
|
3598
|
+
...failure !== void 0 ? { failure } : {},
|
|
3599
|
+
...files.length > 0 ? { files } : {}
|
|
3370
3600
|
};
|
|
3371
3601
|
}
|
|
3372
3602
|
function walk(suite, describes, depth, ctx, out) {
|
|
@@ -3393,7 +3623,7 @@ function readPlaywrightJson(source, file) {
|
|
|
3393
3623
|
const { config, suites } = parsed;
|
|
3394
3624
|
const rootDir = typeof config?.rootDir === "string" ? config.rootDir : void 0;
|
|
3395
3625
|
const buildHref = config?.metadata?.ci?.buildHref;
|
|
3396
|
-
const repoRoot = rootDir !== void 0 && (0,
|
|
3626
|
+
const repoRoot = rootDir !== void 0 && (0, import_node_fs4.existsSync)(rootDir) ? repoRootOf(rootDir) : void 0;
|
|
3397
3627
|
const ctx = {
|
|
3398
3628
|
...rootDir !== void 0 ? { rootDir } : {},
|
|
3399
3629
|
...repoRoot !== void 0 ? { repoRoot } : {},
|
|
@@ -3402,21 +3632,26 @@ function readPlaywrightJson(source, file) {
|
|
|
3402
3632
|
const out = [];
|
|
3403
3633
|
for (const suite of suites) walk(suite, [], 0, ctx, out);
|
|
3404
3634
|
const ciUrl = webLink(ctx.buildHref);
|
|
3405
|
-
|
|
3635
|
+
const ci = config?.metadata?.ci;
|
|
3636
|
+
return {
|
|
3637
|
+
results: out,
|
|
3638
|
+
...ciUrl !== void 0 ? { ciUrl } : {},
|
|
3639
|
+
...ci !== void 0 ? { ci } : {}
|
|
3640
|
+
};
|
|
3406
3641
|
}
|
|
3407
3642
|
function looksLikePlaywrightJson(source) {
|
|
3408
3643
|
return /^\s*\{/.test(source) && source.includes('"suites"');
|
|
3409
3644
|
}
|
|
3410
|
-
var
|
|
3645
|
+
var import_node_fs4, SOURCE2, TOKEN2, NOT_FAILED, JsonReportError;
|
|
3411
3646
|
var init_playwright_json = __esm({
|
|
3412
3647
|
"src/importers/playwright-json.ts"() {
|
|
3413
3648
|
"use strict";
|
|
3414
3649
|
init_cjs_shims();
|
|
3415
|
-
|
|
3650
|
+
import_node_fs4 = require("fs");
|
|
3416
3651
|
init_failure_detail();
|
|
3417
3652
|
init_result_key();
|
|
3418
3653
|
SOURCE2 = "playwright";
|
|
3419
|
-
|
|
3654
|
+
TOKEN2 = /@P([A-Za-z0-9_-]+)/;
|
|
3420
3655
|
NOT_FAILED = /* @__PURE__ */ new Set(["passed", "skipped"]);
|
|
3421
3656
|
JsonReportError = class extends Error {
|
|
3422
3657
|
constructor(file, detail) {
|
|
@@ -3485,7 +3720,7 @@ async function handleRunImport(options) {
|
|
|
3485
3720
|
} catch {
|
|
3486
3721
|
throw new RunCommandError(`Cannot read ${options.file}.`);
|
|
3487
3722
|
}
|
|
3488
|
-
const { format, results, ciUrl } = readReport(source, options.file, options.format);
|
|
3723
|
+
const { format, results, ciUrl, ci } = readReport(source, options.file, options.format);
|
|
3489
3724
|
if (results.length === 0) {
|
|
3490
3725
|
throw new RunCommandError(`No test cases in ${options.file} \u2014 it parsed, but there is nothing in it.`);
|
|
3491
3726
|
}
|
|
@@ -3502,7 +3737,10 @@ async function handleRunImport(options) {
|
|
|
3502
3737
|
...options.create === true ? { offerDiscovered: true } : {},
|
|
3503
3738
|
kind: "automated",
|
|
3504
3739
|
// The CI run the report names, never this machine's: whoever imports may be another job (#790).
|
|
3505
|
-
|
|
3740
|
+
// The commit and branch likewise, where the report names a commit; a report that names none —
|
|
3741
|
+
// JUnit's never does — takes this job's own, which is where the tests ran when import follows
|
|
3742
|
+
// them in one job (plune-ai/plune#927).
|
|
3743
|
+
meta: { runner: format, ...ciUrl !== void 0 ? { ciUrl } : {}, ...ciCommit(ci) },
|
|
3506
3744
|
log: write
|
|
3507
3745
|
},
|
|
3508
3746
|
// The file is the complete list of what ran, so the platform can resolve every test in one
|
|
@@ -3526,7 +3764,9 @@ async function handleRunImport(options) {
|
|
|
3526
3764
|
offered: stats.offered,
|
|
3527
3765
|
created: stats.created,
|
|
3528
3766
|
unoffered: stats.unoffered,
|
|
3767
|
+
...stats.unofferedWhy !== void 0 ? { unofferedWhy: stats.unofferedWhy } : {},
|
|
3529
3768
|
deferred: stats.deferred,
|
|
3769
|
+
screenshots: { ...stats.screenshots },
|
|
3530
3770
|
unlocatable: results.filter((r) => r.specRef === void 0).length,
|
|
3531
3771
|
offering: options.create === true
|
|
3532
3772
|
};
|
|
@@ -3550,6 +3790,12 @@ function describe(result, apiUrl) {
|
|
|
3550
3790
|
if (result.conflict > 0 || result.rejected > 0) {
|
|
3551
3791
|
lines.push(`${result.conflict} conflicted and ${result.rejected} were refused.`);
|
|
3552
3792
|
}
|
|
3793
|
+
const shots = result.screenshots;
|
|
3794
|
+
if (shots.uploaded + shots.skipped + shots.failed > 0) {
|
|
3795
|
+
lines.push(
|
|
3796
|
+
`${shots.uploaded} screenshot(s) uploaded to their results` + (shots.skipped > 0 ? `, ${shots.skipped} skipped` : "") + (shots.failed > 0 ? `, ${shots.failed} could not be uploaded` : "") + "."
|
|
3797
|
+
);
|
|
3798
|
+
}
|
|
3553
3799
|
if (result.deferred > 0) {
|
|
3554
3800
|
lines.push(
|
|
3555
3801
|
`${result.deferred} could not be sent and are in the fallback file \u2014 "plune run report" retries them.`
|
|
@@ -3569,7 +3815,7 @@ function describe(result, apiUrl) {
|
|
|
3569
3815
|
}
|
|
3570
3816
|
if (result.unoffered > 0) {
|
|
3571
3817
|
lines.push(
|
|
3572
|
-
`${result.unoffered} more could not be offered \u2014
|
|
3818
|
+
`${result.unoffered} more could not be offered \u2014 ${NEXT[result.unofferedWhy ?? "other"]}`
|
|
3573
3819
|
);
|
|
3574
3820
|
}
|
|
3575
3821
|
if (result.unlocatable > 0) {
|
|
@@ -3580,7 +3826,7 @@ function describe(result, apiUrl) {
|
|
|
3580
3826
|
if (result.runId !== null) lines.push(runUrl2(apiUrl, result.runId));
|
|
3581
3827
|
return lines;
|
|
3582
3828
|
}
|
|
3583
|
-
var fs8;
|
|
3829
|
+
var fs8, NEXT;
|
|
3584
3830
|
var init_run_import = __esm({
|
|
3585
3831
|
"src/cli/commands/run-import.ts"() {
|
|
3586
3832
|
"use strict";
|
|
@@ -3592,6 +3838,11 @@ var init_run_import = __esm({
|
|
|
3592
3838
|
init_api_url();
|
|
3593
3839
|
init_credentials();
|
|
3594
3840
|
init_env2();
|
|
3841
|
+
NEXT = {
|
|
3842
|
+
queue: "the review queue is full. Approve or reject what is waiting, then import this report again to offer the rest.",
|
|
3843
|
+
cases: "the project is at its test case limit. Delete cases you no longer need, or ask an operator to raise the limit, then import this report again to offer the rest.",
|
|
3844
|
+
other: 'the reason is in the "could not offer" line above.'
|
|
3845
|
+
};
|
|
3595
3846
|
}
|
|
3596
3847
|
});
|
|
3597
3848
|
|
|
@@ -3608,7 +3859,7 @@ function isRunResult(v) {
|
|
|
3608
3859
|
}
|
|
3609
3860
|
function handleReport(opts = {}) {
|
|
3610
3861
|
const cwd = opts.cwd ?? process.cwd();
|
|
3611
|
-
const file =
|
|
3862
|
+
const file = path9.join(cwd, ".plune", "last-run.json");
|
|
3612
3863
|
let raw;
|
|
3613
3864
|
try {
|
|
3614
3865
|
raw = fs9.readFileSync(file, "utf8");
|
|
@@ -3626,13 +3877,13 @@ function handleReport(opts = {}) {
|
|
|
3626
3877
|
}
|
|
3627
3878
|
return parsed;
|
|
3628
3879
|
}
|
|
3629
|
-
var fs9,
|
|
3880
|
+
var fs9, path9, ReportNotFoundError;
|
|
3630
3881
|
var init_report = __esm({
|
|
3631
3882
|
"src/cli/commands/report.ts"() {
|
|
3632
3883
|
"use strict";
|
|
3633
3884
|
init_cjs_shims();
|
|
3634
3885
|
fs9 = __toESM(require("fs"), 1);
|
|
3635
|
-
|
|
3886
|
+
path9 = __toESM(require("path"), 1);
|
|
3636
3887
|
ReportNotFoundError = class extends Error {
|
|
3637
3888
|
code = "NO_SAVED_RUN";
|
|
3638
3889
|
constructor(message) {
|
|
@@ -3836,7 +4087,7 @@ async function runInitWizard(cwd) {
|
|
|
3836
4087
|
process.exit(1);
|
|
3837
4088
|
return;
|
|
3838
4089
|
}
|
|
3839
|
-
const defaultModel = provider === "anthropic" ? "claude-
|
|
4090
|
+
const defaultModel = provider === "anthropic" ? "claude-sonnet-5-5" : "gpt-4o-mini";
|
|
3840
4091
|
const model = await clack.text({
|
|
3841
4092
|
message: "Model name:",
|
|
3842
4093
|
defaultValue: defaultModel,
|
|
@@ -3856,7 +4107,7 @@ async function runInitWizard(cwd) {
|
|
|
3856
4107
|
process.exit(1);
|
|
3857
4108
|
return;
|
|
3858
4109
|
}
|
|
3859
|
-
const outputPath =
|
|
4110
|
+
const outputPath = path10.join(cwd, "plune.yaml");
|
|
3860
4111
|
if (fs11.existsSync(outputPath)) {
|
|
3861
4112
|
const overwrite = await clack.confirm({
|
|
3862
4113
|
message: "plune.yaml already exists. Overwrite?"
|
|
@@ -3900,14 +4151,14 @@ async function runInitWizard(cwd) {
|
|
|
3900
4151
|
spin.stop("plune.yaml created!");
|
|
3901
4152
|
clack.outro("plune.yaml is ready.");
|
|
3902
4153
|
}
|
|
3903
|
-
var clack, fs11,
|
|
4154
|
+
var clack, fs11, path10, import_yaml2;
|
|
3904
4155
|
var init_wizard = __esm({
|
|
3905
4156
|
"src/config/init/wizard.ts"() {
|
|
3906
4157
|
"use strict";
|
|
3907
4158
|
init_cjs_shims();
|
|
3908
4159
|
clack = __toESM(require("@clack/prompts"), 1);
|
|
3909
4160
|
fs11 = __toESM(require("fs"), 1);
|
|
3910
|
-
|
|
4161
|
+
path10 = __toESM(require("path"), 1);
|
|
3911
4162
|
import_yaml2 = require("yaml");
|
|
3912
4163
|
init_errors();
|
|
3913
4164
|
}
|
|
@@ -3915,21 +4166,21 @@ var init_wizard = __esm({
|
|
|
3915
4166
|
|
|
3916
4167
|
// src/cli/templates/index.ts
|
|
3917
4168
|
function writeTemplateFile(cwd, rel, content, force) {
|
|
3918
|
-
const target =
|
|
4169
|
+
const target = path11.join(cwd, rel);
|
|
3919
4170
|
if (!force && fs12.existsSync(target)) {
|
|
3920
4171
|
return "skipped";
|
|
3921
4172
|
}
|
|
3922
|
-
fs12.mkdirSync(
|
|
4173
|
+
fs12.mkdirSync(path11.dirname(target), { recursive: true });
|
|
3923
4174
|
fs12.writeFileSync(target, content, "utf8");
|
|
3924
4175
|
return "written";
|
|
3925
4176
|
}
|
|
3926
|
-
var fs12,
|
|
4177
|
+
var fs12, path11, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
|
|
3927
4178
|
var init_templates = __esm({
|
|
3928
4179
|
"src/cli/templates/index.ts"() {
|
|
3929
4180
|
"use strict";
|
|
3930
4181
|
init_cjs_shims();
|
|
3931
4182
|
fs12 = __toESM(require("fs"), 1);
|
|
3932
|
-
|
|
4183
|
+
path11 = __toESM(require("path"), 1);
|
|
3933
4184
|
ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
|
|
3934
4185
|
# .env is gitignored; .env.example is safe to commit.
|
|
3935
4186
|
# Plune reads whichever key matches your configured provider.
|
|
@@ -3950,7 +4201,7 @@ OPENROUTER_API_KEY=YOUR_OPENROUTER_API_KEY_HERE
|
|
|
3950
4201
|
version: 1
|
|
3951
4202
|
provider:
|
|
3952
4203
|
type: anthropic
|
|
3953
|
-
model: claude-
|
|
4204
|
+
model: claude-sonnet-5-5
|
|
3954
4205
|
evals:
|
|
3955
4206
|
- id: example
|
|
3956
4207
|
prompt: "Answer concisely. {{question}}"
|
|
@@ -4185,7 +4436,7 @@ __export(sync_exports, {
|
|
|
4185
4436
|
reportSyncFailure: () => reportSyncFailure
|
|
4186
4437
|
});
|
|
4187
4438
|
function readRun(cwd, file) {
|
|
4188
|
-
const target = file !== void 0 ?
|
|
4439
|
+
const target = file !== void 0 ? path12.resolve(cwd, file) : path12.join(cwd, ".plune", "last-run.json");
|
|
4189
4440
|
let raw;
|
|
4190
4441
|
try {
|
|
4191
4442
|
raw = fs13.readFileSync(target, "utf8");
|
|
@@ -4252,13 +4503,13 @@ function reportSyncFailure(err, write) {
|
|
|
4252
4503
|
}
|
|
4253
4504
|
return null;
|
|
4254
4505
|
}
|
|
4255
|
-
var fs13,
|
|
4506
|
+
var fs13, path12, NotLoggedInError, TokenRejectedError2, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
4256
4507
|
var init_sync = __esm({
|
|
4257
4508
|
"src/cli/commands/sync.ts"() {
|
|
4258
4509
|
"use strict";
|
|
4259
4510
|
init_cjs_shims();
|
|
4260
4511
|
fs13 = __toESM(require("fs"), 1);
|
|
4261
|
-
|
|
4512
|
+
path12 = __toESM(require("path"), 1);
|
|
4262
4513
|
init_run_result_schema();
|
|
4263
4514
|
init_api_url();
|
|
4264
4515
|
init_credentials();
|
|
@@ -4308,9 +4559,9 @@ async function findLatestCairnRun(runsDir = "runs") {
|
|
|
4308
4559
|
let newest;
|
|
4309
4560
|
for (const entry of entries) {
|
|
4310
4561
|
if (!entry.isDirectory()) continue;
|
|
4311
|
-
const dir =
|
|
4562
|
+
const dir = path13.join(runsDir, entry.name);
|
|
4312
4563
|
try {
|
|
4313
|
-
const report = await fs14.lstat(
|
|
4564
|
+
const report = await fs14.lstat(path13.join(dir, "report.json"));
|
|
4314
4565
|
if (!report.isFile()) continue;
|
|
4315
4566
|
if (newest === void 0 || report.mtimeMs > newest.at) {
|
|
4316
4567
|
newest = { dir, at: report.mtimeMs };
|
|
@@ -4329,7 +4580,7 @@ async function readJson(file) {
|
|
|
4329
4580
|
}
|
|
4330
4581
|
}
|
|
4331
4582
|
async function defaultReadEvidence(dir) {
|
|
4332
|
-
const raw = await readJson(
|
|
4583
|
+
const raw = await readJson(path13.join(dir, "api-evidence.json"));
|
|
4333
4584
|
if (!Array.isArray(raw)) return void 0;
|
|
4334
4585
|
return raw.map((r) => {
|
|
4335
4586
|
const rec = asRecord(r);
|
|
@@ -4371,7 +4622,7 @@ function casesFromApi(report, verdicts) {
|
|
|
4371
4622
|
});
|
|
4372
4623
|
}
|
|
4373
4624
|
async function readCairnRun(dir, deps = {}) {
|
|
4374
|
-
const raw = await readJson(
|
|
4625
|
+
const raw = await readJson(path13.join(dir, "report.json"));
|
|
4375
4626
|
if (raw === void 0) throw new CairnRunNotFoundError(dir);
|
|
4376
4627
|
const report = asRecord(raw);
|
|
4377
4628
|
if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
|
|
@@ -4401,20 +4652,20 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
4401
4652
|
return {
|
|
4402
4653
|
schemaVersion: INGEST_SCHEMA_VERSION,
|
|
4403
4654
|
run: {
|
|
4404
|
-
runId: str(report["runId"],
|
|
4655
|
+
runId: str(report["runId"], path13.basename(dir)),
|
|
4405
4656
|
mode,
|
|
4406
4657
|
target: str(report["url"], "unknown")
|
|
4407
4658
|
},
|
|
4408
4659
|
cases
|
|
4409
4660
|
};
|
|
4410
4661
|
}
|
|
4411
|
-
var fs14,
|
|
4662
|
+
var fs14, path13, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
|
|
4412
4663
|
var init_cairn_artifact = __esm({
|
|
4413
4664
|
"src/cli/commands/cairn-artifact.ts"() {
|
|
4414
4665
|
"use strict";
|
|
4415
4666
|
init_cjs_shims();
|
|
4416
4667
|
fs14 = __toESM(require("fs/promises"), 1);
|
|
4417
|
-
|
|
4668
|
+
path13 = __toESM(require("path"), 1);
|
|
4418
4669
|
SUPPORTED_ARTIFACT_VERSION = 1;
|
|
4419
4670
|
INGEST_SCHEMA_VERSION = 1;
|
|
4420
4671
|
NoCairnRunsError = class extends Error {
|
|
@@ -4581,7 +4832,7 @@ __export(cases_exports, {
|
|
|
4581
4832
|
function gitDirty(file) {
|
|
4582
4833
|
try {
|
|
4583
4834
|
const out = (0, import_node_child_process2.execFileSync)("git", ["status", "--porcelain", "--", file], {
|
|
4584
|
-
cwd:
|
|
4835
|
+
cwd: path14.dirname(file),
|
|
4585
4836
|
stdio: ["ignore", "pipe", "ignore"],
|
|
4586
4837
|
encoding: "utf8"
|
|
4587
4838
|
});
|
|
@@ -4607,9 +4858,9 @@ async function handlePull(deps = {}) {
|
|
|
4607
4858
|
const cwd = deps.cwd ?? process.cwd();
|
|
4608
4859
|
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4609
4860
|
const token = tokenOf(deps);
|
|
4610
|
-
const file =
|
|
4861
|
+
const file = path14.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
4611
4862
|
if (fs15.existsSync(file) && !deps.force && (deps.isDirty ?? gitDirty)(file))
|
|
4612
|
-
throw new DirtyFileError(
|
|
4863
|
+
throw new DirtyFileError(path14.relative(cwd, file) || file);
|
|
4613
4864
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
4614
4865
|
const endpoint = deps.suiteId !== void 0 ? `${apiUrl}/v1/suites/${encodeURIComponent(deps.suiteId)}/markdown` : `${apiUrl}/v1/test-cases/markdown`;
|
|
4615
4866
|
let res;
|
|
@@ -4624,7 +4875,7 @@ async function handlePull(deps = {}) {
|
|
|
4624
4875
|
throw new SyncHttpError(res.status, detail);
|
|
4625
4876
|
}
|
|
4626
4877
|
const text2 = await res.text();
|
|
4627
|
-
fs15.mkdirSync(
|
|
4878
|
+
fs15.mkdirSync(path14.dirname(file), { recursive: true });
|
|
4628
4879
|
fs15.writeFileSync(file, text2, "utf8");
|
|
4629
4880
|
return { file, cases: Number(res.headers.get("x-plune-cases") ?? "0") };
|
|
4630
4881
|
}
|
|
@@ -4632,7 +4883,7 @@ async function handlePush(deps = {}) {
|
|
|
4632
4883
|
const cwd = deps.cwd ?? process.cwd();
|
|
4633
4884
|
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4634
4885
|
const token = tokenOf(deps);
|
|
4635
|
-
const file =
|
|
4886
|
+
const file = path14.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
4636
4887
|
let text2;
|
|
4637
4888
|
try {
|
|
4638
4889
|
text2 = fs15.readFileSync(file, "utf8");
|
|
@@ -4686,18 +4937,18 @@ function reportCasesFailure(err, write) {
|
|
|
4686
4937
|
}
|
|
4687
4938
|
return null;
|
|
4688
4939
|
}
|
|
4689
|
-
var import_node_child_process2, fs15,
|
|
4940
|
+
var import_node_child_process2, fs15, path14, DEFAULT_CASES_FILE, DirtyFileError;
|
|
4690
4941
|
var init_cases = __esm({
|
|
4691
4942
|
"src/cli/commands/cases.ts"() {
|
|
4692
4943
|
"use strict";
|
|
4693
4944
|
init_cjs_shims();
|
|
4694
4945
|
import_node_child_process2 = require("child_process");
|
|
4695
4946
|
fs15 = __toESM(require("fs"), 1);
|
|
4696
|
-
|
|
4947
|
+
path14 = __toESM(require("path"), 1);
|
|
4697
4948
|
init_api_url();
|
|
4698
4949
|
init_credentials();
|
|
4699
4950
|
init_sync();
|
|
4700
|
-
DEFAULT_CASES_FILE =
|
|
4951
|
+
DEFAULT_CASES_FILE = path14.join("plune", "cases.md");
|
|
4701
4952
|
DirtyFileError = class extends Error {
|
|
4702
4953
|
constructor(file) {
|
|
4703
4954
|
super(`${file} has uncommitted changes; commit them or pass --force to overwrite.`);
|
|
@@ -4778,11 +5029,11 @@ module.exports = __toCommonJS(cli_exports);
|
|
|
4778
5029
|
init_cjs_shims();
|
|
4779
5030
|
init_api_url();
|
|
4780
5031
|
var import_commander = require("commander");
|
|
4781
|
-
var
|
|
4782
|
-
var
|
|
5032
|
+
var import_node_fs5 = require("fs");
|
|
5033
|
+
var import_node_path4 = require("path");
|
|
4783
5034
|
var import_node_url = require("url");
|
|
4784
|
-
var __dirname = (0,
|
|
4785
|
-
var pkg = JSON.parse((0,
|
|
5035
|
+
var __dirname = (0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
5036
|
+
var pkg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
4786
5037
|
function toFormat(s) {
|
|
4787
5038
|
return s === "json" || s === "markdown" ? s : "console";
|
|
4788
5039
|
}
|
|
@@ -4791,7 +5042,7 @@ function shouldColor(outputPath) {
|
|
|
4791
5042
|
}
|
|
4792
5043
|
function writeReport(text2, outputPath) {
|
|
4793
5044
|
if (outputPath !== void 0) {
|
|
4794
|
-
(0,
|
|
5045
|
+
(0, import_node_fs5.writeFileSync)(outputPath, text2);
|
|
4795
5046
|
} else {
|
|
4796
5047
|
process.stdout.write(text2 + "\n");
|
|
4797
5048
|
}
|
|
@@ -4833,6 +5084,12 @@ function createProgram() {
|
|
|
4833
5084
|
const program = new import_commander.Command();
|
|
4834
5085
|
program.name("plune").description("Plune \u2014 AI-powered assertion testing CLI").version(pkg.version);
|
|
4835
5086
|
program.option("-c, --config <path>", "Path to plune.yaml config file (applies to all commands)").option("-v, --verbose", "Verbose output, including stack traces on unexpected errors", false).option("--no-color", "Disable colored output regardless of TTY");
|
|
5087
|
+
program.hook("preAction", async (_program, action) => {
|
|
5088
|
+
const { loadEnv: loadEnv2 } = await Promise.resolve().then(() => (init_env(), env_exports));
|
|
5089
|
+
const { config } = action.optsWithGlobals();
|
|
5090
|
+
if (config !== void 0) loadEnv2((0, import_node_path4.dirname)((0, import_node_path4.resolve)(config)));
|
|
5091
|
+
loadEnv2(process.cwd());
|
|
5092
|
+
});
|
|
4836
5093
|
const runCommand = program.command("run").description("Run assertions against a dataset").option("--dry-run", "Estimate cost/tokens; do not call the model", false).option("--config <path>", "Path to plune.yaml config file").option(
|
|
4837
5094
|
"--only <selector>",
|
|
4838
5095
|
"Run a subset by eval id or tag (repeatable)",
|
|
@@ -4843,21 +5100,18 @@ function createProgram() {
|
|
|
4843
5100
|
const globals = command.optsWithGlobals();
|
|
4844
5101
|
const configPath = globals.config;
|
|
4845
5102
|
const [
|
|
4846
|
-
{ loadEnv: loadEnv2 },
|
|
4847
5103
|
{ handleRun: handleRun2 },
|
|
4848
5104
|
{ exitCodeFor: exitCodeFor2, RunConfigError: RunConfigError2 },
|
|
4849
5105
|
{ renderReport: renderReport2 },
|
|
4850
5106
|
{ ConfigNotFoundError: ConfigNotFoundError2, ConfigValidationError: ConfigValidationError2, YamlParseError: YamlParseError2 },
|
|
4851
5107
|
{ AuthError: AuthError2 }
|
|
4852
5108
|
] = await Promise.all([
|
|
4853
|
-
Promise.resolve().then(() => (init_env(), env_exports)),
|
|
4854
5109
|
Promise.resolve().then(() => (init_run2(), run_exports)),
|
|
4855
5110
|
Promise.resolve().then(() => (init_orchestrator(), orchestrator_exports)),
|
|
4856
5111
|
Promise.resolve().then(() => (init_reporters(), reporters_exports)),
|
|
4857
5112
|
Promise.resolve().then(() => (init_errors(), errors_exports)),
|
|
4858
5113
|
Promise.resolve().then(() => (init_errors2(), errors_exports2))
|
|
4859
5114
|
]);
|
|
4860
|
-
loadEnv2(configPath !== void 0 ? (0, import_node_path3.dirname)((0, import_node_path3.resolve)(configPath)) : process.cwd());
|
|
4861
5115
|
try {
|
|
4862
5116
|
const result = await handleRun2({
|
|
4863
5117
|
dryRun: options.dryRun,
|
|
@@ -5017,13 +5271,11 @@ function createProgram() {
|
|
|
5017
5271
|
);
|
|
5018
5272
|
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) => {
|
|
5019
5273
|
const globals = command.optsWithGlobals();
|
|
5020
|
-
const [{
|
|
5021
|
-
Promise.resolve().then(() => (init_env(), env_exports)),
|
|
5274
|
+
const [{ handleReport: handleReport2, ReportNotFoundError: ReportNotFoundError2 }, { renderReport: renderReport2 }] = await Promise.all([
|
|
5022
5275
|
Promise.resolve().then(() => (init_report(), report_exports)),
|
|
5023
5276
|
Promise.resolve().then(() => (init_reporters(), reporters_exports))
|
|
5024
5277
|
]);
|
|
5025
|
-
const cwd = globals.config !== void 0 ? (0,
|
|
5026
|
-
loadEnv2(cwd);
|
|
5278
|
+
const cwd = globals.config !== void 0 ? (0, import_node_path4.dirname)((0, import_node_path4.resolve)(globals.config)) : process.cwd();
|
|
5027
5279
|
try {
|
|
5028
5280
|
const result = handleReport2({ cwd });
|
|
5029
5281
|
const text2 = renderReport2(result, toFormat(options.format), {
|
|
@@ -5087,10 +5339,10 @@ function createProgram() {
|
|
|
5087
5339
|
const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
5088
5340
|
try {
|
|
5089
5341
|
const token = options.token ?? await readTokenFromStdin();
|
|
5090
|
-
const { path:
|
|
5342
|
+
const { path: path15, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
|
|
5091
5343
|
process.stdout.write(
|
|
5092
|
-
verified ? `Logged in. Token checked against the API and saved to ${
|
|
5093
|
-
` : `Token saved to ${
|
|
5344
|
+
verified ? `Logged in. Token checked against the API and saved to ${path15}
|
|
5345
|
+
` : `Token saved to ${path15} WITHOUT being checked (--skip-verify).
|
|
5094
5346
|
`
|
|
5095
5347
|
);
|
|
5096
5348
|
} catch (err) {
|
|
@@ -5107,9 +5359,9 @@ function createProgram() {
|
|
|
5107
5359
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
5108
5360
|
const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
5109
5361
|
try {
|
|
5110
|
-
const { removed, path:
|
|
5362
|
+
const { removed, path: path15 } = handleLogout2();
|
|
5111
5363
|
process.stdout.write(
|
|
5112
|
-
removed ? `Logged out. Removed ${
|
|
5364
|
+
removed ? `Logged out. Removed ${path15}
|
|
5113
5365
|
` : "Not logged in \u2014 nothing to remove.\n"
|
|
5114
5366
|
);
|
|
5115
5367
|
} catch (err) {
|
|
@@ -5120,7 +5372,7 @@ function createProgram() {
|
|
|
5120
5372
|
const globals = command.optsWithGlobals();
|
|
5121
5373
|
const verbose = globals.verbose === true;
|
|
5122
5374
|
const { handleSync: handleSync2, reportSyncFailure: reportSyncFailure2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
5123
|
-
const cwd = globals.config !== void 0 ? (0,
|
|
5375
|
+
const cwd = globals.config !== void 0 ? (0, import_node_path4.dirname)((0, import_node_path4.resolve)(globals.config)) : process.cwd();
|
|
5124
5376
|
try {
|
|
5125
5377
|
const { id, url } = await handleSync2({
|
|
5126
5378
|
cwd,
|
|
@@ -5238,7 +5490,7 @@ function isMainModule() {
|
|
|
5238
5490
|
const entry = process.argv[1];
|
|
5239
5491
|
if (entry === void 0) return false;
|
|
5240
5492
|
try {
|
|
5241
|
-
return (0,
|
|
5493
|
+
return (0, import_node_fs5.realpathSync)(entry) === (0, import_node_fs5.realpathSync)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
5242
5494
|
} catch {
|
|
5243
5495
|
return false;
|
|
5244
5496
|
}
|