@plune-ai/cli 0.28.0 → 0.30.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 +17 -1
- package/dist/cli.cjs +382 -59
- package/dist/index.cjs +89 -21
- package/dist/index.js +89 -21
- package/dist/reporter-core.cjs +15 -2
- package/dist/reporter-core.d.cts +11 -1
- package/dist/reporter-core.d.ts +11 -1
- package/dist/reporter-core.js +14 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ the last resort, used only when `PLUNE_TOKEN` is set nowhere else. Nothing write
|
|
|
121
121
|
| `plune init` | Scaffold `plune.yaml`, a sample dataset, and `.env.example`. Flags: `--yes` (non-interactive), `--force`. |
|
|
122
122
|
| `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. |
|
|
123
123
|
| `plune logout` | Remove the saved token. |
|
|
124
|
-
| `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. Each upload carries an `Idempotency-Key` (SHA-256 of the body), so sending the same run again returns the same run rather than creating a second one. |
|
|
124
|
+
| `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. Each upload carries an `Idempotency-Key` (SHA-256 of the body), so sending the same run again returns the same run rather than creating a second one. Against a platform with `POST /v1/jobs` the upload is also reported as a job (kind `cli-sync`) that ends failed with the error text if the sync fails; an older platform without the route is not told, and a job report never fails the sync. |
|
|
125
125
|
| `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), `--name <text>` to call the run something other than the [folder-based default](#what-the-run-is-called), `--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), `--upload-concurrency <n>` for files uploaded at once ([1 to 32, default 16](#many-screenshots-upload-concurrency-and-retries)), `--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. |
|
|
126
126
|
| `plune run start` | Open a platform run — or join the one already carrying `--key` — and print its id. Flags: `--key <externalKey>` (generated when absent), `--name <text>` to call the run something other than the [default](#what-the-run-is-called), `--json` for one machine-readable line. |
|
|
127
127
|
| `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. |
|
|
@@ -132,7 +132,9 @@ the last resort, used only when `PLUNE_TOKEN` is set nowhere else. Nothing write
|
|
|
132
132
|
| `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. |
|
|
133
133
|
| `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. |
|
|
134
134
|
| `plune requirements import <file.md>` | Import requirements from a **Markdown file** (an SRS, a PRD, a page exported from a wiki). With no mapping flags it lists the tables and sections the file has and exits `0`; with `--table` or `--sections` it is a **dry run** that writes nothing until you add `--yes`. See [Requirements from a Markdown file](#requirements-from-a-markdown-file). Needs a Plune platform that serves `POST /v1/projects/:id/requirements/import` (plune-ai/plune#1621). |
|
|
135
|
+
| `plune requirements links <file.md>` | Import a **traceability matrix** (a Markdown table of requirement key to test) as links between requirements and cases. With no flags it lists the tables the file has and exits `0`; with `--table`, `--requirement-column` and `--test-column` it is a **dry run** that writes nothing until you add `--yes`. See [Links from a traceability matrix](#links-from-a-traceability-matrix). Needs a platform that serves `POST /v1/projects/:id/requirements/import/links` (plune-ai/plune#1608). |
|
|
135
136
|
| `plune verify` | Compare the project's **automated cases** with the Playwright tests in the code, **read-only** (plune-ai/plune#1291). Prints both sides: cases in Plune that the code no longer has (already `detached`, or one a full run would detach) and tests in the code that Plune has no case for. A test is matched by the id it names (`PluneId` annotation or `@P<id>` in its title), else by its `path-title` key. Nothing is written to Plune and no id is added to your source. Playwright only. Flags: `--from <file>` (a saved `playwright test --list --reporter=json`, instead of running Playwright), `--playwright-config <path>`, `--json`, `--fail-on-drift` (exit `1` when either side has something the other lacks). |
|
|
137
|
+
| `plune ids [--write]` | Write the Plune **case id of each Playwright test that names none** into a mapfile, `plune/ids.json` (`{ "<path-title>": "<case id>" }`), instead of editing your test files (plune-ai/plune#1291). A **dry run** unless `--write`: it prints what it would add. Only tests whose case Plune already holds get an entry (matched by the `path-title` key); a test with no case yet is listed and skipped. Idempotent: entries are only added, never overwritten or removed (an entry for a test no longer in the code is kept and listed), the file is sorted so diffs stay small, and nothing is written when nothing changes. A mapfile that is not a JSON object of strings is refused (exit `2`) and left as it is. Playwright only. The Playwright reporter reads the mapfile: an entry names the case after the `PluneId` annotation, `@P<id>` and a metadata attachment, and before the `playwright-id` and `path-title` keys, so a renamed test keeps its case when the entry is updated with `plune ids --write` (override the path with `PLUNE_IDS_FILE` or the reporter option `idsFile`). Flags: `--write`, `--file <path>` (default `plune/ids.json`), `--from <file>`, `--playwright-config <path>`, `--json`. Exit codes as `plune verify`. |
|
|
136
138
|
| `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. |
|
|
137
139
|
| `plune run grep <run> [--failed]` | Print a run's **automated** cases as one `--grep` pattern, built the way `plan grep` builds a plan's — so the commands Plune shows for a run work as they are: `npx playwright test --grep "$(plune run grep <run-id> --failed)"` re-runs an automated run's failures where your tests run (Plune starts no CI), and `PLUNE_RUN=<key> npx playwright test --grep "$(plune run grep <key>)"` runs the automated half of a mixed run launched without a plan. `<run>` is the run's id (a UUID) or its key (`rk_` and ten hex); anything else is refused with exit `2` before anything is sent, and a key no run carries exits `1`. The cases are the ones the run expected — cases added on its page among them, run or not — or holds a result of; manual cases are left out. `--failed` keeps the cases whose latest result the platform counts as failed or broken, so a muted case's failure stays out, as from **Re-run N failed**. Only the pattern reaches stdout; how many cases were taken and how many left out goes to stderr; nothing taken prints `(?!)`. Needs a Plune that serves `GET /v1/runs/:id/cases` (plune-ai/plune#1333). |
|
|
138
140
|
|
|
@@ -196,6 +198,20 @@ Inside a `--table` the names are `key`, `title` or `text` (one of the two), `des
|
|
|
196
198
|
|
|
197
199
|
`--json` prints the platform's answer instead. The project is the only one of the account, or `--project <id>` / `PLUNE_PROJECT` (a token only works in the project it was minted in). Exit `0` on success, `2` for a flag combination that cannot work (`--table` with `--sections`, `--title-column` with `--text-column`, a `--table "<n>:..."` spec with the single-table column flags), an unreadable or over-limit file, or a rejected token, `1` when the platform refuses (400, 403, 409 a key taken meanwhile, 413) or does not have the routes yet.
|
|
198
200
|
|
|
201
|
+
### Links from a traceability matrix
|
|
202
|
+
|
|
203
|
+
Requirements documents often end with a table of requirement id to test. `plune requirements links <file.md>` imports it as links between requirements and cases, in the same two steps as `requirements import`: it sends the file as it is, and you say which table and which two columns.
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
plune requirements links matrix.md # lists the tables, exit 0
|
|
207
|
+
plune requirements links matrix.md --table 0 --requirement-column "Requirement" --test-column "Test" # dry run
|
|
208
|
+
plune requirements links matrix.md --table 0 --requirement-column "Requirement" --test-column "Test" --yes
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
A requirement is matched by its key (import the requirements first, with `--key-column`). A test is matched by the exact title of a case, or for an automated case by its source path (`e2e/login.spec.ts` or `e2e/login.spec.ts#title`); put several tests in one cell on separate lines (`<br>`). A requirement cell with several keys is split by `--separator` (1 to 3 characters, default `,`; nothing else is guessed). The report lists what it would link, what is already linked, and every row that matched nothing, with the reason (unknown key, archived requirement, no such case, a title that names several cases). A second import changes nothing. Links never change a requirement's coverage, which still comes from each case's last result.
|
|
212
|
+
|
|
213
|
+
Exit `0` after a dry run or a write, even when rows are unmatched; `--fail-on-unmatched` makes any unmatched row exit `1`. A wrong flag or file is `2`; a read-only member, a rejected mapping or a matrix over 2000 rows (after splitting) is `1` with the platform's reason. `--json` prints the platform's answer; `--project <id>` / `PLUNE_PROJECT` picks the project.
|
|
214
|
+
|
|
199
215
|
### A release gate: `--fail-on-failures`
|
|
200
216
|
|
|
201
217
|
`plune run finish`, `plune run exec`, `plune run import` and `plune sync` take `--fail-on-failures`. The exit
|
package/dist/cli.cjs
CHANGED
|
@@ -555,22 +555,22 @@ var init_prices = __esm({
|
|
|
555
555
|
});
|
|
556
556
|
|
|
557
557
|
// src/providers/cost.ts
|
|
558
|
-
function priceFrom(price,
|
|
559
|
-
return
|
|
558
|
+
function priceFrom(price, usage3) {
|
|
559
|
+
return usage3.input_tokens / 1e3 * price.input_per_1k_usd + usage3.output_tokens / 1e3 * price.output_per_1k_usd;
|
|
560
560
|
}
|
|
561
|
-
function resolveCost(
|
|
561
|
+
function resolveCost(usage3, model, reportedCostUsd, overrides, deps = {}) {
|
|
562
562
|
const table = deps.table ?? PRICE_TABLE;
|
|
563
563
|
const warn = deps.warn ?? defaultWarn;
|
|
564
564
|
const override = overrides?.[model];
|
|
565
565
|
if (override !== void 0) {
|
|
566
|
-
return priceFrom(override,
|
|
566
|
+
return priceFrom(override, usage3);
|
|
567
567
|
}
|
|
568
568
|
if (reportedCostUsd !== void 0) {
|
|
569
569
|
return reportedCostUsd;
|
|
570
570
|
}
|
|
571
571
|
const tablePrice = table[model];
|
|
572
572
|
if (tablePrice !== void 0) {
|
|
573
|
-
return priceFrom(tablePrice,
|
|
573
|
+
return priceFrom(tablePrice, usage3);
|
|
574
574
|
}
|
|
575
575
|
warn(
|
|
576
576
|
`plune: no price for model "${model}" \u2014 reporting cost_usd=0. Set pricing["${model}"] in plune.yaml to track its cost.`
|
|
@@ -629,8 +629,8 @@ function makeAnthropicProvider(config, env, pricing) {
|
|
|
629
629
|
throw normalizeProviderError(err, apiKey, ENV_VAR);
|
|
630
630
|
}
|
|
631
631
|
},
|
|
632
|
-
estimateCost(
|
|
633
|
-
return { cost_usd: resolveCost(
|
|
632
|
+
estimateCost(usage3, reportedCostUsd) {
|
|
633
|
+
return { cost_usd: resolveCost(usage3, config.model, reportedCostUsd, pricing) };
|
|
634
634
|
}
|
|
635
635
|
};
|
|
636
636
|
}
|
|
@@ -696,8 +696,8 @@ function makeOpenAiCompatibleProvider(opts) {
|
|
|
696
696
|
throw normalizeProviderError(err, apiKey, apiKeyEnv);
|
|
697
697
|
}
|
|
698
698
|
},
|
|
699
|
-
estimateCost(
|
|
700
|
-
return { cost_usd: resolveCost(
|
|
699
|
+
estimateCost(usage3, reportedCostUsd) {
|
|
700
|
+
return { cost_usd: resolveCost(usage3, config.model, reportedCostUsd, pricing) };
|
|
701
701
|
}
|
|
702
702
|
};
|
|
703
703
|
}
|
|
@@ -800,8 +800,8 @@ function makeEstimateProvider(config, pricing) {
|
|
|
800
800
|
complete: () => Promise.reject(
|
|
801
801
|
new Error("The --dry-run provider only estimates cost; it never calls a model.")
|
|
802
802
|
),
|
|
803
|
-
estimateCost: (
|
|
804
|
-
cost_usd: resolveCost(
|
|
803
|
+
estimateCost: (usage3, reportedCostUsd) => ({
|
|
804
|
+
cost_usd: resolveCost(usage3, config.model, reportedCostUsd, pricing)
|
|
805
805
|
})
|
|
806
806
|
};
|
|
807
807
|
}
|
|
@@ -1781,8 +1781,8 @@ async function runRow(p) {
|
|
|
1781
1781
|
const maxTokens = p.providerConfig.max_tokens ?? DEFAULT_MAX_TOKENS2;
|
|
1782
1782
|
if (p.dryRun) {
|
|
1783
1783
|
const est = estimateUsage(prompt5, maxTokens);
|
|
1784
|
-
const
|
|
1785
|
-
return { vars: p.row.vars, output: null, cached: false, usage:
|
|
1784
|
+
const usage4 = { ...est, cost_usd: p.provider.estimateCost(est).cost_usd };
|
|
1785
|
+
return { vars: p.row.vars, output: null, cached: false, usage: usage4, assertions: [] };
|
|
1786
1786
|
}
|
|
1787
1787
|
const key = cacheKey(
|
|
1788
1788
|
{
|
|
@@ -1795,7 +1795,7 @@ async function runRow(p) {
|
|
|
1795
1795
|
p.policyVersion
|
|
1796
1796
|
);
|
|
1797
1797
|
let output;
|
|
1798
|
-
let
|
|
1798
|
+
let usage3;
|
|
1799
1799
|
let cached = false;
|
|
1800
1800
|
let cacheSaved;
|
|
1801
1801
|
let hit = p.noCache ? void 0 : p.cache.get(key, p.cacheMaxAgeMs !== void 0 ? { maxAgeMs: p.cacheMaxAgeMs } : {});
|
|
@@ -1816,7 +1816,7 @@ async function runRow(p) {
|
|
|
1816
1816
|
if (hit !== void 0) {
|
|
1817
1817
|
output = hit.output;
|
|
1818
1818
|
cached = true;
|
|
1819
|
-
|
|
1819
|
+
usage3 = {
|
|
1820
1820
|
input_tokens: hit.usage.input_tokens,
|
|
1821
1821
|
output_tokens: hit.usage.output_tokens,
|
|
1822
1822
|
cost_usd: 0
|
|
@@ -1836,7 +1836,7 @@ async function runRow(p) {
|
|
|
1836
1836
|
prompt_resolved: prompt5
|
|
1837
1837
|
});
|
|
1838
1838
|
output = res.output;
|
|
1839
|
-
|
|
1839
|
+
usage3 = {
|
|
1840
1840
|
input_tokens: res.usage.input_tokens,
|
|
1841
1841
|
output_tokens: res.usage.output_tokens,
|
|
1842
1842
|
// Prefer the provider-reported actual cost (res.cost_usd) when present; resolveCost still
|
|
@@ -1884,7 +1884,7 @@ async function runRow(p) {
|
|
|
1884
1884
|
break;
|
|
1885
1885
|
}
|
|
1886
1886
|
}
|
|
1887
|
-
const finalUsage = { ...
|
|
1887
|
+
const finalUsage = { ...usage3, cost_usd: usage3.cost_usd + judgeCost };
|
|
1888
1888
|
return {
|
|
1889
1889
|
vars: p.row.vars,
|
|
1890
1890
|
output,
|
|
@@ -2853,7 +2853,14 @@ function repoRootOf(dir, stopAt) {
|
|
|
2853
2853
|
if (here === stopAt || (0, import_node_path2.dirname)(here) === here) return void 0;
|
|
2854
2854
|
}
|
|
2855
2855
|
}
|
|
2856
|
-
function
|
|
2856
|
+
function errorContextFields(attempt, home = (0, import_node_os2.homedir)()) {
|
|
2857
|
+
const { text: text2, omitted: omitted2 } = cutErrorContext(attempt, home);
|
|
2858
|
+
return {
|
|
2859
|
+
...text2 !== "" ? { errorContext: text2 } : {},
|
|
2860
|
+
...omitted2 > 0 ? { errorContextOmitted: omitted2 } : {}
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
function cutErrorContext(attempt, home) {
|
|
2857
2864
|
const text2 = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
2858
2865
|
return cut(machineless(text2, attempt.repoRoot, home));
|
|
2859
2866
|
}
|
|
@@ -2879,7 +2886,7 @@ function pathIn(path17) {
|
|
|
2879
2886
|
return new RegExp(`${url}${escaped}`, /^[A-Za-z]:$/.test(parts[0]) ? "gi" : "g");
|
|
2880
2887
|
}
|
|
2881
2888
|
function cut(text2) {
|
|
2882
|
-
if (jsonBytes(text2) <= TEXT_LIMIT) return text2;
|
|
2889
|
+
if (jsonBytes(text2) <= TEXT_LIMIT) return { text: text2, omitted: 0 };
|
|
2883
2890
|
const lines = text2.split("\n");
|
|
2884
2891
|
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
2885
2892
|
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
@@ -2888,7 +2895,8 @@ function cut(text2) {
|
|
|
2888
2895
|
let tail = lines.length;
|
|
2889
2896
|
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
2890
2897
|
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
2891
|
-
|
|
2898
|
+
const kept = [...lines.slice(0, head), ...lines.slice(tail)].join("\n");
|
|
2899
|
+
return { text: [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n"), omitted: text2.length - kept.length };
|
|
2892
2900
|
}
|
|
2893
2901
|
function chosen({ status, errors }) {
|
|
2894
2902
|
if (status === "timedOut") {
|
|
@@ -4359,30 +4367,46 @@ async function handleSync(deps = {}) {
|
|
|
4359
4367
|
const endpoint = `${apiUrl}/v1/runs`;
|
|
4360
4368
|
const payload = JSON.stringify(run);
|
|
4361
4369
|
const idempotencyKey = (0, import_node_crypto4.createHash)("sha256").update(payload).digest("hex");
|
|
4362
|
-
|
|
4370
|
+
const bearer = token;
|
|
4371
|
+
const job = new JobReporter(doFetch, apiUrl, bearer);
|
|
4372
|
+
await job.open(path9.basename(deps.file !== void 0 ? path9.resolve(cwd, deps.file) : cwd));
|
|
4363
4373
|
try {
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
},
|
|
4371
|
-
body: payload
|
|
4372
|
-
});
|
|
4373
|
-
} catch {
|
|
4374
|
-
throw new SyncNetworkError(apiUrl);
|
|
4374
|
+
const result = await upload();
|
|
4375
|
+
await job.finish({ ok: true });
|
|
4376
|
+
return result;
|
|
4377
|
+
} catch (err) {
|
|
4378
|
+
await job.finish({ ok: false, reason: err instanceof Error ? err.message : "Sync failed." });
|
|
4379
|
+
throw err;
|
|
4375
4380
|
}
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4381
|
+
async function upload() {
|
|
4382
|
+
let res;
|
|
4383
|
+
try {
|
|
4384
|
+
res = await doFetch(endpoint, {
|
|
4385
|
+
method: "POST",
|
|
4386
|
+
headers: {
|
|
4387
|
+
"content-type": "application/json",
|
|
4388
|
+
authorization: `Bearer ${bearer}`,
|
|
4389
|
+
"idempotency-key": idempotencyKey
|
|
4390
|
+
},
|
|
4391
|
+
body: payload
|
|
4392
|
+
});
|
|
4393
|
+
} catch {
|
|
4394
|
+
throw new SyncNetworkError(apiUrl);
|
|
4395
|
+
}
|
|
4396
|
+
if (!res.ok) {
|
|
4397
|
+
const detail = await safeErrorDetail(res);
|
|
4398
|
+
if (res.status === 401) throw new TokenRejectedError();
|
|
4399
|
+
throw new SyncHttpError(res.status, detail);
|
|
4400
|
+
}
|
|
4401
|
+
const body = await res.json().catch(() => ({}));
|
|
4402
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
4403
|
+
const result = { id, url: `${endpoint}/${id}` };
|
|
4404
|
+
await job.report(deps.failOnFailures === true ? 80 : 95, id || void 0);
|
|
4405
|
+
if (deps.failOnFailures === true) {
|
|
4406
|
+
result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, bearer));
|
|
4407
|
+
}
|
|
4408
|
+
return result;
|
|
4380
4409
|
}
|
|
4381
|
-
const body = await res.json().catch(() => ({}));
|
|
4382
|
-
const id = typeof body.id === "string" ? body.id : "";
|
|
4383
|
-
const result = { id, url: `${endpoint}/${id}` };
|
|
4384
|
-
if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
|
|
4385
|
-
return result;
|
|
4386
4410
|
}
|
|
4387
4411
|
async function readVerdict(doFetch, url, token) {
|
|
4388
4412
|
try {
|
|
@@ -4404,7 +4428,7 @@ function reportSyncFailure(err, write) {
|
|
|
4404
4428
|
}
|
|
4405
4429
|
return null;
|
|
4406
4430
|
}
|
|
4407
|
-
var import_node_crypto4, fs8, path9, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError;
|
|
4431
|
+
var import_node_crypto4, fs8, path9, NotLoggedInError, TokenRejectedError, SyncFileError, SyncNetworkError, SyncHttpError, JOB_KIND, JOB_TIMEOUT_MS, JobReporter;
|
|
4408
4432
|
var init_sync = __esm({
|
|
4409
4433
|
"src/cli/commands/sync.ts"() {
|
|
4410
4434
|
"use strict";
|
|
@@ -4452,6 +4476,58 @@ var init_sync = __esm({
|
|
|
4452
4476
|
}
|
|
4453
4477
|
status;
|
|
4454
4478
|
};
|
|
4479
|
+
JOB_KIND = "cli-sync";
|
|
4480
|
+
JOB_TIMEOUT_MS = 5e3;
|
|
4481
|
+
JobReporter = class {
|
|
4482
|
+
constructor(doFetch, base, token) {
|
|
4483
|
+
this.doFetch = doFetch;
|
|
4484
|
+
this.base = base;
|
|
4485
|
+
this.token = token;
|
|
4486
|
+
}
|
|
4487
|
+
doFetch;
|
|
4488
|
+
base;
|
|
4489
|
+
token;
|
|
4490
|
+
id;
|
|
4491
|
+
async send(method, url, body) {
|
|
4492
|
+
try {
|
|
4493
|
+
const res = await this.doFetch(url, {
|
|
4494
|
+
method,
|
|
4495
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${this.token}` },
|
|
4496
|
+
body: JSON.stringify(body),
|
|
4497
|
+
signal: AbortSignal.timeout(JOB_TIMEOUT_MS)
|
|
4498
|
+
});
|
|
4499
|
+
const json = await res.json().catch(() => void 0);
|
|
4500
|
+
return res.ok ? json ?? {} : void 0;
|
|
4501
|
+
} catch {
|
|
4502
|
+
return void 0;
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
async open(subjectName) {
|
|
4506
|
+
const body = await this.send("POST", `${this.base}/v1/jobs`, {
|
|
4507
|
+
kind: JOB_KIND,
|
|
4508
|
+
subjectName,
|
|
4509
|
+
progress: 0
|
|
4510
|
+
});
|
|
4511
|
+
if (typeof body?.id === "string") this.id = body.id;
|
|
4512
|
+
}
|
|
4513
|
+
async report(progress, cursor) {
|
|
4514
|
+
if (this.id === void 0) return;
|
|
4515
|
+
await this.send("PATCH", `${this.base}/v1/jobs/${encodeURIComponent(this.id)}`, {
|
|
4516
|
+
progress,
|
|
4517
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
4518
|
+
});
|
|
4519
|
+
}
|
|
4520
|
+
async finish(outcome) {
|
|
4521
|
+
if (this.id === void 0) return;
|
|
4522
|
+
const id = this.id;
|
|
4523
|
+
this.id = void 0;
|
|
4524
|
+
await this.send(
|
|
4525
|
+
"PATCH",
|
|
4526
|
+
`${this.base}/v1/jobs/${encodeURIComponent(id)}`,
|
|
4527
|
+
outcome.ok ? { status: "succeeded", progress: 100 } : { status: "failed", reason: outcome.reason }
|
|
4528
|
+
);
|
|
4529
|
+
}
|
|
4530
|
+
};
|
|
4455
4531
|
}
|
|
4456
4532
|
});
|
|
4457
4533
|
|
|
@@ -5008,7 +5084,7 @@ function pendingOf(r, retry, ctx) {
|
|
|
5008
5084
|
})),
|
|
5009
5085
|
testFile: ""
|
|
5010
5086
|
};
|
|
5011
|
-
const errorContext =
|
|
5087
|
+
const errorContext = errorContextFields(attempt);
|
|
5012
5088
|
const failure = FAILED.has(status) ? failureOf(attempt) : void 0;
|
|
5013
5089
|
const pkg2 = labelOf(r, "package");
|
|
5014
5090
|
const where = (key.includes("#") ? key.split("#")[0] : pkg2)?.replace(/^[A-Za-z][A-Za-z0-9_-]+:(?![\\/])/, "");
|
|
@@ -5032,7 +5108,7 @@ function pendingOf(r, retry, ctx) {
|
|
|
5032
5108
|
retry
|
|
5033
5109
|
}
|
|
5034
5110
|
} : {},
|
|
5035
|
-
...errorContext
|
|
5111
|
+
...errorContext,
|
|
5036
5112
|
...failure !== void 0 ? { failure } : {},
|
|
5037
5113
|
...files.length > 0 ? { files } : {}
|
|
5038
5114
|
};
|
|
@@ -5362,7 +5438,7 @@ function pendingFrom(spec, test, result, file, titles, ctx) {
|
|
|
5362
5438
|
const stated = statedIdOf(test, spec.title ?? "");
|
|
5363
5439
|
const expected = test.expectedStatus;
|
|
5364
5440
|
const attempt = attemptOf(result, file, ctx);
|
|
5365
|
-
const errorContext =
|
|
5441
|
+
const errorContext = errorContextFields(attempt);
|
|
5366
5442
|
const failure = NOT_FAILED.has(attempt.status) ? void 0 : failureOf(attempt);
|
|
5367
5443
|
const files = filesOf(attempt);
|
|
5368
5444
|
const keys = [
|
|
@@ -5392,7 +5468,7 @@ function pendingFrom(spec, test, result, file, titles, ctx) {
|
|
|
5392
5468
|
...result.workerIndex !== void 0 ? { worker: String(result.workerIndex) } : {}
|
|
5393
5469
|
}
|
|
5394
5470
|
} : {},
|
|
5395
|
-
...errorContext
|
|
5471
|
+
...errorContext,
|
|
5396
5472
|
...failure !== void 0 ? { failure } : {},
|
|
5397
5473
|
...files.length > 0 ? { files } : {}
|
|
5398
5474
|
};
|
|
@@ -6509,12 +6585,18 @@ __export(requirements_import_exports, {
|
|
|
6509
6585
|
MAX_MARKDOWN_BYTES: () => MAX_MARKDOWN_BYTES,
|
|
6510
6586
|
RequirementsImportError: () => RequirementsImportError,
|
|
6511
6587
|
buildImportBody: () => buildImportBody,
|
|
6588
|
+
cut: () => cut2,
|
|
6589
|
+
flat: () => flat,
|
|
6512
6590
|
formatDiscover: () => formatDiscover,
|
|
6513
6591
|
formatReport: () => formatReport2,
|
|
6514
6592
|
handleRequirementsImport: () => handleRequirementsImport,
|
|
6515
6593
|
parseMap: () => parseMap,
|
|
6516
6594
|
parseTableSpec: () => parseTableSpec,
|
|
6517
|
-
|
|
6595
|
+
plural: () => plural,
|
|
6596
|
+
readMarkdown: () => readMarkdown,
|
|
6597
|
+
refusal: () => refusal,
|
|
6598
|
+
reportRequirementsImportFailure: () => reportRequirementsImportFailure,
|
|
6599
|
+
resolveProject: () => resolveProject
|
|
6518
6600
|
});
|
|
6519
6601
|
function parseMap(flag2, raw) {
|
|
6520
6602
|
const out = {};
|
|
@@ -6801,12 +6883,94 @@ var init_requirements_import = __esm({
|
|
|
6801
6883
|
}
|
|
6802
6884
|
});
|
|
6803
6885
|
|
|
6886
|
+
// src/cli/commands/requirements-links.ts
|
|
6887
|
+
var requirements_links_exports = {};
|
|
6888
|
+
__export(requirements_links_exports, {
|
|
6889
|
+
buildLinksBody: () => buildLinksBody,
|
|
6890
|
+
formatLinks: () => formatLinks,
|
|
6891
|
+
formatLinksDiscover: () => formatLinksDiscover,
|
|
6892
|
+
handleRequirementsLinks: () => handleRequirementsLinks
|
|
6893
|
+
});
|
|
6894
|
+
function buildLinksBody(f) {
|
|
6895
|
+
if (f.table === void 0 && f.requirementColumn === void 0 && f.testColumn === void 0 && f.separator === void 0) return null;
|
|
6896
|
+
if (f.table === void 0) return usage2("--requirement-column, --test-column and --separator need --table <n>. Run without flags to see which tables the file has.");
|
|
6897
|
+
if (!/^\d+$/.test(f.table)) usage2(`--table is the number of the table in the list (0, 1, ...), not "${f.table}".`);
|
|
6898
|
+
if (f.requirementColumn === void 0 || f.requirementColumn.trim() === "") usage2("--table needs --requirement-column <header> (the column with the requirement keys).");
|
|
6899
|
+
if (f.testColumn === void 0 || f.testColumn.trim() === "") usage2("--table needs --test-column <header> (the column with the test titles or paths).");
|
|
6900
|
+
const body = { table: Number(f.table), requirementColumn: f.requirementColumn, testColumn: f.testColumn };
|
|
6901
|
+
if (f.separator !== void 0) {
|
|
6902
|
+
if (f.separator.length < 1 || f.separator.length > 3) usage2(`--separator is 1 to 3 characters, not "${f.separator}".`);
|
|
6903
|
+
body["requirementSeparator"] = f.separator;
|
|
6904
|
+
}
|
|
6905
|
+
return body;
|
|
6906
|
+
}
|
|
6907
|
+
async function handleRequirementsLinks(deps) {
|
|
6908
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
6909
|
+
const mapping = buildLinksBody(deps);
|
|
6910
|
+
const markdown = readMarkdown(deps.cwd ?? process.cwd(), deps.file);
|
|
6911
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
6912
|
+
if (token === null) throw new RequirementsImportError('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.', 2);
|
|
6913
|
+
const client = createClient({ apiUrl, token, ...deps.fetchImpl !== void 0 ? { fetchImpl: deps.fetchImpl } : {} });
|
|
6914
|
+
const projectId = await resolveProject(client, deps.project, apiUrl);
|
|
6915
|
+
const base = `/v1/projects/${encodeURIComponent(projectId)}/requirements/import`;
|
|
6916
|
+
if (mapping === null) {
|
|
6917
|
+
const out2 = await client.post(`${base}/discover`, { markdown });
|
|
6918
|
+
if (!out2.ok) throw refusal(out2, apiUrl, projectId);
|
|
6919
|
+
return { step: "discover", result: out2.body };
|
|
6920
|
+
}
|
|
6921
|
+
const out = await client.post(`${base}/links`, { markdown, dryRun: deps.yes !== true, ...mapping });
|
|
6922
|
+
if (!out.ok) throw refusal(out, apiUrl, projectId);
|
|
6923
|
+
return { step: "links", report: out.body };
|
|
6924
|
+
}
|
|
6925
|
+
function formatLinksDiscover(file, r) {
|
|
6926
|
+
const out = [`${file}: ${plural(r.tables.length, "table")}`, ""];
|
|
6927
|
+
if (r.tables.length === 0) out.push(" (none)");
|
|
6928
|
+
for (const t of r.tables) {
|
|
6929
|
+
const mark = t.unreadable !== void 0 ? ` cannot be read: ${t.unreadable}` : "";
|
|
6930
|
+
out.push(` #${t.index} line ${t.line} ${t.heading === null ? "(no heading)" : `"${flat(t.heading)}"`} ${cut2(t.headers.map(flat).join(" | "), 100)} ${plural(t.rowCount, "row")}${mark}`);
|
|
6931
|
+
}
|
|
6932
|
+
if (r.truncated) out.push("", "The file has more tables than are listed here.");
|
|
6933
|
+
out.push("", "Pick the matrix and name its two columns (a dry run, nothing is written):", ` plune requirements links ${file} --table <n> --requirement-column <header> --test-column <header>`);
|
|
6934
|
+
return out.join("\n") + "\n";
|
|
6935
|
+
}
|
|
6936
|
+
function formatLinks(report) {
|
|
6937
|
+
const s = report.summary;
|
|
6938
|
+
const out = [`link ${s.link} \xB7 unchanged ${s.unchanged} \xB7 unmatched ${s.unmatched}`];
|
|
6939
|
+
for (const action of ["unmatched", "link", "unchanged"]) {
|
|
6940
|
+
const rows = report.items.filter((i) => i.action === action);
|
|
6941
|
+
if (rows.length === 0) continue;
|
|
6942
|
+
out.push("", `${action} (${rows.length})`);
|
|
6943
|
+
for (const i of rows.slice(0, SHOWN2)) {
|
|
6944
|
+
out.push(` line ${i.line} ${i.key === "" ? "-" : i.key} -> ${i.test === "" ? "-" : cut2(flat(i.test), 80)} - ${flat(i.reason)}`);
|
|
6945
|
+
}
|
|
6946
|
+
if (rows.length > SHOWN2) out.push(` ... and ${rows.length - SHOWN2} more (--json lists every row)`);
|
|
6947
|
+
}
|
|
6948
|
+
out.push("", report.applied ? `applied \u2014 ${plural(s.link, "link")} created.` : "dry run \u2014 nothing was written; add --yes to apply");
|
|
6949
|
+
return out.join("\n") + "\n";
|
|
6950
|
+
}
|
|
6951
|
+
var usage2, SHOWN2;
|
|
6952
|
+
var init_requirements_links = __esm({
|
|
6953
|
+
"src/cli/commands/requirements-links.ts"() {
|
|
6954
|
+
"use strict";
|
|
6955
|
+
init_cjs_shims();
|
|
6956
|
+
init_client();
|
|
6957
|
+
init_api_url();
|
|
6958
|
+
init_credentials();
|
|
6959
|
+
init_requirements_import();
|
|
6960
|
+
usage2 = (message) => {
|
|
6961
|
+
throw new RequirementsImportError(message, 2);
|
|
6962
|
+
};
|
|
6963
|
+
SHOWN2 = 50;
|
|
6964
|
+
}
|
|
6965
|
+
});
|
|
6966
|
+
|
|
6804
6967
|
// src/cli/commands/verify.ts
|
|
6805
6968
|
var verify_exports = {};
|
|
6806
6969
|
__export(verify_exports, {
|
|
6807
6970
|
compare: () => compare,
|
|
6808
6971
|
formatVerify: () => formatVerify,
|
|
6809
6972
|
handleVerify: () => handleVerify,
|
|
6973
|
+
loadSources: () => loadSources,
|
|
6810
6974
|
testsOfListing: () => testsOfListing
|
|
6811
6975
|
});
|
|
6812
6976
|
function testsOfListing(listing) {
|
|
@@ -6859,7 +7023,7 @@ function runPlaywrightList(args, cwd) {
|
|
|
6859
7023
|
throw new SyncFileError(`could not list the Playwright tests${stderr ? `: ${stderr}` : ""}`);
|
|
6860
7024
|
}
|
|
6861
7025
|
}
|
|
6862
|
-
async function
|
|
7026
|
+
async function loadSources(deps) {
|
|
6863
7027
|
const get = platformGet(deps);
|
|
6864
7028
|
let raw;
|
|
6865
7029
|
if (deps.from !== void 0) {
|
|
@@ -6891,7 +7055,11 @@ async function handleVerify(deps) {
|
|
|
6891
7055
|
cases.push(...page.testCases);
|
|
6892
7056
|
cursor = page.nextCursor;
|
|
6893
7057
|
} while (cursor !== null);
|
|
6894
|
-
return
|
|
7058
|
+
return { code: testsOfListing(listing), cases };
|
|
7059
|
+
}
|
|
7060
|
+
async function handleVerify(deps) {
|
|
7061
|
+
const { code, cases } = await loadSources(deps);
|
|
7062
|
+
return compare(code, cases);
|
|
6895
7063
|
}
|
|
6896
7064
|
function formatVerify(r) {
|
|
6897
7065
|
const lines = [];
|
|
@@ -6925,6 +7093,111 @@ var init_verify = __esm({
|
|
|
6925
7093
|
}
|
|
6926
7094
|
});
|
|
6927
7095
|
|
|
7096
|
+
// src/cli/commands/ids.ts
|
|
7097
|
+
var ids_exports = {};
|
|
7098
|
+
__export(ids_exports, {
|
|
7099
|
+
DEFAULT_IDS_FILE: () => DEFAULT_IDS_FILE,
|
|
7100
|
+
formatIds: () => formatIds,
|
|
7101
|
+
handleIds: () => handleIds,
|
|
7102
|
+
planIds: () => planIds,
|
|
7103
|
+
readMap: () => readMap,
|
|
7104
|
+
serializeMap: () => serializeMap
|
|
7105
|
+
});
|
|
7106
|
+
function readMap(file) {
|
|
7107
|
+
if (!(0, import_node_fs9.existsSync)(file)) return {};
|
|
7108
|
+
let parsed;
|
|
7109
|
+
try {
|
|
7110
|
+
parsed = JSON.parse((0, import_node_fs9.readFileSync)(file, "utf8"));
|
|
7111
|
+
} catch {
|
|
7112
|
+
throw new SyncFileError(`${file} is not valid JSON; fix or remove it, nothing was written`);
|
|
7113
|
+
}
|
|
7114
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || Object.values(parsed).some((v) => typeof v !== "string" || v === ""))
|
|
7115
|
+
throw new SyncFileError(
|
|
7116
|
+
`${file} must be an object of path-title to case id strings; nothing was written`
|
|
7117
|
+
);
|
|
7118
|
+
return parsed;
|
|
7119
|
+
}
|
|
7120
|
+
function planIds(code, cases, current) {
|
|
7121
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
7122
|
+
for (const c of cases)
|
|
7123
|
+
if (c.type === "automated" && c.state !== "deprecated") {
|
|
7124
|
+
for (const k of c.externalKeys ?? []) if (k.kind === "path-title") byPath.set(k.value, c);
|
|
7125
|
+
}
|
|
7126
|
+
const added = {};
|
|
7127
|
+
const noCase = [];
|
|
7128
|
+
let covered = 0;
|
|
7129
|
+
for (const t of code) {
|
|
7130
|
+
if (t.id !== void 0 || current[t.pathTitle] !== void 0) covered++;
|
|
7131
|
+
else {
|
|
7132
|
+
const hit = byPath.get(t.pathTitle);
|
|
7133
|
+
if (hit === void 0) noCase.push(t.pathTitle);
|
|
7134
|
+
else added[t.pathTitle] = hit.id;
|
|
7135
|
+
}
|
|
7136
|
+
}
|
|
7137
|
+
const inCode = new Set(code.map((t) => t.pathTitle));
|
|
7138
|
+
return {
|
|
7139
|
+
added,
|
|
7140
|
+
covered,
|
|
7141
|
+
noCase: noCase.sort(compareKeys),
|
|
7142
|
+
orphaned: Object.keys(current).filter((k) => !inCode.has(k)).sort(compareKeys)
|
|
7143
|
+
};
|
|
7144
|
+
}
|
|
7145
|
+
function serializeMap(map) {
|
|
7146
|
+
const sorted = {};
|
|
7147
|
+
for (const k of Object.keys(map).sort(compareKeys)) sorted[k] = map[k];
|
|
7148
|
+
return JSON.stringify(sorted, null, 2) + "\n";
|
|
7149
|
+
}
|
|
7150
|
+
async function handleIds(deps) {
|
|
7151
|
+
const file = deps.file ?? DEFAULT_IDS_FILE;
|
|
7152
|
+
const current = readMap(file);
|
|
7153
|
+
const { code, cases } = await loadSources(deps);
|
|
7154
|
+
const plan = planIds(code, cases, current);
|
|
7155
|
+
let written = false;
|
|
7156
|
+
if (deps.write === true && Object.keys(plan.added).length > 0) {
|
|
7157
|
+
try {
|
|
7158
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path5.dirname)(file), { recursive: true });
|
|
7159
|
+
(0, import_node_fs9.writeFileSync)(file, serializeMap({ ...current, ...plan.added }));
|
|
7160
|
+
} catch {
|
|
7161
|
+
throw new SyncFileError(`cannot write ${file}`);
|
|
7162
|
+
}
|
|
7163
|
+
written = true;
|
|
7164
|
+
}
|
|
7165
|
+
return { file, ...plan, written };
|
|
7166
|
+
}
|
|
7167
|
+
function formatIds(r, write) {
|
|
7168
|
+
const n = Object.keys(r.added).length;
|
|
7169
|
+
const lines = [];
|
|
7170
|
+
if (n > 0) {
|
|
7171
|
+
lines.push(`${write ? "Added to" : "Would add to"} ${r.file} (${n}):`);
|
|
7172
|
+
for (const k of Object.keys(r.added).sort(compareKeys)) lines.push(` ${k} -> ${r.added[k]}`);
|
|
7173
|
+
}
|
|
7174
|
+
if (r.noCase.length > 0) {
|
|
7175
|
+
lines.push(`In the code, no case in Plune yet, nothing to write (${r.noCase.length}):`);
|
|
7176
|
+
for (const k of r.noCase) lines.push(` ${k}`);
|
|
7177
|
+
}
|
|
7178
|
+
if (r.orphaned.length > 0) {
|
|
7179
|
+
lines.push(`In ${r.file}, test no longer in the code, kept (${r.orphaned.length}):`);
|
|
7180
|
+
for (const k of r.orphaned) lines.push(` ${k}`);
|
|
7181
|
+
}
|
|
7182
|
+
lines.push(
|
|
7183
|
+
n === 0 ? `Nothing to write: ${r.covered} test(s) already have an id.` : write ? `${r.covered} already had an id. ${r.file} written.` : `${r.covered} already had an id. Dry run: nothing was written; add --write.`
|
|
7184
|
+
);
|
|
7185
|
+
return lines.join("\n") + "\n";
|
|
7186
|
+
}
|
|
7187
|
+
var import_node_fs9, import_node_path5, DEFAULT_IDS_FILE, compareKeys;
|
|
7188
|
+
var init_ids = __esm({
|
|
7189
|
+
"src/cli/commands/ids.ts"() {
|
|
7190
|
+
"use strict";
|
|
7191
|
+
init_cjs_shims();
|
|
7192
|
+
import_node_fs9 = require("fs");
|
|
7193
|
+
import_node_path5 = require("path");
|
|
7194
|
+
init_verify();
|
|
7195
|
+
init_sync();
|
|
7196
|
+
DEFAULT_IDS_FILE = "plune/ids.json";
|
|
7197
|
+
compareKeys = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
7198
|
+
}
|
|
7199
|
+
});
|
|
7200
|
+
|
|
6928
7201
|
// src/cli.ts
|
|
6929
7202
|
var cli_exports = {};
|
|
6930
7203
|
__export(cli_exports, {
|
|
@@ -6934,8 +7207,8 @@ module.exports = __toCommonJS(cli_exports);
|
|
|
6934
7207
|
init_cjs_shims();
|
|
6935
7208
|
init_api_url();
|
|
6936
7209
|
var import_commander = require("commander");
|
|
6937
|
-
var
|
|
6938
|
-
var
|
|
7210
|
+
var import_node_fs10 = require("fs");
|
|
7211
|
+
var import_node_path6 = require("path");
|
|
6939
7212
|
var import_node_url = require("url");
|
|
6940
7213
|
|
|
6941
7214
|
// src/util/duration.ts
|
|
@@ -6951,8 +7224,8 @@ function parseDuration(text2) {
|
|
|
6951
7224
|
}
|
|
6952
7225
|
|
|
6953
7226
|
// src/cli.ts
|
|
6954
|
-
var __dirname = (0,
|
|
6955
|
-
var pkg = JSON.parse((0,
|
|
7227
|
+
var __dirname = (0, import_node_path6.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
7228
|
+
var pkg = JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path6.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
6956
7229
|
function toFormat(s) {
|
|
6957
7230
|
return s === "json" || s === "markdown" ? s : "console";
|
|
6958
7231
|
}
|
|
@@ -6961,7 +7234,7 @@ function shouldColor(outputPath) {
|
|
|
6961
7234
|
}
|
|
6962
7235
|
function writeReport(text2, outputPath) {
|
|
6963
7236
|
if (outputPath !== void 0) {
|
|
6964
|
-
(0,
|
|
7237
|
+
(0, import_node_fs10.writeFileSync)(outputPath, text2);
|
|
6965
7238
|
} else {
|
|
6966
7239
|
process.stdout.write(text2 + "\n");
|
|
6967
7240
|
}
|
|
@@ -7006,7 +7279,7 @@ function createProgram() {
|
|
|
7006
7279
|
program.hook("preAction", async (_program, action) => {
|
|
7007
7280
|
const { loadEnv: loadEnv2 } = await Promise.resolve().then(() => (init_env(), env_exports));
|
|
7008
7281
|
const { config } = action.optsWithGlobals();
|
|
7009
|
-
if (config !== void 0) loadEnv2((0,
|
|
7282
|
+
if (config !== void 0) loadEnv2((0, import_node_path6.dirname)((0, import_node_path6.resolve)(config)));
|
|
7010
7283
|
loadEnv2(process.cwd());
|
|
7011
7284
|
});
|
|
7012
7285
|
const runCommand = program.command("run").description("Run assertions against a dataset").option("--dry-run", "Estimate cost/tokens; do not call the model", false).option("--config <path>", "Path to plune.yaml config file").option(
|
|
@@ -7286,7 +7559,7 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7286
7559
|
Promise.resolve().then(() => (init_report(), report_exports)),
|
|
7287
7560
|
Promise.resolve().then(() => (init_reporters(), reporters_exports))
|
|
7288
7561
|
]);
|
|
7289
|
-
const cwd = globals.config !== void 0 ? (0,
|
|
7562
|
+
const cwd = globals.config !== void 0 ? (0, import_node_path6.dirname)((0, import_node_path6.resolve)(globals.config)) : process.cwd();
|
|
7290
7563
|
try {
|
|
7291
7564
|
const result = handleReport2({ cwd });
|
|
7292
7565
|
const text2 = renderReport2(result, toFormat(options.format), {
|
|
@@ -7383,7 +7656,7 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7383
7656
|
const globals = command.optsWithGlobals();
|
|
7384
7657
|
const verbose = globals.verbose === true;
|
|
7385
7658
|
const { handleSync: handleSync2, reportSyncFailure: reportSyncFailure2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
7386
|
-
const cwd = globals.config !== void 0 ? (0,
|
|
7659
|
+
const cwd = globals.config !== void 0 ? (0, import_node_path6.dirname)((0, import_node_path6.resolve)(globals.config)) : process.cwd();
|
|
7387
7660
|
try {
|
|
7388
7661
|
const { id, url, gate } = await handleSync2({
|
|
7389
7662
|
cwd,
|
|
@@ -7472,7 +7745,7 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7472
7745
|
failUnexpected(err, verbose);
|
|
7473
7746
|
}
|
|
7474
7747
|
});
|
|
7475
|
-
const requirementsCommand = program.command("requirements").description("Requirements on the platform \u2014 import them from a Markdown file");
|
|
7748
|
+
const requirementsCommand = program.command("requirements").description("Requirements on the platform \u2014 import them, or their links to tests, from a Markdown file");
|
|
7476
7749
|
requirementsCommand.command("import").argument("<file>", "A Markdown file (UTF-8, at most 1 MiB)").description(
|
|
7477
7750
|
"Import requirements from a Markdown file. With no mapping flags it lists the tables and sections found; with them it is a dry run until --yes"
|
|
7478
7751
|
).option(
|
|
@@ -7499,6 +7772,30 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7499
7772
|
failUnexpected(err, verbose);
|
|
7500
7773
|
}
|
|
7501
7774
|
});
|
|
7775
|
+
requirementsCommand.command("links").argument("<file>", "A Markdown file with a traceability matrix (UTF-8, at most 1 MiB)").description(
|
|
7776
|
+
"Import a traceability matrix (requirement key -> test) as links. With no flags it lists the tables found; with them it is a dry run until --yes"
|
|
7777
|
+
).option("--table <n>", "The table with the matrix (its number from the list)").option("--requirement-column <header>", "The column holding the requirement keys").option("--test-column <header>", "The column holding the tests: an exact case title or a source path, one per line").option("--separator <text>", 'Splits a requirement cell into several keys, 1 to 3 characters (default ",")').option("--project <id>", "The project (default: PLUNE_PROJECT, else the only project of the account)").option("--yes", "Write it. Without it the import is a dry run", false).option("--json", "Print the platform\u2019s answer as JSON", false).option("--fail-on-unmatched", "Exit 1 when any requirement or test of the matrix matched nothing", false).action(async (file, options, command) => {
|
|
7778
|
+
const verbose = command.optsWithGlobals().verbose === true;
|
|
7779
|
+
const { handleRequirementsLinks: handleRequirementsLinks2, formatLinksDiscover: formatLinksDiscover2, formatLinks: formatLinks2 } = await Promise.resolve().then(() => (init_requirements_links(), requirements_links_exports));
|
|
7780
|
+
const { reportRequirementsImportFailure: reportRequirementsImportFailure2 } = await Promise.resolve().then(() => (init_requirements_import(), requirements_import_exports));
|
|
7781
|
+
try {
|
|
7782
|
+
const res = await handleRequirementsLinks2({ file, ...options });
|
|
7783
|
+
if (options.json === true) {
|
|
7784
|
+
process.stdout.write(JSON.stringify(res.step === "discover" ? res.result : res.report, null, 2) + "\n");
|
|
7785
|
+
} else {
|
|
7786
|
+
process.stdout.write(res.step === "discover" ? formatLinksDiscover2(file, res.result) : formatLinks2(res.report));
|
|
7787
|
+
}
|
|
7788
|
+
if (options.failOnUnmatched === true && res.step === "links" && res.report.summary.unmatched > 0) process.exitCode = 1;
|
|
7789
|
+
} catch (err) {
|
|
7790
|
+
const code = reportRequirementsImportFailure2(err, (s) => process.stderr.write(s));
|
|
7791
|
+
if (code !== null) {
|
|
7792
|
+
maybeStack(err, verbose);
|
|
7793
|
+
process.exitCode = code;
|
|
7794
|
+
return;
|
|
7795
|
+
}
|
|
7796
|
+
failUnexpected(err, verbose);
|
|
7797
|
+
}
|
|
7798
|
+
});
|
|
7502
7799
|
const planCommand = program.command("plan").description("Test plans on the platform \u2014 what a plan collects, as something a runner can take");
|
|
7503
7800
|
planCommand.command("grep").argument("<id>", "The plan").description(
|
|
7504
7801
|
"Print the plan's cases as one --grep pattern for the runner (GET /v1/plans/:id/cases)"
|
|
@@ -7548,6 +7845,32 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7548
7845
|
}
|
|
7549
7846
|
}
|
|
7550
7847
|
);
|
|
7848
|
+
program.command("ids").description(
|
|
7849
|
+
"Write the Plune case id of each Playwright test that names none into plune/ids.json (dry run unless --write); test sources are not touched"
|
|
7850
|
+
).option("--write", "Write the file; without it only shows what would be written", false).option("--file <path>", "The mapfile", "plune/ids.json").option("--from <file>", "A saved `playwright test --list --reporter=json` document instead of running Playwright").option("--playwright-config <path>", "Passed to `playwright test --config`").option("--json", "Print the report as JSON", false).action(
|
|
7851
|
+
async (options, command) => {
|
|
7852
|
+
const verbose = verboseOf(command);
|
|
7853
|
+
const { handleIds: handleIds2, formatIds: formatIds2 } = await Promise.resolve().then(() => (init_ids(), ids_exports));
|
|
7854
|
+
const { reportCasesFailure: reportCasesFailure2 } = await Promise.resolve().then(() => (init_cases(), cases_exports));
|
|
7855
|
+
try {
|
|
7856
|
+
const report = await handleIds2({
|
|
7857
|
+
write: options.write,
|
|
7858
|
+
file: options.file,
|
|
7859
|
+
...options.from === void 0 ? {} : { from: options.from },
|
|
7860
|
+
...options.playwrightConfig === void 0 ? {} : { playwrightConfig: options.playwrightConfig }
|
|
7861
|
+
});
|
|
7862
|
+
process.stdout.write(options.json ? JSON.stringify(report, null, 2) + "\n" : formatIds2(report, options.write));
|
|
7863
|
+
} catch (err) {
|
|
7864
|
+
const code = reportCasesFailure2(err, (s) => process.stderr.write(s));
|
|
7865
|
+
if (code !== null) {
|
|
7866
|
+
maybeStack(err, verbose);
|
|
7867
|
+
process.exitCode = code;
|
|
7868
|
+
return;
|
|
7869
|
+
}
|
|
7870
|
+
failUnexpected(err, verbose);
|
|
7871
|
+
}
|
|
7872
|
+
}
|
|
7873
|
+
);
|
|
7551
7874
|
program.on("command:*", (operands) => {
|
|
7552
7875
|
process.stderr.write(`Unknown command: ${operands.join(" ")}
|
|
7553
7876
|
`);
|
|
@@ -7559,7 +7882,7 @@ function isMainModule() {
|
|
|
7559
7882
|
const entry = process.argv[1];
|
|
7560
7883
|
if (entry === void 0) return false;
|
|
7561
7884
|
try {
|
|
7562
|
-
return (0,
|
|
7885
|
+
return (0, import_node_fs10.realpathSync)(entry) === (0, import_node_fs10.realpathSync)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
7563
7886
|
} catch {
|
|
7564
7887
|
return false;
|
|
7565
7888
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -1892,6 +1892,58 @@ var SyncHttpError = class extends Error {
|
|
|
1892
1892
|
}
|
|
1893
1893
|
status;
|
|
1894
1894
|
};
|
|
1895
|
+
var JOB_KIND = "cli-sync";
|
|
1896
|
+
var JOB_TIMEOUT_MS = 5e3;
|
|
1897
|
+
var JobReporter = class {
|
|
1898
|
+
constructor(doFetch, base, token) {
|
|
1899
|
+
this.doFetch = doFetch;
|
|
1900
|
+
this.base = base;
|
|
1901
|
+
this.token = token;
|
|
1902
|
+
}
|
|
1903
|
+
doFetch;
|
|
1904
|
+
base;
|
|
1905
|
+
token;
|
|
1906
|
+
id;
|
|
1907
|
+
async send(method, url, body) {
|
|
1908
|
+
try {
|
|
1909
|
+
const res = await this.doFetch(url, {
|
|
1910
|
+
method,
|
|
1911
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${this.token}` },
|
|
1912
|
+
body: JSON.stringify(body),
|
|
1913
|
+
signal: AbortSignal.timeout(JOB_TIMEOUT_MS)
|
|
1914
|
+
});
|
|
1915
|
+
const json = await res.json().catch(() => void 0);
|
|
1916
|
+
return res.ok ? json ?? {} : void 0;
|
|
1917
|
+
} catch {
|
|
1918
|
+
return void 0;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
async open(subjectName) {
|
|
1922
|
+
const body = await this.send("POST", `${this.base}/v1/jobs`, {
|
|
1923
|
+
kind: JOB_KIND,
|
|
1924
|
+
subjectName,
|
|
1925
|
+
progress: 0
|
|
1926
|
+
});
|
|
1927
|
+
if (typeof body?.id === "string") this.id = body.id;
|
|
1928
|
+
}
|
|
1929
|
+
async report(progress, cursor) {
|
|
1930
|
+
if (this.id === void 0) return;
|
|
1931
|
+
await this.send("PATCH", `${this.base}/v1/jobs/${encodeURIComponent(this.id)}`, {
|
|
1932
|
+
progress,
|
|
1933
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
async finish(outcome) {
|
|
1937
|
+
if (this.id === void 0) return;
|
|
1938
|
+
const id = this.id;
|
|
1939
|
+
this.id = void 0;
|
|
1940
|
+
await this.send(
|
|
1941
|
+
"PATCH",
|
|
1942
|
+
`${this.base}/v1/jobs/${encodeURIComponent(id)}`,
|
|
1943
|
+
outcome.ok ? { status: "succeeded", progress: 100 } : { status: "failed", reason: outcome.reason }
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
};
|
|
1895
1947
|
function readRun(cwd, file) {
|
|
1896
1948
|
const target = file !== void 0 ? path5.resolve(cwd, file) : path5.join(cwd, ".plune", "last-run.json");
|
|
1897
1949
|
let raw;
|
|
@@ -1932,30 +1984,46 @@ async function handleSync(deps = {}) {
|
|
|
1932
1984
|
const endpoint = `${apiUrl}/v1/runs`;
|
|
1933
1985
|
const payload = JSON.stringify(run);
|
|
1934
1986
|
const idempotencyKey = (0, import_node_crypto2.createHash)("sha256").update(payload).digest("hex");
|
|
1935
|
-
|
|
1987
|
+
const bearer = token;
|
|
1988
|
+
const job = new JobReporter(doFetch, apiUrl, bearer);
|
|
1989
|
+
await job.open(path5.basename(deps.file !== void 0 ? path5.resolve(cwd, deps.file) : cwd));
|
|
1936
1990
|
try {
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
},
|
|
1944
|
-
body: payload
|
|
1945
|
-
});
|
|
1946
|
-
} catch {
|
|
1947
|
-
throw new SyncNetworkError(apiUrl);
|
|
1991
|
+
const result = await upload();
|
|
1992
|
+
await job.finish({ ok: true });
|
|
1993
|
+
return result;
|
|
1994
|
+
} catch (err) {
|
|
1995
|
+
await job.finish({ ok: false, reason: err instanceof Error ? err.message : "Sync failed." });
|
|
1996
|
+
throw err;
|
|
1948
1997
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1998
|
+
async function upload() {
|
|
1999
|
+
let res;
|
|
2000
|
+
try {
|
|
2001
|
+
res = await doFetch(endpoint, {
|
|
2002
|
+
method: "POST",
|
|
2003
|
+
headers: {
|
|
2004
|
+
"content-type": "application/json",
|
|
2005
|
+
authorization: `Bearer ${bearer}`,
|
|
2006
|
+
"idempotency-key": idempotencyKey
|
|
2007
|
+
},
|
|
2008
|
+
body: payload
|
|
2009
|
+
});
|
|
2010
|
+
} catch {
|
|
2011
|
+
throw new SyncNetworkError(apiUrl);
|
|
2012
|
+
}
|
|
2013
|
+
if (!res.ok) {
|
|
2014
|
+
const detail = await safeErrorDetail(res);
|
|
2015
|
+
if (res.status === 401) throw new TokenRejectedError();
|
|
2016
|
+
throw new SyncHttpError(res.status, detail);
|
|
2017
|
+
}
|
|
2018
|
+
const body = await res.json().catch(() => ({}));
|
|
2019
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
2020
|
+
const result = { id, url: `${endpoint}/${id}` };
|
|
2021
|
+
await job.report(deps.failOnFailures === true ? 80 : 95, id || void 0);
|
|
2022
|
+
if (deps.failOnFailures === true) {
|
|
2023
|
+
result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, bearer));
|
|
2024
|
+
}
|
|
2025
|
+
return result;
|
|
1953
2026
|
}
|
|
1954
|
-
const body = await res.json().catch(() => ({}));
|
|
1955
|
-
const id = typeof body.id === "string" ? body.id : "";
|
|
1956
|
-
const result = { id, url: `${endpoint}/${id}` };
|
|
1957
|
-
if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
|
|
1958
|
-
return result;
|
|
1959
2027
|
}
|
|
1960
2028
|
async function readVerdict(doFetch, url, token) {
|
|
1961
2029
|
try {
|
package/dist/index.js
CHANGED
|
@@ -1832,6 +1832,58 @@ var SyncHttpError = class extends Error {
|
|
|
1832
1832
|
}
|
|
1833
1833
|
status;
|
|
1834
1834
|
};
|
|
1835
|
+
var JOB_KIND = "cli-sync";
|
|
1836
|
+
var JOB_TIMEOUT_MS = 5e3;
|
|
1837
|
+
var JobReporter = class {
|
|
1838
|
+
constructor(doFetch, base, token) {
|
|
1839
|
+
this.doFetch = doFetch;
|
|
1840
|
+
this.base = base;
|
|
1841
|
+
this.token = token;
|
|
1842
|
+
}
|
|
1843
|
+
doFetch;
|
|
1844
|
+
base;
|
|
1845
|
+
token;
|
|
1846
|
+
id;
|
|
1847
|
+
async send(method, url, body) {
|
|
1848
|
+
try {
|
|
1849
|
+
const res = await this.doFetch(url, {
|
|
1850
|
+
method,
|
|
1851
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${this.token}` },
|
|
1852
|
+
body: JSON.stringify(body),
|
|
1853
|
+
signal: AbortSignal.timeout(JOB_TIMEOUT_MS)
|
|
1854
|
+
});
|
|
1855
|
+
const json = await res.json().catch(() => void 0);
|
|
1856
|
+
return res.ok ? json ?? {} : void 0;
|
|
1857
|
+
} catch {
|
|
1858
|
+
return void 0;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
async open(subjectName) {
|
|
1862
|
+
const body = await this.send("POST", `${this.base}/v1/jobs`, {
|
|
1863
|
+
kind: JOB_KIND,
|
|
1864
|
+
subjectName,
|
|
1865
|
+
progress: 0
|
|
1866
|
+
});
|
|
1867
|
+
if (typeof body?.id === "string") this.id = body.id;
|
|
1868
|
+
}
|
|
1869
|
+
async report(progress, cursor) {
|
|
1870
|
+
if (this.id === void 0) return;
|
|
1871
|
+
await this.send("PATCH", `${this.base}/v1/jobs/${encodeURIComponent(this.id)}`, {
|
|
1872
|
+
progress,
|
|
1873
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
async finish(outcome) {
|
|
1877
|
+
if (this.id === void 0) return;
|
|
1878
|
+
const id = this.id;
|
|
1879
|
+
this.id = void 0;
|
|
1880
|
+
await this.send(
|
|
1881
|
+
"PATCH",
|
|
1882
|
+
`${this.base}/v1/jobs/${encodeURIComponent(id)}`,
|
|
1883
|
+
outcome.ok ? { status: "succeeded", progress: 100 } : { status: "failed", reason: outcome.reason }
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1835
1887
|
function readRun(cwd, file) {
|
|
1836
1888
|
const target = file !== void 0 ? path5.resolve(cwd, file) : path5.join(cwd, ".plune", "last-run.json");
|
|
1837
1889
|
let raw;
|
|
@@ -1872,30 +1924,46 @@ async function handleSync(deps = {}) {
|
|
|
1872
1924
|
const endpoint = `${apiUrl}/v1/runs`;
|
|
1873
1925
|
const payload = JSON.stringify(run);
|
|
1874
1926
|
const idempotencyKey = createHash2("sha256").update(payload).digest("hex");
|
|
1875
|
-
|
|
1927
|
+
const bearer = token;
|
|
1928
|
+
const job = new JobReporter(doFetch, apiUrl, bearer);
|
|
1929
|
+
await job.open(path5.basename(deps.file !== void 0 ? path5.resolve(cwd, deps.file) : cwd));
|
|
1876
1930
|
try {
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
},
|
|
1884
|
-
body: payload
|
|
1885
|
-
});
|
|
1886
|
-
} catch {
|
|
1887
|
-
throw new SyncNetworkError(apiUrl);
|
|
1931
|
+
const result = await upload();
|
|
1932
|
+
await job.finish({ ok: true });
|
|
1933
|
+
return result;
|
|
1934
|
+
} catch (err) {
|
|
1935
|
+
await job.finish({ ok: false, reason: err instanceof Error ? err.message : "Sync failed." });
|
|
1936
|
+
throw err;
|
|
1888
1937
|
}
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1938
|
+
async function upload() {
|
|
1939
|
+
let res;
|
|
1940
|
+
try {
|
|
1941
|
+
res = await doFetch(endpoint, {
|
|
1942
|
+
method: "POST",
|
|
1943
|
+
headers: {
|
|
1944
|
+
"content-type": "application/json",
|
|
1945
|
+
authorization: `Bearer ${bearer}`,
|
|
1946
|
+
"idempotency-key": idempotencyKey
|
|
1947
|
+
},
|
|
1948
|
+
body: payload
|
|
1949
|
+
});
|
|
1950
|
+
} catch {
|
|
1951
|
+
throw new SyncNetworkError(apiUrl);
|
|
1952
|
+
}
|
|
1953
|
+
if (!res.ok) {
|
|
1954
|
+
const detail = await safeErrorDetail(res);
|
|
1955
|
+
if (res.status === 401) throw new TokenRejectedError();
|
|
1956
|
+
throw new SyncHttpError(res.status, detail);
|
|
1957
|
+
}
|
|
1958
|
+
const body = await res.json().catch(() => ({}));
|
|
1959
|
+
const id = typeof body.id === "string" ? body.id : "";
|
|
1960
|
+
const result = { id, url: `${endpoint}/${id}` };
|
|
1961
|
+
await job.report(deps.failOnFailures === true ? 80 : 95, id || void 0);
|
|
1962
|
+
if (deps.failOnFailures === true) {
|
|
1963
|
+
result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, bearer));
|
|
1964
|
+
}
|
|
1965
|
+
return result;
|
|
1893
1966
|
}
|
|
1894
|
-
const body = await res.json().catch(() => ({}));
|
|
1895
|
-
const id = typeof body.id === "string" ? body.id : "";
|
|
1896
|
-
const result = { id, url: `${endpoint}/${id}` };
|
|
1897
|
-
if (deps.failOnFailures === true) result.gate = gateExit(await readVerdict(doFetch, `${endpoint}/${id}/summary`, token));
|
|
1898
|
-
return result;
|
|
1899
1967
|
}
|
|
1900
1968
|
async function readVerdict(doFetch, url, token) {
|
|
1901
1969
|
try {
|
package/dist/reporter-core.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(reporter_core_exports, {
|
|
|
36
36
|
appendBatch: () => appendBatch,
|
|
37
37
|
ciCommit: () => ciCommit,
|
|
38
38
|
createClient: () => createClient,
|
|
39
|
+
errorContextFields: () => errorContextFields,
|
|
39
40
|
errorContextOf: () => errorContextOf,
|
|
40
41
|
failureOf: () => failureOf,
|
|
41
42
|
filesOf: () => filesOf,
|
|
@@ -449,6 +450,16 @@ function repoRootOf(dir, stopAt) {
|
|
|
449
450
|
}
|
|
450
451
|
}
|
|
451
452
|
function errorContextOf(attempt, home = (0, import_node_os2.homedir)()) {
|
|
453
|
+
return cutErrorContext(attempt, home).text;
|
|
454
|
+
}
|
|
455
|
+
function errorContextFields(attempt, home = (0, import_node_os2.homedir)()) {
|
|
456
|
+
const { text, omitted: omitted2 } = cutErrorContext(attempt, home);
|
|
457
|
+
return {
|
|
458
|
+
...text !== "" ? { errorContext: text } : {},
|
|
459
|
+
...omitted2 > 0 ? { errorContextOmitted: omitted2 } : {}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function cutErrorContext(attempt, home) {
|
|
452
463
|
const text = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
453
464
|
return cut(machineless(text, attempt.repoRoot, home));
|
|
454
465
|
}
|
|
@@ -484,7 +495,7 @@ function pathIn(path3) {
|
|
|
484
495
|
}
|
|
485
496
|
var fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
486
497
|
function cut(text) {
|
|
487
|
-
if (jsonBytes(text) <= TEXT_LIMIT) return text;
|
|
498
|
+
if (jsonBytes(text) <= TEXT_LIMIT) return { text, omitted: 0 };
|
|
488
499
|
const lines = text.split("\n");
|
|
489
500
|
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
490
501
|
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
@@ -493,7 +504,8 @@ function cut(text) {
|
|
|
493
504
|
let tail = lines.length;
|
|
494
505
|
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
495
506
|
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
496
|
-
|
|
507
|
+
const kept = [...lines.slice(0, head), ...lines.slice(tail)].join("\n");
|
|
508
|
+
return { text: [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n"), omitted: text.length - kept.length };
|
|
497
509
|
}
|
|
498
510
|
var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
499
511
|
var METADATA_TYPE = "application/plune.metadata+json";
|
|
@@ -1585,6 +1597,7 @@ function resultKey(testId, retry) {
|
|
|
1585
1597
|
appendBatch,
|
|
1586
1598
|
ciCommit,
|
|
1587
1599
|
createClient,
|
|
1600
|
+
errorContextFields,
|
|
1588
1601
|
errorContextOf,
|
|
1589
1602
|
failureOf,
|
|
1590
1603
|
filesOf,
|
package/dist/reporter-core.d.cts
CHANGED
|
@@ -541,6 +541,16 @@ declare function repoRootOf(dir: string, stopAt?: string): string | undefined;
|
|
|
541
541
|
* searched for credentials: the platform does that before its own cut (ADR-0005).
|
|
542
542
|
*/
|
|
543
543
|
declare function errorContextOf(attempt: FailedAttempt, home?: string): string;
|
|
544
|
+
/**
|
|
545
|
+
* The `errorContext` and `errorContextOmitted` keys of a result (plune-ai/plune#1281 AC-03), ready to spread:
|
|
546
|
+
* none for an empty text, and `errorContextOmitted` only when the cut took something out, a whole number of at
|
|
547
|
+
* least 1 (the platform refuses 0). Counted in characters of the text as it would have been sent (`.length`,
|
|
548
|
+
* the unit the platform counts in), never bytes; the `…[omitted N lines]…` marker is not counted.
|
|
549
|
+
*/
|
|
550
|
+
declare function errorContextFields(attempt: FailedAttempt, home?: string): {
|
|
551
|
+
errorContext?: string;
|
|
552
|
+
errorContextOmitted?: number;
|
|
553
|
+
};
|
|
544
554
|
/**
|
|
545
555
|
* The files the runner kept for one attempt, whatever its status: a screenshot of a test that passed
|
|
546
556
|
* is how a person sees what it checked (plune-ai/plune#913). Which of them the platform takes, the
|
|
@@ -642,4 +652,4 @@ interface PlatformClient {
|
|
|
642
652
|
}
|
|
643
653
|
declare function createClient(opts: ClientOptions): PlatformClient;
|
|
644
654
|
|
|
645
|
-
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 };
|
|
655
|
+
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, errorContextFields, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
|
package/dist/reporter-core.d.ts
CHANGED
|
@@ -541,6 +541,16 @@ declare function repoRootOf(dir: string, stopAt?: string): string | undefined;
|
|
|
541
541
|
* searched for credentials: the platform does that before its own cut (ADR-0005).
|
|
542
542
|
*/
|
|
543
543
|
declare function errorContextOf(attempt: FailedAttempt, home?: string): string;
|
|
544
|
+
/**
|
|
545
|
+
* The `errorContext` and `errorContextOmitted` keys of a result (plune-ai/plune#1281 AC-03), ready to spread:
|
|
546
|
+
* none for an empty text, and `errorContextOmitted` only when the cut took something out, a whole number of at
|
|
547
|
+
* least 1 (the platform refuses 0). Counted in characters of the text as it would have been sent (`.length`,
|
|
548
|
+
* the unit the platform counts in), never bytes; the `…[omitted N lines]…` marker is not counted.
|
|
549
|
+
*/
|
|
550
|
+
declare function errorContextFields(attempt: FailedAttempt, home?: string): {
|
|
551
|
+
errorContext?: string;
|
|
552
|
+
errorContextOmitted?: number;
|
|
553
|
+
};
|
|
544
554
|
/**
|
|
545
555
|
* The files the runner kept for one attempt, whatever its status: a screenshot of a test that passed
|
|
546
556
|
* is how a person sees what it checked (plune-ai/plune#913). Which of them the platform takes, the
|
|
@@ -642,4 +652,4 @@ interface PlatformClient {
|
|
|
642
652
|
}
|
|
643
653
|
declare function createClient(opts: ClientOptions): PlatformClient;
|
|
644
654
|
|
|
645
|
-
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 };
|
|
655
|
+
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, errorContextFields, errorContextOf, failureOf, filesOf, readEnv, repoRootOf, resultKey, startRun, webLink };
|
package/dist/reporter-core.js
CHANGED
|
@@ -400,6 +400,16 @@ function repoRootOf(dir, stopAt) {
|
|
|
400
400
|
}
|
|
401
401
|
}
|
|
402
402
|
function errorContextOf(attempt, home = homedir2()) {
|
|
403
|
+
return cutErrorContext(attempt, home).text;
|
|
404
|
+
}
|
|
405
|
+
function errorContextFields(attempt, home = homedir2()) {
|
|
406
|
+
const { text, omitted: omitted2 } = cutErrorContext(attempt, home);
|
|
407
|
+
return {
|
|
408
|
+
...text !== "" ? { errorContext: text } : {},
|
|
409
|
+
...omitted2 > 0 ? { errorContextOmitted: omitted2 } : {}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
function cutErrorContext(attempt, home) {
|
|
403
413
|
const text = attempt.errors.map((e) => e.text).filter((t) => t !== "").join("\n\n");
|
|
404
414
|
return cut(machineless(text, attempt.repoRoot, home));
|
|
405
415
|
}
|
|
@@ -435,7 +445,7 @@ function pathIn(path3) {
|
|
|
435
445
|
}
|
|
436
446
|
var fileNameOf = (name) => /^(?:file:\/\/|[\\/]|~[\\/]|[A-Za-z]:[\\/])/.test(name) ? name.split(/[\\/]/).pop() ?? "" : name;
|
|
437
447
|
function cut(text) {
|
|
438
|
-
if (jsonBytes(text) <= TEXT_LIMIT) return text;
|
|
448
|
+
if (jsonBytes(text) <= TEXT_LIMIT) return { text, omitted: 0 };
|
|
439
449
|
const lines = text.split("\n");
|
|
440
450
|
const size = lines.map((line) => jsonBytes(line) + 2);
|
|
441
451
|
const budget = TEXT_LIMIT - jsonBytes(omitted(lines.length));
|
|
@@ -444,7 +454,8 @@ function cut(text) {
|
|
|
444
454
|
let tail = lines.length;
|
|
445
455
|
while (head < tail && used + size[head] <= budget / 2) used += size[head++];
|
|
446
456
|
while (tail > head && used + size[tail - 1] <= budget) used += size[--tail];
|
|
447
|
-
|
|
457
|
+
const kept = [...lines.slice(0, head), ...lines.slice(tail)].join("\n");
|
|
458
|
+
return { text: [...lines.slice(0, head), omitted(tail - head), ...lines.slice(tail)].join("\n"), omitted: text.length - kept.length };
|
|
448
459
|
}
|
|
449
460
|
var omitted = (lines) => `\u2026[omitted ${lines} lines]\u2026`;
|
|
450
461
|
var METADATA_TYPE = "application/plune.metadata+json";
|
|
@@ -1535,6 +1546,7 @@ export {
|
|
|
1535
1546
|
appendBatch,
|
|
1536
1547
|
ciCommit,
|
|
1537
1548
|
createClient,
|
|
1549
|
+
errorContextFields,
|
|
1538
1550
|
errorContextOf,
|
|
1539
1551
|
failureOf,
|
|
1540
1552
|
filesOf,
|
package/package.json
CHANGED