@plune-ai/cli 0.15.0 → 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 +20 -3
- package/dist/cli.cjs +223 -83
- package/dist/index.cjs +34 -3
- package/dist/index.js +34 -3
- package/dist/reporter-core.cjs +41 -1
- package/dist/reporter-core.d.cts +38 -1
- package/dist/reporter-core.d.ts +38 -1
- package/dist/reporter-core.js +41 -2
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -414,11 +414,25 @@ async function withRetry(fn, options) {
|
|
|
414
414
|
|
|
415
415
|
// src/providers/prices.ts
|
|
416
416
|
var PRICE_TABLE = {
|
|
417
|
-
// Anthropic
|
|
417
|
+
// Anthropic, current models. Source: platform.claude.com/docs/en/about-claude/pricing — base
|
|
418
|
+
// input and output tokens per million, read on 2026-09-29, kept here per thousand.
|
|
419
|
+
"claude-fable-5-1": { input_per_1k_usd: 0.01, output_per_1k_usd: 0.05 },
|
|
420
|
+
"claude-opus-5-5": { input_per_1k_usd: 4e-3, output_per_1k_usd: 0.02 },
|
|
421
|
+
"claude-sonnet-5-5": { input_per_1k_usd: 2e-3, output_per_1k_usd: 0.01 },
|
|
422
|
+
"claude-haiku-4-5": { input_per_1k_usd: 1e-3, output_per_1k_usd: 5e-3 },
|
|
423
|
+
// The dated id of the same model; a config may name either.
|
|
424
|
+
"claude-haiku-4-5-20251001": { input_per_1k_usd: 1e-3, output_per_1k_usd: 5e-3 },
|
|
425
|
+
// Anthropic, older ids: unchanged, so a config that still names one prices as it did. Anthropic has
|
|
426
|
+
// retired these models — a new project should not start on them.
|
|
418
427
|
"claude-3-5-haiku-latest": { input_per_1k_usd: 8e-4, output_per_1k_usd: 4e-3 },
|
|
419
428
|
"claude-3-5-sonnet-latest": { input_per_1k_usd: 3e-3, output_per_1k_usd: 0.015 },
|
|
420
429
|
"claude-3-opus-latest": { input_per_1k_usd: 0.015, output_per_1k_usd: 0.075 },
|
|
421
|
-
// OpenAI
|
|
430
|
+
// OpenAI, current models. Source: developers.openai.com/api/docs/pricing — standard tier, the
|
|
431
|
+
// short-context rates (a long-context request costs more), read on 2026-09-29.
|
|
432
|
+
"gpt-6-astra": { input_per_1k_usd: 0.01, output_per_1k_usd: 0.05 },
|
|
433
|
+
"gpt-6.1-sol": { input_per_1k_usd: 2e-3, output_per_1k_usd: 0.01 },
|
|
434
|
+
"gpt-6-luna": { input_per_1k_usd: 1e-4, output_per_1k_usd: 5e-4 },
|
|
435
|
+
// OpenAI, older ids: unchanged (the same rates the page above still lists for both).
|
|
422
436
|
"gpt-4o": { input_per_1k_usd: 25e-4, output_per_1k_usd: 0.01 },
|
|
423
437
|
"gpt-4o-mini": { input_per_1k_usd: 15e-5, output_per_1k_usd: 6e-4 },
|
|
424
438
|
// OpenAI via OpenRouter (namespaced ids). OpenRouter passes OpenAI list price through for
|
|
@@ -607,6 +621,18 @@ function getProvider(config, env, pricing) {
|
|
|
607
621
|
return createDefaultRegistry().resolve(config.type, config, env, pricing);
|
|
608
622
|
}
|
|
609
623
|
|
|
624
|
+
// src/providers/estimate.ts
|
|
625
|
+
function makeEstimateProvider(config, pricing) {
|
|
626
|
+
return {
|
|
627
|
+
complete: () => Promise.reject(
|
|
628
|
+
new Error("The --dry-run provider only estimates cost; it never calls a model.")
|
|
629
|
+
),
|
|
630
|
+
estimateCost: (usage, reportedCostUsd) => ({
|
|
631
|
+
cost_usd: resolveCost(usage, config.model, reportedCostUsd, pricing)
|
|
632
|
+
})
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
610
636
|
// src/providers/mock.ts
|
|
611
637
|
function makeMockProvider() {
|
|
612
638
|
return {
|
|
@@ -1473,11 +1499,16 @@ var NOOP_CACHE = { get: () => void 0, set: () => {
|
|
|
1473
1499
|
function isMockMode(env) {
|
|
1474
1500
|
return env["PLUNE_MOCK_PROVIDER"] === "1";
|
|
1475
1501
|
}
|
|
1502
|
+
function resolveProviderFor(config, dryRun) {
|
|
1503
|
+
if (isMockMode(process.env)) return () => makeMockProvider();
|
|
1504
|
+
if (dryRun) return (cfg) => makeEstimateProvider(cfg, config.pricing);
|
|
1505
|
+
return (cfg) => getProvider(cfg, process.env, config.pricing);
|
|
1506
|
+
}
|
|
1476
1507
|
function buildRealDeps(config, baseDir, dryRun) {
|
|
1477
1508
|
const dir = path4.join(baseDir, ".plune");
|
|
1478
1509
|
fs5.mkdirSync(dir, { recursive: true });
|
|
1479
1510
|
return {
|
|
1480
|
-
resolveProvider:
|
|
1511
|
+
resolveProvider: resolveProviderFor(config, dryRun),
|
|
1481
1512
|
embedder: getDefaultEmbedder(),
|
|
1482
1513
|
cache: dryRun ? NOOP_CACHE : openCache(path4.join(dir, "cache.db")),
|
|
1483
1514
|
now: Date.now,
|
package/dist/index.js
CHANGED
|
@@ -361,11 +361,25 @@ async function withRetry(fn, options) {
|
|
|
361
361
|
|
|
362
362
|
// src/providers/prices.ts
|
|
363
363
|
var PRICE_TABLE = {
|
|
364
|
-
// Anthropic
|
|
364
|
+
// Anthropic, current models. Source: platform.claude.com/docs/en/about-claude/pricing — base
|
|
365
|
+
// input and output tokens per million, read on 2026-09-29, kept here per thousand.
|
|
366
|
+
"claude-fable-5-1": { input_per_1k_usd: 0.01, output_per_1k_usd: 0.05 },
|
|
367
|
+
"claude-opus-5-5": { input_per_1k_usd: 4e-3, output_per_1k_usd: 0.02 },
|
|
368
|
+
"claude-sonnet-5-5": { input_per_1k_usd: 2e-3, output_per_1k_usd: 0.01 },
|
|
369
|
+
"claude-haiku-4-5": { input_per_1k_usd: 1e-3, output_per_1k_usd: 5e-3 },
|
|
370
|
+
// The dated id of the same model; a config may name either.
|
|
371
|
+
"claude-haiku-4-5-20251001": { input_per_1k_usd: 1e-3, output_per_1k_usd: 5e-3 },
|
|
372
|
+
// Anthropic, older ids: unchanged, so a config that still names one prices as it did. Anthropic has
|
|
373
|
+
// retired these models — a new project should not start on them.
|
|
365
374
|
"claude-3-5-haiku-latest": { input_per_1k_usd: 8e-4, output_per_1k_usd: 4e-3 },
|
|
366
375
|
"claude-3-5-sonnet-latest": { input_per_1k_usd: 3e-3, output_per_1k_usd: 0.015 },
|
|
367
376
|
"claude-3-opus-latest": { input_per_1k_usd: 0.015, output_per_1k_usd: 0.075 },
|
|
368
|
-
// OpenAI
|
|
377
|
+
// OpenAI, current models. Source: developers.openai.com/api/docs/pricing — standard tier, the
|
|
378
|
+
// short-context rates (a long-context request costs more), read on 2026-09-29.
|
|
379
|
+
"gpt-6-astra": { input_per_1k_usd: 0.01, output_per_1k_usd: 0.05 },
|
|
380
|
+
"gpt-6.1-sol": { input_per_1k_usd: 2e-3, output_per_1k_usd: 0.01 },
|
|
381
|
+
"gpt-6-luna": { input_per_1k_usd: 1e-4, output_per_1k_usd: 5e-4 },
|
|
382
|
+
// OpenAI, older ids: unchanged (the same rates the page above still lists for both).
|
|
369
383
|
"gpt-4o": { input_per_1k_usd: 25e-4, output_per_1k_usd: 0.01 },
|
|
370
384
|
"gpt-4o-mini": { input_per_1k_usd: 15e-5, output_per_1k_usd: 6e-4 },
|
|
371
385
|
// OpenAI via OpenRouter (namespaced ids). OpenRouter passes OpenAI list price through for
|
|
@@ -554,6 +568,18 @@ function getProvider(config, env, pricing) {
|
|
|
554
568
|
return createDefaultRegistry().resolve(config.type, config, env, pricing);
|
|
555
569
|
}
|
|
556
570
|
|
|
571
|
+
// src/providers/estimate.ts
|
|
572
|
+
function makeEstimateProvider(config, pricing) {
|
|
573
|
+
return {
|
|
574
|
+
complete: () => Promise.reject(
|
|
575
|
+
new Error("The --dry-run provider only estimates cost; it never calls a model.")
|
|
576
|
+
),
|
|
577
|
+
estimateCost: (usage, reportedCostUsd) => ({
|
|
578
|
+
cost_usd: resolveCost(usage, config.model, reportedCostUsd, pricing)
|
|
579
|
+
})
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
557
583
|
// src/providers/mock.ts
|
|
558
584
|
function makeMockProvider() {
|
|
559
585
|
return {
|
|
@@ -1420,11 +1446,16 @@ var NOOP_CACHE = { get: () => void 0, set: () => {
|
|
|
1420
1446
|
function isMockMode(env) {
|
|
1421
1447
|
return env["PLUNE_MOCK_PROVIDER"] === "1";
|
|
1422
1448
|
}
|
|
1449
|
+
function resolveProviderFor(config, dryRun) {
|
|
1450
|
+
if (isMockMode(process.env)) return () => makeMockProvider();
|
|
1451
|
+
if (dryRun) return (cfg) => makeEstimateProvider(cfg, config.pricing);
|
|
1452
|
+
return (cfg) => getProvider(cfg, process.env, config.pricing);
|
|
1453
|
+
}
|
|
1423
1454
|
function buildRealDeps(config, baseDir, dryRun) {
|
|
1424
1455
|
const dir = path4.join(baseDir, ".plune");
|
|
1425
1456
|
fs5.mkdirSync(dir, { recursive: true });
|
|
1426
1457
|
return {
|
|
1427
|
-
resolveProvider:
|
|
1458
|
+
resolveProvider: resolveProviderFor(config, dryRun),
|
|
1428
1459
|
embedder: getDefaultEmbedder(),
|
|
1429
1460
|
cache: dryRun ? NOOP_CACHE : openCache(path4.join(dir, "cache.db")),
|
|
1430
1461
|
now: Date.now,
|
package/dist/reporter-core.cjs
CHANGED
|
@@ -34,6 +34,7 @@ __export(reporter_core_exports, {
|
|
|
34
34
|
RESULT_KEY_MAX: () => RESULT_KEY_MAX,
|
|
35
35
|
UNSUPPORTED_VARS: () => UNSUPPORTED_VARS,
|
|
36
36
|
appendBatch: () => appendBatch,
|
|
37
|
+
ciCommit: () => ciCommit,
|
|
37
38
|
createClient: () => createClient,
|
|
38
39
|
errorContextOf: () => errorContextOf,
|
|
39
40
|
failureOf: () => failureOf,
|
|
@@ -247,6 +248,28 @@ function defaultRunTitle(env = process.env, now = /* @__PURE__ */ new Date(), cw
|
|
|
247
248
|
const where = flag(env, "CI") || flag(env, "GITHUB_ACTIONS") ? "ci" : "local";
|
|
248
249
|
return `${path.basename(cwd)} \xB7 ${stamp} \xB7 ${where}`;
|
|
249
250
|
}
|
|
251
|
+
var PROVIDERS = [
|
|
252
|
+
{ is: "GITHUB_ACTIONS", sha: "GITHUB_SHA", branch: ["GITHUB_HEAD_REF", "GITHUB_REF_NAME"] },
|
|
253
|
+
{ is: "GITLAB_CI", sha: "CI_COMMIT_SHA", branch: ["CI_COMMIT_REF_NAME"] },
|
|
254
|
+
{ is: "JENKINS_URL", sha: "GIT_COMMIT", branch: ["GIT_BRANCH"] }
|
|
255
|
+
];
|
|
256
|
+
function ciCommit(reported, env = process.env) {
|
|
257
|
+
const said = typeof reported === "object" && reported !== null ? reported : {};
|
|
258
|
+
const text = (raw) => typeof raw === "string" && raw.trim() !== "" ? raw.trim() : void 0;
|
|
259
|
+
const provider = PROVIDERS.find((p) => value(env, p.is) !== void 0);
|
|
260
|
+
const job = {
|
|
261
|
+
sha: provider === void 0 ? void 0 : value(env, provider.sha),
|
|
262
|
+
branch: provider?.branch.map((name) => value(env, name)).find((v) => v !== void 0)
|
|
263
|
+
};
|
|
264
|
+
const sha = text(said["commitHash"]);
|
|
265
|
+
const sameCommit = sha === void 0 || sha === job.sha;
|
|
266
|
+
const commit = sha ?? job.sha;
|
|
267
|
+
const branch = text(said["branch"]) ?? (sameCommit ? job.branch : void 0);
|
|
268
|
+
return {
|
|
269
|
+
...commit !== void 0 ? { sha: commit } : {},
|
|
270
|
+
...branch !== void 0 ? { branch } : {}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
250
273
|
|
|
251
274
|
// src/reporter-core/fallback.ts
|
|
252
275
|
var fs = __toESM(require("fs"), 1);
|
|
@@ -258,6 +281,8 @@ function appendBatch(file, batch) {
|
|
|
258
281
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
259
282
|
runId: batch.runId,
|
|
260
283
|
externalKey: batch.externalKey,
|
|
284
|
+
...batch.session !== void 0 ? { session: batch.session } : {},
|
|
285
|
+
...batch.meta !== void 0 ? { meta: batch.meta } : {},
|
|
261
286
|
results: batch.results
|
|
262
287
|
};
|
|
263
288
|
fs.appendFileSync(file, `${JSON.stringify(line)}
|
|
@@ -311,6 +336,12 @@ function boundKey(key) {
|
|
|
311
336
|
if (/[\uD800-\uDBFF]$/.test(head)) head = head.slice(0, -1);
|
|
312
337
|
return { ...key, value: `${head}#${digest}` };
|
|
313
338
|
}
|
|
339
|
+
function whyUnoffered(out) {
|
|
340
|
+
if (out.ok || out.status !== 429) return "other";
|
|
341
|
+
if (/review queue quota reached/i.test(out.detail)) return "queue";
|
|
342
|
+
if (/test case quota reached/i.test(out.detail)) return "cases";
|
|
343
|
+
return "other";
|
|
344
|
+
}
|
|
314
345
|
function boundTitle(title) {
|
|
315
346
|
return title.length <= TITLE_MAX ? title : `${title.slice(0, TITLE_MAX - 1)}\u2026`;
|
|
316
347
|
}
|
|
@@ -354,6 +385,7 @@ async function startRun(passed, declared) {
|
|
|
354
385
|
`));
|
|
355
386
|
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
356
387
|
const externalKey = cfg.externalKey ?? null;
|
|
388
|
+
const sessionMarker = (0, import_node_crypto.randomUUID)();
|
|
357
389
|
const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
|
|
358
390
|
const offerDiscovered = cfg.offerDiscovered ?? false;
|
|
359
391
|
const token = cfg.token ?? loadToken() ?? "";
|
|
@@ -405,7 +437,13 @@ async function startRun(passed, declared) {
|
|
|
405
437
|
log(`plune: ignoring ${env.ignored.join(", ")} \u2014 a run has nowhere to store them yet.`);
|
|
406
438
|
}
|
|
407
439
|
function defer(results) {
|
|
408
|
-
appendBatch(fallbackPath, {
|
|
440
|
+
appendBatch(fallbackPath, {
|
|
441
|
+
runId,
|
|
442
|
+
externalKey,
|
|
443
|
+
session: sessionMarker,
|
|
444
|
+
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
445
|
+
results
|
|
446
|
+
});
|
|
409
447
|
stats.deferred += results.length;
|
|
410
448
|
}
|
|
411
449
|
async function resolveKeys(values) {
|
|
@@ -502,6 +540,7 @@ async function startRun(passed, declared) {
|
|
|
502
540
|
});
|
|
503
541
|
if (!out.ok || !Array.isArray(out.body?.results)) {
|
|
504
542
|
stats.unoffered = batches.slice(index).reduce((n, rest) => n + rest.length, 0);
|
|
543
|
+
stats.unofferedWhy = whyUnoffered(out);
|
|
505
544
|
log(
|
|
506
545
|
`plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.ok ? "the answer had no results" : out.detail || out.kind}).`
|
|
507
546
|
);
|
|
@@ -844,6 +883,7 @@ function webLink(href) {
|
|
|
844
883
|
RESULT_KEY_MAX,
|
|
845
884
|
UNSUPPORTED_VARS,
|
|
846
885
|
appendBatch,
|
|
886
|
+
ciCommit,
|
|
847
887
|
createClient,
|
|
848
888
|
errorContextOf,
|
|
849
889
|
failureOf,
|
package/dist/reporter-core.d.cts
CHANGED
|
@@ -200,6 +200,12 @@ interface ExpectedEntry {
|
|
|
200
200
|
testCaseId?: string;
|
|
201
201
|
externalKey?: KeyRef;
|
|
202
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Why the platform would not take the tests offered to it (plune-ai/cli#71): its review queue is at its
|
|
205
|
+
* ceiling, the project is at its case limit, or anything else — a throttle, an outage, a run that is not
|
|
206
|
+
* there. What a person does next depends on which, and only the first two have advice of their own.
|
|
207
|
+
*/
|
|
208
|
+
type UnofferedWhy = 'queue' | 'cases' | 'other';
|
|
203
209
|
/** The run as the platform answers with it. Only `id` is load-bearing for the client. */
|
|
204
210
|
interface RunRecord {
|
|
205
211
|
id: string;
|
|
@@ -240,6 +246,12 @@ interface RunStats extends SubmitCounts {
|
|
|
240
246
|
* queue at capacity means "empty it and import again"; nothing else here means that.
|
|
241
247
|
*/
|
|
242
248
|
unoffered: number;
|
|
249
|
+
/**
|
|
250
|
+
* Why they were not, said by the platform's own words and present only when `unoffered` is not zero:
|
|
251
|
+
* that is what carries the reason the paragraph above says a subtraction cannot. The platform's line
|
|
252
|
+
* about it is printed with the failure; this is what a command turns into the advice that fits.
|
|
253
|
+
*/
|
|
254
|
+
unofferedWhy?: UnofferedWhy;
|
|
243
255
|
/**
|
|
244
256
|
* Results not delivered, written to the fallback file instead of the platform (#790): a batch
|
|
245
257
|
* refused for any reason — its size, the network, the token, a closed run — or a result whose keys
|
|
@@ -375,6 +387,20 @@ interface EnvSettings {
|
|
|
375
387
|
ignored: string[];
|
|
376
388
|
}
|
|
377
389
|
declare function readEnv(env?: NodeJS.ProcessEnv): EnvSettings;
|
|
390
|
+
/**
|
|
391
|
+
* The commit and the branch a run belongs to (plune-ai/plune#927), for `meta.sha` and `meta.branch`.
|
|
392
|
+
*
|
|
393
|
+
* `reported` is what the runner said about its CI — Playwright's `metadata.ci`, or the copy of it in
|
|
394
|
+
* its JSON report — and outranks the job's variables: it is a project's own word where the CI is one
|
|
395
|
+
* Playwright does not know, and it is where the tests ran when another job imports the report. So a
|
|
396
|
+
* report naming a commit other than this job's gets that commit and no branch of this job's: half of
|
|
397
|
+
* each would be a commit on a branch it is not on. Playwright gives no branch on GitHub at all, which
|
|
398
|
+
* is why the job's variables are read there.
|
|
399
|
+
*
|
|
400
|
+
* Each field is present only when something said it: outside a CI there is nothing to say, and nothing
|
|
401
|
+
* is guessed.
|
|
402
|
+
*/
|
|
403
|
+
declare function ciCommit(reported?: unknown, env?: NodeJS.ProcessEnv): Pick<RunMeta, 'sha' | 'branch'>;
|
|
378
404
|
|
|
379
405
|
/**
|
|
380
406
|
* What ends up in a deferred batch.
|
|
@@ -397,10 +423,21 @@ declare const DEFAULT_FALLBACK_PATH: string;
|
|
|
397
423
|
*
|
|
398
424
|
* `runId: null` is a real state, not a missing value — the platform was unreachable when the run
|
|
399
425
|
* was supposed to be created, so these results have no run yet and replay has to make one.
|
|
426
|
+
*
|
|
427
|
+
* `meta` is where that run came from — the commit and branch, the CI run, the runner (plune-ai/plune#927).
|
|
428
|
+
* Recorded so the run replay makes is the run the reporter would have made; replay reads it and nothing
|
|
429
|
+
* else about the machine it runs on. A line written before the field existed has none.
|
|
430
|
+
*
|
|
431
|
+
* `session` marks the reporter session that wrote the line, made once by that session (plune-ai/cli#45).
|
|
432
|
+
* A run of 24 tests at a batch of 10 is three lines, and replay opens one run for the lines that share a
|
|
433
|
+
* marker rather than one for each. A line written before the field existed has none, and is one run's
|
|
434
|
+
* worth by itself — nothing in it says otherwise.
|
|
400
435
|
*/
|
|
401
436
|
interface DeferredBatch {
|
|
402
437
|
runId: string | null;
|
|
403
438
|
externalKey: string | null;
|
|
439
|
+
session?: string;
|
|
440
|
+
meta?: RunMeta;
|
|
404
441
|
results: DeferredResult[];
|
|
405
442
|
}
|
|
406
443
|
/**
|
|
@@ -496,4 +533,4 @@ interface PlatformClient {
|
|
|
496
533
|
}
|
|
497
534
|
declare function createClient(opts: ClientOptions): PlatformClient;
|
|
498
535
|
|
|
499
|
-
export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, appendBatch, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
|
|
536
|
+
export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, type UnofferedWhy, appendBatch, ciCommit, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
|
package/dist/reporter-core.d.ts
CHANGED
|
@@ -200,6 +200,12 @@ interface ExpectedEntry {
|
|
|
200
200
|
testCaseId?: string;
|
|
201
201
|
externalKey?: KeyRef;
|
|
202
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Why the platform would not take the tests offered to it (plune-ai/cli#71): its review queue is at its
|
|
205
|
+
* ceiling, the project is at its case limit, or anything else — a throttle, an outage, a run that is not
|
|
206
|
+
* there. What a person does next depends on which, and only the first two have advice of their own.
|
|
207
|
+
*/
|
|
208
|
+
type UnofferedWhy = 'queue' | 'cases' | 'other';
|
|
203
209
|
/** The run as the platform answers with it. Only `id` is load-bearing for the client. */
|
|
204
210
|
interface RunRecord {
|
|
205
211
|
id: string;
|
|
@@ -240,6 +246,12 @@ interface RunStats extends SubmitCounts {
|
|
|
240
246
|
* queue at capacity means "empty it and import again"; nothing else here means that.
|
|
241
247
|
*/
|
|
242
248
|
unoffered: number;
|
|
249
|
+
/**
|
|
250
|
+
* Why they were not, said by the platform's own words and present only when `unoffered` is not zero:
|
|
251
|
+
* that is what carries the reason the paragraph above says a subtraction cannot. The platform's line
|
|
252
|
+
* about it is printed with the failure; this is what a command turns into the advice that fits.
|
|
253
|
+
*/
|
|
254
|
+
unofferedWhy?: UnofferedWhy;
|
|
243
255
|
/**
|
|
244
256
|
* Results not delivered, written to the fallback file instead of the platform (#790): a batch
|
|
245
257
|
* refused for any reason — its size, the network, the token, a closed run — or a result whose keys
|
|
@@ -375,6 +387,20 @@ interface EnvSettings {
|
|
|
375
387
|
ignored: string[];
|
|
376
388
|
}
|
|
377
389
|
declare function readEnv(env?: NodeJS.ProcessEnv): EnvSettings;
|
|
390
|
+
/**
|
|
391
|
+
* The commit and the branch a run belongs to (plune-ai/plune#927), for `meta.sha` and `meta.branch`.
|
|
392
|
+
*
|
|
393
|
+
* `reported` is what the runner said about its CI — Playwright's `metadata.ci`, or the copy of it in
|
|
394
|
+
* its JSON report — and outranks the job's variables: it is a project's own word where the CI is one
|
|
395
|
+
* Playwright does not know, and it is where the tests ran when another job imports the report. So a
|
|
396
|
+
* report naming a commit other than this job's gets that commit and no branch of this job's: half of
|
|
397
|
+
* each would be a commit on a branch it is not on. Playwright gives no branch on GitHub at all, which
|
|
398
|
+
* is why the job's variables are read there.
|
|
399
|
+
*
|
|
400
|
+
* Each field is present only when something said it: outside a CI there is nothing to say, and nothing
|
|
401
|
+
* is guessed.
|
|
402
|
+
*/
|
|
403
|
+
declare function ciCommit(reported?: unknown, env?: NodeJS.ProcessEnv): Pick<RunMeta, 'sha' | 'branch'>;
|
|
378
404
|
|
|
379
405
|
/**
|
|
380
406
|
* What ends up in a deferred batch.
|
|
@@ -397,10 +423,21 @@ declare const DEFAULT_FALLBACK_PATH: string;
|
|
|
397
423
|
*
|
|
398
424
|
* `runId: null` is a real state, not a missing value — the platform was unreachable when the run
|
|
399
425
|
* was supposed to be created, so these results have no run yet and replay has to make one.
|
|
426
|
+
*
|
|
427
|
+
* `meta` is where that run came from — the commit and branch, the CI run, the runner (plune-ai/plune#927).
|
|
428
|
+
* Recorded so the run replay makes is the run the reporter would have made; replay reads it and nothing
|
|
429
|
+
* else about the machine it runs on. A line written before the field existed has none.
|
|
430
|
+
*
|
|
431
|
+
* `session` marks the reporter session that wrote the line, made once by that session (plune-ai/cli#45).
|
|
432
|
+
* A run of 24 tests at a batch of 10 is three lines, and replay opens one run for the lines that share a
|
|
433
|
+
* marker rather than one for each. A line written before the field existed has none, and is one run's
|
|
434
|
+
* worth by itself — nothing in it says otherwise.
|
|
400
435
|
*/
|
|
401
436
|
interface DeferredBatch {
|
|
402
437
|
runId: string | null;
|
|
403
438
|
externalKey: string | null;
|
|
439
|
+
session?: string;
|
|
440
|
+
meta?: RunMeta;
|
|
404
441
|
results: DeferredResult[];
|
|
405
442
|
}
|
|
406
443
|
/**
|
|
@@ -496,4 +533,4 @@ interface PlatformClient {
|
|
|
496
533
|
}
|
|
497
534
|
declare function createClient(opts: ClientOptions): PlatformClient;
|
|
498
535
|
|
|
499
|
-
export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, appendBatch, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
|
|
536
|
+
export { type AssertionRecord, type Attachment, type AttemptError, type ClientFailureKind, type ClientOptions, type ClientOutcome, DEFAULT_FALLBACK_PATH, type DeclaredStep, type DeferredBatch, type DeferredResult, type EnvSettings, type Execution, type ExpectedEntry, type ExternalKeyKind, type FailedAttempt, type FailureDetail, type KeyRef, type PendingResult, type PlatformClient, RESULT_KEY_MAX, type ReporterConfig, type ResultFile, type ResultStatus, type ResultSubmission, type RunKind, type RunMeta, type RunRecord, type RunSession, type RunStats, type RunnerLocation, type SubmitCounts, UNSUPPORTED_VARS, type UnofferedWhy, appendBatch, ciCommit, createClient, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
|
package/dist/reporter-core.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/reporter-core/session.ts
|
|
2
|
-
import { createHash } from "crypto";
|
|
2
|
+
import { createHash, randomUUID } from "crypto";
|
|
3
3
|
import { promises as disk } from "fs";
|
|
4
4
|
import { basename as basename2, extname } from "path";
|
|
5
5
|
|
|
@@ -199,6 +199,28 @@ function defaultRunTitle(env = process.env, now = /* @__PURE__ */ new Date(), cw
|
|
|
199
199
|
const where = flag(env, "CI") || flag(env, "GITHUB_ACTIONS") ? "ci" : "local";
|
|
200
200
|
return `${path.basename(cwd)} \xB7 ${stamp} \xB7 ${where}`;
|
|
201
201
|
}
|
|
202
|
+
var PROVIDERS = [
|
|
203
|
+
{ is: "GITHUB_ACTIONS", sha: "GITHUB_SHA", branch: ["GITHUB_HEAD_REF", "GITHUB_REF_NAME"] },
|
|
204
|
+
{ is: "GITLAB_CI", sha: "CI_COMMIT_SHA", branch: ["CI_COMMIT_REF_NAME"] },
|
|
205
|
+
{ is: "JENKINS_URL", sha: "GIT_COMMIT", branch: ["GIT_BRANCH"] }
|
|
206
|
+
];
|
|
207
|
+
function ciCommit(reported, env = process.env) {
|
|
208
|
+
const said = typeof reported === "object" && reported !== null ? reported : {};
|
|
209
|
+
const text = (raw) => typeof raw === "string" && raw.trim() !== "" ? raw.trim() : void 0;
|
|
210
|
+
const provider = PROVIDERS.find((p) => value(env, p.is) !== void 0);
|
|
211
|
+
const job = {
|
|
212
|
+
sha: provider === void 0 ? void 0 : value(env, provider.sha),
|
|
213
|
+
branch: provider?.branch.map((name) => value(env, name)).find((v) => v !== void 0)
|
|
214
|
+
};
|
|
215
|
+
const sha = text(said["commitHash"]);
|
|
216
|
+
const sameCommit = sha === void 0 || sha === job.sha;
|
|
217
|
+
const commit = sha ?? job.sha;
|
|
218
|
+
const branch = text(said["branch"]) ?? (sameCommit ? job.branch : void 0);
|
|
219
|
+
return {
|
|
220
|
+
...commit !== void 0 ? { sha: commit } : {},
|
|
221
|
+
...branch !== void 0 ? { branch } : {}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
202
224
|
|
|
203
225
|
// src/reporter-core/fallback.ts
|
|
204
226
|
import * as fs from "fs";
|
|
@@ -210,6 +232,8 @@ function appendBatch(file, batch) {
|
|
|
210
232
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
211
233
|
runId: batch.runId,
|
|
212
234
|
externalKey: batch.externalKey,
|
|
235
|
+
...batch.session !== void 0 ? { session: batch.session } : {},
|
|
236
|
+
...batch.meta !== void 0 ? { meta: batch.meta } : {},
|
|
213
237
|
results: batch.results
|
|
214
238
|
};
|
|
215
239
|
fs.appendFileSync(file, `${JSON.stringify(line)}
|
|
@@ -263,6 +287,12 @@ function boundKey(key) {
|
|
|
263
287
|
if (/[\uD800-\uDBFF]$/.test(head)) head = head.slice(0, -1);
|
|
264
288
|
return { ...key, value: `${head}#${digest}` };
|
|
265
289
|
}
|
|
290
|
+
function whyUnoffered(out) {
|
|
291
|
+
if (out.ok || out.status !== 429) return "other";
|
|
292
|
+
if (/review queue quota reached/i.test(out.detail)) return "queue";
|
|
293
|
+
if (/test case quota reached/i.test(out.detail)) return "cases";
|
|
294
|
+
return "other";
|
|
295
|
+
}
|
|
266
296
|
function boundTitle(title) {
|
|
267
297
|
return title.length <= TITLE_MAX ? title : `${title.slice(0, TITLE_MAX - 1)}\u2026`;
|
|
268
298
|
}
|
|
@@ -306,6 +336,7 @@ async function startRun(passed, declared) {
|
|
|
306
336
|
`));
|
|
307
337
|
const fallbackPath = cfg.fallbackPath ?? DEFAULT_FALLBACK_PATH;
|
|
308
338
|
const externalKey = cfg.externalKey ?? null;
|
|
339
|
+
const sessionMarker = randomUUID();
|
|
309
340
|
const batchSize = Math.min(cfg.batchSize ?? BATCH_DEFAULT, BATCH_MAX);
|
|
310
341
|
const offerDiscovered = cfg.offerDiscovered ?? false;
|
|
311
342
|
const token = cfg.token ?? loadToken() ?? "";
|
|
@@ -357,7 +388,13 @@ async function startRun(passed, declared) {
|
|
|
357
388
|
log(`plune: ignoring ${env.ignored.join(", ")} \u2014 a run has nowhere to store them yet.`);
|
|
358
389
|
}
|
|
359
390
|
function defer(results) {
|
|
360
|
-
appendBatch(fallbackPath, {
|
|
391
|
+
appendBatch(fallbackPath, {
|
|
392
|
+
runId,
|
|
393
|
+
externalKey,
|
|
394
|
+
session: sessionMarker,
|
|
395
|
+
...cfg.meta !== void 0 ? { meta: cfg.meta } : {},
|
|
396
|
+
results
|
|
397
|
+
});
|
|
361
398
|
stats.deferred += results.length;
|
|
362
399
|
}
|
|
363
400
|
async function resolveKeys(values) {
|
|
@@ -454,6 +491,7 @@ async function startRun(passed, declared) {
|
|
|
454
491
|
});
|
|
455
492
|
if (!out.ok || !Array.isArray(out.body?.results)) {
|
|
456
493
|
stats.unoffered = batches.slice(index).reduce((n, rest) => n + rest.length, 0);
|
|
494
|
+
stats.unofferedWhy = whyUnoffered(out);
|
|
457
495
|
log(
|
|
458
496
|
`plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.ok ? "the answer had no results" : out.detail || out.kind}).`
|
|
459
497
|
);
|
|
@@ -795,6 +833,7 @@ export {
|
|
|
795
833
|
RESULT_KEY_MAX,
|
|
796
834
|
UNSUPPORTED_VARS,
|
|
797
835
|
appendBatch,
|
|
836
|
+
ciCommit,
|
|
798
837
|
createClient,
|
|
799
838
|
errorContextOf,
|
|
800
839
|
failureOf,
|
package/package.json
CHANGED