@plune-ai/cli 0.11.1 → 0.13.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 +28 -5
- package/dist/cli.cjs +331 -33
- package/dist/index.cjs +4 -2
- package/dist/index.js +4 -2
- package/dist/reporter-core.cjs +13 -2
- package/dist/reporter-core.d.cts +10 -0
- package/dist/reporter-core.d.ts +10 -0
- package/dist/reporter-core.js +13 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ provider API key is read from the environment based on `provider.type`:
|
|
|
100
100
|
| `plune report` | Re-render the most recent run. Flags: `--format`, `-o`. |
|
|
101
101
|
| `plune diff <baseline> <current>` | Compare two `plune run --format json` outputs and report pass→fail regressions. Flags: `--fail-on-regression`, `--format`, `-o`. |
|
|
102
102
|
| `plune init` | Scaffold `plune.yaml`, a sample dataset, and `.env.example`. Flags: `--yes` (non-interactive), `--force`. |
|
|
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). |
|
|
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). 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. |
|
|
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
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. |
|
|
@@ -110,10 +110,13 @@ provider API key is read from the environment based on `provider.type`:
|
|
|
110
110
|
| `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 six months** (ask Plune to put it back), then gone for good. No prompt: it is your data, and this command belongs in scripts. |
|
|
111
111
|
| `plune run report` | Replay `.plune/pending-results.jsonl` — send what the reporter could not. Flags: `--file <path>`. |
|
|
112
112
|
| `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. |
|
|
113
|
+
| `plune pull [file]` | Write the project's test cases as **one Markdown document** — Testomat's classical format, plus Plune's own columns — to `plune/cases.md` (or `[file]`). Refuses to overwrite a file git sees as modified; `--force` overrides, `--suite <id>` takes one suite or folder and what is under it. |
|
|
114
|
+
| `plune push [file] [--dry-run]` | Send the document back. Cases are matched by `id`; a block without one becomes a draft; an unknown id is refused by line; **nothing is deleted**. Prints the report — created, updated, unchanged, refused, warnings — and with `--dry-run` writes nothing. Exit `4` when anything was refused. |
|
|
115
|
+
| `plune plan grep <id>` | Print a [test plan](https://docs.plune.ai/platform/plans/) as **one `--grep` pattern** — `npx playwright test --grep "$(plune plan grep <id>)"` or `npx vitest run -t "$(plune plan grep <id>)"` runs exactly what the plan collects. Each case contributes its title path from the `path-title` key the reporter wrote (the file left out, `[ >#]+` between the segments — a space for Playwright, jest, mocha and vitest ≤ 4, ` > ` for vitest 5), escaped, joined with `\|`; a case without one goes in by title. The pattern is all that reaches stdout; the summary — how many cases, how many keyed — goes to stderr. An empty plan prints `(?!)`, which matches nothing, because an empty `--grep` would run everything. |
|
|
113
116
|
|
|
114
117
|
Global flags: `-c, --config <path>` · `-v, --verbose` · `--no-color`.
|
|
115
118
|
|
|
116
|
-
**Exit codes:** `0` everything passed · `1` an assertion failed · `2` configuration or execution error.
|
|
119
|
+
**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.
|
|
117
120
|
|
|
118
121
|
## Already running tests? Bring the results in
|
|
119
122
|
|
|
@@ -122,7 +125,7 @@ suite, there is nothing to generate — the results exist and only have to arriv
|
|
|
122
125
|
costs nothing beyond a Plune token.
|
|
123
126
|
|
|
124
127
|
```bash
|
|
125
|
-
plune login
|
|
128
|
+
plune login # once, on your machine; in CI export PLUNE_TOKEN instead
|
|
126
129
|
plune run import ./junit.xml # Jest, Vitest, pytest, PHPUnit, Surefire, Cypress, …
|
|
127
130
|
plune run import ./playwright.json # or Playwright's own JSON report
|
|
128
131
|
```
|
|
@@ -158,6 +161,23 @@ job closes the run instead of the explicit `finish`.
|
|
|
158
161
|
`plune run exec` sets it for you — anything reporting inside it, this command included, joins
|
|
159
162
|
without closing.
|
|
160
163
|
|
|
164
|
+
### When a test is deleted
|
|
165
|
+
|
|
166
|
+
A test that vanishes from the code stays in Plune as an active case until something says otherwise.
|
|
167
|
+
The run that covers the whole suite is what says it:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
PLUNE_FULL_RUN=1 plune run import ./junit.xml # or the same variable on the Playwright reporter's job
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
When that run finishes having reported everything it expected, every case its source used to
|
|
174
|
+
report and did not this time is marked **detached** — not deleted, not retired: it keeps its history
|
|
175
|
+
and its place, and the dashboard shows it with the run as the reason. The next result on it brings
|
|
176
|
+
it back by itself. Set the variable only on the job that runs everything: a run of one file or a
|
|
177
|
+
`--grep` also reports everything it expected, and with the flag it would detach the rest. A run that
|
|
178
|
+
lost a shard (`notRun` is not empty) never detaches anything. `plune run start` has no list of
|
|
179
|
+
tests to compare against, so the flag means nothing there.
|
|
180
|
+
|
|
161
181
|
### What the run is called
|
|
162
182
|
|
|
163
183
|
A run nobody named is called after the directory, the minute it started and where it ran —
|
|
@@ -177,11 +197,14 @@ the [Plune platform](https://plune.ai/platform):
|
|
|
177
197
|
plune login # paste the API token from your platform settings page
|
|
178
198
|
plune run # exactly as before — the run is saved locally
|
|
179
199
|
plune sync # upload .plune/last-run.json, print the read-back URL
|
|
200
|
+
plune pull # the test cases as plune/cases.md — edit them anywhere
|
|
201
|
+
plune push --dry-run # what a push would change, before it changes anything
|
|
180
202
|
```
|
|
181
203
|
|
|
182
204
|
The token is stored at `~/.config/plune/credentials.json` (mode `0600`, honours `XDG_CONFIG_HOME`)
|
|
183
|
-
and is never printed or logged. `PLUNE_API_URL` points `sync` at a different server —
|
|
184
|
-
self-hosted backend.
|
|
205
|
+
and is never printed or logged. `PLUNE_API_URL` points `sync`, `pull` and `push` at a different server —
|
|
206
|
+
useful for a self-hosted backend. The Markdown format `pull` writes and `push` reads is documented at
|
|
207
|
+
[docs.plune.ai/platform/cases/markdown](https://docs.plune.ai/platform/cases/markdown/).
|
|
185
208
|
|
|
186
209
|
## Programmatic API
|
|
187
210
|
|
package/dist/cli.cjs
CHANGED
|
@@ -512,7 +512,7 @@ var init_retry = __esm({
|
|
|
512
512
|
"use strict";
|
|
513
513
|
init_cjs_shims();
|
|
514
514
|
init_errors2();
|
|
515
|
-
defaultSleep = (ms) => new Promise((
|
|
515
|
+
defaultSleep = (ms) => new Promise((resolve7) => setTimeout(resolve7, ms));
|
|
516
516
|
}
|
|
517
517
|
});
|
|
518
518
|
|
|
@@ -2159,6 +2159,8 @@ function saveToken(token) {
|
|
|
2159
2159
|
return file;
|
|
2160
2160
|
}
|
|
2161
2161
|
function loadToken() {
|
|
2162
|
+
const fromEnv = process.env["PLUNE_TOKEN"]?.trim();
|
|
2163
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
2162
2164
|
try {
|
|
2163
2165
|
const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
|
|
2164
2166
|
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
@@ -2210,7 +2212,7 @@ function createClient(opts) {
|
|
|
2210
2212
|
const wait = opts.wait ?? sleep;
|
|
2211
2213
|
const base = opts.apiUrl.replace(/\/+$/, "");
|
|
2212
2214
|
const scrub = (text2) => opts.token === "" ? text2 : text2.split(opts.token).join("***");
|
|
2213
|
-
async function send(method,
|
|
2215
|
+
async function send(method, path14, body) {
|
|
2214
2216
|
let last = {
|
|
2215
2217
|
kind: "unavailable",
|
|
2216
2218
|
status: null,
|
|
@@ -2219,7 +2221,7 @@ function createClient(opts) {
|
|
|
2219
2221
|
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt += 1) {
|
|
2220
2222
|
let res;
|
|
2221
2223
|
try {
|
|
2222
|
-
res = await doFetch(`${base}${
|
|
2224
|
+
res = await doFetch(`${base}${path14}`, {
|
|
2223
2225
|
method,
|
|
2224
2226
|
headers: { "content-type": "application/json", authorization: `Bearer ${opts.token}` },
|
|
2225
2227
|
// A DELETE carries none. Sending `"undefined"` as a body is what a naive
|
|
@@ -2247,8 +2249,8 @@ function createClient(opts) {
|
|
|
2247
2249
|
return { ok: false, ...last };
|
|
2248
2250
|
}
|
|
2249
2251
|
return {
|
|
2250
|
-
post: (
|
|
2251
|
-
del: (
|
|
2252
|
+
post: (path14, body) => send("POST", path14, body),
|
|
2253
|
+
del: (path14) => send("DELETE", path14)
|
|
2252
2254
|
};
|
|
2253
2255
|
}
|
|
2254
2256
|
var RETRY_DELAYS_MS, sleep;
|
|
@@ -2292,6 +2294,7 @@ function readEnv(env = process.env) {
|
|
|
2292
2294
|
const environment = value(env, "PLUNE_ENV");
|
|
2293
2295
|
const labels = list(env, "PLUNE_LABELS");
|
|
2294
2296
|
const offerDiscovered = flag(env, "PLUNE_CREATE");
|
|
2297
|
+
const full = flag(env, "PLUNE_FULL_RUN");
|
|
2295
2298
|
return {
|
|
2296
2299
|
config: {
|
|
2297
2300
|
...externalKey !== void 0 ? { externalKey } : {},
|
|
@@ -2304,7 +2307,8 @@ function readEnv(env = process.env) {
|
|
|
2304
2307
|
...labels !== void 0 ? { labels } : {},
|
|
2305
2308
|
// Only when set: `false` here would outrank a config file that asked for it, and an unset
|
|
2306
2309
|
// variable is not an instruction.
|
|
2307
|
-
...offerDiscovered ? { offerDiscovered } : {}
|
|
2310
|
+
...offerDiscovered ? { offerDiscovered } : {},
|
|
2311
|
+
...full ? { full } : {}
|
|
2308
2312
|
},
|
|
2309
2313
|
// Two names for two situations that end the same way. `SHARED_RUN` says other processes are
|
|
2310
2314
|
// reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
|
|
@@ -2459,7 +2463,11 @@ async function startRun(passed, declared) {
|
|
|
2459
2463
|
const hit = keys.map((k) => resolved.get(k.value)).find((v) => typeof v === "string");
|
|
2460
2464
|
if (typeof hit === "string") return [{ testCaseId: hit }];
|
|
2461
2465
|
return keys[0] === void 0 ? [] : [{ externalKey: keys[0] }];
|
|
2462
|
-
})
|
|
2466
|
+
}),
|
|
2467
|
+
// The claim travels with the list it is about (D20). A runner that could not enumerate
|
|
2468
|
+
// its tests has no list, and a claim without one is nothing the platform can check —
|
|
2469
|
+
// so it is not sent, rather than sent and silently meaningless.
|
|
2470
|
+
...cfg.full === true ? { full: true } : {}
|
|
2463
2471
|
};
|
|
2464
2472
|
if (expected !== void 0 && expected.length > EXPECTED_MAX) {
|
|
2465
2473
|
log(`plune: ${expected.length} tests is more than the run configuration holds \u2014 reporting the first ${EXPECTED_MAX}.`);
|
|
@@ -2626,6 +2634,9 @@ async function startRun(passed, declared) {
|
|
|
2626
2634
|
...reason !== void 0 ? { reason } : {}
|
|
2627
2635
|
});
|
|
2628
2636
|
if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
|
|
2637
|
+
else if (typeof out.body.detached === "number" && out.body.detached > 0) {
|
|
2638
|
+
log(`plune: ${out.body.detached} test case(s) marked detached \u2014 this full run no longer reports them.`);
|
|
2639
|
+
}
|
|
2629
2640
|
}
|
|
2630
2641
|
summarise();
|
|
2631
2642
|
},
|
|
@@ -2754,14 +2765,14 @@ async function handleRunExec(options) {
|
|
|
2754
2765
|
if (command === void 0) throw new RunCommandError("Nothing to run \u2014 pass a command after `--`.");
|
|
2755
2766
|
const run = await handleRunStart(options);
|
|
2756
2767
|
const spawnImpl = options.spawnImpl ?? import_node_child_process.spawn;
|
|
2757
|
-
const code = await new Promise((
|
|
2768
|
+
const code = await new Promise((resolve7) => {
|
|
2758
2769
|
const child = spawnImpl(command, args, {
|
|
2759
2770
|
stdio: "inherit",
|
|
2760
2771
|
shell: process.platform === "win32",
|
|
2761
2772
|
env: { ...process.env, PLUNE_RUN: run.externalKey, PLUNE_PROCEED: "1" }
|
|
2762
2773
|
});
|
|
2763
|
-
child.on("close", (exitCode) =>
|
|
2764
|
-
child.on("error", () =>
|
|
2774
|
+
child.on("close", (exitCode) => resolve7(exitCode ?? 1));
|
|
2775
|
+
child.on("error", () => resolve7(1));
|
|
2765
2776
|
});
|
|
2766
2777
|
await handleRunFinish(run.id, options);
|
|
2767
2778
|
return code;
|
|
@@ -4020,7 +4031,7 @@ var init_sync = __esm({
|
|
|
4020
4031
|
init_credentials();
|
|
4021
4032
|
NotLoggedInError = class extends Error {
|
|
4022
4033
|
constructor() {
|
|
4023
|
-
super('Not logged in. Run "plune login" first
|
|
4034
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
4024
4035
|
this.name = "NotLoggedInError";
|
|
4025
4036
|
}
|
|
4026
4037
|
};
|
|
@@ -4297,7 +4308,7 @@ var init_ingest = __esm({
|
|
|
4297
4308
|
init_cairn_artifact();
|
|
4298
4309
|
IngestNotLoggedInError = class extends Error {
|
|
4299
4310
|
constructor() {
|
|
4300
|
-
super('Not logged in. Run "plune login" first
|
|
4311
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
4301
4312
|
this.name = "IngestNotLoggedInError";
|
|
4302
4313
|
}
|
|
4303
4314
|
};
|
|
@@ -4324,6 +4335,207 @@ var init_ingest = __esm({
|
|
|
4324
4335
|
}
|
|
4325
4336
|
});
|
|
4326
4337
|
|
|
4338
|
+
// src/cli/commands/cases.ts
|
|
4339
|
+
var cases_exports = {};
|
|
4340
|
+
__export(cases_exports, {
|
|
4341
|
+
DEFAULT_CASES_FILE: () => DEFAULT_CASES_FILE,
|
|
4342
|
+
DirtyFileError: () => DirtyFileError,
|
|
4343
|
+
formatReport: () => formatReport,
|
|
4344
|
+
handlePull: () => handlePull,
|
|
4345
|
+
handlePush: () => handlePush,
|
|
4346
|
+
reportCasesFailure: () => reportCasesFailure
|
|
4347
|
+
});
|
|
4348
|
+
function gitDirty(file) {
|
|
4349
|
+
try {
|
|
4350
|
+
const out = (0, import_node_child_process2.execFileSync)("git", ["status", "--porcelain", "--", file], {
|
|
4351
|
+
cwd: path13.dirname(file),
|
|
4352
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
4353
|
+
encoding: "utf8"
|
|
4354
|
+
});
|
|
4355
|
+
return out.trim() !== "";
|
|
4356
|
+
} catch {
|
|
4357
|
+
return false;
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
async function safeErrorDetail3(res) {
|
|
4361
|
+
try {
|
|
4362
|
+
const body = await res.json();
|
|
4363
|
+
return typeof body.error === "string" ? body.error : "";
|
|
4364
|
+
} catch {
|
|
4365
|
+
return "";
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
function tokenOf(deps) {
|
|
4369
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
4370
|
+
if (token === null) throw new NotLoggedInError();
|
|
4371
|
+
return token;
|
|
4372
|
+
}
|
|
4373
|
+
async function handlePull(deps = {}) {
|
|
4374
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
4375
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4376
|
+
const token = tokenOf(deps);
|
|
4377
|
+
const file = path13.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
4378
|
+
if (fs15.existsSync(file) && !deps.force && (deps.isDirty ?? gitDirty)(file))
|
|
4379
|
+
throw new DirtyFileError(path13.relative(cwd, file) || file);
|
|
4380
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
4381
|
+
const endpoint = deps.suiteId !== void 0 ? `${apiUrl}/v1/suites/${encodeURIComponent(deps.suiteId)}/markdown` : `${apiUrl}/v1/test-cases/markdown`;
|
|
4382
|
+
let res;
|
|
4383
|
+
try {
|
|
4384
|
+
res = await doFetch(endpoint, { headers: { authorization: `Bearer ${token}` } });
|
|
4385
|
+
} catch {
|
|
4386
|
+
throw new SyncNetworkError(apiUrl);
|
|
4387
|
+
}
|
|
4388
|
+
if (!res.ok) {
|
|
4389
|
+
const detail = await safeErrorDetail3(res);
|
|
4390
|
+
if (res.status === 401) throw new TokenRejectedError2();
|
|
4391
|
+
throw new SyncHttpError(res.status, detail);
|
|
4392
|
+
}
|
|
4393
|
+
const text2 = await res.text();
|
|
4394
|
+
fs15.mkdirSync(path13.dirname(file), { recursive: true });
|
|
4395
|
+
fs15.writeFileSync(file, text2, "utf8");
|
|
4396
|
+
return { file, cases: Number(res.headers.get("x-plune-cases") ?? "0") };
|
|
4397
|
+
}
|
|
4398
|
+
async function handlePush(deps = {}) {
|
|
4399
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
4400
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4401
|
+
const token = tokenOf(deps);
|
|
4402
|
+
const file = path13.resolve(cwd, deps.file ?? DEFAULT_CASES_FILE);
|
|
4403
|
+
let text2;
|
|
4404
|
+
try {
|
|
4405
|
+
text2 = fs15.readFileSync(file, "utf8");
|
|
4406
|
+
} catch {
|
|
4407
|
+
throw new SyncFileError(
|
|
4408
|
+
`No document to push: ${file}. Run "plune pull" first (or pass a file).`
|
|
4409
|
+
);
|
|
4410
|
+
}
|
|
4411
|
+
if (text2.trim() === "") throw new SyncFileError(`The document is empty: ${file}`);
|
|
4412
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
4413
|
+
let res;
|
|
4414
|
+
try {
|
|
4415
|
+
res = await doFetch(`${apiUrl}/v1/test-cases/markdown${deps.dryRun ? "?dryRun=true" : ""}`, {
|
|
4416
|
+
method: "POST",
|
|
4417
|
+
headers: { "content-type": "text/markdown", authorization: `Bearer ${token}` },
|
|
4418
|
+
body: text2
|
|
4419
|
+
});
|
|
4420
|
+
} catch {
|
|
4421
|
+
throw new SyncNetworkError(apiUrl);
|
|
4422
|
+
}
|
|
4423
|
+
if (!res.ok) {
|
|
4424
|
+
const detail = await safeErrorDetail3(res);
|
|
4425
|
+
if (res.status === 401) throw new TokenRejectedError2();
|
|
4426
|
+
throw new SyncHttpError(res.status, detail);
|
|
4427
|
+
}
|
|
4428
|
+
return await res.json();
|
|
4429
|
+
}
|
|
4430
|
+
function formatReport(report) {
|
|
4431
|
+
const verb = (past, future) => report.dryRun ? future : past;
|
|
4432
|
+
const lines = [
|
|
4433
|
+
`${report.dryRun ? "Dry run \u2014 nothing written." : "Pushed."} ${report.created.length} ${verb("created", "to create")} \xB7 ${report.updated.length} ${verb("updated", "to update")} \xB7 ${report.unchanged.length} unchanged \xB7 ${report.refused.length} refused` + (report.suites.created.length > 0 ? ` \xB7 ${report.suites.created.length} suite(s) ${verb("created", "to create")}` : "")
|
|
4434
|
+
];
|
|
4435
|
+
for (const r of report.refused)
|
|
4436
|
+
lines.push(` refused line ${r.line} ${r.title || "(no title)"} \u2014 ${r.error}`);
|
|
4437
|
+
for (const w of report.warnings)
|
|
4438
|
+
lines.push(` warning ${w.line !== void 0 ? `line ${w.line} ` : ""}${w.message}`);
|
|
4439
|
+
return lines.join("\n") + "\n";
|
|
4440
|
+
}
|
|
4441
|
+
function reportCasesFailure(err, write) {
|
|
4442
|
+
if (err instanceof DirtyFileError) {
|
|
4443
|
+
write(err.message + "\n");
|
|
4444
|
+
return 3;
|
|
4445
|
+
}
|
|
4446
|
+
if (err instanceof NotLoggedInError || err instanceof TokenRejectedError2 || err instanceof SyncFileError) {
|
|
4447
|
+
write(err.message + "\n");
|
|
4448
|
+
return 2;
|
|
4449
|
+
}
|
|
4450
|
+
if (err instanceof SyncNetworkError || err instanceof SyncHttpError) {
|
|
4451
|
+
write(err.message + "\n");
|
|
4452
|
+
return 1;
|
|
4453
|
+
}
|
|
4454
|
+
return null;
|
|
4455
|
+
}
|
|
4456
|
+
var import_node_child_process2, fs15, path13, DEFAULT_CASES_FILE, DirtyFileError;
|
|
4457
|
+
var init_cases = __esm({
|
|
4458
|
+
"src/cli/commands/cases.ts"() {
|
|
4459
|
+
"use strict";
|
|
4460
|
+
init_cjs_shims();
|
|
4461
|
+
import_node_child_process2 = require("child_process");
|
|
4462
|
+
fs15 = __toESM(require("fs"), 1);
|
|
4463
|
+
path13 = __toESM(require("path"), 1);
|
|
4464
|
+
init_api_url();
|
|
4465
|
+
init_credentials();
|
|
4466
|
+
init_sync();
|
|
4467
|
+
DEFAULT_CASES_FILE = path13.join("plune", "cases.md");
|
|
4468
|
+
DirtyFileError = class extends Error {
|
|
4469
|
+
constructor(file) {
|
|
4470
|
+
super(`${file} has uncommitted changes; commit them or pass --force to overwrite.`);
|
|
4471
|
+
this.name = "DirtyFileError";
|
|
4472
|
+
}
|
|
4473
|
+
};
|
|
4474
|
+
}
|
|
4475
|
+
});
|
|
4476
|
+
|
|
4477
|
+
// src/cli/commands/plan.ts
|
|
4478
|
+
var plan_exports = {};
|
|
4479
|
+
__export(plan_exports, {
|
|
4480
|
+
formatGrep: () => formatGrep,
|
|
4481
|
+
grepFragment: () => grepFragment,
|
|
4482
|
+
handlePlanGrep: () => handlePlanGrep
|
|
4483
|
+
});
|
|
4484
|
+
function grepFragment(c) {
|
|
4485
|
+
const pathTitle = c.externalKeys.find((k) => k.kind === "path-title")?.value;
|
|
4486
|
+
const segments = pathTitle?.split("#") ?? [c.title];
|
|
4487
|
+
return (segments.length > 1 ? segments.slice(1) : segments).flatMap((s) => s.split(/ [>›] /)).map(escape).join(BOUNDARY);
|
|
4488
|
+
}
|
|
4489
|
+
function formatGrep(cases) {
|
|
4490
|
+
if (cases.length === 0) return "(?!)";
|
|
4491
|
+
return cases.map(grepFragment).join("|");
|
|
4492
|
+
}
|
|
4493
|
+
async function safeErrorDetail4(res) {
|
|
4494
|
+
try {
|
|
4495
|
+
const body = await res.json();
|
|
4496
|
+
return typeof body.error === "string" ? body.error : "";
|
|
4497
|
+
} catch {
|
|
4498
|
+
return "";
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
async function handlePlanGrep(deps) {
|
|
4502
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
4503
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
4504
|
+
if (token === null) throw new NotLoggedInError();
|
|
4505
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
4506
|
+
let res;
|
|
4507
|
+
try {
|
|
4508
|
+
res = await doFetch(`${apiUrl}/v1/plans/${encodeURIComponent(deps.planId)}/cases`, {
|
|
4509
|
+
headers: { authorization: `Bearer ${token}` }
|
|
4510
|
+
});
|
|
4511
|
+
} catch {
|
|
4512
|
+
throw new SyncNetworkError(apiUrl);
|
|
4513
|
+
}
|
|
4514
|
+
if (!res.ok) {
|
|
4515
|
+
const detail = await safeErrorDetail4(res);
|
|
4516
|
+
if (res.status === 401) throw new TokenRejectedError2();
|
|
4517
|
+
throw new SyncHttpError(res.status, detail);
|
|
4518
|
+
}
|
|
4519
|
+
const { cases } = await res.json();
|
|
4520
|
+
return {
|
|
4521
|
+
pattern: formatGrep(cases),
|
|
4522
|
+
cases: cases.length,
|
|
4523
|
+
keyed: cases.filter((c) => c.externalKeys.some((k) => k.kind === "path-title")).length
|
|
4524
|
+
};
|
|
4525
|
+
}
|
|
4526
|
+
var escape, BOUNDARY;
|
|
4527
|
+
var init_plan = __esm({
|
|
4528
|
+
"src/cli/commands/plan.ts"() {
|
|
4529
|
+
"use strict";
|
|
4530
|
+
init_cjs_shims();
|
|
4531
|
+
init_api_url();
|
|
4532
|
+
init_credentials();
|
|
4533
|
+
init_sync();
|
|
4534
|
+
escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4535
|
+
BOUNDARY = "[ >#]+";
|
|
4536
|
+
}
|
|
4537
|
+
});
|
|
4538
|
+
|
|
4327
4539
|
// src/cli.ts
|
|
4328
4540
|
var cli_exports = {};
|
|
4329
4541
|
__export(cli_exports, {
|
|
@@ -4481,23 +4693,30 @@ function createProgram() {
|
|
|
4481
4693
|
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) => {
|
|
4482
4694
|
try {
|
|
4483
4695
|
const { handleRunStart: handleRunStart2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
4484
|
-
await handleRunStart2({
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
}
|
|
4488
|
-
});
|
|
4489
|
-
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(async (id, options, command) => {
|
|
4490
|
-
try {
|
|
4491
|
-
const { handleRunFinish: handleRunFinish2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
4492
|
-
await handleRunFinish2(id, {
|
|
4493
|
-
terminate: options.terminate,
|
|
4494
|
-
...options.reason !== void 0 ? { reason: options.reason } : {}
|
|
4696
|
+
await handleRunStart2({
|
|
4697
|
+
...options.key !== void 0 ? { key: options.key } : {},
|
|
4698
|
+
json: options.json
|
|
4495
4699
|
});
|
|
4496
4700
|
} catch (err) {
|
|
4497
4701
|
await failRunCommand(err, verboseOf(command));
|
|
4498
4702
|
}
|
|
4499
4703
|
});
|
|
4500
|
-
runCommand.command("
|
|
4704
|
+
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(
|
|
4705
|
+
async (id, options, command) => {
|
|
4706
|
+
try {
|
|
4707
|
+
const { handleRunFinish: handleRunFinish2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
4708
|
+
await handleRunFinish2(id, {
|
|
4709
|
+
terminate: options.terminate,
|
|
4710
|
+
...options.reason !== void 0 ? { reason: options.reason } : {}
|
|
4711
|
+
});
|
|
4712
|
+
} catch (err) {
|
|
4713
|
+
await failRunCommand(err, verboseOf(command));
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
);
|
|
4717
|
+
runCommand.command("exec").argument("<command...>", "The command to run inside the run, after `--`").description(
|
|
4718
|
+
"Open a run, run a command inside it, close the run \u2014 whatever the command returns"
|
|
4719
|
+
).option("--key <externalKey>", "The key several processes share; generated when absent").action(async (argv, options, command) => {
|
|
4501
4720
|
try {
|
|
4502
4721
|
const { handleRunExec: handleRunExec2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
4503
4722
|
process.exitCode = await handleRunExec2({
|
|
@@ -4508,7 +4727,10 @@ function createProgram() {
|
|
|
4508
4727
|
await failRunCommand(err, verboseOf(command));
|
|
4509
4728
|
}
|
|
4510
4729
|
});
|
|
4511
|
-
runCommand.command("delete").argument(
|
|
4730
|
+
runCommand.command("delete").argument(
|
|
4731
|
+
"<id>",
|
|
4732
|
+
"The run id \u2014 the one `plune run import` printed, or the one in the dashboard URL"
|
|
4733
|
+
).description("Delete a run and everything it produced \u2014 recoverable for six months").action(async (id, _options, command) => {
|
|
4512
4734
|
try {
|
|
4513
4735
|
const { handleRunDelete: handleRunDelete2 } = await Promise.resolve().then(() => (init_run_lifecycle(), run_lifecycle_exports));
|
|
4514
4736
|
await handleRunDelete2(id);
|
|
@@ -4526,14 +4748,19 @@ function createProgram() {
|
|
|
4526
4748
|
});
|
|
4527
4749
|
runCommand.command("import").argument("<file>", "A JUnit XML or Playwright JSON report").description("Turn a report another runner wrote into a run in Plune \u2014 no provider key needed").option("--format <fmt>", "junit | playwright-json; detected from the file when omitted").option("--key <externalKey>", "The key several jobs share so their reports land in one run").option("--create", "Offer tests nothing matched to the review queue", false).action(
|
|
4528
4750
|
async (file, options, command) => {
|
|
4529
|
-
const [
|
|
4751
|
+
const [
|
|
4752
|
+
{ handleRunImport: handleRunImport2 },
|
|
4753
|
+
{ IMPORT_FORMATS: IMPORT_FORMATS2, UnknownFormatError: UnknownFormatError2, XmlParseError: XmlParseError2, JsonReportError: JsonReportError2 }
|
|
4754
|
+
] = await Promise.all([
|
|
4530
4755
|
Promise.resolve().then(() => (init_run_import(), run_import_exports)),
|
|
4531
4756
|
Promise.resolve().then(() => (init_importers(), importers_exports))
|
|
4532
4757
|
]);
|
|
4533
4758
|
const format = options.format;
|
|
4534
4759
|
if (format !== void 0 && !IMPORT_FORMATS2.includes(format)) {
|
|
4535
|
-
process.stderr.write(
|
|
4536
|
-
`)
|
|
4760
|
+
process.stderr.write(
|
|
4761
|
+
`Unknown --format "${format}". Use ${IMPORT_FORMATS2.join(" or ")}.
|
|
4762
|
+
`
|
|
4763
|
+
);
|
|
4537
4764
|
process.exit(2);
|
|
4538
4765
|
return;
|
|
4539
4766
|
}
|
|
@@ -4626,10 +4853,10 @@ function createProgram() {
|
|
|
4626
4853
|
const { handleLogin: handleLogin2, reportLoginFailure: reportLoginFailure2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
4627
4854
|
try {
|
|
4628
4855
|
const token = options.token ?? await readTokenFromStdin();
|
|
4629
|
-
const { path:
|
|
4856
|
+
const { path: path14, verified } = await handleLogin2({ token, skipVerify: options.skipVerify });
|
|
4630
4857
|
process.stdout.write(
|
|
4631
|
-
verified ? `Logged in. Token checked against the API and saved to ${
|
|
4632
|
-
` : `Token saved to ${
|
|
4858
|
+
verified ? `Logged in. Token checked against the API and saved to ${path14}
|
|
4859
|
+
` : `Token saved to ${path14} WITHOUT being checked (--skip-verify).
|
|
4633
4860
|
`
|
|
4634
4861
|
);
|
|
4635
4862
|
} catch (err) {
|
|
@@ -4646,9 +4873,9 @@ function createProgram() {
|
|
|
4646
4873
|
const verbose = command.optsWithGlobals().verbose === true;
|
|
4647
4874
|
const { handleLogout: handleLogout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
|
|
4648
4875
|
try {
|
|
4649
|
-
const { removed, path:
|
|
4876
|
+
const { removed, path: path14 } = handleLogout2();
|
|
4650
4877
|
process.stdout.write(
|
|
4651
|
-
removed ? `Logged out. Removed ${
|
|
4878
|
+
removed ? `Logged out. Removed ${path14}
|
|
4652
4879
|
` : "Not logged in \u2014 nothing to remove.\n"
|
|
4653
4880
|
);
|
|
4654
4881
|
} catch (err) {
|
|
@@ -4695,6 +4922,77 @@ function createProgram() {
|
|
|
4695
4922
|
failUnexpected(err, verbose);
|
|
4696
4923
|
}
|
|
4697
4924
|
});
|
|
4925
|
+
program.command("pull").argument("[file]", "Where to write the document (default: plune/cases.md)").description(
|
|
4926
|
+
"Write the project's test cases as one Markdown document (GET /v1/test-cases/markdown)"
|
|
4927
|
+
).option("--suite <id>", "Only this suite or folder and what is under it").option("--force", "Overwrite the file even when git says it has uncommitted changes", false).action(
|
|
4928
|
+
async (file, options, command) => {
|
|
4929
|
+
const verbose = command.optsWithGlobals().verbose === true;
|
|
4930
|
+
const { handlePull: handlePull2, reportCasesFailure: reportCasesFailure2 } = await Promise.resolve().then(() => (init_cases(), cases_exports));
|
|
4931
|
+
try {
|
|
4932
|
+
const result = await handlePull2({
|
|
4933
|
+
...file !== void 0 ? { file } : {},
|
|
4934
|
+
...options.suite !== void 0 ? { suiteId: options.suite } : {},
|
|
4935
|
+
force: options.force
|
|
4936
|
+
});
|
|
4937
|
+
process.stdout.write(`Pulled ${result.cases} case(s) \u2192 ${result.file}
|
|
4938
|
+
`);
|
|
4939
|
+
} catch (err) {
|
|
4940
|
+
const code = reportCasesFailure2(err, (s) => process.stderr.write(s));
|
|
4941
|
+
if (code !== null) {
|
|
4942
|
+
maybeStack(err, verbose);
|
|
4943
|
+
process.exitCode = code;
|
|
4944
|
+
return;
|
|
4945
|
+
}
|
|
4946
|
+
failUnexpected(err, verbose);
|
|
4947
|
+
}
|
|
4948
|
+
}
|
|
4949
|
+
);
|
|
4950
|
+
program.command("push").argument("[file]", "The document to send (default: plune/cases.md)").description(
|
|
4951
|
+
"Send a Markdown document of test cases back to Plune (POST /v1/test-cases/markdown)"
|
|
4952
|
+
).option("--dry-run", "Show what would change and write nothing", false).action(async (file, options, command) => {
|
|
4953
|
+
const verbose = command.optsWithGlobals().verbose === true;
|
|
4954
|
+
const { handlePush: handlePush2, formatReport: formatReport2, reportCasesFailure: reportCasesFailure2 } = await Promise.resolve().then(() => (init_cases(), cases_exports));
|
|
4955
|
+
try {
|
|
4956
|
+
const report = await handlePush2({
|
|
4957
|
+
...file !== void 0 ? { file } : {},
|
|
4958
|
+
dryRun: options.dryRun
|
|
4959
|
+
});
|
|
4960
|
+
process.stdout.write(formatReport2(report));
|
|
4961
|
+
if (report.refused.length > 0) process.exitCode = 4;
|
|
4962
|
+
} catch (err) {
|
|
4963
|
+
const code = reportCasesFailure2(err, (s) => process.stderr.write(s));
|
|
4964
|
+
if (code !== null) {
|
|
4965
|
+
maybeStack(err, verbose);
|
|
4966
|
+
process.exitCode = code;
|
|
4967
|
+
return;
|
|
4968
|
+
}
|
|
4969
|
+
failUnexpected(err, verbose);
|
|
4970
|
+
}
|
|
4971
|
+
});
|
|
4972
|
+
const planCommand = program.command("plan").description("Test plans on the platform \u2014 what a plan collects, as something a runner can take");
|
|
4973
|
+
planCommand.command("grep").argument("<id>", "The plan").description(
|
|
4974
|
+
"Print the plan's cases as one --grep pattern for the runner (GET /v1/plans/:id/cases)"
|
|
4975
|
+
).action(async (id, _options, command) => {
|
|
4976
|
+
const verbose = command.optsWithGlobals().verbose === true;
|
|
4977
|
+
const { handlePlanGrep: handlePlanGrep2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
|
|
4978
|
+
const { reportCasesFailure: reportCasesFailure2 } = await Promise.resolve().then(() => (init_cases(), cases_exports));
|
|
4979
|
+
try {
|
|
4980
|
+
const out = await handlePlanGrep2({ planId: id });
|
|
4981
|
+
process.stdout.write(out.pattern + "\n");
|
|
4982
|
+
process.stderr.write(
|
|
4983
|
+
out.cases === 0 ? "plune: the plan matches no case \u2014 the pattern matches nothing.\n" : `plune: ${out.cases} case(s) in the plan, ${out.keyed} with a test key; the rest by title.
|
|
4984
|
+
`
|
|
4985
|
+
);
|
|
4986
|
+
} catch (err) {
|
|
4987
|
+
const code = reportCasesFailure2(err, (s) => process.stderr.write(s));
|
|
4988
|
+
if (code !== null) {
|
|
4989
|
+
maybeStack(err, verbose);
|
|
4990
|
+
process.exitCode = code;
|
|
4991
|
+
return;
|
|
4992
|
+
}
|
|
4993
|
+
failUnexpected(err, verbose);
|
|
4994
|
+
}
|
|
4995
|
+
});
|
|
4698
4996
|
program.on("command:*", (operands) => {
|
|
4699
4997
|
process.stderr.write(`Unknown command: ${operands.join(" ")}
|
|
4700
4998
|
`);
|
package/dist/index.cjs
CHANGED
|
@@ -1595,6 +1595,8 @@ function credentialsFile() {
|
|
|
1595
1595
|
return (0, import_node_path.join)(base, "plune", "credentials.json");
|
|
1596
1596
|
}
|
|
1597
1597
|
function loadToken() {
|
|
1598
|
+
const fromEnv = process.env["PLUNE_TOKEN"]?.trim();
|
|
1599
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
1598
1600
|
try {
|
|
1599
1601
|
const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
|
|
1600
1602
|
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
@@ -1606,7 +1608,7 @@ function loadToken() {
|
|
|
1606
1608
|
// src/cli/commands/sync.ts
|
|
1607
1609
|
var NotLoggedInError = class extends Error {
|
|
1608
1610
|
constructor() {
|
|
1609
|
-
super('Not logged in. Run "plune login" first
|
|
1611
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1610
1612
|
this.name = "NotLoggedInError";
|
|
1611
1613
|
}
|
|
1612
1614
|
};
|
|
@@ -1862,7 +1864,7 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
1862
1864
|
// src/cli/commands/ingest.ts
|
|
1863
1865
|
var IngestNotLoggedInError = class extends Error {
|
|
1864
1866
|
constructor() {
|
|
1865
|
-
super('Not logged in. Run "plune login" first
|
|
1867
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1866
1868
|
this.name = "IngestNotLoggedInError";
|
|
1867
1869
|
}
|
|
1868
1870
|
};
|
package/dist/index.js
CHANGED
|
@@ -1542,6 +1542,8 @@ function credentialsFile() {
|
|
|
1542
1542
|
return join3(base, "plune", "credentials.json");
|
|
1543
1543
|
}
|
|
1544
1544
|
function loadToken() {
|
|
1545
|
+
const fromEnv = process.env["PLUNE_TOKEN"]?.trim();
|
|
1546
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
1545
1547
|
try {
|
|
1546
1548
|
const parsed = JSON.parse(readFileSync3(credentialsFile(), "utf-8"));
|
|
1547
1549
|
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
@@ -1553,7 +1555,7 @@ function loadToken() {
|
|
|
1553
1555
|
// src/cli/commands/sync.ts
|
|
1554
1556
|
var NotLoggedInError = class extends Error {
|
|
1555
1557
|
constructor() {
|
|
1556
|
-
super('Not logged in. Run "plune login" first
|
|
1558
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1557
1559
|
this.name = "NotLoggedInError";
|
|
1558
1560
|
}
|
|
1559
1561
|
};
|
|
@@ -1809,7 +1811,7 @@ async function readCairnRun(dir, deps = {}) {
|
|
|
1809
1811
|
// src/cli/commands/ingest.ts
|
|
1810
1812
|
var IngestNotLoggedInError = class extends Error {
|
|
1811
1813
|
constructor() {
|
|
1812
|
-
super('Not logged in. Run "plune login" first
|
|
1814
|
+
super('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.');
|
|
1813
1815
|
this.name = "IngestNotLoggedInError";
|
|
1814
1816
|
}
|
|
1815
1817
|
};
|
package/dist/reporter-core.cjs
CHANGED
|
@@ -60,6 +60,8 @@ function credentialsFile() {
|
|
|
60
60
|
return (0, import_node_path.join)(base, "plune", "credentials.json");
|
|
61
61
|
}
|
|
62
62
|
function loadToken() {
|
|
63
|
+
const fromEnv = process.env["PLUNE_TOKEN"]?.trim();
|
|
64
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
63
65
|
try {
|
|
64
66
|
const parsed = JSON.parse((0, import_node_fs.readFileSync)(credentialsFile(), "utf-8"));
|
|
65
67
|
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
@@ -172,6 +174,7 @@ function readEnv(env = process.env) {
|
|
|
172
174
|
const environment = value(env, "PLUNE_ENV");
|
|
173
175
|
const labels = list(env, "PLUNE_LABELS");
|
|
174
176
|
const offerDiscovered = flag(env, "PLUNE_CREATE");
|
|
177
|
+
const full = flag(env, "PLUNE_FULL_RUN");
|
|
175
178
|
return {
|
|
176
179
|
config: {
|
|
177
180
|
...externalKey !== void 0 ? { externalKey } : {},
|
|
@@ -184,7 +187,8 @@ function readEnv(env = process.env) {
|
|
|
184
187
|
...labels !== void 0 ? { labels } : {},
|
|
185
188
|
// Only when set: `false` here would outrank a config file that asked for it, and an unset
|
|
186
189
|
// variable is not an instruction.
|
|
187
|
-
...offerDiscovered ? { offerDiscovered } : {}
|
|
190
|
+
...offerDiscovered ? { offerDiscovered } : {},
|
|
191
|
+
...full ? { full } : {}
|
|
188
192
|
},
|
|
189
193
|
// Two names for two situations that end the same way. `SHARED_RUN` says other processes are
|
|
190
194
|
// reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
|
|
@@ -330,7 +334,11 @@ async function startRun(passed, declared) {
|
|
|
330
334
|
const hit = keys.map((k) => resolved.get(k.value)).find((v) => typeof v === "string");
|
|
331
335
|
if (typeof hit === "string") return [{ testCaseId: hit }];
|
|
332
336
|
return keys[0] === void 0 ? [] : [{ externalKey: keys[0] }];
|
|
333
|
-
})
|
|
337
|
+
}),
|
|
338
|
+
// The claim travels with the list it is about (D20). A runner that could not enumerate
|
|
339
|
+
// its tests has no list, and a claim without one is nothing the platform can check —
|
|
340
|
+
// so it is not sent, rather than sent and silently meaningless.
|
|
341
|
+
...cfg.full === true ? { full: true } : {}
|
|
334
342
|
};
|
|
335
343
|
if (expected !== void 0 && expected.length > EXPECTED_MAX) {
|
|
336
344
|
log(`plune: ${expected.length} tests is more than the run configuration holds \u2014 reporting the first ${EXPECTED_MAX}.`);
|
|
@@ -497,6 +505,9 @@ async function startRun(passed, declared) {
|
|
|
497
505
|
...reason !== void 0 ? { reason } : {}
|
|
498
506
|
});
|
|
499
507
|
if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
|
|
508
|
+
else if (typeof out.body.detached === "number" && out.body.detached > 0) {
|
|
509
|
+
log(`plune: ${out.body.detached} test case(s) marked detached \u2014 this full run no longer reports them.`);
|
|
510
|
+
}
|
|
500
511
|
}
|
|
501
512
|
summarise();
|
|
502
513
|
},
|
package/dist/reporter-core.d.cts
CHANGED
|
@@ -188,6 +188,16 @@ interface ReporterConfig {
|
|
|
188
188
|
* how somebody says "yes, tell me what we are running that you do not know about".
|
|
189
189
|
*/
|
|
190
190
|
offerDiscovered?: boolean;
|
|
191
|
+
/**
|
|
192
|
+
* This run is the whole suite (`PLUNE_FULL_RUN=1`, D20).
|
|
193
|
+
*
|
|
194
|
+
* Sent as `configuration.full` beside `expected`. When such a run finishes having reported
|
|
195
|
+
* everything it expected, the platform moves the cases this source used to report and did not
|
|
196
|
+
* this time to `detached` — a test that vanished from the code stops looking alive. Off by
|
|
197
|
+
* default because nothing here can tell a suite that shrank from `-g smoke` or a single file:
|
|
198
|
+
* only whoever typed the command knows, so the CI job that runs everything is the one to set it.
|
|
199
|
+
*/
|
|
200
|
+
full?: boolean;
|
|
191
201
|
/** Transport seam — the same one `sync.ts` uses so tests need no server. */
|
|
192
202
|
fetchImpl?: typeof fetch;
|
|
193
203
|
/** Where a line for the operator goes. Defaults to stderr. */
|
package/dist/reporter-core.d.ts
CHANGED
|
@@ -188,6 +188,16 @@ interface ReporterConfig {
|
|
|
188
188
|
* how somebody says "yes, tell me what we are running that you do not know about".
|
|
189
189
|
*/
|
|
190
190
|
offerDiscovered?: boolean;
|
|
191
|
+
/**
|
|
192
|
+
* This run is the whole suite (`PLUNE_FULL_RUN=1`, D20).
|
|
193
|
+
*
|
|
194
|
+
* Sent as `configuration.full` beside `expected`. When such a run finishes having reported
|
|
195
|
+
* everything it expected, the platform moves the cases this source used to report and did not
|
|
196
|
+
* this time to `detached` — a test that vanished from the code stops looking alive. Off by
|
|
197
|
+
* default because nothing here can tell a suite that shrank from `-g smoke` or a single file:
|
|
198
|
+
* only whoever typed the command knows, so the CI job that runs everything is the one to set it.
|
|
199
|
+
*/
|
|
200
|
+
full?: boolean;
|
|
191
201
|
/** Transport seam — the same one `sync.ts` uses so tests need no server. */
|
|
192
202
|
fetchImpl?: typeof fetch;
|
|
193
203
|
/** Where a line for the operator goes. Defaults to stderr. */
|
package/dist/reporter-core.js
CHANGED
|
@@ -17,6 +17,8 @@ function credentialsFile() {
|
|
|
17
17
|
return join(base, "plune", "credentials.json");
|
|
18
18
|
}
|
|
19
19
|
function loadToken() {
|
|
20
|
+
const fromEnv = process.env["PLUNE_TOKEN"]?.trim();
|
|
21
|
+
if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
|
|
20
22
|
try {
|
|
21
23
|
const parsed = JSON.parse(readFileSync(credentialsFile(), "utf-8"));
|
|
22
24
|
return typeof parsed.token === "string" && parsed.token.length > 0 ? parsed.token : null;
|
|
@@ -129,6 +131,7 @@ function readEnv(env = process.env) {
|
|
|
129
131
|
const environment = value(env, "PLUNE_ENV");
|
|
130
132
|
const labels = list(env, "PLUNE_LABELS");
|
|
131
133
|
const offerDiscovered = flag(env, "PLUNE_CREATE");
|
|
134
|
+
const full = flag(env, "PLUNE_FULL_RUN");
|
|
132
135
|
return {
|
|
133
136
|
config: {
|
|
134
137
|
...externalKey !== void 0 ? { externalKey } : {},
|
|
@@ -141,7 +144,8 @@ function readEnv(env = process.env) {
|
|
|
141
144
|
...labels !== void 0 ? { labels } : {},
|
|
142
145
|
// Only when set: `false` here would outrank a config file that asked for it, and an unset
|
|
143
146
|
// variable is not an instruction.
|
|
144
|
-
...offerDiscovered ? { offerDiscovered } : {}
|
|
147
|
+
...offerDiscovered ? { offerDiscovered } : {},
|
|
148
|
+
...full ? { full } : {}
|
|
145
149
|
},
|
|
146
150
|
// Two names for two situations that end the same way. `SHARED_RUN` says other processes are
|
|
147
151
|
// reporting into this run; `PROCEED` says the job will close it on its own schedule. Either
|
|
@@ -287,7 +291,11 @@ async function startRun(passed, declared) {
|
|
|
287
291
|
const hit = keys.map((k) => resolved.get(k.value)).find((v) => typeof v === "string");
|
|
288
292
|
if (typeof hit === "string") return [{ testCaseId: hit }];
|
|
289
293
|
return keys[0] === void 0 ? [] : [{ externalKey: keys[0] }];
|
|
290
|
-
})
|
|
294
|
+
}),
|
|
295
|
+
// The claim travels with the list it is about (D20). A runner that could not enumerate
|
|
296
|
+
// its tests has no list, and a claim without one is nothing the platform can check —
|
|
297
|
+
// so it is not sent, rather than sent and silently meaningless.
|
|
298
|
+
...cfg.full === true ? { full: true } : {}
|
|
291
299
|
};
|
|
292
300
|
if (expected !== void 0 && expected.length > EXPECTED_MAX) {
|
|
293
301
|
log(`plune: ${expected.length} tests is more than the run configuration holds \u2014 reporting the first ${EXPECTED_MAX}.`);
|
|
@@ -454,6 +462,9 @@ async function startRun(passed, declared) {
|
|
|
454
462
|
...reason !== void 0 ? { reason } : {}
|
|
455
463
|
});
|
|
456
464
|
if (!out.ok) log(`plune: could not close run ${runId} (${out.detail || out.kind}).`);
|
|
465
|
+
else if (typeof out.body.detached === "number" && out.body.detached > 0) {
|
|
466
|
+
log(`plune: ${out.body.detached} test case(s) marked detached \u2014 this full run no longer reports them.`);
|
|
467
|
+
}
|
|
457
468
|
}
|
|
458
469
|
summarise();
|
|
459
470
|
},
|
package/package.json
CHANGED