@plune-ai/cli 0.28.0 → 0.29.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 +16 -0
- package/dist/cli.cjs +292 -37
- 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
|
@@ -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") {
|
|
@@ -5008,7 +5016,7 @@ function pendingOf(r, retry, ctx) {
|
|
|
5008
5016
|
})),
|
|
5009
5017
|
testFile: ""
|
|
5010
5018
|
};
|
|
5011
|
-
const errorContext =
|
|
5019
|
+
const errorContext = errorContextFields(attempt);
|
|
5012
5020
|
const failure = FAILED.has(status) ? failureOf(attempt) : void 0;
|
|
5013
5021
|
const pkg2 = labelOf(r, "package");
|
|
5014
5022
|
const where = (key.includes("#") ? key.split("#")[0] : pkg2)?.replace(/^[A-Za-z][A-Za-z0-9_-]+:(?![\\/])/, "");
|
|
@@ -5032,7 +5040,7 @@ function pendingOf(r, retry, ctx) {
|
|
|
5032
5040
|
retry
|
|
5033
5041
|
}
|
|
5034
5042
|
} : {},
|
|
5035
|
-
...errorContext
|
|
5043
|
+
...errorContext,
|
|
5036
5044
|
...failure !== void 0 ? { failure } : {},
|
|
5037
5045
|
...files.length > 0 ? { files } : {}
|
|
5038
5046
|
};
|
|
@@ -5362,7 +5370,7 @@ function pendingFrom(spec, test, result, file, titles, ctx) {
|
|
|
5362
5370
|
const stated = statedIdOf(test, spec.title ?? "");
|
|
5363
5371
|
const expected = test.expectedStatus;
|
|
5364
5372
|
const attempt = attemptOf(result, file, ctx);
|
|
5365
|
-
const errorContext =
|
|
5373
|
+
const errorContext = errorContextFields(attempt);
|
|
5366
5374
|
const failure = NOT_FAILED.has(attempt.status) ? void 0 : failureOf(attempt);
|
|
5367
5375
|
const files = filesOf(attempt);
|
|
5368
5376
|
const keys = [
|
|
@@ -5392,7 +5400,7 @@ function pendingFrom(spec, test, result, file, titles, ctx) {
|
|
|
5392
5400
|
...result.workerIndex !== void 0 ? { worker: String(result.workerIndex) } : {}
|
|
5393
5401
|
}
|
|
5394
5402
|
} : {},
|
|
5395
|
-
...errorContext
|
|
5403
|
+
...errorContext,
|
|
5396
5404
|
...failure !== void 0 ? { failure } : {},
|
|
5397
5405
|
...files.length > 0 ? { files } : {}
|
|
5398
5406
|
};
|
|
@@ -6509,12 +6517,18 @@ __export(requirements_import_exports, {
|
|
|
6509
6517
|
MAX_MARKDOWN_BYTES: () => MAX_MARKDOWN_BYTES,
|
|
6510
6518
|
RequirementsImportError: () => RequirementsImportError,
|
|
6511
6519
|
buildImportBody: () => buildImportBody,
|
|
6520
|
+
cut: () => cut2,
|
|
6521
|
+
flat: () => flat,
|
|
6512
6522
|
formatDiscover: () => formatDiscover,
|
|
6513
6523
|
formatReport: () => formatReport2,
|
|
6514
6524
|
handleRequirementsImport: () => handleRequirementsImport,
|
|
6515
6525
|
parseMap: () => parseMap,
|
|
6516
6526
|
parseTableSpec: () => parseTableSpec,
|
|
6517
|
-
|
|
6527
|
+
plural: () => plural,
|
|
6528
|
+
readMarkdown: () => readMarkdown,
|
|
6529
|
+
refusal: () => refusal,
|
|
6530
|
+
reportRequirementsImportFailure: () => reportRequirementsImportFailure,
|
|
6531
|
+
resolveProject: () => resolveProject
|
|
6518
6532
|
});
|
|
6519
6533
|
function parseMap(flag2, raw) {
|
|
6520
6534
|
const out = {};
|
|
@@ -6801,12 +6815,94 @@ var init_requirements_import = __esm({
|
|
|
6801
6815
|
}
|
|
6802
6816
|
});
|
|
6803
6817
|
|
|
6818
|
+
// src/cli/commands/requirements-links.ts
|
|
6819
|
+
var requirements_links_exports = {};
|
|
6820
|
+
__export(requirements_links_exports, {
|
|
6821
|
+
buildLinksBody: () => buildLinksBody,
|
|
6822
|
+
formatLinks: () => formatLinks,
|
|
6823
|
+
formatLinksDiscover: () => formatLinksDiscover,
|
|
6824
|
+
handleRequirementsLinks: () => handleRequirementsLinks
|
|
6825
|
+
});
|
|
6826
|
+
function buildLinksBody(f) {
|
|
6827
|
+
if (f.table === void 0 && f.requirementColumn === void 0 && f.testColumn === void 0 && f.separator === void 0) return null;
|
|
6828
|
+
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.");
|
|
6829
|
+
if (!/^\d+$/.test(f.table)) usage2(`--table is the number of the table in the list (0, 1, ...), not "${f.table}".`);
|
|
6830
|
+
if (f.requirementColumn === void 0 || f.requirementColumn.trim() === "") usage2("--table needs --requirement-column <header> (the column with the requirement keys).");
|
|
6831
|
+
if (f.testColumn === void 0 || f.testColumn.trim() === "") usage2("--table needs --test-column <header> (the column with the test titles or paths).");
|
|
6832
|
+
const body = { table: Number(f.table), requirementColumn: f.requirementColumn, testColumn: f.testColumn };
|
|
6833
|
+
if (f.separator !== void 0) {
|
|
6834
|
+
if (f.separator.length < 1 || f.separator.length > 3) usage2(`--separator is 1 to 3 characters, not "${f.separator}".`);
|
|
6835
|
+
body["requirementSeparator"] = f.separator;
|
|
6836
|
+
}
|
|
6837
|
+
return body;
|
|
6838
|
+
}
|
|
6839
|
+
async function handleRequirementsLinks(deps) {
|
|
6840
|
+
const apiUrl = resolveApiUrl(deps.apiUrl);
|
|
6841
|
+
const mapping = buildLinksBody(deps);
|
|
6842
|
+
const markdown = readMarkdown(deps.cwd ?? process.cwd(), deps.file);
|
|
6843
|
+
const token = (deps.loadToken ?? loadToken)();
|
|
6844
|
+
if (token === null) throw new RequirementsImportError('Not logged in. Run "plune login" first, or set PLUNE_TOKEN.', 2);
|
|
6845
|
+
const client = createClient({ apiUrl, token, ...deps.fetchImpl !== void 0 ? { fetchImpl: deps.fetchImpl } : {} });
|
|
6846
|
+
const projectId = await resolveProject(client, deps.project, apiUrl);
|
|
6847
|
+
const base = `/v1/projects/${encodeURIComponent(projectId)}/requirements/import`;
|
|
6848
|
+
if (mapping === null) {
|
|
6849
|
+
const out2 = await client.post(`${base}/discover`, { markdown });
|
|
6850
|
+
if (!out2.ok) throw refusal(out2, apiUrl, projectId);
|
|
6851
|
+
return { step: "discover", result: out2.body };
|
|
6852
|
+
}
|
|
6853
|
+
const out = await client.post(`${base}/links`, { markdown, dryRun: deps.yes !== true, ...mapping });
|
|
6854
|
+
if (!out.ok) throw refusal(out, apiUrl, projectId);
|
|
6855
|
+
return { step: "links", report: out.body };
|
|
6856
|
+
}
|
|
6857
|
+
function formatLinksDiscover(file, r) {
|
|
6858
|
+
const out = [`${file}: ${plural(r.tables.length, "table")}`, ""];
|
|
6859
|
+
if (r.tables.length === 0) out.push(" (none)");
|
|
6860
|
+
for (const t of r.tables) {
|
|
6861
|
+
const mark = t.unreadable !== void 0 ? ` cannot be read: ${t.unreadable}` : "";
|
|
6862
|
+
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}`);
|
|
6863
|
+
}
|
|
6864
|
+
if (r.truncated) out.push("", "The file has more tables than are listed here.");
|
|
6865
|
+
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>`);
|
|
6866
|
+
return out.join("\n") + "\n";
|
|
6867
|
+
}
|
|
6868
|
+
function formatLinks(report) {
|
|
6869
|
+
const s = report.summary;
|
|
6870
|
+
const out = [`link ${s.link} \xB7 unchanged ${s.unchanged} \xB7 unmatched ${s.unmatched}`];
|
|
6871
|
+
for (const action of ["unmatched", "link", "unchanged"]) {
|
|
6872
|
+
const rows = report.items.filter((i) => i.action === action);
|
|
6873
|
+
if (rows.length === 0) continue;
|
|
6874
|
+
out.push("", `${action} (${rows.length})`);
|
|
6875
|
+
for (const i of rows.slice(0, SHOWN2)) {
|
|
6876
|
+
out.push(` line ${i.line} ${i.key === "" ? "-" : i.key} -> ${i.test === "" ? "-" : cut2(flat(i.test), 80)} - ${flat(i.reason)}`);
|
|
6877
|
+
}
|
|
6878
|
+
if (rows.length > SHOWN2) out.push(` ... and ${rows.length - SHOWN2} more (--json lists every row)`);
|
|
6879
|
+
}
|
|
6880
|
+
out.push("", report.applied ? `applied \u2014 ${plural(s.link, "link")} created.` : "dry run \u2014 nothing was written; add --yes to apply");
|
|
6881
|
+
return out.join("\n") + "\n";
|
|
6882
|
+
}
|
|
6883
|
+
var usage2, SHOWN2;
|
|
6884
|
+
var init_requirements_links = __esm({
|
|
6885
|
+
"src/cli/commands/requirements-links.ts"() {
|
|
6886
|
+
"use strict";
|
|
6887
|
+
init_cjs_shims();
|
|
6888
|
+
init_client();
|
|
6889
|
+
init_api_url();
|
|
6890
|
+
init_credentials();
|
|
6891
|
+
init_requirements_import();
|
|
6892
|
+
usage2 = (message) => {
|
|
6893
|
+
throw new RequirementsImportError(message, 2);
|
|
6894
|
+
};
|
|
6895
|
+
SHOWN2 = 50;
|
|
6896
|
+
}
|
|
6897
|
+
});
|
|
6898
|
+
|
|
6804
6899
|
// src/cli/commands/verify.ts
|
|
6805
6900
|
var verify_exports = {};
|
|
6806
6901
|
__export(verify_exports, {
|
|
6807
6902
|
compare: () => compare,
|
|
6808
6903
|
formatVerify: () => formatVerify,
|
|
6809
6904
|
handleVerify: () => handleVerify,
|
|
6905
|
+
loadSources: () => loadSources,
|
|
6810
6906
|
testsOfListing: () => testsOfListing
|
|
6811
6907
|
});
|
|
6812
6908
|
function testsOfListing(listing) {
|
|
@@ -6859,7 +6955,7 @@ function runPlaywrightList(args, cwd) {
|
|
|
6859
6955
|
throw new SyncFileError(`could not list the Playwright tests${stderr ? `: ${stderr}` : ""}`);
|
|
6860
6956
|
}
|
|
6861
6957
|
}
|
|
6862
|
-
async function
|
|
6958
|
+
async function loadSources(deps) {
|
|
6863
6959
|
const get = platformGet(deps);
|
|
6864
6960
|
let raw;
|
|
6865
6961
|
if (deps.from !== void 0) {
|
|
@@ -6891,7 +6987,11 @@ async function handleVerify(deps) {
|
|
|
6891
6987
|
cases.push(...page.testCases);
|
|
6892
6988
|
cursor = page.nextCursor;
|
|
6893
6989
|
} while (cursor !== null);
|
|
6894
|
-
return
|
|
6990
|
+
return { code: testsOfListing(listing), cases };
|
|
6991
|
+
}
|
|
6992
|
+
async function handleVerify(deps) {
|
|
6993
|
+
const { code, cases } = await loadSources(deps);
|
|
6994
|
+
return compare(code, cases);
|
|
6895
6995
|
}
|
|
6896
6996
|
function formatVerify(r) {
|
|
6897
6997
|
const lines = [];
|
|
@@ -6925,6 +7025,111 @@ var init_verify = __esm({
|
|
|
6925
7025
|
}
|
|
6926
7026
|
});
|
|
6927
7027
|
|
|
7028
|
+
// src/cli/commands/ids.ts
|
|
7029
|
+
var ids_exports = {};
|
|
7030
|
+
__export(ids_exports, {
|
|
7031
|
+
DEFAULT_IDS_FILE: () => DEFAULT_IDS_FILE,
|
|
7032
|
+
formatIds: () => formatIds,
|
|
7033
|
+
handleIds: () => handleIds,
|
|
7034
|
+
planIds: () => planIds,
|
|
7035
|
+
readMap: () => readMap,
|
|
7036
|
+
serializeMap: () => serializeMap
|
|
7037
|
+
});
|
|
7038
|
+
function readMap(file) {
|
|
7039
|
+
if (!(0, import_node_fs9.existsSync)(file)) return {};
|
|
7040
|
+
let parsed;
|
|
7041
|
+
try {
|
|
7042
|
+
parsed = JSON.parse((0, import_node_fs9.readFileSync)(file, "utf8"));
|
|
7043
|
+
} catch {
|
|
7044
|
+
throw new SyncFileError(`${file} is not valid JSON; fix or remove it, nothing was written`);
|
|
7045
|
+
}
|
|
7046
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || Object.values(parsed).some((v) => typeof v !== "string" || v === ""))
|
|
7047
|
+
throw new SyncFileError(
|
|
7048
|
+
`${file} must be an object of path-title to case id strings; nothing was written`
|
|
7049
|
+
);
|
|
7050
|
+
return parsed;
|
|
7051
|
+
}
|
|
7052
|
+
function planIds(code, cases, current) {
|
|
7053
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
7054
|
+
for (const c of cases)
|
|
7055
|
+
if (c.type === "automated" && c.state !== "deprecated") {
|
|
7056
|
+
for (const k of c.externalKeys ?? []) if (k.kind === "path-title") byPath.set(k.value, c);
|
|
7057
|
+
}
|
|
7058
|
+
const added = {};
|
|
7059
|
+
const noCase = [];
|
|
7060
|
+
let covered = 0;
|
|
7061
|
+
for (const t of code) {
|
|
7062
|
+
if (t.id !== void 0 || current[t.pathTitle] !== void 0) covered++;
|
|
7063
|
+
else {
|
|
7064
|
+
const hit = byPath.get(t.pathTitle);
|
|
7065
|
+
if (hit === void 0) noCase.push(t.pathTitle);
|
|
7066
|
+
else added[t.pathTitle] = hit.id;
|
|
7067
|
+
}
|
|
7068
|
+
}
|
|
7069
|
+
const inCode = new Set(code.map((t) => t.pathTitle));
|
|
7070
|
+
return {
|
|
7071
|
+
added,
|
|
7072
|
+
covered,
|
|
7073
|
+
noCase: noCase.sort(compareKeys),
|
|
7074
|
+
orphaned: Object.keys(current).filter((k) => !inCode.has(k)).sort(compareKeys)
|
|
7075
|
+
};
|
|
7076
|
+
}
|
|
7077
|
+
function serializeMap(map) {
|
|
7078
|
+
const sorted = {};
|
|
7079
|
+
for (const k of Object.keys(map).sort(compareKeys)) sorted[k] = map[k];
|
|
7080
|
+
return JSON.stringify(sorted, null, 2) + "\n";
|
|
7081
|
+
}
|
|
7082
|
+
async function handleIds(deps) {
|
|
7083
|
+
const file = deps.file ?? DEFAULT_IDS_FILE;
|
|
7084
|
+
const current = readMap(file);
|
|
7085
|
+
const { code, cases } = await loadSources(deps);
|
|
7086
|
+
const plan = planIds(code, cases, current);
|
|
7087
|
+
let written = false;
|
|
7088
|
+
if (deps.write === true && Object.keys(plan.added).length > 0) {
|
|
7089
|
+
try {
|
|
7090
|
+
(0, import_node_fs9.mkdirSync)((0, import_node_path5.dirname)(file), { recursive: true });
|
|
7091
|
+
(0, import_node_fs9.writeFileSync)(file, serializeMap({ ...current, ...plan.added }));
|
|
7092
|
+
} catch {
|
|
7093
|
+
throw new SyncFileError(`cannot write ${file}`);
|
|
7094
|
+
}
|
|
7095
|
+
written = true;
|
|
7096
|
+
}
|
|
7097
|
+
return { file, ...plan, written };
|
|
7098
|
+
}
|
|
7099
|
+
function formatIds(r, write) {
|
|
7100
|
+
const n = Object.keys(r.added).length;
|
|
7101
|
+
const lines = [];
|
|
7102
|
+
if (n > 0) {
|
|
7103
|
+
lines.push(`${write ? "Added to" : "Would add to"} ${r.file} (${n}):`);
|
|
7104
|
+
for (const k of Object.keys(r.added).sort(compareKeys)) lines.push(` ${k} -> ${r.added[k]}`);
|
|
7105
|
+
}
|
|
7106
|
+
if (r.noCase.length > 0) {
|
|
7107
|
+
lines.push(`In the code, no case in Plune yet, nothing to write (${r.noCase.length}):`);
|
|
7108
|
+
for (const k of r.noCase) lines.push(` ${k}`);
|
|
7109
|
+
}
|
|
7110
|
+
if (r.orphaned.length > 0) {
|
|
7111
|
+
lines.push(`In ${r.file}, test no longer in the code, kept (${r.orphaned.length}):`);
|
|
7112
|
+
for (const k of r.orphaned) lines.push(` ${k}`);
|
|
7113
|
+
}
|
|
7114
|
+
lines.push(
|
|
7115
|
+
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.`
|
|
7116
|
+
);
|
|
7117
|
+
return lines.join("\n") + "\n";
|
|
7118
|
+
}
|
|
7119
|
+
var import_node_fs9, import_node_path5, DEFAULT_IDS_FILE, compareKeys;
|
|
7120
|
+
var init_ids = __esm({
|
|
7121
|
+
"src/cli/commands/ids.ts"() {
|
|
7122
|
+
"use strict";
|
|
7123
|
+
init_cjs_shims();
|
|
7124
|
+
import_node_fs9 = require("fs");
|
|
7125
|
+
import_node_path5 = require("path");
|
|
7126
|
+
init_verify();
|
|
7127
|
+
init_sync();
|
|
7128
|
+
DEFAULT_IDS_FILE = "plune/ids.json";
|
|
7129
|
+
compareKeys = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
7130
|
+
}
|
|
7131
|
+
});
|
|
7132
|
+
|
|
6928
7133
|
// src/cli.ts
|
|
6929
7134
|
var cli_exports = {};
|
|
6930
7135
|
__export(cli_exports, {
|
|
@@ -6934,8 +7139,8 @@ module.exports = __toCommonJS(cli_exports);
|
|
|
6934
7139
|
init_cjs_shims();
|
|
6935
7140
|
init_api_url();
|
|
6936
7141
|
var import_commander = require("commander");
|
|
6937
|
-
var
|
|
6938
|
-
var
|
|
7142
|
+
var import_node_fs10 = require("fs");
|
|
7143
|
+
var import_node_path6 = require("path");
|
|
6939
7144
|
var import_node_url = require("url");
|
|
6940
7145
|
|
|
6941
7146
|
// src/util/duration.ts
|
|
@@ -6951,8 +7156,8 @@ function parseDuration(text2) {
|
|
|
6951
7156
|
}
|
|
6952
7157
|
|
|
6953
7158
|
// src/cli.ts
|
|
6954
|
-
var __dirname = (0,
|
|
6955
|
-
var pkg = JSON.parse((0,
|
|
7159
|
+
var __dirname = (0, import_node_path6.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
7160
|
+
var pkg = JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path6.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
6956
7161
|
function toFormat(s) {
|
|
6957
7162
|
return s === "json" || s === "markdown" ? s : "console";
|
|
6958
7163
|
}
|
|
@@ -6961,7 +7166,7 @@ function shouldColor(outputPath) {
|
|
|
6961
7166
|
}
|
|
6962
7167
|
function writeReport(text2, outputPath) {
|
|
6963
7168
|
if (outputPath !== void 0) {
|
|
6964
|
-
(0,
|
|
7169
|
+
(0, import_node_fs10.writeFileSync)(outputPath, text2);
|
|
6965
7170
|
} else {
|
|
6966
7171
|
process.stdout.write(text2 + "\n");
|
|
6967
7172
|
}
|
|
@@ -7006,7 +7211,7 @@ function createProgram() {
|
|
|
7006
7211
|
program.hook("preAction", async (_program, action) => {
|
|
7007
7212
|
const { loadEnv: loadEnv2 } = await Promise.resolve().then(() => (init_env(), env_exports));
|
|
7008
7213
|
const { config } = action.optsWithGlobals();
|
|
7009
|
-
if (config !== void 0) loadEnv2((0,
|
|
7214
|
+
if (config !== void 0) loadEnv2((0, import_node_path6.dirname)((0, import_node_path6.resolve)(config)));
|
|
7010
7215
|
loadEnv2(process.cwd());
|
|
7011
7216
|
});
|
|
7012
7217
|
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 +7491,7 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7286
7491
|
Promise.resolve().then(() => (init_report(), report_exports)),
|
|
7287
7492
|
Promise.resolve().then(() => (init_reporters(), reporters_exports))
|
|
7288
7493
|
]);
|
|
7289
|
-
const cwd = globals.config !== void 0 ? (0,
|
|
7494
|
+
const cwd = globals.config !== void 0 ? (0, import_node_path6.dirname)((0, import_node_path6.resolve)(globals.config)) : process.cwd();
|
|
7290
7495
|
try {
|
|
7291
7496
|
const result = handleReport2({ cwd });
|
|
7292
7497
|
const text2 = renderReport2(result, toFormat(options.format), {
|
|
@@ -7383,7 +7588,7 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7383
7588
|
const globals = command.optsWithGlobals();
|
|
7384
7589
|
const verbose = globals.verbose === true;
|
|
7385
7590
|
const { handleSync: handleSync2, reportSyncFailure: reportSyncFailure2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
|
|
7386
|
-
const cwd = globals.config !== void 0 ? (0,
|
|
7591
|
+
const cwd = globals.config !== void 0 ? (0, import_node_path6.dirname)((0, import_node_path6.resolve)(globals.config)) : process.cwd();
|
|
7387
7592
|
try {
|
|
7388
7593
|
const { id, url, gate } = await handleSync2({
|
|
7389
7594
|
cwd,
|
|
@@ -7472,7 +7677,7 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7472
7677
|
failUnexpected(err, verbose);
|
|
7473
7678
|
}
|
|
7474
7679
|
});
|
|
7475
|
-
const requirementsCommand = program.command("requirements").description("Requirements on the platform \u2014 import them from a Markdown file");
|
|
7680
|
+
const requirementsCommand = program.command("requirements").description("Requirements on the platform \u2014 import them, or their links to tests, from a Markdown file");
|
|
7476
7681
|
requirementsCommand.command("import").argument("<file>", "A Markdown file (UTF-8, at most 1 MiB)").description(
|
|
7477
7682
|
"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
7683
|
).option(
|
|
@@ -7499,6 +7704,30 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7499
7704
|
failUnexpected(err, verbose);
|
|
7500
7705
|
}
|
|
7501
7706
|
});
|
|
7707
|
+
requirementsCommand.command("links").argument("<file>", "A Markdown file with a traceability matrix (UTF-8, at most 1 MiB)").description(
|
|
7708
|
+
"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"
|
|
7709
|
+
).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) => {
|
|
7710
|
+
const verbose = command.optsWithGlobals().verbose === true;
|
|
7711
|
+
const { handleRequirementsLinks: handleRequirementsLinks2, formatLinksDiscover: formatLinksDiscover2, formatLinks: formatLinks2 } = await Promise.resolve().then(() => (init_requirements_links(), requirements_links_exports));
|
|
7712
|
+
const { reportRequirementsImportFailure: reportRequirementsImportFailure2 } = await Promise.resolve().then(() => (init_requirements_import(), requirements_import_exports));
|
|
7713
|
+
try {
|
|
7714
|
+
const res = await handleRequirementsLinks2({ file, ...options });
|
|
7715
|
+
if (options.json === true) {
|
|
7716
|
+
process.stdout.write(JSON.stringify(res.step === "discover" ? res.result : res.report, null, 2) + "\n");
|
|
7717
|
+
} else {
|
|
7718
|
+
process.stdout.write(res.step === "discover" ? formatLinksDiscover2(file, res.result) : formatLinks2(res.report));
|
|
7719
|
+
}
|
|
7720
|
+
if (options.failOnUnmatched === true && res.step === "links" && res.report.summary.unmatched > 0) process.exitCode = 1;
|
|
7721
|
+
} catch (err) {
|
|
7722
|
+
const code = reportRequirementsImportFailure2(err, (s) => process.stderr.write(s));
|
|
7723
|
+
if (code !== null) {
|
|
7724
|
+
maybeStack(err, verbose);
|
|
7725
|
+
process.exitCode = code;
|
|
7726
|
+
return;
|
|
7727
|
+
}
|
|
7728
|
+
failUnexpected(err, verbose);
|
|
7729
|
+
}
|
|
7730
|
+
});
|
|
7502
7731
|
const planCommand = program.command("plan").description("Test plans on the platform \u2014 what a plan collects, as something a runner can take");
|
|
7503
7732
|
planCommand.command("grep").argument("<id>", "The plan").description(
|
|
7504
7733
|
"Print the plan's cases as one --grep pattern for the runner (GET /v1/plans/:id/cases)"
|
|
@@ -7548,6 +7777,32 @@ ${missed.map((m) => ` - ${m}`).join("\n")}
|
|
|
7548
7777
|
}
|
|
7549
7778
|
}
|
|
7550
7779
|
);
|
|
7780
|
+
program.command("ids").description(
|
|
7781
|
+
"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"
|
|
7782
|
+
).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(
|
|
7783
|
+
async (options, command) => {
|
|
7784
|
+
const verbose = verboseOf(command);
|
|
7785
|
+
const { handleIds: handleIds2, formatIds: formatIds2 } = await Promise.resolve().then(() => (init_ids(), ids_exports));
|
|
7786
|
+
const { reportCasesFailure: reportCasesFailure2 } = await Promise.resolve().then(() => (init_cases(), cases_exports));
|
|
7787
|
+
try {
|
|
7788
|
+
const report = await handleIds2({
|
|
7789
|
+
write: options.write,
|
|
7790
|
+
file: options.file,
|
|
7791
|
+
...options.from === void 0 ? {} : { from: options.from },
|
|
7792
|
+
...options.playwrightConfig === void 0 ? {} : { playwrightConfig: options.playwrightConfig }
|
|
7793
|
+
});
|
|
7794
|
+
process.stdout.write(options.json ? JSON.stringify(report, null, 2) + "\n" : formatIds2(report, options.write));
|
|
7795
|
+
} catch (err) {
|
|
7796
|
+
const code = reportCasesFailure2(err, (s) => process.stderr.write(s));
|
|
7797
|
+
if (code !== null) {
|
|
7798
|
+
maybeStack(err, verbose);
|
|
7799
|
+
process.exitCode = code;
|
|
7800
|
+
return;
|
|
7801
|
+
}
|
|
7802
|
+
failUnexpected(err, verbose);
|
|
7803
|
+
}
|
|
7804
|
+
}
|
|
7805
|
+
);
|
|
7551
7806
|
program.on("command:*", (operands) => {
|
|
7552
7807
|
process.stderr.write(`Unknown command: ${operands.join(" ")}
|
|
7553
7808
|
`);
|
|
@@ -7559,7 +7814,7 @@ function isMainModule() {
|
|
|
7559
7814
|
const entry = process.argv[1];
|
|
7560
7815
|
if (entry === void 0) return false;
|
|
7561
7816
|
try {
|
|
7562
|
-
return (0,
|
|
7817
|
+
return (0, import_node_fs10.realpathSync)(entry) === (0, import_node_fs10.realpathSync)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
7563
7818
|
} catch {
|
|
7564
7819
|
return false;
|
|
7565
7820
|
}
|
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