@plune-ai/cli 0.9.0 → 0.9.1
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 -6
- package/dist/cli.cjs +32 -6
- package/dist/reporter-core.cjs +18 -3
- package/dist/reporter-core.d.cts +8 -0
- package/dist/reporter-core.d.ts +8 -0
- package/dist/reporter-core.js +18 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ provider API key is read from the environment based on `provider.type`:
|
|
|
103
103
|
| `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` → Settings → API tokens. Flags: `--token <token>` (omit to paste it or pipe it via stdin), `--skip-verify` (save without checking, for offline setup). |
|
|
104
104
|
| `plune logout` | Remove the saved token. |
|
|
105
105
|
| `plune sync` | Upload the latest local run to the platform. Flags: `--file <path>` to send a specific run JSON. |
|
|
106
|
-
| `plune run import <file>` | Turn a **JUnit XML** or **Playwright JSON** report into a run in Plune. Needs no provider key — nothing is generated. Flags: `--format junit\|playwright-json` (detected from the file when omitted), `--key <externalKey>` to land several reports in one run, `--create` to offer unmatched tests to the review queue. |
|
|
106
|
+
| `plune run import <file>` | Turn a **JUnit XML** or **Playwright JSON** report into a run in Plune. Needs no provider key — nothing is generated. Flags: `--format junit\|playwright-json` (detected from the file when omitted), `--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. |
|
|
107
107
|
| `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. |
|
|
108
108
|
|
|
109
109
|
Global flags: `-c, --config <path>` · `-v, --verbose` · `--no-color`.
|
|
@@ -135,14 +135,30 @@ which reports as the run happens and needs no second step:
|
|
|
135
135
|
reporter: [['list'], ['@plune-ai/playwright']],
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
### Several jobs, one run
|
|
139
|
+
|
|
140
|
+
Two suites, or a sharded matrix, report into a single run when every job shares a key **and** knows
|
|
141
|
+
it is not the last one:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
PLUNE_SHARED_RUN=1 plune run import ./junit.xml --key "$GITHUB_RUN_ID"
|
|
145
|
+
PLUNE_SHARED_RUN=1 plune run import ./app/junit.xml --key "$GITHUB_RUN_ID"
|
|
146
|
+
plune run finish "$RUN_ID" # once, when they are all done
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`PLUNE_SHARED_RUN=1` is what stops a job closing a run its siblings are still reporting into; the
|
|
150
|
+
key alone says where the results go, not who ends the run. Leave it unset in the last job and that
|
|
151
|
+
job closes the run instead of the explicit `finish`.
|
|
152
|
+
|
|
153
|
+
`plune run exec` sets it for you — anything reporting inside it, this command included, joins
|
|
154
|
+
without closing.
|
|
140
155
|
|
|
141
156
|
## Optional: keep a history
|
|
142
157
|
|
|
143
|
-
Everything above works with no account, no network, and no token — that does not change.
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
Everything above works with no account, no network, and no token — that does not change. `run`,
|
|
159
|
+
`report`, `diff` and `init` never open a socket, and that is the half this promise is about. If you
|
|
160
|
+
also want run history, trends, and a shared dashboard, the commands below push your local runs to
|
|
161
|
+
the [Plune platform](https://plune.ai/platform):
|
|
146
162
|
|
|
147
163
|
```bash
|
|
148
164
|
plune login # paste the API token from your platform settings page
|
package/dist/cli.cjs
CHANGED
|
@@ -2373,6 +2373,7 @@ async function startRun(passed, expected) {
|
|
|
2373
2373
|
rejected: 0,
|
|
2374
2374
|
unresolved: 0,
|
|
2375
2375
|
offered: 0,
|
|
2376
|
+
unoffered: 0,
|
|
2376
2377
|
deferred: 0
|
|
2377
2378
|
};
|
|
2378
2379
|
const resolved = /* @__PURE__ */ new Map();
|
|
@@ -2465,13 +2466,17 @@ async function startRun(passed, expected) {
|
|
|
2465
2466
|
}
|
|
2466
2467
|
async function offer() {
|
|
2467
2468
|
if (discoveries.size === 0 || offline || runId === null) return;
|
|
2468
|
-
|
|
2469
|
+
const batches = chunk([...discoveries.values()], DISCOVER_MAX);
|
|
2470
|
+
for (const [index, batch] of batches.entries()) {
|
|
2469
2471
|
const out = await client.post("/v1/review-items/discovered", {
|
|
2470
2472
|
discovered: batch,
|
|
2471
2473
|
runId
|
|
2472
2474
|
});
|
|
2473
2475
|
if (!out.ok) {
|
|
2474
|
-
|
|
2476
|
+
stats.unoffered = batches.slice(index).reduce((n, rest) => n + rest.length, 0);
|
|
2477
|
+
log(
|
|
2478
|
+
`plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.detail || out.kind}).`
|
|
2479
|
+
);
|
|
2475
2480
|
return;
|
|
2476
2481
|
}
|
|
2477
2482
|
stats.offered += out.body.results.filter((r) => r.outcome === "queued").length;
|
|
@@ -2501,7 +2506,10 @@ async function startRun(passed, expected) {
|
|
|
2501
2506
|
}
|
|
2502
2507
|
}
|
|
2503
2508
|
if (unanswered.length > 0) defer(unanswered);
|
|
2504
|
-
if (submissions.length === 0)
|
|
2509
|
+
if (submissions.length === 0) {
|
|
2510
|
+
progress();
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2505
2513
|
const out = await client.post(
|
|
2506
2514
|
`/v1/runs/${runId}/results`,
|
|
2507
2515
|
{ results: submissions }
|
|
@@ -2511,6 +2519,7 @@ async function startRun(passed, expected) {
|
|
|
2511
2519
|
stats.duplicate += out.body.counts.duplicate ?? 0;
|
|
2512
2520
|
stats.conflict += out.body.counts.conflict ?? 0;
|
|
2513
2521
|
stats.rejected += out.body.counts.rejected ?? 0;
|
|
2522
|
+
progress();
|
|
2514
2523
|
return;
|
|
2515
2524
|
}
|
|
2516
2525
|
if (out.kind === "auth") {
|
|
@@ -2523,6 +2532,12 @@ async function startRun(passed, expected) {
|
|
|
2523
2532
|
}
|
|
2524
2533
|
defer(submissions);
|
|
2525
2534
|
}
|
|
2535
|
+
function progress() {
|
|
2536
|
+
const total = expected?.length ?? 0;
|
|
2537
|
+
if (total <= batchSize) return;
|
|
2538
|
+
const done2 = stats.accepted + stats.duplicate + stats.conflict + stats.rejected + stats.unresolved;
|
|
2539
|
+
log(`plune: ${Math.min(done2, total)} of ${total} results sent`);
|
|
2540
|
+
}
|
|
2526
2541
|
function summarise() {
|
|
2527
2542
|
const parts = [`${stats.accepted} accepted`];
|
|
2528
2543
|
if (stats.duplicate > 0) parts.push(`${stats.duplicate} already reported`);
|
|
@@ -3180,7 +3195,8 @@ async function handleRunImport(options) {
|
|
|
3180
3195
|
results.map((r) => r.keys)
|
|
3181
3196
|
);
|
|
3182
3197
|
for (const result2 of results) await session.add(result2);
|
|
3183
|
-
await session.
|
|
3198
|
+
if (env.keepOpen) await session.leaveOpen();
|
|
3199
|
+
else await session.finish();
|
|
3184
3200
|
const stats = session.stats;
|
|
3185
3201
|
const result = {
|
|
3186
3202
|
runId: session.runId,
|
|
@@ -3192,6 +3208,7 @@ async function handleRunImport(options) {
|
|
|
3192
3208
|
rejected: stats.rejected,
|
|
3193
3209
|
unresolved: stats.unresolved,
|
|
3194
3210
|
offered: stats.offered,
|
|
3211
|
+
unoffered: stats.unoffered,
|
|
3195
3212
|
deferred: stats.deferred,
|
|
3196
3213
|
unlocatable: results.filter((r) => r.specRef === void 0).length,
|
|
3197
3214
|
offering: options.create === true
|
|
@@ -3205,7 +3222,11 @@ function runUrl2(apiUrl, id) {
|
|
|
3205
3222
|
}
|
|
3206
3223
|
function describe(result, apiUrl) {
|
|
3207
3224
|
const lines = [
|
|
3208
|
-
`Imported
|
|
3225
|
+
// `Read`, not `Imported`: the verb describes the FILE, which was read whatever happened next.
|
|
3226
|
+
// «Imported 555 result(s): 0 accepted, 0 already there, 0 unmatched» was a sentence whose two
|
|
3227
|
+
// halves disagreed, and readers stop at the verb — a run that reached nobody read as one that
|
|
3228
|
+
// worked (#622).
|
|
3229
|
+
`Read ${result.parsed} result(s) from a ${result.format} report: ${result.accepted} accepted, ${result.duplicate} already there, ${result.unresolved} unmatched.`
|
|
3209
3230
|
];
|
|
3210
3231
|
if (result.conflict > 0 || result.rejected > 0) {
|
|
3211
3232
|
lines.push(`${result.conflict} conflicted and ${result.rejected} were refused.`);
|
|
@@ -3219,9 +3240,14 @@ function describe(result, apiUrl) {
|
|
|
3219
3240
|
lines.push(`${result.offered} unknown test(s) offered to the review queue.`);
|
|
3220
3241
|
} else if (result.unresolved > 0 && !result.offering) {
|
|
3221
3242
|
lines.push("Nothing was offered to the review queue \u2014 pass --create to propose the unmatched tests.");
|
|
3222
|
-
} else if (result.unresolved > 0) {
|
|
3243
|
+
} else if (result.unresolved > 0 && result.unoffered === 0) {
|
|
3223
3244
|
lines.push("Nothing new to offer \u2014 the unmatched tests are already in the review queue.");
|
|
3224
3245
|
}
|
|
3246
|
+
if (result.unoffered > 0) {
|
|
3247
|
+
lines.push(
|
|
3248
|
+
`${result.unoffered} more could not be offered \u2014 the review queue is full. Approve or reject what is waiting, then import this report again to offer the rest.`
|
|
3249
|
+
);
|
|
3250
|
+
}
|
|
3225
3251
|
if (result.unlocatable > 0) {
|
|
3226
3252
|
lines.push(
|
|
3227
3253
|
`${result.unlocatable} test(s) carry no file or class in the report, so they cannot be offered for review \u2014 a reviewer would have nowhere to look.`
|
package/dist/reporter-core.cjs
CHANGED
|
@@ -239,6 +239,7 @@ async function startRun(passed, expected) {
|
|
|
239
239
|
rejected: 0,
|
|
240
240
|
unresolved: 0,
|
|
241
241
|
offered: 0,
|
|
242
|
+
unoffered: 0,
|
|
242
243
|
deferred: 0
|
|
243
244
|
};
|
|
244
245
|
const resolved = /* @__PURE__ */ new Map();
|
|
@@ -331,13 +332,17 @@ async function startRun(passed, expected) {
|
|
|
331
332
|
}
|
|
332
333
|
async function offer() {
|
|
333
334
|
if (discoveries.size === 0 || offline || runId === null) return;
|
|
334
|
-
|
|
335
|
+
const batches = chunk([...discoveries.values()], DISCOVER_MAX);
|
|
336
|
+
for (const [index, batch] of batches.entries()) {
|
|
335
337
|
const out = await client.post("/v1/review-items/discovered", {
|
|
336
338
|
discovered: batch,
|
|
337
339
|
runId
|
|
338
340
|
});
|
|
339
341
|
if (!out.ok) {
|
|
340
|
-
|
|
342
|
+
stats.unoffered = batches.slice(index).reduce((n, rest) => n + rest.length, 0);
|
|
343
|
+
log(
|
|
344
|
+
`plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.detail || out.kind}).`
|
|
345
|
+
);
|
|
341
346
|
return;
|
|
342
347
|
}
|
|
343
348
|
stats.offered += out.body.results.filter((r) => r.outcome === "queued").length;
|
|
@@ -367,7 +372,10 @@ async function startRun(passed, expected) {
|
|
|
367
372
|
}
|
|
368
373
|
}
|
|
369
374
|
if (unanswered.length > 0) defer(unanswered);
|
|
370
|
-
if (submissions.length === 0)
|
|
375
|
+
if (submissions.length === 0) {
|
|
376
|
+
progress();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
371
379
|
const out = await client.post(
|
|
372
380
|
`/v1/runs/${runId}/results`,
|
|
373
381
|
{ results: submissions }
|
|
@@ -377,6 +385,7 @@ async function startRun(passed, expected) {
|
|
|
377
385
|
stats.duplicate += out.body.counts.duplicate ?? 0;
|
|
378
386
|
stats.conflict += out.body.counts.conflict ?? 0;
|
|
379
387
|
stats.rejected += out.body.counts.rejected ?? 0;
|
|
388
|
+
progress();
|
|
380
389
|
return;
|
|
381
390
|
}
|
|
382
391
|
if (out.kind === "auth") {
|
|
@@ -389,6 +398,12 @@ async function startRun(passed, expected) {
|
|
|
389
398
|
}
|
|
390
399
|
defer(submissions);
|
|
391
400
|
}
|
|
401
|
+
function progress() {
|
|
402
|
+
const total = expected?.length ?? 0;
|
|
403
|
+
if (total <= batchSize) return;
|
|
404
|
+
const done2 = stats.accepted + stats.duplicate + stats.conflict + stats.rejected + stats.unresolved;
|
|
405
|
+
log(`plune: ${Math.min(done2, total)} of ${total} results sent`);
|
|
406
|
+
}
|
|
392
407
|
function summarise() {
|
|
393
408
|
const parts = [`${stats.accepted} accepted`];
|
|
394
409
|
if (stats.duplicate > 0) parts.push(`${stats.duplicate} already reported`);
|
package/dist/reporter-core.d.cts
CHANGED
|
@@ -139,6 +139,14 @@ interface RunStats extends SubmitCounts {
|
|
|
139
139
|
/** Unresolved tests offered to the review queue under `PLUNE_CREATE=1` (D14). Counted separately
|
|
140
140
|
* from `unresolved` because they are the same tests seen twice, not a second population. */
|
|
141
141
|
offered: number;
|
|
142
|
+
/**
|
|
143
|
+
* Unmatched tests that were NOT offered, because the queue would not take them (#627).
|
|
144
|
+
*
|
|
145
|
+
* Separate from `offered` rather than a shortfall computed against `unresolved`: the reason a
|
|
146
|
+
* test went unoffered is the caller's next action, and a subtraction cannot carry a reason. A
|
|
147
|
+
* queue at capacity means "empty it and import again"; nothing else here means that.
|
|
148
|
+
*/
|
|
149
|
+
unoffered: number;
|
|
142
150
|
/** Results written to the fallback file instead of the platform. */
|
|
143
151
|
deferred: number;
|
|
144
152
|
}
|
package/dist/reporter-core.d.ts
CHANGED
|
@@ -139,6 +139,14 @@ interface RunStats extends SubmitCounts {
|
|
|
139
139
|
/** Unresolved tests offered to the review queue under `PLUNE_CREATE=1` (D14). Counted separately
|
|
140
140
|
* from `unresolved` because they are the same tests seen twice, not a second population. */
|
|
141
141
|
offered: number;
|
|
142
|
+
/**
|
|
143
|
+
* Unmatched tests that were NOT offered, because the queue would not take them (#627).
|
|
144
|
+
*
|
|
145
|
+
* Separate from `offered` rather than a shortfall computed against `unresolved`: the reason a
|
|
146
|
+
* test went unoffered is the caller's next action, and a subtraction cannot carry a reason. A
|
|
147
|
+
* queue at capacity means "empty it and import again"; nothing else here means that.
|
|
148
|
+
*/
|
|
149
|
+
unoffered: number;
|
|
142
150
|
/** Results written to the fallback file instead of the platform. */
|
|
143
151
|
deferred: number;
|
|
144
152
|
}
|
package/dist/reporter-core.js
CHANGED
|
@@ -196,6 +196,7 @@ async function startRun(passed, expected) {
|
|
|
196
196
|
rejected: 0,
|
|
197
197
|
unresolved: 0,
|
|
198
198
|
offered: 0,
|
|
199
|
+
unoffered: 0,
|
|
199
200
|
deferred: 0
|
|
200
201
|
};
|
|
201
202
|
const resolved = /* @__PURE__ */ new Map();
|
|
@@ -288,13 +289,17 @@ async function startRun(passed, expected) {
|
|
|
288
289
|
}
|
|
289
290
|
async function offer() {
|
|
290
291
|
if (discoveries.size === 0 || offline || runId === null) return;
|
|
291
|
-
|
|
292
|
+
const batches = chunk([...discoveries.values()], DISCOVER_MAX);
|
|
293
|
+
for (const [index, batch] of batches.entries()) {
|
|
292
294
|
const out = await client.post("/v1/review-items/discovered", {
|
|
293
295
|
discovered: batch,
|
|
294
296
|
runId
|
|
295
297
|
});
|
|
296
298
|
if (!out.ok) {
|
|
297
|
-
|
|
299
|
+
stats.unoffered = batches.slice(index).reduce((n, rest) => n + rest.length, 0);
|
|
300
|
+
log(
|
|
301
|
+
`plune: could not offer ${stats.unoffered} unknown test(s) for review (${out.detail || out.kind}).`
|
|
302
|
+
);
|
|
298
303
|
return;
|
|
299
304
|
}
|
|
300
305
|
stats.offered += out.body.results.filter((r) => r.outcome === "queued").length;
|
|
@@ -324,7 +329,10 @@ async function startRun(passed, expected) {
|
|
|
324
329
|
}
|
|
325
330
|
}
|
|
326
331
|
if (unanswered.length > 0) defer(unanswered);
|
|
327
|
-
if (submissions.length === 0)
|
|
332
|
+
if (submissions.length === 0) {
|
|
333
|
+
progress();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
328
336
|
const out = await client.post(
|
|
329
337
|
`/v1/runs/${runId}/results`,
|
|
330
338
|
{ results: submissions }
|
|
@@ -334,6 +342,7 @@ async function startRun(passed, expected) {
|
|
|
334
342
|
stats.duplicate += out.body.counts.duplicate ?? 0;
|
|
335
343
|
stats.conflict += out.body.counts.conflict ?? 0;
|
|
336
344
|
stats.rejected += out.body.counts.rejected ?? 0;
|
|
345
|
+
progress();
|
|
337
346
|
return;
|
|
338
347
|
}
|
|
339
348
|
if (out.kind === "auth") {
|
|
@@ -346,6 +355,12 @@ async function startRun(passed, expected) {
|
|
|
346
355
|
}
|
|
347
356
|
defer(submissions);
|
|
348
357
|
}
|
|
358
|
+
function progress() {
|
|
359
|
+
const total = expected?.length ?? 0;
|
|
360
|
+
if (total <= batchSize) return;
|
|
361
|
+
const done2 = stats.accepted + stats.duplicate + stats.conflict + stats.rejected + stats.unresolved;
|
|
362
|
+
log(`plune: ${Math.min(done2, total)} of ${total} results sent`);
|
|
363
|
+
}
|
|
349
364
|
function summarise() {
|
|
350
365
|
const parts = [`${stats.accepted} accepted`];
|
|
351
366
|
if (stats.duplicate > 0) parts.push(`${stats.duplicate} already reported`);
|
package/package.json
CHANGED