@plune-ai/cli 0.15.0 → 0.17.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 +467 -257
- package/dist/index.cjs +34 -3
- package/dist/index.js +34 -3
- package/dist/reporter-core.cjs +270 -169
- package/dist/reporter-core.d.cts +68 -7
- package/dist/reporter-core.d.ts +68 -7
- package/dist/reporter-core.js +267 -167
- 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,7 +2289,7 @@ 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
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
|
|
@@ -2282,9 +2321,9 @@ function createClient(opts) {
|
|
|
2282
2321
|
return { ok: false, ...last };
|
|
2283
2322
|
}
|
|
2284
2323
|
return {
|
|
2285
|
-
post: (
|
|
2286
|
-
upload: (
|
|
2287
|
-
del: (
|
|
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)
|
|
2288
2327
|
};
|
|
2289
2328
|
}
|
|
2290
2329
|
var import_node_http, import_node_https, RETRY_DELAYS_MS, IDLE_MS, sleep;
|
|
@@ -2360,13 +2399,35 @@ function defaultRunTitle(env = process.env, now = /* @__PURE__ */ new Date(), cw
|
|
|
2360
2399
|
const where = flag(env, "CI") || flag(env, "GITHUB_ACTIONS") ? "ci" : "local";
|
|
2361
2400
|
return `${path6.basename(cwd)} \xB7 ${stamp} \xB7 ${where}`;
|
|
2362
2401
|
}
|
|
2363
|
-
|
|
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;
|
|
2364
2420
|
var init_env2 = __esm({
|
|
2365
2421
|
"src/reporter-core/env.ts"() {
|
|
2366
2422
|
"use strict";
|
|
2367
2423
|
init_cjs_shims();
|
|
2368
2424
|
path6 = __toESM(require("path"), 1);
|
|
2369
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
|
+
];
|
|
2370
2431
|
}
|
|
2371
2432
|
});
|
|
2372
2433
|
|
|
@@ -2377,6 +2438,8 @@ function appendBatch(file, batch) {
|
|
|
2377
2438
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2378
2439
|
runId: batch.runId,
|
|
2379
2440
|
externalKey: batch.externalKey,
|
|
2441
|
+
...batch.session !== void 0 ? { session: batch.session } : {},
|
|
2442
|
+
...batch.meta !== void 0 ? { meta: batch.meta } : {},
|
|
2380
2443
|
results: batch.results
|
|
2381
2444
|
};
|
|
2382
2445
|
fs6.appendFileSync(file, `${JSON.stringify(line)}
|
|
@@ -2393,11 +2456,149 @@ var init_fallback = __esm({
|
|
|
2393
2456
|
}
|
|
2394
2457
|
});
|
|
2395
2458
|
|
|
2459
|
+
// src/reporter-core/failure-detail.ts
|
|
2460
|
+
function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
2461
|
+
const error = chosen(attempt);
|
|
2462
|
+
const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
|
|
2463
|
+
const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
|
|
2464
|
+
const steps = chainOf(attempt.steps);
|
|
2465
|
+
const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
|
|
2466
|
+
const artifacts = attempt.attachments.filter((a) => a.path !== void 0 && !a.name.startsWith("_") && a.contentType !== METADATA_TYPE).map(({ name, contentType }) => ({ name: fileNameOf(name), ...contentType ? { contentType } : {} })).filter((a) => a.name !== "");
|
|
2467
|
+
const ciUrl = webLink(attempt.buildHref);
|
|
2468
|
+
const failure = {
|
|
2469
|
+
...headline !== void 0 ? { headline } : {},
|
|
2470
|
+
...steps.length > 0 ? { steps } : {},
|
|
2471
|
+
...location !== void 0 ? { location } : {},
|
|
2472
|
+
...artifacts.length > 0 ? { artifacts } : {},
|
|
2473
|
+
...ciUrl !== void 0 ? { ciUrl } : {}
|
|
2474
|
+
};
|
|
2475
|
+
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
2476
|
+
}
|
|
2477
|
+
function repoRootOf(dir, stopAt) {
|
|
2478
|
+
for (let here = (0, import_node_path2.resolve)(dir); ; here = (0, import_node_path2.dirname)(here)) {
|
|
2479
|
+
if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(here, ".git"))) return here;
|
|
2480
|
+
if (here === stopAt || (0, import_node_path2.dirname)(here) === here) return void 0;
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
2484
|
+
const text2 = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
2485
|
+
return cut(machineless(text2, attempt.repoRoot, home));
|
|
2486
|
+
}
|
|
2487
|
+
function filesOf(attempt) {
|
|
2488
|
+
return attempt.attachments.filter((a) => a.path !== void 0 || a.body !== void 0 && isTextType(a.contentType));
|
|
2489
|
+
}
|
|
2490
|
+
function machineless(raw, repoRoot, home) {
|
|
2491
|
+
let text2 = (0, import_node_util.stripVTControlCharacters)(raw).replaceAll("\x1B", "");
|
|
2492
|
+
const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
|
|
2493
|
+
if (root !== void 0) text2 = text2.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
|
|
2494
|
+
const own = pathIn(home);
|
|
2495
|
+
if (own !== void 0) text2 = text2.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
|
|
2496
|
+
if (repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
|
|
2497
|
+
return text2;
|
|
2498
|
+
}
|
|
2499
|
+
function pathIn(path15) {
|
|
2500
|
+
const parts = path15.split(/[\\/]+/);
|
|
2501
|
+
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
2502
|
+
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
2503
|
+
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
2504
|
+
const escaped = /^\/(?!\/)/.test(path15) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
2505
|
+
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
2506
|
+
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
2507
|
+
}
|
|
2508
|
+
function cut(text2) {
|
|
2509
|
+
if (jsonBytes(text2) <= TEXT_LIMIT) return text2;
|
|
2510
|
+
const lines = text2.split("\n");
|
|
2511
|
+
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
2512
|
+
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
2513
|
+
let used = 0;
|
|
2514
|
+
let head = 0;
|
|
2515
|
+
let tail = lines.length;
|
|
2516
|
+
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
2517
|
+
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
2518
|
+
return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
|
|
2519
|
+
}
|
|
2520
|
+
function chosen({ status, errors }) {
|
|
2521
|
+
if (status === "timedOut") {
|
|
2522
|
+
const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
|
|
2523
|
+
if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
|
|
2524
|
+
}
|
|
2525
|
+
return errors[0];
|
|
2526
|
+
}
|
|
2527
|
+
function chainOf(steps) {
|
|
2528
|
+
const chain = [];
|
|
2529
|
+
for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
|
|
2530
|
+
chain.push(failed.title);
|
|
2531
|
+
}
|
|
2532
|
+
return chain;
|
|
2533
|
+
}
|
|
2534
|
+
function placeOf(error, testFile) {
|
|
2535
|
+
const own = normal(testFile);
|
|
2536
|
+
let first;
|
|
2537
|
+
for (const line of (0, import_node_util.stripVTControlCharacters)(error.text).split("\n")) {
|
|
2538
|
+
if (line.length > FRAME_MAX) continue;
|
|
2539
|
+
const frame = FRAME.exec(line);
|
|
2540
|
+
if (frame === null) continue;
|
|
2541
|
+
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
2542
|
+
if (normal(place.file) === own) return place;
|
|
2543
|
+
if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
|
|
2544
|
+
}
|
|
2545
|
+
return error.location ?? first;
|
|
2546
|
+
}
|
|
2547
|
+
function fileOf(raw) {
|
|
2548
|
+
if (!raw.startsWith("file://")) return raw;
|
|
2549
|
+
let path15 = raw.slice("file://".length);
|
|
2550
|
+
try {
|
|
2551
|
+
path15 = decodeURIComponent(path15);
|
|
2552
|
+
} catch {
|
|
2553
|
+
}
|
|
2554
|
+
return /^\/[A-Za-z]:\//.test(path15) ? path15.slice(1) : path15;
|
|
2555
|
+
}
|
|
2556
|
+
function fromRoot(place, root) {
|
|
2557
|
+
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
2558
|
+
const base = normal(root).replace(/\/+$/, "");
|
|
2559
|
+
const file = normal(place.file);
|
|
2560
|
+
if (!file.startsWith(`${base}/`)) return void 0;
|
|
2561
|
+
const relative2 = file.slice(base.length + 1);
|
|
2562
|
+
if (!REPO_RELATIVE.test(relative2)) return void 0;
|
|
2563
|
+
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
2564
|
+
return column === void 0 ? { file: relative2, line: place.line } : { file: relative2, line: place.line, column };
|
|
2565
|
+
}
|
|
2566
|
+
function webLink(href) {
|
|
2567
|
+
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
2568
|
+
}
|
|
2569
|
+
var import_node_fs2, import_node_os2, import_node_path2, import_node_util, mediaTypeOf, TEXT_TYPES, isTextType, PATH_START, TEXT_LIMIT, HEADLINE_LIMIT, jsonBytes, ANY_HOME, fileNameOf, omitted, METADATA_TYPE, REPO_RELATIVE, FRAME, FRAME_MAX, firstLine, normal;
|
|
2570
|
+
var init_failure_detail = __esm({
|
|
2571
|
+
"src/reporter-core/failure-detail.ts"() {
|
|
2572
|
+
"use strict";
|
|
2573
|
+
init_cjs_shims();
|
|
2574
|
+
import_node_fs2 = require("fs");
|
|
2575
|
+
import_node_os2 = require("os");
|
|
2576
|
+
import_node_path2 = require("path");
|
|
2577
|
+
import_node_util = require("util");
|
|
2578
|
+
mediaTypeOf = (contentType) => contentType?.split(";")[0]?.trim().toLowerCase() || void 0;
|
|
2579
|
+
TEXT_TYPES = /* @__PURE__ */ new Set(["application/json", "text/plain"]);
|
|
2580
|
+
isTextType = (contentType) => TEXT_TYPES.has(mediaTypeOf(contentType) ?? "");
|
|
2581
|
+
PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
2582
|
+
TEXT_LIMIT = 524288;
|
|
2583
|
+
HEADLINE_LIMIT = 8 * 1024;
|
|
2584
|
+
jsonBytes = (text2) => Buffer.byteLength(JSON.stringify(text2)) - 2;
|
|
2585
|
+
ANY_HOME = /(?<=^|[\s'"`(=,[])(?:(?:file|webpack|webpack-internal):(?:\\*\/){2,3})?(?:\\*\/home\\*\/|\\*\/Users\\*\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
|
|
2586
|
+
fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
2587
|
+
omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
2588
|
+
METADATA_TYPE = "application/plune.metadata+json";
|
|
2589
|
+
REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
2590
|
+
FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
2591
|
+
FRAME_MAX = 8192;
|
|
2592
|
+
firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
2593
|
+
normal = (path15) => import_node_path2.posix.normalize(path15.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
2594
|
+
}
|
|
2595
|
+
});
|
|
2596
|
+
|
|
2396
2597
|
// src/reporter-core/session.ts
|
|
2397
2598
|
function nameOf(file) {
|
|
2398
|
-
const own = file.name.split(/[\\/]/).pop()?.trim() || (0,
|
|
2399
|
-
const fileExt = (0,
|
|
2400
|
-
const ownExt = (0,
|
|
2599
|
+
const own = file.name.split(/[\\/]/).pop()?.trim() || (file.path === void 0 ? "" : (0, import_node_path3.basename)(file.path));
|
|
2600
|
+
const fileExt = file.path === void 0 ? "" : (0, import_node_path3.extname)(file.path);
|
|
2601
|
+
const ownExt = (0, import_node_path3.extname)(own);
|
|
2401
2602
|
const typed = ownExt !== "" && (ownExt.toLowerCase() === fileExt.toLowerCase() || TYPE_BY_EXTENSION[ownExt.toLowerCase()] !== void 0);
|
|
2402
2603
|
const ext = typed ? ownExt : fileExt;
|
|
2403
2604
|
let stem = typed ? own.slice(0, -ownExt.length) : own;
|
|
@@ -2407,6 +2608,35 @@ function nameOf(file) {
|
|
|
2407
2608
|
}
|
|
2408
2609
|
return `${stem}${ext}`;
|
|
2409
2610
|
}
|
|
2611
|
+
function kindOf(type) {
|
|
2612
|
+
if (type.startsWith("image/")) return "screenshot";
|
|
2613
|
+
return TEXT_TYPES.has(type) ? "text" : void 0;
|
|
2614
|
+
}
|
|
2615
|
+
function textTypeOf(contentType, type) {
|
|
2616
|
+
const charset = /;\s*charset\s*=\s*"?([^\s";]+)"?/i.exec(contentType)?.[1];
|
|
2617
|
+
return charset?.toLowerCase() === "utf-8" ? `${type}; charset=utf-8` : type;
|
|
2618
|
+
}
|
|
2619
|
+
function textRefusal(bytes) {
|
|
2620
|
+
if (bytes.length > TEXT_BYTES) return "textSize";
|
|
2621
|
+
if (bytes.length === 0) return "empty";
|
|
2622
|
+
if (bytes.includes(0)) return "binary";
|
|
2623
|
+
try {
|
|
2624
|
+
UTF8.decode(bytes);
|
|
2625
|
+
} catch {
|
|
2626
|
+
return "encoding";
|
|
2627
|
+
}
|
|
2628
|
+
return void 0;
|
|
2629
|
+
}
|
|
2630
|
+
function onDisk(result) {
|
|
2631
|
+
if (!("files" in result) || result.files === void 0) return result;
|
|
2632
|
+
const { files, ...rest } = result;
|
|
2633
|
+
const kept = files.filter((f) => f.path !== void 0).map(({ body: _body, ...file }) => file);
|
|
2634
|
+
return kept.length === 0 ? rest : { ...rest, files: kept };
|
|
2635
|
+
}
|
|
2636
|
+
function tally(counts, noun) {
|
|
2637
|
+
if (counts.uploaded + counts.skipped + counts.failed === 0) return void 0;
|
|
2638
|
+
return `${counts.uploaded} ${noun}(s) uploaded` + (counts.skipped > 0 ? `, ${counts.skipped} skipped` : "") + (counts.failed > 0 ? `, ${counts.failed} failed to upload` : "");
|
|
2639
|
+
}
|
|
2410
2640
|
function boundKey(key) {
|
|
2411
2641
|
if (key.value.length <= KEY_MAX) return key;
|
|
2412
2642
|
const digest = (0, import_node_crypto2.createHash)("sha256").update(key.value).digest("hex").slice(0, 16);
|
|
@@ -2414,6 +2644,12 @@ function boundKey(key) {
|
|
|
2414
2644
|
if (/[\uD800-\uDBFF]$/.test(head)) head = head.slice(0, -1);
|
|
2415
2645
|
return { ...key, value: `${head}#${digest}` };
|
|
2416
2646
|
}
|
|
2647
|
+
function whyUnoffered(out) {
|
|
2648
|
+
if (out.ok || out.status !== 429) return "other";
|
|
2649
|
+
if (/review queue quota reached/i.test(out.detail)) return "queue";
|
|
2650
|
+
if (/test case quota reached/i.test(out.detail)) return "cases";
|
|
2651
|
+
return "other";
|
|
2652
|
+
}
|
|
2417
2653
|
function boundTitle(title) {
|
|
2418
2654
|
return title.length <= TITLE_MAX ? title : `${title.slice(0, TITLE_MAX - 1)}\u2026`;
|
|
2419
2655
|
}
|
|
@@ -2455,6 +2691,7 @@ async function startRun(passed, declared) {
|
|
|
2455
2691
|
`));
|
|
2456
2692
|
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
2457
2693
|
const externalKey = cfg.externalKey ?? null;
|
|
2694
|
+
const sessionMarker = (0, import_node_crypto2.randomUUID)();
|
|
2458
2695
|
const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
|
|
2459
2696
|
const offerDiscovered = cfg.offerDiscovered ?? false;
|
|
2460
2697
|
const token = cfg.token ?? loadToken() ?? "";
|
|
@@ -2473,7 +2710,8 @@ async function startRun(passed, declared) {
|
|
|
2473
2710
|
created: 0,
|
|
2474
2711
|
unoffered: 0,
|
|
2475
2712
|
deferred: 0,
|
|
2476
|
-
screenshots: { uploaded: 0, skipped: 0, failed: 0 }
|
|
2713
|
+
screenshots: { uploaded: 0, skipped: 0, failed: 0 },
|
|
2714
|
+
textFiles: { uploaded: 0, skipped: 0, failed: 0 }
|
|
2477
2715
|
};
|
|
2478
2716
|
const resolved = /* @__PURE__ */ new Map();
|
|
2479
2717
|
let buffer = [];
|
|
@@ -2484,6 +2722,7 @@ async function startRun(passed, declared) {
|
|
|
2484
2722
|
let done = false;
|
|
2485
2723
|
const shortened = /* @__PURE__ */ new Set();
|
|
2486
2724
|
const skipsSaid = /* @__PURE__ */ new Set();
|
|
2725
|
+
const countsOf = (kind) => kind === "text" ? stats.textFiles : stats.screenshots;
|
|
2487
2726
|
function bound(keys) {
|
|
2488
2727
|
return keys.map((key) => {
|
|
2489
2728
|
const fit = boundKey(key);
|
|
@@ -2506,7 +2745,13 @@ async function startRun(passed, declared) {
|
|
|
2506
2745
|
log(`plune: ignoring ${env.ignored.join(", ")} \u2014 a run has nowhere to store them yet.`);
|
|
2507
2746
|
}
|
|
2508
2747
|
function defer(results) {
|
|
2509
|
-
appendBatch(fallbackPath, {
|
|
2748
|
+
appendBatch(fallbackPath, {
|
|
2749
|
+
runId,
|
|
2750
|
+
externalKey,
|
|
2751
|
+
session: sessionMarker,
|
|
2752
|
+
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
2753
|
+
results: results.map(onDisk)
|
|
2754
|
+
});
|
|
2510
2755
|
stats.deferred += results.length;
|
|
2511
2756
|
}
|
|
2512
2757
|
async function resolveKeys(values) {
|
|
@@ -2603,6 +2848,7 @@ async function startRun(passed, declared) {
|
|
|
2603
2848
|
});
|
|
2604
2849
|
if (!out.ok || !Array.isArray(out.body?.results)) {
|
|
2605
2850
|
stats.unoffered = batches.slice(index).reduce((n, rest) => n + rest.length, 0);
|
|
2851
|
+
stats.unofferedWhy = whyUnoffered(out);
|
|
2606
2852
|
log(
|
|
2607
2853
|
`plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.ok ? "the answer had no results" : out.detail || out.kind}).`
|
|
2608
2854
|
);
|
|
@@ -2612,57 +2858,74 @@ async function startRun(passed, declared) {
|
|
|
2612
2858
|
stats.created += out.body.results.filter((r) => r?.outcome === "created").length;
|
|
2613
2859
|
}
|
|
2614
2860
|
}
|
|
2615
|
-
function skip(name, why) {
|
|
2616
|
-
|
|
2617
|
-
if (skipsSaid.has(why)) return;
|
|
2618
|
-
skipsSaid.add(why);
|
|
2619
|
-
log(`plune:
|
|
2861
|
+
function skip(kind, name, why) {
|
|
2862
|
+
countsOf(kind).skipped += 1;
|
|
2863
|
+
if (skipsSaid.has(`${kind}/${why}`)) return;
|
|
2864
|
+
skipsSaid.add(`${kind}/${why}`);
|
|
2865
|
+
log(`plune: ${KINDS[kind].label} "${name}" not uploaded \u2014 ${SKIPPED[why]}.`);
|
|
2620
2866
|
}
|
|
2621
|
-
async function uploadOne(
|
|
2867
|
+
async function uploadOne(job) {
|
|
2868
|
+
const { kind } = job;
|
|
2869
|
+
const { label, bytes: ceiling, size } = KINDS[kind];
|
|
2622
2870
|
let bytes;
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2871
|
+
if (job.body !== void 0) bytes = job.body;
|
|
2872
|
+
else if (job.path === void 0) return skip(kind, job.name, "missing");
|
|
2873
|
+
else {
|
|
2874
|
+
try {
|
|
2875
|
+
if ((await import_node_fs3.promises.stat(job.path)).size > ceiling) return skip(kind, job.name, size);
|
|
2876
|
+
bytes = await import_node_fs3.promises.readFile(job.path);
|
|
2877
|
+
} catch {
|
|
2878
|
+
return skip(kind, job.name, "missing");
|
|
2879
|
+
}
|
|
2628
2880
|
}
|
|
2881
|
+
const refusal = kind === "text" ? textRefusal(bytes) : void 0;
|
|
2882
|
+
if (refusal !== void 0) return skip(kind, job.name, refusal);
|
|
2629
2883
|
const out = await client.upload(
|
|
2630
|
-
`/v1/results/${encodeURIComponent(
|
|
2884
|
+
`/v1/results/${encodeURIComponent(job.id)}/files?name=${encodeURIComponent(job.name)}`,
|
|
2631
2885
|
bytes,
|
|
2632
|
-
|
|
2886
|
+
job.type
|
|
2633
2887
|
);
|
|
2888
|
+
const counts = countsOf(kind);
|
|
2634
2889
|
if (out.ok) {
|
|
2635
|
-
|
|
2890
|
+
counts.uploaded += 1;
|
|
2636
2891
|
return;
|
|
2637
2892
|
}
|
|
2638
|
-
|
|
2639
|
-
if (
|
|
2640
|
-
log(`plune: could not upload a
|
|
2893
|
+
counts.failed += 1;
|
|
2894
|
+
if (counts.failed === 1) {
|
|
2895
|
+
log(`plune: could not upload a ${label} (${out.detail || out.kind}) \u2014 its result is reported without it.`);
|
|
2641
2896
|
}
|
|
2642
2897
|
}
|
|
2643
2898
|
async function upload(items, filesByKey) {
|
|
2644
2899
|
if (filesByKey.size === 0 || !Array.isArray(items)) return;
|
|
2645
|
-
const
|
|
2900
|
+
const jobs = [];
|
|
2646
2901
|
for (const item of items) {
|
|
2647
2902
|
if (item?.status !== "accepted" || typeof item.id !== "string" || typeof item.resultKey !== "string") continue;
|
|
2648
|
-
|
|
2903
|
+
const room = { screenshot: KINDS.screenshot.room, text: KINDS.text.room };
|
|
2649
2904
|
for (const file of filesByKey.get(item.resultKey) ?? []) {
|
|
2650
|
-
const type = file.contentType
|
|
2651
|
-
|
|
2905
|
+
const type = mediaTypeOf(file.contentType) ?? (file.path === void 0 ? void 0 : TYPE_BY_EXTENSION[(0, import_node_path3.extname)(file.path).toLowerCase()]);
|
|
2906
|
+
const kind = type === void 0 ? void 0 : kindOf(type);
|
|
2907
|
+
if (type === void 0 || kind === void 0 || kind === "screenshot" && file.path === void 0) continue;
|
|
2652
2908
|
const name = nameOf(file);
|
|
2653
|
-
if (file.name.startsWith("_")) skip(name, "hidden");
|
|
2654
|
-
else if (!SCREENSHOT_TYPES.has(type)) skip(name, "type");
|
|
2655
|
-
else if (room === 0) skip(name,
|
|
2909
|
+
if (file.name.startsWith("_")) skip(kind, name, "hidden");
|
|
2910
|
+
else if (kind === "screenshot" && !SCREENSHOT_TYPES.has(type)) skip(kind, name, "type");
|
|
2911
|
+
else if (room[kind] === 0) skip(kind, name, KINDS[kind].limit);
|
|
2656
2912
|
else {
|
|
2657
|
-
room -= 1;
|
|
2658
|
-
|
|
2913
|
+
room[kind] -= 1;
|
|
2914
|
+
jobs.push({
|
|
2915
|
+
id: item.id,
|
|
2916
|
+
kind,
|
|
2917
|
+
name,
|
|
2918
|
+
type: kind === "text" ? textTypeOf(file.contentType ?? "", type) : type,
|
|
2919
|
+
...file.body !== void 0 ? { body: file.body } : {},
|
|
2920
|
+
...file.path !== void 0 ? { path: file.path } : {}
|
|
2921
|
+
});
|
|
2659
2922
|
}
|
|
2660
2923
|
}
|
|
2661
2924
|
}
|
|
2662
2925
|
let next = 0;
|
|
2663
2926
|
await Promise.all(
|
|
2664
|
-
Array.from({ length: Math.min(UPLOADS_AT_ONCE,
|
|
2665
|
-
while (next <
|
|
2927
|
+
Array.from({ length: Math.min(UPLOADS_AT_ONCE, jobs.length) }, async () => {
|
|
2928
|
+
while (next < jobs.length) await uploadOne(jobs[next++]);
|
|
2666
2929
|
})
|
|
2667
2930
|
);
|
|
2668
2931
|
}
|
|
@@ -2740,11 +3003,8 @@ async function startRun(passed, declared) {
|
|
|
2740
3003
|
if (stats.unresolved > 0) parts.push(`${stats.unresolved} with no matching test case`);
|
|
2741
3004
|
if (stats.offered > 0) parts.push(`${stats.offered} offered for review`);
|
|
2742
3005
|
if (stats.created > 0) parts.push(`${stats.created} added as cases (trusted source)`);
|
|
2743
|
-
const
|
|
2744
|
-
|
|
2745
|
-
parts.push(
|
|
2746
|
-
`${shots.uploaded} screenshot(s) uploaded` + (shots.skipped > 0 ? `, ${shots.skipped} skipped` : "") + (shots.failed > 0 ? `, ${shots.failed} failed to upload` : "")
|
|
2747
|
-
);
|
|
3006
|
+
for (const said of [tally(stats.screenshots, "screenshot"), tally(stats.textFiles, "text file")]) {
|
|
3007
|
+
if (said !== void 0) parts.push(said);
|
|
2748
3008
|
}
|
|
2749
3009
|
if (stats.deferred > 0) parts.push(`${stats.deferred} not delivered \u2014 written to ${fallbackPath}`);
|
|
2750
3010
|
if (shortened.size > 0) parts.push(`${shortened.size} test name(s) shortened to fit a key`);
|
|
@@ -2797,18 +3057,20 @@ async function startRun(passed, declared) {
|
|
|
2797
3057
|
}
|
|
2798
3058
|
};
|
|
2799
3059
|
}
|
|
2800
|
-
var import_node_crypto2,
|
|
3060
|
+
var import_node_crypto2, import_node_fs3, 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, UPLOADS_AT_ONCE, NAME_MAX, SKIPPED, KINDS, BATCH_BYTES, ENVELOPE_BYTES;
|
|
2801
3061
|
var init_session = __esm({
|
|
2802
3062
|
"src/reporter-core/session.ts"() {
|
|
2803
3063
|
"use strict";
|
|
2804
3064
|
init_cjs_shims();
|
|
2805
3065
|
import_node_crypto2 = require("crypto");
|
|
2806
|
-
|
|
2807
|
-
|
|
3066
|
+
import_node_fs3 = require("fs");
|
|
3067
|
+
import_node_path3 = require("path");
|
|
3068
|
+
import_node_util2 = require("util");
|
|
2808
3069
|
init_api_url();
|
|
2809
3070
|
init_credentials();
|
|
2810
3071
|
init_client();
|
|
2811
3072
|
init_env2();
|
|
3073
|
+
init_failure_detail();
|
|
2812
3074
|
init_fallback();
|
|
2813
3075
|
RESOLVE_CHUNK = 500;
|
|
2814
3076
|
BATCH_MAX = 500;
|
|
@@ -2821,6 +3083,9 @@ var init_session = __esm({
|
|
|
2821
3083
|
SCREENSHOT_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/webp"]);
|
|
2822
3084
|
SCREENSHOT_BYTES = 2 * 1024 * 1024;
|
|
2823
3085
|
SCREENSHOTS_PER_RESULT = 20;
|
|
3086
|
+
TEXT_BYTES = 512 * 1024;
|
|
3087
|
+
TEXTS_PER_RESULT = 10;
|
|
3088
|
+
UTF8 = new import_node_util2.TextDecoder("utf-8", { fatal: true });
|
|
2824
3089
|
TYPE_BY_EXTENSION = {
|
|
2825
3090
|
".png": "image/png",
|
|
2826
3091
|
".jpg": "image/jpeg",
|
|
@@ -2834,7 +3099,16 @@ var init_session = __esm({
|
|
|
2834
3099
|
type: "Plune takes PNG, JPEG and WebP",
|
|
2835
3100
|
limit: `a result holds at most ${SCREENSHOTS_PER_RESULT}`,
|
|
2836
3101
|
missing: "the file cannot be read on this machine",
|
|
2837
|
-
size: "Plune takes a file of at most 2 MB"
|
|
3102
|
+
size: "Plune takes a file of at most 2 MB",
|
|
3103
|
+
textLimit: `a result holds at most ${TEXTS_PER_RESULT} text files`,
|
|
3104
|
+
textSize: "Plune takes a text file of at most 512 KiB",
|
|
3105
|
+
empty: "the file is empty",
|
|
3106
|
+
binary: "the file holds a NUL byte, and Plune takes UTF-8 text without one",
|
|
3107
|
+
encoding: "Plune takes UTF-8 text only"
|
|
3108
|
+
};
|
|
3109
|
+
KINDS = {
|
|
3110
|
+
screenshot: { label: "screenshot", bytes: SCREENSHOT_BYTES, room: SCREENSHOTS_PER_RESULT, limit: "limit", size: "size" },
|
|
3111
|
+
text: { label: "text file", bytes: TEXT_BYTES, room: TEXTS_PER_RESULT, limit: "textLimit", size: "textSize" }
|
|
2838
3112
|
};
|
|
2839
3113
|
BATCH_BYTES = 8 * 1024 * 1024;
|
|
2840
3114
|
ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ results: [] }));
|
|
@@ -2959,23 +3233,42 @@ async function handleRunReport(options = {}) {
|
|
|
2959
3233
|
if (lines.length === 0) throw new RunCommandError(`Nothing to send \u2014 ${file} is empty.`);
|
|
2960
3234
|
const { client } = connect(options);
|
|
2961
3235
|
const result = { batches: lines.length, sent: 0, failed: 0 };
|
|
3236
|
+
const groups = [];
|
|
3237
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
2962
3238
|
for (const line of lines) {
|
|
2963
|
-
const
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
3239
|
+
const group = line.session === void 0 ? void 0 : bySession.get(line.session);
|
|
3240
|
+
if (group !== void 0) {
|
|
3241
|
+
group.push(line);
|
|
3242
|
+
continue;
|
|
3243
|
+
}
|
|
3244
|
+
const alone = [line];
|
|
3245
|
+
groups.push(alone);
|
|
3246
|
+
if (line.session !== void 0) bySession.set(line.session, alone);
|
|
3247
|
+
}
|
|
3248
|
+
for (const group of groups) {
|
|
3249
|
+
for (const line of group) {
|
|
3250
|
+
const ready = line.results.filter((r) => !isPending(r));
|
|
3251
|
+
if (line.runId !== null && ready.length > 0) {
|
|
3252
|
+
const out = await client.post(`/v1/runs/${line.runId}/results`, { results: ready });
|
|
3253
|
+
if (out.ok) result.sent += ready.length;
|
|
3254
|
+
else {
|
|
3255
|
+
result.failed += ready.length;
|
|
3256
|
+
write(
|
|
3257
|
+
`Could not send ${ready.length} result(s) to run ${line.runId} \u2014 ${out.detail || out.kind}.`
|
|
3258
|
+
);
|
|
3259
|
+
}
|
|
2971
3260
|
}
|
|
2972
3261
|
}
|
|
3262
|
+
const pending = group.flatMap((line) => line.results.filter(isPending));
|
|
2973
3263
|
if (pending.length > 0) {
|
|
3264
|
+
const first = group[0];
|
|
2974
3265
|
const session = await startRun({
|
|
2975
3266
|
...options.apiUrl !== void 0 ? { apiUrl: options.apiUrl } : {},
|
|
2976
3267
|
...options.token !== void 0 ? { token: options.token } : {},
|
|
2977
3268
|
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {},
|
|
2978
|
-
...
|
|
3269
|
+
...first.externalKey !== null ? { externalKey: first.externalKey } : {},
|
|
3270
|
+
// Where the run came from, as the reporter recorded it (plune#927) — never this machine's own.
|
|
3271
|
+
...first.meta !== void 0 ? { meta: first.meta } : {},
|
|
2979
3272
|
// The replay does not know whether the run is complete, so it must not say that it is.
|
|
2980
3273
|
log: write
|
|
2981
3274
|
});
|
|
@@ -2986,15 +3279,34 @@ async function handleRunReport(options = {}) {
|
|
|
2986
3279
|
}
|
|
2987
3280
|
}
|
|
2988
3281
|
write(`Replayed ${result.batches} batch(es) from ${file}: ${result.sent} sent, ${result.failed} not.`);
|
|
3282
|
+
if (result.failed === 0) {
|
|
3283
|
+
const sentAs = sentName(file, (options.now ?? Date.now)());
|
|
3284
|
+
try {
|
|
3285
|
+
fs7.renameSync(file, sentAs);
|
|
3286
|
+
write(
|
|
3287
|
+
`Every result was sent \u2014 ${file} is now ${sentAs}, so a later replay does not send them twice.`
|
|
3288
|
+
);
|
|
3289
|
+
} catch (err) {
|
|
3290
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
3291
|
+
write(
|
|
3292
|
+
`Every result was sent, but ${file} could not be renamed (${why}) \u2014 take it away by hand, or a later replay sends them again.`
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
2989
3296
|
return result;
|
|
2990
3297
|
}
|
|
2991
|
-
|
|
3298
|
+
function sentName(file, at) {
|
|
3299
|
+
const { dir, name, ext } = path8.parse(file);
|
|
3300
|
+
return path8.join(dir, `${name}.${new Date(at).toISOString().replace(/[:.]/g, "-")}.sent${ext}`);
|
|
3301
|
+
}
|
|
3302
|
+
var import_node_child_process, fs7, path8, NoTokenError, RunCommandError, isPending;
|
|
2992
3303
|
var init_run_lifecycle = __esm({
|
|
2993
3304
|
"src/cli/commands/run-lifecycle.ts"() {
|
|
2994
3305
|
"use strict";
|
|
2995
3306
|
init_cjs_shims();
|
|
2996
3307
|
import_node_child_process = require("child_process");
|
|
2997
3308
|
fs7 = __toESM(require("fs"), 1);
|
|
3309
|
+
path8 = __toESM(require("path"), 1);
|
|
2998
3310
|
init_api_url();
|
|
2999
3311
|
init_credentials();
|
|
3000
3312
|
init_client();
|
|
@@ -3202,7 +3514,12 @@ function errorTextOf(detail) {
|
|
|
3202
3514
|
const head = [detail.attrs["message"], detail.attrs["type"]].filter((s) => s !== void 0 && s !== "");
|
|
3203
3515
|
return [...head, detail.text.trim()].filter((s) => s !== "").join("\n\n");
|
|
3204
3516
|
}
|
|
3205
|
-
function
|
|
3517
|
+
function playwrightOf(classname, name) {
|
|
3518
|
+
if (!SPEC_FILE.test(classname)) return void 0;
|
|
3519
|
+
return { file: classname.replaceAll("\\", "/"), titles: name.split(" \u203A ") };
|
|
3520
|
+
}
|
|
3521
|
+
function keyOf(classname, name, playwright) {
|
|
3522
|
+
if (playwright !== void 0) return { kind: "path-title", value: [playwright.file, ...playwright.titles].join("#") };
|
|
3206
3523
|
return { kind: "path-title", value: classname === "" ? name : `${classname}#${name}` };
|
|
3207
3524
|
}
|
|
3208
3525
|
function titleOf(classname, name) {
|
|
@@ -3242,12 +3559,15 @@ function readJUnit(source, file) {
|
|
|
3242
3559
|
const name = test.attrs["name"] ?? "";
|
|
3243
3560
|
const classname = test.attrs["classname"] ?? test.attrs["class"] ?? "";
|
|
3244
3561
|
const { status, detail } = outcomeOf(test);
|
|
3245
|
-
const
|
|
3562
|
+
const playwright = playwrightOf(classname, name);
|
|
3563
|
+
const key = keyOf(classname, name, playwright);
|
|
3564
|
+
const stated = TOKEN.exec(playwright?.titles.at(-1) ?? name)?.[1];
|
|
3246
3565
|
const specRef = specRefOf(test, suite, classname);
|
|
3247
3566
|
const errorContext = errorTextOf(detail);
|
|
3248
3567
|
const execution = executionOf(test, suite);
|
|
3249
3568
|
return {
|
|
3250
3569
|
resultKey: resultKey(key.value, 0),
|
|
3570
|
+
...stated !== void 0 ? { testCaseId: stated } : {},
|
|
3251
3571
|
keys: [key],
|
|
3252
3572
|
source: SOURCE,
|
|
3253
3573
|
rawStatus: status,
|
|
@@ -3261,7 +3581,7 @@ function readJUnit(source, file) {
|
|
|
3261
3581
|
function looksLikeJUnit(source) {
|
|
3262
3582
|
return /<\s*testsuites?\b/.test(source.slice(0, 4096));
|
|
3263
3583
|
}
|
|
3264
|
-
var SOURCE;
|
|
3584
|
+
var SOURCE, TOKEN, SPEC_FILE;
|
|
3265
3585
|
var init_junit = __esm({
|
|
3266
3586
|
"src/importers/junit.ts"() {
|
|
3267
3587
|
"use strict";
|
|
@@ -3269,141 +3589,8 @@ var init_junit = __esm({
|
|
|
3269
3589
|
init_result_key();
|
|
3270
3590
|
init_xml();
|
|
3271
3591
|
SOURCE = "junit";
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
// src/reporter-core/failure-detail.ts
|
|
3276
|
-
function failureOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
3277
|
-
const error = chosen(attempt);
|
|
3278
|
-
const line = error === void 0 ? void 0 : firstLine(machineless(error.text, attempt.repoRoot, home));
|
|
3279
|
-
const headline = line !== void 0 && jsonBytes(line) <= HEADLINE_LIMIT ? line : void 0;
|
|
3280
|
-
const steps = chainOf(attempt.steps);
|
|
3281
|
-
const location = error === void 0 ? void 0 : fromRoot(placeOf(error, attempt.testFile), attempt.repoRoot);
|
|
3282
|
-
const artifacts = attempt.attachments.filter((a) => a.path !== void 0 && !a.name.startsWith("_") && a.contentType !== METADATA_TYPE).map(({ name, contentType }) => ({ name: fileNameOf(name), ...contentType ? { contentType } : {} })).filter((a) => a.name !== "");
|
|
3283
|
-
const ciUrl = webLink(attempt.buildHref);
|
|
3284
|
-
const failure = {
|
|
3285
|
-
...headline !== void 0 ? { headline } : {},
|
|
3286
|
-
...steps.length > 0 ? { steps } : {},
|
|
3287
|
-
...location !== void 0 ? { location } : {},
|
|
3288
|
-
...artifacts.length > 0 ? { artifacts } : {},
|
|
3289
|
-
...ciUrl !== void 0 ? { ciUrl } : {}
|
|
3290
|
-
};
|
|
3291
|
-
return Object.keys(failure).length === 0 ? void 0 : failure;
|
|
3292
|
-
}
|
|
3293
|
-
function repoRootOf(dir, stopAt) {
|
|
3294
|
-
for (let here = (0, import_node_path3.resolve)(dir); ; here = (0, import_node_path3.dirname)(here)) {
|
|
3295
|
-
if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(here, ".git"))) return here;
|
|
3296
|
-
if (here === stopAt || (0, import_node_path3.dirname)(here) === here) return void 0;
|
|
3297
|
-
}
|
|
3298
|
-
}
|
|
3299
|
-
function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
3300
|
-
const text2 = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
3301
|
-
return cut(machineless(text2, attempt.repoRoot, home));
|
|
3302
|
-
}
|
|
3303
|
-
function filesOf(attempt) {
|
|
3304
|
-
return attempt.attachments.filter((a) => a.path !== void 0);
|
|
3305
|
-
}
|
|
3306
|
-
function machineless(raw, repoRoot, home) {
|
|
3307
|
-
let text2 = (0, import_node_util.stripVTControlCharacters)(raw).replaceAll("\x1B", "");
|
|
3308
|
-
const root = repoRoot === void 0 ? void 0 : pathIn(repoRoot);
|
|
3309
|
-
if (root !== void 0) text2 = text2.replace(new RegExp(`${PATH_START}${root.source}[\\\\/]+`, root.flags), "");
|
|
3310
|
-
const own = pathIn(home);
|
|
3311
|
-
if (own !== void 0) text2 = text2.replace(new RegExp(`${PATH_START}${own.source}(?![\\w.-])`, own.flags), "~");
|
|
3312
|
-
if (repoRoot === void 0) text2 = text2.replace(ANY_HOME, "~");
|
|
3313
|
-
return text2;
|
|
3314
|
-
}
|
|
3315
|
-
function pathIn(path14) {
|
|
3316
|
-
const parts = path14.split(/[\\/]+/);
|
|
3317
|
-
while (parts.length > 1 && parts.at(-1) === "") parts.pop();
|
|
3318
|
-
if (!parts.some((part) => part !== "" && !/^[A-Za-z]:$/.test(part))) return void 0;
|
|
3319
|
-
const joined = parts.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("[\\\\/]+");
|
|
3320
|
-
const escaped = /^\/(?!\/)/.test(path14) ? joined.replace("[\\\\/]+", "\\\\*\\/") : joined;
|
|
3321
|
-
const url = "(?:(?:file|webpack|webpack-internal):(?:\\\\*\\/){2,3})?";
|
|
3322
|
-
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
3323
|
-
}
|
|
3324
|
-
function cut(text2) {
|
|
3325
|
-
if (jsonBytes(text2) <= TEXT_LIMIT) return text2;
|
|
3326
|
-
const lines = text2.split("\n");
|
|
3327
|
-
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
3328
|
-
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
3329
|
-
let used = 0;
|
|
3330
|
-
let head = 0;
|
|
3331
|
-
let tail = lines.length;
|
|
3332
|
-
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
3333
|
-
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
3334
|
-
return [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n");
|
|
3335
|
-
}
|
|
3336
|
-
function chosen({ status, errors }) {
|
|
3337
|
-
if (status === "timedOut") {
|
|
3338
|
-
const timeout = errors.findIndex((e) => firstLine(e.text)?.startsWith("Test timeout of ") === true);
|
|
3339
|
-
if (timeout !== -1 && timeout + 1 < errors.length) return errors[timeout + 1];
|
|
3340
|
-
}
|
|
3341
|
-
return errors[0];
|
|
3342
|
-
}
|
|
3343
|
-
function chainOf(steps) {
|
|
3344
|
-
const chain = [];
|
|
3345
|
-
for (let failed = steps.find((s) => s.failed); failed !== void 0; failed = failed.steps.find((s) => s.failed)) {
|
|
3346
|
-
chain.push(failed.title);
|
|
3347
|
-
}
|
|
3348
|
-
return chain;
|
|
3349
|
-
}
|
|
3350
|
-
function placeOf(error, testFile) {
|
|
3351
|
-
const own = normal(testFile);
|
|
3352
|
-
let first;
|
|
3353
|
-
for (const line of (0, import_node_util.stripVTControlCharacters)(error.text).split("\n")) {
|
|
3354
|
-
if (line.length > FRAME_MAX) continue;
|
|
3355
|
-
const frame = FRAME.exec(line);
|
|
3356
|
-
if (frame === null) continue;
|
|
3357
|
-
const place = { file: fileOf(frame[1]), line: Number(frame[2]), column: Number(frame[3]) };
|
|
3358
|
-
if (normal(place.file) === own) return place;
|
|
3359
|
-
if (first === void 0 && !normal(place.file).includes("/node_modules/")) first = place;
|
|
3360
|
-
}
|
|
3361
|
-
return error.location ?? first;
|
|
3362
|
-
}
|
|
3363
|
-
function fileOf(raw) {
|
|
3364
|
-
if (!raw.startsWith("file://")) return raw;
|
|
3365
|
-
let path14 = raw.slice("file://".length);
|
|
3366
|
-
try {
|
|
3367
|
-
path14 = decodeURIComponent(path14);
|
|
3368
|
-
} catch {
|
|
3369
|
-
}
|
|
3370
|
-
return /^\/[A-Za-z]:\//.test(path14) ? path14.slice(1) : path14;
|
|
3371
|
-
}
|
|
3372
|
-
function fromRoot(place, root) {
|
|
3373
|
-
if (place === void 0 || root === void 0 || !Number.isInteger(place.line) || place.line < 1) return void 0;
|
|
3374
|
-
const base = normal(root).replace(/\/+$/, "");
|
|
3375
|
-
const file = normal(place.file);
|
|
3376
|
-
if (!file.startsWith(`${base}/`)) return void 0;
|
|
3377
|
-
const relative2 = file.slice(base.length + 1);
|
|
3378
|
-
if (!REPO_RELATIVE.test(relative2)) return void 0;
|
|
3379
|
-
const column = place.column !== void 0 && Number.isInteger(place.column) && place.column >= 1 ? place.column : void 0;
|
|
3380
|
-
return column === void 0 ? { file: relative2, line: place.line } : { file: relative2, line: place.line, column };
|
|
3381
|
-
}
|
|
3382
|
-
function webLink(href) {
|
|
3383
|
-
return href !== void 0 && /^https?:\/\//i.test(href) && URL.canParse(href) ? href : void 0;
|
|
3384
|
-
}
|
|
3385
|
-
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;
|
|
3386
|
-
var init_failure_detail = __esm({
|
|
3387
|
-
"src/reporter-core/failure-detail.ts"() {
|
|
3388
|
-
"use strict";
|
|
3389
|
-
init_cjs_shims();
|
|
3390
|
-
import_node_fs3 = require("fs");
|
|
3391
|
-
import_node_os2 = require("os");
|
|
3392
|
-
import_node_path3 = require("path");
|
|
3393
|
-
import_node_util = require("util");
|
|
3394
|
-
PATH_START = "(?<![\\w.~%/\\\\-])";
|
|
3395
|
-
TEXT_LIMIT = 524288;
|
|
3396
|
-
HEADLINE_LIMIT = 8 * 1024;
|
|
3397
|
-
jsonBytes = (text2) => Buffer.byteLength(JSON.stringify(text2)) - 2;
|
|
3398
|
-
ANY_HOME = /(?<=^|[\s'"`(=,[])(?:(?:file|webpack|webpack-internal):(?:\\*\/){2,3})?(?:\\*\/home\\*\/|\\*\/Users\\*\/|[A-Za-z]:[\\/]+Users[\\/]+)[^\\/\s'"`:*?<>|]+/g;
|
|
3399
|
-
fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
3400
|
-
omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
3401
|
-
METADATA_TYPE = "application/plune.metadata+json";
|
|
3402
|
-
REPO_RELATIVE = /^(?![A-Za-z][A-Za-z0-9+.-]*:)(?![\\/~])(?!(?:.*[\\/])?\.\.(?:[\\/]|$)).+$/;
|
|
3403
|
-
FRAME = /^\s+at (?:.*? \()?(.+?):(\d+):(\d+)\)?$/;
|
|
3404
|
-
FRAME_MAX = 8192;
|
|
3405
|
-
firstLine = (text2) => (0, import_node_util.stripVTControlCharacters)(text2).split("\n").map((line) => line.trim()).find((line) => line !== "");
|
|
3406
|
-
normal = (path14) => import_node_path3.posix.normalize(path14.replace(/\\/g, "/")).replace(/^([a-z]):/, (_, drive) => `${drive.toUpperCase()}:`);
|
|
3592
|
+
TOKEN = /@P([A-Za-z0-9_-]+)/;
|
|
3593
|
+
SPEC_FILE = /\.(?:spec|test)\.[cm]?[jt]sx?$/;
|
|
3407
3594
|
}
|
|
3408
3595
|
});
|
|
3409
3596
|
|
|
@@ -3411,7 +3598,7 @@ var init_failure_detail = __esm({
|
|
|
3411
3598
|
function statedIdOf(test, title) {
|
|
3412
3599
|
const annotation = test.annotations?.find((a) => a.type === "PluneId")?.description;
|
|
3413
3600
|
if (annotation !== void 0 && annotation !== "") return annotation;
|
|
3414
|
-
return
|
|
3601
|
+
return TOKEN2.exec(title)?.[1];
|
|
3415
3602
|
}
|
|
3416
3603
|
function attemptOf(result, file, ctx) {
|
|
3417
3604
|
const steps = (list2) => (list2 ?? []).map((s) => ({ title: s.title ?? "", failed: s.error !== void 0, steps: steps(s.steps) }));
|
|
@@ -3422,10 +3609,13 @@ function attemptOf(result, file, ctx) {
|
|
|
3422
3609
|
...typeof at?.file === "string" && typeof at.line === "number" ? { location: { file: at.file, line: at.line, ...typeof at.column === "number" ? { column: at.column } : {} } } : {}
|
|
3423
3610
|
})),
|
|
3424
3611
|
steps: steps(result.steps),
|
|
3425
|
-
attachments: (result.attachments ?? []).map(({ name, contentType, path:
|
|
3612
|
+
attachments: (result.attachments ?? []).map(({ name, contentType, path: path15, body }) => ({
|
|
3426
3613
|
name: name ?? "",
|
|
3427
3614
|
...contentType !== void 0 ? { contentType } : {},
|
|
3428
|
-
...
|
|
3615
|
+
...path15 !== void 0 ? { path: path15 } : {},
|
|
3616
|
+
// Decoded only where the platform would take it (plune-ai/plune#928): a report of screenshots kept as
|
|
3617
|
+
// bodies is megabytes of base64 that nothing here will read.
|
|
3618
|
+
...typeof body === "string" && isTextType(contentType) ? { body: Buffer.from(body, "base64") } : {}
|
|
3429
3619
|
})),
|
|
3430
3620
|
...ctx.buildHref !== void 0 ? { buildHref: ctx.buildHref } : {},
|
|
3431
3621
|
// Spec files are posix and relative to the root dir; the core reads either slash.
|
|
@@ -3507,12 +3697,17 @@ function readPlaywrightJson(source, file) {
|
|
|
3507
3697
|
const out = [];
|
|
3508
3698
|
for (const suite of suites) walk(suite, [], 0, ctx, out);
|
|
3509
3699
|
const ciUrl = webLink(ctx.buildHref);
|
|
3510
|
-
|
|
3700
|
+
const ci = config?.metadata?.ci;
|
|
3701
|
+
return {
|
|
3702
|
+
results: out,
|
|
3703
|
+
...ciUrl !== void 0 ? { ciUrl } : {},
|
|
3704
|
+
...ci !== void 0 ? { ci } : {}
|
|
3705
|
+
};
|
|
3511
3706
|
}
|
|
3512
3707
|
function looksLikePlaywrightJson(source) {
|
|
3513
3708
|
return /^\s*\{/.test(source) && source.includes('"suites"');
|
|
3514
3709
|
}
|
|
3515
|
-
var import_node_fs4, SOURCE2,
|
|
3710
|
+
var import_node_fs4, SOURCE2, TOKEN2, NOT_FAILED, JsonReportError;
|
|
3516
3711
|
var init_playwright_json = __esm({
|
|
3517
3712
|
"src/importers/playwright-json.ts"() {
|
|
3518
3713
|
"use strict";
|
|
@@ -3521,7 +3716,7 @@ var init_playwright_json = __esm({
|
|
|
3521
3716
|
init_failure_detail();
|
|
3522
3717
|
init_result_key();
|
|
3523
3718
|
SOURCE2 = "playwright";
|
|
3524
|
-
|
|
3719
|
+
TOKEN2 = /@P([A-Za-z0-9_-]+)/;
|
|
3525
3720
|
NOT_FAILED = /* @__PURE__ */ new Set(["passed", "skipped"]);
|
|
3526
3721
|
JsonReportError = class extends Error {
|
|
3527
3722
|
constructor(file, detail) {
|
|
@@ -3590,7 +3785,7 @@ async function handleRunImport(options) {
|
|
|
3590
3785
|
} catch {
|
|
3591
3786
|
throw new RunCommandError(`Cannot read ${options.file}.`);
|
|
3592
3787
|
}
|
|
3593
|
-
const { format, results, ciUrl } = readReport(source, options.file, options.format);
|
|
3788
|
+
const { format, results, ciUrl, ci } = readReport(source, options.file, options.format);
|
|
3594
3789
|
if (results.length === 0) {
|
|
3595
3790
|
throw new RunCommandError(`No test cases in ${options.file} \u2014 it parsed, but there is nothing in it.`);
|
|
3596
3791
|
}
|
|
@@ -3607,7 +3802,10 @@ async function handleRunImport(options) {
|
|
|
3607
3802
|
...options.create === true ? { offerDiscovered: true } : {},
|
|
3608
3803
|
kind: "automated",
|
|
3609
3804
|
// The CI run the report names, never this machine's: whoever imports may be another job (#790).
|
|
3610
|
-
|
|
3805
|
+
// The commit and branch likewise, where the report names a commit; a report that names none —
|
|
3806
|
+
// JUnit's never does — takes this job's own, which is where the tests ran when import follows
|
|
3807
|
+
// them in one job (plune-ai/plune#927).
|
|
3808
|
+
meta: { runner: format, ...ciUrl !== void 0 ? { ciUrl } : {}, ...ciCommit(ci) },
|
|
3611
3809
|
log: write
|
|
3612
3810
|
},
|
|
3613
3811
|
// The file is the complete list of what ran, so the platform can resolve every test in one
|
|
@@ -3631,8 +3829,10 @@ async function handleRunImport(options) {
|
|
|
3631
3829
|
offered: stats.offered,
|
|
3632
3830
|
created: stats.created,
|
|
3633
3831
|
unoffered: stats.unoffered,
|
|
3832
|
+
...stats.unofferedWhy !== void 0 ? { unofferedWhy: stats.unofferedWhy } : {},
|
|
3634
3833
|
deferred: stats.deferred,
|
|
3635
3834
|
screenshots: { ...stats.screenshots },
|
|
3835
|
+
textFiles: { ...stats.textFiles },
|
|
3636
3836
|
unlocatable: results.filter((r) => r.specRef === void 0).length,
|
|
3637
3837
|
offering: options.create === true
|
|
3638
3838
|
};
|
|
@@ -3656,11 +3856,15 @@ function describe(result, apiUrl) {
|
|
|
3656
3856
|
if (result.conflict > 0 || result.rejected > 0) {
|
|
3657
3857
|
lines.push(`${result.conflict} conflicted and ${result.rejected} were refused.`);
|
|
3658
3858
|
}
|
|
3659
|
-
const
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
)
|
|
3859
|
+
for (const [counts, noun] of [
|
|
3860
|
+
[result.screenshots, "screenshot"],
|
|
3861
|
+
[result.textFiles, "text file"]
|
|
3862
|
+
]) {
|
|
3863
|
+
if (counts.uploaded + counts.skipped + counts.failed > 0) {
|
|
3864
|
+
lines.push(
|
|
3865
|
+
`${counts.uploaded} ${noun}(s) uploaded to their results` + (counts.skipped > 0 ? `, ${counts.skipped} skipped` : "") + (counts.failed > 0 ? `, ${counts.failed} could not be uploaded` : "") + "."
|
|
3866
|
+
);
|
|
3867
|
+
}
|
|
3664
3868
|
}
|
|
3665
3869
|
if (result.deferred > 0) {
|
|
3666
3870
|
lines.push(
|
|
@@ -3681,7 +3885,7 @@ function describe(result, apiUrl) {
|
|
|
3681
3885
|
}
|
|
3682
3886
|
if (result.unoffered > 0) {
|
|
3683
3887
|
lines.push(
|
|
3684
|
-
`${result.unoffered} more could not be offered \u2014
|
|
3888
|
+
`${result.unoffered} more could not be offered \u2014 ${NEXT[result.unofferedWhy ?? "other"]}`
|
|
3685
3889
|
);
|
|
3686
3890
|
}
|
|
3687
3891
|
if (result.unlocatable > 0) {
|
|
@@ -3692,7 +3896,7 @@ function describe(result, apiUrl) {
|
|
|
3692
3896
|
if (result.runId !== null) lines.push(runUrl2(apiUrl, result.runId));
|
|
3693
3897
|
return lines;
|
|
3694
3898
|
}
|
|
3695
|
-
var fs8;
|
|
3899
|
+
var fs8, NEXT;
|
|
3696
3900
|
var init_run_import = __esm({
|
|
3697
3901
|
"src/cli/commands/run-import.ts"() {
|
|
3698
3902
|
"use strict";
|
|
@@ -3704,6 +3908,11 @@ var init_run_import = __esm({
|
|
|
3704
3908
|
init_api_url();
|
|
3705
3909
|
init_credentials();
|
|
3706
3910
|
init_env2();
|
|
3911
|
+
NEXT = {
|
|
3912
|
+
queue: "the review queue is full. Approve or reject what is waiting, then import this report again to offer the rest.",
|
|
3913
|
+
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.",
|
|
3914
|
+
other: 'the reason is in the "could not offer" line above.'
|
|
3915
|
+
};
|
|
3707
3916
|
}
|
|
3708
3917
|
});
|
|
3709
3918
|
|
|
@@ -3720,7 +3929,7 @@ function isRunResult(v) {
|
|
|
3720
3929
|
}
|
|
3721
3930
|
function handleReport(opts = {}) {
|
|
3722
3931
|
const cwd = opts.cwd ?? process.cwd();
|
|
3723
|
-
const file =
|
|
3932
|
+
const file = path9.join(cwd, ".plune", "last-run.json");
|
|
3724
3933
|
let raw;
|
|
3725
3934
|
try {
|
|
3726
3935
|
raw = fs9.readFileSync(file, "utf8");
|
|
@@ -3738,13 +3947,13 @@ function handleReport(opts = {}) {
|
|
|
3738
3947
|
}
|
|
3739
3948
|
return parsed;
|
|
3740
3949
|
}
|
|
3741
|
-
var fs9,
|
|
3950
|
+
var fs9, path9, ReportNotFoundError;
|
|
3742
3951
|
var init_report = __esm({
|
|
3743
3952
|
"src/cli/commands/report.ts"() {
|
|
3744
3953
|
"use strict";
|
|
3745
3954
|
init_cjs_shims();
|
|
3746
3955
|
fs9 = __toESM(require("fs"), 1);
|
|
3747
|
-
|
|
3956
|
+
path9 = __toESM(require("path"), 1);
|
|
3748
3957
|
ReportNotFoundError = class extends Error {
|
|
3749
3958
|
code = "NO_SAVED_RUN";
|
|
3750
3959
|
constructor(message) {
|
|
@@ -3948,7 +4157,7 @@ async function runInitWizard(cwd) {
|
|
|
3948
4157
|
process.exit(1);
|
|
3949
4158
|
return;
|
|
3950
4159
|
}
|
|
3951
|
-
const defaultModel = provider === "anthropic" ? "claude-
|
|
4160
|
+
const defaultModel = provider === "anthropic" ? "claude-sonnet-5-5" : "gpt-4o-mini";
|
|
3952
4161
|
const model = await clack.text({
|
|
3953
4162
|
message: "Model name:",
|
|
3954
4163
|
defaultValue: defaultModel,
|
|
@@ -3968,7 +4177,7 @@ async function runInitWizard(cwd) {
|
|
|
3968
4177
|
process.exit(1);
|
|
3969
4178
|
return;
|
|
3970
4179
|
}
|
|
3971
|
-
const outputPath =
|
|
4180
|
+
const outputPath = path10.join(cwd, "plune.yaml");
|
|
3972
4181
|
if (fs11.existsSync(outputPath)) {
|
|
3973
4182
|
const overwrite = await clack.confirm({
|
|
3974
4183
|
message: "plune.yaml already exists. Overwrite?"
|
|
@@ -4012,14 +4221,14 @@ async function runInitWizard(cwd) {
|
|
|
4012
4221
|
spin.stop("plune.yaml created!");
|
|
4013
4222
|
clack.outro("plune.yaml is ready.");
|
|
4014
4223
|
}
|
|
4015
|
-
var clack, fs11,
|
|
4224
|
+
var clack, fs11, path10, import_yaml2;
|
|
4016
4225
|
var init_wizard = __esm({
|
|
4017
4226
|
"src/config/init/wizard.ts"() {
|
|
4018
4227
|
"use strict";
|
|
4019
4228
|
init_cjs_shims();
|
|
4020
4229
|
clack = __toESM(require("@clack/prompts"), 1);
|
|
4021
4230
|
fs11 = __toESM(require("fs"), 1);
|
|
4022
|
-
|
|
4231
|
+
path10 = __toESM(require("path"), 1);
|
|
4023
4232
|
import_yaml2 = require("yaml");
|
|
4024
4233
|
init_errors();
|
|
4025
4234
|
}
|
|
@@ -4027,21 +4236,21 @@ var init_wizard = __esm({
|
|
|
4027
4236
|
|
|
4028
4237
|
// src/cli/templates/index.ts
|
|
4029
4238
|
function writeTemplateFile(cwd, rel, content, force) {
|
|
4030
|
-
const target =
|
|
4239
|
+
const target = path11.join(cwd, rel);
|
|
4031
4240
|
if (!force && fs12.existsSync(target)) {
|
|
4032
4241
|
return "skipped";
|
|
4033
4242
|
}
|
|
4034
|
-
fs12.mkdirSync(
|
|
4243
|
+
fs12.mkdirSync(path11.dirname(target), { recursive: true });
|
|
4035
4244
|
fs12.writeFileSync(target, content, "utf8");
|
|
4036
4245
|
return "written";
|
|
4037
4246
|
}
|
|
4038
|
-
var fs12,
|
|
4247
|
+
var fs12, path11, ENV_EXAMPLE_TPL, EXAMPLE_JSONL_TPL, PLUNE_YAML_TPL;
|
|
4039
4248
|
var init_templates = __esm({
|
|
4040
4249
|
"src/cli/templates/index.ts"() {
|
|
4041
4250
|
"use strict";
|
|
4042
4251
|
init_cjs_shims();
|
|
4043
4252
|
fs12 = __toESM(require("fs"), 1);
|
|
4044
|
-
|
|
4253
|
+
path11 = __toESM(require("path"), 1);
|
|
4045
4254
|
ENV_EXAMPLE_TPL = `# Copy this file to .env and replace the placeholders with your real keys.
|
|
4046
4255
|
# .env is gitignored; .env.example is safe to commit.
|
|
4047
4256
|
# Plune reads whichever key matches your configured provider.
|
|
@@ -4062,7 +4271,7 @@ OPENROUTER_API_KEY=YOUR_OPENROUTER_API_KEY_HERE
|
|
|
4062
4271
|
version: 1
|
|
4063
4272
|
provider:
|
|
4064
4273
|
type: anthropic
|
|
4065
|
-
model: claude-
|
|
4274
|
+
model: claude-sonnet-5-5
|
|
4066
4275
|
evals:
|
|
4067
4276
|
- id: example
|
|
4068
4277
|
prompt: "Answer concisely. {{question}}"
|
|
@@ -4297,7 +4506,7 @@ __export(sync_exports, {
|
|
|
4297
4506
|
reportSyncFailure: () => reportSyncFailure
|
|
4298
4507
|
});
|
|
4299
4508
|
function readRun(cwd, file) {
|
|
4300
|
-
const target = file !== void 0 ?
|
|
4509
|
+
const target = file !== void 0 ? path12.resolve(cwd, file) : path12.join(cwd, ".plune", "last-run.json");
|
|
4301
4510
|
let raw;
|
|
4302
4511
|
try {
|
|
4303
4512
|
raw = fs13.readFileSync(target, "utf8");
|
|
@@ -4364,13 +4573,13 @@ function reportSyncFailure(err, write) {
|
|
|
4364
4573
|
}
|
|
4365
4574
|
return null;
|
|
4366
4575
|
}
|
|
4367
|
-
var fs13,
|
|
4576
|
+
var fs13, path12, NotLoggedInError, TokenRejectedError2, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
4368
4577
|
var init_sync = __esm({
|
|
4369
4578
|
"src/cli/commands/sync.ts"() {
|
|
4370
4579
|
"use strict";
|
|
4371
4580
|
init_cjs_shims();
|
|
4372
4581
|
fs13 = __toESM(require("fs"), 1);
|
|
4373
|
-
|
|
4582
|
+
path12 = __toESM(require("path"), 1);
|
|
4374
4583
|
init_run_result_schema();
|
|
4375
4584
|
init_api_url();
|
|
4376
4585
|
init_credentials();
|
|
@@ -4420,9 +4629,9 @@ async function findLatestCairnRun(runsDir = "runs") {
|
|
|
4420
4629
|
let newest;
|
|
4421
4630
|
for (const entry of entries) {
|
|
4422
4631
|
if (!entry.isDirectory()) continue;
|
|
4423
|
-
const dir =
|
|
4632
|
+
const dir = path13.join(runsDir, entry.name);
|
|
4424
4633
|
try {
|
|
4425
|
-
const report = await fs14.lstat(
|
|
4634
|
+
const report = await fs14.lstat(path13.join(dir, "report.json"));
|
|
4426
4635
|
if (!report.isFile()) continue;
|
|
4427
4636
|
if (newest === void 0 || report.mtimeMs > newest.at) {
|
|
4428
4637
|
newest = { dir, at: report.mtimeMs };
|
|
@@ -4441,7 +4650,7 @@ async function readJson(file) {
|
|
|
4441
4650
|
}
|
|
4442
4651
|
}
|
|
4443
4652
|
async function defaultReadEvidence(dir) {
|
|
4444
|
-
const raw = await readJson(
|
|
4653
|
+
const raw = await readJson(path13.join(dir, "api-evidence.json"));
|
|
4445
4654
|
if (!Array.isArray(raw)) return void 0;
|
|
4446
4655
|
return raw.map((r) => {
|
|
4447
4656
|
const rec = asRecord(r);
|
|
@@ -4483,7 +4692,7 @@ function casesFromApi(report, verdicts) {
|
|
|
4483
4692
|
});
|
|
4484
4693
|
}
|
|
4485
4694
|
async function readCairnRun(dir, deps = {}) {
|
|
4486
|
-
const raw = await readJson(
|
|
4695
|
+
const raw = await readJson(path13.join(dir, "report.json"));
|
|
4487
4696
|
if (raw === void 0) throw new CairnRunNotFoundError(dir);
|
|
4488
4697
|
const report = asRecord(raw);
|
|
4489
4698
|
if (report["schemaVersion"] !== SUPPORTED_ARTIFACT_VERSION) {
|
|
@@ -4513,20 +4722,20 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
4513
4722
|
return {
|
|
4514
4723
|
schemaVersion: INGEST_SCHEMA_VERSION,
|
|
4515
4724
|
run: {
|
|
4516
|
-
runId: str(report["runId"],
|
|
4725
|
+
runId: str(report["runId"], path13.basename(dir)),
|
|
4517
4726
|
mode,
|
|
4518
4727
|
target: str(report["url"], "unknown")
|
|
4519
4728
|
},
|
|
4520
4729
|
cases
|
|
4521
4730
|
};
|
|
4522
4731
|
}
|
|
4523
|
-
var fs14,
|
|
4732
|
+
var fs14, path13, SUPPORTED_ARTIFACT_VERSION, INGEST_SCHEMA_VERSION, NoCairnRunsError, CairnRunNotFoundError, CairnRunVersionError, CairnRunPartialError, CairnEvidenceAmbiguousError, asRecord, str;
|
|
4524
4733
|
var init_cairn_artifact = __esm({
|
|
4525
4734
|
"src/cli/commands/cairn-artifact.ts"() {
|
|
4526
4735
|
"use strict";
|
|
4527
4736
|
init_cjs_shims();
|
|
4528
4737
|
fs14 = __toESM(require("fs/promises"), 1);
|
|
4529
|
-
|
|
4738
|
+
path13 = __toESM(require("path"), 1);
|
|
4530
4739
|
SUPPORTED_ARTIFACT_VERSION = 1;
|
|
4531
4740
|
INGEST_SCHEMA_VERSION = 1;
|
|
4532
4741
|
NoCairnRunsError = class extends Error {
|
|
@@ -4693,7 +4902,7 @@ __export(cases_exports, {
|
|
|
4693
4902
|
function gitDirty(file) {
|
|
4694
4903
|
try {
|
|
4695
4904
|
const out = (0, import_node_child_process2.execFileSync)("git", ["status", "--porcelain", "--", file], {
|
|
4696
|
-
cwd:
|
|
4905
|
+
cwd: path14.dirname(file),
|
|
4697
4906
|
stdio: ["ignore", "pipe", "ignore"],
|
|
4698
4907
|
encoding: "utf8"
|
|
4699
4908
|
});
|
|
@@ -4719,9 +4928,9 @@ async function handlePull(deps = {}) {
|
|
|
4719
4928
|
const cwd = deps.cwd ?? process.cwd();
|
|
4720
4929
|
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4721
4930
|
const token = tokenOf(deps);
|
|
4722
|
-
const file =
|
|
4931
|
+
const file = path14.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
4723
4932
|
if (fs15.existsSync(file) && !deps.force && (deps.isDirty ?? gitDirty)(file))
|
|
4724
|
-
throw new DirtyFileError(
|
|
4933
|
+
throw new DirtyFileError(path14.relative(cwd, file) || file);
|
|
4725
4934
|
const doFetch = deps.fetchImpl ?? fetch;
|
|
4726
4935
|
const endpoint = deps.suiteId !== void 0 ? `${apiUrl}/v1/suites/${encodeURIComponent(deps.suiteId)}/markdown` : `${apiUrl}/v1/test-cases/markdown`;
|
|
4727
4936
|
let res;
|
|
@@ -4736,7 +4945,7 @@ async function handlePull(deps = {}) {
|
|
|
4736
4945
|
throw new SyncHttpError(res.status, detail);
|
|
4737
4946
|
}
|
|
4738
4947
|
const text2 = await res.text();
|
|
4739
|
-
fs15.mkdirSync(
|
|
4948
|
+
fs15.mkdirSync(path14.dirname(file), { recursive: true });
|
|
4740
4949
|
fs15.writeFileSync(file, text2, "utf8");
|
|
4741
4950
|
return { file, cases: Number(res.headers.get("x-plune-cases") ?? "0") };
|
|
4742
4951
|
}
|
|
@@ -4744,7 +4953,7 @@ async function handlePush(deps = {}) {
|
|
|
4744
4953
|
const cwd = deps.cwd ?? process.cwd();
|
|
4745
4954
|
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4746
4955
|
const token = tokenOf(deps);
|
|
4747
|
-
const file =
|
|
4956
|
+
const file = path14.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
4748
4957
|
let text2;
|
|
4749
4958
|
try {
|
|
4750
4959
|
text2 = fs15.readFileSync(file, "utf8");
|
|
@@ -4798,18 +5007,18 @@ function reportCasesFailure(err, write) {
|
|
|
4798
5007
|
}
|
|
4799
5008
|
return null;
|
|
4800
5009
|
}
|
|
4801
|
-
var import_node_child_process2, fs15,
|
|
5010
|
+
var import_node_child_process2, fs15, path14, DEFAULT_CASES_FILE, DirtyFileError;
|
|
4802
5011
|
var init_cases = __esm({
|
|
4803
5012
|
"src/cli/commands/cases.ts"() {
|
|
4804
5013
|
"use strict";
|
|
4805
5014
|
init_cjs_shims();
|
|
4806
5015
|
import_node_child_process2 = require("child_process");
|
|
4807
5016
|
fs15 = __toESM(require("fs"), 1);
|
|
4808
|
-
|
|
5017
|
+
path14 = __toESM(require("path"), 1);
|
|
4809
5018
|
init_api_url();
|
|
4810
5019
|
init_credentials();
|
|
4811
5020
|
init_sync();
|
|
4812
|
-
DEFAULT_CASES_FILE =
|
|
5021
|
+
DEFAULT_CASES_FILE = path14.join("plune", "cases.md");
|
|
4813
5022
|
DirtyFileError = class extends Error {
|
|
4814
5023
|
constructor(file) {
|
|
4815
5024
|
super(`${file} has uncommitted changes; commit them or pass --force to overwrite.`);
|
|
@@ -4945,6 +5154,12 @@ function createProgram() {
|
|
|
4945
5154
|
const program = new import_commander.Command();
|
|
4946
5155
|
program.name("plune").description("Plune \u2014 AI-powered assertion testing CLI").version(pkg.version);
|
|
4947
5156
|
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");
|
|
5157
|
+
program.hook("preAction", async (_program, action) => {
|
|
5158
|
+
const { loadEnv: loadEnv2 } = await Promise.resolve().then(() => (init_env(), env_exports));
|
|
5159
|
+
const { config } = action.optsWithGlobals();
|
|
5160
|
+
if (config !== void 0) loadEnv2((0, import_node_path4.dirname)((0, import_node_path4.resolve)(config)));
|
|
5161
|
+
loadEnv2(process.cwd());
|
|
5162
|
+
});
|
|
4948
5163
|
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(
|
|
4949
5164
|
"--only <selector>",
|
|
4950
5165
|
"Run a subset by eval id or tag (repeatable)",
|
|
@@ -4955,21 +5170,18 @@ function createProgram() {
|
|
|
4955
5170
|
const globals = command.optsWithGlobals();
|
|
4956
5171
|
const configPath = globals.config;
|
|
4957
5172
|
const [
|
|
4958
|
-
{ loadEnv: loadEnv2 },
|
|
4959
5173
|
{ handleRun: handleRun2 },
|
|
4960
5174
|
{ exitCodeFor: exitCodeFor2, RunConfigError: RunConfigError2 },
|
|
4961
5175
|
{ renderReport: renderReport2 },
|
|
4962
5176
|
{ ConfigNotFoundError: ConfigNotFoundError2, ConfigValidationError: ConfigValidationError2, YamlParseError: YamlParseError2 },
|
|
4963
5177
|
{ AuthError: AuthError2 }
|
|
4964
5178
|
] = await Promise.all([
|
|
4965
|
-
Promise.resolve().then(() => (init_env(), env_exports)),
|
|
4966
5179
|
Promise.resolve().then(() => (init_run2(), run_exports)),
|
|
4967
5180
|
Promise.resolve().then(() => (init_orchestrator(), orchestrator_exports)),
|
|
4968
5181
|
Promise.resolve().then(() => (init_reporters(), reporters_exports)),
|
|
4969
5182
|
Promise.resolve().then(() => (init_errors(), errors_exports)),
|
|
4970
5183
|
Promise.resolve().then(() => (init_errors2(), errors_exports2))
|
|
4971
5184
|
]);
|
|
4972
|
-
loadEnv2(configPath !== void 0 ? (0, import_node_path4.dirname)((0, import_node_path4.resolve)(configPath)) : process.cwd());
|
|
4973
5185
|
try {
|
|
4974
5186
|
const result = await handleRun2({
|
|
4975
5187
|
dryRun: options.dryRun,
|
|
@@ -5129,13 +5341,11 @@ function createProgram() {
|
|
|
5129
5341
|
);
|
|
5130
5342
|
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) => {
|
|
5131
5343
|
const globals = command.optsWithGlobals();
|
|
5132
|
-
const [{
|
|
5133
|
-
Promise.resolve().then(() => (init_env(), env_exports)),
|
|
5344
|
+
const [{ handleReport: handleReport2, ReportNotFoundError: ReportNotFoundError2 }, { renderReport: renderReport2 }] = await Promise.all([
|
|
5134
5345
|
Promise.resolve().then(() => (init_report(), report_exports)),
|
|
5135
5346
|
Promise.resolve().then(() => (init_reporters(), reporters_exports))
|
|
5136
5347
|
]);
|
|
5137
5348
|
const cwd = globals.config !== void 0 ? (0, import_node_path4.dirname)((0, import_node_path4.resolve)(globals.config)) : process.cwd();
|
|
5138
|
-
loadEnv2(cwd);
|
|
5139
5349
|
try {
|
|
5140
5350
|
const result = handleReport2({ cwd });
|
|
5141
5351
|
const text2 = renderReport2(result, toFormat(options.format), {
|
|
@@ -5199,10 +5409,10 @@ function createProgram() {
|
|
|
5199
5409
|
const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
5200
5410
|
try {
|
|
5201
5411
|
const token = options.token ?? await readTokenFromStdin();
|
|
5202
|
-
const { path:
|
|
5412
|
+
const { path: path15, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
|
|
5203
5413
|
process.stdout.write(
|
|
5204
|
-
verified ? `Logged in. Token checked against the API and saved to ${
|
|
5205
|
-
` : `Token saved to ${
|
|
5414
|
+
verified ? `Logged in. Token checked against the API and saved to ${path15}
|
|
5415
|
+
` : `Token saved to ${path15} WITHOUT being checked (--skip-verify).
|
|
5206
5416
|
`
|
|
5207
5417
|
);
|
|
5208
5418
|
} catch (err) {
|
|
@@ -5219,9 +5429,9 @@ function createProgram() {
|
|
|
5219
5429
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
5220
5430
|
const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
5221
5431
|
try {
|
|
5222
|
-
const { removed, path:
|
|
5432
|
+
const { removed, path: path15 } = handleLogout2();
|
|
5223
5433
|
process.stdout.write(
|
|
5224
|
-
removed ? `Logged out. Removed ${
|
|
5434
|
+
removed ? `Logged out. Removed ${path15}
|
|
5225
5435
|
` : "Not logged in \u2014 nothing to remove.\n"
|
|
5226
5436
|
);
|
|
5227
5437
|
} catch (err) {
|