executable-stories-formatters 0.11.4 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +812 -13
- package/dist/cli.js.map +1 -1
- package/dist/{index-DF16Xl5i.d.cts → index-CXrzCk9p.d.cts} +10 -1
- package/dist/{index-DF16Xl5i.d.ts → index-CXrzCk9p.d.ts} +10 -1
- package/dist/index.cjs +671 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +280 -6
- package/dist/index.d.ts +280 -6
- package/dist/index.js +662 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/raw-run.schema.json +30 -0
- package/schemas/story-report-v1.json +24 -0
package/dist/cli.js
CHANGED
|
@@ -191,6 +191,11 @@ var raw_run_schema_default = {
|
|
|
191
191
|
},
|
|
192
192
|
description: "Ticket/issue references. Each item is either a string ID or an object with id and optional url."
|
|
193
193
|
},
|
|
194
|
+
covers: {
|
|
195
|
+
type: "array",
|
|
196
|
+
items: { type: "string" },
|
|
197
|
+
description: "Product-code paths/globs this scenario exercises (project-root-relative). Used to map code changes to at-risk scenarios."
|
|
198
|
+
},
|
|
194
199
|
meta: {
|
|
195
200
|
type: "object",
|
|
196
201
|
description: "User-defined metadata for this story."
|
|
@@ -416,6 +421,27 @@ var raw_run_schema_default = {
|
|
|
416
421
|
required: ["kind", "path", "phase"],
|
|
417
422
|
additionalProperties: false
|
|
418
423
|
},
|
|
424
|
+
{
|
|
425
|
+
type: "object",
|
|
426
|
+
description: "Embedded HTML rendered in a sandboxed iframe. Exactly one of path/url/content.",
|
|
427
|
+
properties: {
|
|
428
|
+
kind: { const: "html" },
|
|
429
|
+
path: { type: "string", description: "Local HTML file path (inlined into the report by default)." },
|
|
430
|
+
url: { type: "string", description: "Remote URL rendered via iframe src." },
|
|
431
|
+
content: { type: "string", description: "Inline HTML content rendered via iframe srcdoc." },
|
|
432
|
+
title: { type: "string" },
|
|
433
|
+
height: { oneOf: [{ type: "number" }, { type: "string" }], description: "Iframe height: number \u2192 px, string passed through (e.g. '60vh'). Default 400px." },
|
|
434
|
+
phase: { $ref: "#/$defs/DocPhase" },
|
|
435
|
+
children: { type: "array", items: { $ref: "#/$defs/DocEntry" }, description: "Nested child doc entries for grouping." }
|
|
436
|
+
},
|
|
437
|
+
required: ["kind", "phase"],
|
|
438
|
+
oneOf: [
|
|
439
|
+
{ required: ["path"] },
|
|
440
|
+
{ required: ["url"] },
|
|
441
|
+
{ required: ["content"] }
|
|
442
|
+
],
|
|
443
|
+
additionalProperties: false
|
|
444
|
+
},
|
|
419
445
|
{
|
|
420
446
|
type: "object",
|
|
421
447
|
description: "Custom documentation entry with arbitrary data.",
|
|
@@ -491,6 +517,10 @@ var raw_run_schema_default = {
|
|
|
491
517
|
minimum: 0,
|
|
492
518
|
description: "Step index (0-based)."
|
|
493
519
|
},
|
|
520
|
+
stepId: {
|
|
521
|
+
type: "string",
|
|
522
|
+
description: "Stable step ID when the framework provides one (correlates with StoryStep.id)."
|
|
523
|
+
},
|
|
494
524
|
title: {
|
|
495
525
|
type: "string",
|
|
496
526
|
description: "Step title/description."
|
|
@@ -1237,6 +1267,14 @@ var CucumberJsonFormatter = class {
|
|
|
1237
1267
|
}
|
|
1238
1268
|
const embeddings = [];
|
|
1239
1269
|
for (const doc of step.docs) {
|
|
1270
|
+
if (doc.kind === "html" && doc.content !== void 0) {
|
|
1271
|
+
embeddings.push({
|
|
1272
|
+
data: Buffer.from(doc.content, "utf8").toString("base64"),
|
|
1273
|
+
mime_type: "text/html",
|
|
1274
|
+
name: doc.title
|
|
1275
|
+
});
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1240
1278
|
if (doc.kind !== "screenshot" || !doc.path.startsWith("data:")) {
|
|
1241
1279
|
continue;
|
|
1242
1280
|
}
|
|
@@ -1261,14 +1299,14 @@ var CucumberJsonFormatter = class {
|
|
|
1261
1299
|
duration: 0
|
|
1262
1300
|
};
|
|
1263
1301
|
}
|
|
1264
|
-
const
|
|
1302
|
+
const statusMap2 = {
|
|
1265
1303
|
passed: "passed",
|
|
1266
1304
|
failed: "failed",
|
|
1267
1305
|
skipped: "skipped",
|
|
1268
1306
|
pending: "pending"
|
|
1269
1307
|
};
|
|
1270
1308
|
const stepResult = {
|
|
1271
|
-
status:
|
|
1309
|
+
status: statusMap2[result.status] ?? "undefined",
|
|
1272
1310
|
// Duration in nanoseconds (Cucumber uses nanoseconds)
|
|
1273
1311
|
duration: result.durationMs * 1e6
|
|
1274
1312
|
};
|
|
@@ -1416,6 +1454,17 @@ ${doc.markdown}`,
|
|
|
1416
1454
|
};
|
|
1417
1455
|
case "screenshot":
|
|
1418
1456
|
return null;
|
|
1457
|
+
case "html":
|
|
1458
|
+
if (doc.url !== void 0 || doc.path !== void 0) {
|
|
1459
|
+
return {
|
|
1460
|
+
doc_string: {
|
|
1461
|
+
content: `[${doc.title ?? "Embedded HTML"}](${doc.url ?? doc.path})`,
|
|
1462
|
+
content_type: "text/markdown",
|
|
1463
|
+
line: 0
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
return null;
|
|
1419
1468
|
default:
|
|
1420
1469
|
return null;
|
|
1421
1470
|
}
|
|
@@ -1514,6 +1563,17 @@ function copyDocEntry(entry) {
|
|
|
1514
1563
|
phase: entry.phase,
|
|
1515
1564
|
...children
|
|
1516
1565
|
};
|
|
1566
|
+
case "html":
|
|
1567
|
+
return {
|
|
1568
|
+
kind: "html",
|
|
1569
|
+
...entry.path !== void 0 ? { path: entry.path } : {},
|
|
1570
|
+
...entry.url !== void 0 ? { url: entry.url } : {},
|
|
1571
|
+
...entry.content !== void 0 ? { content: entry.content } : {},
|
|
1572
|
+
...entry.title !== void 0 ? { title: entry.title } : {},
|
|
1573
|
+
...entry.height !== void 0 ? { height: entry.height } : {},
|
|
1574
|
+
phase: entry.phase,
|
|
1575
|
+
...children
|
|
1576
|
+
};
|
|
1517
1577
|
case "custom":
|
|
1518
1578
|
return {
|
|
1519
1579
|
kind: "custom",
|
|
@@ -1902,7 +1962,7 @@ function scenarioHasDocs(scenario) {
|
|
|
1902
1962
|
import * as fs2 from "fs";
|
|
1903
1963
|
import * as path3 from "path";
|
|
1904
1964
|
|
|
1905
|
-
// src/formatters/html/template.ts
|
|
1965
|
+
// src/formatters/html/template-scripts.ts
|
|
1906
1966
|
var JS_THEME = `
|
|
1907
1967
|
// Theme management
|
|
1908
1968
|
function getSystemTheme() {
|
|
@@ -2704,6 +2764,23 @@ function parseMarkdownSections(marked) {
|
|
|
2704
2764
|
});
|
|
2705
2765
|
}
|
|
2706
2766
|
`;
|
|
2767
|
+
var JS_HTML_EMBED = `
|
|
2768
|
+
// Open srcdoc-embedded HTML (doc-html iframes) in a new tab via a blob URL
|
|
2769
|
+
function initHtmlEmbeds() {
|
|
2770
|
+
document.querySelectorAll('.doc-html-open-srcdoc').forEach((btn) => {
|
|
2771
|
+
btn.addEventListener('click', function() {
|
|
2772
|
+
const container = btn.closest('.doc-html');
|
|
2773
|
+
const iframe = container ? container.querySelector('iframe.doc-html-frame') : null;
|
|
2774
|
+
const html = iframe ? iframe.getAttribute('srcdoc') : null;
|
|
2775
|
+
if (!html) return;
|
|
2776
|
+
const url = URL.createObjectURL(new Blob([html], { type: 'text/html' }));
|
|
2777
|
+
window.open(url, '_blank', 'noopener');
|
|
2778
|
+
});
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
`;
|
|
2782
|
+
|
|
2783
|
+
// src/formatters/html/template.ts
|
|
2707
2784
|
function generateScript(options) {
|
|
2708
2785
|
const initCalls = [];
|
|
2709
2786
|
if (options.includeDarkMode) {
|
|
@@ -2721,6 +2798,7 @@ function generateScript(options) {
|
|
|
2721
2798
|
initCalls.push("initHashScroll();");
|
|
2722
2799
|
initCalls.push("initToc();");
|
|
2723
2800
|
initCalls.push("initThemePicker();");
|
|
2801
|
+
initCalls.push("initHtmlEmbeds();");
|
|
2724
2802
|
const initScript = `
|
|
2725
2803
|
// Initialize on load
|
|
2726
2804
|
document.addEventListener('DOMContentLoaded', () => {
|
|
@@ -2729,6 +2807,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2729
2807
|
`;
|
|
2730
2808
|
let script = options.includeDarkMode ? JS_THEME : "";
|
|
2731
2809
|
script += JS_CORE;
|
|
2810
|
+
script += JS_HTML_EMBED;
|
|
2732
2811
|
if (options.additionalJs) {
|
|
2733
2812
|
script += options.additionalJs;
|
|
2734
2813
|
}
|
|
@@ -4368,6 +4447,82 @@ body {
|
|
|
4368
4447
|
opacity: 0.8;
|
|
4369
4448
|
}
|
|
4370
4449
|
|
|
4450
|
+
/* ============================================================================
|
|
4451
|
+
Documentation Entries - Embedded HTML
|
|
4452
|
+
============================================================================ */
|
|
4453
|
+
.doc-html {
|
|
4454
|
+
margin-bottom: 0.5rem;
|
|
4455
|
+
border: 1px solid var(--border);
|
|
4456
|
+
border-radius: calc(var(--radius) - 2px);
|
|
4457
|
+
overflow: hidden;
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
.doc-html:last-child {
|
|
4461
|
+
margin-bottom: 0;
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
.doc-html-header {
|
|
4465
|
+
display: flex;
|
|
4466
|
+
align-items: center;
|
|
4467
|
+
justify-content: space-between;
|
|
4468
|
+
gap: 0.5rem;
|
|
4469
|
+
padding: 0.375rem 0.75rem;
|
|
4470
|
+
background: var(--muted, transparent);
|
|
4471
|
+
border-bottom: 1px solid var(--border);
|
|
4472
|
+
}
|
|
4473
|
+
|
|
4474
|
+
.doc-html-title {
|
|
4475
|
+
font-size: 0.75rem;
|
|
4476
|
+
font-weight: 600;
|
|
4477
|
+
color: var(--muted-foreground);
|
|
4478
|
+
text-transform: uppercase;
|
|
4479
|
+
letter-spacing: 0.04em;
|
|
4480
|
+
}
|
|
4481
|
+
|
|
4482
|
+
.doc-html-open {
|
|
4483
|
+
font-size: 0.875rem;
|
|
4484
|
+
line-height: 1;
|
|
4485
|
+
padding: 0.125rem 0.375rem;
|
|
4486
|
+
border: 1px solid var(--border);
|
|
4487
|
+
border-radius: calc(var(--radius) - 4px);
|
|
4488
|
+
background: transparent;
|
|
4489
|
+
color: var(--muted-foreground);
|
|
4490
|
+
cursor: pointer;
|
|
4491
|
+
text-decoration: none;
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4494
|
+
.doc-html-open:hover {
|
|
4495
|
+
color: var(--foreground);
|
|
4496
|
+
border-color: var(--foreground);
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
.doc-html-frame {
|
|
4500
|
+
display: block;
|
|
4501
|
+
width: 100%;
|
|
4502
|
+
border: 0;
|
|
4503
|
+
background: #fff;
|
|
4504
|
+
}
|
|
4505
|
+
|
|
4506
|
+
.doc-html-missing {
|
|
4507
|
+
padding: 0.75rem 1rem;
|
|
4508
|
+
border: 1px dashed var(--border);
|
|
4509
|
+
background: var(--muted, transparent);
|
|
4510
|
+
color: var(--muted-foreground);
|
|
4511
|
+
font-size: 0.8125rem;
|
|
4512
|
+
}
|
|
4513
|
+
|
|
4514
|
+
.doc-html-missing-label {
|
|
4515
|
+
font-weight: 600;
|
|
4516
|
+
margin-bottom: 0.25rem;
|
|
4517
|
+
}
|
|
4518
|
+
|
|
4519
|
+
.doc-html-missing-path {
|
|
4520
|
+
font-family: var(--font-mono, ui-monospace, monospace);
|
|
4521
|
+
font-size: 0.75rem;
|
|
4522
|
+
word-break: break-all;
|
|
4523
|
+
opacity: 0.8;
|
|
4524
|
+
}
|
|
4525
|
+
|
|
4371
4526
|
/* ============================================================================
|
|
4372
4527
|
Documentation Entries - Visual Check
|
|
4373
4528
|
============================================================================ */
|
|
@@ -14663,6 +14818,36 @@ function renderDocVideo(entry, deps) {
|
|
|
14663
14818
|
${captionHtml}
|
|
14664
14819
|
</div>`;
|
|
14665
14820
|
}
|
|
14821
|
+
function resolveHtmlSource(entry, deps) {
|
|
14822
|
+
if (entry.url !== void 0) return { mode: "src", value: entry.url };
|
|
14823
|
+
if (entry.content !== void 0) return { mode: "srcdoc", value: entry.content };
|
|
14824
|
+
const filePath = entry.path ?? "";
|
|
14825
|
+
if (/^https?:/i.test(filePath)) return { mode: "src", value: filePath };
|
|
14826
|
+
const inlined = deps.readHtmlFile?.(filePath);
|
|
14827
|
+
if (inlined !== void 0) return { mode: "srcdoc", value: inlined };
|
|
14828
|
+
const isAbsoluteFsPath = /^(?:[/\\]|[A-Za-z]:[/\\])/.test(filePath);
|
|
14829
|
+
if (deps.readHtmlFile && isAbsoluteFsPath) return { mode: "missing", value: filePath };
|
|
14830
|
+
return { mode: "src", value: filePath };
|
|
14831
|
+
}
|
|
14832
|
+
function renderDocHtml(entry, deps) {
|
|
14833
|
+
const source = resolveHtmlSource(entry, deps);
|
|
14834
|
+
if (source.mode === "missing") {
|
|
14835
|
+
return `<div class="doc-html doc-html-missing">
|
|
14836
|
+
<div class="doc-html-missing-label">HTML unavailable</div>
|
|
14837
|
+
<div class="doc-html-missing-path">${deps.escapeHtml(source.value)}</div>
|
|
14838
|
+
</div>`;
|
|
14839
|
+
}
|
|
14840
|
+
const heightCss = typeof entry.height === "number" ? `${entry.height}px` : entry.height ?? "400px";
|
|
14841
|
+
const frame = `<iframe class="doc-html-frame" sandbox="allow-scripts" loading="lazy" style="height: ${deps.escapeHtml(heightCss)};" title="${deps.escapeHtml(entry.title ?? "Embedded HTML")}" ${source.mode}="${deps.escapeHtml(source.value)}"></iframe>`;
|
|
14842
|
+
const openBtn = source.mode === "src" ? `<a class="doc-html-open" href="${deps.escapeHtml(source.value)}" target="_blank" rel="noopener noreferrer" title="Open in new tab" aria-label="Open in new tab">↗</a>` : `<button type="button" class="doc-html-open doc-html-open-srcdoc" title="Open in new tab" aria-label="Open in new tab">↗</button>`;
|
|
14843
|
+
return `<div class="doc-html">
|
|
14844
|
+
<div class="doc-html-header">
|
|
14845
|
+
<span class="doc-html-title">${deps.escapeHtml(entry.title ?? "HTML")}</span>
|
|
14846
|
+
${openBtn}
|
|
14847
|
+
</div>
|
|
14848
|
+
${frame}
|
|
14849
|
+
</div>`;
|
|
14850
|
+
}
|
|
14666
14851
|
function renderDocCustom(entry, deps) {
|
|
14667
14852
|
if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
|
|
14668
14853
|
const data = entry.data;
|
|
@@ -14719,6 +14904,9 @@ function renderDocEntry(entry, deps) {
|
|
|
14719
14904
|
case "video":
|
|
14720
14905
|
html = renderDocVideo(entry, deps);
|
|
14721
14906
|
break;
|
|
14907
|
+
case "html":
|
|
14908
|
+
html = renderDocHtml(entry, deps);
|
|
14909
|
+
break;
|
|
14722
14910
|
case "custom":
|
|
14723
14911
|
html = renderDocCustom(entry, deps);
|
|
14724
14912
|
break;
|
|
@@ -15285,6 +15473,21 @@ function readScreenshotAsDataUri(filePath) {
|
|
|
15285
15473
|
return void 0;
|
|
15286
15474
|
}
|
|
15287
15475
|
}
|
|
15476
|
+
var HTML_INLINE_WARN_BYTES = 1024 * 1024;
|
|
15477
|
+
function readHtmlFileContent(filePath) {
|
|
15478
|
+
try {
|
|
15479
|
+
if (!fs2.existsSync(filePath)) return void 0;
|
|
15480
|
+
const buf = fs2.readFileSync(filePath);
|
|
15481
|
+
if (buf.byteLength > HTML_INLINE_WARN_BYTES) {
|
|
15482
|
+
console.warn(
|
|
15483
|
+
`[executable-stories] Inlining large HTML file (${Math.round(buf.byteLength / 1024)} KiB) into the report: ${filePath}. Consider --asset-mode copy.`
|
|
15484
|
+
);
|
|
15485
|
+
}
|
|
15486
|
+
return buf.toString("utf8");
|
|
15487
|
+
} catch {
|
|
15488
|
+
return void 0;
|
|
15489
|
+
}
|
|
15490
|
+
}
|
|
15288
15491
|
function normalizeOptions(options = {}) {
|
|
15289
15492
|
return {
|
|
15290
15493
|
title: options.title ?? "Test Results",
|
|
@@ -15292,6 +15495,7 @@ function normalizeOptions(options = {}) {
|
|
|
15292
15495
|
searchable: options.searchable ?? true,
|
|
15293
15496
|
startCollapsed: options.startCollapsed ?? false,
|
|
15294
15497
|
embedScreenshots: options.embedScreenshots ?? true,
|
|
15498
|
+
embedHtmlFiles: options.embedHtmlFiles ?? true,
|
|
15295
15499
|
syntaxHighlighting: options.syntaxHighlighting ?? true,
|
|
15296
15500
|
mermaidEnabled: options.mermaidEnabled ?? true,
|
|
15297
15501
|
markdownEnabled: options.markdownEnabled ?? true,
|
|
@@ -15310,7 +15514,10 @@ function createHtmlFormatter(options = {}) {
|
|
|
15310
15514
|
markdownEnabled: opts.markdownEnabled,
|
|
15311
15515
|
mermaidEnabled: opts.mermaidEnabled,
|
|
15312
15516
|
embedScreenshots: opts.embedScreenshots,
|
|
15313
|
-
readScreenshot: (filePath) => readScreenshotAsDataUri(filePath)
|
|
15517
|
+
readScreenshot: (filePath) => readScreenshotAsDataUri(filePath),
|
|
15518
|
+
// When html-file inlining is off (e.g. --asset-mode copy), omit the read
|
|
15519
|
+
// hook so doc-html iframes keep their src path for the asset bundler.
|
|
15520
|
+
...opts.embedHtmlFiles ? { readHtmlFile: (filePath) => readHtmlFileContent(filePath) } : {}
|
|
15314
15521
|
};
|
|
15315
15522
|
const renderDocs = (docs, containerClass) => {
|
|
15316
15523
|
if (!docs || docs.length === 0) return "";
|
|
@@ -15602,6 +15809,8 @@ var JUnitFormatter = class {
|
|
|
15602
15809
|
}
|
|
15603
15810
|
case "screenshot":
|
|
15604
15811
|
return `${indent}Screenshot: ${entry.alt ?? entry.path}`;
|
|
15812
|
+
case "html":
|
|
15813
|
+
return `${indent}HTML: ${entry.title ?? "Embedded HTML"} (${entry.url ?? entry.path ?? "inline"})`;
|
|
15605
15814
|
case "custom": {
|
|
15606
15815
|
const dataStr = JSON.stringify(entry.data, null, 2);
|
|
15607
15816
|
const lines = [];
|
|
@@ -16053,6 +16262,25 @@ var MarkdownFormatter = class {
|
|
|
16053
16262
|
lines.push(`${indent}`);
|
|
16054
16263
|
break;
|
|
16055
16264
|
}
|
|
16265
|
+
case "html": {
|
|
16266
|
+
const htmlLabel = entry.title ?? "Embedded HTML";
|
|
16267
|
+
if (entry.url !== void 0 || entry.path !== void 0) {
|
|
16268
|
+
lines.push(`${indent}[${htmlLabel}](${entry.url ?? entry.path})`);
|
|
16269
|
+
break;
|
|
16270
|
+
}
|
|
16271
|
+
lines.push(`${indent}<details>`);
|
|
16272
|
+
lines.push(`${indent}<summary>${htmlLabel}</summary>`);
|
|
16273
|
+
lines.push(`${indent}`);
|
|
16274
|
+
lines.push(`${indent}\`\`\`html`);
|
|
16275
|
+
for (const line of (entry.content ?? "").split("\n")) {
|
|
16276
|
+
lines.push(`${indent}${line}`);
|
|
16277
|
+
}
|
|
16278
|
+
lines.push(`${indent}\`\`\``);
|
|
16279
|
+
lines.push(`${indent}`);
|
|
16280
|
+
lines.push(`${indent}</details>`);
|
|
16281
|
+
lines.push(`${indent}`);
|
|
16282
|
+
break;
|
|
16283
|
+
}
|
|
16056
16284
|
case "custom":
|
|
16057
16285
|
if (entry.type === "visual" && entry.data && typeof entry.data === "object") {
|
|
16058
16286
|
const data = entry.data;
|
|
@@ -16227,6 +16455,132 @@ function escapePipe(value) {
|
|
|
16227
16455
|
return value.replace(/\|/g, "\\|");
|
|
16228
16456
|
}
|
|
16229
16457
|
|
|
16458
|
+
// src/formatters/traceability-matrix.ts
|
|
16459
|
+
var TraceabilityMatrixFormatter = class {
|
|
16460
|
+
format(run) {
|
|
16461
|
+
const matrix = toTraceabilityMatrix(run);
|
|
16462
|
+
const lines = [];
|
|
16463
|
+
lines.push("# Traceability Matrix");
|
|
16464
|
+
lines.push("");
|
|
16465
|
+
lines.push(`Generated: ${matrix.generatedAt}`);
|
|
16466
|
+
lines.push(`Run: ${matrix.run.startedAt} to ${matrix.run.finishedAt}`);
|
|
16467
|
+
if (matrix.run.branch) lines.push(`Branch: ${matrix.run.branch}`);
|
|
16468
|
+
if (matrix.run.gitSha) lines.push(`Commit: ${matrix.run.gitSha}`);
|
|
16469
|
+
lines.push("");
|
|
16470
|
+
lines.push("| Requirements | Verified | Failing | Scenarios | Untraced |");
|
|
16471
|
+
lines.push("| ---: | ---: | ---: | ---: | ---: |");
|
|
16472
|
+
lines.push(
|
|
16473
|
+
`| ${matrix.summary.requirements} | ${matrix.summary.requirementsVerified} | ${matrix.summary.requirementsFailing} | ${matrix.summary.scenarios} | ${matrix.summary.untracedScenarios} |`
|
|
16474
|
+
);
|
|
16475
|
+
lines.push("");
|
|
16476
|
+
for (const req of matrix.requirements) {
|
|
16477
|
+
const heading2 = req.url ? `[${req.ticket}](${req.url})` : req.ticket;
|
|
16478
|
+
lines.push(`## ${heading2}`);
|
|
16479
|
+
lines.push("");
|
|
16480
|
+
lines.push(`Status: ${renderRequirementStatus(req.status)}`);
|
|
16481
|
+
if (req.covers.length > 0) {
|
|
16482
|
+
lines.push(`Covers: ${req.covers.map((path16) => `\`${path16}\``).join(", ")}`);
|
|
16483
|
+
}
|
|
16484
|
+
lines.push("");
|
|
16485
|
+
lines.push("| Status | Scenario | Source | Covers |");
|
|
16486
|
+
lines.push("| --- | --- | --- | --- |");
|
|
16487
|
+
for (const scenario of req.scenarios) {
|
|
16488
|
+
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
16489
|
+
const covers = scenario.covers.length > 0 ? scenario.covers.map((path16) => `\`${path16}\``).join(", ") : "";
|
|
16490
|
+
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
|
|
16491
|
+
}
|
|
16492
|
+
lines.push("");
|
|
16493
|
+
}
|
|
16494
|
+
if (matrix.untraced.length > 0) {
|
|
16495
|
+
lines.push("## Untraced scenarios");
|
|
16496
|
+
lines.push("");
|
|
16497
|
+
lines.push("Behavior with no requirement link. Add a `ticket` to each so it appears against a requirement.");
|
|
16498
|
+
lines.push("");
|
|
16499
|
+
lines.push("| Status | Scenario | Source |");
|
|
16500
|
+
lines.push("| --- | --- | --- |");
|
|
16501
|
+
for (const scenario of matrix.untraced) {
|
|
16502
|
+
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
16503
|
+
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` |`);
|
|
16504
|
+
}
|
|
16505
|
+
lines.push("");
|
|
16506
|
+
}
|
|
16507
|
+
return lines.join("\n").trimEnd();
|
|
16508
|
+
}
|
|
16509
|
+
};
|
|
16510
|
+
function toTraceabilityMatrix(run) {
|
|
16511
|
+
const sorted = [...run.testCases].sort((a, b) => a.id.localeCompare(b.id));
|
|
16512
|
+
const byTicket = /* @__PURE__ */ new Map();
|
|
16513
|
+
const untraced = [];
|
|
16514
|
+
for (const tc of sorted) {
|
|
16515
|
+
const tickets = tc.story.tickets ?? [];
|
|
16516
|
+
if (tickets.length === 0) {
|
|
16517
|
+
untraced.push({
|
|
16518
|
+
id: tc.id,
|
|
16519
|
+
title: tc.story.scenario,
|
|
16520
|
+
status: tc.status,
|
|
16521
|
+
sourceFile: tc.sourceFile,
|
|
16522
|
+
sourceLine: tc.sourceLine
|
|
16523
|
+
});
|
|
16524
|
+
continue;
|
|
16525
|
+
}
|
|
16526
|
+
for (const ticket of tickets) {
|
|
16527
|
+
const entry = byTicket.get(ticket.id) ?? { url: ticket.url, cases: [] };
|
|
16528
|
+
if (!entry.url && ticket.url) entry.url = ticket.url;
|
|
16529
|
+
entry.cases.push(tc);
|
|
16530
|
+
byTicket.set(ticket.id, entry);
|
|
16531
|
+
}
|
|
16532
|
+
}
|
|
16533
|
+
const requirements = [...byTicket.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([ticket, entry]) => {
|
|
16534
|
+
const scenarios = entry.cases.map((tc) => ({
|
|
16535
|
+
id: tc.id,
|
|
16536
|
+
title: tc.story.scenario,
|
|
16537
|
+
status: tc.status,
|
|
16538
|
+
sourceFile: tc.sourceFile,
|
|
16539
|
+
sourceLine: tc.sourceLine,
|
|
16540
|
+
covers: tc.story.covers ?? []
|
|
16541
|
+
}));
|
|
16542
|
+
const covers = [...new Set(scenarios.flatMap((s) => s.covers))].sort();
|
|
16543
|
+
return { ticket, url: entry.url, status: requirementStatus(entry.cases), scenarios, covers };
|
|
16544
|
+
});
|
|
16545
|
+
return {
|
|
16546
|
+
schemaVersion: "1.0",
|
|
16547
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16548
|
+
run: {
|
|
16549
|
+
startedAt: new Date(run.startedAtMs).toISOString(),
|
|
16550
|
+
finishedAt: new Date(run.finishedAtMs).toISOString(),
|
|
16551
|
+
gitSha: run.gitSha,
|
|
16552
|
+
branch: run.ci?.branch
|
|
16553
|
+
},
|
|
16554
|
+
summary: {
|
|
16555
|
+
requirements: requirements.length,
|
|
16556
|
+
requirementsVerified: requirements.filter((r) => r.status === "verified").length,
|
|
16557
|
+
requirementsFailing: requirements.filter((r) => r.status === "failing").length,
|
|
16558
|
+
scenarios: run.testCases.length,
|
|
16559
|
+
untracedScenarios: untraced.length
|
|
16560
|
+
},
|
|
16561
|
+
requirements,
|
|
16562
|
+
untraced
|
|
16563
|
+
};
|
|
16564
|
+
}
|
|
16565
|
+
function requirementStatus(cases) {
|
|
16566
|
+
if (cases.some((tc) => tc.status === "failed")) return "failing";
|
|
16567
|
+
if (cases.some((tc) => tc.status === "passed")) return "verified";
|
|
16568
|
+
return "incomplete";
|
|
16569
|
+
}
|
|
16570
|
+
function renderRequirementStatus(status) {
|
|
16571
|
+
switch (status) {
|
|
16572
|
+
case "verified":
|
|
16573
|
+
return "verified (all scenarios passed)";
|
|
16574
|
+
case "failing":
|
|
16575
|
+
return "failing (a scenario failed)";
|
|
16576
|
+
default:
|
|
16577
|
+
return "incomplete (no scenario passed yet)";
|
|
16578
|
+
}
|
|
16579
|
+
}
|
|
16580
|
+
function escapePipe2(value) {
|
|
16581
|
+
return value.replace(/\|/g, "\\|");
|
|
16582
|
+
}
|
|
16583
|
+
|
|
16230
16584
|
// src/formatters/cucumber-messages/synthesize-feature.ts
|
|
16231
16585
|
function extractFeatureName(testCases, uri) {
|
|
16232
16586
|
for (const tc of testCases) {
|
|
@@ -17326,6 +17680,8 @@ function formatDocEntry(doc) {
|
|
|
17326
17680
|
return `${doc.alt ? `${escapeHtml2(doc.alt)}: ` : ""}${escapeHtml2(doc.path)}`;
|
|
17327
17681
|
case "video":
|
|
17328
17682
|
return `${doc.caption ? `${escapeHtml2(doc.caption)}: ` : ""}${escapeHtml2(doc.path)}`;
|
|
17683
|
+
case "html":
|
|
17684
|
+
return `${doc.title ? `${escapeHtml2(doc.title)}: ` : ""}${escapeHtml2(doc.url ?? doc.path ?? "(inline html)")}`;
|
|
17329
17685
|
case "custom":
|
|
17330
17686
|
return `${escapeHtml2(doc.type)}: ${escapeHtml2(JSON.stringify(doc.data))}`;
|
|
17331
17687
|
}
|
|
@@ -17784,6 +18140,8 @@ function formatDocEntry2(doc) {
|
|
|
17784
18140
|
return `${doc.alt ? `${doc.alt}: ` : ""}${doc.path}`;
|
|
17785
18141
|
case "video":
|
|
17786
18142
|
return `${doc.caption ? `${doc.caption}: ` : ""}${doc.path}`;
|
|
18143
|
+
case "html":
|
|
18144
|
+
return `${doc.title ? `${doc.title}: ` : ""}${doc.url ?? doc.path ?? "(inline html)"}`;
|
|
17787
18145
|
case "custom":
|
|
17788
18146
|
return `${doc.type}: ${JSON.stringify(doc.data)}`;
|
|
17789
18147
|
}
|
|
@@ -17933,7 +18291,7 @@ import * as path5 from "path";
|
|
|
17933
18291
|
function scanHtmlAssets(html) {
|
|
17934
18292
|
const seen = /* @__PURE__ */ new Set();
|
|
17935
18293
|
const patterns = [
|
|
17936
|
-
/<(?:img|video)\b[^>]*?\bsrc=["']([^"']+)["']/g,
|
|
18294
|
+
/<(?:img|video|iframe)\b[^>]*?\bsrc=["']([^"']+)["']/g,
|
|
17937
18295
|
/<a\b[^>]*?\bclass=["']attachment["'][^>]*?\bhref=["']([^"']+)["']/g,
|
|
17938
18296
|
/<a\b[^>]*?\bhref=["']([^"']+)["'][^>]*?\bclass=["']attachment["']/g
|
|
17939
18297
|
];
|
|
@@ -18012,7 +18370,7 @@ function bundleAssets(htmlPath, options = {}) {
|
|
|
18012
18370
|
function replaceAssetRef(html, original, replacement) {
|
|
18013
18371
|
const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
18014
18372
|
const srcPattern = new RegExp(
|
|
18015
|
-
`(<(?:img|video)\\b[^>]*?\\bsrc=["'])${escaped}(["'])`,
|
|
18373
|
+
`(<(?:img|video|iframe)\\b[^>]*?\\bsrc=["'])${escaped}(["'])`,
|
|
18016
18374
|
"g"
|
|
18017
18375
|
);
|
|
18018
18376
|
html = html.replace(srcPattern, `$1${replacement}$2`);
|
|
@@ -18400,6 +18758,21 @@ ${tc.errorStack}` : "");
|
|
|
18400
18758
|
])
|
|
18401
18759
|
);
|
|
18402
18760
|
break;
|
|
18761
|
+
case "html":
|
|
18762
|
+
if (entry.url !== void 0 || entry.path !== void 0) {
|
|
18763
|
+
const target = entry.url ?? entry.path ?? "";
|
|
18764
|
+
content.push(
|
|
18765
|
+
paragraph([
|
|
18766
|
+
text(entry.title ?? "Embedded HTML", strong()),
|
|
18767
|
+
text(": "),
|
|
18768
|
+
link(target, target)
|
|
18769
|
+
])
|
|
18770
|
+
);
|
|
18771
|
+
break;
|
|
18772
|
+
}
|
|
18773
|
+
content.push(paragraph([text(entry.title ?? "Embedded HTML", strong())]));
|
|
18774
|
+
content.push(codeBlock(entry.content ?? "", "html"));
|
|
18775
|
+
break;
|
|
18403
18776
|
case "custom":
|
|
18404
18777
|
content.push(paragraph([text(`[${entry.type}]`, strong())]));
|
|
18405
18778
|
content.push(codeBlock(JSON.stringify(entry.data ?? null, null, 2), "json"));
|
|
@@ -20024,6 +20397,280 @@ function collectDocKinds(testCase) {
|
|
|
20024
20397
|
return [...kinds].sort();
|
|
20025
20398
|
}
|
|
20026
20399
|
|
|
20400
|
+
// src/scenario-failure.ts
|
|
20401
|
+
function failingScenarioMessage(tc) {
|
|
20402
|
+
const failingStep = tc.stepResults.find((s) => s.status === "failed" && s.errorMessage);
|
|
20403
|
+
return failingStep?.errorMessage ?? tc.errorMessage;
|
|
20404
|
+
}
|
|
20405
|
+
|
|
20406
|
+
// src/check.ts
|
|
20407
|
+
var ICON_PASS = "\u2713";
|
|
20408
|
+
var ICON_FAIL = "\u2717";
|
|
20409
|
+
var ICON_SKIP = "\u2298";
|
|
20410
|
+
var ICON_PENDING = "\u23F3";
|
|
20411
|
+
var ICON_WARN = "\u26A0";
|
|
20412
|
+
function buildCheck(args, _deps = {}) {
|
|
20413
|
+
const { testCases, baseline } = args;
|
|
20414
|
+
const summary = {
|
|
20415
|
+
total: testCases.length,
|
|
20416
|
+
passed: testCases.filter((tc) => tc.status === "passed").length,
|
|
20417
|
+
failed: testCases.filter((tc) => tc.status === "failed").length,
|
|
20418
|
+
skipped: testCases.filter((tc) => tc.status === "skipped").length,
|
|
20419
|
+
pending: testCases.filter((tc) => tc.status === "pending").length
|
|
20420
|
+
};
|
|
20421
|
+
let regressed = 0;
|
|
20422
|
+
let fixed = 0;
|
|
20423
|
+
if (baseline) {
|
|
20424
|
+
for (const tc of testCases) {
|
|
20425
|
+
const before = baseline.get(tc.id);
|
|
20426
|
+
if (before === "passed" && tc.status === "failed") regressed += 1;
|
|
20427
|
+
if (before === "failed" && tc.status === "passed") fixed += 1;
|
|
20428
|
+
}
|
|
20429
|
+
}
|
|
20430
|
+
const failures = testCases.filter((tc) => tc.status === "failed").map((tc) => toFailure(tc, baseline)).sort((a, b) => {
|
|
20431
|
+
if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
|
|
20432
|
+
return a.location.localeCompare(b.location);
|
|
20433
|
+
});
|
|
20434
|
+
return {
|
|
20435
|
+
summary,
|
|
20436
|
+
failures,
|
|
20437
|
+
regressed,
|
|
20438
|
+
fixed,
|
|
20439
|
+
comparedToBaseline: baseline !== void 0
|
|
20440
|
+
};
|
|
20441
|
+
}
|
|
20442
|
+
function toFailure(tc, baseline) {
|
|
20443
|
+
const failedIndexes = new Set(
|
|
20444
|
+
tc.stepResults.filter((s) => s.status === "failed").map((s) => s.index)
|
|
20445
|
+
);
|
|
20446
|
+
const steps = tc.story.steps.map((step, index) => ({
|
|
20447
|
+
keyword: step.keyword,
|
|
20448
|
+
text: step.text,
|
|
20449
|
+
failed: failedIndexes.has(index)
|
|
20450
|
+
}));
|
|
20451
|
+
return {
|
|
20452
|
+
id: tc.id,
|
|
20453
|
+
scenario: tc.story.scenario,
|
|
20454
|
+
location: `${tc.sourceFile}:${tc.sourceLine}`,
|
|
20455
|
+
steps,
|
|
20456
|
+
errorMessage: failingScenarioMessage(tc),
|
|
20457
|
+
covers: tc.story.covers ?? [],
|
|
20458
|
+
tickets: (tc.story.tickets ?? []).map((t) => t.id),
|
|
20459
|
+
regressed: baseline?.get(tc.id) === "passed"
|
|
20460
|
+
};
|
|
20461
|
+
}
|
|
20462
|
+
function renderCheck(report, format) {
|
|
20463
|
+
return format === "json" ? JSON.stringify(report, null, 2) : renderCheckText(report);
|
|
20464
|
+
}
|
|
20465
|
+
function renderCheckText(report) {
|
|
20466
|
+
const { summary, failures } = report;
|
|
20467
|
+
const headlineParts = [`${ICON_PASS} ${summary.passed} passed`];
|
|
20468
|
+
if (summary.failed > 0) headlineParts.push(`${ICON_FAIL} ${summary.failed} failed`);
|
|
20469
|
+
if (summary.skipped > 0) headlineParts.push(`${ICON_SKIP} ${summary.skipped} skipped`);
|
|
20470
|
+
if (summary.pending > 0) headlineParts.push(`${ICON_PENDING} ${summary.pending} pending`);
|
|
20471
|
+
const headline = `${headlineParts.join(" ")} (${summary.total} scenarios)`;
|
|
20472
|
+
if (failures.length === 0) {
|
|
20473
|
+
const lines2 = [headline];
|
|
20474
|
+
if (report.comparedToBaseline && report.fixed > 0) {
|
|
20475
|
+
lines2.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
|
|
20476
|
+
}
|
|
20477
|
+
lines2.push("All scenarios green.");
|
|
20478
|
+
return lines2.join("\n");
|
|
20479
|
+
}
|
|
20480
|
+
const lines = [headline, ""];
|
|
20481
|
+
for (const f of failures) {
|
|
20482
|
+
lines.push(`${ICON_FAIL} ${f.scenario}${f.regressed ? " (regressed)" : ""}`);
|
|
20483
|
+
lines.push(` ${f.location}`);
|
|
20484
|
+
for (const step of f.steps) {
|
|
20485
|
+
const marker = step.failed ? ` ${ICON_FAIL} ` : " ";
|
|
20486
|
+
lines.push(`${marker}${step.keyword} ${step.text}`);
|
|
20487
|
+
}
|
|
20488
|
+
if (f.errorMessage) {
|
|
20489
|
+
const firstLine = f.errorMessage.split("\n")[0];
|
|
20490
|
+
lines.push(` \u2192 ${firstLine}`);
|
|
20491
|
+
}
|
|
20492
|
+
if (f.covers.length > 0) {
|
|
20493
|
+
lines.push(` covers: ${f.covers.join(", ")}`);
|
|
20494
|
+
}
|
|
20495
|
+
if (f.tickets.length > 0) {
|
|
20496
|
+
lines.push(` ticket: ${f.tickets.join(", ")}`);
|
|
20497
|
+
}
|
|
20498
|
+
lines.push("");
|
|
20499
|
+
}
|
|
20500
|
+
if (report.comparedToBaseline) {
|
|
20501
|
+
if (report.regressed > 0) {
|
|
20502
|
+
lines.push(`${ICON_WARN} ${report.regressed} regressed since baseline (was passing).`);
|
|
20503
|
+
}
|
|
20504
|
+
if (report.fixed > 0) {
|
|
20505
|
+
lines.push(`${ICON_PASS} ${report.fixed} fixed since baseline.`);
|
|
20506
|
+
}
|
|
20507
|
+
if (report.regressed === 0 && report.fixed === 0) {
|
|
20508
|
+
lines.push("No status changes vs. baseline.");
|
|
20509
|
+
}
|
|
20510
|
+
}
|
|
20511
|
+
return lines.join("\n").trimEnd();
|
|
20512
|
+
}
|
|
20513
|
+
|
|
20514
|
+
// src/goal.ts
|
|
20515
|
+
var ACTIVE = ["passed", "failed"];
|
|
20516
|
+
function buildGoal(args, _deps = {}) {
|
|
20517
|
+
const { run, baseline } = args;
|
|
20518
|
+
const cases = run.testCases;
|
|
20519
|
+
const selectors = [
|
|
20520
|
+
...args.requireTags.map((tag) => ({ label: `tag:${tag}`, match: (tc) => tc.tags.includes(tag) })),
|
|
20521
|
+
...args.requireTickets.map((id) => ({ label: `ticket:${id}`, match: (tc) => (tc.story.tickets ?? []).some((t) => t.id === id) })),
|
|
20522
|
+
...args.requireScenarios.map((sel) => ({ label: `scenario:${sel}`, match: (tc) => tc.id === sel || tc.story.scenario === sel }))
|
|
20523
|
+
];
|
|
20524
|
+
const requirements = selectors.length === 0 ? [evaluate("all scenarios", cases)] : selectors.map((s) => evaluate(s.label, cases.filter(s.match)));
|
|
20525
|
+
const regressions = [];
|
|
20526
|
+
if (baseline && args.enforceNoRegressions) {
|
|
20527
|
+
const before = statusMap(baseline);
|
|
20528
|
+
for (const tc of cases) {
|
|
20529
|
+
if (before.get(tc.id) === "passed" && tc.status === "failed") {
|
|
20530
|
+
regressions.push({ id: tc.id, title: tc.story.scenario });
|
|
20531
|
+
}
|
|
20532
|
+
}
|
|
20533
|
+
}
|
|
20534
|
+
const violations = [];
|
|
20535
|
+
if (baseline && args.enforceRatchet) {
|
|
20536
|
+
const current = new Map(cases.map((tc) => [tc.id, tc]));
|
|
20537
|
+
for (const base of baseline.testCases) {
|
|
20538
|
+
const now = current.get(base.id);
|
|
20539
|
+
if (!now) {
|
|
20540
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "removed", detail: "scenario no longer present" });
|
|
20541
|
+
continue;
|
|
20542
|
+
}
|
|
20543
|
+
if (ACTIVE.includes(base.status) && (now.status === "skipped" || now.status === "pending")) {
|
|
20544
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "disabled", detail: `${base.status} -> ${now.status}` });
|
|
20545
|
+
}
|
|
20546
|
+
const baseSteps = base.story.steps.length;
|
|
20547
|
+
const nowSteps = now.story.steps.length;
|
|
20548
|
+
if (nowSteps < baseSteps) {
|
|
20549
|
+
violations.push({ id: base.id, title: base.story.scenario, kind: "weakened", detail: `${baseSteps} steps -> ${nowSteps} steps` });
|
|
20550
|
+
}
|
|
20551
|
+
}
|
|
20552
|
+
}
|
|
20553
|
+
const met = requirements.every((r) => r.met) && regressions.length === 0 && violations.length === 0;
|
|
20554
|
+
return {
|
|
20555
|
+
met,
|
|
20556
|
+
requirements,
|
|
20557
|
+
regressions,
|
|
20558
|
+
regressionsEnforced: Boolean(baseline && args.enforceNoRegressions),
|
|
20559
|
+
ratchet: { enforced: Boolean(baseline && args.enforceRatchet), violations }
|
|
20560
|
+
};
|
|
20561
|
+
}
|
|
20562
|
+
function evaluate(selector, matched) {
|
|
20563
|
+
const passed = matched.filter((tc) => tc.status === "passed").length;
|
|
20564
|
+
const failing = matched.filter((tc) => tc.status !== "passed").map((tc) => tc.story.scenario);
|
|
20565
|
+
return {
|
|
20566
|
+
selector,
|
|
20567
|
+
matched: matched.length,
|
|
20568
|
+
passed,
|
|
20569
|
+
failing,
|
|
20570
|
+
met: matched.length > 0 && failing.length === 0
|
|
20571
|
+
};
|
|
20572
|
+
}
|
|
20573
|
+
function statusMap(run) {
|
|
20574
|
+
return new Map(run.testCases.map((tc) => [tc.id, tc.status]));
|
|
20575
|
+
}
|
|
20576
|
+
function renderGoal(report, format) {
|
|
20577
|
+
if (format === "json") return JSON.stringify(report, null, 2);
|
|
20578
|
+
const lines = [`GOAL: ${report.met ? "met" : "not met"}`];
|
|
20579
|
+
for (const req of report.requirements) {
|
|
20580
|
+
if (req.matched === 0) {
|
|
20581
|
+
lines.push(` ${req.selector}: no matching scenario (no proof)`);
|
|
20582
|
+
continue;
|
|
20583
|
+
}
|
|
20584
|
+
const tail = req.failing.length > 0 ? ` (${req.failing.length} failing)` : "";
|
|
20585
|
+
lines.push(` ${req.selector}: ${req.passed}/${req.matched} scenarios pass${tail}`);
|
|
20586
|
+
}
|
|
20587
|
+
if (report.regressionsEnforced) {
|
|
20588
|
+
if (report.regressions.length === 0) {
|
|
20589
|
+
lines.push(" regressions: 0");
|
|
20590
|
+
} else {
|
|
20591
|
+
lines.push(` regressions: ${report.regressions.length} (${report.regressions.map((r) => r.title).join(", ")})`);
|
|
20592
|
+
}
|
|
20593
|
+
}
|
|
20594
|
+
if (report.ratchet.enforced) {
|
|
20595
|
+
if (report.ratchet.violations.length === 0) {
|
|
20596
|
+
lines.push(" ratchet: clean (0 scenarios removed/weakened)");
|
|
20597
|
+
} else {
|
|
20598
|
+
lines.push(` ratchet: ${report.ratchet.violations.length} removed/weakened`);
|
|
20599
|
+
for (const v of report.ratchet.violations) {
|
|
20600
|
+
lines.push(` ${v.kind}: ${v.title} (${v.detail})`);
|
|
20601
|
+
}
|
|
20602
|
+
}
|
|
20603
|
+
}
|
|
20604
|
+
return lines.join("\n");
|
|
20605
|
+
}
|
|
20606
|
+
|
|
20607
|
+
// src/triage.ts
|
|
20608
|
+
function buildTriage(args, _deps = {}) {
|
|
20609
|
+
const { testCases, baseline } = args;
|
|
20610
|
+
const failing = testCases.filter((tc) => tc.status === "failed");
|
|
20611
|
+
const ranked = failing.map((tc) => {
|
|
20612
|
+
const regressed = baseline?.get(tc.id) === "passed";
|
|
20613
|
+
return {
|
|
20614
|
+
tc,
|
|
20615
|
+
regressed,
|
|
20616
|
+
covers: tc.story.covers ?? []
|
|
20617
|
+
};
|
|
20618
|
+
}).sort((a, b) => {
|
|
20619
|
+
if (a.regressed !== b.regressed) return a.regressed ? -1 : 1;
|
|
20620
|
+
const la = `${a.tc.sourceFile}:${a.tc.sourceLine}`;
|
|
20621
|
+
const lb = `${b.tc.sourceFile}:${b.tc.sourceLine}`;
|
|
20622
|
+
return la.localeCompare(lb);
|
|
20623
|
+
});
|
|
20624
|
+
const items = ranked.map((entry, index) => ({
|
|
20625
|
+
rank: index + 1,
|
|
20626
|
+
id: entry.tc.id,
|
|
20627
|
+
scenario: entry.tc.story.scenario,
|
|
20628
|
+
status: entry.tc.status,
|
|
20629
|
+
location: `${entry.tc.sourceFile}:${entry.tc.sourceLine}`,
|
|
20630
|
+
covers: entry.covers,
|
|
20631
|
+
tickets: (entry.tc.story.tickets ?? []).map((t) => t.id),
|
|
20632
|
+
errorMessage: failingScenarioMessage(entry.tc),
|
|
20633
|
+
regressed: entry.regressed,
|
|
20634
|
+
reason: entry.regressed ? "regression" : "failing"
|
|
20635
|
+
}));
|
|
20636
|
+
return {
|
|
20637
|
+
total: testCases.length,
|
|
20638
|
+
failing: failing.length,
|
|
20639
|
+
regressions: items.filter((i) => i.regressed).length,
|
|
20640
|
+
needsCovers: items.filter((i) => i.covers.length === 0).length,
|
|
20641
|
+
items
|
|
20642
|
+
};
|
|
20643
|
+
}
|
|
20644
|
+
function renderTriage(report, format) {
|
|
20645
|
+
if (format === "json") return JSON.stringify(report, null, 2);
|
|
20646
|
+
if (report.items.length === 0) {
|
|
20647
|
+
return "Nothing to triage. No failing scenarios.";
|
|
20648
|
+
}
|
|
20649
|
+
const header = report.regressions > 0 ? `${report.items.length} items to triage (${report.regressions} regression${report.regressions === 1 ? "" : "s"})` : `${report.items.length} items to triage`;
|
|
20650
|
+
const lines = [header, ""];
|
|
20651
|
+
for (const item of report.items) {
|
|
20652
|
+
const tag = item.regressed ? "[regression] " : "";
|
|
20653
|
+
lines.push(`${item.rank}. ${tag}${item.scenario}`);
|
|
20654
|
+
lines.push(` ${item.location}`);
|
|
20655
|
+
if (item.errorMessage) {
|
|
20656
|
+
lines.push(` \u2192 ${item.errorMessage.split("\n")[0]}`);
|
|
20657
|
+
}
|
|
20658
|
+
if (item.covers.length > 0) {
|
|
20659
|
+
lines.push(` fix: ${item.covers.join(", ")}`);
|
|
20660
|
+
} else {
|
|
20661
|
+
lines.push(" fix: (no covers declared \u2014 add `covers` to route this to code)");
|
|
20662
|
+
}
|
|
20663
|
+
if (item.tickets.length > 0) {
|
|
20664
|
+
lines.push(` ticket: ${item.tickets.join(", ")}`);
|
|
20665
|
+
}
|
|
20666
|
+
lines.push("");
|
|
20667
|
+
}
|
|
20668
|
+
if (report.needsCovers > 0) {
|
|
20669
|
+
lines.push(`${report.needsCovers} failing scenario(s) have no covers and can't be routed to code automatically.`);
|
|
20670
|
+
}
|
|
20671
|
+
return lines.join("\n").trimEnd();
|
|
20672
|
+
}
|
|
20673
|
+
|
|
20027
20674
|
// src/review/conventions.ts
|
|
20028
20675
|
var CHANGE_TAG_PREFIX = "change:";
|
|
20029
20676
|
var AUDIENCE_TAG_PREFIX = "audience:";
|
|
@@ -20825,6 +21472,7 @@ var FORMAT_EXTENSIONS = {
|
|
|
20825
21472
|
"behavior-manifest-json": ".behavior-manifest.json",
|
|
20826
21473
|
markdown: ".md",
|
|
20827
21474
|
"release-manifest": ".release-manifest.md",
|
|
21475
|
+
"traceability-matrix": ".traceability-matrix.md",
|
|
20828
21476
|
html: ".html",
|
|
20829
21477
|
"cucumber-html": ".cucumber.html",
|
|
20830
21478
|
junit: ".junit.xml",
|
|
@@ -20985,6 +21633,9 @@ var ReportGenerator = class {
|
|
|
20985
21633
|
searchable: options.html?.searchable ?? true,
|
|
20986
21634
|
startCollapsed: options.html?.startCollapsed ?? false,
|
|
20987
21635
|
embedScreenshots: options.html?.embedScreenshots ?? true,
|
|
21636
|
+
// Under "copy" asset mode local html files become hashed assets with
|
|
21637
|
+
// an iframe src instead of being inlined into the report.
|
|
21638
|
+
embedHtmlFiles: options.html?.embedHtmlFiles ?? (options.assetMode ?? "none") !== "copy",
|
|
20988
21639
|
syntaxHighlighting: options.html?.syntaxHighlighting ?? true,
|
|
20989
21640
|
mermaidEnabled: options.html?.mermaidEnabled ?? true,
|
|
20990
21641
|
markdownEnabled: options.html?.markdownEnabled ?? true,
|
|
@@ -21160,6 +21811,7 @@ var ReportGenerator = class {
|
|
|
21160
21811
|
searchable: this.options.html.searchable,
|
|
21161
21812
|
startCollapsed: this.options.html.startCollapsed,
|
|
21162
21813
|
embedScreenshots: this.options.html.embedScreenshots,
|
|
21814
|
+
embedHtmlFiles: this.options.html.embedHtmlFiles,
|
|
21163
21815
|
syntaxHighlighting: this.options.html.syntaxHighlighting,
|
|
21164
21816
|
mermaidEnabled: this.options.html.mermaidEnabled,
|
|
21165
21817
|
markdownEnabled: this.options.html.markdownEnabled,
|
|
@@ -21247,6 +21899,10 @@ var ReportGenerator = class {
|
|
|
21247
21899
|
const formatter = new ReleaseManifestFormatter();
|
|
21248
21900
|
return formatter.format(run);
|
|
21249
21901
|
}
|
|
21902
|
+
case "traceability-matrix": {
|
|
21903
|
+
const formatter = new TraceabilityMatrixFormatter();
|
|
21904
|
+
return formatter.format(run);
|
|
21905
|
+
}
|
|
21250
21906
|
case "story-report-json": {
|
|
21251
21907
|
const formatter = new StoryReportJsonFormatter({
|
|
21252
21908
|
pretty: this.options.storyReportJson.pretty
|
|
@@ -21922,7 +22578,7 @@ function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets"
|
|
|
21922
22578
|
const visit = (entries) => {
|
|
21923
22579
|
for (const entry of entries ?? []) {
|
|
21924
22580
|
const e = entry;
|
|
21925
|
-
if (e.kind === "screenshot" || e.kind === "video") {
|
|
22581
|
+
if (e.kind === "screenshot" || e.kind === "video" || e.kind === "html") {
|
|
21926
22582
|
if (typeof e.path === "string" && !isRemote(e.path) && fs13.existsSync(e.path)) {
|
|
21927
22583
|
e.path = bundle(e.path);
|
|
21928
22584
|
}
|
|
@@ -22044,6 +22700,7 @@ var EXIT_GENERATION = 3;
|
|
|
22044
22700
|
var EXIT_USAGE = 4;
|
|
22045
22701
|
var EXIT_COMPARE_GATE = 5;
|
|
22046
22702
|
var EXIT_REVIEW_GATE = 5;
|
|
22703
|
+
var EXIT_AGENT_GATE = 5;
|
|
22047
22704
|
var EXIT_RELEASE_GATE = 6;
|
|
22048
22705
|
var HELP_TEXT = `
|
|
22049
22706
|
executable-stories \u2014 Generate reports from test results JSON.
|
|
@@ -22055,6 +22712,9 @@ USAGE
|
|
|
22055
22712
|
executable-stories gate-release <dev-run.json> <rc-run.json> [options]
|
|
22056
22713
|
executable-stories review <file> --changed-files <path> [options]
|
|
22057
22714
|
executable-stories list <file> [options]
|
|
22715
|
+
executable-stories check <file> [--baseline <path|auto>] [--check-format text|json] [--no-fail]
|
|
22716
|
+
executable-stories goal <file> [--require-tags <csv>] [--require-tickets <csv>] [--require-scenarios <csv>] [--baseline <path|auto>] [--no-regressions] [--goal-format text|json]
|
|
22717
|
+
executable-stories triage <file> [--baseline <path|auto>] [--triage-format text|json]
|
|
22058
22718
|
executable-stories validate <file>
|
|
22059
22719
|
executable-stories validate --stdin
|
|
22060
22720
|
executable-stories init-astro [directory]
|
|
@@ -22074,6 +22734,9 @@ SUBCOMMANDS
|
|
|
22074
22734
|
gate-release Verify a release candidate against the dev test baseline (RC gate)
|
|
22075
22735
|
review Generate an Evidence Review of AI-authored changes (correlate a run to the diff)
|
|
22076
22736
|
list List scenarios from a test run (text table or JSON)
|
|
22737
|
+
check Backpressure summary: compress passing, expand failing (GWT + error + covers); non-zero exit on failures
|
|
22738
|
+
goal Behavioral definition-of-done for agent loops: required scenarios pass, no regressions, no weakened scenarios (exit 0 = met, 5 = not)
|
|
22739
|
+
triage Discovery worklist for agent loops: failing scenarios, regressions first, each with the code it covers
|
|
22077
22740
|
validate Validate a JSON file against the schema (no output generated)
|
|
22078
22741
|
init-astro Scaffold an Astro docs site for story output (Starlight with themed CSS)
|
|
22079
22742
|
new Scaffold a docs page from a template (adr, runbook, decision-log, incident)
|
|
@@ -22084,7 +22747,7 @@ SUBCOMMANDS
|
|
|
22084
22747
|
deploy Record deployments, show environment status, detect drift
|
|
22085
22748
|
|
|
22086
22749
|
OPTIONS
|
|
22087
|
-
--format <formats> Comma-separated formats: html, markdown, release-manifest, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, story-report-json, scenario-index-json, behavior-manifest-json, or custom names from config (default: html)
|
|
22750
|
+
--format <formats> Comma-separated formats: html, markdown, release-manifest, traceability-matrix, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, story-report-json, scenario-index-json, behavior-manifest-json, or custom names from config (default: html)
|
|
22088
22751
|
astro Themed Markdown (for Astro docs sites with matching CSS)
|
|
22089
22752
|
confluence Atlassian Document Format (ADF) JSON for Confluence / Jira
|
|
22090
22753
|
behavior-manifest-json Agent-readable behavior manifest and debugger warnings
|
|
@@ -22096,6 +22759,7 @@ OPTIONS
|
|
|
22096
22759
|
cucumber-messages Raw NDJSON (Cucumber Messages)
|
|
22097
22760
|
story-report-json StoryReport v1 JSON (consumed by executable-stories-react and other UI renderers)
|
|
22098
22761
|
scenario-index-json Storybook-like scenario index for agents and explorers
|
|
22762
|
+
traceability-matrix Requirement-first matrix (ticket -> scenarios -> covered code -> status)
|
|
22099
22763
|
--config <path> Path to executable-stories.config.js (default: ./executable-stories.config.js)
|
|
22100
22764
|
--input-type <type> Input type: raw, canonical, or ndjson (default: raw)
|
|
22101
22765
|
--output-dir <dir> Output directory (default: reports)
|
|
@@ -22122,6 +22786,15 @@ OPTIONS
|
|
|
22122
22786
|
--stdin Read JSON from stdin instead of file
|
|
22123
22787
|
--list-format <format> list output format: text (default), json, csv, markdown-table
|
|
22124
22788
|
--json-summary Deprecated alias for --list-format json
|
|
22789
|
+
--check-format <format> check output format: text (default) or json
|
|
22790
|
+
--no-fail (check) Report only \u2014 always exit 0 even when scenarios failed
|
|
22791
|
+
--require-tags <csv> (goal) Every scenario carrying any of these tags must pass
|
|
22792
|
+
--require-tickets <csv> (goal) Every scenario carrying any of these tickets must pass
|
|
22793
|
+
--require-scenarios <csv> (goal) These scenarios (by id or exact title) must pass
|
|
22794
|
+
--no-regressions (goal) Not met if any scenario regressed vs --baseline
|
|
22795
|
+
--no-ratchet (goal) Disable the removed/weakened-scenario guard (on by default with --baseline)
|
|
22796
|
+
--goal-format <format> goal output format: text (default) or json
|
|
22797
|
+
--triage-format <format> triage output format: text (default) or json
|
|
22125
22798
|
--baseline <path|auto> Compare baseline file, or auto-pick a prior run for compare
|
|
22126
22799
|
--baseline-dir <dir> Directory to scan when --baseline auto is used
|
|
22127
22800
|
--pr-summary Print a PR-friendly markdown summary after compare
|
|
@@ -22146,6 +22819,31 @@ LIST
|
|
|
22146
22819
|
list supports --include-tags, --exclude-tags for filtering
|
|
22147
22820
|
list supports --input-type and --stdin
|
|
22148
22821
|
|
|
22822
|
+
CHECK
|
|
22823
|
+
check is the inner-loop "backpressure" view for coding agents: run it after tests.
|
|
22824
|
+
Passing scenarios collapse to a single count line; each failing scenario expands
|
|
22825
|
+
to its Given/When/Then steps, the step that broke, the error, and the product
|
|
22826
|
+
code it covers \u2014 so the agent gets an actionable signal, not a wall of green.
|
|
22827
|
+
check exits 5 when any scenario failed (so the agent loop pushes back); pass
|
|
22828
|
+
--no-fail to report only. --baseline <path|auto> adds "N regressed / N fixed"
|
|
22829
|
+
since the prior run. --check-format json emits the structured report.
|
|
22830
|
+
|
|
22831
|
+
GOAL
|
|
22832
|
+
goal is the behavioral stopping condition for an agent loop (the /goal pattern).
|
|
22833
|
+
It is "met" when the required scenarios pass, nothing regressed (with
|
|
22834
|
+
--no-regressions), and no scenario was removed, disabled, or had steps deleted
|
|
22835
|
+
versus --baseline (the ratchet, on by default when a baseline is given). Declare
|
|
22836
|
+
the target with --require-tags / --require-tickets / --require-scenarios; with
|
|
22837
|
+
none given, the goal is "every scenario passes". Exit 0 means met, 5 means not
|
|
22838
|
+
yet, so a loop can run until the verdict flips. --goal-format json for machines.
|
|
22839
|
+
|
|
22840
|
+
TRIAGE
|
|
22841
|
+
triage is the discovery-phase worklist for a loop. It lists failing scenarios,
|
|
22842
|
+
regressions first (with --baseline), each with the product code it covers, the
|
|
22843
|
+
error, and its tickets, so the loop can route each fix to a sub-agent. Failures
|
|
22844
|
+
with no covers are flagged. --triage-format json emits the work queue. triage
|
|
22845
|
+
always exits 0 \u2014 it reports work, it does not gate.
|
|
22846
|
+
|
|
22149
22847
|
COMPARE
|
|
22150
22848
|
compare supports --format html,markdown
|
|
22151
22849
|
compare uses the same --input-type for both baseline and current files
|
|
@@ -22218,9 +22916,16 @@ EXIT CODES
|
|
|
22218
22916
|
2 Canonical validation failure
|
|
22219
22917
|
3 Formatter/generation failure
|
|
22220
22918
|
4 Bad arguments / usage error
|
|
22221
|
-
5 Compare gate failed
|
|
22919
|
+
5 Compare / review / check gate failed
|
|
22222
22920
|
6 Release gate failed
|
|
22223
22921
|
`.trim();
|
|
22922
|
+
function parseTextJsonFormat(flag, value) {
|
|
22923
|
+
if (value !== "text" && value !== "json") {
|
|
22924
|
+
console.error(`Error: ${flag} must be "text" or "json", got "${value}".`);
|
|
22925
|
+
process.exit(EXIT_USAGE);
|
|
22926
|
+
}
|
|
22927
|
+
return value;
|
|
22928
|
+
}
|
|
22224
22929
|
async function parseCliArgs(argv) {
|
|
22225
22930
|
const args = argv.slice(2);
|
|
22226
22931
|
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
@@ -22228,9 +22933,9 @@ async function parseCliArgs(argv) {
|
|
|
22228
22933
|
process.exit(EXIT_SUCCESS);
|
|
22229
22934
|
}
|
|
22230
22935
|
const subcommand = args[0];
|
|
22231
|
-
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "build-docs" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
22936
|
+
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "check" && subcommand !== "goal" && subcommand !== "triage" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "build-docs" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
22232
22937
|
console.error(
|
|
22233
|
-
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "validate", "init-astro", "build-docs", "new", "check-links", "import-openapi", "publish-confluence", or "publish-jira".`
|
|
22938
|
+
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "check", "goal", "triage", "validate", "init-astro", "build-docs", "new", "check-links", "import-openapi", "publish-confluence", or "publish-jira".`
|
|
22234
22939
|
);
|
|
22235
22940
|
process.exit(EXIT_USAGE);
|
|
22236
22941
|
}
|
|
@@ -22318,6 +23023,15 @@ async function parseCliArgs(argv) {
|
|
|
22318
23023
|
stdin: { type: "boolean", default: false },
|
|
22319
23024
|
"json-summary": { type: "boolean", default: false },
|
|
22320
23025
|
"list-format": { type: "string", default: "text" },
|
|
23026
|
+
"check-format": { type: "string", default: "text" },
|
|
23027
|
+
"no-fail": { type: "boolean", default: false },
|
|
23028
|
+
"require-tags": { type: "string" },
|
|
23029
|
+
"require-tickets": { type: "string" },
|
|
23030
|
+
"require-scenarios": { type: "string" },
|
|
23031
|
+
"no-regressions": { type: "boolean", default: false },
|
|
23032
|
+
"no-ratchet": { type: "boolean", default: false },
|
|
23033
|
+
"goal-format": { type: "string", default: "text" },
|
|
23034
|
+
"triage-format": { type: "string", default: "text" },
|
|
22321
23035
|
"emit-canonical": { type: "string" },
|
|
22322
23036
|
"slack-webhook": { type: "string" },
|
|
22323
23037
|
"teams-webhook": { type: "string" },
|
|
@@ -22388,7 +23102,7 @@ async function parseCliArgs(argv) {
|
|
|
22388
23102
|
}
|
|
22389
23103
|
const pluginConfig = await loadConfig(values["config"]);
|
|
22390
23104
|
const customFormatterNames = new Set(Object.keys(pluginConfig.formatters ?? {}));
|
|
22391
|
-
const builtInFormats = /* @__PURE__ */ new Set(["astro", "behavior-manifest-json", "confluence", "html", "markdown", "release-manifest", "junit", "cucumber-json", "cucumber-messages", "cucumber-html", "scenario-index-json", "story-report-json"]);
|
|
23105
|
+
const builtInFormats = /* @__PURE__ */ new Set(["astro", "behavior-manifest-json", "confluence", "html", "markdown", "release-manifest", "traceability-matrix", "junit", "cucumber-json", "cucumber-messages", "cucumber-html", "scenario-index-json", "story-report-json"]);
|
|
22392
23106
|
const formatStr = values.format;
|
|
22393
23107
|
const allRequestedFormats = formatStr.split(",").map((f) => f.trim());
|
|
22394
23108
|
const builtInRequested = allRequestedFormats.filter((f) => builtInFormats.has(f));
|
|
@@ -22396,7 +23110,7 @@ async function parseCliArgs(argv) {
|
|
|
22396
23110
|
const unknownFormats = allRequestedFormats.filter((f) => !builtInFormats.has(f) && !customFormatterNames.has(f));
|
|
22397
23111
|
if (unknownFormats.length > 0) {
|
|
22398
23112
|
const knownCustom = customFormatterNames.size > 0 ? `, ${[...customFormatterNames].join(", ")}` : "";
|
|
22399
|
-
console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, behavior-manifest-json, confluence, html, markdown, release-manifest, junit, cucumber-json, cucumber-messages, cucumber-html, scenario-index-json, story-report-json${knownCustom}.`);
|
|
23113
|
+
console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, behavior-manifest-json, confluence, html, markdown, release-manifest, traceability-matrix, junit, cucumber-json, cucumber-messages, cucumber-html, scenario-index-json, story-report-json${knownCustom}.`);
|
|
22400
23114
|
process.exit(EXIT_USAGE);
|
|
22401
23115
|
}
|
|
22402
23116
|
const formats = builtInRequested;
|
|
@@ -22484,10 +23198,14 @@ async function parseCliArgs(argv) {
|
|
|
22484
23198
|
console.error(`Error: --min-evidence must be "weak", "moderate", or "strong", got "${minEvidenceRaw}".`);
|
|
22485
23199
|
process.exit(EXIT_USAGE);
|
|
22486
23200
|
}
|
|
23201
|
+
const checkFormat = parseTextJsonFormat("--check-format", values["check-format"]);
|
|
23202
|
+
const goalFormat = parseTextJsonFormat("--goal-format", values["goal-format"]);
|
|
23203
|
+
const triageFormat = parseTextJsonFormat("--triage-format", values["triage-format"]);
|
|
22487
23204
|
const cliArgs = {
|
|
22488
23205
|
subcommand,
|
|
22489
23206
|
inputFile,
|
|
22490
23207
|
baselineFile,
|
|
23208
|
+
baselineArg: baselineValue,
|
|
22491
23209
|
currentFile,
|
|
22492
23210
|
baselineMode,
|
|
22493
23211
|
baselineDir: values["baseline-dir"],
|
|
@@ -22514,6 +23232,15 @@ async function parseCliArgs(argv) {
|
|
|
22514
23232
|
htmlThemePicker: values["html-theme-picker"],
|
|
22515
23233
|
jsonSummary: values["json-summary"],
|
|
22516
23234
|
listFormat: values["list-format"],
|
|
23235
|
+
checkFormat,
|
|
23236
|
+
noFail: values["no-fail"],
|
|
23237
|
+
requireTags: parseGlobs(values["require-tags"]),
|
|
23238
|
+
requireTickets: parseGlobs(values["require-tickets"]),
|
|
23239
|
+
requireScenarios: parseGlobs(values["require-scenarios"]),
|
|
23240
|
+
noRegressions: values["no-regressions"],
|
|
23241
|
+
noRatchet: values["no-ratchet"],
|
|
23242
|
+
goalFormat,
|
|
23243
|
+
triageFormat,
|
|
22517
23244
|
emitCanonical: values["emit-canonical"],
|
|
22518
23245
|
slackWebhook,
|
|
22519
23246
|
teamsWebhook,
|
|
@@ -22730,6 +23457,24 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
|
|
|
22730
23457
|
}
|
|
22731
23458
|
return picked.file;
|
|
22732
23459
|
}
|
|
23460
|
+
function resolveBaselineRun(args, currentRun) {
|
|
23461
|
+
if (!args.baselineArg) return void 0;
|
|
23462
|
+
let baselineFile;
|
|
23463
|
+
if (args.baselineArg === "auto") {
|
|
23464
|
+
if (!args.inputFile) {
|
|
23465
|
+
console.error("Error: --baseline auto requires a current input file (not --stdin).");
|
|
23466
|
+
process.exit(EXIT_USAGE);
|
|
23467
|
+
}
|
|
23468
|
+
baselineFile = resolveBaselineAuto(args.inputFile, currentRun, args);
|
|
23469
|
+
} else {
|
|
23470
|
+
baselineFile = args.baselineArg;
|
|
23471
|
+
}
|
|
23472
|
+
return applySelection(normalizeRunFromText(readFileInput(baselineFile), args).run, args);
|
|
23473
|
+
}
|
|
23474
|
+
function resolveBaselineStatusMap(args, currentRun) {
|
|
23475
|
+
const baselineRun = resolveBaselineRun(args, currentRun);
|
|
23476
|
+
return baselineRun ? new Map(baselineRun.testCases.map((tc) => [tc.id, tc.status])) : void 0;
|
|
23477
|
+
}
|
|
22733
23478
|
async function main() {
|
|
22734
23479
|
const { args, pluginConfig, customRequested } = await parseCliArgs(process.argv);
|
|
22735
23480
|
const startMs = Date.now();
|
|
@@ -22840,6 +23585,51 @@ async function main() {
|
|
|
22840
23585
|
console.log(output);
|
|
22841
23586
|
process.exit(EXIT_SUCCESS);
|
|
22842
23587
|
}
|
|
23588
|
+
if (args.subcommand === "check") {
|
|
23589
|
+
const text3 = await readInput(args);
|
|
23590
|
+
const run = applySelection(normalizeRunFromText(text3, args).run, args);
|
|
23591
|
+
const baseline = resolveBaselineStatusMap(args, run);
|
|
23592
|
+
const report = buildCheck(
|
|
23593
|
+
{ testCases: run.testCases, baseline, format: args.checkFormat },
|
|
23594
|
+
{}
|
|
23595
|
+
);
|
|
23596
|
+
console.log(renderCheck(report, args.checkFormat));
|
|
23597
|
+
if (report.summary.failed > 0 && !args.noFail) {
|
|
23598
|
+
process.exit(EXIT_AGENT_GATE);
|
|
23599
|
+
}
|
|
23600
|
+
process.exit(EXIT_SUCCESS);
|
|
23601
|
+
}
|
|
23602
|
+
if (args.subcommand === "goal") {
|
|
23603
|
+
const text3 = await readInput(args);
|
|
23604
|
+
const run = applySelection(normalizeRunFromText(text3, args).run, args);
|
|
23605
|
+
const baseline = resolveBaselineRun(args, run);
|
|
23606
|
+
const report = buildGoal(
|
|
23607
|
+
{
|
|
23608
|
+
run,
|
|
23609
|
+
baseline,
|
|
23610
|
+
requireTags: args.requireTags,
|
|
23611
|
+
requireTickets: args.requireTickets,
|
|
23612
|
+
requireScenarios: args.requireScenarios,
|
|
23613
|
+
enforceNoRegressions: args.noRegressions,
|
|
23614
|
+
enforceRatchet: !args.noRatchet,
|
|
23615
|
+
format: args.goalFormat
|
|
23616
|
+
},
|
|
23617
|
+
{}
|
|
23618
|
+
);
|
|
23619
|
+
console.log(renderGoal(report, args.goalFormat));
|
|
23620
|
+
process.exit(report.met ? EXIT_SUCCESS : EXIT_AGENT_GATE);
|
|
23621
|
+
}
|
|
23622
|
+
if (args.subcommand === "triage") {
|
|
23623
|
+
const text3 = await readInput(args);
|
|
23624
|
+
const run = applySelection(normalizeRunFromText(text3, args).run, args);
|
|
23625
|
+
const baseline = resolveBaselineStatusMap(args, run);
|
|
23626
|
+
const report = buildTriage(
|
|
23627
|
+
{ testCases: run.testCases, baseline, format: args.triageFormat },
|
|
23628
|
+
{}
|
|
23629
|
+
);
|
|
23630
|
+
console.log(renderTriage(report, args.triageFormat));
|
|
23631
|
+
process.exit(EXIT_SUCCESS);
|
|
23632
|
+
}
|
|
22843
23633
|
if (args.subcommand === "watch") {
|
|
22844
23634
|
if (!args.inputFile) {
|
|
22845
23635
|
console.error("Error: watch requires an input file (the raw-run JSON the framework writes).");
|
|
@@ -23958,6 +24748,15 @@ function createDefaultCliArgs() {
|
|
|
23958
24748
|
htmlThemePicker: false,
|
|
23959
24749
|
jsonSummary: false,
|
|
23960
24750
|
listFormat: "text",
|
|
24751
|
+
checkFormat: "text",
|
|
24752
|
+
noFail: false,
|
|
24753
|
+
requireTags: [],
|
|
24754
|
+
requireTickets: [],
|
|
24755
|
+
requireScenarios: [],
|
|
24756
|
+
noRegressions: false,
|
|
24757
|
+
noRatchet: false,
|
|
24758
|
+
goalFormat: "text",
|
|
24759
|
+
triageFormat: "text",
|
|
23961
24760
|
notify: "never",
|
|
23962
24761
|
maxFailedTests: 5,
|
|
23963
24762
|
maxHistoryRuns: 10,
|