executable-stories-formatters 0.12.0 → 0.14.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 +20 -0
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +991 -111
- package/dist/cli.js.map +1 -1
- package/dist/{index-mrT6-JSt.d.cts → index-CXrzCk9p.d.cts} +1 -1
- package/dist/{index-mrT6-JSt.d.ts → index-CXrzCk9p.d.ts} +1 -1
- package/dist/index.cjs +466 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +252 -4
- package/dist/index.d.ts +252 -4
- package/dist/index.js +458 -54
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/raw-run.schema.json +9 -0
package/dist/index.js
CHANGED
|
@@ -526,14 +526,14 @@ var CucumberJsonFormatter = class {
|
|
|
526
526
|
duration: 0
|
|
527
527
|
};
|
|
528
528
|
}
|
|
529
|
-
const
|
|
529
|
+
const statusMap2 = {
|
|
530
530
|
passed: "passed",
|
|
531
531
|
failed: "failed",
|
|
532
532
|
skipped: "skipped",
|
|
533
533
|
pending: "pending"
|
|
534
534
|
};
|
|
535
535
|
const stepResult = {
|
|
536
|
-
status:
|
|
536
|
+
status: statusMap2[result.status] ?? "undefined",
|
|
537
537
|
// Duration in nanoseconds (Cucumber uses nanoseconds)
|
|
538
538
|
duration: result.durationMs * 1e6
|
|
539
539
|
};
|
|
@@ -15124,7 +15124,9 @@ var MarkdownFormatter = class {
|
|
|
15124
15124
|
ticketUrlTemplate: options.ticketUrlTemplate,
|
|
15125
15125
|
traceUrlTemplate: options.traceUrlTemplate,
|
|
15126
15126
|
includeSourceLinks: options.includeSourceLinks ?? true,
|
|
15127
|
-
customRenderers: options.customRenderers
|
|
15127
|
+
customRenderers: options.customRenderers,
|
|
15128
|
+
scenarioAnchor: options.scenarioAnchor,
|
|
15129
|
+
scenarioBadge: options.scenarioBadge
|
|
15128
15130
|
};
|
|
15129
15131
|
}
|
|
15130
15132
|
/**
|
|
@@ -15311,6 +15313,11 @@ var MarkdownFormatter = class {
|
|
|
15311
15313
|
* Render a single scenario.
|
|
15312
15314
|
*/
|
|
15313
15315
|
renderScenario(lines, tc) {
|
|
15316
|
+
const anchorId = this.options.scenarioAnchor?.(tc);
|
|
15317
|
+
if (anchorId) {
|
|
15318
|
+
lines.push(`<a id="${anchorId}"></a>`);
|
|
15319
|
+
lines.push("");
|
|
15320
|
+
}
|
|
15314
15321
|
if (this.options.customRenderers?.renderScenarioHeader) {
|
|
15315
15322
|
const custom = this.options.customRenderers.renderScenarioHeader(tc);
|
|
15316
15323
|
if (custom !== null) {
|
|
@@ -15326,6 +15333,10 @@ var MarkdownFormatter = class {
|
|
|
15326
15333
|
icon = this.getStatusIcon(tc.status) + " ";
|
|
15327
15334
|
}
|
|
15328
15335
|
lines.push(`${headingPrefix} ${icon}${tc.story.scenario}`);
|
|
15336
|
+
const badge = this.options.scenarioBadge?.(tc);
|
|
15337
|
+
if (badge) {
|
|
15338
|
+
lines.push(badge);
|
|
15339
|
+
}
|
|
15329
15340
|
if (this.options.includeSourceLinks && this.options.permalinkBaseUrl && tc.sourceFile !== "unknown") {
|
|
15330
15341
|
const permalink = this.buildPermalink(tc);
|
|
15331
15342
|
lines.push(`Source: [${tc.sourceFile}](${permalink})`);
|
|
@@ -15717,6 +15728,132 @@ function escapePipe(value) {
|
|
|
15717
15728
|
return value.replace(/\|/g, "\\|");
|
|
15718
15729
|
}
|
|
15719
15730
|
|
|
15731
|
+
// src/formatters/traceability-matrix.ts
|
|
15732
|
+
var TraceabilityMatrixFormatter = class {
|
|
15733
|
+
format(run) {
|
|
15734
|
+
const matrix = toTraceabilityMatrix(run);
|
|
15735
|
+
const lines = [];
|
|
15736
|
+
lines.push("# Traceability Matrix");
|
|
15737
|
+
lines.push("");
|
|
15738
|
+
lines.push(`Generated: ${matrix.generatedAt}`);
|
|
15739
|
+
lines.push(`Run: ${matrix.run.startedAt} to ${matrix.run.finishedAt}`);
|
|
15740
|
+
if (matrix.run.branch) lines.push(`Branch: ${matrix.run.branch}`);
|
|
15741
|
+
if (matrix.run.gitSha) lines.push(`Commit: ${matrix.run.gitSha}`);
|
|
15742
|
+
lines.push("");
|
|
15743
|
+
lines.push("| Requirements | Verified | Failing | Scenarios | Untraced |");
|
|
15744
|
+
lines.push("| ---: | ---: | ---: | ---: | ---: |");
|
|
15745
|
+
lines.push(
|
|
15746
|
+
`| ${matrix.summary.requirements} | ${matrix.summary.requirementsVerified} | ${matrix.summary.requirementsFailing} | ${matrix.summary.scenarios} | ${matrix.summary.untracedScenarios} |`
|
|
15747
|
+
);
|
|
15748
|
+
lines.push("");
|
|
15749
|
+
for (const req of matrix.requirements) {
|
|
15750
|
+
const heading2 = req.url ? `[${req.ticket}](${req.url})` : req.ticket;
|
|
15751
|
+
lines.push(`## ${heading2}`);
|
|
15752
|
+
lines.push("");
|
|
15753
|
+
lines.push(`Status: ${renderRequirementStatus(req.status)}`);
|
|
15754
|
+
if (req.covers.length > 0) {
|
|
15755
|
+
lines.push(`Covers: ${req.covers.map((path12) => `\`${path12}\``).join(", ")}`);
|
|
15756
|
+
}
|
|
15757
|
+
lines.push("");
|
|
15758
|
+
lines.push("| Status | Scenario | Source | Covers |");
|
|
15759
|
+
lines.push("| --- | --- | --- | --- |");
|
|
15760
|
+
for (const scenario of req.scenarios) {
|
|
15761
|
+
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
15762
|
+
const covers = scenario.covers.length > 0 ? scenario.covers.map((path12) => `\`${path12}\``).join(", ") : "";
|
|
15763
|
+
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
|
|
15764
|
+
}
|
|
15765
|
+
lines.push("");
|
|
15766
|
+
}
|
|
15767
|
+
if (matrix.untraced.length > 0) {
|
|
15768
|
+
lines.push("## Untraced scenarios");
|
|
15769
|
+
lines.push("");
|
|
15770
|
+
lines.push("Behavior with no requirement link. Add a `ticket` to each so it appears against a requirement.");
|
|
15771
|
+
lines.push("");
|
|
15772
|
+
lines.push("| Status | Scenario | Source |");
|
|
15773
|
+
lines.push("| --- | --- | --- |");
|
|
15774
|
+
for (const scenario of matrix.untraced) {
|
|
15775
|
+
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
15776
|
+
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` |`);
|
|
15777
|
+
}
|
|
15778
|
+
lines.push("");
|
|
15779
|
+
}
|
|
15780
|
+
return lines.join("\n").trimEnd();
|
|
15781
|
+
}
|
|
15782
|
+
};
|
|
15783
|
+
function toTraceabilityMatrix(run) {
|
|
15784
|
+
const sorted = [...run.testCases].sort((a, b) => a.id.localeCompare(b.id));
|
|
15785
|
+
const byTicket = /* @__PURE__ */ new Map();
|
|
15786
|
+
const untraced = [];
|
|
15787
|
+
for (const tc of sorted) {
|
|
15788
|
+
const tickets = tc.story.tickets ?? [];
|
|
15789
|
+
if (tickets.length === 0) {
|
|
15790
|
+
untraced.push({
|
|
15791
|
+
id: tc.id,
|
|
15792
|
+
title: tc.story.scenario,
|
|
15793
|
+
status: tc.status,
|
|
15794
|
+
sourceFile: tc.sourceFile,
|
|
15795
|
+
sourceLine: tc.sourceLine
|
|
15796
|
+
});
|
|
15797
|
+
continue;
|
|
15798
|
+
}
|
|
15799
|
+
for (const ticket of tickets) {
|
|
15800
|
+
const entry = byTicket.get(ticket.id) ?? { url: ticket.url, cases: [] };
|
|
15801
|
+
if (!entry.url && ticket.url) entry.url = ticket.url;
|
|
15802
|
+
entry.cases.push(tc);
|
|
15803
|
+
byTicket.set(ticket.id, entry);
|
|
15804
|
+
}
|
|
15805
|
+
}
|
|
15806
|
+
const requirements = [...byTicket.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([ticket, entry]) => {
|
|
15807
|
+
const scenarios = entry.cases.map((tc) => ({
|
|
15808
|
+
id: tc.id,
|
|
15809
|
+
title: tc.story.scenario,
|
|
15810
|
+
status: tc.status,
|
|
15811
|
+
sourceFile: tc.sourceFile,
|
|
15812
|
+
sourceLine: tc.sourceLine,
|
|
15813
|
+
covers: tc.story.covers ?? []
|
|
15814
|
+
}));
|
|
15815
|
+
const covers = [...new Set(scenarios.flatMap((s) => s.covers))].sort();
|
|
15816
|
+
return { ticket, url: entry.url, status: requirementStatus(entry.cases), scenarios, covers };
|
|
15817
|
+
});
|
|
15818
|
+
return {
|
|
15819
|
+
schemaVersion: "1.0",
|
|
15820
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
15821
|
+
run: {
|
|
15822
|
+
startedAt: new Date(run.startedAtMs).toISOString(),
|
|
15823
|
+
finishedAt: new Date(run.finishedAtMs).toISOString(),
|
|
15824
|
+
gitSha: run.gitSha,
|
|
15825
|
+
branch: run.ci?.branch
|
|
15826
|
+
},
|
|
15827
|
+
summary: {
|
|
15828
|
+
requirements: requirements.length,
|
|
15829
|
+
requirementsVerified: requirements.filter((r) => r.status === "verified").length,
|
|
15830
|
+
requirementsFailing: requirements.filter((r) => r.status === "failing").length,
|
|
15831
|
+
scenarios: run.testCases.length,
|
|
15832
|
+
untracedScenarios: untraced.length
|
|
15833
|
+
},
|
|
15834
|
+
requirements,
|
|
15835
|
+
untraced
|
|
15836
|
+
};
|
|
15837
|
+
}
|
|
15838
|
+
function requirementStatus(cases) {
|
|
15839
|
+
if (cases.some((tc) => tc.status === "failed")) return "failing";
|
|
15840
|
+
if (cases.some((tc) => tc.status === "passed")) return "verified";
|
|
15841
|
+
return "incomplete";
|
|
15842
|
+
}
|
|
15843
|
+
function renderRequirementStatus(status) {
|
|
15844
|
+
switch (status) {
|
|
15845
|
+
case "verified":
|
|
15846
|
+
return "verified (all scenarios passed)";
|
|
15847
|
+
case "failing":
|
|
15848
|
+
return "failing (a scenario failed)";
|
|
15849
|
+
default:
|
|
15850
|
+
return "incomplete (no scenario passed yet)";
|
|
15851
|
+
}
|
|
15852
|
+
}
|
|
15853
|
+
function escapePipe2(value) {
|
|
15854
|
+
return value.replace(/\|/g, "\\|");
|
|
15855
|
+
}
|
|
15856
|
+
|
|
15720
15857
|
// src/formatters/cucumber-messages/synthesize-feature.ts
|
|
15721
15858
|
function extractFeatureName(testCases, uri) {
|
|
15722
15859
|
for (const tc of testCases) {
|
|
@@ -18016,12 +18153,16 @@ function groupBy7(items, keyFn) {
|
|
|
18016
18153
|
import * as fs5 from "fs";
|
|
18017
18154
|
import * as path6 from "path";
|
|
18018
18155
|
var SKIP_PREFIXES = ["http://", "https://", "data:", "#"];
|
|
18019
|
-
function
|
|
18156
|
+
function isRemoteRef(src) {
|
|
18020
18157
|
const trimmed = src.trim();
|
|
18021
|
-
|
|
18022
|
-
|
|
18023
|
-
|
|
18024
|
-
|
|
18158
|
+
return SKIP_PREFIXES.some((prefix) => trimmed.startsWith(prefix));
|
|
18159
|
+
}
|
|
18160
|
+
function isAbsoluteRef(src) {
|
|
18161
|
+
const trimmed = src.trim();
|
|
18162
|
+
return path6.posix.isAbsolute(trimmed) || path6.win32.isAbsolute(trimmed);
|
|
18163
|
+
}
|
|
18164
|
+
function isRelativeLocalPath(src) {
|
|
18165
|
+
return !isRemoteRef(src) && !isAbsoluteRef(src);
|
|
18025
18166
|
}
|
|
18026
18167
|
function stripCodeContent(markdown) {
|
|
18027
18168
|
let result = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
|
|
@@ -18037,21 +18178,21 @@ function scanMarkdownAssets(markdown) {
|
|
|
18037
18178
|
let match;
|
|
18038
18179
|
while ((match = mdImageRe.exec(stripped)) !== null) {
|
|
18039
18180
|
const src = match[1].trim();
|
|
18040
|
-
if (
|
|
18181
|
+
if (!isRemoteRef(src)) {
|
|
18041
18182
|
found.add(src);
|
|
18042
18183
|
}
|
|
18043
18184
|
}
|
|
18044
18185
|
const htmlSrcRe = /<(?:img|source|video)[^>]+\bsrc=["']([^"']+)["'][^>]*>/gi;
|
|
18045
18186
|
while ((match = htmlSrcRe.exec(stripped)) !== null) {
|
|
18046
18187
|
const src = match[1].trim();
|
|
18047
|
-
if (
|
|
18188
|
+
if (!isRemoteRef(src)) {
|
|
18048
18189
|
found.add(src);
|
|
18049
18190
|
}
|
|
18050
18191
|
}
|
|
18051
18192
|
const posterRe = /<video[^>]+\bposter=["']([^"']+)["'][^>]*>/gi;
|
|
18052
18193
|
while ((match = posterRe.exec(stripped)) !== null) {
|
|
18053
18194
|
const src = match[1].trim();
|
|
18054
|
-
if (
|
|
18195
|
+
if (!isRemoteRef(src)) {
|
|
18055
18196
|
found.add(src);
|
|
18056
18197
|
}
|
|
18057
18198
|
}
|
|
@@ -18077,48 +18218,21 @@ function isCode(segment) {
|
|
|
18077
18218
|
const trimmed = segment.trimStart();
|
|
18078
18219
|
return trimmed.startsWith("`") || trimmed.startsWith("~") || trimmed.startsWith("<pre") || trimmed.startsWith("<code");
|
|
18079
18220
|
}
|
|
18221
|
+
function resolveRewrite(trimmed, assetsBaseUrl, pathMap) {
|
|
18222
|
+
if (isRemoteRef(trimmed)) return null;
|
|
18223
|
+
if (pathMap) {
|
|
18224
|
+
const mapped = pathMap.get(trimmed);
|
|
18225
|
+
return mapped === void 0 ? null : `${assetsBaseUrl}/${mapped}`;
|
|
18226
|
+
}
|
|
18227
|
+
if (!isRelativeLocalPath(trimmed)) return null;
|
|
18228
|
+
return `${assetsBaseUrl}/${trimmed}`;
|
|
18229
|
+
}
|
|
18080
18230
|
function rewriteProseSegment(prose, assetsBaseUrl, pathMap) {
|
|
18081
|
-
|
|
18082
|
-
|
|
18083
|
-
|
|
18084
|
-
|
|
18085
|
-
|
|
18086
|
-
if (!isLocalPath(trimmed)) return full;
|
|
18087
|
-
if (pathMap) {
|
|
18088
|
-
const mapped = pathMap.get(trimmed);
|
|
18089
|
-
if (mapped === void 0) return full;
|
|
18090
|
-
return `${pre}${assetsBaseUrl}/${mapped}${post}`;
|
|
18091
|
-
}
|
|
18092
|
-
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
18093
|
-
}
|
|
18094
|
-
);
|
|
18095
|
-
result = result.replace(
|
|
18096
|
-
/(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi,
|
|
18097
|
-
(full, pre, src, post) => {
|
|
18098
|
-
const trimmed = src.trim();
|
|
18099
|
-
if (!isLocalPath(trimmed)) return full;
|
|
18100
|
-
if (pathMap) {
|
|
18101
|
-
const mapped = pathMap.get(trimmed);
|
|
18102
|
-
if (mapped === void 0) return full;
|
|
18103
|
-
return `${pre}${assetsBaseUrl}/${mapped}${post}`;
|
|
18104
|
-
}
|
|
18105
|
-
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
18106
|
-
}
|
|
18107
|
-
);
|
|
18108
|
-
result = result.replace(
|
|
18109
|
-
/(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi,
|
|
18110
|
-
(full, pre, src, post) => {
|
|
18111
|
-
const trimmed = src.trim();
|
|
18112
|
-
if (!isLocalPath(trimmed)) return full;
|
|
18113
|
-
if (pathMap) {
|
|
18114
|
-
const mapped = pathMap.get(trimmed);
|
|
18115
|
-
if (mapped === void 0) return full;
|
|
18116
|
-
return `${pre}${assetsBaseUrl}/${mapped}${post}`;
|
|
18117
|
-
}
|
|
18118
|
-
return `${pre}${assetsBaseUrl}/${trimmed}${post}`;
|
|
18119
|
-
}
|
|
18120
|
-
);
|
|
18121
|
-
return result;
|
|
18231
|
+
const rewrite = (full, pre, src, post) => {
|
|
18232
|
+
const target = resolveRewrite(src.trim(), assetsBaseUrl, pathMap);
|
|
18233
|
+
return target === null ? full : `${pre}${target}${post}`;
|
|
18234
|
+
};
|
|
18235
|
+
return prose.replace(/(!\[[^\]]*\]\()([^)"'\s]+)((?:\s+["'][^"']*["'])?\s*\))/g, rewrite).replace(/(<(?:img|source|video)[^>]+\bsrc=["'])([^"']+)(["'][^>]*>)/gi, rewrite).replace(/(<video[^>]+\bposter=["'])([^"']+)(["'][^>]*>)/gi, rewrite);
|
|
18122
18236
|
}
|
|
18123
18237
|
function rewriteAssetPaths(markdown, assetsBaseUrl, pathMap) {
|
|
18124
18238
|
return splitByCode(markdown).map((seg) => isCode(seg) ? seg : rewriteProseSegment(seg, assetsBaseUrl, pathMap)).join("");
|
|
@@ -18135,8 +18249,9 @@ function copyMarkdownAssets(options) {
|
|
|
18135
18249
|
const pathMap = /* @__PURE__ */ new Map();
|
|
18136
18250
|
const missing = [];
|
|
18137
18251
|
for (const ref of refs) {
|
|
18138
|
-
const absPath = path6.resolve(markdownDir, ref);
|
|
18252
|
+
const absPath = isAbsoluteRef(ref) ? ref : path6.resolve(markdownDir, ref);
|
|
18139
18253
|
if (!fs5.existsSync(absPath)) {
|
|
18254
|
+
if (isAbsoluteRef(ref)) continue;
|
|
18140
18255
|
if (!allowMissing) {
|
|
18141
18256
|
throw new Error(`Asset not found: ${absPath}`);
|
|
18142
18257
|
}
|
|
@@ -20464,6 +20579,280 @@ function collectDocKinds(testCase) {
|
|
|
20464
20579
|
return [...kinds].sort();
|
|
20465
20580
|
}
|
|
20466
20581
|
|
|
20582
|
+
// src/scenario-failure.ts
|
|
20583
|
+
function failingScenarioMessage(tc) {
|
|
20584
|
+
const failingStep = tc.stepResults.find((s) => s.status === "failed" && s.errorMessage);
|
|
20585
|
+
return failingStep?.errorMessage ?? tc.errorMessage;
|
|
20586
|
+
}
|
|
20587
|
+
|
|
20588
|
+
// src/check.ts
|
|
20589
|
+
var ICON_PASS = "\u2713";
|
|
20590
|
+
var ICON_FAIL = "\u2717";
|
|
20591
|
+
var ICON_SKIP = "\u2298";
|
|
20592
|
+
var ICON_PENDING = "\u23F3";
|
|
20593
|
+
var ICON_WARN = "\u26A0";
|
|
20594
|
+
function buildCheck(args, _deps = {}) {
|
|
20595
|
+
const { testCases, baseline } = args;
|
|
20596
|
+
const summary = {
|
|
20597
|
+
total: testCases.length,
|
|
20598
|
+
passed: testCases.filter((tc) => tc.status === "passed").length,
|
|
20599
|
+
failed: testCases.filter((tc) => tc.status === "failed").length,
|
|
20600
|
+
skipped: testCases.filter((tc) => tc.status === "skipped").length,
|
|
20601
|
+
pending: testCases.filter((tc) => tc.status === "pending").length
|
|
20602
|
+
};
|
|
20603
|
+
let regressed = 0;
|
|
20604
|
+
let fixed = 0;
|
|
20605
|
+
if (baseline) {
|
|
20606
|
+
for (const tc of testCases) {
|
|
20607
|
+
const before = baseline.get(tc.id);
|
|
20608
|
+
if (before === "passed" && tc.status === "failed") regressed += 1;
|
|
20609
|
+
if (before === "failed" && tc.status === "passed") fixed += 1;
|
|
20610
|
+
}
|
|
20611
|
+
}
|
|
20612
|
+
const failures = testCases.filter((tc) => tc.status === "failed").map((tc) => toFailure(tc, baseline)).sort((a, b) => {
|
|
20613
|
+
if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
|
|
20614
|
+
return a.location.localeCompare(b.location);
|
|
20615
|
+
});
|
|
20616
|
+
return {
|
|
20617
|
+
summary,
|
|
20618
|
+
failures,
|
|
20619
|
+
regressed,
|
|
20620
|
+
fixed,
|
|
20621
|
+
comparedToBaseline: baseline !== void 0
|
|
20622
|
+
};
|
|
20623
|
+
}
|
|
20624
|
+
function toFailure(tc, baseline) {
|
|
20625
|
+
const failedIndexes = new Set(
|
|
20626
|
+
tc.stepResults.filter((s) => s.status === "failed").map((s) => s.index)
|
|
20627
|
+
);
|
|
20628
|
+
const steps = tc.story.steps.map((step, index) => ({
|
|
20629
|
+
keyword: step.keyword,
|
|
20630
|
+
text: step.text,
|
|
20631
|
+
failed: failedIndexes.has(index)
|
|
20632
|
+
}));
|
|
20633
|
+
return {
|
|
20634
|
+
id: tc.id,
|
|
20635
|
+
scenario: tc.story.scenario,
|
|
20636
|
+
location: `${tc.sourceFile}:${tc.sourceLine}`,
|
|
20637
|
+
steps,
|
|
20638
|
+
errorMessage: failingScenarioMessage(tc),
|
|
20639
|
+
covers: tc.story.covers ?? [],
|
|
20640
|
+
tickets: (tc.story.tickets ?? []).map((t) => t.id),
|
|
20641
|
+
regressed: baseline?.get(tc.id) === "passed"
|
|
20642
|
+
};
|
|
20643
|
+
}
|
|
20644
|
+
function renderCheck(report, format) {
|
|
20645
|
+
return format === "json" ? JSON.stringify(report, null, 2) : renderCheckText(report);
|
|
20646
|
+
}
|
|
20647
|
+
function renderCheckText(report) {
|
|
20648
|
+
const { summary, failures } = report;
|
|
20649
|
+
const headlineParts = [`${ICON_PASS} ${summary.passed} passed`];
|
|
20650
|
+
if (summary.failed > 0) headlineParts.push(`${ICON_FAIL} ${summary.failed} failed`);
|
|
20651
|
+
if (summary.skipped > 0) headlineParts.push(`${ICON_SKIP} ${summary.skipped} skipped`);
|
|
20652
|
+
if (summary.pending > 0) headlineParts.push(`${ICON_PENDING} ${summary.pending} pending`);
|
|
20653
|
+
const headline = `${headlineParts.join(" ")} (${summary.total} scenarios)`;
|
|
20654
|
+
if (failures.length === 0) {
|
|
20655
|
+
const lines2 = [headline];
|
|
20656
|
+
if (report.comparedToBaseline && report.fixed > 0) {
|
|
20657
|
+
lines2.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
|
|
20658
|
+
}
|
|
20659
|
+
lines2.push("All scenarios green.");
|
|
20660
|
+
return lines2.join("\n");
|
|
20661
|
+
}
|
|
20662
|
+
const lines = [headline, ""];
|
|
20663
|
+
for (const f of failures) {
|
|
20664
|
+
lines.push(`${ICON_FAIL} ${f.scenario}${f.regressed ? " (regressed)" : ""}`);
|
|
20665
|
+
lines.push(` ${f.location}`);
|
|
20666
|
+
for (const step of f.steps) {
|
|
20667
|
+
const marker = step.failed ? ` ${ICON_FAIL} ` : " ";
|
|
20668
|
+
lines.push(`${marker}${step.keyword} ${step.text}`);
|
|
20669
|
+
}
|
|
20670
|
+
if (f.errorMessage) {
|
|
20671
|
+
const firstLine = f.errorMessage.split("\n")[0];
|
|
20672
|
+
lines.push(` \u2192 ${firstLine}`);
|
|
20673
|
+
}
|
|
20674
|
+
if (f.covers.length > 0) {
|
|
20675
|
+
lines.push(` covers: ${f.covers.join(", ")}`);
|
|
20676
|
+
}
|
|
20677
|
+
if (f.tickets.length > 0) {
|
|
20678
|
+
lines.push(` ticket: ${f.tickets.join(", ")}`);
|
|
20679
|
+
}
|
|
20680
|
+
lines.push("");
|
|
20681
|
+
}
|
|
20682
|
+
if (report.comparedToBaseline) {
|
|
20683
|
+
if (report.regressed > 0) {
|
|
20684
|
+
lines.push(`${ICON_WARN} ${report.regressed} regressed since baseline (was passing).`);
|
|
20685
|
+
}
|
|
20686
|
+
if (report.fixed > 0) {
|
|
20687
|
+
lines.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
|
|
20688
|
+
}
|
|
20689
|
+
if (report.regressed === 0 && report.fixed === 0) {
|
|
20690
|
+
lines.push("No status changes vs. baseline.");
|
|
20691
|
+
}
|
|
20692
|
+
}
|
|
20693
|
+
return lines.join("\n").trimEnd();
|
|
20694
|
+
}
|
|
20695
|
+
|
|
20696
|
+
// src/goal.ts
|
|
20697
|
+
var ACTIVE = ["passed", "failed"];
|
|
20698
|
+
function buildGoal(args, _deps = {}) {
|
|
20699
|
+
const { run, baseline } = args;
|
|
20700
|
+
const cases = run.testCases;
|
|
20701
|
+
const selectors = [
|
|
20702
|
+
...args.requireTags.map((tag) => ({ label: `tag:${tag}`, match: (tc) => tc.tags.includes(tag) })),
|
|
20703
|
+
...args.requireTickets.map((id) => ({ label: `ticket:${id}`, match: (tc) => (tc.story.tickets ?? []).some((t) => t.id === id) })),
|
|
20704
|
+
...args.requireScenarios.map((sel) => ({ label: `scenario:${sel}`, match: (tc) => tc.id === sel || tc.story.scenario === sel }))
|
|
20705
|
+
];
|
|
20706
|
+
const requirements = selectors.length === 0 ? [evaluate("all scenarios", cases)] : selectors.map((s) => evaluate(s.label, cases.filter(s.match)));
|
|
20707
|
+
const regressions = [];
|
|
20708
|
+
if (baseline && args.enforceNoRegressions) {
|
|
20709
|
+
const before = statusMap(baseline);
|
|
20710
|
+
for (const tc of cases) {
|
|
20711
|
+
if (before.get(tc.id) === "passed" && tc.status === "failed") {
|
|
20712
|
+
regressions.push({ id: tc.id, title: tc.story.scenario });
|
|
20713
|
+
}
|
|
20714
|
+
}
|
|
20715
|
+
}
|
|
20716
|
+
const violations = [];
|
|
20717
|
+
if (baseline && args.enforceRatchet) {
|
|
20718
|
+
const current = new Map(cases.map((tc) => [tc.id, tc]));
|
|
20719
|
+
for (const base of baseline.testCases) {
|
|
20720
|
+
const now = current.get(base.id);
|
|
20721
|
+
if (!now) {
|
|
20722
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "removed", detail: "scenario no longer present" });
|
|
20723
|
+
continue;
|
|
20724
|
+
}
|
|
20725
|
+
if (ACTIVE.includes(base.status) && (now.status === "skipped" || now.status === "pending")) {
|
|
20726
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "disabled", detail: `${base.status} -> ${now.status}` });
|
|
20727
|
+
}
|
|
20728
|
+
const baseSteps = base.story.steps.length;
|
|
20729
|
+
const nowSteps = now.story.steps.length;
|
|
20730
|
+
if (nowSteps < baseSteps) {
|
|
20731
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "weakened", detail: `${baseSteps} steps -> ${nowSteps} steps` });
|
|
20732
|
+
}
|
|
20733
|
+
}
|
|
20734
|
+
}
|
|
20735
|
+
const met = requirements.every((r) => r.met) && regressions.length === 0 && violations.length === 0;
|
|
20736
|
+
return {
|
|
20737
|
+
met,
|
|
20738
|
+
requirements,
|
|
20739
|
+
regressions,
|
|
20740
|
+
regressionsEnforced: Boolean(baseline && args.enforceNoRegressions),
|
|
20741
|
+
ratchet: { enforced: Boolean(baseline && args.enforceRatchet), violations }
|
|
20742
|
+
};
|
|
20743
|
+
}
|
|
20744
|
+
function evaluate(selector, matched) {
|
|
20745
|
+
const passed = matched.filter((tc) => tc.status === "passed").length;
|
|
20746
|
+
const failing = matched.filter((tc) => tc.status !== "passed").map((tc) => tc.story.scenario);
|
|
20747
|
+
return {
|
|
20748
|
+
selector,
|
|
20749
|
+
matched: matched.length,
|
|
20750
|
+
passed,
|
|
20751
|
+
failing,
|
|
20752
|
+
met: matched.length > 0 && failing.length === 0
|
|
20753
|
+
};
|
|
20754
|
+
}
|
|
20755
|
+
function statusMap(run) {
|
|
20756
|
+
return new Map(run.testCases.map((tc) => [tc.id, tc.status]));
|
|
20757
|
+
}
|
|
20758
|
+
function renderGoal(report, format) {
|
|
20759
|
+
if (format === "json") return JSON.stringify(report, null, 2);
|
|
20760
|
+
const lines = [`GOAL: ${report.met ? "met" : "not met"}`];
|
|
20761
|
+
for (const req of report.requirements) {
|
|
20762
|
+
if (req.matched === 0) {
|
|
20763
|
+
lines.push(` ${req.selector}: no matching scenario (no proof)`);
|
|
20764
|
+
continue;
|
|
20765
|
+
}
|
|
20766
|
+
const tail = req.failing.length > 0 ? ` (${req.failing.length} failing)` : "";
|
|
20767
|
+
lines.push(` ${req.selector}: ${req.passed}/${req.matched} scenarios pass${tail}`);
|
|
20768
|
+
}
|
|
20769
|
+
if (report.regressionsEnforced) {
|
|
20770
|
+
if (report.regressions.length === 0) {
|
|
20771
|
+
lines.push(" regressions: 0");
|
|
20772
|
+
} else {
|
|
20773
|
+
lines.push(` regressions: ${report.regressions.length} (${report.regressions.map((r) => r.title).join(", ")})`);
|
|
20774
|
+
}
|
|
20775
|
+
}
|
|
20776
|
+
if (report.ratchet.enforced) {
|
|
20777
|
+
if (report.ratchet.violations.length === 0) {
|
|
20778
|
+
lines.push(" ratchet: clean (0 scenarios removed/weakened)");
|
|
20779
|
+
} else {
|
|
20780
|
+
lines.push(` ratchet: ${report.ratchet.violations.length} removed/weakened`);
|
|
20781
|
+
for (const v of report.ratchet.violations) {
|
|
20782
|
+
lines.push(` ${v.kind}: ${v.title} (${v.detail})`);
|
|
20783
|
+
}
|
|
20784
|
+
}
|
|
20785
|
+
}
|
|
20786
|
+
return lines.join("\n");
|
|
20787
|
+
}
|
|
20788
|
+
|
|
20789
|
+
// src/triage.ts
|
|
20790
|
+
function buildTriage(args, _deps = {}) {
|
|
20791
|
+
const { testCases, baseline } = args;
|
|
20792
|
+
const failing = testCases.filter((tc) => tc.status === "failed");
|
|
20793
|
+
const ranked = failing.map((tc) => {
|
|
20794
|
+
const regressed = baseline?.get(tc.id) === "passed";
|
|
20795
|
+
return {
|
|
20796
|
+
tc,
|
|
20797
|
+
regressed,
|
|
20798
|
+
covers: tc.story.covers ?? []
|
|
20799
|
+
};
|
|
20800
|
+
}).sort((a, b) => {
|
|
20801
|
+
if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
|
|
20802
|
+
const la = `${a.tc.sourceFile}:${a.tc.sourceLine}`;
|
|
20803
|
+
const lb = `${b.tc.sourceFile}:${b.tc.sourceLine}`;
|
|
20804
|
+
return la.localeCompare(lb);
|
|
20805
|
+
});
|
|
20806
|
+
const items = ranked.map((entry, index) => ({
|
|
20807
|
+
rank: index + 1,
|
|
20808
|
+
id: entry.tc.id,
|
|
20809
|
+
scenario: entry.tc.story.scenario,
|
|
20810
|
+
status: entry.tc.status,
|
|
20811
|
+
location: `${entry.tc.sourceFile}:${entry.tc.sourceLine}`,
|
|
20812
|
+
covers: entry.covers,
|
|
20813
|
+
tickets: (entry.tc.story.tickets ?? []).map((t) => t.id),
|
|
20814
|
+
errorMessage: failingScenarioMessage(entry.tc),
|
|
20815
|
+
regressed: entry.regressed,
|
|
20816
|
+
reason: entry.regressed ? "regression" : "failing"
|
|
20817
|
+
}));
|
|
20818
|
+
return {
|
|
20819
|
+
total: testCases.length,
|
|
20820
|
+
failing: failing.length,
|
|
20821
|
+
regressions: items.filter((i) => i.regressed).length,
|
|
20822
|
+
needsCovers: items.filter((i) => i.covers.length === 0).length,
|
|
20823
|
+
items
|
|
20824
|
+
};
|
|
20825
|
+
}
|
|
20826
|
+
function renderTriage(report, format) {
|
|
20827
|
+
if (format === "json") return JSON.stringify(report, null, 2);
|
|
20828
|
+
if (report.items.length === 0) {
|
|
20829
|
+
return "Nothing to triage. No failing scenarios.";
|
|
20830
|
+
}
|
|
20831
|
+
const header = report.regressions > 0 ? `${report.items.length} items to triage (${report.regressions} regression${report.regressions === 1 ? "" : "s"})` : `${report.items.length} items to triage`;
|
|
20832
|
+
const lines = [header, ""];
|
|
20833
|
+
for (const item of report.items) {
|
|
20834
|
+
const tag = item.regressed ? "[regression] " : "";
|
|
20835
|
+
lines.push(`${item.rank}. ${tag}${item.scenario}`);
|
|
20836
|
+
lines.push(` ${item.location}`);
|
|
20837
|
+
if (item.errorMessage) {
|
|
20838
|
+
lines.push(` \u2192 ${item.errorMessage.split("\n")[0]}`);
|
|
20839
|
+
}
|
|
20840
|
+
if (item.covers.length > 0) {
|
|
20841
|
+
lines.push(` fix: ${item.covers.join(", ")}`);
|
|
20842
|
+
} else {
|
|
20843
|
+
lines.push(" fix: (no covers declared \u2014 add `covers` to route this to code)");
|
|
20844
|
+
}
|
|
20845
|
+
if (item.tickets.length > 0) {
|
|
20846
|
+
lines.push(` ticket: ${item.tickets.join(", ")}`);
|
|
20847
|
+
}
|
|
20848
|
+
lines.push("");
|
|
20849
|
+
}
|
|
20850
|
+
if (report.needsCovers > 0) {
|
|
20851
|
+
lines.push(`${report.needsCovers} failing scenario(s) have no covers and can't be routed to code automatically.`);
|
|
20852
|
+
}
|
|
20853
|
+
return lines.join("\n").trimEnd();
|
|
20854
|
+
}
|
|
20855
|
+
|
|
20467
20856
|
// src/review/conventions.ts
|
|
20468
20857
|
var CHANGE_TAG_PREFIX = "change:";
|
|
20469
20858
|
var AUDIENCE_TAG_PREFIX = "audience:";
|
|
@@ -21265,6 +21654,7 @@ var FORMAT_EXTENSIONS = {
|
|
|
21265
21654
|
"behavior-manifest-json": ".behavior-manifest.json",
|
|
21266
21655
|
markdown: ".md",
|
|
21267
21656
|
"release-manifest": ".release-manifest.md",
|
|
21657
|
+
"traceability-matrix": ".traceability-matrix.md",
|
|
21268
21658
|
html: ".html",
|
|
21269
21659
|
"cucumber-html": ".cucumber.html",
|
|
21270
21660
|
junit: ".junit.xml",
|
|
@@ -21487,7 +21877,9 @@ var ReportGenerator = class {
|
|
|
21487
21877
|
permalinkBaseUrl: options.astro?.markdown?.permalinkBaseUrl,
|
|
21488
21878
|
ticketUrlTemplate: options.astro?.markdown?.ticketUrlTemplate,
|
|
21489
21879
|
traceUrlTemplate: options.astro?.markdown?.traceUrlTemplate,
|
|
21490
|
-
customRenderers: options.astro?.markdown?.customRenderers
|
|
21880
|
+
customRenderers: options.astro?.markdown?.customRenderers,
|
|
21881
|
+
scenarioAnchor: options.astro?.markdown?.scenarioAnchor,
|
|
21882
|
+
scenarioBadge: options.astro?.markdown?.scenarioBadge
|
|
21491
21883
|
}
|
|
21492
21884
|
},
|
|
21493
21885
|
assetMode: options.assetMode ?? "none",
|
|
@@ -21691,6 +22083,10 @@ var ReportGenerator = class {
|
|
|
21691
22083
|
const formatter = new ReleaseManifestFormatter();
|
|
21692
22084
|
return formatter.format(run);
|
|
21693
22085
|
}
|
|
22086
|
+
case "traceability-matrix": {
|
|
22087
|
+
const formatter = new TraceabilityMatrixFormatter();
|
|
22088
|
+
return formatter.format(run);
|
|
22089
|
+
}
|
|
21694
22090
|
case "story-report-json": {
|
|
21695
22091
|
const formatter = new StoryReportJsonFormatter({
|
|
21696
22092
|
pretty: this.options.storyReportJson.pretty
|
|
@@ -21770,12 +22166,16 @@ export {
|
|
|
21770
22166
|
STORY_REPORT_SCHEMA_VERSION,
|
|
21771
22167
|
ScenarioIndexJsonFormatter,
|
|
21772
22168
|
StoryReportJsonFormatter,
|
|
22169
|
+
TraceabilityMatrixFormatter,
|
|
21773
22170
|
adaptJestRun,
|
|
21774
22171
|
adaptPlaywrightRun,
|
|
21775
22172
|
adaptVitestRun,
|
|
21776
22173
|
assertValidRun,
|
|
22174
|
+
buildCheck,
|
|
22175
|
+
buildGoal,
|
|
21777
22176
|
buildHtmlDocEntry,
|
|
21778
22177
|
buildReview,
|
|
22178
|
+
buildTriage,
|
|
21779
22179
|
bundleAssets,
|
|
21780
22180
|
calculateFlakiness,
|
|
21781
22181
|
calculateStability,
|
|
@@ -21825,6 +22225,9 @@ export {
|
|
|
21825
22225
|
readPackageVersion,
|
|
21826
22226
|
recordDeployment,
|
|
21827
22227
|
regenerateArtifacts,
|
|
22228
|
+
renderCheck,
|
|
22229
|
+
renderGoal,
|
|
22230
|
+
renderTriage,
|
|
21828
22231
|
resolveAttachment,
|
|
21829
22232
|
resolveAttachments,
|
|
21830
22233
|
resolveTheme,
|
|
@@ -21846,6 +22249,7 @@ export {
|
|
|
21846
22249
|
toReleaseManifest,
|
|
21847
22250
|
toScenarioIndex,
|
|
21848
22251
|
toStoryReport,
|
|
22252
|
+
toTraceabilityMatrix,
|
|
21849
22253
|
tryGetActiveOtelContext,
|
|
21850
22254
|
updateHistory,
|
|
21851
22255
|
validateCanonicalRun
|