executable-stories-formatters 1.9.2 → 1.10.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/dist/cli.js +2301 -567
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1559 -126
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +563 -1
- package/dist/index.d.ts +563 -1
- package/dist/index.js +1534 -123
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import * as
|
|
2
|
+
import * as path10 from "path";
|
|
3
3
|
import * as fsPromises from "fs/promises";
|
|
4
4
|
import { toStoryReportWithIndex } from "executable-stories-core/converters/story-report";
|
|
5
5
|
|
|
@@ -647,50 +647,50 @@ function scenarioLines(scenario) {
|
|
|
647
647
|
if (scenario.errorMessage) lines.push(indent(`error: ${scenario.errorMessage}`, " "));
|
|
648
648
|
return lines;
|
|
649
649
|
}
|
|
650
|
-
function docLines(entry,
|
|
651
|
-
const lines = ownDocLines(entry,
|
|
652
|
-
for (const child of entry.children ?? []) lines.push(...docLines(child,
|
|
650
|
+
function docLines(entry, pad2) {
|
|
651
|
+
const lines = ownDocLines(entry, pad2);
|
|
652
|
+
for (const child of entry.children ?? []) lines.push(...docLines(child, pad2 + " "));
|
|
653
653
|
return lines;
|
|
654
654
|
}
|
|
655
|
-
function ownDocLines(entry,
|
|
655
|
+
function ownDocLines(entry, pad2) {
|
|
656
656
|
switch (entry.kind) {
|
|
657
657
|
case "note":
|
|
658
|
-
return [indent(entry.text,
|
|
658
|
+
return [indent(entry.text, pad2)];
|
|
659
659
|
case "tag":
|
|
660
660
|
return [];
|
|
661
661
|
// already on the scenario's tag line
|
|
662
662
|
case "kv":
|
|
663
|
-
return [`${
|
|
663
|
+
return [`${pad2}${entry.label}: ${compact(entry.value)}`];
|
|
664
664
|
case "code":
|
|
665
|
-
return [`${
|
|
665
|
+
return [`${pad2}code ${entry.label}${entry.lang ? ` (${entry.lang})` : ""}:`, indent(entry.content, pad2 + " ")];
|
|
666
666
|
case "table":
|
|
667
667
|
return [
|
|
668
|
-
`${
|
|
669
|
-
...entry.rows.map((row) => `${
|
|
668
|
+
`${pad2}table ${entry.label}: ${entry.columns.join(" | ")}`,
|
|
669
|
+
...entry.rows.map((row) => `${pad2} ${row.join(" | ")}`)
|
|
670
670
|
];
|
|
671
671
|
case "link":
|
|
672
|
-
return [`${
|
|
672
|
+
return [`${pad2}link ${entry.label}: ${entry.url}`];
|
|
673
673
|
case "section":
|
|
674
|
-
return [`${
|
|
674
|
+
return [`${pad2}section ${entry.title}:`, indent(entry.markdown, pad2 + " ")];
|
|
675
675
|
case "mermaid":
|
|
676
|
-
return [`${
|
|
676
|
+
return [`${pad2}mermaid${entry.title ? ` ${entry.title}` : ""}:`, indent(entry.code, pad2 + " ")];
|
|
677
677
|
case "screenshot":
|
|
678
|
-
return [`${
|
|
678
|
+
return [`${pad2}screenshot ${entry.path}${entry.alt ? ` \u2014 ${entry.alt}` : ""}`];
|
|
679
679
|
case "video":
|
|
680
|
-
return [`${
|
|
680
|
+
return [`${pad2}video ${entry.path}${entry.caption ? ` \u2014 ${entry.caption}` : ""}`];
|
|
681
681
|
case "html":
|
|
682
|
-
return [`${
|
|
682
|
+
return [`${pad2}html ${entry.title ?? entry.path ?? entry.url ?? "(inline)"}`];
|
|
683
683
|
case "state":
|
|
684
|
-
return [`${
|
|
684
|
+
return [`${pad2}state${entry.label ? ` ${entry.label}` : ""}: ${compact(entry.value)}`];
|
|
685
685
|
case "custom":
|
|
686
|
-
return [`${
|
|
686
|
+
return [`${pad2}custom ${entry.type}: ${compact(entry.data)}`];
|
|
687
687
|
}
|
|
688
688
|
}
|
|
689
689
|
function compact(value) {
|
|
690
690
|
return typeof value === "string" ? value : JSON.stringify(value);
|
|
691
691
|
}
|
|
692
|
-
function indent(text2,
|
|
693
|
-
return text2.split("\n").map((line) =>
|
|
692
|
+
function indent(text2, pad2) {
|
|
693
|
+
return text2.split("\n").map((line) => pad2 + line).join("\n");
|
|
694
694
|
}
|
|
695
695
|
|
|
696
696
|
// src/formatters/junit-xml.ts
|
|
@@ -1593,9 +1593,9 @@ var MAX_FUZZ = 2;
|
|
|
1593
1593
|
var normalize = (line) => line.trim();
|
|
1594
1594
|
var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@ ?(.*)$/;
|
|
1595
1595
|
function stripPathPrefix(raw) {
|
|
1596
|
-
const
|
|
1597
|
-
if (
|
|
1598
|
-
return
|
|
1596
|
+
const path11 = raw.split(" ")[0].trim();
|
|
1597
|
+
if (path11 === "/dev/null") return void 0;
|
|
1598
|
+
return path11.replace(/^[ab]\//, "");
|
|
1599
1599
|
}
|
|
1600
1600
|
function parseUnifiedDiff(patch) {
|
|
1601
1601
|
const files = [];
|
|
@@ -1675,7 +1675,7 @@ function createAnchor(args) {
|
|
|
1675
1675
|
};
|
|
1676
1676
|
}
|
|
1677
1677
|
function changedRunCandidates(anchor, file, fileIndex) {
|
|
1678
|
-
const
|
|
1678
|
+
const path11 = file.newPath ?? file.oldPath ?? "";
|
|
1679
1679
|
const out = [];
|
|
1680
1680
|
file.hunks.forEach((hunk, hunkIndex) => {
|
|
1681
1681
|
outer: for (let i = 0; i + anchor.changed.length <= hunk.lines.length; i++) {
|
|
@@ -1686,7 +1686,7 @@ function changedRunCandidates(anchor, file, fileIndex) {
|
|
|
1686
1686
|
continue outer;
|
|
1687
1687
|
}
|
|
1688
1688
|
}
|
|
1689
|
-
out.push({ fileIndex, file:
|
|
1689
|
+
out.push({ fileIndex, file: path11, hunkIndex, lineIndex: i, lines: hunk.lines });
|
|
1690
1690
|
}
|
|
1691
1691
|
});
|
|
1692
1692
|
return out;
|
|
@@ -1783,18 +1783,18 @@ function deriveChangeType(tags) {
|
|
|
1783
1783
|
}
|
|
1784
1784
|
return "unknown";
|
|
1785
1785
|
}
|
|
1786
|
-
function extensionOf(
|
|
1787
|
-
const base =
|
|
1786
|
+
function extensionOf(path11) {
|
|
1787
|
+
const base = path11.split("/").pop() ?? path11;
|
|
1788
1788
|
const dot = base.lastIndexOf(".");
|
|
1789
1789
|
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
1790
1790
|
}
|
|
1791
|
-
function isTestFile(
|
|
1792
|
-
return TEST_INFIX.test(
|
|
1791
|
+
function isTestFile(path11) {
|
|
1792
|
+
return TEST_INFIX.test(path11);
|
|
1793
1793
|
}
|
|
1794
|
-
function isReviewableSource(
|
|
1795
|
-
if (isTestFile(
|
|
1796
|
-
if (
|
|
1797
|
-
return CODE_EXTENSIONS.has(extensionOf(
|
|
1794
|
+
function isReviewableSource(path11) {
|
|
1795
|
+
if (isTestFile(path11)) return false;
|
|
1796
|
+
if (path11.endsWith(".d.ts")) return false;
|
|
1797
|
+
return CODE_EXTENSIONS.has(extensionOf(path11));
|
|
1798
1798
|
}
|
|
1799
1799
|
function testBaseKey(testFile) {
|
|
1800
1800
|
return testFile.replace(TEST_INFIX, "");
|
|
@@ -1898,7 +1898,7 @@ function toClaim(testCase, changedSourcePaths) {
|
|
|
1898
1898
|
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
1899
1899
|
const key = testBaseKey(testCase.sourceFile);
|
|
1900
1900
|
const coversFiles = changedSourcePaths.filter(
|
|
1901
|
-
(
|
|
1901
|
+
(path11) => sourceBaseKey(path11) === key
|
|
1902
1902
|
);
|
|
1903
1903
|
return {
|
|
1904
1904
|
id: testCase.id,
|
|
@@ -2070,14 +2070,14 @@ var TraceabilityMatrixFormatter = class {
|
|
|
2070
2070
|
lines.push("");
|
|
2071
2071
|
lines.push(`Status: ${renderRequirementStatus(req.status)}`);
|
|
2072
2072
|
if (req.covers.length > 0) {
|
|
2073
|
-
lines.push(`Covers: ${req.covers.map((
|
|
2073
|
+
lines.push(`Covers: ${req.covers.map((path11) => `\`${path11}\``).join(", ")}`);
|
|
2074
2074
|
}
|
|
2075
2075
|
lines.push("");
|
|
2076
2076
|
lines.push("| Status | Scenario | Source | Covers |");
|
|
2077
2077
|
lines.push("| --- | --- | --- | --- |");
|
|
2078
2078
|
for (const scenario of req.scenarios) {
|
|
2079
2079
|
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
2080
|
-
const covers = scenario.covers.length > 0 ? scenario.covers.map((
|
|
2080
|
+
const covers = scenario.covers.length > 0 ? scenario.covers.map((path11) => `\`${path11}\``).join(", ") : "";
|
|
2081
2081
|
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
|
|
2082
2082
|
}
|
|
2083
2083
|
lines.push("");
|
|
@@ -2842,8 +2842,8 @@ function extractDocAttachments(step) {
|
|
|
2842
2842
|
}
|
|
2843
2843
|
return attachments;
|
|
2844
2844
|
}
|
|
2845
|
-
function guessMediaType(
|
|
2846
|
-
const lower =
|
|
2845
|
+
function guessMediaType(path11) {
|
|
2846
|
+
const lower = path11.toLowerCase();
|
|
2847
2847
|
if (lower.endsWith(".png")) return "image/png";
|
|
2848
2848
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
2849
2849
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -2984,11 +2984,11 @@ var CucumberHtmlFormatter = class {
|
|
|
2984
2984
|
for (const envelope of envelopes) {
|
|
2985
2985
|
const accepted = htmlStream.write(envelope);
|
|
2986
2986
|
if (!accepted) {
|
|
2987
|
-
await new Promise((
|
|
2987
|
+
await new Promise((resolve9) => htmlStream.once("drain", resolve9));
|
|
2988
2988
|
}
|
|
2989
2989
|
}
|
|
2990
|
-
await new Promise((
|
|
2991
|
-
collector.on("finish",
|
|
2990
|
+
await new Promise((resolve9, reject) => {
|
|
2991
|
+
collector.on("finish", resolve9);
|
|
2992
2992
|
collector.on("error", reject);
|
|
2993
2993
|
htmlStream.end();
|
|
2994
2994
|
});
|
|
@@ -5492,6 +5492,1395 @@ function convertAttachments(attachments) {
|
|
|
5492
5492
|
|
|
5493
5493
|
// src/index.ts
|
|
5494
5494
|
import { STORY_META_KEY } from "executable-stories-core/types/story";
|
|
5495
|
+
|
|
5496
|
+
// src/sync/engine.ts
|
|
5497
|
+
import { behaviourFingerprint as behaviourFingerprint2, behaviourSimilarity as behaviourSimilarity2 } from "executable-stories-core/converters/acl/ids";
|
|
5498
|
+
|
|
5499
|
+
// src/sync/lockfile.ts
|
|
5500
|
+
import * as fs4 from "fs";
|
|
5501
|
+
import * as path5 from "path";
|
|
5502
|
+
import { createHash as createHash5 } from "crypto";
|
|
5503
|
+
var DEFAULT_LOCKFILE_PATH = ".executable-stories/sync.lock.json";
|
|
5504
|
+
var LOCKFILE_VERSION = 1;
|
|
5505
|
+
function emptyLockfile() {
|
|
5506
|
+
return { version: LOCKFILE_VERSION, providers: {} };
|
|
5507
|
+
}
|
|
5508
|
+
function hashCaseBody(body) {
|
|
5509
|
+
const canonical = JSON.stringify({
|
|
5510
|
+
title: body.title.trim(),
|
|
5511
|
+
steps: body.steps.map((s) => `${s.keyword.toLowerCase()}:${s.text.trim()}`),
|
|
5512
|
+
description: body.description.trim()
|
|
5513
|
+
});
|
|
5514
|
+
return createHash5("sha1").update(canonical).digest("hex").slice(0, 16);
|
|
5515
|
+
}
|
|
5516
|
+
function parseLockfile(contents, label) {
|
|
5517
|
+
let parsed;
|
|
5518
|
+
try {
|
|
5519
|
+
parsed = JSON.parse(contents);
|
|
5520
|
+
} catch (err) {
|
|
5521
|
+
throw new Error(
|
|
5522
|
+
`Sync lockfile at ${label} is not valid JSON: ${err.message}
|
|
5523
|
+
Fix or delete it \u2014 deleting orphans every existing case binding, so prefer fixing.`
|
|
5524
|
+
);
|
|
5525
|
+
}
|
|
5526
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5527
|
+
throw new Error(`Sync lockfile at ${label} must contain an object.`);
|
|
5528
|
+
}
|
|
5529
|
+
const lock = parsed;
|
|
5530
|
+
if (lock.version !== LOCKFILE_VERSION) {
|
|
5531
|
+
throw new Error(
|
|
5532
|
+
`Sync lockfile at ${label} has version ${String(lock.version)}, expected ${LOCKFILE_VERSION}.`
|
|
5533
|
+
);
|
|
5534
|
+
}
|
|
5535
|
+
return { version: LOCKFILE_VERSION, providers: lock.providers ?? {} };
|
|
5536
|
+
}
|
|
5537
|
+
function serializeLockfile(lock) {
|
|
5538
|
+
const providers = {};
|
|
5539
|
+
for (const provider of Object.keys(lock.providers).sort()) {
|
|
5540
|
+
const entries = lock.providers[provider] ?? {};
|
|
5541
|
+
const sorted = {};
|
|
5542
|
+
for (const key of Object.keys(entries).sort()) sorted[key] = entries[key];
|
|
5543
|
+
providers[provider] = sorted;
|
|
5544
|
+
}
|
|
5545
|
+
return `${JSON.stringify({ version: LOCKFILE_VERSION, providers }, null, 2)}
|
|
5546
|
+
`;
|
|
5547
|
+
}
|
|
5548
|
+
function readLockfile(file) {
|
|
5549
|
+
if (!fs4.existsSync(file)) return emptyLockfile();
|
|
5550
|
+
return parseLockfile(fs4.readFileSync(file, "utf8"), file);
|
|
5551
|
+
}
|
|
5552
|
+
function writeLockfile(file, lock) {
|
|
5553
|
+
fs4.mkdirSync(path5.dirname(path5.resolve(file)), { recursive: true });
|
|
5554
|
+
fs4.writeFileSync(file, serializeLockfile(lock), "utf8");
|
|
5555
|
+
}
|
|
5556
|
+
function entriesFor(lock, provider) {
|
|
5557
|
+
return lock.providers[provider] ?? {};
|
|
5558
|
+
}
|
|
5559
|
+
function setEntry(lock, provider, fingerprint, entry) {
|
|
5560
|
+
lock.providers[provider] ??= {};
|
|
5561
|
+
lock.providers[provider][fingerprint] = entry;
|
|
5562
|
+
}
|
|
5563
|
+
|
|
5564
|
+
// src/sync/engine.ts
|
|
5565
|
+
var DEFAULT_DUPLICATE_THRESHOLD = 0.7;
|
|
5566
|
+
var PARTIAL_RUN_ORPHAN_RATIO = 0.25;
|
|
5567
|
+
function normalizeTitle(text2) {
|
|
5568
|
+
return text2.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
|
|
5569
|
+
}
|
|
5570
|
+
function renderDocs(docs, depth = 0) {
|
|
5571
|
+
if (!docs || docs.length === 0) return "";
|
|
5572
|
+
const lines = [];
|
|
5573
|
+
for (const doc of docs) {
|
|
5574
|
+
switch (doc.kind) {
|
|
5575
|
+
case "note":
|
|
5576
|
+
lines.push(doc.text);
|
|
5577
|
+
break;
|
|
5578
|
+
case "kv":
|
|
5579
|
+
lines.push(`**${doc.label}:** ${formatValue(doc.value)}`);
|
|
5580
|
+
break;
|
|
5581
|
+
case "state":
|
|
5582
|
+
lines.push(`**${doc.label ?? "State"}:** ${formatValue(doc.value)}`);
|
|
5583
|
+
break;
|
|
5584
|
+
case "code":
|
|
5585
|
+
lines.push(`**${doc.label}**`, "", "```" + (doc.lang ?? ""), doc.content, "```");
|
|
5586
|
+
break;
|
|
5587
|
+
case "table":
|
|
5588
|
+
lines.push(
|
|
5589
|
+
`**${doc.label}**`,
|
|
5590
|
+
"",
|
|
5591
|
+
`| ${doc.columns.join(" | ")} |`,
|
|
5592
|
+
`| ${doc.columns.map(() => "---").join(" | ")} |`,
|
|
5593
|
+
...doc.rows.map((row) => `| ${row.join(" | ")} |`)
|
|
5594
|
+
);
|
|
5595
|
+
break;
|
|
5596
|
+
case "link":
|
|
5597
|
+
lines.push(`[${doc.label}](${doc.url})`);
|
|
5598
|
+
break;
|
|
5599
|
+
case "section":
|
|
5600
|
+
lines.push(`${"#".repeat(Math.min(6, depth + 3))} ${doc.title}`, "", doc.markdown);
|
|
5601
|
+
break;
|
|
5602
|
+
case "mermaid":
|
|
5603
|
+
lines.push(...doc.title ? [`**${doc.title}**`, ""] : [], "```mermaid", doc.code, "```");
|
|
5604
|
+
break;
|
|
5605
|
+
case "screenshot":
|
|
5606
|
+
lines.push(`_Screenshot: ${doc.alt ?? doc.path}_`);
|
|
5607
|
+
break;
|
|
5608
|
+
case "video":
|
|
5609
|
+
lines.push(`_Video: ${doc.caption ?? doc.path}_`);
|
|
5610
|
+
break;
|
|
5611
|
+
case "html":
|
|
5612
|
+
lines.push(`_Embedded: ${doc.title ?? doc.url ?? doc.path ?? "html"}_`);
|
|
5613
|
+
break;
|
|
5614
|
+
case "custom":
|
|
5615
|
+
lines.push(`_${doc.type}_: ${formatValue(doc.data)}`);
|
|
5616
|
+
break;
|
|
5617
|
+
case "tag":
|
|
5618
|
+
break;
|
|
5619
|
+
}
|
|
5620
|
+
const children = renderDocs(doc.children, depth + 1);
|
|
5621
|
+
if (children) lines.push(children);
|
|
5622
|
+
}
|
|
5623
|
+
return lines.join("\n");
|
|
5624
|
+
}
|
|
5625
|
+
function formatValue(value) {
|
|
5626
|
+
if (typeof value === "string") return value;
|
|
5627
|
+
return JSON.stringify(value);
|
|
5628
|
+
}
|
|
5629
|
+
function scenarioUrl(config, tc) {
|
|
5630
|
+
if (!config.reportUrl) return void 0;
|
|
5631
|
+
const base = config.reportUrl.replace(/\/$/, "");
|
|
5632
|
+
const anchor = config.scenarioAnchor?.(tc);
|
|
5633
|
+
return anchor ? `${base}#${anchor}` : base;
|
|
5634
|
+
}
|
|
5635
|
+
function toCaseBody(tc, config) {
|
|
5636
|
+
const sections = [];
|
|
5637
|
+
const docs = renderDocs(tc.story.docs);
|
|
5638
|
+
if (docs) sections.push(docs);
|
|
5639
|
+
const tickets = tc.story.tickets ?? [];
|
|
5640
|
+
if (tickets.length > 0) {
|
|
5641
|
+
sections.push(
|
|
5642
|
+
`**Requirements:** ${tickets.map((t) => t.url ? `[${t.id}](${t.url})` : t.id).join(", ")}`
|
|
5643
|
+
);
|
|
5644
|
+
}
|
|
5645
|
+
if (tc.story.covers?.length) {
|
|
5646
|
+
sections.push(`**Covers:** ${tc.story.covers.join(", ")}`);
|
|
5647
|
+
}
|
|
5648
|
+
sections.push(`_Generated from ${tc.sourceFile}:${tc.sourceLine} by executable-stories. Edit the test, not this case._`);
|
|
5649
|
+
const links = [];
|
|
5650
|
+
const report = scenarioUrl(config, tc);
|
|
5651
|
+
if (report) links.push({ label: "Living documentation", url: report });
|
|
5652
|
+
for (const ticket of tickets) {
|
|
5653
|
+
if (ticket.url) links.push({ label: ticket.id, url: ticket.url });
|
|
5654
|
+
}
|
|
5655
|
+
return {
|
|
5656
|
+
title: tc.story.scenario,
|
|
5657
|
+
steps: tc.story.steps.map((s) => ({ keyword: s.keyword, text: s.text })),
|
|
5658
|
+
description: sections.join("\n\n"),
|
|
5659
|
+
links
|
|
5660
|
+
};
|
|
5661
|
+
}
|
|
5662
|
+
function projectBehaviours(run, config) {
|
|
5663
|
+
const fingerprints = run.testCases.map(
|
|
5664
|
+
(tc) => behaviourFingerprint2({
|
|
5665
|
+
scenario: tc.story.scenario,
|
|
5666
|
+
sourceFile: tc.sourceFile,
|
|
5667
|
+
steps: tc.story.steps.map((s) => ({ keyword: s.keyword, text: s.text })),
|
|
5668
|
+
covers: tc.story.covers
|
|
5669
|
+
})
|
|
5670
|
+
);
|
|
5671
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5672
|
+
for (const fp of fingerprints) {
|
|
5673
|
+
if (fp) counts.set(fp, (counts.get(fp) ?? 0) + 1);
|
|
5674
|
+
}
|
|
5675
|
+
return run.testCases.map((tc, index) => {
|
|
5676
|
+
const fp = fingerprints[index];
|
|
5677
|
+
const unique = fp !== "" && counts.get(fp) === 1;
|
|
5678
|
+
return {
|
|
5679
|
+
fingerprint: unique ? fp : tc.id,
|
|
5680
|
+
testCase: tc,
|
|
5681
|
+
body: toCaseBody(tc, config)
|
|
5682
|
+
};
|
|
5683
|
+
});
|
|
5684
|
+
}
|
|
5685
|
+
function roleFor(attachment) {
|
|
5686
|
+
const type = attachment.mediaType.toLowerCase();
|
|
5687
|
+
if (type.startsWith("image/")) return "screenshot";
|
|
5688
|
+
if (type.startsWith("video/")) return "video";
|
|
5689
|
+
if (attachment.name.toLowerCase().includes("trace")) return "trace";
|
|
5690
|
+
return "log";
|
|
5691
|
+
}
|
|
5692
|
+
function decode(attachment) {
|
|
5693
|
+
return attachment.contentEncoding === "BASE64" ? Uint8Array.from(Buffer.from(attachment.body, "base64")) : new TextEncoder().encode(attachment.body);
|
|
5694
|
+
}
|
|
5695
|
+
function collectAttachments(args) {
|
|
5696
|
+
const { testCase, policy, maxBytes } = args;
|
|
5697
|
+
if (policy === "none") return { attachments: [], oversized: [] };
|
|
5698
|
+
if (policy === "failed" && testCase.status !== "failed") return { attachments: [], oversized: [] };
|
|
5699
|
+
const attachments = [];
|
|
5700
|
+
const oversized = [];
|
|
5701
|
+
for (const raw of testCase.attachments) {
|
|
5702
|
+
const body = decode(raw);
|
|
5703
|
+
if (maxBytes !== void 0 && body.byteLength > maxBytes) {
|
|
5704
|
+
oversized.push({ filename: raw.name, bytes: body.byteLength, limit: maxBytes });
|
|
5705
|
+
continue;
|
|
5706
|
+
}
|
|
5707
|
+
attachments.push({
|
|
5708
|
+
filename: raw.name,
|
|
5709
|
+
mediaType: raw.mediaType,
|
|
5710
|
+
body,
|
|
5711
|
+
role: roleFor(raw)
|
|
5712
|
+
});
|
|
5713
|
+
}
|
|
5714
|
+
return { attachments, oversized };
|
|
5715
|
+
}
|
|
5716
|
+
function toCaseResult(args) {
|
|
5717
|
+
const { behaviour, caseId, provider, config } = args;
|
|
5718
|
+
const tc = behaviour.testCase;
|
|
5719
|
+
if (tc.status === "pending") return void 0;
|
|
5720
|
+
const { attachments, oversized } = collectAttachments({
|
|
5721
|
+
testCase: tc,
|
|
5722
|
+
policy: config.attach ?? "failed",
|
|
5723
|
+
maxBytes: provider.maxAttachmentBytes
|
|
5724
|
+
});
|
|
5725
|
+
return {
|
|
5726
|
+
result: {
|
|
5727
|
+
caseId,
|
|
5728
|
+
status: tc.status,
|
|
5729
|
+
durationMs: tc.durationMs,
|
|
5730
|
+
message: tc.errorMessage,
|
|
5731
|
+
url: scenarioUrl(config, tc),
|
|
5732
|
+
attachments: attachments.length > 0 ? attachments : void 0
|
|
5733
|
+
},
|
|
5734
|
+
oversized
|
|
5735
|
+
};
|
|
5736
|
+
}
|
|
5737
|
+
function ticketBinding(tc, config) {
|
|
5738
|
+
const prefix = config.ticketPrefix;
|
|
5739
|
+
if (!prefix) return void 0;
|
|
5740
|
+
const match = (tc.story.tickets ?? []).find((t) => t.id.startsWith(prefix));
|
|
5741
|
+
if (!match) return void 0;
|
|
5742
|
+
return config.ticketPrefixStrip === false ? match.id : match.id.slice(prefix.length);
|
|
5743
|
+
}
|
|
5744
|
+
async function analyzeSync(args) {
|
|
5745
|
+
const { run, provider, lockfile, config } = args;
|
|
5746
|
+
const local = projectBehaviours(run, config);
|
|
5747
|
+
const remoteCases = await provider.listCases();
|
|
5748
|
+
const remoteById = new Map(remoteCases.map((c) => [c.id, c]));
|
|
5749
|
+
const locked = entriesFor(lockfile, provider.name);
|
|
5750
|
+
const create = [];
|
|
5751
|
+
const update = [];
|
|
5752
|
+
const unchanged = [];
|
|
5753
|
+
const adopted = [];
|
|
5754
|
+
const skipped = [];
|
|
5755
|
+
const results = [];
|
|
5756
|
+
const oversized = [];
|
|
5757
|
+
const boundCaseIds = /* @__PURE__ */ new Set();
|
|
5758
|
+
let driftUncheckable = 0;
|
|
5759
|
+
for (const behaviour of local) {
|
|
5760
|
+
const entry = locked[behaviour.fingerprint];
|
|
5761
|
+
const caseId = entry?.caseId ?? ticketBinding(behaviour.testCase, config);
|
|
5762
|
+
const remote2 = caseId ? remoteById.get(caseId) : void 0;
|
|
5763
|
+
if (!caseId) {
|
|
5764
|
+
create.push({
|
|
5765
|
+
fingerprint: behaviour.fingerprint,
|
|
5766
|
+
scenario: behaviour.body.title,
|
|
5767
|
+
body: behaviour.body
|
|
5768
|
+
});
|
|
5769
|
+
continue;
|
|
5770
|
+
}
|
|
5771
|
+
if (!remote2) {
|
|
5772
|
+
skipped.push({
|
|
5773
|
+
fingerprint: behaviour.fingerprint,
|
|
5774
|
+
caseId,
|
|
5775
|
+
url: entry?.url ?? "",
|
|
5776
|
+
title: entry?.title ?? behaviour.body.title,
|
|
5777
|
+
reason: "case-missing"
|
|
5778
|
+
});
|
|
5779
|
+
continue;
|
|
5780
|
+
}
|
|
5781
|
+
boundCaseIds.add(caseId);
|
|
5782
|
+
const pending = toCaseResult({ behaviour, caseId, provider, config });
|
|
5783
|
+
if (pending) {
|
|
5784
|
+
results.push(pending.result);
|
|
5785
|
+
oversized.push(...pending.oversized);
|
|
5786
|
+
}
|
|
5787
|
+
const planned = {
|
|
5788
|
+
fingerprint: behaviour.fingerprint,
|
|
5789
|
+
caseId,
|
|
5790
|
+
url: remote2.url,
|
|
5791
|
+
scenario: behaviour.body.title,
|
|
5792
|
+
body: behaviour.body
|
|
5793
|
+
};
|
|
5794
|
+
if (!entry?.owned) {
|
|
5795
|
+
adopted.push(planned);
|
|
5796
|
+
continue;
|
|
5797
|
+
}
|
|
5798
|
+
const remoteHash = remote2.body ? hashCaseBody(remote2.body) : void 0;
|
|
5799
|
+
if (remoteHash !== void 0 && remoteHash !== entry.hash) {
|
|
5800
|
+
skipped.push({
|
|
5801
|
+
fingerprint: behaviour.fingerprint,
|
|
5802
|
+
caseId,
|
|
5803
|
+
url: remote2.url,
|
|
5804
|
+
title: remote2.title,
|
|
5805
|
+
reason: "remote-edited"
|
|
5806
|
+
});
|
|
5807
|
+
continue;
|
|
5808
|
+
}
|
|
5809
|
+
const baseline = remoteHash ?? entry.hash;
|
|
5810
|
+
if (baseline !== "" && hashCaseBody(behaviour.body) === baseline) {
|
|
5811
|
+
unchanged.push(planned);
|
|
5812
|
+
} else {
|
|
5813
|
+
update.push(planned);
|
|
5814
|
+
}
|
|
5815
|
+
if (remoteHash === void 0) driftUncheckable += 1;
|
|
5816
|
+
}
|
|
5817
|
+
const byNormalizedTitle = /* @__PURE__ */ new Map();
|
|
5818
|
+
for (const behaviour of local) byNormalizedTitle.set(normalizeTitle(behaviour.body.title), behaviour);
|
|
5819
|
+
const threshold = config.duplicateThreshold ?? DEFAULT_DUPLICATE_THRESHOLD;
|
|
5820
|
+
const remote = remoteCases.map((remoteCase) => {
|
|
5821
|
+
if (boundCaseIds.has(remoteCase.id)) {
|
|
5822
|
+
return { case: remoteCase, classification: "automated" };
|
|
5823
|
+
}
|
|
5824
|
+
const titleMatch = byNormalizedTitle.get(normalizeTitle(remoteCase.title));
|
|
5825
|
+
if (titleMatch) {
|
|
5826
|
+
return {
|
|
5827
|
+
case: remoteCase,
|
|
5828
|
+
classification: "duplicated",
|
|
5829
|
+
resembles: titleMatch.body.title
|
|
5830
|
+
};
|
|
5831
|
+
}
|
|
5832
|
+
if (remoteCase.body && remoteCase.body.steps.length > 0) {
|
|
5833
|
+
let best;
|
|
5834
|
+
for (const behaviour of local) {
|
|
5835
|
+
const score = behaviourSimilarity2(
|
|
5836
|
+
{
|
|
5837
|
+
scenario: remoteCase.title,
|
|
5838
|
+
sourceFile: "",
|
|
5839
|
+
steps: remoteCase.body.steps
|
|
5840
|
+
},
|
|
5841
|
+
{
|
|
5842
|
+
scenario: behaviour.body.title,
|
|
5843
|
+
sourceFile: behaviour.testCase.sourceFile,
|
|
5844
|
+
steps: behaviour.body.steps
|
|
5845
|
+
}
|
|
5846
|
+
);
|
|
5847
|
+
if (!best || score > best.score) best = { behaviour, score };
|
|
5848
|
+
}
|
|
5849
|
+
if (best && best.score >= threshold) {
|
|
5850
|
+
return {
|
|
5851
|
+
case: remoteCase,
|
|
5852
|
+
classification: "possible-duplicate",
|
|
5853
|
+
resembles: best.behaviour.body.title,
|
|
5854
|
+
similarity: Number(best.score.toFixed(2))
|
|
5855
|
+
};
|
|
5856
|
+
}
|
|
5857
|
+
}
|
|
5858
|
+
return { case: remoteCase, classification: "manual-only" };
|
|
5859
|
+
});
|
|
5860
|
+
const localFingerprints = new Set(local.map((b) => b.fingerprint));
|
|
5861
|
+
const orphaned = Object.entries(locked).filter(([fingerprint]) => !localFingerprints.has(fingerprint)).map(([fingerprint, entry]) => ({
|
|
5862
|
+
fingerprint,
|
|
5863
|
+
caseId: entry.caseId,
|
|
5864
|
+
url: entry.url,
|
|
5865
|
+
title: entry.title
|
|
5866
|
+
}));
|
|
5867
|
+
const lockedCount = Object.keys(locked).length;
|
|
5868
|
+
const partialRunWarning = lockedCount > 0 && orphaned.length / lockedCount > PARTIAL_RUN_ORPHAN_RATIO ? `${orphaned.length} of ${lockedCount} bindings have no matching story. If this run was filtered (a -t/--grep flag, a single file), those are not deletions. Nothing is removed either way.` : void 0;
|
|
5869
|
+
const unsupported = [];
|
|
5870
|
+
if (create.length > 0 && !provider.createCase) unsupported.push("createCase");
|
|
5871
|
+
if (update.length > 0 && !provider.updateCase) unsupported.push("updateCase");
|
|
5872
|
+
if (results.length > 0 && !provider.recordResults) unsupported.push("recordResults");
|
|
5873
|
+
const byRole = {};
|
|
5874
|
+
let files = 0;
|
|
5875
|
+
let bytes = 0;
|
|
5876
|
+
for (const result of results) {
|
|
5877
|
+
for (const attachment of result.attachments ?? []) {
|
|
5878
|
+
files += 1;
|
|
5879
|
+
bytes += attachment.body.byteLength;
|
|
5880
|
+
const role = attachment.role ?? "log";
|
|
5881
|
+
byRole[role] = (byRole[role] ?? 0) + 1;
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
5884
|
+
return {
|
|
5885
|
+
provider: provider.name,
|
|
5886
|
+
target: provider.describeTarget?.(),
|
|
5887
|
+
local,
|
|
5888
|
+
remote,
|
|
5889
|
+
create,
|
|
5890
|
+
update,
|
|
5891
|
+
unchanged,
|
|
5892
|
+
adopted,
|
|
5893
|
+
skipped,
|
|
5894
|
+
orphaned,
|
|
5895
|
+
results,
|
|
5896
|
+
attachments: { files, bytes, oversized, byRole },
|
|
5897
|
+
unsupported,
|
|
5898
|
+
driftUncheckable,
|
|
5899
|
+
partialRunWarning
|
|
5900
|
+
};
|
|
5901
|
+
}
|
|
5902
|
+
async function applySync(args, deps) {
|
|
5903
|
+
const { analysis, provider, lockfile, config } = args;
|
|
5904
|
+
const result = {
|
|
5905
|
+
created: [],
|
|
5906
|
+
updated: [],
|
|
5907
|
+
resultsRecorded: 0,
|
|
5908
|
+
resultsSkipped: [],
|
|
5909
|
+
attachmentsUploaded: 0,
|
|
5910
|
+
errors: []
|
|
5911
|
+
};
|
|
5912
|
+
const byFingerprint = new Map(analysis.local.map((b) => [b.fingerprint, b]));
|
|
5913
|
+
const results = [...analysis.results];
|
|
5914
|
+
if (analysis.create.length > 0 && provider.createCase) {
|
|
5915
|
+
for (const planned of analysis.create) {
|
|
5916
|
+
try {
|
|
5917
|
+
const created = await provider.createCase(planned.body);
|
|
5918
|
+
setEntry(lockfile, provider.name, planned.fingerprint, {
|
|
5919
|
+
caseId: created.id,
|
|
5920
|
+
url: created.url,
|
|
5921
|
+
hash: hashCaseBody(created.body ?? planned.body),
|
|
5922
|
+
title: created.title,
|
|
5923
|
+
owned: true
|
|
5924
|
+
});
|
|
5925
|
+
result.created.push({ scenario: planned.scenario, caseId: created.id, url: created.url });
|
|
5926
|
+
const behaviour = byFingerprint.get(planned.fingerprint);
|
|
5927
|
+
if (behaviour) {
|
|
5928
|
+
const pending = toCaseResult({ behaviour, caseId: created.id, provider, config });
|
|
5929
|
+
if (pending) results.push(pending.result);
|
|
5930
|
+
}
|
|
5931
|
+
} catch (err) {
|
|
5932
|
+
result.errors.push(`create "${planned.scenario}": ${err.message}`);
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
5935
|
+
}
|
|
5936
|
+
if (analysis.update.length > 0 && provider.updateCase) {
|
|
5937
|
+
for (const planned of analysis.update) {
|
|
5938
|
+
try {
|
|
5939
|
+
const updated = await provider.updateCase(planned.caseId, planned.body);
|
|
5940
|
+
setEntry(lockfile, provider.name, planned.fingerprint, {
|
|
5941
|
+
caseId: updated.id,
|
|
5942
|
+
url: updated.url,
|
|
5943
|
+
hash: hashCaseBody(updated.body ?? planned.body),
|
|
5944
|
+
title: updated.title,
|
|
5945
|
+
owned: true
|
|
5946
|
+
});
|
|
5947
|
+
result.updated.push({ scenario: planned.scenario, caseId: updated.id, url: updated.url });
|
|
5948
|
+
} catch (err) {
|
|
5949
|
+
result.errors.push(`update "${planned.scenario}" (${planned.caseId}): ${err.message}`);
|
|
5950
|
+
}
|
|
5951
|
+
}
|
|
5952
|
+
}
|
|
5953
|
+
for (const planned of analysis.unchanged) {
|
|
5954
|
+
const existing = entriesFor(lockfile, provider.name)[planned.fingerprint];
|
|
5955
|
+
if (existing) {
|
|
5956
|
+
setEntry(lockfile, provider.name, planned.fingerprint, { ...existing, url: planned.url });
|
|
5957
|
+
}
|
|
5958
|
+
}
|
|
5959
|
+
for (const planned of analysis.adopted) {
|
|
5960
|
+
setEntry(lockfile, provider.name, planned.fingerprint, {
|
|
5961
|
+
caseId: planned.caseId,
|
|
5962
|
+
url: planned.url,
|
|
5963
|
+
hash: "",
|
|
5964
|
+
title: planned.scenario,
|
|
5965
|
+
owned: false
|
|
5966
|
+
});
|
|
5967
|
+
}
|
|
5968
|
+
if (results.length > 0 && provider.recordResults) {
|
|
5969
|
+
try {
|
|
5970
|
+
const summary = await provider.recordResults(results);
|
|
5971
|
+
result.resultsRecorded = summary.recorded;
|
|
5972
|
+
result.resultsSkipped = summary.skipped;
|
|
5973
|
+
result.attachmentsUploaded = summary.attachmentsUploaded;
|
|
5974
|
+
result.runUrl = summary.runUrl;
|
|
5975
|
+
} catch (err) {
|
|
5976
|
+
result.errors.push(`record results: ${err.message}`);
|
|
5977
|
+
}
|
|
5978
|
+
} else if (results.length > 0) {
|
|
5979
|
+
deps.logger.warn(
|
|
5980
|
+
`${provider.name} does not support recording results \u2014 ${results.length} execution(s) not pushed.`
|
|
5981
|
+
);
|
|
5982
|
+
}
|
|
5983
|
+
return result;
|
|
5984
|
+
}
|
|
5985
|
+
|
|
5986
|
+
// src/sync/report.ts
|
|
5987
|
+
var MANUAL_ONLY_LIMIT = 50;
|
|
5988
|
+
function summarize2(analysis) {
|
|
5989
|
+
const count2 = (kind) => analysis.remote.filter((c) => c.classification === kind).length;
|
|
5990
|
+
return {
|
|
5991
|
+
provider: analysis.provider,
|
|
5992
|
+
target: analysis.target,
|
|
5993
|
+
totalCases: analysis.remote.length,
|
|
5994
|
+
automated: count2("automated"),
|
|
5995
|
+
duplicated: count2("duplicated"),
|
|
5996
|
+
possibleDuplicate: count2("possible-duplicate"),
|
|
5997
|
+
manualOnly: count2("manual-only"),
|
|
5998
|
+
untracked: analysis.create.length,
|
|
5999
|
+
adopted: analysis.adopted.length
|
|
6000
|
+
};
|
|
6001
|
+
}
|
|
6002
|
+
function sectionBreakdown(analysis) {
|
|
6003
|
+
const sections = /* @__PURE__ */ new Map();
|
|
6004
|
+
for (const entry of analysis.remote) {
|
|
6005
|
+
const name = entry.case.section ?? "(no section)";
|
|
6006
|
+
const bucket2 = sections.get(name) ?? { total: 0, automated: 0 };
|
|
6007
|
+
bucket2.total += 1;
|
|
6008
|
+
if (entry.classification === "automated") bucket2.automated += 1;
|
|
6009
|
+
sections.set(name, bucket2);
|
|
6010
|
+
}
|
|
6011
|
+
return [...sections.entries()].map(([name, value]) => ({ name, ...value })).sort((a, b) => b.automated - a.automated || b.total - a.total);
|
|
6012
|
+
}
|
|
6013
|
+
function renderCoverageText(analysis) {
|
|
6014
|
+
const summary = summarize2(analysis);
|
|
6015
|
+
const lines = [];
|
|
6016
|
+
lines.push(`${analysis.provider}: ${analysis.target ?? "(target not described)"} (${summary.totalCases} cases)`);
|
|
6017
|
+
lines.push("");
|
|
6018
|
+
lines.push(` ${pad(summary.automated)} automated already covered by a story`);
|
|
6019
|
+
lines.push(` ${pad(summary.duplicated)} duplicated manual case duplicates an automated story`);
|
|
6020
|
+
if (summary.possibleDuplicate > 0) {
|
|
6021
|
+
lines.push(` ${pad(summary.possibleDuplicate)} possible dupe similar to a story, needs a human to confirm`);
|
|
6022
|
+
}
|
|
6023
|
+
lines.push(` ${pad(summary.manualOnly)} manual only no automated equivalent`);
|
|
6024
|
+
lines.push(` ${pad(summary.untracked)} untracked story with no case`);
|
|
6025
|
+
if (summary.adopted > 0) {
|
|
6026
|
+
lines.push(` ${pad(summary.adopted)} linked story bound to a hand-authored case`);
|
|
6027
|
+
}
|
|
6028
|
+
const sections = sectionBreakdown(analysis).filter((s) => s.automated > 0);
|
|
6029
|
+
if (sections.length > 0) {
|
|
6030
|
+
const top = sections[0];
|
|
6031
|
+
lines.push("");
|
|
6032
|
+
lines.push(`Biggest overlap: "${top.name}" section, ${top.automated} of ${top.total} automated.`);
|
|
6033
|
+
}
|
|
6034
|
+
if (analysis.orphaned.length > 0) {
|
|
6035
|
+
lines.push("");
|
|
6036
|
+
lines.push(`${analysis.orphaned.length} case(s) bound to a story that no longer exists. Nothing was removed.`);
|
|
6037
|
+
}
|
|
6038
|
+
if (analysis.partialRunWarning) {
|
|
6039
|
+
lines.push("");
|
|
6040
|
+
lines.push(`Note: ${analysis.partialRunWarning}`);
|
|
6041
|
+
}
|
|
6042
|
+
return lines.join("\n");
|
|
6043
|
+
}
|
|
6044
|
+
function pad(value) {
|
|
6045
|
+
return String(value).padStart(4, " ");
|
|
6046
|
+
}
|
|
6047
|
+
function renderCoverageMarkdown(analysis) {
|
|
6048
|
+
const summary = summarize2(analysis);
|
|
6049
|
+
const lines = [];
|
|
6050
|
+
lines.push(`# Test coverage vs ${analysis.provider}`);
|
|
6051
|
+
lines.push("");
|
|
6052
|
+
if (analysis.target) lines.push(`**Target:** ${analysis.target}`, "");
|
|
6053
|
+
lines.push("| Cases | Count | Meaning |");
|
|
6054
|
+
lines.push("| --- | ---: | --- |");
|
|
6055
|
+
lines.push(`| Automated | ${summary.automated} | Already covered by a story |`);
|
|
6056
|
+
lines.push(`| Duplicated | ${summary.duplicated} | Manual case duplicates an automated story |`);
|
|
6057
|
+
lines.push(`| Possible duplicate | ${summary.possibleDuplicate} | Similar to a story, needs review |`);
|
|
6058
|
+
lines.push(`| Manual only | ${summary.manualOnly} | No automated equivalent |`);
|
|
6059
|
+
lines.push(`| Untracked stories | ${summary.untracked} | Story with no case |`);
|
|
6060
|
+
lines.push("");
|
|
6061
|
+
const duplicates = analysis.remote.filter(
|
|
6062
|
+
(c) => c.classification === "duplicated" || c.classification === "possible-duplicate"
|
|
6063
|
+
);
|
|
6064
|
+
if (duplicates.length > 0) {
|
|
6065
|
+
lines.push("## Retire these first");
|
|
6066
|
+
lines.push("");
|
|
6067
|
+
lines.push("Manual cases an automated story already covers.");
|
|
6068
|
+
lines.push("");
|
|
6069
|
+
lines.push("| Case | Title | Covered by | Confidence |");
|
|
6070
|
+
lines.push("| --- | --- | --- | --- |");
|
|
6071
|
+
for (const entry of duplicates) {
|
|
6072
|
+
const confidence = entry.classification === "duplicated" ? "exact title" : `similarity ${entry.similarity}`;
|
|
6073
|
+
lines.push(
|
|
6074
|
+
`| [${entry.case.id}](${entry.case.url}) | ${escapeCell2(entry.case.title)} | ${escapeCell2(entry.resembles ?? "")} | ${confidence} |`
|
|
6075
|
+
);
|
|
6076
|
+
}
|
|
6077
|
+
lines.push("");
|
|
6078
|
+
}
|
|
6079
|
+
const manualOnly = analysis.remote.filter((c) => c.classification === "manual-only");
|
|
6080
|
+
if (manualOnly.length > 0) {
|
|
6081
|
+
lines.push("## Not automated yet");
|
|
6082
|
+
lines.push("");
|
|
6083
|
+
lines.push("Cases with no automated equivalent. This is the backlog.");
|
|
6084
|
+
lines.push("");
|
|
6085
|
+
lines.push("| Case | Title | Section |");
|
|
6086
|
+
lines.push("| --- | --- | --- |");
|
|
6087
|
+
for (const entry of manualOnly.slice(0, MANUAL_ONLY_LIMIT)) {
|
|
6088
|
+
lines.push(
|
|
6089
|
+
`| [${entry.case.id}](${entry.case.url}) | ${escapeCell2(entry.case.title)} | ${escapeCell2(entry.case.section ?? "")} |`
|
|
6090
|
+
);
|
|
6091
|
+
}
|
|
6092
|
+
if (manualOnly.length > MANUAL_ONLY_LIMIT) {
|
|
6093
|
+
lines.push("");
|
|
6094
|
+
lines.push(
|
|
6095
|
+
`_${manualOnly.length - MANUAL_ONLY_LIMIT} more not listed here. The JSON artifact has all ${manualOnly.length}._`
|
|
6096
|
+
);
|
|
6097
|
+
}
|
|
6098
|
+
lines.push("");
|
|
6099
|
+
}
|
|
6100
|
+
if (analysis.create.length > 0) {
|
|
6101
|
+
lines.push("## Stories with no case");
|
|
6102
|
+
lines.push("");
|
|
6103
|
+
for (const planned of analysis.create) lines.push(`- ${planned.scenario}`);
|
|
6104
|
+
lines.push("");
|
|
6105
|
+
}
|
|
6106
|
+
if (analysis.orphaned.length > 0) {
|
|
6107
|
+
lines.push("## Cases with no story");
|
|
6108
|
+
lines.push("");
|
|
6109
|
+
lines.push("Bound to a story that has since been deleted. Nothing was removed automatically.");
|
|
6110
|
+
lines.push("");
|
|
6111
|
+
for (const orphan of analysis.orphaned) {
|
|
6112
|
+
lines.push(`- [${orphan.caseId}](${orphan.url}) ${escapeCell2(orphan.title)}`);
|
|
6113
|
+
}
|
|
6114
|
+
lines.push("");
|
|
6115
|
+
}
|
|
6116
|
+
const sections = sectionBreakdown(analysis);
|
|
6117
|
+
if (sections.length > 1) {
|
|
6118
|
+
lines.push("## By section");
|
|
6119
|
+
lines.push("");
|
|
6120
|
+
lines.push("| Section | Automated | Total |");
|
|
6121
|
+
lines.push("| --- | ---: | ---: |");
|
|
6122
|
+
for (const section of sections) {
|
|
6123
|
+
lines.push(`| ${escapeCell2(section.name)} | ${section.automated} | ${section.total} |`);
|
|
6124
|
+
}
|
|
6125
|
+
lines.push("");
|
|
6126
|
+
}
|
|
6127
|
+
if (analysis.partialRunWarning) {
|
|
6128
|
+
lines.push(`> ${analysis.partialRunWarning}`, "");
|
|
6129
|
+
}
|
|
6130
|
+
return lines.join("\n");
|
|
6131
|
+
}
|
|
6132
|
+
function escapeCell2(text2) {
|
|
6133
|
+
return text2.replace(/\|/g, "\\|");
|
|
6134
|
+
}
|
|
6135
|
+
function buildCoverageJson(analysis) {
|
|
6136
|
+
return {
|
|
6137
|
+
schema: "executable-stories/sync-coverage/v1",
|
|
6138
|
+
...summarize2(analysis),
|
|
6139
|
+
cases: analysis.remote.map((entry) => ({
|
|
6140
|
+
id: entry.case.id,
|
|
6141
|
+
url: entry.case.url,
|
|
6142
|
+
title: entry.case.title,
|
|
6143
|
+
section: entry.case.section,
|
|
6144
|
+
classification: entry.classification,
|
|
6145
|
+
resembles: entry.resembles,
|
|
6146
|
+
similarity: entry.similarity
|
|
6147
|
+
})),
|
|
6148
|
+
untrackedScenarios: analysis.create.map((c) => c.scenario),
|
|
6149
|
+
orphaned: analysis.orphaned.map((o) => ({ caseId: o.caseId, url: o.url, title: o.title })),
|
|
6150
|
+
sections: sectionBreakdown(analysis)
|
|
6151
|
+
};
|
|
6152
|
+
}
|
|
6153
|
+
function formatBytes(bytes) {
|
|
6154
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
6155
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
6156
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
6157
|
+
}
|
|
6158
|
+
function renderPlan(analysis, opts) {
|
|
6159
|
+
const lines = [];
|
|
6160
|
+
lines.push(`${analysis.provider}: ${analysis.target ?? "(target not described)"}`);
|
|
6161
|
+
lines.push("");
|
|
6162
|
+
lines.push(` + create ${pad(analysis.create.length)} cases`);
|
|
6163
|
+
lines.push(` ~ update ${pad(analysis.update.length)} cases`);
|
|
6164
|
+
lines.push(` = unchanged ${pad(analysis.unchanged.length)} cases`);
|
|
6165
|
+
if (analysis.adopted.length > 0) {
|
|
6166
|
+
lines.push(` \xB7 linked ${pad(analysis.adopted.length)} cases (hand-authored, results only)`);
|
|
6167
|
+
}
|
|
6168
|
+
if (analysis.skipped.length > 0) {
|
|
6169
|
+
const edited = analysis.skipped.filter((s) => s.reason === "remote-edited").length;
|
|
6170
|
+
const missing = analysis.skipped.filter((s) => s.reason === "case-missing").length;
|
|
6171
|
+
if (edited > 0) {
|
|
6172
|
+
lines.push(` ! skipped ${pad(edited)} cases (edited in ${analysis.provider} since last sync)`);
|
|
6173
|
+
}
|
|
6174
|
+
if (missing > 0) {
|
|
6175
|
+
lines.push(` ! skipped ${pad(missing)} cases (bound case no longer exists)`);
|
|
6176
|
+
}
|
|
6177
|
+
}
|
|
6178
|
+
if (analysis.orphaned.length > 0) {
|
|
6179
|
+
lines.push(` ? orphaned ${pad(analysis.orphaned.length)} cases (story deleted from codebase, never removed)`);
|
|
6180
|
+
}
|
|
6181
|
+
lines.push(` \u2192 results ${pad(analysis.results.length)} executions`);
|
|
6182
|
+
if (analysis.attachments.files > 0) {
|
|
6183
|
+
const roles = Object.entries(analysis.attachments.byRole).map(([role, count2]) => `${count2} ${role}${count2 === 1 ? "" : "s"}`).join(", ");
|
|
6184
|
+
lines.push(
|
|
6185
|
+
` \u2191 upload ${pad(analysis.attachments.files)} attachments (${roles}, ${formatBytes(analysis.attachments.bytes)})`
|
|
6186
|
+
);
|
|
6187
|
+
}
|
|
6188
|
+
for (const oversized of analysis.attachments.oversized) {
|
|
6189
|
+
lines.push(
|
|
6190
|
+
` ! oversized ${oversized.filename} (${formatBytes(oversized.bytes)}, provider limit ${formatBytes(oversized.limit)})`
|
|
6191
|
+
);
|
|
6192
|
+
}
|
|
6193
|
+
for (const capability of analysis.unsupported) {
|
|
6194
|
+
lines.push(` ! ${analysis.provider} does not support ${capability} \u2014 those changes are not applied`);
|
|
6195
|
+
}
|
|
6196
|
+
if (analysis.driftUncheckable > 0) {
|
|
6197
|
+
lines.push(
|
|
6198
|
+
` ! ${analysis.driftUncheckable} case(s): ${analysis.provider} returned no body, so a hand edit to them cannot be detected`
|
|
6199
|
+
);
|
|
6200
|
+
}
|
|
6201
|
+
if (analysis.partialRunWarning) {
|
|
6202
|
+
lines.push("");
|
|
6203
|
+
lines.push(`Note: ${analysis.partialRunWarning}`);
|
|
6204
|
+
}
|
|
6205
|
+
if (analysis.skipped.some((s) => s.reason === "remote-edited")) {
|
|
6206
|
+
lines.push("");
|
|
6207
|
+
lines.push("Skipped cases were edited by hand after we last wrote them. Nothing overwrites them.");
|
|
6208
|
+
for (const skip of analysis.skipped.filter((s) => s.reason === "remote-edited")) {
|
|
6209
|
+
lines.push(` ${skip.caseId} ${skip.title} ${skip.url}`);
|
|
6210
|
+
}
|
|
6211
|
+
}
|
|
6212
|
+
if (opts.dryRun && hasWork(analysis)) {
|
|
6213
|
+
lines.push("");
|
|
6214
|
+
lines.push("Nothing was written. Run the same command with --apply to make these changes.");
|
|
6215
|
+
}
|
|
6216
|
+
return lines.join("\n");
|
|
6217
|
+
}
|
|
6218
|
+
function hasWork(analysis) {
|
|
6219
|
+
return analysis.create.length > 0 || analysis.update.length > 0 || analysis.results.length > 0;
|
|
6220
|
+
}
|
|
6221
|
+
function renderApplyResult(result) {
|
|
6222
|
+
const lines = [];
|
|
6223
|
+
for (const created of result.created) {
|
|
6224
|
+
lines.push(` + ${created.caseId} ${created.scenario} ${created.url}`);
|
|
6225
|
+
}
|
|
6226
|
+
for (const updated of result.updated) {
|
|
6227
|
+
lines.push(` ~ ${updated.caseId} ${updated.scenario}`);
|
|
6228
|
+
}
|
|
6229
|
+
lines.push("");
|
|
6230
|
+
lines.push(
|
|
6231
|
+
`Created ${result.created.length}, updated ${result.updated.length}, recorded ${result.resultsRecorded} execution(s), uploaded ${result.attachmentsUploaded} attachment(s).`
|
|
6232
|
+
);
|
|
6233
|
+
if (result.runUrl) lines.push(`Run: ${result.runUrl}`);
|
|
6234
|
+
for (const skipped of result.resultsSkipped) {
|
|
6235
|
+
lines.push(` ! result for case ${skipped.caseId} not recorded: ${skipped.reason}`);
|
|
6236
|
+
}
|
|
6237
|
+
for (const error of result.errors) {
|
|
6238
|
+
lines.push(` \u2717 ${error}`);
|
|
6239
|
+
}
|
|
6240
|
+
return lines.join("\n");
|
|
6241
|
+
}
|
|
6242
|
+
|
|
6243
|
+
// src/sync/adapters/case-text.ts
|
|
6244
|
+
var BDD_KEYWORDS = ["Given", "When", "Then", "And", "But"];
|
|
6245
|
+
function encodeStepText(step) {
|
|
6246
|
+
const keyword = step.keyword.trim();
|
|
6247
|
+
return keyword ? `${keyword} ${step.text}` : step.text;
|
|
6248
|
+
}
|
|
6249
|
+
function decodeStepText(content) {
|
|
6250
|
+
const trimmed = content.trim();
|
|
6251
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
6252
|
+
if (firstSpace > 0) {
|
|
6253
|
+
const head = trimmed.slice(0, firstSpace);
|
|
6254
|
+
if (BDD_KEYWORDS.some((keyword) => keyword.toLowerCase() === head.toLowerCase())) {
|
|
6255
|
+
return { keyword: head, text: trimmed.slice(firstSpace + 1) };
|
|
6256
|
+
}
|
|
6257
|
+
}
|
|
6258
|
+
return { keyword: "", text: trimmed };
|
|
6259
|
+
}
|
|
6260
|
+
function encodeDescription(body) {
|
|
6261
|
+
if (body.links.length === 0) return body.description;
|
|
6262
|
+
const links = body.links.map((link2) => `- [${link2.label}](${link2.url})`).join("\n");
|
|
6263
|
+
return `${body.description}
|
|
6264
|
+
|
|
6265
|
+
${links}`;
|
|
6266
|
+
}
|
|
6267
|
+
function decodeDescription(raw) {
|
|
6268
|
+
const links = [];
|
|
6269
|
+
const lines = raw.split("\n");
|
|
6270
|
+
let cut = lines.length;
|
|
6271
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
6272
|
+
const line = lines[index].trim();
|
|
6273
|
+
if (line === "") continue;
|
|
6274
|
+
const match = /^- \[([^\]]+)\]\(([^)]+)\)$/.exec(line);
|
|
6275
|
+
if (!match) break;
|
|
6276
|
+
links.unshift({ label: match[1], url: match[2] });
|
|
6277
|
+
cut = index;
|
|
6278
|
+
}
|
|
6279
|
+
return { description: lines.slice(0, cut).join("\n").trim(), links };
|
|
6280
|
+
}
|
|
6281
|
+
|
|
6282
|
+
// src/sync/adapters/testrail.ts
|
|
6283
|
+
var DEFAULT_STEPS_FIELD = "custom_steps_separated";
|
|
6284
|
+
var DEFAULT_DESCRIPTION_FIELD = "custom_preconds";
|
|
6285
|
+
var DEFAULT_MAX_ATTACHMENT_BYTES = 64 * 1024 * 1024;
|
|
6286
|
+
var PAGE_LIMIT = 250;
|
|
6287
|
+
var MAX_ATTEMPTS = 3;
|
|
6288
|
+
function unwrapList(payload, key) {
|
|
6289
|
+
if (Array.isArray(payload)) return payload;
|
|
6290
|
+
if (payload && typeof payload === "object") {
|
|
6291
|
+
const list = payload[key];
|
|
6292
|
+
if (Array.isArray(list)) return list;
|
|
6293
|
+
}
|
|
6294
|
+
return [];
|
|
6295
|
+
}
|
|
6296
|
+
function encodeElapsed(durationMs) {
|
|
6297
|
+
const seconds = Math.floor(durationMs / 1e3);
|
|
6298
|
+
if (seconds < 1) return void 0;
|
|
6299
|
+
if (seconds < 60) return `${seconds}s`;
|
|
6300
|
+
const minutes = Math.floor(seconds / 60);
|
|
6301
|
+
const rest = seconds % 60;
|
|
6302
|
+
return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
|
|
6303
|
+
}
|
|
6304
|
+
function authHint(status) {
|
|
6305
|
+
if (status === 401) {
|
|
6306
|
+
return "\n TESTRAIL_USERNAME must be the login email and TESTRAIL_API_KEY an API key from My Settings -> API Keys. A password fails here when the instance enforces API keys.";
|
|
6307
|
+
}
|
|
6308
|
+
if (status === 403) {
|
|
6309
|
+
return "\n The credentials were accepted but the request was refused. Usually the API is disabled: an admin enables it under Administration -> Site Settings -> API.";
|
|
6310
|
+
}
|
|
6311
|
+
return "";
|
|
6312
|
+
}
|
|
6313
|
+
function createTestRailProvider(config, auth, deps) {
|
|
6314
|
+
const base = config.url.replace(/\/$/, "");
|
|
6315
|
+
const stepsField = config.fields?.steps ?? DEFAULT_STEPS_FIELD;
|
|
6316
|
+
const descriptionField = config.fields?.description ?? DEFAULT_DESCRIPTION_FIELD;
|
|
6317
|
+
const basicAuth = Buffer.from(`${auth.username}:${auth.apiKey}`).toString("base64");
|
|
6318
|
+
let projectName;
|
|
6319
|
+
let suiteName;
|
|
6320
|
+
const sectionNames = /* @__PURE__ */ new Map();
|
|
6321
|
+
async function api(method, init) {
|
|
6322
|
+
const url = `${base}/index.php?/api/v2/${method}`;
|
|
6323
|
+
const headers = {
|
|
6324
|
+
Authorization: `Basic ${basicAuth}`
|
|
6325
|
+
};
|
|
6326
|
+
let body;
|
|
6327
|
+
if (init?.form) {
|
|
6328
|
+
body = init.form;
|
|
6329
|
+
} else if (init?.body !== void 0) {
|
|
6330
|
+
headers["Content-Type"] = "application/json";
|
|
6331
|
+
body = JSON.stringify(init.body);
|
|
6332
|
+
}
|
|
6333
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
6334
|
+
const response = await deps.fetch(url, {
|
|
6335
|
+
method: body === void 0 ? "GET" : "POST",
|
|
6336
|
+
headers,
|
|
6337
|
+
body
|
|
6338
|
+
});
|
|
6339
|
+
if (response.status === 429 && attempt < MAX_ATTEMPTS - 1) {
|
|
6340
|
+
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
|
|
6341
|
+
deps.logger.warn(`TestRail rate limit hit, retrying in ${retryAfter}s`);
|
|
6342
|
+
await new Promise((resolve9) => setTimeout(resolve9, Math.max(1, retryAfter) * 1e3));
|
|
6343
|
+
continue;
|
|
6344
|
+
}
|
|
6345
|
+
const text2 = await response.text();
|
|
6346
|
+
if (text2.length === 0) {
|
|
6347
|
+
if (response.ok) return void 0;
|
|
6348
|
+
throw new Error(
|
|
6349
|
+
`TestRail ${method} failed (${response.status}) with an empty response${authHint(response.status)}`
|
|
6350
|
+
);
|
|
6351
|
+
}
|
|
6352
|
+
let parsed;
|
|
6353
|
+
try {
|
|
6354
|
+
parsed = JSON.parse(text2);
|
|
6355
|
+
} catch {
|
|
6356
|
+
throw new Error(
|
|
6357
|
+
`TestRail ${method} returned HTML rather than JSON (status ${response.status}).
|
|
6358
|
+
Check that sync.testrail.url is the instance root, e.g. https://acme.testrail.io, with no path after it.`
|
|
6359
|
+
);
|
|
6360
|
+
}
|
|
6361
|
+
if (!response.ok) {
|
|
6362
|
+
const detail = parsed.error ?? text2;
|
|
6363
|
+
throw new Error(
|
|
6364
|
+
`TestRail ${method} failed (${response.status}): ${detail}${authHint(response.status)}`
|
|
6365
|
+
);
|
|
6366
|
+
}
|
|
6367
|
+
return parsed;
|
|
6368
|
+
}
|
|
6369
|
+
throw new Error(`TestRail ${method} failed: rate limited after ${MAX_ATTEMPTS} attempts`);
|
|
6370
|
+
}
|
|
6371
|
+
async function paginate(method, key) {
|
|
6372
|
+
const all = [];
|
|
6373
|
+
for (let offset = 0; ; offset += PAGE_LIMIT) {
|
|
6374
|
+
const page = await api(`${method}&limit=${PAGE_LIMIT}&offset=${offset}`);
|
|
6375
|
+
const items = unwrapList(page, key);
|
|
6376
|
+
all.push(...items);
|
|
6377
|
+
if (items.length < PAGE_LIMIT) return all;
|
|
6378
|
+
}
|
|
6379
|
+
}
|
|
6380
|
+
function caseUrl(id) {
|
|
6381
|
+
return `${base}/index.php?/cases/view/${id}`;
|
|
6382
|
+
}
|
|
6383
|
+
function toRemoteCase(raw, context) {
|
|
6384
|
+
if (raw?.id === void 0) {
|
|
6385
|
+
throw new Error(
|
|
6386
|
+
`TestRail ${context} returned no case id. The response was: ${JSON.stringify(raw)?.slice(0, 200)}`
|
|
6387
|
+
);
|
|
6388
|
+
}
|
|
6389
|
+
const title = typeof raw.title === "string" ? raw.title : "";
|
|
6390
|
+
const steps = Array.isArray(raw[stepsField]) ? raw[stepsField] : [];
|
|
6391
|
+
const rawDescription = typeof raw[descriptionField] === "string" ? raw[descriptionField] : "";
|
|
6392
|
+
const { description, links } = decodeDescription(rawDescription);
|
|
6393
|
+
return {
|
|
6394
|
+
id: String(raw.id),
|
|
6395
|
+
url: caseUrl(raw.id),
|
|
6396
|
+
title,
|
|
6397
|
+
section: raw.section_id === void 0 ? void 0 : sectionNames.get(raw.section_id),
|
|
6398
|
+
body: {
|
|
6399
|
+
title,
|
|
6400
|
+
steps: steps.map((step) => decodeStepText(step.content ?? "")),
|
|
6401
|
+
description,
|
|
6402
|
+
links
|
|
6403
|
+
}
|
|
6404
|
+
};
|
|
6405
|
+
}
|
|
6406
|
+
function bodyToPayload(body) {
|
|
6407
|
+
const payload = {
|
|
6408
|
+
title: body.title,
|
|
6409
|
+
[stepsField]: body.steps.map((step) => ({ content: encodeStepText(step), expected: "" })),
|
|
6410
|
+
[descriptionField]: encodeDescription(body)
|
|
6411
|
+
};
|
|
6412
|
+
if (config.templateId !== void 0) payload["template_id"] = config.templateId;
|
|
6413
|
+
return payload;
|
|
6414
|
+
}
|
|
6415
|
+
const suiteQuery = config.suiteId === void 0 ? "" : `&suite_id=${config.suiteId}`;
|
|
6416
|
+
return {
|
|
6417
|
+
name: "testrail",
|
|
6418
|
+
maxAttachmentBytes: config.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES,
|
|
6419
|
+
describeTarget() {
|
|
6420
|
+
const project = projectName ?? `project ${config.projectId}`;
|
|
6421
|
+
const suite = suiteName ?? (config.suiteId === void 0 ? void 0 : `suite ${config.suiteId}`);
|
|
6422
|
+
return suite ? `${project} / ${suite}` : project;
|
|
6423
|
+
},
|
|
6424
|
+
async listCases() {
|
|
6425
|
+
try {
|
|
6426
|
+
const project = await api(`get_project/${config.projectId}`);
|
|
6427
|
+
projectName = project?.name;
|
|
6428
|
+
if (config.suiteId !== void 0) {
|
|
6429
|
+
const suite = await api(`get_suite/${config.suiteId}`);
|
|
6430
|
+
suiteName = suite?.name;
|
|
6431
|
+
}
|
|
6432
|
+
const sections = await paginate(
|
|
6433
|
+
`get_sections/${config.projectId}${suiteQuery}`,
|
|
6434
|
+
"sections"
|
|
6435
|
+
);
|
|
6436
|
+
for (const section of sections) sectionNames.set(section.id, section.name);
|
|
6437
|
+
} catch (err) {
|
|
6438
|
+
deps.logger.warn(`TestRail metadata lookup failed, continuing without names: ${err.message}`);
|
|
6439
|
+
}
|
|
6440
|
+
const cases = await paginate(`get_cases/${config.projectId}${suiteQuery}`, "cases");
|
|
6441
|
+
return cases.map((raw) => toRemoteCase(raw, "get_cases"));
|
|
6442
|
+
},
|
|
6443
|
+
async createCase(body) {
|
|
6444
|
+
if (config.sectionId === void 0) {
|
|
6445
|
+
throw new Error(
|
|
6446
|
+
"TestRail needs a sectionId to create cases. Set sync.testrail.sectionId to the section new cases should land in."
|
|
6447
|
+
);
|
|
6448
|
+
}
|
|
6449
|
+
const created = await api(`add_case/${config.sectionId}`, {
|
|
6450
|
+
body: bodyToPayload(body)
|
|
6451
|
+
});
|
|
6452
|
+
return toRemoteCase(created, `add_case/${config.sectionId}`);
|
|
6453
|
+
},
|
|
6454
|
+
async updateCase(id, body) {
|
|
6455
|
+
const updated = await api(`update_case/${id}`, { body: bodyToPayload(body) });
|
|
6456
|
+
return toRemoteCase(updated, `update_case/${id}`);
|
|
6457
|
+
},
|
|
6458
|
+
async recordResults(results) {
|
|
6459
|
+
const statusIds = {
|
|
6460
|
+
passed: config.statusIds?.passed ?? 1,
|
|
6461
|
+
failed: config.statusIds?.failed ?? 5,
|
|
6462
|
+
skipped: config.statusIds?.skipped
|
|
6463
|
+
};
|
|
6464
|
+
const skipped = [];
|
|
6465
|
+
const sendable = [];
|
|
6466
|
+
for (const result of results) {
|
|
6467
|
+
const caseId = Number(result.caseId);
|
|
6468
|
+
if (!Number.isFinite(caseId)) {
|
|
6469
|
+
skipped.push({ caseId: result.caseId, reason: "case id is not numeric" });
|
|
6470
|
+
continue;
|
|
6471
|
+
}
|
|
6472
|
+
const statusId = result.status === "skipped" ? statusIds.skipped : statusIds[result.status];
|
|
6473
|
+
if (statusId === void 0) {
|
|
6474
|
+
skipped.push({
|
|
6475
|
+
caseId: result.caseId,
|
|
6476
|
+
reason: "no TestRail status id configured for skipped (set sync.testrail.statusIds.skipped)"
|
|
6477
|
+
});
|
|
6478
|
+
continue;
|
|
6479
|
+
}
|
|
6480
|
+
sendable.push({ result, caseId, statusId });
|
|
6481
|
+
}
|
|
6482
|
+
if (sendable.length === 0) {
|
|
6483
|
+
return { recorded: 0, skipped, attachmentsUploaded: 0 };
|
|
6484
|
+
}
|
|
6485
|
+
let runId = config.runId;
|
|
6486
|
+
if (runId === void 0) {
|
|
6487
|
+
const name = `${config.runName ?? "executable-stories"} ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
6488
|
+
const run = await api(`add_run/${config.projectId}`, {
|
|
6489
|
+
body: {
|
|
6490
|
+
...config.suiteId === void 0 ? {} : { suite_id: config.suiteId },
|
|
6491
|
+
name,
|
|
6492
|
+
include_all: false,
|
|
6493
|
+
// Deduplicated: two stories can carry the same ticket id, and
|
|
6494
|
+
// TestRail rejects a run whose case list repeats one.
|
|
6495
|
+
case_ids: [...new Set(sendable.map((s) => s.caseId))]
|
|
6496
|
+
}
|
|
6497
|
+
});
|
|
6498
|
+
runId = run.id;
|
|
6499
|
+
}
|
|
6500
|
+
const runUrl = `${base}/index.php?/runs/view/${runId}`;
|
|
6501
|
+
const payload = sendable.map(({ result, caseId, statusId }) => {
|
|
6502
|
+
const elapsed = encodeElapsed(result.durationMs);
|
|
6503
|
+
const comment = [result.message, result.url ? `Living documentation: ${result.url}` : void 0].filter(Boolean).join("\n\n");
|
|
6504
|
+
return {
|
|
6505
|
+
case_id: caseId,
|
|
6506
|
+
status_id: statusId,
|
|
6507
|
+
...comment ? { comment } : {},
|
|
6508
|
+
...elapsed ? { elapsed } : {}
|
|
6509
|
+
};
|
|
6510
|
+
});
|
|
6511
|
+
const recorded = unwrapList(
|
|
6512
|
+
await api(`add_results_for_cases/${runId}`, { body: { results: payload } }),
|
|
6513
|
+
"results"
|
|
6514
|
+
);
|
|
6515
|
+
let attachmentsUploaded = 0;
|
|
6516
|
+
for (const [index, entry] of recorded.entries()) {
|
|
6517
|
+
const source = sendable[index]?.result;
|
|
6518
|
+
for (const attachment of source?.attachments ?? []) {
|
|
6519
|
+
try {
|
|
6520
|
+
const form = new FormData();
|
|
6521
|
+
const bytes = attachment.body.buffer.slice(
|
|
6522
|
+
attachment.body.byteOffset,
|
|
6523
|
+
attachment.body.byteOffset + attachment.body.byteLength
|
|
6524
|
+
);
|
|
6525
|
+
form.append(
|
|
6526
|
+
"attachment",
|
|
6527
|
+
new Blob([bytes], { type: attachment.mediaType }),
|
|
6528
|
+
attachment.filename
|
|
6529
|
+
);
|
|
6530
|
+
await api(`add_attachment_to_result/${entry.id}`, { form });
|
|
6531
|
+
attachmentsUploaded += 1;
|
|
6532
|
+
} catch (err) {
|
|
6533
|
+
deps.logger.warn(
|
|
6534
|
+
`TestRail attachment "${attachment.filename}" failed: ${err.message}`
|
|
6535
|
+
);
|
|
6536
|
+
}
|
|
6537
|
+
}
|
|
6538
|
+
}
|
|
6539
|
+
if (config.closeRun && config.runId === void 0) {
|
|
6540
|
+
await api(`close_run/${runId}`, { body: {} });
|
|
6541
|
+
}
|
|
6542
|
+
return {
|
|
6543
|
+
runId: String(runId),
|
|
6544
|
+
runUrl,
|
|
6545
|
+
recorded: recorded.length,
|
|
6546
|
+
skipped,
|
|
6547
|
+
attachmentsUploaded
|
|
6548
|
+
};
|
|
6549
|
+
}
|
|
6550
|
+
};
|
|
6551
|
+
}
|
|
6552
|
+
|
|
6553
|
+
// src/sync/adapters/xray.ts
|
|
6554
|
+
var DEFAULT_XRAY_BASE = "https://xray.cloud.getxray.app";
|
|
6555
|
+
var DEFAULT_TEST_TYPE = "Manual";
|
|
6556
|
+
var DEFAULT_MAX_ATTACHMENT_BYTES2 = 32 * 1024 * 1024;
|
|
6557
|
+
var PAGE_LIMIT2 = 100;
|
|
6558
|
+
function toAdf(text2) {
|
|
6559
|
+
const paragraphs = text2.split("\n\n").filter((block) => block.trim() !== "");
|
|
6560
|
+
return {
|
|
6561
|
+
version: 1,
|
|
6562
|
+
type: "doc",
|
|
6563
|
+
content: paragraphs.length === 0 ? [{ type: "paragraph", content: [] }] : paragraphs.map((block) => ({
|
|
6564
|
+
type: "paragraph",
|
|
6565
|
+
content: [{ type: "text", text: block }]
|
|
6566
|
+
}))
|
|
6567
|
+
};
|
|
6568
|
+
}
|
|
6569
|
+
function fromAdf(value) {
|
|
6570
|
+
if (typeof value === "string") return value;
|
|
6571
|
+
if (!value || typeof value !== "object") return "";
|
|
6572
|
+
const blocks = [];
|
|
6573
|
+
const walk = (node, collected) => {
|
|
6574
|
+
if (!node || typeof node !== "object") return;
|
|
6575
|
+
const typed = node;
|
|
6576
|
+
if (typed.type === "text" && typeof typed.text === "string") collected.push(typed.text);
|
|
6577
|
+
for (const child of typed.content ?? []) walk(child, collected);
|
|
6578
|
+
};
|
|
6579
|
+
for (const node of value.content ?? []) {
|
|
6580
|
+
const collected = [];
|
|
6581
|
+
walk(node, collected);
|
|
6582
|
+
blocks.push(collected.join(""));
|
|
6583
|
+
}
|
|
6584
|
+
return blocks.join("\n\n").trim();
|
|
6585
|
+
}
|
|
6586
|
+
function createXrayProvider(config, auth, deps) {
|
|
6587
|
+
const xrayBase = (config.xrayBaseUrl ?? DEFAULT_XRAY_BASE).replace(/\/$/, "");
|
|
6588
|
+
const jiraBase = config.jiraBaseUrl.replace(/\/$/, "");
|
|
6589
|
+
const jql = config.jql ?? `project = "${config.projectKey}" AND issuetype = Test`;
|
|
6590
|
+
const issueIds = /* @__PURE__ */ new Map();
|
|
6591
|
+
let token;
|
|
6592
|
+
async function authenticate() {
|
|
6593
|
+
if (token) return token;
|
|
6594
|
+
const response = await deps.fetch(`${xrayBase}/api/v2/authenticate`, {
|
|
6595
|
+
method: "POST",
|
|
6596
|
+
headers: { "Content-Type": "application/json" },
|
|
6597
|
+
body: JSON.stringify({ client_id: auth.clientId, client_secret: auth.clientSecret })
|
|
6598
|
+
});
|
|
6599
|
+
const text2 = await response.text();
|
|
6600
|
+
if (!response.ok) {
|
|
6601
|
+
throw new Error(
|
|
6602
|
+
`Xray authentication failed (${response.status}): ${text2}
|
|
6603
|
+
XRAY_CLIENT_ID and XRAY_CLIENT_SECRET come from Jira -> Apps -> Xray -> API Keys. A Jira API token is a different credential and is rejected here.`
|
|
6604
|
+
);
|
|
6605
|
+
}
|
|
6606
|
+
token = JSON.parse(text2).replace(/^"|"$/g, "");
|
|
6607
|
+
return token;
|
|
6608
|
+
}
|
|
6609
|
+
async function graphql(query, variables) {
|
|
6610
|
+
const bearer = await authenticate();
|
|
6611
|
+
const response = await deps.fetch(`${xrayBase}/api/v2/graphql`, {
|
|
6612
|
+
method: "POST",
|
|
6613
|
+
headers: {
|
|
6614
|
+
Authorization: `Bearer ${bearer}`,
|
|
6615
|
+
"Content-Type": "application/json"
|
|
6616
|
+
},
|
|
6617
|
+
body: JSON.stringify({ query, variables })
|
|
6618
|
+
});
|
|
6619
|
+
const text2 = await response.text();
|
|
6620
|
+
if (!response.ok) throw new Error(`Xray GraphQL failed (${response.status}): ${text2}`);
|
|
6621
|
+
const payload = JSON.parse(text2);
|
|
6622
|
+
if (payload.errors?.length) {
|
|
6623
|
+
throw new Error(`Xray GraphQL error: ${payload.errors.map((e) => e.message).join("; ")}`);
|
|
6624
|
+
}
|
|
6625
|
+
return payload.data;
|
|
6626
|
+
}
|
|
6627
|
+
function issueUrl(key) {
|
|
6628
|
+
return `${jiraBase}/browse/${key}`;
|
|
6629
|
+
}
|
|
6630
|
+
function toRemoteCase(test) {
|
|
6631
|
+
const key = test.jira?.key ?? test.issueId;
|
|
6632
|
+
if (!key) {
|
|
6633
|
+
throw new Error(
|
|
6634
|
+
`Xray returned a test with neither a Jira key nor an issue id: ${JSON.stringify(test)?.slice(0, 200)}`
|
|
6635
|
+
);
|
|
6636
|
+
}
|
|
6637
|
+
if (test.jira?.key) issueIds.set(test.jira.key, test.issueId);
|
|
6638
|
+
const { description, links } = decodeDescription(fromAdf(test.jira?.description));
|
|
6639
|
+
return {
|
|
6640
|
+
id: key,
|
|
6641
|
+
url: issueUrl(key),
|
|
6642
|
+
title: test.jira?.summary ?? key,
|
|
6643
|
+
body: {
|
|
6644
|
+
title: test.jira?.summary ?? key,
|
|
6645
|
+
steps: (test.steps ?? []).map((step) => decodeStepText(step.action ?? "")),
|
|
6646
|
+
description,
|
|
6647
|
+
links
|
|
6648
|
+
}
|
|
6649
|
+
};
|
|
6650
|
+
}
|
|
6651
|
+
async function jiraUpdate(key, fields) {
|
|
6652
|
+
if (!auth.jiraEmail || !auth.jiraToken) {
|
|
6653
|
+
deps.logger.warn(
|
|
6654
|
+
`Xray: summary/description for ${key} left unchanged. Set JIRA_EMAIL and JIRA_TOKEN to update Jira fields (steps are updated either way).`
|
|
6655
|
+
);
|
|
6656
|
+
return;
|
|
6657
|
+
}
|
|
6658
|
+
const basic = Buffer.from(`${auth.jiraEmail}:${auth.jiraToken}`).toString("base64");
|
|
6659
|
+
const response = await deps.fetch(`${jiraBase}/rest/api/3/issue/${key}`, {
|
|
6660
|
+
method: "PUT",
|
|
6661
|
+
headers: { Authorization: `Basic ${basic}`, "Content-Type": "application/json" },
|
|
6662
|
+
body: JSON.stringify({ fields })
|
|
6663
|
+
});
|
|
6664
|
+
if (!response.ok) {
|
|
6665
|
+
throw new Error(`Jira update of ${key} failed (${response.status}): ${await response.text()}`);
|
|
6666
|
+
}
|
|
6667
|
+
}
|
|
6668
|
+
return {
|
|
6669
|
+
name: "xray",
|
|
6670
|
+
maxAttachmentBytes: config.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES2,
|
|
6671
|
+
describeTarget() {
|
|
6672
|
+
return `${config.projectKey} (${jiraBase})`;
|
|
6673
|
+
},
|
|
6674
|
+
async listCases() {
|
|
6675
|
+
const tests = [];
|
|
6676
|
+
for (let start = 0; ; start += PAGE_LIMIT2) {
|
|
6677
|
+
const data = await graphql(
|
|
6678
|
+
`query($jql: String!, $limit: Int!, $start: Int!) {
|
|
6679
|
+
getTests(jql: $jql, limit: $limit, start: $start) {
|
|
6680
|
+
total
|
|
6681
|
+
results {
|
|
6682
|
+
issueId
|
|
6683
|
+
jira(fields: ["key", "summary", "description"])
|
|
6684
|
+
steps { id action data result }
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6687
|
+
}`,
|
|
6688
|
+
{ jql, limit: PAGE_LIMIT2, start }
|
|
6689
|
+
);
|
|
6690
|
+
const page = data.getTests?.results ?? [];
|
|
6691
|
+
tests.push(...page);
|
|
6692
|
+
if (page.length < PAGE_LIMIT2) break;
|
|
6693
|
+
}
|
|
6694
|
+
return tests.map(toRemoteCase);
|
|
6695
|
+
},
|
|
6696
|
+
async createCase(body) {
|
|
6697
|
+
const data = await graphql(
|
|
6698
|
+
`mutation($testType: UpdateTestTypeInput!, $steps: [CreateStepInput], $jira: JSON!) {
|
|
6699
|
+
createTest(testType: $testType, steps: $steps, jira: $jira) {
|
|
6700
|
+
test { issueId jira(fields: ["key", "summary", "description"]) }
|
|
6701
|
+
warnings
|
|
6702
|
+
}
|
|
6703
|
+
}`,
|
|
6704
|
+
{
|
|
6705
|
+
testType: { name: config.testType ?? DEFAULT_TEST_TYPE },
|
|
6706
|
+
steps: body.steps.map((step) => ({ action: encodeStepText(step), result: "" })),
|
|
6707
|
+
jira: {
|
|
6708
|
+
fields: {
|
|
6709
|
+
summary: body.title,
|
|
6710
|
+
description: toAdf(encodeDescription(body)),
|
|
6711
|
+
project: { key: config.projectKey }
|
|
6712
|
+
}
|
|
6713
|
+
}
|
|
6714
|
+
}
|
|
6715
|
+
);
|
|
6716
|
+
const created = data.createTest?.test;
|
|
6717
|
+
if (!created) throw new Error(`Xray createTest returned no test for "${body.title}"`);
|
|
6718
|
+
for (const warning of data.createTest?.warnings ?? []) deps.logger.warn(`Xray: ${warning}`);
|
|
6719
|
+
return toRemoteCase(created);
|
|
6720
|
+
},
|
|
6721
|
+
async updateCase(id, body) {
|
|
6722
|
+
const issueId = issueIds.get(id);
|
|
6723
|
+
if (!issueId) {
|
|
6724
|
+
throw new Error(`Xray: no internal issue id cached for ${id}. Run listCases first.`);
|
|
6725
|
+
}
|
|
6726
|
+
const current = await graphql(
|
|
6727
|
+
`query($issueId: String!) { getTest(issueId: $issueId) { steps { id } } }`,
|
|
6728
|
+
{ issueId }
|
|
6729
|
+
);
|
|
6730
|
+
const existing = current.getTest?.steps ?? [];
|
|
6731
|
+
for (const [index, step] of body.steps.entries()) {
|
|
6732
|
+
const action = encodeStepText(step);
|
|
6733
|
+
const target = existing[index];
|
|
6734
|
+
if (target) {
|
|
6735
|
+
await graphql(
|
|
6736
|
+
`mutation($stepId: String!, $step: UpdateStepInput!) {
|
|
6737
|
+
updateTestStep(stepId: $stepId, step: $step)
|
|
6738
|
+
}`,
|
|
6739
|
+
{ stepId: target.id, step: { action, result: "" } }
|
|
6740
|
+
);
|
|
6741
|
+
} else {
|
|
6742
|
+
await graphql(
|
|
6743
|
+
`mutation($issueId: String!, $step: CreateStepInput!) {
|
|
6744
|
+
addTestStep(issueId: $issueId, step: $step) { id }
|
|
6745
|
+
}`,
|
|
6746
|
+
{ issueId, step: { action, result: "" } }
|
|
6747
|
+
);
|
|
6748
|
+
}
|
|
6749
|
+
}
|
|
6750
|
+
for (const surplus of existing.slice(body.steps.length).reverse()) {
|
|
6751
|
+
await graphql(`mutation($stepId: String!) { removeTestStep(stepId: $stepId) }`, {
|
|
6752
|
+
stepId: surplus.id
|
|
6753
|
+
});
|
|
6754
|
+
}
|
|
6755
|
+
await jiraUpdate(id, {
|
|
6756
|
+
summary: body.title,
|
|
6757
|
+
description: toAdf(encodeDescription(body))
|
|
6758
|
+
});
|
|
6759
|
+
return {
|
|
6760
|
+
id,
|
|
6761
|
+
url: issueUrl(id),
|
|
6762
|
+
title: body.title,
|
|
6763
|
+
body
|
|
6764
|
+
};
|
|
6765
|
+
},
|
|
6766
|
+
async recordResults(results) {
|
|
6767
|
+
const statuses = {
|
|
6768
|
+
passed: config.statuses?.passed ?? "PASSED",
|
|
6769
|
+
failed: config.statuses?.failed ?? "FAILED",
|
|
6770
|
+
skipped: config.statuses?.skipped ?? "TODO"
|
|
6771
|
+
};
|
|
6772
|
+
let attachmentsUploaded = 0;
|
|
6773
|
+
const tests = results.map((result) => {
|
|
6774
|
+
const evidence = (result.attachments ?? []).map((attachment) => {
|
|
6775
|
+
attachmentsUploaded += 1;
|
|
6776
|
+
return {
|
|
6777
|
+
data: Buffer.from(attachment.body).toString("base64"),
|
|
6778
|
+
filename: attachment.filename,
|
|
6779
|
+
contentType: attachment.mediaType
|
|
6780
|
+
};
|
|
6781
|
+
});
|
|
6782
|
+
const comment = [result.message, result.url ? `Living documentation: ${result.url}` : void 0].filter(Boolean).join("\n\n");
|
|
6783
|
+
return {
|
|
6784
|
+
testKey: result.caseId,
|
|
6785
|
+
status: statuses[result.status],
|
|
6786
|
+
...comment ? { comment } : {},
|
|
6787
|
+
...evidence.length > 0 ? { evidence } : {}
|
|
6788
|
+
};
|
|
6789
|
+
});
|
|
6790
|
+
const bearer = await authenticate();
|
|
6791
|
+
const payload = {
|
|
6792
|
+
...config.testExecutionKey ? { testExecutionKey: config.testExecutionKey } : {},
|
|
6793
|
+
info: {
|
|
6794
|
+
summary: `${config.executionSummary ?? "executable-stories"} ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
6795
|
+
project: config.projectKey,
|
|
6796
|
+
...config.testPlanKey ? { testPlanKey: config.testPlanKey } : {}
|
|
6797
|
+
},
|
|
6798
|
+
tests
|
|
6799
|
+
};
|
|
6800
|
+
const response = await deps.fetch(`${xrayBase}/api/v2/import/execution`, {
|
|
6801
|
+
method: "POST",
|
|
6802
|
+
headers: { Authorization: `Bearer ${bearer}`, "Content-Type": "application/json" },
|
|
6803
|
+
body: JSON.stringify(payload)
|
|
6804
|
+
});
|
|
6805
|
+
const text2 = await response.text();
|
|
6806
|
+
if (!response.ok) {
|
|
6807
|
+
throw new Error(`Xray import execution failed (${response.status}): ${text2}`);
|
|
6808
|
+
}
|
|
6809
|
+
const imported = JSON.parse(text2);
|
|
6810
|
+
return {
|
|
6811
|
+
runId: imported.key,
|
|
6812
|
+
runUrl: imported.key ? issueUrl(imported.key) : void 0,
|
|
6813
|
+
recorded: tests.length,
|
|
6814
|
+
skipped: [],
|
|
6815
|
+
attachmentsUploaded
|
|
6816
|
+
};
|
|
6817
|
+
}
|
|
6818
|
+
};
|
|
6819
|
+
}
|
|
6820
|
+
|
|
6821
|
+
// src/sync/adapters/index.ts
|
|
6822
|
+
var PROVIDER_NAMES = ["testrail", "xray"];
|
|
6823
|
+
function isProviderName(value) {
|
|
6824
|
+
return PROVIDER_NAMES.includes(value);
|
|
6825
|
+
}
|
|
6826
|
+
function required(env, name, hint) {
|
|
6827
|
+
const value = env[name];
|
|
6828
|
+
if (!value) throw new Error(`Missing ${name}. ${hint}`);
|
|
6829
|
+
return value;
|
|
6830
|
+
}
|
|
6831
|
+
function buildProvider(args, deps) {
|
|
6832
|
+
const { name, targets, env } = args;
|
|
6833
|
+
if (name === "testrail") {
|
|
6834
|
+
const config2 = targets.testrail;
|
|
6835
|
+
if (!config2) {
|
|
6836
|
+
throw new Error(
|
|
6837
|
+
'No TestRail config found. Add a `sync: { testrail: { url, projectId } }` block to executable-stories.config.mjs, or run "executable-stories sync testrail --init".'
|
|
6838
|
+
);
|
|
6839
|
+
}
|
|
6840
|
+
const provider2 = createTestRailProvider(
|
|
6841
|
+
config2,
|
|
6842
|
+
{
|
|
6843
|
+
username: required(env, "TESTRAIL_USERNAME", "This is the login email of the TestRail account."),
|
|
6844
|
+
apiKey: required(
|
|
6845
|
+
env,
|
|
6846
|
+
"TESTRAIL_API_KEY",
|
|
6847
|
+
"Generate one in TestRail under My Settings -> API Keys. A password will not work when the instance requires API keys."
|
|
6848
|
+
)
|
|
6849
|
+
},
|
|
6850
|
+
deps
|
|
6851
|
+
);
|
|
6852
|
+
return {
|
|
6853
|
+
provider: provider2,
|
|
6854
|
+
// TestRail ids are numeric and conventionally written "C1234" in a ticket,
|
|
6855
|
+
// so the prefix is decoration and gets stripped.
|
|
6856
|
+
engineDefaults: { ticketPrefix: "C", ticketPrefixStrip: true }
|
|
6857
|
+
};
|
|
6858
|
+
}
|
|
6859
|
+
const config = targets.xray;
|
|
6860
|
+
if (!config) {
|
|
6861
|
+
throw new Error(
|
|
6862
|
+
'No Xray config found. Add a `sync: { xray: { jiraBaseUrl, projectKey } }` block to executable-stories.config.mjs, or run "executable-stories sync xray --init".'
|
|
6863
|
+
);
|
|
6864
|
+
}
|
|
6865
|
+
const provider = createXrayProvider(
|
|
6866
|
+
config,
|
|
6867
|
+
{
|
|
6868
|
+
clientId: required(env, "XRAY_CLIENT_ID", "Create an API key pair in Jira under Apps -> Xray -> API Keys."),
|
|
6869
|
+
clientSecret: required(env, "XRAY_CLIENT_SECRET", "This is the secret half of the Xray API key pair."),
|
|
6870
|
+
jiraEmail: env["JIRA_EMAIL"],
|
|
6871
|
+
jiraToken: env["JIRA_TOKEN"]
|
|
6872
|
+
},
|
|
6873
|
+
deps
|
|
6874
|
+
);
|
|
6875
|
+
return {
|
|
6876
|
+
provider,
|
|
6877
|
+
// An Xray case id is a Jira issue key, so the project prefix is part of the
|
|
6878
|
+
// id and must survive.
|
|
6879
|
+
engineDefaults: { ticketPrefix: `${config.projectKey}-`, ticketPrefixStrip: false }
|
|
6880
|
+
};
|
|
6881
|
+
}
|
|
6882
|
+
|
|
6883
|
+
// src/index.ts
|
|
5495
6884
|
import {
|
|
5496
6885
|
STORY_REPORT_SCHEMA_VERSION,
|
|
5497
6886
|
STORY_REPORT_SCHEMA_MAJOR
|
|
@@ -5512,21 +6901,21 @@ import {
|
|
|
5512
6901
|
} from "executable-stories-core/converters/acl/validate";
|
|
5513
6902
|
|
|
5514
6903
|
// src/coverage-index.ts
|
|
5515
|
-
function normalizePath(
|
|
5516
|
-
return
|
|
6904
|
+
function normalizePath(path11) {
|
|
6905
|
+
return path11.replace(/^\.\//, "");
|
|
5517
6906
|
}
|
|
5518
6907
|
function scenariosCoveringPaths(index, paths) {
|
|
5519
6908
|
const queries = paths.map(normalizePath);
|
|
5520
6909
|
return index.scenarios.filter(
|
|
5521
6910
|
(scenario) => scenario.covers.some(
|
|
5522
|
-
(glob) => queries.some((
|
|
6911
|
+
(glob) => queries.some((path11) => matchesPattern(normalizePath(glob), path11))
|
|
5523
6912
|
)
|
|
5524
6913
|
);
|
|
5525
6914
|
}
|
|
5526
6915
|
|
|
5527
6916
|
// src/watch.ts
|
|
5528
|
-
import * as
|
|
5529
|
-
import * as
|
|
6917
|
+
import * as fs5 from "fs";
|
|
6918
|
+
import * as path6 from "path";
|
|
5530
6919
|
import { canonicalizeRun } from "executable-stories-core/converters/acl/index";
|
|
5531
6920
|
import { synthesizeStories } from "executable-stories-core/converters/synthesize";
|
|
5532
6921
|
function toRun(data, inputType, synthesize) {
|
|
@@ -5536,8 +6925,8 @@ function toRun(data, inputType, synthesize) {
|
|
|
5536
6925
|
return canonicalizeRun(raw);
|
|
5537
6926
|
}
|
|
5538
6927
|
async function regenerateRun(options, deps = {}) {
|
|
5539
|
-
const read = deps.readFile ?? ((filePath) =>
|
|
5540
|
-
const data = JSON.parse(read(
|
|
6928
|
+
const read = deps.readFile ?? ((filePath) => fs5.readFileSync(filePath, "utf8"));
|
|
6929
|
+
const data = JSON.parse(read(path6.resolve(options.input)));
|
|
5541
6930
|
const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
|
|
5542
6931
|
const generator = new ReportGenerator({
|
|
5543
6932
|
formats: options.formats,
|
|
@@ -5553,7 +6942,7 @@ async function regenerateArtifacts(options, deps = {}) {
|
|
|
5553
6942
|
function startWatch(options, deps = {}) {
|
|
5554
6943
|
const log = deps.log ?? ((message) => console.log(message));
|
|
5555
6944
|
const regenerate = deps.regenerate ?? ((input) => regenerateArtifacts({ ...options, input }, deps));
|
|
5556
|
-
const watchFn = deps.watch ?? ((filePath, listener) =>
|
|
6945
|
+
const watchFn = deps.watch ?? ((filePath, listener) => fs5.watch(filePath, listener));
|
|
5557
6946
|
const debounceMs = options.debounceMs ?? 150;
|
|
5558
6947
|
let timer;
|
|
5559
6948
|
let running = false;
|
|
@@ -5582,7 +6971,7 @@ function startWatch(options, deps = {}) {
|
|
|
5582
6971
|
timer = setTimeout(() => void run(), debounceMs);
|
|
5583
6972
|
};
|
|
5584
6973
|
trigger();
|
|
5585
|
-
const watcher = watchFn(
|
|
6974
|
+
const watcher = watchFn(path6.resolve(options.input), trigger);
|
|
5586
6975
|
return {
|
|
5587
6976
|
close: () => {
|
|
5588
6977
|
if (timer) clearTimeout(timer);
|
|
@@ -5874,27 +7263,27 @@ async function updateDescription(issueKey, base, adf, headers, fetchFn) {
|
|
|
5874
7263
|
import { parseNdjson, parseEnvelopes } from "executable-stories-core/converters/ndjson-parser";
|
|
5875
7264
|
|
|
5876
7265
|
// src/utils/git-info.ts
|
|
5877
|
-
import * as
|
|
5878
|
-
import * as
|
|
7266
|
+
import * as fs6 from "fs";
|
|
7267
|
+
import * as path7 from "path";
|
|
5879
7268
|
function readGitSha(cwd = process.cwd()) {
|
|
5880
7269
|
const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
|
|
5881
7270
|
if (envSha) return envSha;
|
|
5882
7271
|
const gitDir = findGitDir(cwd);
|
|
5883
7272
|
if (!gitDir) return void 0;
|
|
5884
7273
|
try {
|
|
5885
|
-
const headPath =
|
|
5886
|
-
const head =
|
|
7274
|
+
const headPath = path7.join(gitDir, "HEAD");
|
|
7275
|
+
const head = fs6.readFileSync(headPath, "utf8").trim();
|
|
5887
7276
|
if (!head.startsWith("ref:")) {
|
|
5888
7277
|
return head;
|
|
5889
7278
|
}
|
|
5890
7279
|
const refPath = head.replace("ref:", "").trim();
|
|
5891
|
-
const refFile =
|
|
5892
|
-
if (
|
|
5893
|
-
return
|
|
7280
|
+
const refFile = path7.join(gitDir, refPath);
|
|
7281
|
+
if (fs6.existsSync(refFile)) {
|
|
7282
|
+
return fs6.readFileSync(refFile, "utf8").trim();
|
|
5894
7283
|
}
|
|
5895
|
-
const packedRefs =
|
|
5896
|
-
if (
|
|
5897
|
-
const content =
|
|
7284
|
+
const packedRefs = path7.join(gitDir, "packed-refs");
|
|
7285
|
+
if (fs6.existsSync(packedRefs)) {
|
|
7286
|
+
const content = fs6.readFileSync(packedRefs, "utf8");
|
|
5898
7287
|
for (const line of content.split("\n")) {
|
|
5899
7288
|
if (!line || line.startsWith("#") || line.startsWith("^")) continue;
|
|
5900
7289
|
const [sha, ref] = line.split(" ");
|
|
@@ -5909,19 +7298,19 @@ function readGitSha(cwd = process.cwd()) {
|
|
|
5909
7298
|
function findGitDir(start) {
|
|
5910
7299
|
let current = start;
|
|
5911
7300
|
while (true) {
|
|
5912
|
-
const candidate =
|
|
5913
|
-
if (
|
|
5914
|
-
const stat =
|
|
7301
|
+
const candidate = path7.join(current, ".git");
|
|
7302
|
+
if (fs6.existsSync(candidate)) {
|
|
7303
|
+
const stat = fs6.statSync(candidate);
|
|
5915
7304
|
if (stat.isFile()) {
|
|
5916
|
-
const content =
|
|
7305
|
+
const content = fs6.readFileSync(candidate, "utf8").trim();
|
|
5917
7306
|
const match = content.match(/^gitdir: (.+)$/);
|
|
5918
7307
|
if (match) {
|
|
5919
|
-
return
|
|
7308
|
+
return path7.resolve(current, match[1]);
|
|
5920
7309
|
}
|
|
5921
7310
|
}
|
|
5922
7311
|
return candidate;
|
|
5923
7312
|
}
|
|
5924
|
-
const parent =
|
|
7313
|
+
const parent = path7.dirname(current);
|
|
5925
7314
|
if (parent === current) return void 0;
|
|
5926
7315
|
current = parent;
|
|
5927
7316
|
}
|
|
@@ -5932,8 +7321,8 @@ function readBranchName(cwd = process.cwd()) {
|
|
|
5932
7321
|
const gitDir = findGitDir(cwd);
|
|
5933
7322
|
if (!gitDir) return void 0;
|
|
5934
7323
|
try {
|
|
5935
|
-
const headPath =
|
|
5936
|
-
const head =
|
|
7324
|
+
const headPath = path7.join(gitDir, "HEAD");
|
|
7325
|
+
const head = fs6.readFileSync(headPath, "utf8").trim();
|
|
5937
7326
|
if (head.startsWith("ref:")) {
|
|
5938
7327
|
const refPath = head.replace("ref:", "").trim();
|
|
5939
7328
|
const match = refPath.match(/^refs\/heads\/(.+)$/);
|
|
@@ -5951,8 +7340,8 @@ import { msToNanoseconds } from "executable-stories-core/utils/duration";
|
|
|
5951
7340
|
import { nanosecondsToMs } from "executable-stories-core/utils/duration";
|
|
5952
7341
|
|
|
5953
7342
|
// src/utils/metadata.ts
|
|
5954
|
-
import * as
|
|
5955
|
-
import * as
|
|
7343
|
+
import * as fs7 from "fs";
|
|
7344
|
+
import * as path8 from "path";
|
|
5956
7345
|
var versionCache = /* @__PURE__ */ new Map();
|
|
5957
7346
|
function readPackageVersion(root) {
|
|
5958
7347
|
if (versionCache.has(root)) {
|
|
@@ -5963,18 +7352,18 @@ function readPackageVersion(root) {
|
|
|
5963
7352
|
return version;
|
|
5964
7353
|
}
|
|
5965
7354
|
function findPackageVersion(startDir) {
|
|
5966
|
-
let current =
|
|
7355
|
+
let current = path8.resolve(startDir);
|
|
5967
7356
|
while (true) {
|
|
5968
|
-
const pkgPath =
|
|
7357
|
+
const pkgPath = path8.join(current, "package.json");
|
|
5969
7358
|
try {
|
|
5970
|
-
if (
|
|
5971
|
-
const raw =
|
|
7359
|
+
if (fs7.existsSync(pkgPath)) {
|
|
7360
|
+
const raw = fs7.readFileSync(pkgPath, "utf8");
|
|
5972
7361
|
const parsed = JSON.parse(raw);
|
|
5973
7362
|
return parsed.version;
|
|
5974
7363
|
}
|
|
5975
7364
|
} catch {
|
|
5976
7365
|
}
|
|
5977
|
-
const parent =
|
|
7366
|
+
const parent = path8.dirname(current);
|
|
5978
7367
|
if (parent === current) {
|
|
5979
7368
|
return void 0;
|
|
5980
7369
|
}
|
|
@@ -6542,24 +7931,8 @@ function calculateFlakiness(args) {
|
|
|
6542
7931
|
const countable = entries.filter(
|
|
6543
7932
|
(e) => e.status === "passed" || e.status === "failed"
|
|
6544
7933
|
);
|
|
6545
|
-
if (countable.length < MIN_FLAKINESS_SAMPLES) {
|
|
6546
|
-
return {
|
|
6547
|
-
flakinessLevel: "stable",
|
|
6548
|
-
flakinessScore: 0,
|
|
6549
|
-
failureRate: 0,
|
|
6550
|
-
longestPassStreak: countable.length,
|
|
6551
|
-
longestFailStreak: 0
|
|
6552
|
-
};
|
|
6553
|
-
}
|
|
6554
|
-
let transitions = 0;
|
|
6555
|
-
for (let i = 1; i < countable.length; i++) {
|
|
6556
|
-
if (countable[i].status !== countable[i - 1].status) {
|
|
6557
|
-
transitions++;
|
|
6558
|
-
}
|
|
6559
|
-
}
|
|
6560
|
-
const transitionScore = transitions / (countable.length - 1);
|
|
6561
7934
|
const failures = countable.filter((e) => e.status === "failed").length;
|
|
6562
|
-
const failureRate = failures / countable.length;
|
|
7935
|
+
const failureRate = countable.length > 0 ? failures / countable.length : 0;
|
|
6563
7936
|
let longestPassStreak = 0;
|
|
6564
7937
|
let longestFailStreak = 0;
|
|
6565
7938
|
let currentPassStreak = 0;
|
|
@@ -6579,6 +7952,22 @@ function calculateFlakiness(args) {
|
|
|
6579
7952
|
}
|
|
6580
7953
|
}
|
|
6581
7954
|
}
|
|
7955
|
+
if (countable.length < MIN_FLAKINESS_SAMPLES) {
|
|
7956
|
+
return {
|
|
7957
|
+
flakinessLevel: "stable",
|
|
7958
|
+
flakinessScore: 0,
|
|
7959
|
+
failureRate,
|
|
7960
|
+
longestPassStreak,
|
|
7961
|
+
longestFailStreak
|
|
7962
|
+
};
|
|
7963
|
+
}
|
|
7964
|
+
let transitions = 0;
|
|
7965
|
+
for (let i = 1; i < countable.length; i++) {
|
|
7966
|
+
if (countable[i].status !== countable[i - 1].status) {
|
|
7967
|
+
transitions++;
|
|
7968
|
+
}
|
|
7969
|
+
}
|
|
7970
|
+
const transitionScore = transitions / (countable.length - 1);
|
|
6582
7971
|
let flakinessLevel;
|
|
6583
7972
|
if (transitionScore > 0.5 || transitionScore > 0.3 && failureRate > 0.2) {
|
|
6584
7973
|
flakinessLevel = "flaky";
|
|
@@ -7171,7 +8560,7 @@ function statusIcon2(status) {
|
|
|
7171
8560
|
return "\u2022";
|
|
7172
8561
|
}
|
|
7173
8562
|
}
|
|
7174
|
-
function
|
|
8563
|
+
function escapeCell3(value) {
|
|
7175
8564
|
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
7176
8565
|
}
|
|
7177
8566
|
function intentSummary(intent) {
|
|
@@ -7200,7 +8589,7 @@ function renderWeakBand(lines, files) {
|
|
|
7200
8589
|
lines.push(`## \u{1F7E1} Changed code with weak evidence (${weak.length})`);
|
|
7201
8590
|
lines.push("");
|
|
7202
8591
|
for (const file of weak) {
|
|
7203
|
-
const covered = file.claims.map((c) => `${
|
|
8592
|
+
const covered = file.claims.map((c) => `${escapeCell3(c.scenario)} (${c.strength})`).join(", ");
|
|
7204
8593
|
lines.push(`- \`${file.path}\` _(${file.changeKind})_ \u2014 only: ${covered}`);
|
|
7205
8594
|
}
|
|
7206
8595
|
lines.push("");
|
|
@@ -7225,7 +8614,7 @@ function renderClaim(lines, claim) {
|
|
|
7225
8614
|
);
|
|
7226
8615
|
}
|
|
7227
8616
|
if (claim.intent) {
|
|
7228
|
-
lines.push(`- Why: ${
|
|
8617
|
+
lines.push(`- Why: ${escapeCell3(intentSummary(claim.intent))}`);
|
|
7229
8618
|
}
|
|
7230
8619
|
lines.push("");
|
|
7231
8620
|
}
|
|
@@ -7282,7 +8671,7 @@ function renderCodeDiff(lines, evidence) {
|
|
|
7282
8671
|
} else {
|
|
7283
8672
|
for (const ref of annotation.scenarios) {
|
|
7284
8673
|
lines.push(
|
|
7285
|
-
ref.resolved && ref.status ? `- ${statusIcon2(ref.status)} ${
|
|
8674
|
+
ref.resolved && ref.status ? `- ${statusIcon2(ref.status)} ${escapeCell3(ref.scenario ?? ref.id)} (\`${ref.id}\`)` : `- \u26A0\uFE0F \`${ref.id}\` \u2014 unverified reference (scenario not in this run)`
|
|
7286
8675
|
);
|
|
7287
8676
|
}
|
|
7288
8677
|
}
|
|
@@ -7501,9 +8890,9 @@ function renderDiffHunk(file, hunk, anchoredStart, anchoredCount) {
|
|
|
7501
8890
|
const sign = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
|
|
7502
8891
|
return `<tr class="${cls}"><td class="diff-ln">${o}</td><td class="diff-ln">${n}</td><td class="diff-sign">${sign}</td><td class="diff-code">${escapeHtml3(line.text)}</td></tr>`;
|
|
7503
8892
|
});
|
|
7504
|
-
const
|
|
8893
|
+
const path11 = file.newPath ?? file.oldPath ?? "";
|
|
7505
8894
|
return `<div class="diff-hunk">
|
|
7506
|
-
<div class="diff-file-header"><code>${escapeHtml3(
|
|
8895
|
+
<div class="diff-file-header"><code>${escapeHtml3(path11)}</code> <span class="subtle">@@ -${hunk.oldStart} +${hunk.newStart} @@ ${escapeHtml3(hunk.header)}</span></div>
|
|
7507
8896
|
<table class="diff-table"><tbody>${rows.join("")}</tbody></table>
|
|
7508
8897
|
</div>`;
|
|
7509
8898
|
}
|
|
@@ -7738,8 +9127,8 @@ applyTheme(getEffectiveTheme());` : "";
|
|
|
7738
9127
|
};
|
|
7739
9128
|
|
|
7740
9129
|
// src/deploy/ledger.ts
|
|
7741
|
-
import * as
|
|
7742
|
-
import * as
|
|
9130
|
+
import * as fs8 from "fs";
|
|
9131
|
+
import * as path9 from "path";
|
|
7743
9132
|
function createEmptyLedger() {
|
|
7744
9133
|
return {
|
|
7745
9134
|
deployments: [],
|
|
@@ -7747,12 +9136,12 @@ function createEmptyLedger() {
|
|
|
7747
9136
|
};
|
|
7748
9137
|
}
|
|
7749
9138
|
function loadLedger(ledgerPath) {
|
|
7750
|
-
const resolved =
|
|
7751
|
-
if (!
|
|
9139
|
+
const resolved = path9.resolve(ledgerPath);
|
|
9140
|
+
if (!fs8.existsSync(resolved)) {
|
|
7752
9141
|
return createEmptyLedger();
|
|
7753
9142
|
}
|
|
7754
9143
|
try {
|
|
7755
|
-
const raw = JSON.parse(
|
|
9144
|
+
const raw = JSON.parse(fs8.readFileSync(resolved, "utf8"));
|
|
7756
9145
|
if (raw.schemaVersion !== 1) {
|
|
7757
9146
|
throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
|
|
7758
9147
|
}
|
|
@@ -7763,10 +9152,10 @@ function loadLedger(ledgerPath) {
|
|
|
7763
9152
|
}
|
|
7764
9153
|
}
|
|
7765
9154
|
function saveLedger(ledger, ledgerPath) {
|
|
7766
|
-
const resolved =
|
|
7767
|
-
const dir =
|
|
7768
|
-
|
|
7769
|
-
|
|
9155
|
+
const resolved = path9.resolve(ledgerPath);
|
|
9156
|
+
const dir = path9.dirname(resolved);
|
|
9157
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
9158
|
+
fs8.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
|
|
7770
9159
|
}
|
|
7771
9160
|
function getLatestDeployment(ledger, environment) {
|
|
7772
9161
|
return [...ledger.deployments].reverse().find((d) => d.environment === environment);
|
|
@@ -7893,11 +9282,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
7893
9282
|
const ext = FORMAT_EXTENSIONS[format];
|
|
7894
9283
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
7895
9284
|
if (mode === "aggregated") {
|
|
7896
|
-
return toPosix(
|
|
9285
|
+
return toPosix(path10.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
|
|
7897
9286
|
}
|
|
7898
9287
|
const normalizedSource = toPosix(sourceFile);
|
|
7899
|
-
const dirOfSource =
|
|
7900
|
-
let baseName =
|
|
9288
|
+
const dirOfSource = path10.posix.dirname(normalizedSource);
|
|
9289
|
+
let baseName = path10.posix.basename(normalizedSource);
|
|
7901
9290
|
for (const testExt of TEST_EXTENSIONS) {
|
|
7902
9291
|
if (baseName.endsWith(testExt)) {
|
|
7903
9292
|
baseName = baseName.slice(0, -testExt.length);
|
|
@@ -7906,12 +9295,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
7906
9295
|
}
|
|
7907
9296
|
const fileName = `${baseName}.${effectiveName}${ext}`;
|
|
7908
9297
|
if (colocatedStyle === "adjacent") {
|
|
7909
|
-
return toPosix(
|
|
9298
|
+
return toPosix(path10.posix.join(dirOfSource, fileName));
|
|
7910
9299
|
}
|
|
7911
9300
|
if (colocatedStyle === "flat") {
|
|
7912
|
-
return toPosix(
|
|
9301
|
+
return toPosix(path10.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
|
|
7913
9302
|
}
|
|
7914
|
-
return toPosix(
|
|
9303
|
+
return toPosix(path10.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
7915
9304
|
}
|
|
7916
9305
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
7917
9306
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -8131,8 +9520,8 @@ var ReportGenerator = class {
|
|
|
8131
9520
|
if (astroPaths) {
|
|
8132
9521
|
for (const mdPath of astroPaths) {
|
|
8133
9522
|
const content = await fsPromises.readFile(mdPath, "utf8");
|
|
8134
|
-
const mdDir =
|
|
8135
|
-
const assetsDir =
|
|
9523
|
+
const mdDir = path10.dirname(mdPath);
|
|
9524
|
+
const assetsDir = path10.resolve(this.options.astro.assetsDir);
|
|
8136
9525
|
const result = copyMarkdownAssets({
|
|
8137
9526
|
markdown: content,
|
|
8138
9527
|
markdownDir: mdDir,
|
|
@@ -8180,16 +9569,16 @@ var ReportGenerator = class {
|
|
|
8180
9569
|
bySourceFile.set(sourceFile, outputPath);
|
|
8181
9570
|
}
|
|
8182
9571
|
if (bySourceFile.size === 0) return void 0;
|
|
8183
|
-
const indexPath = toPosix(
|
|
9572
|
+
const indexPath = toPosix(path10.join(this.options.outputDir, "index.html"));
|
|
8184
9573
|
if (htmlPaths.some((p) => toPosix(p) === indexPath)) {
|
|
8185
9574
|
this.deps.logger.warn?.(
|
|
8186
9575
|
`Skipping colocated index: a report already occupies ${indexPath}.`
|
|
8187
9576
|
);
|
|
8188
9577
|
return void 0;
|
|
8189
9578
|
}
|
|
8190
|
-
const entries = buildIndexEntries(run, bySourceFile,
|
|
9579
|
+
const entries = buildIndexEntries(run, bySourceFile, path10.dirname(indexPath));
|
|
8191
9580
|
const html = renderColocatedIndex(entries, this.options.html.title);
|
|
8192
|
-
await fsPromises.mkdir(
|
|
9581
|
+
await fsPromises.mkdir(path10.dirname(indexPath), { recursive: true });
|
|
8193
9582
|
await this.deps.writeFile(indexPath, html);
|
|
8194
9583
|
return indexPath;
|
|
8195
9584
|
}
|
|
@@ -8208,9 +9597,9 @@ var ReportGenerator = class {
|
|
|
8208
9597
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
8209
9598
|
const ext = FORMAT_EXTENSIONS[format];
|
|
8210
9599
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
8211
|
-
const outputPath = toPosix(
|
|
9600
|
+
const outputPath = toPosix(path10.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
|
|
8212
9601
|
const content = await this.formatContent(run, format);
|
|
8213
|
-
const dir =
|
|
9602
|
+
const dir = path10.dirname(outputPath);
|
|
8214
9603
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
8215
9604
|
await this.deps.writeFile(outputPath, content);
|
|
8216
9605
|
return [outputPath];
|
|
@@ -8222,7 +9611,7 @@ var ReportGenerator = class {
|
|
|
8222
9611
|
testCases
|
|
8223
9612
|
};
|
|
8224
9613
|
const content = await this.formatContent(groupRun, format);
|
|
8225
|
-
const dir =
|
|
9614
|
+
const dir = path10.dirname(outputPath);
|
|
8226
9615
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
8227
9616
|
await this.deps.writeFile(outputPath, content);
|
|
8228
9617
|
writtenPaths.push(outputPath);
|
|
@@ -8413,7 +9802,7 @@ async function generateRunComparison(args) {
|
|
|
8413
9802
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
8414
9803
|
for (const format of args.formats) {
|
|
8415
9804
|
const ext = format === "html" ? ".html" : format === "changelog" ? ".changelog.md" : ".md";
|
|
8416
|
-
const outputPath = toPosix(
|
|
9805
|
+
const outputPath = toPosix(path10.join(outputDir, `${outputName}${ext}`));
|
|
8417
9806
|
const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : format === "changelog" ? new RunDiffChangelogFormatter().format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
|
|
8418
9807
|
await fsPromises.writeFile(outputPath, content, "utf8");
|
|
8419
9808
|
files.push(outputPath);
|
|
@@ -8440,6 +9829,7 @@ export {
|
|
|
8440
9829
|
CucumberHtmlFormatter,
|
|
8441
9830
|
CucumberJsonFormatter,
|
|
8442
9831
|
CucumberMessagesFormatter,
|
|
9832
|
+
DEFAULT_LOCKFILE_PATH,
|
|
8443
9833
|
ES_THEME_TOKENS_CSS2 as ES_THEME_TOKENS_CSS,
|
|
8444
9834
|
ES_THEME_TOKEN_VALUES,
|
|
8445
9835
|
JUnitFormatter,
|
|
@@ -8447,6 +9837,7 @@ export {
|
|
|
8447
9837
|
MIN_METRIC_SAMPLES,
|
|
8448
9838
|
MIN_PERF_SAMPLES,
|
|
8449
9839
|
MarkdownFormatter,
|
|
9840
|
+
PROVIDER_NAMES,
|
|
8450
9841
|
ReleaseManifestFormatter,
|
|
8451
9842
|
ReportGenerator,
|
|
8452
9843
|
ReviewHtmlFormatter,
|
|
@@ -8465,11 +9856,15 @@ export {
|
|
|
8465
9856
|
adaptPlaywrightRun,
|
|
8466
9857
|
adaptVitestRun,
|
|
8467
9858
|
advanceState,
|
|
9859
|
+
analyzeSync,
|
|
9860
|
+
applySync,
|
|
8468
9861
|
assembleCodeDiff,
|
|
8469
9862
|
assertValidRun,
|
|
8470
9863
|
buildCheck,
|
|
9864
|
+
buildCoverageJson,
|
|
8471
9865
|
buildGoal,
|
|
8472
9866
|
buildHtmlDocEntry,
|
|
9867
|
+
buildProvider,
|
|
8473
9868
|
buildReview,
|
|
8474
9869
|
buildTriage,
|
|
8475
9870
|
bundleAssets,
|
|
@@ -8479,11 +9874,14 @@ export {
|
|
|
8479
9874
|
classifyStatusChange,
|
|
8480
9875
|
clearVersionCache,
|
|
8481
9876
|
codeDiffDiagnostics,
|
|
9877
|
+
collectAttachments,
|
|
8482
9878
|
computeTestMetrics,
|
|
8483
9879
|
copyMarkdownAssets,
|
|
8484
9880
|
createAnchor,
|
|
8485
9881
|
createPrCommentSummary,
|
|
8486
9882
|
createReportGenerator,
|
|
9883
|
+
createTestRailProvider,
|
|
9884
|
+
createXrayProvider,
|
|
8487
9885
|
deriveAudience,
|
|
8488
9886
|
deriveChangeType,
|
|
8489
9887
|
deriveStepResults,
|
|
@@ -8491,6 +9889,7 @@ export {
|
|
|
8491
9889
|
detectPerformanceTrend,
|
|
8492
9890
|
diffRuns,
|
|
8493
9891
|
diffStoryReports,
|
|
9892
|
+
emptyLockfile,
|
|
8494
9893
|
findGitDir,
|
|
8495
9894
|
formatDuration4 as formatDuration,
|
|
8496
9895
|
generateRunComparison,
|
|
@@ -8500,7 +9899,9 @@ export {
|
|
|
8500
9899
|
getEnvironmentDrift,
|
|
8501
9900
|
gradeEvidence,
|
|
8502
9901
|
hasSufficientHistory,
|
|
9902
|
+
hashCaseBody,
|
|
8503
9903
|
initialRunState,
|
|
9904
|
+
isProviderName,
|
|
8504
9905
|
isReviewableSource,
|
|
8505
9906
|
isTestFile,
|
|
8506
9907
|
joinNameAndExt,
|
|
@@ -8515,19 +9916,26 @@ export {
|
|
|
8515
9916
|
normalizeStatus,
|
|
8516
9917
|
normalizeVitestResults,
|
|
8517
9918
|
parseEnvelopes,
|
|
9919
|
+
parseLockfile,
|
|
8518
9920
|
parseNdjson,
|
|
8519
9921
|
parseUnifiedDiff,
|
|
9922
|
+
projectBehaviours,
|
|
8520
9923
|
publishConfluencePage,
|
|
8521
9924
|
publishJiraIssue,
|
|
8522
9925
|
readBranchName,
|
|
8523
9926
|
readGitSha,
|
|
9927
|
+
readLockfile,
|
|
8524
9928
|
readPackageVersion,
|
|
8525
9929
|
recordDeployment,
|
|
8526
9930
|
regenerateArtifacts,
|
|
8527
9931
|
regenerateRun,
|
|
8528
9932
|
relocateAnchor,
|
|
9933
|
+
renderApplyResult,
|
|
8529
9934
|
renderCheck,
|
|
9935
|
+
renderCoverageMarkdown,
|
|
9936
|
+
renderCoverageText,
|
|
8530
9937
|
renderGoal,
|
|
9938
|
+
renderPlan,
|
|
8531
9939
|
renderTriage,
|
|
8532
9940
|
resolveAttachment,
|
|
8533
9941
|
resolveAttachments,
|
|
@@ -8539,6 +9947,7 @@ export {
|
|
|
8539
9947
|
sendSlackNotification,
|
|
8540
9948
|
sendTeamsNotification,
|
|
8541
9949
|
sendWebhookNotification,
|
|
9950
|
+
serializeLockfile,
|
|
8542
9951
|
signBody,
|
|
8543
9952
|
slugify2 as slugify,
|
|
8544
9953
|
startWatch,
|
|
@@ -8546,6 +9955,7 @@ export {
|
|
|
8546
9955
|
toAgentText,
|
|
8547
9956
|
toBehaviorManifest,
|
|
8548
9957
|
toCIInfo,
|
|
9958
|
+
toCaseBody,
|
|
8549
9959
|
toRawCIInfo,
|
|
8550
9960
|
toReleaseManifest,
|
|
8551
9961
|
toScenarioIndex,
|
|
@@ -8553,6 +9963,7 @@ export {
|
|
|
8553
9963
|
toTraceabilityMatrix,
|
|
8554
9964
|
tryGetActiveOtelContext,
|
|
8555
9965
|
updateHistory,
|
|
8556
|
-
validateCanonicalRun
|
|
9966
|
+
validateCanonicalRun,
|
|
9967
|
+
writeLockfile
|
|
8557
9968
|
};
|
|
8558
9969
|
//# sourceMappingURL=index.js.map
|