executable-stories-formatters 1.9.2 → 1.11.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 +2458 -585
- 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.cjs
CHANGED
|
@@ -37,6 +37,7 @@ __export(src_exports, {
|
|
|
37
37
|
CucumberHtmlFormatter: () => CucumberHtmlFormatter,
|
|
38
38
|
CucumberJsonFormatter: () => CucumberJsonFormatter,
|
|
39
39
|
CucumberMessagesFormatter: () => CucumberMessagesFormatter,
|
|
40
|
+
DEFAULT_LOCKFILE_PATH: () => DEFAULT_LOCKFILE_PATH,
|
|
40
41
|
ES_THEME_TOKENS_CSS: () => import_tokens2.ES_THEME_TOKENS_CSS,
|
|
41
42
|
ES_THEME_TOKEN_VALUES: () => import_tokens2.ES_THEME_TOKEN_VALUES,
|
|
42
43
|
JUnitFormatter: () => JUnitFormatter,
|
|
@@ -44,6 +45,7 @@ __export(src_exports, {
|
|
|
44
45
|
MIN_METRIC_SAMPLES: () => MIN_METRIC_SAMPLES,
|
|
45
46
|
MIN_PERF_SAMPLES: () => MIN_PERF_SAMPLES,
|
|
46
47
|
MarkdownFormatter: () => MarkdownFormatter,
|
|
48
|
+
PROVIDER_NAMES: () => PROVIDER_NAMES,
|
|
47
49
|
ReleaseManifestFormatter: () => ReleaseManifestFormatter,
|
|
48
50
|
ReportGenerator: () => ReportGenerator,
|
|
49
51
|
ReviewHtmlFormatter: () => ReviewHtmlFormatter,
|
|
@@ -62,11 +64,15 @@ __export(src_exports, {
|
|
|
62
64
|
adaptPlaywrightRun: () => adaptPlaywrightRun,
|
|
63
65
|
adaptVitestRun: () => adaptVitestRun,
|
|
64
66
|
advanceState: () => import_executable_stories_core3.advanceState,
|
|
67
|
+
analyzeSync: () => analyzeSync,
|
|
68
|
+
applySync: () => applySync,
|
|
65
69
|
assembleCodeDiff: () => assembleCodeDiff,
|
|
66
70
|
assertValidRun: () => import_validate.assertValidRun,
|
|
67
71
|
buildCheck: () => buildCheck,
|
|
72
|
+
buildCoverageJson: () => buildCoverageJson,
|
|
68
73
|
buildGoal: () => buildGoal,
|
|
69
74
|
buildHtmlDocEntry: () => import_doc_builders.buildHtmlDocEntry,
|
|
75
|
+
buildProvider: () => buildProvider,
|
|
70
76
|
buildReview: () => buildReview,
|
|
71
77
|
buildTriage: () => buildTriage,
|
|
72
78
|
bundleAssets: () => bundleAssets,
|
|
@@ -76,11 +82,14 @@ __export(src_exports, {
|
|
|
76
82
|
classifyStatusChange: () => classifyStatusChange,
|
|
77
83
|
clearVersionCache: () => clearVersionCache,
|
|
78
84
|
codeDiffDiagnostics: () => codeDiffDiagnostics,
|
|
85
|
+
collectAttachments: () => collectAttachments,
|
|
79
86
|
computeTestMetrics: () => computeTestMetrics,
|
|
80
87
|
copyMarkdownAssets: () => copyMarkdownAssets,
|
|
81
88
|
createAnchor: () => createAnchor,
|
|
82
89
|
createPrCommentSummary: () => createPrCommentSummary,
|
|
83
90
|
createReportGenerator: () => createReportGenerator,
|
|
91
|
+
createTestRailProvider: () => createTestRailProvider,
|
|
92
|
+
createXrayProvider: () => createXrayProvider,
|
|
84
93
|
deriveAudience: () => deriveAudience,
|
|
85
94
|
deriveChangeType: () => deriveChangeType,
|
|
86
95
|
deriveStepResults: () => import_acl8.deriveStepResults,
|
|
@@ -88,6 +97,7 @@ __export(src_exports, {
|
|
|
88
97
|
detectPerformanceTrend: () => detectPerformanceTrend,
|
|
89
98
|
diffRuns: () => diffRuns,
|
|
90
99
|
diffStoryReports: () => diffStoryReports,
|
|
100
|
+
emptyLockfile: () => emptyLockfile,
|
|
91
101
|
findGitDir: () => findGitDir,
|
|
92
102
|
formatDuration: () => import_duration.formatDuration,
|
|
93
103
|
generateRunComparison: () => generateRunComparison,
|
|
@@ -97,7 +107,9 @@ __export(src_exports, {
|
|
|
97
107
|
getEnvironmentDrift: () => getEnvironmentDrift,
|
|
98
108
|
gradeEvidence: () => gradeEvidence,
|
|
99
109
|
hasSufficientHistory: () => hasSufficientHistory,
|
|
110
|
+
hashCaseBody: () => hashCaseBody,
|
|
100
111
|
initialRunState: () => import_executable_stories_core3.initialRunState,
|
|
112
|
+
isProviderName: () => isProviderName,
|
|
101
113
|
isReviewableSource: () => isReviewableSource,
|
|
102
114
|
isTestFile: () => isTestFile,
|
|
103
115
|
joinNameAndExt: () => joinNameAndExt,
|
|
@@ -112,19 +124,26 @@ __export(src_exports, {
|
|
|
112
124
|
normalizeStatus: () => import_acl4.normalizeStatus,
|
|
113
125
|
normalizeVitestResults: () => normalizeVitestResults,
|
|
114
126
|
parseEnvelopes: () => import_ndjson_parser.parseEnvelopes,
|
|
127
|
+
parseLockfile: () => parseLockfile,
|
|
115
128
|
parseNdjson: () => import_ndjson_parser.parseNdjson,
|
|
116
129
|
parseUnifiedDiff: () => parseUnifiedDiff,
|
|
130
|
+
projectBehaviours: () => projectBehaviours,
|
|
117
131
|
publishConfluencePage: () => publishConfluencePage,
|
|
118
132
|
publishJiraIssue: () => publishJiraIssue,
|
|
119
133
|
readBranchName: () => readBranchName,
|
|
120
134
|
readGitSha: () => readGitSha,
|
|
135
|
+
readLockfile: () => readLockfile,
|
|
121
136
|
readPackageVersion: () => readPackageVersion,
|
|
122
137
|
recordDeployment: () => recordDeployment,
|
|
123
138
|
regenerateArtifacts: () => regenerateArtifacts,
|
|
124
139
|
regenerateRun: () => regenerateRun,
|
|
125
140
|
relocateAnchor: () => relocateAnchor,
|
|
141
|
+
renderApplyResult: () => renderApplyResult,
|
|
126
142
|
renderCheck: () => renderCheck,
|
|
143
|
+
renderCoverageMarkdown: () => renderCoverageMarkdown,
|
|
144
|
+
renderCoverageText: () => renderCoverageText,
|
|
127
145
|
renderGoal: () => renderGoal,
|
|
146
|
+
renderPlan: () => renderPlan,
|
|
128
147
|
renderTriage: () => renderTriage,
|
|
129
148
|
resolveAttachment: () => import_acl10.resolveAttachment,
|
|
130
149
|
resolveAttachments: () => import_acl11.resolveAttachments,
|
|
@@ -136,6 +155,7 @@ __export(src_exports, {
|
|
|
136
155
|
sendSlackNotification: () => sendSlackNotification,
|
|
137
156
|
sendTeamsNotification: () => sendTeamsNotification,
|
|
138
157
|
sendWebhookNotification: () => sendWebhookNotification,
|
|
158
|
+
serializeLockfile: () => serializeLockfile,
|
|
139
159
|
signBody: () => signBody,
|
|
140
160
|
slugify: () => import_acl7.slugify,
|
|
141
161
|
startWatch: () => startWatch,
|
|
@@ -143,6 +163,7 @@ __export(src_exports, {
|
|
|
143
163
|
toAgentText: () => toAgentText,
|
|
144
164
|
toBehaviorManifest: () => toBehaviorManifest,
|
|
145
165
|
toCIInfo: () => import_ci.toCIInfo,
|
|
166
|
+
toCaseBody: () => toCaseBody,
|
|
146
167
|
toRawCIInfo: () => import_ci.toRawCIInfo,
|
|
147
168
|
toReleaseManifest: () => toReleaseManifest,
|
|
148
169
|
toScenarioIndex: () => toScenarioIndex,
|
|
@@ -150,10 +171,11 @@ __export(src_exports, {
|
|
|
150
171
|
toTraceabilityMatrix: () => toTraceabilityMatrix,
|
|
151
172
|
tryGetActiveOtelContext: () => import_otel_detect.tryGetActiveOtelContext,
|
|
152
173
|
updateHistory: () => updateHistory,
|
|
153
|
-
validateCanonicalRun: () => import_validate.validateCanonicalRun
|
|
174
|
+
validateCanonicalRun: () => import_validate.validateCanonicalRun,
|
|
175
|
+
writeLockfile: () => writeLockfile
|
|
154
176
|
});
|
|
155
177
|
module.exports = __toCommonJS(src_exports);
|
|
156
|
-
var
|
|
178
|
+
var path10 = __toESM(require("path"), 1);
|
|
157
179
|
var fsPromises = __toESM(require("fs/promises"), 1);
|
|
158
180
|
var import_story_report5 = require("executable-stories-core/converters/story-report");
|
|
159
181
|
|
|
@@ -801,50 +823,50 @@ function scenarioLines(scenario) {
|
|
|
801
823
|
if (scenario.errorMessage) lines.push(indent(`error: ${scenario.errorMessage}`, " "));
|
|
802
824
|
return lines;
|
|
803
825
|
}
|
|
804
|
-
function docLines(entry,
|
|
805
|
-
const lines = ownDocLines(entry,
|
|
806
|
-
for (const child of entry.children ?? []) lines.push(...docLines(child,
|
|
826
|
+
function docLines(entry, pad2) {
|
|
827
|
+
const lines = ownDocLines(entry, pad2);
|
|
828
|
+
for (const child of entry.children ?? []) lines.push(...docLines(child, pad2 + " "));
|
|
807
829
|
return lines;
|
|
808
830
|
}
|
|
809
|
-
function ownDocLines(entry,
|
|
831
|
+
function ownDocLines(entry, pad2) {
|
|
810
832
|
switch (entry.kind) {
|
|
811
833
|
case "note":
|
|
812
|
-
return [indent(entry.text,
|
|
834
|
+
return [indent(entry.text, pad2)];
|
|
813
835
|
case "tag":
|
|
814
836
|
return [];
|
|
815
837
|
// already on the scenario's tag line
|
|
816
838
|
case "kv":
|
|
817
|
-
return [`${
|
|
839
|
+
return [`${pad2}${entry.label}: ${compact(entry.value)}`];
|
|
818
840
|
case "code":
|
|
819
|
-
return [`${
|
|
841
|
+
return [`${pad2}code ${entry.label}${entry.lang ? ` (${entry.lang})` : ""}:`, indent(entry.content, pad2 + " ")];
|
|
820
842
|
case "table":
|
|
821
843
|
return [
|
|
822
|
-
`${
|
|
823
|
-
...entry.rows.map((row) => `${
|
|
844
|
+
`${pad2}table ${entry.label}: ${entry.columns.join(" | ")}`,
|
|
845
|
+
...entry.rows.map((row) => `${pad2} ${row.join(" | ")}`)
|
|
824
846
|
];
|
|
825
847
|
case "link":
|
|
826
|
-
return [`${
|
|
848
|
+
return [`${pad2}link ${entry.label}: ${entry.url}`];
|
|
827
849
|
case "section":
|
|
828
|
-
return [`${
|
|
850
|
+
return [`${pad2}section ${entry.title}:`, indent(entry.markdown, pad2 + " ")];
|
|
829
851
|
case "mermaid":
|
|
830
|
-
return [`${
|
|
852
|
+
return [`${pad2}mermaid${entry.title ? ` ${entry.title}` : ""}:`, indent(entry.code, pad2 + " ")];
|
|
831
853
|
case "screenshot":
|
|
832
|
-
return [`${
|
|
854
|
+
return [`${pad2}screenshot ${entry.path}${entry.alt ? ` \u2014 ${entry.alt}` : ""}`];
|
|
833
855
|
case "video":
|
|
834
|
-
return [`${
|
|
856
|
+
return [`${pad2}video ${entry.path}${entry.caption ? ` \u2014 ${entry.caption}` : ""}`];
|
|
835
857
|
case "html":
|
|
836
|
-
return [`${
|
|
858
|
+
return [`${pad2}html ${entry.title ?? entry.path ?? entry.url ?? "(inline)"}`];
|
|
837
859
|
case "state":
|
|
838
|
-
return [`${
|
|
860
|
+
return [`${pad2}state${entry.label ? ` ${entry.label}` : ""}: ${compact(entry.value)}`];
|
|
839
861
|
case "custom":
|
|
840
|
-
return [`${
|
|
862
|
+
return [`${pad2}custom ${entry.type}: ${compact(entry.data)}`];
|
|
841
863
|
}
|
|
842
864
|
}
|
|
843
865
|
function compact(value) {
|
|
844
866
|
return typeof value === "string" ? value : JSON.stringify(value);
|
|
845
867
|
}
|
|
846
|
-
function indent(text2,
|
|
847
|
-
return text2.split("\n").map((line) =>
|
|
868
|
+
function indent(text2, pad2) {
|
|
869
|
+
return text2.split("\n").map((line) => pad2 + line).join("\n");
|
|
848
870
|
}
|
|
849
871
|
|
|
850
872
|
// src/formatters/junit-xml.ts
|
|
@@ -1747,9 +1769,9 @@ var MAX_FUZZ = 2;
|
|
|
1747
1769
|
var normalize = (line) => line.trim();
|
|
1748
1770
|
var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@ ?(.*)$/;
|
|
1749
1771
|
function stripPathPrefix(raw) {
|
|
1750
|
-
const
|
|
1751
|
-
if (
|
|
1752
|
-
return
|
|
1772
|
+
const path11 = raw.split(" ")[0].trim();
|
|
1773
|
+
if (path11 === "/dev/null") return void 0;
|
|
1774
|
+
return path11.replace(/^[ab]\//, "");
|
|
1753
1775
|
}
|
|
1754
1776
|
function parseUnifiedDiff(patch) {
|
|
1755
1777
|
const files = [];
|
|
@@ -1829,7 +1851,7 @@ function createAnchor(args) {
|
|
|
1829
1851
|
};
|
|
1830
1852
|
}
|
|
1831
1853
|
function changedRunCandidates(anchor, file, fileIndex) {
|
|
1832
|
-
const
|
|
1854
|
+
const path11 = file.newPath ?? file.oldPath ?? "";
|
|
1833
1855
|
const out = [];
|
|
1834
1856
|
file.hunks.forEach((hunk, hunkIndex) => {
|
|
1835
1857
|
outer: for (let i = 0; i + anchor.changed.length <= hunk.lines.length; i++) {
|
|
@@ -1840,7 +1862,7 @@ function changedRunCandidates(anchor, file, fileIndex) {
|
|
|
1840
1862
|
continue outer;
|
|
1841
1863
|
}
|
|
1842
1864
|
}
|
|
1843
|
-
out.push({ fileIndex, file:
|
|
1865
|
+
out.push({ fileIndex, file: path11, hunkIndex, lineIndex: i, lines: hunk.lines });
|
|
1844
1866
|
}
|
|
1845
1867
|
});
|
|
1846
1868
|
return out;
|
|
@@ -1937,18 +1959,18 @@ function deriveChangeType(tags) {
|
|
|
1937
1959
|
}
|
|
1938
1960
|
return "unknown";
|
|
1939
1961
|
}
|
|
1940
|
-
function extensionOf(
|
|
1941
|
-
const base =
|
|
1962
|
+
function extensionOf(path11) {
|
|
1963
|
+
const base = path11.split("/").pop() ?? path11;
|
|
1942
1964
|
const dot = base.lastIndexOf(".");
|
|
1943
1965
|
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
1944
1966
|
}
|
|
1945
|
-
function isTestFile(
|
|
1946
|
-
return TEST_INFIX.test(
|
|
1967
|
+
function isTestFile(path11) {
|
|
1968
|
+
return TEST_INFIX.test(path11);
|
|
1947
1969
|
}
|
|
1948
|
-
function isReviewableSource(
|
|
1949
|
-
if (isTestFile(
|
|
1950
|
-
if (
|
|
1951
|
-
return CODE_EXTENSIONS.has(extensionOf(
|
|
1970
|
+
function isReviewableSource(path11) {
|
|
1971
|
+
if (isTestFile(path11)) return false;
|
|
1972
|
+
if (path11.endsWith(".d.ts")) return false;
|
|
1973
|
+
return CODE_EXTENSIONS.has(extensionOf(path11));
|
|
1952
1974
|
}
|
|
1953
1975
|
function testBaseKey(testFile) {
|
|
1954
1976
|
return testFile.replace(TEST_INFIX, "");
|
|
@@ -2052,7 +2074,7 @@ function toClaim(testCase, changedSourcePaths) {
|
|
|
2052
2074
|
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
2053
2075
|
const key = testBaseKey(testCase.sourceFile);
|
|
2054
2076
|
const coversFiles = changedSourcePaths.filter(
|
|
2055
|
-
(
|
|
2077
|
+
(path11) => sourceBaseKey(path11) === key
|
|
2056
2078
|
);
|
|
2057
2079
|
return {
|
|
2058
2080
|
id: testCase.id,
|
|
@@ -2224,14 +2246,14 @@ var TraceabilityMatrixFormatter = class {
|
|
|
2224
2246
|
lines.push("");
|
|
2225
2247
|
lines.push(`Status: ${renderRequirementStatus(req.status)}`);
|
|
2226
2248
|
if (req.covers.length > 0) {
|
|
2227
|
-
lines.push(`Covers: ${req.covers.map((
|
|
2249
|
+
lines.push(`Covers: ${req.covers.map((path11) => `\`${path11}\``).join(", ")}`);
|
|
2228
2250
|
}
|
|
2229
2251
|
lines.push("");
|
|
2230
2252
|
lines.push("| Status | Scenario | Source | Covers |");
|
|
2231
2253
|
lines.push("| --- | --- | --- | --- |");
|
|
2232
2254
|
for (const scenario of req.scenarios) {
|
|
2233
2255
|
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
2234
|
-
const covers = scenario.covers.length > 0 ? scenario.covers.map((
|
|
2256
|
+
const covers = scenario.covers.length > 0 ? scenario.covers.map((path11) => `\`${path11}\``).join(", ") : "";
|
|
2235
2257
|
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
|
|
2236
2258
|
}
|
|
2237
2259
|
lines.push("");
|
|
@@ -2996,8 +3018,8 @@ function extractDocAttachments(step) {
|
|
|
2996
3018
|
}
|
|
2997
3019
|
return attachments;
|
|
2998
3020
|
}
|
|
2999
|
-
function guessMediaType(
|
|
3000
|
-
const lower =
|
|
3021
|
+
function guessMediaType(path11) {
|
|
3022
|
+
const lower = path11.toLowerCase();
|
|
3001
3023
|
if (lower.endsWith(".png")) return "image/png";
|
|
3002
3024
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
3003
3025
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -3138,11 +3160,11 @@ var CucumberHtmlFormatter = class {
|
|
|
3138
3160
|
for (const envelope of envelopes) {
|
|
3139
3161
|
const accepted = htmlStream.write(envelope);
|
|
3140
3162
|
if (!accepted) {
|
|
3141
|
-
await new Promise((
|
|
3163
|
+
await new Promise((resolve9) => htmlStream.once("drain", resolve9));
|
|
3142
3164
|
}
|
|
3143
3165
|
}
|
|
3144
|
-
await new Promise((
|
|
3145
|
-
collector.on("finish",
|
|
3166
|
+
await new Promise((resolve9, reject) => {
|
|
3167
|
+
collector.on("finish", resolve9);
|
|
3146
3168
|
collector.on("error", reject);
|
|
3147
3169
|
htmlStream.end();
|
|
3148
3170
|
});
|
|
@@ -5643,6 +5665,1395 @@ function convertAttachments(attachments) {
|
|
|
5643
5665
|
|
|
5644
5666
|
// src/index.ts
|
|
5645
5667
|
var import_story = require("executable-stories-core/types/story");
|
|
5668
|
+
|
|
5669
|
+
// src/sync/engine.ts
|
|
5670
|
+
var import_ids3 = require("executable-stories-core/converters/acl/ids");
|
|
5671
|
+
|
|
5672
|
+
// src/sync/lockfile.ts
|
|
5673
|
+
var fs4 = __toESM(require("fs"), 1);
|
|
5674
|
+
var path5 = __toESM(require("path"), 1);
|
|
5675
|
+
var import_node_crypto4 = require("crypto");
|
|
5676
|
+
var DEFAULT_LOCKFILE_PATH = ".executable-stories/sync.lock.json";
|
|
5677
|
+
var LOCKFILE_VERSION = 1;
|
|
5678
|
+
function emptyLockfile() {
|
|
5679
|
+
return { version: LOCKFILE_VERSION, providers: {} };
|
|
5680
|
+
}
|
|
5681
|
+
function hashCaseBody(body) {
|
|
5682
|
+
const canonical = JSON.stringify({
|
|
5683
|
+
title: body.title.trim(),
|
|
5684
|
+
steps: body.steps.map((s) => `${s.keyword.toLowerCase()}:${s.text.trim()}`),
|
|
5685
|
+
description: body.description.trim()
|
|
5686
|
+
});
|
|
5687
|
+
return (0, import_node_crypto4.createHash)("sha1").update(canonical).digest("hex").slice(0, 16);
|
|
5688
|
+
}
|
|
5689
|
+
function parseLockfile(contents, label) {
|
|
5690
|
+
let parsed;
|
|
5691
|
+
try {
|
|
5692
|
+
parsed = JSON.parse(contents);
|
|
5693
|
+
} catch (err) {
|
|
5694
|
+
throw new Error(
|
|
5695
|
+
`Sync lockfile at ${label} is not valid JSON: ${err.message}
|
|
5696
|
+
Fix or delete it \u2014 deleting orphans every existing case binding, so prefer fixing.`
|
|
5697
|
+
);
|
|
5698
|
+
}
|
|
5699
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5700
|
+
throw new Error(`Sync lockfile at ${label} must contain an object.`);
|
|
5701
|
+
}
|
|
5702
|
+
const lock = parsed;
|
|
5703
|
+
if (lock.version !== LOCKFILE_VERSION) {
|
|
5704
|
+
throw new Error(
|
|
5705
|
+
`Sync lockfile at ${label} has version ${String(lock.version)}, expected ${LOCKFILE_VERSION}.`
|
|
5706
|
+
);
|
|
5707
|
+
}
|
|
5708
|
+
return { version: LOCKFILE_VERSION, providers: lock.providers ?? {} };
|
|
5709
|
+
}
|
|
5710
|
+
function serializeLockfile(lock) {
|
|
5711
|
+
const providers = {};
|
|
5712
|
+
for (const provider of Object.keys(lock.providers).sort()) {
|
|
5713
|
+
const entries = lock.providers[provider] ?? {};
|
|
5714
|
+
const sorted = {};
|
|
5715
|
+
for (const key of Object.keys(entries).sort()) sorted[key] = entries[key];
|
|
5716
|
+
providers[provider] = sorted;
|
|
5717
|
+
}
|
|
5718
|
+
return `${JSON.stringify({ version: LOCKFILE_VERSION, providers }, null, 2)}
|
|
5719
|
+
`;
|
|
5720
|
+
}
|
|
5721
|
+
function readLockfile(file) {
|
|
5722
|
+
if (!fs4.existsSync(file)) return emptyLockfile();
|
|
5723
|
+
return parseLockfile(fs4.readFileSync(file, "utf8"), file);
|
|
5724
|
+
}
|
|
5725
|
+
function writeLockfile(file, lock) {
|
|
5726
|
+
fs4.mkdirSync(path5.dirname(path5.resolve(file)), { recursive: true });
|
|
5727
|
+
fs4.writeFileSync(file, serializeLockfile(lock), "utf8");
|
|
5728
|
+
}
|
|
5729
|
+
function entriesFor(lock, provider) {
|
|
5730
|
+
return lock.providers[provider] ?? {};
|
|
5731
|
+
}
|
|
5732
|
+
function setEntry(lock, provider, fingerprint, entry) {
|
|
5733
|
+
lock.providers[provider] ??= {};
|
|
5734
|
+
lock.providers[provider][fingerprint] = entry;
|
|
5735
|
+
}
|
|
5736
|
+
|
|
5737
|
+
// src/sync/engine.ts
|
|
5738
|
+
var DEFAULT_DUPLICATE_THRESHOLD = 0.7;
|
|
5739
|
+
var PARTIAL_RUN_ORPHAN_RATIO = 0.25;
|
|
5740
|
+
function normalizeTitle(text2) {
|
|
5741
|
+
return text2.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
|
|
5742
|
+
}
|
|
5743
|
+
function renderDocs(docs, depth = 0) {
|
|
5744
|
+
if (!docs || docs.length === 0) return "";
|
|
5745
|
+
const lines = [];
|
|
5746
|
+
for (const doc of docs) {
|
|
5747
|
+
switch (doc.kind) {
|
|
5748
|
+
case "note":
|
|
5749
|
+
lines.push(doc.text);
|
|
5750
|
+
break;
|
|
5751
|
+
case "kv":
|
|
5752
|
+
lines.push(`**${doc.label}:** ${formatValue(doc.value)}`);
|
|
5753
|
+
break;
|
|
5754
|
+
case "state":
|
|
5755
|
+
lines.push(`**${doc.label ?? "State"}:** ${formatValue(doc.value)}`);
|
|
5756
|
+
break;
|
|
5757
|
+
case "code":
|
|
5758
|
+
lines.push(`**${doc.label}**`, "", "```" + (doc.lang ?? ""), doc.content, "```");
|
|
5759
|
+
break;
|
|
5760
|
+
case "table":
|
|
5761
|
+
lines.push(
|
|
5762
|
+
`**${doc.label}**`,
|
|
5763
|
+
"",
|
|
5764
|
+
`| ${doc.columns.join(" | ")} |`,
|
|
5765
|
+
`| ${doc.columns.map(() => "---").join(" | ")} |`,
|
|
5766
|
+
...doc.rows.map((row) => `| ${row.join(" | ")} |`)
|
|
5767
|
+
);
|
|
5768
|
+
break;
|
|
5769
|
+
case "link":
|
|
5770
|
+
lines.push(`[${doc.label}](${doc.url})`);
|
|
5771
|
+
break;
|
|
5772
|
+
case "section":
|
|
5773
|
+
lines.push(`${"#".repeat(Math.min(6, depth + 3))} ${doc.title}`, "", doc.markdown);
|
|
5774
|
+
break;
|
|
5775
|
+
case "mermaid":
|
|
5776
|
+
lines.push(...doc.title ? [`**${doc.title}**`, ""] : [], "```mermaid", doc.code, "```");
|
|
5777
|
+
break;
|
|
5778
|
+
case "screenshot":
|
|
5779
|
+
lines.push(`_Screenshot: ${doc.alt ?? doc.path}_`);
|
|
5780
|
+
break;
|
|
5781
|
+
case "video":
|
|
5782
|
+
lines.push(`_Video: ${doc.caption ?? doc.path}_`);
|
|
5783
|
+
break;
|
|
5784
|
+
case "html":
|
|
5785
|
+
lines.push(`_Embedded: ${doc.title ?? doc.url ?? doc.path ?? "html"}_`);
|
|
5786
|
+
break;
|
|
5787
|
+
case "custom":
|
|
5788
|
+
lines.push(`_${doc.type}_: ${formatValue(doc.data)}`);
|
|
5789
|
+
break;
|
|
5790
|
+
case "tag":
|
|
5791
|
+
break;
|
|
5792
|
+
}
|
|
5793
|
+
const children = renderDocs(doc.children, depth + 1);
|
|
5794
|
+
if (children) lines.push(children);
|
|
5795
|
+
}
|
|
5796
|
+
return lines.join("\n");
|
|
5797
|
+
}
|
|
5798
|
+
function formatValue(value) {
|
|
5799
|
+
if (typeof value === "string") return value;
|
|
5800
|
+
return JSON.stringify(value);
|
|
5801
|
+
}
|
|
5802
|
+
function scenarioUrl(config, tc) {
|
|
5803
|
+
if (!config.reportUrl) return void 0;
|
|
5804
|
+
const base = config.reportUrl.replace(/\/$/, "");
|
|
5805
|
+
const anchor = config.scenarioAnchor?.(tc);
|
|
5806
|
+
return anchor ? `${base}#${anchor}` : base;
|
|
5807
|
+
}
|
|
5808
|
+
function toCaseBody(tc, config) {
|
|
5809
|
+
const sections = [];
|
|
5810
|
+
const docs = renderDocs(tc.story.docs);
|
|
5811
|
+
if (docs) sections.push(docs);
|
|
5812
|
+
const tickets = tc.story.tickets ?? [];
|
|
5813
|
+
if (tickets.length > 0) {
|
|
5814
|
+
sections.push(
|
|
5815
|
+
`**Requirements:** ${tickets.map((t) => t.url ? `[${t.id}](${t.url})` : t.id).join(", ")}`
|
|
5816
|
+
);
|
|
5817
|
+
}
|
|
5818
|
+
if (tc.story.covers?.length) {
|
|
5819
|
+
sections.push(`**Covers:** ${tc.story.covers.join(", ")}`);
|
|
5820
|
+
}
|
|
5821
|
+
sections.push(`_Generated from ${tc.sourceFile}:${tc.sourceLine} by executable-stories. Edit the test, not this case._`);
|
|
5822
|
+
const links = [];
|
|
5823
|
+
const report = scenarioUrl(config, tc);
|
|
5824
|
+
if (report) links.push({ label: "Living documentation", url: report });
|
|
5825
|
+
for (const ticket of tickets) {
|
|
5826
|
+
if (ticket.url) links.push({ label: ticket.id, url: ticket.url });
|
|
5827
|
+
}
|
|
5828
|
+
return {
|
|
5829
|
+
title: tc.story.scenario,
|
|
5830
|
+
steps: tc.story.steps.map((s) => ({ keyword: s.keyword, text: s.text })),
|
|
5831
|
+
description: sections.join("\n\n"),
|
|
5832
|
+
links
|
|
5833
|
+
};
|
|
5834
|
+
}
|
|
5835
|
+
function projectBehaviours(run, config) {
|
|
5836
|
+
const fingerprints = run.testCases.map(
|
|
5837
|
+
(tc) => (0, import_ids3.behaviourFingerprint)({
|
|
5838
|
+
scenario: tc.story.scenario,
|
|
5839
|
+
sourceFile: tc.sourceFile,
|
|
5840
|
+
steps: tc.story.steps.map((s) => ({ keyword: s.keyword, text: s.text })),
|
|
5841
|
+
covers: tc.story.covers
|
|
5842
|
+
})
|
|
5843
|
+
);
|
|
5844
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5845
|
+
for (const fp of fingerprints) {
|
|
5846
|
+
if (fp) counts.set(fp, (counts.get(fp) ?? 0) + 1);
|
|
5847
|
+
}
|
|
5848
|
+
return run.testCases.map((tc, index) => {
|
|
5849
|
+
const fp = fingerprints[index];
|
|
5850
|
+
const unique = fp !== "" && counts.get(fp) === 1;
|
|
5851
|
+
return {
|
|
5852
|
+
fingerprint: unique ? fp : tc.id,
|
|
5853
|
+
testCase: tc,
|
|
5854
|
+
body: toCaseBody(tc, config)
|
|
5855
|
+
};
|
|
5856
|
+
});
|
|
5857
|
+
}
|
|
5858
|
+
function roleFor(attachment) {
|
|
5859
|
+
const type = attachment.mediaType.toLowerCase();
|
|
5860
|
+
if (type.startsWith("image/")) return "screenshot";
|
|
5861
|
+
if (type.startsWith("video/")) return "video";
|
|
5862
|
+
if (attachment.name.toLowerCase().includes("trace")) return "trace";
|
|
5863
|
+
return "log";
|
|
5864
|
+
}
|
|
5865
|
+
function decode(attachment) {
|
|
5866
|
+
return attachment.contentEncoding === "BASE64" ? Uint8Array.from(Buffer.from(attachment.body, "base64")) : new TextEncoder().encode(attachment.body);
|
|
5867
|
+
}
|
|
5868
|
+
function collectAttachments(args) {
|
|
5869
|
+
const { testCase, policy, maxBytes } = args;
|
|
5870
|
+
if (policy === "none") return { attachments: [], oversized: [] };
|
|
5871
|
+
if (policy === "failed" && testCase.status !== "failed") return { attachments: [], oversized: [] };
|
|
5872
|
+
const attachments = [];
|
|
5873
|
+
const oversized = [];
|
|
5874
|
+
for (const raw of testCase.attachments) {
|
|
5875
|
+
const body = decode(raw);
|
|
5876
|
+
if (maxBytes !== void 0 && body.byteLength > maxBytes) {
|
|
5877
|
+
oversized.push({ filename: raw.name, bytes: body.byteLength, limit: maxBytes });
|
|
5878
|
+
continue;
|
|
5879
|
+
}
|
|
5880
|
+
attachments.push({
|
|
5881
|
+
filename: raw.name,
|
|
5882
|
+
mediaType: raw.mediaType,
|
|
5883
|
+
body,
|
|
5884
|
+
role: roleFor(raw)
|
|
5885
|
+
});
|
|
5886
|
+
}
|
|
5887
|
+
return { attachments, oversized };
|
|
5888
|
+
}
|
|
5889
|
+
function toCaseResult(args) {
|
|
5890
|
+
const { behaviour, caseId, provider, config } = args;
|
|
5891
|
+
const tc = behaviour.testCase;
|
|
5892
|
+
if (tc.status === "pending") return void 0;
|
|
5893
|
+
const { attachments, oversized } = collectAttachments({
|
|
5894
|
+
testCase: tc,
|
|
5895
|
+
policy: config.attach ?? "failed",
|
|
5896
|
+
maxBytes: provider.maxAttachmentBytes
|
|
5897
|
+
});
|
|
5898
|
+
return {
|
|
5899
|
+
result: {
|
|
5900
|
+
caseId,
|
|
5901
|
+
status: tc.status,
|
|
5902
|
+
durationMs: tc.durationMs,
|
|
5903
|
+
message: tc.errorMessage,
|
|
5904
|
+
url: scenarioUrl(config, tc),
|
|
5905
|
+
attachments: attachments.length > 0 ? attachments : void 0
|
|
5906
|
+
},
|
|
5907
|
+
oversized
|
|
5908
|
+
};
|
|
5909
|
+
}
|
|
5910
|
+
function ticketBinding(tc, config) {
|
|
5911
|
+
const prefix = config.ticketPrefix;
|
|
5912
|
+
if (!prefix) return void 0;
|
|
5913
|
+
const match = (tc.story.tickets ?? []).find((t) => t.id.startsWith(prefix));
|
|
5914
|
+
if (!match) return void 0;
|
|
5915
|
+
return config.ticketPrefixStrip === false ? match.id : match.id.slice(prefix.length);
|
|
5916
|
+
}
|
|
5917
|
+
async function analyzeSync(args) {
|
|
5918
|
+
const { run, provider, lockfile, config } = args;
|
|
5919
|
+
const local = projectBehaviours(run, config);
|
|
5920
|
+
const remoteCases = await provider.listCases();
|
|
5921
|
+
const remoteById = new Map(remoteCases.map((c) => [c.id, c]));
|
|
5922
|
+
const locked = entriesFor(lockfile, provider.name);
|
|
5923
|
+
const create = [];
|
|
5924
|
+
const update = [];
|
|
5925
|
+
const unchanged = [];
|
|
5926
|
+
const adopted = [];
|
|
5927
|
+
const skipped = [];
|
|
5928
|
+
const results = [];
|
|
5929
|
+
const oversized = [];
|
|
5930
|
+
const boundCaseIds = /* @__PURE__ */ new Set();
|
|
5931
|
+
let driftUncheckable = 0;
|
|
5932
|
+
for (const behaviour of local) {
|
|
5933
|
+
const entry = locked[behaviour.fingerprint];
|
|
5934
|
+
const caseId = entry?.caseId ?? ticketBinding(behaviour.testCase, config);
|
|
5935
|
+
const remote2 = caseId ? remoteById.get(caseId) : void 0;
|
|
5936
|
+
if (!caseId) {
|
|
5937
|
+
create.push({
|
|
5938
|
+
fingerprint: behaviour.fingerprint,
|
|
5939
|
+
scenario: behaviour.body.title,
|
|
5940
|
+
body: behaviour.body
|
|
5941
|
+
});
|
|
5942
|
+
continue;
|
|
5943
|
+
}
|
|
5944
|
+
if (!remote2) {
|
|
5945
|
+
skipped.push({
|
|
5946
|
+
fingerprint: behaviour.fingerprint,
|
|
5947
|
+
caseId,
|
|
5948
|
+
url: entry?.url ?? "",
|
|
5949
|
+
title: entry?.title ?? behaviour.body.title,
|
|
5950
|
+
reason: "case-missing"
|
|
5951
|
+
});
|
|
5952
|
+
continue;
|
|
5953
|
+
}
|
|
5954
|
+
boundCaseIds.add(caseId);
|
|
5955
|
+
const pending = toCaseResult({ behaviour, caseId, provider, config });
|
|
5956
|
+
if (pending) {
|
|
5957
|
+
results.push(pending.result);
|
|
5958
|
+
oversized.push(...pending.oversized);
|
|
5959
|
+
}
|
|
5960
|
+
const planned = {
|
|
5961
|
+
fingerprint: behaviour.fingerprint,
|
|
5962
|
+
caseId,
|
|
5963
|
+
url: remote2.url,
|
|
5964
|
+
scenario: behaviour.body.title,
|
|
5965
|
+
body: behaviour.body
|
|
5966
|
+
};
|
|
5967
|
+
if (!entry?.owned) {
|
|
5968
|
+
adopted.push(planned);
|
|
5969
|
+
continue;
|
|
5970
|
+
}
|
|
5971
|
+
const remoteHash = remote2.body ? hashCaseBody(remote2.body) : void 0;
|
|
5972
|
+
if (remoteHash !== void 0 && remoteHash !== entry.hash) {
|
|
5973
|
+
skipped.push({
|
|
5974
|
+
fingerprint: behaviour.fingerprint,
|
|
5975
|
+
caseId,
|
|
5976
|
+
url: remote2.url,
|
|
5977
|
+
title: remote2.title,
|
|
5978
|
+
reason: "remote-edited"
|
|
5979
|
+
});
|
|
5980
|
+
continue;
|
|
5981
|
+
}
|
|
5982
|
+
const baseline = remoteHash ?? entry.hash;
|
|
5983
|
+
if (baseline !== "" && hashCaseBody(behaviour.body) === baseline) {
|
|
5984
|
+
unchanged.push(planned);
|
|
5985
|
+
} else {
|
|
5986
|
+
update.push(planned);
|
|
5987
|
+
}
|
|
5988
|
+
if (remoteHash === void 0) driftUncheckable += 1;
|
|
5989
|
+
}
|
|
5990
|
+
const byNormalizedTitle = /* @__PURE__ */ new Map();
|
|
5991
|
+
for (const behaviour of local) byNormalizedTitle.set(normalizeTitle(behaviour.body.title), behaviour);
|
|
5992
|
+
const threshold = config.duplicateThreshold ?? DEFAULT_DUPLICATE_THRESHOLD;
|
|
5993
|
+
const remote = remoteCases.map((remoteCase) => {
|
|
5994
|
+
if (boundCaseIds.has(remoteCase.id)) {
|
|
5995
|
+
return { case: remoteCase, classification: "automated" };
|
|
5996
|
+
}
|
|
5997
|
+
const titleMatch = byNormalizedTitle.get(normalizeTitle(remoteCase.title));
|
|
5998
|
+
if (titleMatch) {
|
|
5999
|
+
return {
|
|
6000
|
+
case: remoteCase,
|
|
6001
|
+
classification: "duplicated",
|
|
6002
|
+
resembles: titleMatch.body.title
|
|
6003
|
+
};
|
|
6004
|
+
}
|
|
6005
|
+
if (remoteCase.body && remoteCase.body.steps.length > 0) {
|
|
6006
|
+
let best;
|
|
6007
|
+
for (const behaviour of local) {
|
|
6008
|
+
const score = (0, import_ids3.behaviourSimilarity)(
|
|
6009
|
+
{
|
|
6010
|
+
scenario: remoteCase.title,
|
|
6011
|
+
sourceFile: "",
|
|
6012
|
+
steps: remoteCase.body.steps
|
|
6013
|
+
},
|
|
6014
|
+
{
|
|
6015
|
+
scenario: behaviour.body.title,
|
|
6016
|
+
sourceFile: behaviour.testCase.sourceFile,
|
|
6017
|
+
steps: behaviour.body.steps
|
|
6018
|
+
}
|
|
6019
|
+
);
|
|
6020
|
+
if (!best || score > best.score) best = { behaviour, score };
|
|
6021
|
+
}
|
|
6022
|
+
if (best && best.score >= threshold) {
|
|
6023
|
+
return {
|
|
6024
|
+
case: remoteCase,
|
|
6025
|
+
classification: "possible-duplicate",
|
|
6026
|
+
resembles: best.behaviour.body.title,
|
|
6027
|
+
similarity: Number(best.score.toFixed(2))
|
|
6028
|
+
};
|
|
6029
|
+
}
|
|
6030
|
+
}
|
|
6031
|
+
return { case: remoteCase, classification: "manual-only" };
|
|
6032
|
+
});
|
|
6033
|
+
const localFingerprints = new Set(local.map((b) => b.fingerprint));
|
|
6034
|
+
const orphaned = Object.entries(locked).filter(([fingerprint]) => !localFingerprints.has(fingerprint)).map(([fingerprint, entry]) => ({
|
|
6035
|
+
fingerprint,
|
|
6036
|
+
caseId: entry.caseId,
|
|
6037
|
+
url: entry.url,
|
|
6038
|
+
title: entry.title
|
|
6039
|
+
}));
|
|
6040
|
+
const lockedCount = Object.keys(locked).length;
|
|
6041
|
+
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;
|
|
6042
|
+
const unsupported = [];
|
|
6043
|
+
if (create.length > 0 && !provider.createCase) unsupported.push("createCase");
|
|
6044
|
+
if (update.length > 0 && !provider.updateCase) unsupported.push("updateCase");
|
|
6045
|
+
if (results.length > 0 && !provider.recordResults) unsupported.push("recordResults");
|
|
6046
|
+
const byRole = {};
|
|
6047
|
+
let files = 0;
|
|
6048
|
+
let bytes = 0;
|
|
6049
|
+
for (const result of results) {
|
|
6050
|
+
for (const attachment of result.attachments ?? []) {
|
|
6051
|
+
files += 1;
|
|
6052
|
+
bytes += attachment.body.byteLength;
|
|
6053
|
+
const role = attachment.role ?? "log";
|
|
6054
|
+
byRole[role] = (byRole[role] ?? 0) + 1;
|
|
6055
|
+
}
|
|
6056
|
+
}
|
|
6057
|
+
return {
|
|
6058
|
+
provider: provider.name,
|
|
6059
|
+
target: provider.describeTarget?.(),
|
|
6060
|
+
local,
|
|
6061
|
+
remote,
|
|
6062
|
+
create,
|
|
6063
|
+
update,
|
|
6064
|
+
unchanged,
|
|
6065
|
+
adopted,
|
|
6066
|
+
skipped,
|
|
6067
|
+
orphaned,
|
|
6068
|
+
results,
|
|
6069
|
+
attachments: { files, bytes, oversized, byRole },
|
|
6070
|
+
unsupported,
|
|
6071
|
+
driftUncheckable,
|
|
6072
|
+
partialRunWarning
|
|
6073
|
+
};
|
|
6074
|
+
}
|
|
6075
|
+
async function applySync(args, deps) {
|
|
6076
|
+
const { analysis, provider, lockfile, config } = args;
|
|
6077
|
+
const result = {
|
|
6078
|
+
created: [],
|
|
6079
|
+
updated: [],
|
|
6080
|
+
resultsRecorded: 0,
|
|
6081
|
+
resultsSkipped: [],
|
|
6082
|
+
attachmentsUploaded: 0,
|
|
6083
|
+
errors: []
|
|
6084
|
+
};
|
|
6085
|
+
const byFingerprint = new Map(analysis.local.map((b) => [b.fingerprint, b]));
|
|
6086
|
+
const results = [...analysis.results];
|
|
6087
|
+
if (analysis.create.length > 0 && provider.createCase) {
|
|
6088
|
+
for (const planned of analysis.create) {
|
|
6089
|
+
try {
|
|
6090
|
+
const created = await provider.createCase(planned.body);
|
|
6091
|
+
setEntry(lockfile, provider.name, planned.fingerprint, {
|
|
6092
|
+
caseId: created.id,
|
|
6093
|
+
url: created.url,
|
|
6094
|
+
hash: hashCaseBody(created.body ?? planned.body),
|
|
6095
|
+
title: created.title,
|
|
6096
|
+
owned: true
|
|
6097
|
+
});
|
|
6098
|
+
result.created.push({ scenario: planned.scenario, caseId: created.id, url: created.url });
|
|
6099
|
+
const behaviour = byFingerprint.get(planned.fingerprint);
|
|
6100
|
+
if (behaviour) {
|
|
6101
|
+
const pending = toCaseResult({ behaviour, caseId: created.id, provider, config });
|
|
6102
|
+
if (pending) results.push(pending.result);
|
|
6103
|
+
}
|
|
6104
|
+
} catch (err) {
|
|
6105
|
+
result.errors.push(`create "${planned.scenario}": ${err.message}`);
|
|
6106
|
+
}
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
if (analysis.update.length > 0 && provider.updateCase) {
|
|
6110
|
+
for (const planned of analysis.update) {
|
|
6111
|
+
try {
|
|
6112
|
+
const updated = await provider.updateCase(planned.caseId, planned.body);
|
|
6113
|
+
setEntry(lockfile, provider.name, planned.fingerprint, {
|
|
6114
|
+
caseId: updated.id,
|
|
6115
|
+
url: updated.url,
|
|
6116
|
+
hash: hashCaseBody(updated.body ?? planned.body),
|
|
6117
|
+
title: updated.title,
|
|
6118
|
+
owned: true
|
|
6119
|
+
});
|
|
6120
|
+
result.updated.push({ scenario: planned.scenario, caseId: updated.id, url: updated.url });
|
|
6121
|
+
} catch (err) {
|
|
6122
|
+
result.errors.push(`update "${planned.scenario}" (${planned.caseId}): ${err.message}`);
|
|
6123
|
+
}
|
|
6124
|
+
}
|
|
6125
|
+
}
|
|
6126
|
+
for (const planned of analysis.unchanged) {
|
|
6127
|
+
const existing = entriesFor(lockfile, provider.name)[planned.fingerprint];
|
|
6128
|
+
if (existing) {
|
|
6129
|
+
setEntry(lockfile, provider.name, planned.fingerprint, { ...existing, url: planned.url });
|
|
6130
|
+
}
|
|
6131
|
+
}
|
|
6132
|
+
for (const planned of analysis.adopted) {
|
|
6133
|
+
setEntry(lockfile, provider.name, planned.fingerprint, {
|
|
6134
|
+
caseId: planned.caseId,
|
|
6135
|
+
url: planned.url,
|
|
6136
|
+
hash: "",
|
|
6137
|
+
title: planned.scenario,
|
|
6138
|
+
owned: false
|
|
6139
|
+
});
|
|
6140
|
+
}
|
|
6141
|
+
if (results.length > 0 && provider.recordResults) {
|
|
6142
|
+
try {
|
|
6143
|
+
const summary = await provider.recordResults(results);
|
|
6144
|
+
result.resultsRecorded = summary.recorded;
|
|
6145
|
+
result.resultsSkipped = summary.skipped;
|
|
6146
|
+
result.attachmentsUploaded = summary.attachmentsUploaded;
|
|
6147
|
+
result.runUrl = summary.runUrl;
|
|
6148
|
+
} catch (err) {
|
|
6149
|
+
result.errors.push(`record results: ${err.message}`);
|
|
6150
|
+
}
|
|
6151
|
+
} else if (results.length > 0) {
|
|
6152
|
+
deps.logger.warn(
|
|
6153
|
+
`${provider.name} does not support recording results \u2014 ${results.length} execution(s) not pushed.`
|
|
6154
|
+
);
|
|
6155
|
+
}
|
|
6156
|
+
return result;
|
|
6157
|
+
}
|
|
6158
|
+
|
|
6159
|
+
// src/sync/report.ts
|
|
6160
|
+
var MANUAL_ONLY_LIMIT = 50;
|
|
6161
|
+
function summarize2(analysis) {
|
|
6162
|
+
const count2 = (kind) => analysis.remote.filter((c) => c.classification === kind).length;
|
|
6163
|
+
return {
|
|
6164
|
+
provider: analysis.provider,
|
|
6165
|
+
target: analysis.target,
|
|
6166
|
+
totalCases: analysis.remote.length,
|
|
6167
|
+
automated: count2("automated"),
|
|
6168
|
+
duplicated: count2("duplicated"),
|
|
6169
|
+
possibleDuplicate: count2("possible-duplicate"),
|
|
6170
|
+
manualOnly: count2("manual-only"),
|
|
6171
|
+
untracked: analysis.create.length,
|
|
6172
|
+
adopted: analysis.adopted.length
|
|
6173
|
+
};
|
|
6174
|
+
}
|
|
6175
|
+
function sectionBreakdown(analysis) {
|
|
6176
|
+
const sections = /* @__PURE__ */ new Map();
|
|
6177
|
+
for (const entry of analysis.remote) {
|
|
6178
|
+
const name = entry.case.section ?? "(no section)";
|
|
6179
|
+
const bucket2 = sections.get(name) ?? { total: 0, automated: 0 };
|
|
6180
|
+
bucket2.total += 1;
|
|
6181
|
+
if (entry.classification === "automated") bucket2.automated += 1;
|
|
6182
|
+
sections.set(name, bucket2);
|
|
6183
|
+
}
|
|
6184
|
+
return [...sections.entries()].map(([name, value]) => ({ name, ...value })).sort((a, b) => b.automated - a.automated || b.total - a.total);
|
|
6185
|
+
}
|
|
6186
|
+
function renderCoverageText(analysis) {
|
|
6187
|
+
const summary = summarize2(analysis);
|
|
6188
|
+
const lines = [];
|
|
6189
|
+
lines.push(`${analysis.provider}: ${analysis.target ?? "(target not described)"} (${summary.totalCases} cases)`);
|
|
6190
|
+
lines.push("");
|
|
6191
|
+
lines.push(` ${pad(summary.automated)} automated already covered by a story`);
|
|
6192
|
+
lines.push(` ${pad(summary.duplicated)} duplicated manual case duplicates an automated story`);
|
|
6193
|
+
if (summary.possibleDuplicate > 0) {
|
|
6194
|
+
lines.push(` ${pad(summary.possibleDuplicate)} possible dupe similar to a story, needs a human to confirm`);
|
|
6195
|
+
}
|
|
6196
|
+
lines.push(` ${pad(summary.manualOnly)} manual only no automated equivalent`);
|
|
6197
|
+
lines.push(` ${pad(summary.untracked)} untracked story with no case`);
|
|
6198
|
+
if (summary.adopted > 0) {
|
|
6199
|
+
lines.push(` ${pad(summary.adopted)} linked story bound to a hand-authored case`);
|
|
6200
|
+
}
|
|
6201
|
+
const sections = sectionBreakdown(analysis).filter((s) => s.automated > 0);
|
|
6202
|
+
if (sections.length > 0) {
|
|
6203
|
+
const top = sections[0];
|
|
6204
|
+
lines.push("");
|
|
6205
|
+
lines.push(`Biggest overlap: "${top.name}" section, ${top.automated} of ${top.total} automated.`);
|
|
6206
|
+
}
|
|
6207
|
+
if (analysis.orphaned.length > 0) {
|
|
6208
|
+
lines.push("");
|
|
6209
|
+
lines.push(`${analysis.orphaned.length} case(s) bound to a story that no longer exists. Nothing was removed.`);
|
|
6210
|
+
}
|
|
6211
|
+
if (analysis.partialRunWarning) {
|
|
6212
|
+
lines.push("");
|
|
6213
|
+
lines.push(`Note: ${analysis.partialRunWarning}`);
|
|
6214
|
+
}
|
|
6215
|
+
return lines.join("\n");
|
|
6216
|
+
}
|
|
6217
|
+
function pad(value) {
|
|
6218
|
+
return String(value).padStart(4, " ");
|
|
6219
|
+
}
|
|
6220
|
+
function renderCoverageMarkdown(analysis) {
|
|
6221
|
+
const summary = summarize2(analysis);
|
|
6222
|
+
const lines = [];
|
|
6223
|
+
lines.push(`# Test coverage vs ${analysis.provider}`);
|
|
6224
|
+
lines.push("");
|
|
6225
|
+
if (analysis.target) lines.push(`**Target:** ${analysis.target}`, "");
|
|
6226
|
+
lines.push("| Cases | Count | Meaning |");
|
|
6227
|
+
lines.push("| --- | ---: | --- |");
|
|
6228
|
+
lines.push(`| Automated | ${summary.automated} | Already covered by a story |`);
|
|
6229
|
+
lines.push(`| Duplicated | ${summary.duplicated} | Manual case duplicates an automated story |`);
|
|
6230
|
+
lines.push(`| Possible duplicate | ${summary.possibleDuplicate} | Similar to a story, needs review |`);
|
|
6231
|
+
lines.push(`| Manual only | ${summary.manualOnly} | No automated equivalent |`);
|
|
6232
|
+
lines.push(`| Untracked stories | ${summary.untracked} | Story with no case |`);
|
|
6233
|
+
lines.push("");
|
|
6234
|
+
const duplicates = analysis.remote.filter(
|
|
6235
|
+
(c) => c.classification === "duplicated" || c.classification === "possible-duplicate"
|
|
6236
|
+
);
|
|
6237
|
+
if (duplicates.length > 0) {
|
|
6238
|
+
lines.push("## Retire these first");
|
|
6239
|
+
lines.push("");
|
|
6240
|
+
lines.push("Manual cases an automated story already covers.");
|
|
6241
|
+
lines.push("");
|
|
6242
|
+
lines.push("| Case | Title | Covered by | Confidence |");
|
|
6243
|
+
lines.push("| --- | --- | --- | --- |");
|
|
6244
|
+
for (const entry of duplicates) {
|
|
6245
|
+
const confidence = entry.classification === "duplicated" ? "exact title" : `similarity ${entry.similarity}`;
|
|
6246
|
+
lines.push(
|
|
6247
|
+
`| [${entry.case.id}](${entry.case.url}) | ${escapeCell2(entry.case.title)} | ${escapeCell2(entry.resembles ?? "")} | ${confidence} |`
|
|
6248
|
+
);
|
|
6249
|
+
}
|
|
6250
|
+
lines.push("");
|
|
6251
|
+
}
|
|
6252
|
+
const manualOnly = analysis.remote.filter((c) => c.classification === "manual-only");
|
|
6253
|
+
if (manualOnly.length > 0) {
|
|
6254
|
+
lines.push("## Not automated yet");
|
|
6255
|
+
lines.push("");
|
|
6256
|
+
lines.push("Cases with no automated equivalent. This is the backlog.");
|
|
6257
|
+
lines.push("");
|
|
6258
|
+
lines.push("| Case | Title | Section |");
|
|
6259
|
+
lines.push("| --- | --- | --- |");
|
|
6260
|
+
for (const entry of manualOnly.slice(0, MANUAL_ONLY_LIMIT)) {
|
|
6261
|
+
lines.push(
|
|
6262
|
+
`| [${entry.case.id}](${entry.case.url}) | ${escapeCell2(entry.case.title)} | ${escapeCell2(entry.case.section ?? "")} |`
|
|
6263
|
+
);
|
|
6264
|
+
}
|
|
6265
|
+
if (manualOnly.length > MANUAL_ONLY_LIMIT) {
|
|
6266
|
+
lines.push("");
|
|
6267
|
+
lines.push(
|
|
6268
|
+
`_${manualOnly.length - MANUAL_ONLY_LIMIT} more not listed here. The JSON artifact has all ${manualOnly.length}._`
|
|
6269
|
+
);
|
|
6270
|
+
}
|
|
6271
|
+
lines.push("");
|
|
6272
|
+
}
|
|
6273
|
+
if (analysis.create.length > 0) {
|
|
6274
|
+
lines.push("## Stories with no case");
|
|
6275
|
+
lines.push("");
|
|
6276
|
+
for (const planned of analysis.create) lines.push(`- ${planned.scenario}`);
|
|
6277
|
+
lines.push("");
|
|
6278
|
+
}
|
|
6279
|
+
if (analysis.orphaned.length > 0) {
|
|
6280
|
+
lines.push("## Cases with no story");
|
|
6281
|
+
lines.push("");
|
|
6282
|
+
lines.push("Bound to a story that has since been deleted. Nothing was removed automatically.");
|
|
6283
|
+
lines.push("");
|
|
6284
|
+
for (const orphan of analysis.orphaned) {
|
|
6285
|
+
lines.push(`- [${orphan.caseId}](${orphan.url}) ${escapeCell2(orphan.title)}`);
|
|
6286
|
+
}
|
|
6287
|
+
lines.push("");
|
|
6288
|
+
}
|
|
6289
|
+
const sections = sectionBreakdown(analysis);
|
|
6290
|
+
if (sections.length > 1) {
|
|
6291
|
+
lines.push("## By section");
|
|
6292
|
+
lines.push("");
|
|
6293
|
+
lines.push("| Section | Automated | Total |");
|
|
6294
|
+
lines.push("| --- | ---: | ---: |");
|
|
6295
|
+
for (const section of sections) {
|
|
6296
|
+
lines.push(`| ${escapeCell2(section.name)} | ${section.automated} | ${section.total} |`);
|
|
6297
|
+
}
|
|
6298
|
+
lines.push("");
|
|
6299
|
+
}
|
|
6300
|
+
if (analysis.partialRunWarning) {
|
|
6301
|
+
lines.push(`> ${analysis.partialRunWarning}`, "");
|
|
6302
|
+
}
|
|
6303
|
+
return lines.join("\n");
|
|
6304
|
+
}
|
|
6305
|
+
function escapeCell2(text2) {
|
|
6306
|
+
return text2.replace(/\|/g, "\\|");
|
|
6307
|
+
}
|
|
6308
|
+
function buildCoverageJson(analysis) {
|
|
6309
|
+
return {
|
|
6310
|
+
schema: "executable-stories/sync-coverage/v1",
|
|
6311
|
+
...summarize2(analysis),
|
|
6312
|
+
cases: analysis.remote.map((entry) => ({
|
|
6313
|
+
id: entry.case.id,
|
|
6314
|
+
url: entry.case.url,
|
|
6315
|
+
title: entry.case.title,
|
|
6316
|
+
section: entry.case.section,
|
|
6317
|
+
classification: entry.classification,
|
|
6318
|
+
resembles: entry.resembles,
|
|
6319
|
+
similarity: entry.similarity
|
|
6320
|
+
})),
|
|
6321
|
+
untrackedScenarios: analysis.create.map((c) => c.scenario),
|
|
6322
|
+
orphaned: analysis.orphaned.map((o) => ({ caseId: o.caseId, url: o.url, title: o.title })),
|
|
6323
|
+
sections: sectionBreakdown(analysis)
|
|
6324
|
+
};
|
|
6325
|
+
}
|
|
6326
|
+
function formatBytes(bytes) {
|
|
6327
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
6328
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
6329
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
6330
|
+
}
|
|
6331
|
+
function renderPlan(analysis, opts) {
|
|
6332
|
+
const lines = [];
|
|
6333
|
+
lines.push(`${analysis.provider}: ${analysis.target ?? "(target not described)"}`);
|
|
6334
|
+
lines.push("");
|
|
6335
|
+
lines.push(` + create ${pad(analysis.create.length)} cases`);
|
|
6336
|
+
lines.push(` ~ update ${pad(analysis.update.length)} cases`);
|
|
6337
|
+
lines.push(` = unchanged ${pad(analysis.unchanged.length)} cases`);
|
|
6338
|
+
if (analysis.adopted.length > 0) {
|
|
6339
|
+
lines.push(` \xB7 linked ${pad(analysis.adopted.length)} cases (hand-authored, results only)`);
|
|
6340
|
+
}
|
|
6341
|
+
if (analysis.skipped.length > 0) {
|
|
6342
|
+
const edited = analysis.skipped.filter((s) => s.reason === "remote-edited").length;
|
|
6343
|
+
const missing = analysis.skipped.filter((s) => s.reason === "case-missing").length;
|
|
6344
|
+
if (edited > 0) {
|
|
6345
|
+
lines.push(` ! skipped ${pad(edited)} cases (edited in ${analysis.provider} since last sync)`);
|
|
6346
|
+
}
|
|
6347
|
+
if (missing > 0) {
|
|
6348
|
+
lines.push(` ! skipped ${pad(missing)} cases (bound case no longer exists)`);
|
|
6349
|
+
}
|
|
6350
|
+
}
|
|
6351
|
+
if (analysis.orphaned.length > 0) {
|
|
6352
|
+
lines.push(` ? orphaned ${pad(analysis.orphaned.length)} cases (story deleted from codebase, never removed)`);
|
|
6353
|
+
}
|
|
6354
|
+
lines.push(` \u2192 results ${pad(analysis.results.length)} executions`);
|
|
6355
|
+
if (analysis.attachments.files > 0) {
|
|
6356
|
+
const roles = Object.entries(analysis.attachments.byRole).map(([role, count2]) => `${count2} ${role}${count2 === 1 ? "" : "s"}`).join(", ");
|
|
6357
|
+
lines.push(
|
|
6358
|
+
` \u2191 upload ${pad(analysis.attachments.files)} attachments (${roles}, ${formatBytes(analysis.attachments.bytes)})`
|
|
6359
|
+
);
|
|
6360
|
+
}
|
|
6361
|
+
for (const oversized of analysis.attachments.oversized) {
|
|
6362
|
+
lines.push(
|
|
6363
|
+
` ! oversized ${oversized.filename} (${formatBytes(oversized.bytes)}, provider limit ${formatBytes(oversized.limit)})`
|
|
6364
|
+
);
|
|
6365
|
+
}
|
|
6366
|
+
for (const capability of analysis.unsupported) {
|
|
6367
|
+
lines.push(` ! ${analysis.provider} does not support ${capability} \u2014 those changes are not applied`);
|
|
6368
|
+
}
|
|
6369
|
+
if (analysis.driftUncheckable > 0) {
|
|
6370
|
+
lines.push(
|
|
6371
|
+
` ! ${analysis.driftUncheckable} case(s): ${analysis.provider} returned no body, so a hand edit to them cannot be detected`
|
|
6372
|
+
);
|
|
6373
|
+
}
|
|
6374
|
+
if (analysis.partialRunWarning) {
|
|
6375
|
+
lines.push("");
|
|
6376
|
+
lines.push(`Note: ${analysis.partialRunWarning}`);
|
|
6377
|
+
}
|
|
6378
|
+
if (analysis.skipped.some((s) => s.reason === "remote-edited")) {
|
|
6379
|
+
lines.push("");
|
|
6380
|
+
lines.push("Skipped cases were edited by hand after we last wrote them. Nothing overwrites them.");
|
|
6381
|
+
for (const skip of analysis.skipped.filter((s) => s.reason === "remote-edited")) {
|
|
6382
|
+
lines.push(` ${skip.caseId} ${skip.title} ${skip.url}`);
|
|
6383
|
+
}
|
|
6384
|
+
}
|
|
6385
|
+
if (opts.dryRun && hasWork(analysis)) {
|
|
6386
|
+
lines.push("");
|
|
6387
|
+
lines.push("Nothing was written. Run the same command with --apply to make these changes.");
|
|
6388
|
+
}
|
|
6389
|
+
return lines.join("\n");
|
|
6390
|
+
}
|
|
6391
|
+
function hasWork(analysis) {
|
|
6392
|
+
return analysis.create.length > 0 || analysis.update.length > 0 || analysis.results.length > 0;
|
|
6393
|
+
}
|
|
6394
|
+
function renderApplyResult(result) {
|
|
6395
|
+
const lines = [];
|
|
6396
|
+
for (const created of result.created) {
|
|
6397
|
+
lines.push(` + ${created.caseId} ${created.scenario} ${created.url}`);
|
|
6398
|
+
}
|
|
6399
|
+
for (const updated of result.updated) {
|
|
6400
|
+
lines.push(` ~ ${updated.caseId} ${updated.scenario}`);
|
|
6401
|
+
}
|
|
6402
|
+
lines.push("");
|
|
6403
|
+
lines.push(
|
|
6404
|
+
`Created ${result.created.length}, updated ${result.updated.length}, recorded ${result.resultsRecorded} execution(s), uploaded ${result.attachmentsUploaded} attachment(s).`
|
|
6405
|
+
);
|
|
6406
|
+
if (result.runUrl) lines.push(`Run: ${result.runUrl}`);
|
|
6407
|
+
for (const skipped of result.resultsSkipped) {
|
|
6408
|
+
lines.push(` ! result for case ${skipped.caseId} not recorded: ${skipped.reason}`);
|
|
6409
|
+
}
|
|
6410
|
+
for (const error of result.errors) {
|
|
6411
|
+
lines.push(` \u2717 ${error}`);
|
|
6412
|
+
}
|
|
6413
|
+
return lines.join("\n");
|
|
6414
|
+
}
|
|
6415
|
+
|
|
6416
|
+
// src/sync/adapters/case-text.ts
|
|
6417
|
+
var BDD_KEYWORDS = ["Given", "When", "Then", "And", "But"];
|
|
6418
|
+
function encodeStepText(step) {
|
|
6419
|
+
const keyword = step.keyword.trim();
|
|
6420
|
+
return keyword ? `${keyword} ${step.text}` : step.text;
|
|
6421
|
+
}
|
|
6422
|
+
function decodeStepText(content) {
|
|
6423
|
+
const trimmed = content.trim();
|
|
6424
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
6425
|
+
if (firstSpace > 0) {
|
|
6426
|
+
const head = trimmed.slice(0, firstSpace);
|
|
6427
|
+
if (BDD_KEYWORDS.some((keyword) => keyword.toLowerCase() === head.toLowerCase())) {
|
|
6428
|
+
return { keyword: head, text: trimmed.slice(firstSpace + 1) };
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
6431
|
+
return { keyword: "", text: trimmed };
|
|
6432
|
+
}
|
|
6433
|
+
function encodeDescription(body) {
|
|
6434
|
+
if (body.links.length === 0) return body.description;
|
|
6435
|
+
const links = body.links.map((link2) => `- [${link2.label}](${link2.url})`).join("\n");
|
|
6436
|
+
return `${body.description}
|
|
6437
|
+
|
|
6438
|
+
${links}`;
|
|
6439
|
+
}
|
|
6440
|
+
function decodeDescription(raw) {
|
|
6441
|
+
const links = [];
|
|
6442
|
+
const lines = raw.split("\n");
|
|
6443
|
+
let cut = lines.length;
|
|
6444
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
6445
|
+
const line = lines[index].trim();
|
|
6446
|
+
if (line === "") continue;
|
|
6447
|
+
const match = /^- \[([^\]]+)\]\(([^)]+)\)$/.exec(line);
|
|
6448
|
+
if (!match) break;
|
|
6449
|
+
links.unshift({ label: match[1], url: match[2] });
|
|
6450
|
+
cut = index;
|
|
6451
|
+
}
|
|
6452
|
+
return { description: lines.slice(0, cut).join("\n").trim(), links };
|
|
6453
|
+
}
|
|
6454
|
+
|
|
6455
|
+
// src/sync/adapters/testrail.ts
|
|
6456
|
+
var DEFAULT_STEPS_FIELD = "custom_steps_separated";
|
|
6457
|
+
var DEFAULT_DESCRIPTION_FIELD = "custom_preconds";
|
|
6458
|
+
var DEFAULT_MAX_ATTACHMENT_BYTES = 64 * 1024 * 1024;
|
|
6459
|
+
var PAGE_LIMIT = 250;
|
|
6460
|
+
var MAX_ATTEMPTS = 3;
|
|
6461
|
+
function unwrapList(payload, key) {
|
|
6462
|
+
if (Array.isArray(payload)) return payload;
|
|
6463
|
+
if (payload && typeof payload === "object") {
|
|
6464
|
+
const list = payload[key];
|
|
6465
|
+
if (Array.isArray(list)) return list;
|
|
6466
|
+
}
|
|
6467
|
+
return [];
|
|
6468
|
+
}
|
|
6469
|
+
function encodeElapsed(durationMs) {
|
|
6470
|
+
const seconds = Math.floor(durationMs / 1e3);
|
|
6471
|
+
if (seconds < 1) return void 0;
|
|
6472
|
+
if (seconds < 60) return `${seconds}s`;
|
|
6473
|
+
const minutes = Math.floor(seconds / 60);
|
|
6474
|
+
const rest = seconds % 60;
|
|
6475
|
+
return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
|
|
6476
|
+
}
|
|
6477
|
+
function authHint(status) {
|
|
6478
|
+
if (status === 401) {
|
|
6479
|
+
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.";
|
|
6480
|
+
}
|
|
6481
|
+
if (status === 403) {
|
|
6482
|
+
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.";
|
|
6483
|
+
}
|
|
6484
|
+
return "";
|
|
6485
|
+
}
|
|
6486
|
+
function createTestRailProvider(config, auth, deps) {
|
|
6487
|
+
const base = config.url.replace(/\/$/, "");
|
|
6488
|
+
const stepsField = config.fields?.steps ?? DEFAULT_STEPS_FIELD;
|
|
6489
|
+
const descriptionField = config.fields?.description ?? DEFAULT_DESCRIPTION_FIELD;
|
|
6490
|
+
const basicAuth = Buffer.from(`${auth.username}:${auth.apiKey}`).toString("base64");
|
|
6491
|
+
let projectName;
|
|
6492
|
+
let suiteName;
|
|
6493
|
+
const sectionNames = /* @__PURE__ */ new Map();
|
|
6494
|
+
async function api(method, init) {
|
|
6495
|
+
const url = `${base}/index.php?/api/v2/${method}`;
|
|
6496
|
+
const headers = {
|
|
6497
|
+
Authorization: `Basic ${basicAuth}`
|
|
6498
|
+
};
|
|
6499
|
+
let body;
|
|
6500
|
+
if (init?.form) {
|
|
6501
|
+
body = init.form;
|
|
6502
|
+
} else if (init?.body !== void 0) {
|
|
6503
|
+
headers["Content-Type"] = "application/json";
|
|
6504
|
+
body = JSON.stringify(init.body);
|
|
6505
|
+
}
|
|
6506
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
6507
|
+
const response = await deps.fetch(url, {
|
|
6508
|
+
method: body === void 0 ? "GET" : "POST",
|
|
6509
|
+
headers,
|
|
6510
|
+
body
|
|
6511
|
+
});
|
|
6512
|
+
if (response.status === 429 && attempt < MAX_ATTEMPTS - 1) {
|
|
6513
|
+
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
|
|
6514
|
+
deps.logger.warn(`TestRail rate limit hit, retrying in ${retryAfter}s`);
|
|
6515
|
+
await new Promise((resolve9) => setTimeout(resolve9, Math.max(1, retryAfter) * 1e3));
|
|
6516
|
+
continue;
|
|
6517
|
+
}
|
|
6518
|
+
const text2 = await response.text();
|
|
6519
|
+
if (text2.length === 0) {
|
|
6520
|
+
if (response.ok) return void 0;
|
|
6521
|
+
throw new Error(
|
|
6522
|
+
`TestRail ${method} failed (${response.status}) with an empty response${authHint(response.status)}`
|
|
6523
|
+
);
|
|
6524
|
+
}
|
|
6525
|
+
let parsed;
|
|
6526
|
+
try {
|
|
6527
|
+
parsed = JSON.parse(text2);
|
|
6528
|
+
} catch {
|
|
6529
|
+
throw new Error(
|
|
6530
|
+
`TestRail ${method} returned HTML rather than JSON (status ${response.status}).
|
|
6531
|
+
Check that sync.testrail.url is the instance root, e.g. https://acme.testrail.io, with no path after it.`
|
|
6532
|
+
);
|
|
6533
|
+
}
|
|
6534
|
+
if (!response.ok) {
|
|
6535
|
+
const detail = parsed.error ?? text2;
|
|
6536
|
+
throw new Error(
|
|
6537
|
+
`TestRail ${method} failed (${response.status}): ${detail}${authHint(response.status)}`
|
|
6538
|
+
);
|
|
6539
|
+
}
|
|
6540
|
+
return parsed;
|
|
6541
|
+
}
|
|
6542
|
+
throw new Error(`TestRail ${method} failed: rate limited after ${MAX_ATTEMPTS} attempts`);
|
|
6543
|
+
}
|
|
6544
|
+
async function paginate(method, key) {
|
|
6545
|
+
const all = [];
|
|
6546
|
+
for (let offset = 0; ; offset += PAGE_LIMIT) {
|
|
6547
|
+
const page = await api(`${method}&limit=${PAGE_LIMIT}&offset=${offset}`);
|
|
6548
|
+
const items = unwrapList(page, key);
|
|
6549
|
+
all.push(...items);
|
|
6550
|
+
if (items.length < PAGE_LIMIT) return all;
|
|
6551
|
+
}
|
|
6552
|
+
}
|
|
6553
|
+
function caseUrl(id) {
|
|
6554
|
+
return `${base}/index.php?/cases/view/${id}`;
|
|
6555
|
+
}
|
|
6556
|
+
function toRemoteCase(raw, context) {
|
|
6557
|
+
if (raw?.id === void 0) {
|
|
6558
|
+
throw new Error(
|
|
6559
|
+
`TestRail ${context} returned no case id. The response was: ${JSON.stringify(raw)?.slice(0, 200)}`
|
|
6560
|
+
);
|
|
6561
|
+
}
|
|
6562
|
+
const title = typeof raw.title === "string" ? raw.title : "";
|
|
6563
|
+
const steps = Array.isArray(raw[stepsField]) ? raw[stepsField] : [];
|
|
6564
|
+
const rawDescription = typeof raw[descriptionField] === "string" ? raw[descriptionField] : "";
|
|
6565
|
+
const { description, links } = decodeDescription(rawDescription);
|
|
6566
|
+
return {
|
|
6567
|
+
id: String(raw.id),
|
|
6568
|
+
url: caseUrl(raw.id),
|
|
6569
|
+
title,
|
|
6570
|
+
section: raw.section_id === void 0 ? void 0 : sectionNames.get(raw.section_id),
|
|
6571
|
+
body: {
|
|
6572
|
+
title,
|
|
6573
|
+
steps: steps.map((step) => decodeStepText(step.content ?? "")),
|
|
6574
|
+
description,
|
|
6575
|
+
links
|
|
6576
|
+
}
|
|
6577
|
+
};
|
|
6578
|
+
}
|
|
6579
|
+
function bodyToPayload(body) {
|
|
6580
|
+
const payload = {
|
|
6581
|
+
title: body.title,
|
|
6582
|
+
[stepsField]: body.steps.map((step) => ({ content: encodeStepText(step), expected: "" })),
|
|
6583
|
+
[descriptionField]: encodeDescription(body)
|
|
6584
|
+
};
|
|
6585
|
+
if (config.templateId !== void 0) payload["template_id"] = config.templateId;
|
|
6586
|
+
return payload;
|
|
6587
|
+
}
|
|
6588
|
+
const suiteQuery = config.suiteId === void 0 ? "" : `&suite_id=${config.suiteId}`;
|
|
6589
|
+
return {
|
|
6590
|
+
name: "testrail",
|
|
6591
|
+
maxAttachmentBytes: config.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES,
|
|
6592
|
+
describeTarget() {
|
|
6593
|
+
const project = projectName ?? `project ${config.projectId}`;
|
|
6594
|
+
const suite = suiteName ?? (config.suiteId === void 0 ? void 0 : `suite ${config.suiteId}`);
|
|
6595
|
+
return suite ? `${project} / ${suite}` : project;
|
|
6596
|
+
},
|
|
6597
|
+
async listCases() {
|
|
6598
|
+
try {
|
|
6599
|
+
const project = await api(`get_project/${config.projectId}`);
|
|
6600
|
+
projectName = project?.name;
|
|
6601
|
+
if (config.suiteId !== void 0) {
|
|
6602
|
+
const suite = await api(`get_suite/${config.suiteId}`);
|
|
6603
|
+
suiteName = suite?.name;
|
|
6604
|
+
}
|
|
6605
|
+
const sections = await paginate(
|
|
6606
|
+
`get_sections/${config.projectId}${suiteQuery}`,
|
|
6607
|
+
"sections"
|
|
6608
|
+
);
|
|
6609
|
+
for (const section of sections) sectionNames.set(section.id, section.name);
|
|
6610
|
+
} catch (err) {
|
|
6611
|
+
deps.logger.warn(`TestRail metadata lookup failed, continuing without names: ${err.message}`);
|
|
6612
|
+
}
|
|
6613
|
+
const cases = await paginate(`get_cases/${config.projectId}${suiteQuery}`, "cases");
|
|
6614
|
+
return cases.map((raw) => toRemoteCase(raw, "get_cases"));
|
|
6615
|
+
},
|
|
6616
|
+
async createCase(body) {
|
|
6617
|
+
if (config.sectionId === void 0) {
|
|
6618
|
+
throw new Error(
|
|
6619
|
+
"TestRail needs a sectionId to create cases. Set sync.testrail.sectionId to the section new cases should land in."
|
|
6620
|
+
);
|
|
6621
|
+
}
|
|
6622
|
+
const created = await api(`add_case/${config.sectionId}`, {
|
|
6623
|
+
body: bodyToPayload(body)
|
|
6624
|
+
});
|
|
6625
|
+
return toRemoteCase(created, `add_case/${config.sectionId}`);
|
|
6626
|
+
},
|
|
6627
|
+
async updateCase(id, body) {
|
|
6628
|
+
const updated = await api(`update_case/${id}`, { body: bodyToPayload(body) });
|
|
6629
|
+
return toRemoteCase(updated, `update_case/${id}`);
|
|
6630
|
+
},
|
|
6631
|
+
async recordResults(results) {
|
|
6632
|
+
const statusIds = {
|
|
6633
|
+
passed: config.statusIds?.passed ?? 1,
|
|
6634
|
+
failed: config.statusIds?.failed ?? 5,
|
|
6635
|
+
skipped: config.statusIds?.skipped
|
|
6636
|
+
};
|
|
6637
|
+
const skipped = [];
|
|
6638
|
+
const sendable = [];
|
|
6639
|
+
for (const result of results) {
|
|
6640
|
+
const caseId = Number(result.caseId);
|
|
6641
|
+
if (!Number.isFinite(caseId)) {
|
|
6642
|
+
skipped.push({ caseId: result.caseId, reason: "case id is not numeric" });
|
|
6643
|
+
continue;
|
|
6644
|
+
}
|
|
6645
|
+
const statusId = result.status === "skipped" ? statusIds.skipped : statusIds[result.status];
|
|
6646
|
+
if (statusId === void 0) {
|
|
6647
|
+
skipped.push({
|
|
6648
|
+
caseId: result.caseId,
|
|
6649
|
+
reason: "no TestRail status id configured for skipped (set sync.testrail.statusIds.skipped)"
|
|
6650
|
+
});
|
|
6651
|
+
continue;
|
|
6652
|
+
}
|
|
6653
|
+
sendable.push({ result, caseId, statusId });
|
|
6654
|
+
}
|
|
6655
|
+
if (sendable.length === 0) {
|
|
6656
|
+
return { recorded: 0, skipped, attachmentsUploaded: 0 };
|
|
6657
|
+
}
|
|
6658
|
+
let runId = config.runId;
|
|
6659
|
+
if (runId === void 0) {
|
|
6660
|
+
const name = `${config.runName ?? "executable-stories"} ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
6661
|
+
const run = await api(`add_run/${config.projectId}`, {
|
|
6662
|
+
body: {
|
|
6663
|
+
...config.suiteId === void 0 ? {} : { suite_id: config.suiteId },
|
|
6664
|
+
name,
|
|
6665
|
+
include_all: false,
|
|
6666
|
+
// Deduplicated: two stories can carry the same ticket id, and
|
|
6667
|
+
// TestRail rejects a run whose case list repeats one.
|
|
6668
|
+
case_ids: [...new Set(sendable.map((s) => s.caseId))]
|
|
6669
|
+
}
|
|
6670
|
+
});
|
|
6671
|
+
runId = run.id;
|
|
6672
|
+
}
|
|
6673
|
+
const runUrl = `${base}/index.php?/runs/view/${runId}`;
|
|
6674
|
+
const payload = sendable.map(({ result, caseId, statusId }) => {
|
|
6675
|
+
const elapsed = encodeElapsed(result.durationMs);
|
|
6676
|
+
const comment = [result.message, result.url ? `Living documentation: ${result.url}` : void 0].filter(Boolean).join("\n\n");
|
|
6677
|
+
return {
|
|
6678
|
+
case_id: caseId,
|
|
6679
|
+
status_id: statusId,
|
|
6680
|
+
...comment ? { comment } : {},
|
|
6681
|
+
...elapsed ? { elapsed } : {}
|
|
6682
|
+
};
|
|
6683
|
+
});
|
|
6684
|
+
const recorded = unwrapList(
|
|
6685
|
+
await api(`add_results_for_cases/${runId}`, { body: { results: payload } }),
|
|
6686
|
+
"results"
|
|
6687
|
+
);
|
|
6688
|
+
let attachmentsUploaded = 0;
|
|
6689
|
+
for (const [index, entry] of recorded.entries()) {
|
|
6690
|
+
const source = sendable[index]?.result;
|
|
6691
|
+
for (const attachment of source?.attachments ?? []) {
|
|
6692
|
+
try {
|
|
6693
|
+
const form = new FormData();
|
|
6694
|
+
const bytes = attachment.body.buffer.slice(
|
|
6695
|
+
attachment.body.byteOffset,
|
|
6696
|
+
attachment.body.byteOffset + attachment.body.byteLength
|
|
6697
|
+
);
|
|
6698
|
+
form.append(
|
|
6699
|
+
"attachment",
|
|
6700
|
+
new Blob([bytes], { type: attachment.mediaType }),
|
|
6701
|
+
attachment.filename
|
|
6702
|
+
);
|
|
6703
|
+
await api(`add_attachment_to_result/${entry.id}`, { form });
|
|
6704
|
+
attachmentsUploaded += 1;
|
|
6705
|
+
} catch (err) {
|
|
6706
|
+
deps.logger.warn(
|
|
6707
|
+
`TestRail attachment "${attachment.filename}" failed: ${err.message}`
|
|
6708
|
+
);
|
|
6709
|
+
}
|
|
6710
|
+
}
|
|
6711
|
+
}
|
|
6712
|
+
if (config.closeRun && config.runId === void 0) {
|
|
6713
|
+
await api(`close_run/${runId}`, { body: {} });
|
|
6714
|
+
}
|
|
6715
|
+
return {
|
|
6716
|
+
runId: String(runId),
|
|
6717
|
+
runUrl,
|
|
6718
|
+
recorded: recorded.length,
|
|
6719
|
+
skipped,
|
|
6720
|
+
attachmentsUploaded
|
|
6721
|
+
};
|
|
6722
|
+
}
|
|
6723
|
+
};
|
|
6724
|
+
}
|
|
6725
|
+
|
|
6726
|
+
// src/sync/adapters/xray.ts
|
|
6727
|
+
var DEFAULT_XRAY_BASE = "https://xray.cloud.getxray.app";
|
|
6728
|
+
var DEFAULT_TEST_TYPE = "Manual";
|
|
6729
|
+
var DEFAULT_MAX_ATTACHMENT_BYTES2 = 32 * 1024 * 1024;
|
|
6730
|
+
var PAGE_LIMIT2 = 100;
|
|
6731
|
+
function toAdf(text2) {
|
|
6732
|
+
const paragraphs = text2.split("\n\n").filter((block) => block.trim() !== "");
|
|
6733
|
+
return {
|
|
6734
|
+
version: 1,
|
|
6735
|
+
type: "doc",
|
|
6736
|
+
content: paragraphs.length === 0 ? [{ type: "paragraph", content: [] }] : paragraphs.map((block) => ({
|
|
6737
|
+
type: "paragraph",
|
|
6738
|
+
content: [{ type: "text", text: block }]
|
|
6739
|
+
}))
|
|
6740
|
+
};
|
|
6741
|
+
}
|
|
6742
|
+
function fromAdf(value) {
|
|
6743
|
+
if (typeof value === "string") return value;
|
|
6744
|
+
if (!value || typeof value !== "object") return "";
|
|
6745
|
+
const blocks = [];
|
|
6746
|
+
const walk = (node, collected) => {
|
|
6747
|
+
if (!node || typeof node !== "object") return;
|
|
6748
|
+
const typed = node;
|
|
6749
|
+
if (typed.type === "text" && typeof typed.text === "string") collected.push(typed.text);
|
|
6750
|
+
for (const child of typed.content ?? []) walk(child, collected);
|
|
6751
|
+
};
|
|
6752
|
+
for (const node of value.content ?? []) {
|
|
6753
|
+
const collected = [];
|
|
6754
|
+
walk(node, collected);
|
|
6755
|
+
blocks.push(collected.join(""));
|
|
6756
|
+
}
|
|
6757
|
+
return blocks.join("\n\n").trim();
|
|
6758
|
+
}
|
|
6759
|
+
function createXrayProvider(config, auth, deps) {
|
|
6760
|
+
const xrayBase = (config.xrayBaseUrl ?? DEFAULT_XRAY_BASE).replace(/\/$/, "");
|
|
6761
|
+
const jiraBase = config.jiraBaseUrl.replace(/\/$/, "");
|
|
6762
|
+
const jql = config.jql ?? `project = "${config.projectKey}" AND issuetype = Test`;
|
|
6763
|
+
const issueIds = /* @__PURE__ */ new Map();
|
|
6764
|
+
let token;
|
|
6765
|
+
async function authenticate() {
|
|
6766
|
+
if (token) return token;
|
|
6767
|
+
const response = await deps.fetch(`${xrayBase}/api/v2/authenticate`, {
|
|
6768
|
+
method: "POST",
|
|
6769
|
+
headers: { "Content-Type": "application/json" },
|
|
6770
|
+
body: JSON.stringify({ client_id: auth.clientId, client_secret: auth.clientSecret })
|
|
6771
|
+
});
|
|
6772
|
+
const text2 = await response.text();
|
|
6773
|
+
if (!response.ok) {
|
|
6774
|
+
throw new Error(
|
|
6775
|
+
`Xray authentication failed (${response.status}): ${text2}
|
|
6776
|
+
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.`
|
|
6777
|
+
);
|
|
6778
|
+
}
|
|
6779
|
+
token = JSON.parse(text2).replace(/^"|"$/g, "");
|
|
6780
|
+
return token;
|
|
6781
|
+
}
|
|
6782
|
+
async function graphql(query, variables) {
|
|
6783
|
+
const bearer = await authenticate();
|
|
6784
|
+
const response = await deps.fetch(`${xrayBase}/api/v2/graphql`, {
|
|
6785
|
+
method: "POST",
|
|
6786
|
+
headers: {
|
|
6787
|
+
Authorization: `Bearer ${bearer}`,
|
|
6788
|
+
"Content-Type": "application/json"
|
|
6789
|
+
},
|
|
6790
|
+
body: JSON.stringify({ query, variables })
|
|
6791
|
+
});
|
|
6792
|
+
const text2 = await response.text();
|
|
6793
|
+
if (!response.ok) throw new Error(`Xray GraphQL failed (${response.status}): ${text2}`);
|
|
6794
|
+
const payload = JSON.parse(text2);
|
|
6795
|
+
if (payload.errors?.length) {
|
|
6796
|
+
throw new Error(`Xray GraphQL error: ${payload.errors.map((e) => e.message).join("; ")}`);
|
|
6797
|
+
}
|
|
6798
|
+
return payload.data;
|
|
6799
|
+
}
|
|
6800
|
+
function issueUrl(key) {
|
|
6801
|
+
return `${jiraBase}/browse/${key}`;
|
|
6802
|
+
}
|
|
6803
|
+
function toRemoteCase(test) {
|
|
6804
|
+
const key = test.jira?.key ?? test.issueId;
|
|
6805
|
+
if (!key) {
|
|
6806
|
+
throw new Error(
|
|
6807
|
+
`Xray returned a test with neither a Jira key nor an issue id: ${JSON.stringify(test)?.slice(0, 200)}`
|
|
6808
|
+
);
|
|
6809
|
+
}
|
|
6810
|
+
if (test.jira?.key) issueIds.set(test.jira.key, test.issueId);
|
|
6811
|
+
const { description, links } = decodeDescription(fromAdf(test.jira?.description));
|
|
6812
|
+
return {
|
|
6813
|
+
id: key,
|
|
6814
|
+
url: issueUrl(key),
|
|
6815
|
+
title: test.jira?.summary ?? key,
|
|
6816
|
+
body: {
|
|
6817
|
+
title: test.jira?.summary ?? key,
|
|
6818
|
+
steps: (test.steps ?? []).map((step) => decodeStepText(step.action ?? "")),
|
|
6819
|
+
description,
|
|
6820
|
+
links
|
|
6821
|
+
}
|
|
6822
|
+
};
|
|
6823
|
+
}
|
|
6824
|
+
async function jiraUpdate(key, fields) {
|
|
6825
|
+
if (!auth.jiraEmail || !auth.jiraToken) {
|
|
6826
|
+
deps.logger.warn(
|
|
6827
|
+
`Xray: summary/description for ${key} left unchanged. Set JIRA_EMAIL and JIRA_TOKEN to update Jira fields (steps are updated either way).`
|
|
6828
|
+
);
|
|
6829
|
+
return;
|
|
6830
|
+
}
|
|
6831
|
+
const basic = Buffer.from(`${auth.jiraEmail}:${auth.jiraToken}`).toString("base64");
|
|
6832
|
+
const response = await deps.fetch(`${jiraBase}/rest/api/3/issue/${key}`, {
|
|
6833
|
+
method: "PUT",
|
|
6834
|
+
headers: { Authorization: `Basic ${basic}`, "Content-Type": "application/json" },
|
|
6835
|
+
body: JSON.stringify({ fields })
|
|
6836
|
+
});
|
|
6837
|
+
if (!response.ok) {
|
|
6838
|
+
throw new Error(`Jira update of ${key} failed (${response.status}): ${await response.text()}`);
|
|
6839
|
+
}
|
|
6840
|
+
}
|
|
6841
|
+
return {
|
|
6842
|
+
name: "xray",
|
|
6843
|
+
maxAttachmentBytes: config.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES2,
|
|
6844
|
+
describeTarget() {
|
|
6845
|
+
return `${config.projectKey} (${jiraBase})`;
|
|
6846
|
+
},
|
|
6847
|
+
async listCases() {
|
|
6848
|
+
const tests = [];
|
|
6849
|
+
for (let start = 0; ; start += PAGE_LIMIT2) {
|
|
6850
|
+
const data = await graphql(
|
|
6851
|
+
`query($jql: String!, $limit: Int!, $start: Int!) {
|
|
6852
|
+
getTests(jql: $jql, limit: $limit, start: $start) {
|
|
6853
|
+
total
|
|
6854
|
+
results {
|
|
6855
|
+
issueId
|
|
6856
|
+
jira(fields: ["key", "summary", "description"])
|
|
6857
|
+
steps { id action data result }
|
|
6858
|
+
}
|
|
6859
|
+
}
|
|
6860
|
+
}`,
|
|
6861
|
+
{ jql, limit: PAGE_LIMIT2, start }
|
|
6862
|
+
);
|
|
6863
|
+
const page = data.getTests?.results ?? [];
|
|
6864
|
+
tests.push(...page);
|
|
6865
|
+
if (page.length < PAGE_LIMIT2) break;
|
|
6866
|
+
}
|
|
6867
|
+
return tests.map(toRemoteCase);
|
|
6868
|
+
},
|
|
6869
|
+
async createCase(body) {
|
|
6870
|
+
const data = await graphql(
|
|
6871
|
+
`mutation($testType: UpdateTestTypeInput!, $steps: [CreateStepInput], $jira: JSON!) {
|
|
6872
|
+
createTest(testType: $testType, steps: $steps, jira: $jira) {
|
|
6873
|
+
test { issueId jira(fields: ["key", "summary", "description"]) }
|
|
6874
|
+
warnings
|
|
6875
|
+
}
|
|
6876
|
+
}`,
|
|
6877
|
+
{
|
|
6878
|
+
testType: { name: config.testType ?? DEFAULT_TEST_TYPE },
|
|
6879
|
+
steps: body.steps.map((step) => ({ action: encodeStepText(step), result: "" })),
|
|
6880
|
+
jira: {
|
|
6881
|
+
fields: {
|
|
6882
|
+
summary: body.title,
|
|
6883
|
+
description: toAdf(encodeDescription(body)),
|
|
6884
|
+
project: { key: config.projectKey }
|
|
6885
|
+
}
|
|
6886
|
+
}
|
|
6887
|
+
}
|
|
6888
|
+
);
|
|
6889
|
+
const created = data.createTest?.test;
|
|
6890
|
+
if (!created) throw new Error(`Xray createTest returned no test for "${body.title}"`);
|
|
6891
|
+
for (const warning of data.createTest?.warnings ?? []) deps.logger.warn(`Xray: ${warning}`);
|
|
6892
|
+
return toRemoteCase(created);
|
|
6893
|
+
},
|
|
6894
|
+
async updateCase(id, body) {
|
|
6895
|
+
const issueId = issueIds.get(id);
|
|
6896
|
+
if (!issueId) {
|
|
6897
|
+
throw new Error(`Xray: no internal issue id cached for ${id}. Run listCases first.`);
|
|
6898
|
+
}
|
|
6899
|
+
const current = await graphql(
|
|
6900
|
+
`query($issueId: String!) { getTest(issueId: $issueId) { steps { id } } }`,
|
|
6901
|
+
{ issueId }
|
|
6902
|
+
);
|
|
6903
|
+
const existing = current.getTest?.steps ?? [];
|
|
6904
|
+
for (const [index, step] of body.steps.entries()) {
|
|
6905
|
+
const action = encodeStepText(step);
|
|
6906
|
+
const target = existing[index];
|
|
6907
|
+
if (target) {
|
|
6908
|
+
await graphql(
|
|
6909
|
+
`mutation($stepId: String!, $step: UpdateStepInput!) {
|
|
6910
|
+
updateTestStep(stepId: $stepId, step: $step)
|
|
6911
|
+
}`,
|
|
6912
|
+
{ stepId: target.id, step: { action, result: "" } }
|
|
6913
|
+
);
|
|
6914
|
+
} else {
|
|
6915
|
+
await graphql(
|
|
6916
|
+
`mutation($issueId: String!, $step: CreateStepInput!) {
|
|
6917
|
+
addTestStep(issueId: $issueId, step: $step) { id }
|
|
6918
|
+
}`,
|
|
6919
|
+
{ issueId, step: { action, result: "" } }
|
|
6920
|
+
);
|
|
6921
|
+
}
|
|
6922
|
+
}
|
|
6923
|
+
for (const surplus of existing.slice(body.steps.length).reverse()) {
|
|
6924
|
+
await graphql(`mutation($stepId: String!) { removeTestStep(stepId: $stepId) }`, {
|
|
6925
|
+
stepId: surplus.id
|
|
6926
|
+
});
|
|
6927
|
+
}
|
|
6928
|
+
await jiraUpdate(id, {
|
|
6929
|
+
summary: body.title,
|
|
6930
|
+
description: toAdf(encodeDescription(body))
|
|
6931
|
+
});
|
|
6932
|
+
return {
|
|
6933
|
+
id,
|
|
6934
|
+
url: issueUrl(id),
|
|
6935
|
+
title: body.title,
|
|
6936
|
+
body
|
|
6937
|
+
};
|
|
6938
|
+
},
|
|
6939
|
+
async recordResults(results) {
|
|
6940
|
+
const statuses = {
|
|
6941
|
+
passed: config.statuses?.passed ?? "PASSED",
|
|
6942
|
+
failed: config.statuses?.failed ?? "FAILED",
|
|
6943
|
+
skipped: config.statuses?.skipped ?? "TODO"
|
|
6944
|
+
};
|
|
6945
|
+
let attachmentsUploaded = 0;
|
|
6946
|
+
const tests = results.map((result) => {
|
|
6947
|
+
const evidence = (result.attachments ?? []).map((attachment) => {
|
|
6948
|
+
attachmentsUploaded += 1;
|
|
6949
|
+
return {
|
|
6950
|
+
data: Buffer.from(attachment.body).toString("base64"),
|
|
6951
|
+
filename: attachment.filename,
|
|
6952
|
+
contentType: attachment.mediaType
|
|
6953
|
+
};
|
|
6954
|
+
});
|
|
6955
|
+
const comment = [result.message, result.url ? `Living documentation: ${result.url}` : void 0].filter(Boolean).join("\n\n");
|
|
6956
|
+
return {
|
|
6957
|
+
testKey: result.caseId,
|
|
6958
|
+
status: statuses[result.status],
|
|
6959
|
+
...comment ? { comment } : {},
|
|
6960
|
+
...evidence.length > 0 ? { evidence } : {}
|
|
6961
|
+
};
|
|
6962
|
+
});
|
|
6963
|
+
const bearer = await authenticate();
|
|
6964
|
+
const payload = {
|
|
6965
|
+
...config.testExecutionKey ? { testExecutionKey: config.testExecutionKey } : {},
|
|
6966
|
+
info: {
|
|
6967
|
+
summary: `${config.executionSummary ?? "executable-stories"} ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
6968
|
+
project: config.projectKey,
|
|
6969
|
+
...config.testPlanKey ? { testPlanKey: config.testPlanKey } : {}
|
|
6970
|
+
},
|
|
6971
|
+
tests
|
|
6972
|
+
};
|
|
6973
|
+
const response = await deps.fetch(`${xrayBase}/api/v2/import/execution`, {
|
|
6974
|
+
method: "POST",
|
|
6975
|
+
headers: { Authorization: `Bearer ${bearer}`, "Content-Type": "application/json" },
|
|
6976
|
+
body: JSON.stringify(payload)
|
|
6977
|
+
});
|
|
6978
|
+
const text2 = await response.text();
|
|
6979
|
+
if (!response.ok) {
|
|
6980
|
+
throw new Error(`Xray import execution failed (${response.status}): ${text2}`);
|
|
6981
|
+
}
|
|
6982
|
+
const imported = JSON.parse(text2);
|
|
6983
|
+
return {
|
|
6984
|
+
runId: imported.key,
|
|
6985
|
+
runUrl: imported.key ? issueUrl(imported.key) : void 0,
|
|
6986
|
+
recorded: tests.length,
|
|
6987
|
+
skipped: [],
|
|
6988
|
+
attachmentsUploaded
|
|
6989
|
+
};
|
|
6990
|
+
}
|
|
6991
|
+
};
|
|
6992
|
+
}
|
|
6993
|
+
|
|
6994
|
+
// src/sync/adapters/index.ts
|
|
6995
|
+
var PROVIDER_NAMES = ["testrail", "xray"];
|
|
6996
|
+
function isProviderName(value) {
|
|
6997
|
+
return PROVIDER_NAMES.includes(value);
|
|
6998
|
+
}
|
|
6999
|
+
function required(env, name, hint) {
|
|
7000
|
+
const value = env[name];
|
|
7001
|
+
if (!value) throw new Error(`Missing ${name}. ${hint}`);
|
|
7002
|
+
return value;
|
|
7003
|
+
}
|
|
7004
|
+
function buildProvider(args, deps) {
|
|
7005
|
+
const { name, targets, env } = args;
|
|
7006
|
+
if (name === "testrail") {
|
|
7007
|
+
const config2 = targets.testrail;
|
|
7008
|
+
if (!config2) {
|
|
7009
|
+
throw new Error(
|
|
7010
|
+
'No TestRail config found. Add a `sync: { testrail: { url, projectId } }` block to executable-stories.config.mjs, or run "executable-stories sync testrail --init".'
|
|
7011
|
+
);
|
|
7012
|
+
}
|
|
7013
|
+
const provider2 = createTestRailProvider(
|
|
7014
|
+
config2,
|
|
7015
|
+
{
|
|
7016
|
+
username: required(env, "TESTRAIL_USERNAME", "This is the login email of the TestRail account."),
|
|
7017
|
+
apiKey: required(
|
|
7018
|
+
env,
|
|
7019
|
+
"TESTRAIL_API_KEY",
|
|
7020
|
+
"Generate one in TestRail under My Settings -> API Keys. A password will not work when the instance requires API keys."
|
|
7021
|
+
)
|
|
7022
|
+
},
|
|
7023
|
+
deps
|
|
7024
|
+
);
|
|
7025
|
+
return {
|
|
7026
|
+
provider: provider2,
|
|
7027
|
+
// TestRail ids are numeric and conventionally written "C1234" in a ticket,
|
|
7028
|
+
// so the prefix is decoration and gets stripped.
|
|
7029
|
+
engineDefaults: { ticketPrefix: "C", ticketPrefixStrip: true }
|
|
7030
|
+
};
|
|
7031
|
+
}
|
|
7032
|
+
const config = targets.xray;
|
|
7033
|
+
if (!config) {
|
|
7034
|
+
throw new Error(
|
|
7035
|
+
'No Xray config found. Add a `sync: { xray: { jiraBaseUrl, projectKey } }` block to executable-stories.config.mjs, or run "executable-stories sync xray --init".'
|
|
7036
|
+
);
|
|
7037
|
+
}
|
|
7038
|
+
const provider = createXrayProvider(
|
|
7039
|
+
config,
|
|
7040
|
+
{
|
|
7041
|
+
clientId: required(env, "XRAY_CLIENT_ID", "Create an API key pair in Jira under Apps -> Xray -> API Keys."),
|
|
7042
|
+
clientSecret: required(env, "XRAY_CLIENT_SECRET", "This is the secret half of the Xray API key pair."),
|
|
7043
|
+
jiraEmail: env["JIRA_EMAIL"],
|
|
7044
|
+
jiraToken: env["JIRA_TOKEN"]
|
|
7045
|
+
},
|
|
7046
|
+
deps
|
|
7047
|
+
);
|
|
7048
|
+
return {
|
|
7049
|
+
provider,
|
|
7050
|
+
// An Xray case id is a Jira issue key, so the project prefix is part of the
|
|
7051
|
+
// id and must survive.
|
|
7052
|
+
engineDefaults: { ticketPrefix: `${config.projectKey}-`, ticketPrefixStrip: false }
|
|
7053
|
+
};
|
|
7054
|
+
}
|
|
7055
|
+
|
|
7056
|
+
// src/index.ts
|
|
5646
7057
|
var import_story_report6 = require("executable-stories-core/types/story-report");
|
|
5647
7058
|
var import_tokens2 = require("executable-stories-core/theme/tokens");
|
|
5648
7059
|
var import_acl3 = require("executable-stories-core/converters/acl/index");
|
|
@@ -5657,21 +7068,21 @@ var import_acl11 = require("executable-stories-core/converters/acl/index");
|
|
|
5657
7068
|
var import_validate = require("executable-stories-core/converters/acl/validate");
|
|
5658
7069
|
|
|
5659
7070
|
// src/coverage-index.ts
|
|
5660
|
-
function normalizePath(
|
|
5661
|
-
return
|
|
7071
|
+
function normalizePath(path11) {
|
|
7072
|
+
return path11.replace(/^\.\//, "");
|
|
5662
7073
|
}
|
|
5663
7074
|
function scenariosCoveringPaths(index, paths) {
|
|
5664
7075
|
const queries = paths.map(normalizePath);
|
|
5665
7076
|
return index.scenarios.filter(
|
|
5666
7077
|
(scenario) => scenario.covers.some(
|
|
5667
|
-
(glob) => queries.some((
|
|
7078
|
+
(glob) => queries.some((path11) => matchesPattern(normalizePath(glob), path11))
|
|
5668
7079
|
)
|
|
5669
7080
|
);
|
|
5670
7081
|
}
|
|
5671
7082
|
|
|
5672
7083
|
// src/watch.ts
|
|
5673
|
-
var
|
|
5674
|
-
var
|
|
7084
|
+
var fs5 = __toESM(require("fs"), 1);
|
|
7085
|
+
var path6 = __toESM(require("path"), 1);
|
|
5675
7086
|
var import_acl = require("executable-stories-core/converters/acl/index");
|
|
5676
7087
|
var import_synthesize = require("executable-stories-core/converters/synthesize");
|
|
5677
7088
|
function toRun(data, inputType, synthesize) {
|
|
@@ -5681,8 +7092,8 @@ function toRun(data, inputType, synthesize) {
|
|
|
5681
7092
|
return (0, import_acl.canonicalizeRun)(raw);
|
|
5682
7093
|
}
|
|
5683
7094
|
async function regenerateRun(options, deps = {}) {
|
|
5684
|
-
const read = deps.readFile ?? ((filePath) =>
|
|
5685
|
-
const data = JSON.parse(read(
|
|
7095
|
+
const read = deps.readFile ?? ((filePath) => fs5.readFileSync(filePath, "utf8"));
|
|
7096
|
+
const data = JSON.parse(read(path6.resolve(options.input)));
|
|
5686
7097
|
const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
|
|
5687
7098
|
const generator = new ReportGenerator({
|
|
5688
7099
|
formats: options.formats,
|
|
@@ -5698,7 +7109,7 @@ async function regenerateArtifacts(options, deps = {}) {
|
|
|
5698
7109
|
function startWatch(options, deps = {}) {
|
|
5699
7110
|
const log = deps.log ?? ((message) => console.log(message));
|
|
5700
7111
|
const regenerate = deps.regenerate ?? ((input) => regenerateArtifacts({ ...options, input }, deps));
|
|
5701
|
-
const watchFn = deps.watch ?? ((filePath, listener) =>
|
|
7112
|
+
const watchFn = deps.watch ?? ((filePath, listener) => fs5.watch(filePath, listener));
|
|
5702
7113
|
const debounceMs = options.debounceMs ?? 150;
|
|
5703
7114
|
let timer;
|
|
5704
7115
|
let running = false;
|
|
@@ -5727,7 +7138,7 @@ function startWatch(options, deps = {}) {
|
|
|
5727
7138
|
timer = setTimeout(() => void run(), debounceMs);
|
|
5728
7139
|
};
|
|
5729
7140
|
trigger();
|
|
5730
|
-
const watcher = watchFn(
|
|
7141
|
+
const watcher = watchFn(path6.resolve(options.input), trigger);
|
|
5731
7142
|
return {
|
|
5732
7143
|
close: () => {
|
|
5733
7144
|
if (timer) clearTimeout(timer);
|
|
@@ -6019,27 +7430,27 @@ async function updateDescription(issueKey, base, adf, headers, fetchFn) {
|
|
|
6019
7430
|
var import_ndjson_parser = require("executable-stories-core/converters/ndjson-parser");
|
|
6020
7431
|
|
|
6021
7432
|
// src/utils/git-info.ts
|
|
6022
|
-
var
|
|
6023
|
-
var
|
|
7433
|
+
var fs6 = __toESM(require("fs"), 1);
|
|
7434
|
+
var path7 = __toESM(require("path"), 1);
|
|
6024
7435
|
function readGitSha(cwd = process.cwd()) {
|
|
6025
7436
|
const envSha = process.env.GITHUB_SHA || process.env.GIT_COMMIT || process.env.CI_COMMIT_SHA;
|
|
6026
7437
|
if (envSha) return envSha;
|
|
6027
7438
|
const gitDir = findGitDir(cwd);
|
|
6028
7439
|
if (!gitDir) return void 0;
|
|
6029
7440
|
try {
|
|
6030
|
-
const headPath =
|
|
6031
|
-
const head =
|
|
7441
|
+
const headPath = path7.join(gitDir, "HEAD");
|
|
7442
|
+
const head = fs6.readFileSync(headPath, "utf8").trim();
|
|
6032
7443
|
if (!head.startsWith("ref:")) {
|
|
6033
7444
|
return head;
|
|
6034
7445
|
}
|
|
6035
7446
|
const refPath = head.replace("ref:", "").trim();
|
|
6036
|
-
const refFile =
|
|
6037
|
-
if (
|
|
6038
|
-
return
|
|
7447
|
+
const refFile = path7.join(gitDir, refPath);
|
|
7448
|
+
if (fs6.existsSync(refFile)) {
|
|
7449
|
+
return fs6.readFileSync(refFile, "utf8").trim();
|
|
6039
7450
|
}
|
|
6040
|
-
const packedRefs =
|
|
6041
|
-
if (
|
|
6042
|
-
const content =
|
|
7451
|
+
const packedRefs = path7.join(gitDir, "packed-refs");
|
|
7452
|
+
if (fs6.existsSync(packedRefs)) {
|
|
7453
|
+
const content = fs6.readFileSync(packedRefs, "utf8");
|
|
6043
7454
|
for (const line of content.split("\n")) {
|
|
6044
7455
|
if (!line || line.startsWith("#") || line.startsWith("^")) continue;
|
|
6045
7456
|
const [sha, ref] = line.split(" ");
|
|
@@ -6054,19 +7465,19 @@ function readGitSha(cwd = process.cwd()) {
|
|
|
6054
7465
|
function findGitDir(start) {
|
|
6055
7466
|
let current = start;
|
|
6056
7467
|
while (true) {
|
|
6057
|
-
const candidate =
|
|
6058
|
-
if (
|
|
6059
|
-
const stat =
|
|
7468
|
+
const candidate = path7.join(current, ".git");
|
|
7469
|
+
if (fs6.existsSync(candidate)) {
|
|
7470
|
+
const stat = fs6.statSync(candidate);
|
|
6060
7471
|
if (stat.isFile()) {
|
|
6061
|
-
const content =
|
|
7472
|
+
const content = fs6.readFileSync(candidate, "utf8").trim();
|
|
6062
7473
|
const match = content.match(/^gitdir: (.+)$/);
|
|
6063
7474
|
if (match) {
|
|
6064
|
-
return
|
|
7475
|
+
return path7.resolve(current, match[1]);
|
|
6065
7476
|
}
|
|
6066
7477
|
}
|
|
6067
7478
|
return candidate;
|
|
6068
7479
|
}
|
|
6069
|
-
const parent =
|
|
7480
|
+
const parent = path7.dirname(current);
|
|
6070
7481
|
if (parent === current) return void 0;
|
|
6071
7482
|
current = parent;
|
|
6072
7483
|
}
|
|
@@ -6077,8 +7488,8 @@ function readBranchName(cwd = process.cwd()) {
|
|
|
6077
7488
|
const gitDir = findGitDir(cwd);
|
|
6078
7489
|
if (!gitDir) return void 0;
|
|
6079
7490
|
try {
|
|
6080
|
-
const headPath =
|
|
6081
|
-
const head =
|
|
7491
|
+
const headPath = path7.join(gitDir, "HEAD");
|
|
7492
|
+
const head = fs6.readFileSync(headPath, "utf8").trim();
|
|
6082
7493
|
if (head.startsWith("ref:")) {
|
|
6083
7494
|
const refPath = head.replace("ref:", "").trim();
|
|
6084
7495
|
const match = refPath.match(/^refs\/heads\/(.+)$/);
|
|
@@ -6096,8 +7507,8 @@ var import_duration2 = require("executable-stories-core/utils/duration");
|
|
|
6096
7507
|
var import_duration3 = require("executable-stories-core/utils/duration");
|
|
6097
7508
|
|
|
6098
7509
|
// src/utils/metadata.ts
|
|
6099
|
-
var
|
|
6100
|
-
var
|
|
7510
|
+
var fs7 = __toESM(require("fs"), 1);
|
|
7511
|
+
var path8 = __toESM(require("path"), 1);
|
|
6101
7512
|
var versionCache = /* @__PURE__ */ new Map();
|
|
6102
7513
|
function readPackageVersion(root) {
|
|
6103
7514
|
if (versionCache.has(root)) {
|
|
@@ -6108,18 +7519,18 @@ function readPackageVersion(root) {
|
|
|
6108
7519
|
return version;
|
|
6109
7520
|
}
|
|
6110
7521
|
function findPackageVersion(startDir) {
|
|
6111
|
-
let current =
|
|
7522
|
+
let current = path8.resolve(startDir);
|
|
6112
7523
|
while (true) {
|
|
6113
|
-
const pkgPath =
|
|
7524
|
+
const pkgPath = path8.join(current, "package.json");
|
|
6114
7525
|
try {
|
|
6115
|
-
if (
|
|
6116
|
-
const raw =
|
|
7526
|
+
if (fs7.existsSync(pkgPath)) {
|
|
7527
|
+
const raw = fs7.readFileSync(pkgPath, "utf8");
|
|
6117
7528
|
const parsed = JSON.parse(raw);
|
|
6118
7529
|
return parsed.version;
|
|
6119
7530
|
}
|
|
6120
7531
|
} catch {
|
|
6121
7532
|
}
|
|
6122
|
-
const parent =
|
|
7533
|
+
const parent = path8.dirname(current);
|
|
6123
7534
|
if (parent === current) {
|
|
6124
7535
|
return void 0;
|
|
6125
7536
|
}
|
|
@@ -6427,7 +7838,7 @@ async function sendTeamsNotification(args, deps) {
|
|
|
6427
7838
|
}
|
|
6428
7839
|
|
|
6429
7840
|
// src/notifiers/hmac.ts
|
|
6430
|
-
var
|
|
7841
|
+
var import_node_crypto5 = require("crypto");
|
|
6431
7842
|
function signBody(args) {
|
|
6432
7843
|
let input;
|
|
6433
7844
|
let timestamp;
|
|
@@ -6437,7 +7848,7 @@ function signBody(args) {
|
|
|
6437
7848
|
} else {
|
|
6438
7849
|
input = args.body;
|
|
6439
7850
|
}
|
|
6440
|
-
const hex = (0,
|
|
7851
|
+
const hex = (0, import_node_crypto5.createHmac)("sha256", args.secret).update(input, "utf8").digest("hex");
|
|
6441
7852
|
return {
|
|
6442
7853
|
signature: `sha256=${hex}`,
|
|
6443
7854
|
timestamp
|
|
@@ -6684,24 +8095,8 @@ function calculateFlakiness(args) {
|
|
|
6684
8095
|
const countable = entries.filter(
|
|
6685
8096
|
(e) => e.status === "passed" || e.status === "failed"
|
|
6686
8097
|
);
|
|
6687
|
-
if (countable.length < MIN_FLAKINESS_SAMPLES) {
|
|
6688
|
-
return {
|
|
6689
|
-
flakinessLevel: "stable",
|
|
6690
|
-
flakinessScore: 0,
|
|
6691
|
-
failureRate: 0,
|
|
6692
|
-
longestPassStreak: countable.length,
|
|
6693
|
-
longestFailStreak: 0
|
|
6694
|
-
};
|
|
6695
|
-
}
|
|
6696
|
-
let transitions = 0;
|
|
6697
|
-
for (let i = 1; i < countable.length; i++) {
|
|
6698
|
-
if (countable[i].status !== countable[i - 1].status) {
|
|
6699
|
-
transitions++;
|
|
6700
|
-
}
|
|
6701
|
-
}
|
|
6702
|
-
const transitionScore = transitions / (countable.length - 1);
|
|
6703
8098
|
const failures = countable.filter((e) => e.status === "failed").length;
|
|
6704
|
-
const failureRate = failures / countable.length;
|
|
8099
|
+
const failureRate = countable.length > 0 ? failures / countable.length : 0;
|
|
6705
8100
|
let longestPassStreak = 0;
|
|
6706
8101
|
let longestFailStreak = 0;
|
|
6707
8102
|
let currentPassStreak = 0;
|
|
@@ -6721,6 +8116,22 @@ function calculateFlakiness(args) {
|
|
|
6721
8116
|
}
|
|
6722
8117
|
}
|
|
6723
8118
|
}
|
|
8119
|
+
if (countable.length < MIN_FLAKINESS_SAMPLES) {
|
|
8120
|
+
return {
|
|
8121
|
+
flakinessLevel: "stable",
|
|
8122
|
+
flakinessScore: 0,
|
|
8123
|
+
failureRate,
|
|
8124
|
+
longestPassStreak,
|
|
8125
|
+
longestFailStreak
|
|
8126
|
+
};
|
|
8127
|
+
}
|
|
8128
|
+
let transitions = 0;
|
|
8129
|
+
for (let i = 1; i < countable.length; i++) {
|
|
8130
|
+
if (countable[i].status !== countable[i - 1].status) {
|
|
8131
|
+
transitions++;
|
|
8132
|
+
}
|
|
8133
|
+
}
|
|
8134
|
+
const transitionScore = transitions / (countable.length - 1);
|
|
6724
8135
|
let flakinessLevel;
|
|
6725
8136
|
if (transitionScore > 0.5 || transitionScore > 0.3 && failureRate > 0.2) {
|
|
6726
8137
|
flakinessLevel = "flaky";
|
|
@@ -7313,7 +8724,7 @@ function statusIcon2(status) {
|
|
|
7313
8724
|
return "\u2022";
|
|
7314
8725
|
}
|
|
7315
8726
|
}
|
|
7316
|
-
function
|
|
8727
|
+
function escapeCell3(value) {
|
|
7317
8728
|
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
7318
8729
|
}
|
|
7319
8730
|
function intentSummary(intent) {
|
|
@@ -7342,7 +8753,7 @@ function renderWeakBand(lines, files) {
|
|
|
7342
8753
|
lines.push(`## \u{1F7E1} Changed code with weak evidence (${weak.length})`);
|
|
7343
8754
|
lines.push("");
|
|
7344
8755
|
for (const file of weak) {
|
|
7345
|
-
const covered = file.claims.map((c) => `${
|
|
8756
|
+
const covered = file.claims.map((c) => `${escapeCell3(c.scenario)} (${c.strength})`).join(", ");
|
|
7346
8757
|
lines.push(`- \`${file.path}\` _(${file.changeKind})_ \u2014 only: ${covered}`);
|
|
7347
8758
|
}
|
|
7348
8759
|
lines.push("");
|
|
@@ -7367,7 +8778,7 @@ function renderClaim(lines, claim) {
|
|
|
7367
8778
|
);
|
|
7368
8779
|
}
|
|
7369
8780
|
if (claim.intent) {
|
|
7370
|
-
lines.push(`- Why: ${
|
|
8781
|
+
lines.push(`- Why: ${escapeCell3(intentSummary(claim.intent))}`);
|
|
7371
8782
|
}
|
|
7372
8783
|
lines.push("");
|
|
7373
8784
|
}
|
|
@@ -7424,7 +8835,7 @@ function renderCodeDiff(lines, evidence) {
|
|
|
7424
8835
|
} else {
|
|
7425
8836
|
for (const ref of annotation.scenarios) {
|
|
7426
8837
|
lines.push(
|
|
7427
|
-
ref.resolved && ref.status ? `- ${statusIcon2(ref.status)} ${
|
|
8838
|
+
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)`
|
|
7428
8839
|
);
|
|
7429
8840
|
}
|
|
7430
8841
|
}
|
|
@@ -7643,9 +9054,9 @@ function renderDiffHunk(file, hunk, anchoredStart, anchoredCount) {
|
|
|
7643
9054
|
const sign = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
|
|
7644
9055
|
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>`;
|
|
7645
9056
|
});
|
|
7646
|
-
const
|
|
9057
|
+
const path11 = file.newPath ?? file.oldPath ?? "";
|
|
7647
9058
|
return `<div class="diff-hunk">
|
|
7648
|
-
<div class="diff-file-header"><code>${escapeHtml3(
|
|
9059
|
+
<div class="diff-file-header"><code>${escapeHtml3(path11)}</code> <span class="subtle">@@ -${hunk.oldStart} +${hunk.newStart} @@ ${escapeHtml3(hunk.header)}</span></div>
|
|
7649
9060
|
<table class="diff-table"><tbody>${rows.join("")}</tbody></table>
|
|
7650
9061
|
</div>`;
|
|
7651
9062
|
}
|
|
@@ -7880,8 +9291,8 @@ applyTheme(getEffectiveTheme());` : "";
|
|
|
7880
9291
|
};
|
|
7881
9292
|
|
|
7882
9293
|
// src/deploy/ledger.ts
|
|
7883
|
-
var
|
|
7884
|
-
var
|
|
9294
|
+
var fs8 = __toESM(require("fs"), 1);
|
|
9295
|
+
var path9 = __toESM(require("path"), 1);
|
|
7885
9296
|
function createEmptyLedger() {
|
|
7886
9297
|
return {
|
|
7887
9298
|
deployments: [],
|
|
@@ -7889,12 +9300,12 @@ function createEmptyLedger() {
|
|
|
7889
9300
|
};
|
|
7890
9301
|
}
|
|
7891
9302
|
function loadLedger(ledgerPath) {
|
|
7892
|
-
const resolved =
|
|
7893
|
-
if (!
|
|
9303
|
+
const resolved = path9.resolve(ledgerPath);
|
|
9304
|
+
if (!fs8.existsSync(resolved)) {
|
|
7894
9305
|
return createEmptyLedger();
|
|
7895
9306
|
}
|
|
7896
9307
|
try {
|
|
7897
|
-
const raw = JSON.parse(
|
|
9308
|
+
const raw = JSON.parse(fs8.readFileSync(resolved, "utf8"));
|
|
7898
9309
|
if (raw.schemaVersion !== 1) {
|
|
7899
9310
|
throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
|
|
7900
9311
|
}
|
|
@@ -7905,10 +9316,10 @@ function loadLedger(ledgerPath) {
|
|
|
7905
9316
|
}
|
|
7906
9317
|
}
|
|
7907
9318
|
function saveLedger(ledger, ledgerPath) {
|
|
7908
|
-
const resolved =
|
|
7909
|
-
const dir =
|
|
7910
|
-
|
|
7911
|
-
|
|
9319
|
+
const resolved = path9.resolve(ledgerPath);
|
|
9320
|
+
const dir = path9.dirname(resolved);
|
|
9321
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
9322
|
+
fs8.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
|
|
7912
9323
|
}
|
|
7913
9324
|
function getLatestDeployment(ledger, environment) {
|
|
7914
9325
|
return [...ledger.deployments].reverse().find((d) => d.environment === environment);
|
|
@@ -8035,11 +9446,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
8035
9446
|
const ext = FORMAT_EXTENSIONS[format];
|
|
8036
9447
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
8037
9448
|
if (mode === "aggregated") {
|
|
8038
|
-
return toPosix(
|
|
9449
|
+
return toPosix(path10.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
|
|
8039
9450
|
}
|
|
8040
9451
|
const normalizedSource = toPosix(sourceFile);
|
|
8041
|
-
const dirOfSource =
|
|
8042
|
-
let baseName =
|
|
9452
|
+
const dirOfSource = path10.posix.dirname(normalizedSource);
|
|
9453
|
+
let baseName = path10.posix.basename(normalizedSource);
|
|
8043
9454
|
for (const testExt of TEST_EXTENSIONS) {
|
|
8044
9455
|
if (baseName.endsWith(testExt)) {
|
|
8045
9456
|
baseName = baseName.slice(0, -testExt.length);
|
|
@@ -8048,12 +9459,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
8048
9459
|
}
|
|
8049
9460
|
const fileName = `${baseName}.${effectiveName}${ext}`;
|
|
8050
9461
|
if (colocatedStyle === "adjacent") {
|
|
8051
|
-
return toPosix(
|
|
9462
|
+
return toPosix(path10.posix.join(dirOfSource, fileName));
|
|
8052
9463
|
}
|
|
8053
9464
|
if (colocatedStyle === "flat") {
|
|
8054
|
-
return toPosix(
|
|
9465
|
+
return toPosix(path10.posix.join(baseOutputDir, `${(0, import_source_file2.cleanTestStem)(normalizedSource)}${ext}`));
|
|
8055
9466
|
}
|
|
8056
|
-
return toPosix(
|
|
9467
|
+
return toPosix(path10.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
8057
9468
|
}
|
|
8058
9469
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
8059
9470
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -8273,8 +9684,8 @@ var ReportGenerator = class {
|
|
|
8273
9684
|
if (astroPaths) {
|
|
8274
9685
|
for (const mdPath of astroPaths) {
|
|
8275
9686
|
const content = await fsPromises.readFile(mdPath, "utf8");
|
|
8276
|
-
const mdDir =
|
|
8277
|
-
const assetsDir =
|
|
9687
|
+
const mdDir = path10.dirname(mdPath);
|
|
9688
|
+
const assetsDir = path10.resolve(this.options.astro.assetsDir);
|
|
8278
9689
|
const result = copyMarkdownAssets({
|
|
8279
9690
|
markdown: content,
|
|
8280
9691
|
markdownDir: mdDir,
|
|
@@ -8322,16 +9733,16 @@ var ReportGenerator = class {
|
|
|
8322
9733
|
bySourceFile.set(sourceFile, outputPath);
|
|
8323
9734
|
}
|
|
8324
9735
|
if (bySourceFile.size === 0) return void 0;
|
|
8325
|
-
const indexPath = toPosix(
|
|
9736
|
+
const indexPath = toPosix(path10.join(this.options.outputDir, "index.html"));
|
|
8326
9737
|
if (htmlPaths.some((p) => toPosix(p) === indexPath)) {
|
|
8327
9738
|
this.deps.logger.warn?.(
|
|
8328
9739
|
`Skipping colocated index: a report already occupies ${indexPath}.`
|
|
8329
9740
|
);
|
|
8330
9741
|
return void 0;
|
|
8331
9742
|
}
|
|
8332
|
-
const entries = buildIndexEntries(run, bySourceFile,
|
|
9743
|
+
const entries = buildIndexEntries(run, bySourceFile, path10.dirname(indexPath));
|
|
8333
9744
|
const html = renderColocatedIndex(entries, this.options.html.title);
|
|
8334
|
-
await fsPromises.mkdir(
|
|
9745
|
+
await fsPromises.mkdir(path10.dirname(indexPath), { recursive: true });
|
|
8335
9746
|
await this.deps.writeFile(indexPath, html);
|
|
8336
9747
|
return indexPath;
|
|
8337
9748
|
}
|
|
@@ -8350,9 +9761,9 @@ var ReportGenerator = class {
|
|
|
8350
9761
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
8351
9762
|
const ext = FORMAT_EXTENSIONS[format];
|
|
8352
9763
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
8353
|
-
const outputPath = toPosix(
|
|
9764
|
+
const outputPath = toPosix(path10.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
|
|
8354
9765
|
const content = await this.formatContent(run, format);
|
|
8355
|
-
const dir =
|
|
9766
|
+
const dir = path10.dirname(outputPath);
|
|
8356
9767
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
8357
9768
|
await this.deps.writeFile(outputPath, content);
|
|
8358
9769
|
return [outputPath];
|
|
@@ -8364,7 +9775,7 @@ var ReportGenerator = class {
|
|
|
8364
9775
|
testCases
|
|
8365
9776
|
};
|
|
8366
9777
|
const content = await this.formatContent(groupRun, format);
|
|
8367
|
-
const dir =
|
|
9778
|
+
const dir = path10.dirname(outputPath);
|
|
8368
9779
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
8369
9780
|
await this.deps.writeFile(outputPath, content);
|
|
8370
9781
|
writtenPaths.push(outputPath);
|
|
@@ -8555,7 +9966,7 @@ async function generateRunComparison(args) {
|
|
|
8555
9966
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
8556
9967
|
for (const format of args.formats) {
|
|
8557
9968
|
const ext = format === "html" ? ".html" : format === "changelog" ? ".changelog.md" : ".md";
|
|
8558
|
-
const outputPath = toPosix(
|
|
9969
|
+
const outputPath = toPosix(path10.join(outputDir, `${outputName}${ext}`));
|
|
8559
9970
|
const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : format === "changelog" ? new RunDiffChangelogFormatter().format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
|
|
8560
9971
|
await fsPromises.writeFile(outputPath, content, "utf8");
|
|
8561
9972
|
files.push(outputPath);
|
|
@@ -8583,6 +9994,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
8583
9994
|
CucumberHtmlFormatter,
|
|
8584
9995
|
CucumberJsonFormatter,
|
|
8585
9996
|
CucumberMessagesFormatter,
|
|
9997
|
+
DEFAULT_LOCKFILE_PATH,
|
|
8586
9998
|
ES_THEME_TOKENS_CSS,
|
|
8587
9999
|
ES_THEME_TOKEN_VALUES,
|
|
8588
10000
|
JUnitFormatter,
|
|
@@ -8590,6 +10002,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
8590
10002
|
MIN_METRIC_SAMPLES,
|
|
8591
10003
|
MIN_PERF_SAMPLES,
|
|
8592
10004
|
MarkdownFormatter,
|
|
10005
|
+
PROVIDER_NAMES,
|
|
8593
10006
|
ReleaseManifestFormatter,
|
|
8594
10007
|
ReportGenerator,
|
|
8595
10008
|
ReviewHtmlFormatter,
|
|
@@ -8608,11 +10021,15 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
8608
10021
|
adaptPlaywrightRun,
|
|
8609
10022
|
adaptVitestRun,
|
|
8610
10023
|
advanceState,
|
|
10024
|
+
analyzeSync,
|
|
10025
|
+
applySync,
|
|
8611
10026
|
assembleCodeDiff,
|
|
8612
10027
|
assertValidRun,
|
|
8613
10028
|
buildCheck,
|
|
10029
|
+
buildCoverageJson,
|
|
8614
10030
|
buildGoal,
|
|
8615
10031
|
buildHtmlDocEntry,
|
|
10032
|
+
buildProvider,
|
|
8616
10033
|
buildReview,
|
|
8617
10034
|
buildTriage,
|
|
8618
10035
|
bundleAssets,
|
|
@@ -8622,11 +10039,14 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
8622
10039
|
classifyStatusChange,
|
|
8623
10040
|
clearVersionCache,
|
|
8624
10041
|
codeDiffDiagnostics,
|
|
10042
|
+
collectAttachments,
|
|
8625
10043
|
computeTestMetrics,
|
|
8626
10044
|
copyMarkdownAssets,
|
|
8627
10045
|
createAnchor,
|
|
8628
10046
|
createPrCommentSummary,
|
|
8629
10047
|
createReportGenerator,
|
|
10048
|
+
createTestRailProvider,
|
|
10049
|
+
createXrayProvider,
|
|
8630
10050
|
deriveAudience,
|
|
8631
10051
|
deriveChangeType,
|
|
8632
10052
|
deriveStepResults,
|
|
@@ -8634,6 +10054,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
8634
10054
|
detectPerformanceTrend,
|
|
8635
10055
|
diffRuns,
|
|
8636
10056
|
diffStoryReports,
|
|
10057
|
+
emptyLockfile,
|
|
8637
10058
|
findGitDir,
|
|
8638
10059
|
formatDuration,
|
|
8639
10060
|
generateRunComparison,
|
|
@@ -8643,7 +10064,9 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
8643
10064
|
getEnvironmentDrift,
|
|
8644
10065
|
gradeEvidence,
|
|
8645
10066
|
hasSufficientHistory,
|
|
10067
|
+
hashCaseBody,
|
|
8646
10068
|
initialRunState,
|
|
10069
|
+
isProviderName,
|
|
8647
10070
|
isReviewableSource,
|
|
8648
10071
|
isTestFile,
|
|
8649
10072
|
joinNameAndExt,
|
|
@@ -8658,19 +10081,26 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
8658
10081
|
normalizeStatus,
|
|
8659
10082
|
normalizeVitestResults,
|
|
8660
10083
|
parseEnvelopes,
|
|
10084
|
+
parseLockfile,
|
|
8661
10085
|
parseNdjson,
|
|
8662
10086
|
parseUnifiedDiff,
|
|
10087
|
+
projectBehaviours,
|
|
8663
10088
|
publishConfluencePage,
|
|
8664
10089
|
publishJiraIssue,
|
|
8665
10090
|
readBranchName,
|
|
8666
10091
|
readGitSha,
|
|
10092
|
+
readLockfile,
|
|
8667
10093
|
readPackageVersion,
|
|
8668
10094
|
recordDeployment,
|
|
8669
10095
|
regenerateArtifacts,
|
|
8670
10096
|
regenerateRun,
|
|
8671
10097
|
relocateAnchor,
|
|
10098
|
+
renderApplyResult,
|
|
8672
10099
|
renderCheck,
|
|
10100
|
+
renderCoverageMarkdown,
|
|
10101
|
+
renderCoverageText,
|
|
8673
10102
|
renderGoal,
|
|
10103
|
+
renderPlan,
|
|
8674
10104
|
renderTriage,
|
|
8675
10105
|
resolveAttachment,
|
|
8676
10106
|
resolveAttachments,
|
|
@@ -8682,6 +10112,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
8682
10112
|
sendSlackNotification,
|
|
8683
10113
|
sendTeamsNotification,
|
|
8684
10114
|
sendWebhookNotification,
|
|
10115
|
+
serializeLockfile,
|
|
8685
10116
|
signBody,
|
|
8686
10117
|
slugify,
|
|
8687
10118
|
startWatch,
|
|
@@ -8689,6 +10120,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
8689
10120
|
toAgentText,
|
|
8690
10121
|
toBehaviorManifest,
|
|
8691
10122
|
toCIInfo,
|
|
10123
|
+
toCaseBody,
|
|
8692
10124
|
toRawCIInfo,
|
|
8693
10125
|
toReleaseManifest,
|
|
8694
10126
|
toScenarioIndex,
|
|
@@ -8696,6 +10128,7 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
8696
10128
|
toTraceabilityMatrix,
|
|
8697
10129
|
tryGetActiveOtelContext,
|
|
8698
10130
|
updateHistory,
|
|
8699
|
-
validateCanonicalRun
|
|
10131
|
+
validateCanonicalRun,
|
|
10132
|
+
writeLockfile
|
|
8700
10133
|
});
|
|
8701
10134
|
//# sourceMappingURL=index.cjs.map
|