@plune-ai/cli 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -4
- package/dist/cli.cjs +87 -20
- package/dist/index.cjs +31 -3
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +31 -3
- package/dist/reporter-core.cjs +8 -3
- package/dist/reporter-core.d.cts +17 -3
- package/dist/reporter-core.d.ts +17 -3
- package/dist/reporter-core.js +8 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -119,11 +119,11 @@ the last resort, used only when `PLUNE_TOKEN` is set nowhere else. Nothing write
|
|
|
119
119
|
| `plune init` | Scaffold `plune.yaml`, a sample dataset, and `.env.example`. Flags: `--yes` (non-interactive), `--force`. |
|
|
120
120
|
| `plune login` | Save a [Plune platform](https://plune.ai/platform) API token so `sync` and `ingest` can reach it. The token is **checked against the API before it is saved**, so a wrong one fails here rather than two commands later. Get one at `https://beta.plune.ai` → Account → API tokens. Flags: `--token <token>` (omit to paste it or pipe it via stdin), `--skip-verify` (save without checking, for offline setup). In CI, skip this step: every platform command reads `PLUNE_TOKEN` from the environment first, and the saved login only when it is not set. |
|
|
121
121
|
| `plune logout` | Remove the saved token. |
|
|
122
|
-
| `plune sync` | Upload the latest local run to the platform. Flags: `--file <path>` to send a specific run JSON. |
|
|
123
|
-
| `plune run import <file>` | Turn a **JUnit XML** or **Playwright JSON** report, or an **Allure `allure-results` folder**, into a run in Plune. Needs no provider key — nothing is generated. Flags: `--format junit\|playwright-json\|allure` (detected from the file when omitted; a folder is Allure), `--key <externalKey>` to land several reports in one run (with `PLUNE_SHARED_RUN=1` — see below), `--create` to offer unmatched tests to the review queue, `--fail-on-missing-attachments` to exit 1 when a file a JUnit report names with `[[ATTACHMENT\|path]]`, or an Allure result names as a `source`, is not there (the results are sent either way). |
|
|
122
|
+
| `plune sync` | Upload the latest local run to the platform. Flags: `--file <path>` to send a specific run JSON, `--fail-on-failures` to read the run's summary back once after the upload and exit with [the gate](#a-release-gate---fail-on-failures)'s code. |
|
|
123
|
+
| `plune run import <file>` | Turn a **JUnit XML** or **Playwright JSON** report, or an **Allure `allure-results` folder**, into a run in Plune. Needs no provider key — nothing is generated. Flags: `--format junit\|playwright-json\|allure` (detected from the file when omitted; a folder is Allure), `--key <externalKey>` to land several reports in one run (with `PLUNE_SHARED_RUN=1` — see below), `--create` to offer unmatched tests to the review queue, `--fail-on-missing-attachments` to exit 1 when a file a JUnit report names with `[[ATTACHMENT\|path]]`, or an Allure result names as a `source`, is not there (the results are sent either way), `--fail-on-failures` to exit with [the gate](#a-release-gate---fail-on-failures)'s code on the summary the platform froze when this import finished the run. |
|
|
124
124
|
| `plune run start` | Open a platform run — or join the one already carrying `--key` — and print its id. Flags: `--key <externalKey>` (generated when absent), `--json` for one machine-readable line. |
|
|
125
|
-
| `plune run finish <id>` | Close a platform run. This is what the reporter tells you to do for a run it had to leave open. Flags: `--terminate` to record it as cut short, `--reason <text
|
|
126
|
-
| `plune run exec -- <command>` | Open a run, run the command inside it, close the run — and exit with whatever the command returned. Sets `PLUNE_SHARED_RUN` for you, so anything reporting inside joins that run. Flags: `--key <externalKey
|
|
125
|
+
| `plune run finish <id>` | Close a platform run. This is what the reporter tells you to do for a run it had to leave open. Flags: `--terminate` to record it as cut short, `--reason <text>`, `--fail-on-failures` to exit with [the gate](#a-release-gate---fail-on-failures)'s code on the summary the platform froze. |
|
|
126
|
+
| `plune run exec -- <command>` | Open a run, run the command inside it, close the run — and exit with whatever the command returned. Sets `PLUNE_SHARED_RUN` for you, so anything reporting inside joins that run. Flags: `--key <externalKey>`, `--fail-on-failures` to exit with [the gate](#a-release-gate---fail-on-failures)'s code instead — or with the command's own when no result reached the run. |
|
|
127
127
|
| `plune run delete <id>` | Delete a run and everything it produced — its results and the review-queue entries it raised. Approved test cases and the audit log stay. **Recoverable for 30 days** (an owner of the project can put it back from Settings → Bin), then gone for good. No prompt: it is your data, and this command belongs in scripts. |
|
|
128
128
|
| `plune run report` | Replay `.plune/pending-results.jsonl` — send what the reporter could not. The lines of one reporter run go back as one run, with the commit and branch the reporter recorded. Once every result has gone the file is renamed to `pending-results.<time>.sent.jsonl`; if anything did not go, it stays as it was. Flags: `--file <path>`. |
|
|
129
129
|
| `plune ingest [dir]` | Record a [Cairn](https://github.com/plune-ai/cairn) run in Plune. Omit `[dir]` for the newest run under `./runs`, or name the directory holding `report.json`. Generated cases arrive as **review proposals** — nothing is created until a person approves it. |
|
|
@@ -135,6 +135,24 @@ Global flags: `-c, --config <path>` · `-v, --verbose` · `--no-color`.
|
|
|
135
135
|
|
|
136
136
|
**Exit codes:** `0` everything passed · `1` an assertion failed · `2` configuration or execution error · `3` `pull` refused to overwrite uncommitted changes · `4` `push` had refusals.
|
|
137
137
|
|
|
138
|
+
### A release gate: `--fail-on-failures`
|
|
139
|
+
|
|
140
|
+
`plune run finish`, `plune run exec`, `plune run import` and `plune sync` take `--fail-on-failures`. The exit
|
|
141
|
+
code is then the platform's verdict on the run, not the runner's: the summary the platform froze at `finish`
|
|
142
|
+
(for `sync`, one `GET /v1/runs/:id/summary` after the upload). The platform counts a muted case's failure under
|
|
143
|
+
`muted`, so a mute set in Plune is honoured without the CLI knowing about it.
|
|
144
|
+
|
|
145
|
+
| The run | Exit |
|
|
146
|
+
| ------- | ---- |
|
|
147
|
+
| a `failed` or `broken` result that is not muted | `1` |
|
|
148
|
+
| an expected case that never reported (`notRun`): a dead shard is not green | `1` |
|
|
149
|
+
| only `passed`, `blocked`, `skipped` and muted failures | `0` |
|
|
150
|
+
| no verdict: the platform could not be reached, the finish was refused, the run was left open or terminated | `1` |
|
|
151
|
+
| `run exec` where no result reached the run and nothing was expected of it | the command's own code |
|
|
152
|
+
|
|
153
|
+
Without the flag every command exits as it always did. `plune run` (local evals) is not affected: it stays the
|
|
154
|
+
raw local signal and works offline.
|
|
155
|
+
|
|
138
156
|
## Already running tests? Bring the results in
|
|
139
157
|
|
|
140
158
|
Everything above generates checks, which is why it needs a provider key. If you already have a
|
package/dist/cli.cjs
CHANGED
|
@@ -3306,21 +3306,26 @@ async function startRun(passed, declared2) {
|
|
|
3306
3306
|
},
|
|
3307
3307
|
flush,
|
|
3308
3308
|
async finish(reason) {
|
|
3309
|
-
if (done) return;
|
|
3309
|
+
if (done) return void 0;
|
|
3310
3310
|
done = true;
|
|
3311
3311
|
await flush();
|
|
3312
3312
|
await offer();
|
|
3313
|
+
let frozen;
|
|
3313
3314
|
if (runId !== null && !offline) {
|
|
3314
3315
|
const out = await client.post(`/v1/runs/${runId}/events`, {
|
|
3315
3316
|
event: "finish",
|
|
3316
3317
|
...reason !== void 0 ? { reason } : {}
|
|
3317
3318
|
});
|
|
3318
3319
|
if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
|
|
3319
|
-
else
|
|
3320
|
-
|
|
3320
|
+
else {
|
|
3321
|
+
frozen = out.body?.summary ?? void 0;
|
|
3322
|
+
if (typeof out.body?.detached === "number" && out.body.detached > 0) {
|
|
3323
|
+
log(`plune: ${out.body.detached} test case(s) marked detached \u2014 this full run no longer reports them.`);
|
|
3324
|
+
}
|
|
3321
3325
|
}
|
|
3322
3326
|
}
|
|
3323
3327
|
summarise();
|
|
3328
|
+
return frozen;
|
|
3324
3329
|
},
|
|
3325
3330
|
async leaveOpen() {
|
|
3326
3331
|
if (done) return;
|
|
@@ -3398,6 +3403,28 @@ var init_session = __esm({
|
|
|
3398
3403
|
}
|
|
3399
3404
|
});
|
|
3400
3405
|
|
|
3406
|
+
// src/cli/gate.ts
|
|
3407
|
+
function gateExit(verdict, runnerCode) {
|
|
3408
|
+
if (verdict?.statusEvent === "terminated") {
|
|
3409
|
+
return { code: 1, line: "plune: gate failed \u2014 the run was terminated, so it has no verdict to pass on." };
|
|
3410
|
+
}
|
|
3411
|
+
const counts = verdict?.counts;
|
|
3412
|
+
if (counts == null) return { code: 1, line: "plune: gate failed \u2014 no verdict from the platform, and no verdict is no pass." };
|
|
3413
|
+
const n = (status) => counts[status] ?? 0;
|
|
3414
|
+
const notRun = verdict?.notRun?.length ?? 0;
|
|
3415
|
+
if (runnerCode !== void 0 && notRun === 0 && Object.values(counts).every((k) => !k)) {
|
|
3416
|
+
return { code: runnerCode, line: `plune: gate \u2014 no result reached the run, so the exit code is the command's own (${runnerCode}).` };
|
|
3417
|
+
}
|
|
3418
|
+
const said = `${n("failed")} failed \xB7 ${n("broken")} broken \xB7 ${notRun} not run \xB7 ${n("muted")} muted`;
|
|
3419
|
+
return n("failed") + n("broken") > 0 || notRun > 0 ? { code: 1, line: `plune: gate failed \u2014 ${said}` } : { code: 0, line: `plune: gate passed \u2014 ${said}` };
|
|
3420
|
+
}
|
|
3421
|
+
var init_gate = __esm({
|
|
3422
|
+
"src/cli/gate.ts"() {
|
|
3423
|
+
"use strict";
|
|
3424
|
+
init_cjs_shims();
|
|
3425
|
+
}
|
|
3426
|
+
});
|
|
3427
|
+
|
|
3401
3428
|
// src/cli/commands/run-lifecycle.ts
|
|
3402
3429
|
var run_lifecycle_exports = {};
|
|
3403
3430
|
__export(run_lifecycle_exports, {
|
|
@@ -3455,7 +3482,7 @@ async function handleRunStart(options = {}) {
|
|
|
3455
3482
|
}
|
|
3456
3483
|
return result;
|
|
3457
3484
|
}
|
|
3458
|
-
async function handleRunFinish(id, options = {}) {
|
|
3485
|
+
async function handleRunFinish(id, options = {}, runnerCode) {
|
|
3459
3486
|
const { client, write } = connect(options);
|
|
3460
3487
|
const event = options.terminate === true ? "terminate" : "finish";
|
|
3461
3488
|
const out = await client.post(`/v1/runs/${id}/events`, {
|
|
@@ -3468,6 +3495,11 @@ async function handleRunFinish(id, options = {}) {
|
|
|
3468
3495
|
);
|
|
3469
3496
|
}
|
|
3470
3497
|
write(`Run ${id} ${event === "finish" ? "finished" : "terminated"}.`);
|
|
3498
|
+
if (options.failOnFailures !== true) return runnerCode ?? 0;
|
|
3499
|
+
const summary = out.body?.summary;
|
|
3500
|
+
const gate = gateExit({ statusEvent: out.body?.run?.statusEvent, counts: summary?.counts, notRun: summary?.notRun }, runnerCode);
|
|
3501
|
+
write(gate.line);
|
|
3502
|
+
return gate.code;
|
|
3471
3503
|
}
|
|
3472
3504
|
async function handleRunDelete(id, options = {}) {
|
|
3473
3505
|
const { client, write } = connect(options);
|
|
@@ -3499,8 +3531,7 @@ async function handleRunExec(options) {
|
|
|
3499
3531
|
child.on("close", (exitCode) => resolve9(exitCode ?? 1));
|
|
3500
3532
|
child.on("error", () => resolve9(1));
|
|
3501
3533
|
});
|
|
3502
|
-
|
|
3503
|
-
return code;
|
|
3534
|
+
return handleRunFinish(run.id, options, code);
|
|
3504
3535
|
}
|
|
3505
3536
|
async function handleRunReport(options = {}) {
|
|
3506
3537
|
const file = options.file ?? DEFAULT_FALLBACK_PATH;
|
|
@@ -3596,6 +3627,7 @@ var init_run_lifecycle = __esm({
|
|
|
3596
3627
|
init_env2();
|
|
3597
3628
|
init_fallback();
|
|
3598
3629
|
init_session();
|
|
3630
|
+
init_gate();
|
|
3599
3631
|
NoTokenError = class extends Error {
|
|
3600
3632
|
constructor() {
|
|
3601
3633
|
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
@@ -4469,8 +4501,9 @@ async function handleRunImport(options) {
|
|
|
4469
4501
|
results.map((r) => r.keys)
|
|
4470
4502
|
);
|
|
4471
4503
|
for (const result2 of results) await session.add(result2);
|
|
4504
|
+
let frozen;
|
|
4472
4505
|
if (env.keepOpen) await session.leaveOpen();
|
|
4473
|
-
else await session.finish();
|
|
4506
|
+
else frozen = await session.finish();
|
|
4474
4507
|
const stats = session.stats;
|
|
4475
4508
|
const result = {
|
|
4476
4509
|
runId: session.runId,
|
|
@@ -4499,6 +4532,14 @@ async function handleRunImport(options) {
|
|
|
4499
4532
|
offering: options.create === true
|
|
4500
4533
|
};
|
|
4501
4534
|
for (const line of describe(result, apiUrl)) write(line);
|
|
4535
|
+
if (options.failOnFailures === true) {
|
|
4536
|
+
if (env.keepOpen) {
|
|
4537
|
+
write("plune: the run is left open for the jobs still reporting, so there is no verdict yet \u2014 gate on the step that finishes it.");
|
|
4538
|
+
}
|
|
4539
|
+
const gate = gateExit(frozen);
|
|
4540
|
+
write(gate.line);
|
|
4541
|
+
result.gate = gate.code;
|
|
4542
|
+
}
|
|
4502
4543
|
return result;
|
|
4503
4544
|
}
|
|
4504
4545
|
function runUrl2(apiUrl, id) {
|
|
@@ -4565,6 +4606,7 @@ var init_run_import = __esm({
|
|
|
4565
4606
|
init_run_lifecycle();
|
|
4566
4607
|
init_api_url();
|
|
4567
4608
|
init_credentials();
|
|
4609
|
+
init_gate();
|
|
4568
4610
|
init_env2();
|
|
4569
4611
|
NEXT = {
|
|
4570
4612
|
queue: "the review queue is full. Approve or reject what is waiting, then import this report again to offer the rest.",
|
|
@@ -5220,7 +5262,18 @@ async function handleSync(deps = {}) {
|
|
|
5220
5262
|
}
|
|
5221
5263
|
const body = await res.json().catch(() => ({}));
|
|
5222
5264
|
const id = typeof body.id === "string" ? body.id : "";
|
|
5223
|
-
|
|
5265
|
+
const result = { id, url: `${endpoint}/${id}` };
|
|
5266
|
+
if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
|
|
5267
|
+
return result;
|
|
5268
|
+
}
|
|
5269
|
+
async function readVerdict(doFetch, url, token) {
|
|
5270
|
+
try {
|
|
5271
|
+
const res = await doFetch(url, { method: "GET", headers: { authorization: `Bearer ${token}` } });
|
|
5272
|
+
const body = await res.json().catch(() => void 0);
|
|
5273
|
+
return res.ok ? { statusEvent: body?.run?.statusEvent, counts: body?.counts, notRun: body?.notRun } : void 0;
|
|
5274
|
+
} catch {
|
|
5275
|
+
return void 0;
|
|
5276
|
+
}
|
|
5224
5277
|
}
|
|
5225
5278
|
function reportSyncFailure(err, write) {
|
|
5226
5279
|
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError2 || err instanceof SyncFileError) {
|
|
@@ -5243,6 +5296,7 @@ var init_sync = __esm({
|
|
|
5243
5296
|
init_run_result_schema();
|
|
5244
5297
|
init_api_url();
|
|
5245
5298
|
init_credentials();
|
|
5299
|
+
init_gate();
|
|
5246
5300
|
NotLoggedInError = class extends Error {
|
|
5247
5301
|
constructor() {
|
|
5248
5302
|
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
@@ -5314,7 +5368,8 @@ async function defaultReadEvidence(dir) {
|
|
|
5314
5368
|
if (!Array.isArray(raw)) return void 0;
|
|
5315
5369
|
return raw.map((r) => {
|
|
5316
5370
|
const rec = asRecord(r);
|
|
5317
|
-
|
|
5371
|
+
const error = str(rec["error"]);
|
|
5372
|
+
return { name: str(rec["name"]), passed: rec["passed"] === true, ...error !== "" ? { error } : {} };
|
|
5318
5373
|
});
|
|
5319
5374
|
}
|
|
5320
5375
|
function casesFromDesign(report) {
|
|
@@ -5373,7 +5428,7 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
5373
5428
|
const verdicts = /* @__PURE__ */ new Map();
|
|
5374
5429
|
for (const e of evidence ?? []) {
|
|
5375
5430
|
if (verdicts.has(e.name)) throw new CairnEvidenceAmbiguousError(e.name);
|
|
5376
|
-
verdicts.set(e.name, e.passed ? "passed" : "failed");
|
|
5431
|
+
verdicts.set(e.name, e.error !== void 0 ? "blocked" : e.passed ? "passed" : "failed");
|
|
5377
5432
|
}
|
|
5378
5433
|
cases = casesFromApi(report, verdicts);
|
|
5379
5434
|
} else {
|
|
@@ -5907,6 +5962,7 @@ function createProgram() {
|
|
|
5907
5962
|
process.exit(1);
|
|
5908
5963
|
};
|
|
5909
5964
|
const verboseOf = (command) => command.optsWithGlobals().verbose === true;
|
|
5965
|
+
const FAIL_ON_FAILURES = "Exit with the platform's verdict on the run: 1 on a failed or broken result (muted ones do not count), on an expected case that never ran, or when there is no verdict at all";
|
|
5910
5966
|
runCommand.command("start").description("Open a platform run (or join one already carrying the key) and print its id").option("--key <externalKey>", "The key several processes share; generated when absent").option("--json", "Print one machine-readable line instead of prose", false).action(async (options, command) => {
|
|
5911
5967
|
try {
|
|
5912
5968
|
const { handleRunStart: handleRunStart2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
@@ -5918,14 +5974,16 @@ function createProgram() {
|
|
|
5918
5974
|
await failRunCommand(err, verboseOf(command));
|
|
5919
5975
|
}
|
|
5920
5976
|
});
|
|
5921
|
-
runCommand.command("finish").argument("<id>", "The run to close").description("Close a platform run \u2014 what the reporter tells you to do for a run left open").option("--terminate", "Record the run as cut short rather than finished", false).option("--reason <text>", "Why, for a terminated run").action(
|
|
5977
|
+
runCommand.command("finish").argument("<id>", "The run to close").description("Close a platform run \u2014 what the reporter tells you to do for a run left open").option("--terminate", "Record the run as cut short rather than finished", false).option("--reason <text>", "Why, for a terminated run").option("--fail-on-failures", FAIL_ON_FAILURES, false).action(
|
|
5922
5978
|
async (id, options, command) => {
|
|
5923
5979
|
try {
|
|
5924
5980
|
const { handleRunFinish: handleRunFinish2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
5925
|
-
await handleRunFinish2(id, {
|
|
5981
|
+
const code = await handleRunFinish2(id, {
|
|
5926
5982
|
terminate: options.terminate,
|
|
5927
|
-
...options.reason !== void 0 ? { reason: options.reason } : {}
|
|
5983
|
+
...options.reason !== void 0 ? { reason: options.reason } : {},
|
|
5984
|
+
...options.failOnFailures ? { failOnFailures: true } : {}
|
|
5928
5985
|
});
|
|
5986
|
+
if (options.failOnFailures) process.exitCode = code;
|
|
5929
5987
|
} catch (err) {
|
|
5930
5988
|
await failRunCommand(err, verboseOf(command));
|
|
5931
5989
|
}
|
|
@@ -5933,12 +5991,13 @@ function createProgram() {
|
|
|
5933
5991
|
);
|
|
5934
5992
|
runCommand.command("exec").argument("<command...>", "The command to run inside the run, after `--`").description(
|
|
5935
5993
|
"Open a run, run a command inside it, close the run \u2014 whatever the command returns"
|
|
5936
|
-
).option("--key <externalKey>", "The key several processes share; generated when absent").action(async (argv, options, command) => {
|
|
5994
|
+
).option("--key <externalKey>", "The key several processes share; generated when absent").option("--fail-on-failures", `${FAIL_ON_FAILURES}; the command's own exit code when no result reached the run`, false).action(async (argv, options, command) => {
|
|
5937
5995
|
try {
|
|
5938
5996
|
const { handleRunExec: handleRunExec2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
5939
5997
|
process.exitCode = await handleRunExec2({
|
|
5940
5998
|
argv,
|
|
5941
|
-
...options.key !== void 0 ? { key: options.key } : {}
|
|
5999
|
+
...options.key !== void 0 ? { key: options.key } : {},
|
|
6000
|
+
...options.failOnFailures ? { failOnFailures: true } : {}
|
|
5942
6001
|
});
|
|
5943
6002
|
} catch (err) {
|
|
5944
6003
|
await failRunCommand(err, verboseOf(command));
|
|
@@ -5967,7 +6026,7 @@ function createProgram() {
|
|
|
5967
6026
|
"--fail-on-missing-attachments",
|
|
5968
6027
|
"Exit 1 when a file the report names ([[ATTACHMENT|path]] in JUnit, a source in allure-results) is not there; the results are sent either way",
|
|
5969
6028
|
false
|
|
5970
|
-
).action(
|
|
6029
|
+
).option("--fail-on-failures", FAIL_ON_FAILURES, false).action(
|
|
5971
6030
|
async (file, options, command) => {
|
|
5972
6031
|
const [
|
|
5973
6032
|
{ handleRunImport: handleRunImport2 },
|
|
@@ -5991,9 +6050,11 @@ function createProgram() {
|
|
|
5991
6050
|
file,
|
|
5992
6051
|
...format !== void 0 ? { format } : {},
|
|
5993
6052
|
...options.key !== void 0 ? { key: options.key } : {},
|
|
5994
|
-
...options.create ? { create: true } : {}
|
|
6053
|
+
...options.create ? { create: true } : {},
|
|
6054
|
+
...options.failOnFailures ? { failOnFailures: true } : {}
|
|
5995
6055
|
});
|
|
5996
6056
|
if (options.failOnMissingAttachments && imported.missingAttachments.length > 0) process.exitCode = 1;
|
|
6057
|
+
if (imported.gate === 1) process.exitCode = 1;
|
|
5997
6058
|
} catch (err) {
|
|
5998
6059
|
if (err instanceof UnknownFormatError2 || err instanceof XmlParseError2 || err instanceof JsonReportError2 || err instanceof AllureReportError2) {
|
|
5999
6060
|
process.stderr.write(err.message + "\n");
|
|
@@ -6103,18 +6164,24 @@ function createProgram() {
|
|
|
6103
6164
|
failUnexpected(err, verbose);
|
|
6104
6165
|
}
|
|
6105
6166
|
});
|
|
6106
|
-
program.command("sync").description("Upload your latest local run to the Plune platform (POST /v1/runs)").option("--file <path>", "Sync a specific run JSON instead of .plune/last-run.json").action(async (options, command) => {
|
|
6167
|
+
program.command("sync").description("Upload your latest local run to the Plune platform (POST /v1/runs)").option("--file <path>", "Sync a specific run JSON instead of .plune/last-run.json").option("--fail-on-failures", `${FAIL_ON_FAILURES}; read with one GET of the run's summary after the upload`, false).action(async (options, command) => {
|
|
6107
6168
|
const globals = command.optsWithGlobals();
|
|
6108
6169
|
const verbose = globals.verbose === true;
|
|
6109
6170
|
const { handleSync: handleSync2, reportSyncFailure: reportSyncFailure2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
6110
6171
|
const cwd = globals.config !== void 0 ? (0, import_node_path4.dirname)((0, import_node_path4.resolve)(globals.config)) : process.cwd();
|
|
6111
6172
|
try {
|
|
6112
|
-
const { id, url } = await handleSync2({
|
|
6173
|
+
const { id, url, gate } = await handleSync2({
|
|
6113
6174
|
cwd,
|
|
6114
|
-
...options.file !== void 0 ? { file: options.file } : {}
|
|
6175
|
+
...options.file !== void 0 ? { file: options.file } : {},
|
|
6176
|
+
...options.failOnFailures ? { failOnFailures: true } : {}
|
|
6115
6177
|
});
|
|
6116
6178
|
process.stdout.write(`Synced run ${id} \u2192 ${url}
|
|
6117
6179
|
`);
|
|
6180
|
+
if (gate !== void 0) {
|
|
6181
|
+
process.stdout.write(`${gate.line}
|
|
6182
|
+
`);
|
|
6183
|
+
process.exitCode = gate.code;
|
|
6184
|
+
}
|
|
6118
6185
|
} catch (err) {
|
|
6119
6186
|
const code = reportSyncFailure2(err, (s) => process.stderr.write(s));
|
|
6120
6187
|
if (code !== null) {
|
package/dist/index.cjs
CHANGED
|
@@ -1703,6 +1703,22 @@ function loadToken() {
|
|
|
1703
1703
|
}
|
|
1704
1704
|
}
|
|
1705
1705
|
|
|
1706
|
+
// src/cli/gate.ts
|
|
1707
|
+
function gateExit(verdict, runnerCode) {
|
|
1708
|
+
if (verdict?.statusEvent === "terminated") {
|
|
1709
|
+
return { code: 1, line: "plune: gate failed \u2014 the run was terminated, so it has no verdict to pass on." };
|
|
1710
|
+
}
|
|
1711
|
+
const counts = verdict?.counts;
|
|
1712
|
+
if (counts == null) return { code: 1, line: "plune: gate failed \u2014 no verdict from the platform, and no verdict is no pass." };
|
|
1713
|
+
const n = (status) => counts[status] ?? 0;
|
|
1714
|
+
const notRun = verdict?.notRun?.length ?? 0;
|
|
1715
|
+
if (runnerCode !== void 0 && notRun === 0 && Object.values(counts).every((k) => !k)) {
|
|
1716
|
+
return { code: runnerCode, line: `plune: gate \u2014 no result reached the run, so the exit code is the command's own (${runnerCode}).` };
|
|
1717
|
+
}
|
|
1718
|
+
const said = `${n("failed")} failed \xB7 ${n("broken")} broken \xB7 ${notRun} not run \xB7 ${n("muted")} muted`;
|
|
1719
|
+
return n("failed") + n("broken") > 0 || notRun > 0 ? { code: 1, line: `plune: gate failed \u2014 ${said}` } : { code: 0, line: `plune: gate passed \u2014 ${said}` };
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1706
1722
|
// src/cli/commands/sync.ts
|
|
1707
1723
|
var NotLoggedInError = class extends Error {
|
|
1708
1724
|
constructor() {
|
|
@@ -1795,7 +1811,18 @@ async function handleSync(deps = {}) {
|
|
|
1795
1811
|
}
|
|
1796
1812
|
const body = await res.json().catch(() => ({}));
|
|
1797
1813
|
const id = typeof body.id === "string" ? body.id : "";
|
|
1798
|
-
|
|
1814
|
+
const result = { id, url: `${endpoint}/${id}` };
|
|
1815
|
+
if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
|
|
1816
|
+
return result;
|
|
1817
|
+
}
|
|
1818
|
+
async function readVerdict(doFetch, url, token) {
|
|
1819
|
+
try {
|
|
1820
|
+
const res = await doFetch(url, { method: "GET", headers: { authorization: `Bearer ${token}` } });
|
|
1821
|
+
const body = await res.json().catch(() => void 0);
|
|
1822
|
+
return res.ok ? { statusEvent: body?.run?.statusEvent, counts: body?.counts, notRun: body?.notRun } : void 0;
|
|
1823
|
+
} catch {
|
|
1824
|
+
return void 0;
|
|
1825
|
+
}
|
|
1799
1826
|
}
|
|
1800
1827
|
function reportSyncFailure(err, write) {
|
|
1801
1828
|
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
|
|
@@ -1883,7 +1910,8 @@ async function defaultReadEvidence(dir) {
|
|
|
1883
1910
|
if (!Array.isArray(raw)) return void 0;
|
|
1884
1911
|
return raw.map((r) => {
|
|
1885
1912
|
const rec = asRecord(r);
|
|
1886
|
-
|
|
1913
|
+
const error = str(rec["error"]);
|
|
1914
|
+
return { name: str(rec["name"]), passed: rec["passed"] === true, ...error !== "" ? { error } : {} };
|
|
1887
1915
|
});
|
|
1888
1916
|
}
|
|
1889
1917
|
function casesFromDesign(report) {
|
|
@@ -1942,7 +1970,7 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
1942
1970
|
const verdicts = /* @__PURE__ */ new Map();
|
|
1943
1971
|
for (const e of evidence ?? []) {
|
|
1944
1972
|
if (verdicts.has(e.name)) throw new CairnEvidenceAmbiguousError(e.name);
|
|
1945
|
-
verdicts.set(e.name, e.passed ? "passed" : "failed");
|
|
1973
|
+
verdicts.set(e.name, e.error !== void 0 ? "blocked" : e.passed ? "passed" : "failed");
|
|
1946
1974
|
}
|
|
1947
1975
|
cases = casesFromApi(report, verdicts);
|
|
1948
1976
|
} else {
|
package/dist/index.d.cts
CHANGED
|
@@ -315,12 +315,19 @@ interface SyncDeps {
|
|
|
315
315
|
loadToken?: () => string | null;
|
|
316
316
|
/** HTTP transport (injected in tests — e.g. a real Hono app's `request`). Defaults to global `fetch`. */
|
|
317
317
|
fetchImpl?: typeof fetch;
|
|
318
|
+
/** `--fail-on-failures`: read the run's summary back once after the upload and judge it (plune#1247). */
|
|
319
|
+
failOnFailures?: boolean;
|
|
318
320
|
}
|
|
319
321
|
interface SyncResult {
|
|
320
322
|
/** Server-assigned run id. */
|
|
321
323
|
id: string;
|
|
322
324
|
/** The `GET /v1/runs/:id` URL where the uploaded run can be read back. */
|
|
323
325
|
url: string;
|
|
326
|
+
/** The exit code `--fail-on-failures` asks for and the line that says why; absent without the flag. */
|
|
327
|
+
gate?: {
|
|
328
|
+
code: number;
|
|
329
|
+
line: string;
|
|
330
|
+
};
|
|
324
331
|
}
|
|
325
332
|
/**
|
|
326
333
|
* Upload the latest (or `--file`) local run to `POST {apiUrl}/v1/runs` with the stored Bearer token.
|
|
@@ -645,7 +652,12 @@ declare function formatIngestResult(r: IngestResult): string;
|
|
|
645
652
|
declare const INGEST_SCHEMA_VERSION = 1;
|
|
646
653
|
type CairnRunMode = 'explore' | 'design' | 'api';
|
|
647
654
|
type CairnExecution = 'auto' | 'manual';
|
|
648
|
-
|
|
655
|
+
/**
|
|
656
|
+
* `blocked` (plune-ai/plune#1327): the case's request got no answer — Cairn's evidence records a transport error
|
|
657
|
+
* after the retries — so the product never said anything either way. Sent as `failed`, it blamed the product for an
|
|
658
|
+
* unreachable target. Needs a platform that accepts it (plune-ai/plune#1330); an older one refuses the payload.
|
|
659
|
+
*/
|
|
660
|
+
type CairnVerdict = 'passed' | 'failed' | 'blocked';
|
|
649
661
|
interface CairnIngestCase {
|
|
650
662
|
stableId: string;
|
|
651
663
|
title: string;
|
|
@@ -669,6 +681,8 @@ interface CairnIngestPayload {
|
|
|
669
681
|
interface EvidenceRecord {
|
|
670
682
|
name: string;
|
|
671
683
|
passed: boolean;
|
|
684
|
+
/** Cairn's transport error, when the request got no response after the retries. */
|
|
685
|
+
error?: string;
|
|
672
686
|
}
|
|
673
687
|
interface ReadCairnRunDeps {
|
|
674
688
|
/** Injected for tests; returns `undefined` when the run executed nothing and wrote no evidence. */
|
package/dist/index.d.ts
CHANGED
|
@@ -315,12 +315,19 @@ interface SyncDeps {
|
|
|
315
315
|
loadToken?: () => string | null;
|
|
316
316
|
/** HTTP transport (injected in tests — e.g. a real Hono app's `request`). Defaults to global `fetch`. */
|
|
317
317
|
fetchImpl?: typeof fetch;
|
|
318
|
+
/** `--fail-on-failures`: read the run's summary back once after the upload and judge it (plune#1247). */
|
|
319
|
+
failOnFailures?: boolean;
|
|
318
320
|
}
|
|
319
321
|
interface SyncResult {
|
|
320
322
|
/** Server-assigned run id. */
|
|
321
323
|
id: string;
|
|
322
324
|
/** The `GET /v1/runs/:id` URL where the uploaded run can be read back. */
|
|
323
325
|
url: string;
|
|
326
|
+
/** The exit code `--fail-on-failures` asks for and the line that says why; absent without the flag. */
|
|
327
|
+
gate?: {
|
|
328
|
+
code: number;
|
|
329
|
+
line: string;
|
|
330
|
+
};
|
|
324
331
|
}
|
|
325
332
|
/**
|
|
326
333
|
* Upload the latest (or `--file`) local run to `POST {apiUrl}/v1/runs` with the stored Bearer token.
|
|
@@ -645,7 +652,12 @@ declare function formatIngestResult(r: IngestResult): string;
|
|
|
645
652
|
declare const INGEST_SCHEMA_VERSION = 1;
|
|
646
653
|
type CairnRunMode = 'explore' | 'design' | 'api';
|
|
647
654
|
type CairnExecution = 'auto' | 'manual';
|
|
648
|
-
|
|
655
|
+
/**
|
|
656
|
+
* `blocked` (plune-ai/plune#1327): the case's request got no answer — Cairn's evidence records a transport error
|
|
657
|
+
* after the retries — so the product never said anything either way. Sent as `failed`, it blamed the product for an
|
|
658
|
+
* unreachable target. Needs a platform that accepts it (plune-ai/plune#1330); an older one refuses the payload.
|
|
659
|
+
*/
|
|
660
|
+
type CairnVerdict = 'passed' | 'failed' | 'blocked';
|
|
649
661
|
interface CairnIngestCase {
|
|
650
662
|
stableId: string;
|
|
651
663
|
title: string;
|
|
@@ -669,6 +681,8 @@ interface CairnIngestPayload {
|
|
|
669
681
|
interface EvidenceRecord {
|
|
670
682
|
name: string;
|
|
671
683
|
passed: boolean;
|
|
684
|
+
/** Cairn's transport error, when the request got no response after the retries. */
|
|
685
|
+
error?: string;
|
|
672
686
|
}
|
|
673
687
|
interface ReadCairnRunDeps {
|
|
674
688
|
/** Injected for tests; returns `undefined` when the run executed nothing and wrote no evidence. */
|
package/dist/index.js
CHANGED
|
@@ -1647,6 +1647,22 @@ function loadToken() {
|
|
|
1647
1647
|
}
|
|
1648
1648
|
}
|
|
1649
1649
|
|
|
1650
|
+
// src/cli/gate.ts
|
|
1651
|
+
function gateExit(verdict, runnerCode) {
|
|
1652
|
+
if (verdict?.statusEvent === "terminated") {
|
|
1653
|
+
return { code: 1, line: "plune: gate failed \u2014 the run was terminated, so it has no verdict to pass on." };
|
|
1654
|
+
}
|
|
1655
|
+
const counts = verdict?.counts;
|
|
1656
|
+
if (counts == null) return { code: 1, line: "plune: gate failed \u2014 no verdict from the platform, and no verdict is no pass." };
|
|
1657
|
+
const n = (status) => counts[status] ?? 0;
|
|
1658
|
+
const notRun = verdict?.notRun?.length ?? 0;
|
|
1659
|
+
if (runnerCode !== void 0 && notRun === 0 && Object.values(counts).every((k) => !k)) {
|
|
1660
|
+
return { code: runnerCode, line: `plune: gate \u2014 no result reached the run, so the exit code is the command's own (${runnerCode}).` };
|
|
1661
|
+
}
|
|
1662
|
+
const said = `${n("failed")} failed \xB7 ${n("broken")} broken \xB7 ${notRun} not run \xB7 ${n("muted")} muted`;
|
|
1663
|
+
return n("failed") + n("broken") > 0 || notRun > 0 ? { code: 1, line: `plune: gate failed \u2014 ${said}` } : { code: 0, line: `plune: gate passed \u2014 ${said}` };
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1650
1666
|
// src/cli/commands/sync.ts
|
|
1651
1667
|
var NotLoggedInError = class extends Error {
|
|
1652
1668
|
constructor() {
|
|
@@ -1739,7 +1755,18 @@ async function handleSync(deps = {}) {
|
|
|
1739
1755
|
}
|
|
1740
1756
|
const body = await res.json().catch(() => ({}));
|
|
1741
1757
|
const id = typeof body.id === "string" ? body.id : "";
|
|
1742
|
-
|
|
1758
|
+
const result = { id, url: `${endpoint}/${id}` };
|
|
1759
|
+
if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
|
|
1760
|
+
return result;
|
|
1761
|
+
}
|
|
1762
|
+
async function readVerdict(doFetch, url, token) {
|
|
1763
|
+
try {
|
|
1764
|
+
const res = await doFetch(url, { method: "GET", headers: { authorization: `Bearer ${token}` } });
|
|
1765
|
+
const body = await res.json().catch(() => void 0);
|
|
1766
|
+
return res.ok ? { statusEvent: body?.run?.statusEvent, counts: body?.counts, notRun: body?.notRun } : void 0;
|
|
1767
|
+
} catch {
|
|
1768
|
+
return void 0;
|
|
1769
|
+
}
|
|
1743
1770
|
}
|
|
1744
1771
|
function reportSyncFailure(err, write) {
|
|
1745
1772
|
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError || err instanceof SyncFileError) {
|
|
@@ -1827,7 +1854,8 @@ async function defaultReadEvidence(dir) {
|
|
|
1827
1854
|
if (!Array.isArray(raw)) return void 0;
|
|
1828
1855
|
return raw.map((r) => {
|
|
1829
1856
|
const rec = asRecord(r);
|
|
1830
|
-
|
|
1857
|
+
const error = str(rec["error"]);
|
|
1858
|
+
return { name: str(rec["name"]), passed: rec["passed"] === true, ...error !== "" ? { error } : {} };
|
|
1831
1859
|
});
|
|
1832
1860
|
}
|
|
1833
1861
|
function casesFromDesign(report) {
|
|
@@ -1886,7 +1914,7 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
1886
1914
|
const verdicts = /* @__PURE__ */ new Map();
|
|
1887
1915
|
for (const e of evidence ?? []) {
|
|
1888
1916
|
if (verdicts.has(e.name)) throw new CairnEvidenceAmbiguousError(e.name);
|
|
1889
|
-
verdicts.set(e.name, e.passed ? "passed" : "failed");
|
|
1917
|
+
verdicts.set(e.name, e.error !== void 0 ? "blocked" : e.passed ? "passed" : "failed");
|
|
1890
1918
|
}
|
|
1891
1919
|
cases = casesFromApi(report, verdicts);
|
|
1892
1920
|
} else {
|
package/dist/reporter-core.cjs
CHANGED
|
@@ -1100,21 +1100,26 @@ async function startRun(passed, declared) {
|
|
|
1100
1100
|
},
|
|
1101
1101
|
flush,
|
|
1102
1102
|
async finish(reason) {
|
|
1103
|
-
if (done) return;
|
|
1103
|
+
if (done) return void 0;
|
|
1104
1104
|
done = true;
|
|
1105
1105
|
await flush();
|
|
1106
1106
|
await offer();
|
|
1107
|
+
let frozen;
|
|
1107
1108
|
if (runId !== null && !offline) {
|
|
1108
1109
|
const out = await client.post(`/v1/runs/${runId}/events`, {
|
|
1109
1110
|
event: "finish",
|
|
1110
1111
|
...reason !== void 0 ? { reason } : {}
|
|
1111
1112
|
});
|
|
1112
1113
|
if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
|
|
1113
|
-
else
|
|
1114
|
-
|
|
1114
|
+
else {
|
|
1115
|
+
frozen = out.body?.summary ?? void 0;
|
|
1116
|
+
if (typeof out.body?.detached === "number" && out.body.detached > 0) {
|
|
1117
|
+
log(`plune: ${out.body.detached} test case(s) marked detached \u2014 this full run no longer reports them.`);
|
|
1118
|
+
}
|
|
1115
1119
|
}
|
|
1116
1120
|
}
|
|
1117
1121
|
summarise();
|
|
1122
|
+
return frozen;
|
|
1118
1123
|
},
|
|
1119
1124
|
async leaveOpen() {
|
|
1120
1125
|
if (done) return;
|
package/dist/reporter-core.d.cts
CHANGED
|
@@ -221,6 +221,17 @@ interface RunRecord {
|
|
|
221
221
|
projectId?: string;
|
|
222
222
|
schemaVersion?: number;
|
|
223
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* What the platform froze into a run at `finish` and answers the event with (plune#1247): the verdict a
|
|
226
|
+
* release gate is made on, never recomputed. `counts` is by the status each result is counted under — a
|
|
227
|
+
* muted case's `failed` or `broken` is under `muted`, outside both — and an absent key is zero.
|
|
228
|
+
*/
|
|
229
|
+
interface FrozenSummary {
|
|
230
|
+
counts: Record<string, number>;
|
|
231
|
+
/** The expected cases that never reported. */
|
|
232
|
+
notRun: string[];
|
|
233
|
+
total: number;
|
|
234
|
+
}
|
|
224
235
|
/** Per-element verdicts from a batch. `conflict` means two runners disagreed about one result —
|
|
225
236
|
* the platform refuses to resolve that by overwriting, and neither do we. */
|
|
226
237
|
interface SubmitCounts {
|
|
@@ -357,8 +368,11 @@ interface RunSession {
|
|
|
357
368
|
add(result: PendingResult): Promise<void>;
|
|
358
369
|
/** Send whatever is buffered. Safe to call when nothing is. */
|
|
359
370
|
flush(): Promise<void>;
|
|
360
|
-
/**
|
|
361
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Send what is left and close the run. Resolves to the summary the platform froze at `finish` (plune#1247), or
|
|
373
|
+
* `undefined` when it answered with none: no run, a refused finish, a deployment older than the summary.
|
|
374
|
+
*/
|
|
375
|
+
finish(reason?: string): Promise<FrozenSummary | undefined>;
|
|
362
376
|
/** Send what is left and deliberately leave the run open — the shard case (AC-03). */
|
|
363
377
|
leaveOpen(): Promise<void>;
|
|
364
378
|
}
|
|
@@ -592,4 +606,4 @@ interface PlatformClient {
|
|
|
592
606
|
}
|
|
593
607
|
declare function createClient(opts: ClientOptions): PlatformClient;
|
|
594
608
|
|
|
595
|
-
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 };
|
|
609
|
+
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 FrozenSummary, 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
|
@@ -221,6 +221,17 @@ interface RunRecord {
|
|
|
221
221
|
projectId?: string;
|
|
222
222
|
schemaVersion?: number;
|
|
223
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* What the platform froze into a run at `finish` and answers the event with (plune#1247): the verdict a
|
|
226
|
+
* release gate is made on, never recomputed. `counts` is by the status each result is counted under — a
|
|
227
|
+
* muted case's `failed` or `broken` is under `muted`, outside both — and an absent key is zero.
|
|
228
|
+
*/
|
|
229
|
+
interface FrozenSummary {
|
|
230
|
+
counts: Record<string, number>;
|
|
231
|
+
/** The expected cases that never reported. */
|
|
232
|
+
notRun: string[];
|
|
233
|
+
total: number;
|
|
234
|
+
}
|
|
224
235
|
/** Per-element verdicts from a batch. `conflict` means two runners disagreed about one result —
|
|
225
236
|
* the platform refuses to resolve that by overwriting, and neither do we. */
|
|
226
237
|
interface SubmitCounts {
|
|
@@ -357,8 +368,11 @@ interface RunSession {
|
|
|
357
368
|
add(result: PendingResult): Promise<void>;
|
|
358
369
|
/** Send whatever is buffered. Safe to call when nothing is. */
|
|
359
370
|
flush(): Promise<void>;
|
|
360
|
-
/**
|
|
361
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Send what is left and close the run. Resolves to the summary the platform froze at `finish` (plune#1247), or
|
|
373
|
+
* `undefined` when it answered with none: no run, a refused finish, a deployment older than the summary.
|
|
374
|
+
*/
|
|
375
|
+
finish(reason?: string): Promise<FrozenSummary | undefined>;
|
|
362
376
|
/** Send what is left and deliberately leave the run open — the shard case (AC-03). */
|
|
363
377
|
leaveOpen(): Promise<void>;
|
|
364
378
|
}
|
|
@@ -592,4 +606,4 @@ interface PlatformClient {
|
|
|
592
606
|
}
|
|
593
607
|
declare function createClient(opts: ClientOptions): PlatformClient;
|
|
594
608
|
|
|
595
|
-
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 };
|
|
609
|
+
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 FrozenSummary, 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
|
@@ -1051,21 +1051,26 @@ async function startRun(passed, declared) {
|
|
|
1051
1051
|
},
|
|
1052
1052
|
flush,
|
|
1053
1053
|
async finish(reason) {
|
|
1054
|
-
if (done) return;
|
|
1054
|
+
if (done) return void 0;
|
|
1055
1055
|
done = true;
|
|
1056
1056
|
await flush();
|
|
1057
1057
|
await offer();
|
|
1058
|
+
let frozen;
|
|
1058
1059
|
if (runId !== null && !offline) {
|
|
1059
1060
|
const out = await client.post(`/v1/runs/${runId}/events`, {
|
|
1060
1061
|
event: "finish",
|
|
1061
1062
|
...reason !== void 0 ? { reason } : {}
|
|
1062
1063
|
});
|
|
1063
1064
|
if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
|
|
1064
|
-
else
|
|
1065
|
-
|
|
1065
|
+
else {
|
|
1066
|
+
frozen = out.body?.summary ?? void 0;
|
|
1067
|
+
if (typeof out.body?.detached === "number" && out.body.detached > 0) {
|
|
1068
|
+
log(`plune: ${out.body.detached} test case(s) marked detached \u2014 this full run no longer reports them.`);
|
|
1069
|
+
}
|
|
1066
1070
|
}
|
|
1067
1071
|
}
|
|
1068
1072
|
summarise();
|
|
1073
|
+
return frozen;
|
|
1069
1074
|
},
|
|
1070
1075
|
async leaveOpen() {
|
|
1071
1076
|
if (done) return;
|
package/package.json
CHANGED