executable-stories-formatters 1.9.2 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2301 -567
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1559 -126
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +563 -1
- package/dist/index.d.ts +563 -1
- package/dist/index.js +1534 -123
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { parseArgs as
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
4
|
+
import { parseArgs as parseArgs3 } from "util";
|
|
5
|
+
import * as fs17 from "fs";
|
|
6
|
+
import * as path19 from "path";
|
|
7
7
|
|
|
8
8
|
// src/validation/schema-validator.ts
|
|
9
9
|
import Ajv from "ajv/dist/2020.js";
|
|
@@ -941,28 +941,28 @@ function validateRawRun(data) {
|
|
|
941
941
|
return { valid: true, errors: [] };
|
|
942
942
|
}
|
|
943
943
|
const errors = (validate.errors ?? []).map((err) => {
|
|
944
|
-
const
|
|
944
|
+
const path20 = err.instancePath || "/";
|
|
945
945
|
const message = err.message ?? "unknown error";
|
|
946
946
|
if (err.keyword === "additionalProperties") {
|
|
947
947
|
const extra = err.params.additionalProperty;
|
|
948
|
-
return `${
|
|
948
|
+
return `${path20}: ${message} \u2014 '${extra}'`;
|
|
949
949
|
}
|
|
950
950
|
if (err.keyword === "enum") {
|
|
951
951
|
const allowed = err.params.allowedValues;
|
|
952
|
-
return `${
|
|
952
|
+
return `${path20}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
|
|
953
953
|
}
|
|
954
|
-
return `${
|
|
954
|
+
return `${path20}: ${message}`;
|
|
955
955
|
});
|
|
956
956
|
return { valid: false, errors };
|
|
957
957
|
}
|
|
958
958
|
|
|
959
959
|
// src/cli.ts
|
|
960
|
-
import { synthesizeStories as
|
|
961
|
-
import { canonicalizeRun as
|
|
960
|
+
import { synthesizeStories as synthesizeStories4 } from "executable-stories-core/converters/synthesize";
|
|
961
|
+
import { canonicalizeRun as canonicalizeRun6 } from "executable-stories-core/converters/acl/index";
|
|
962
962
|
import { assertValidRun as assertValidRun2 } from "executable-stories-core/converters/acl/validate";
|
|
963
963
|
|
|
964
964
|
// src/index.ts
|
|
965
|
-
import * as
|
|
965
|
+
import * as path8 from "path";
|
|
966
966
|
import * as fsPromises from "fs/promises";
|
|
967
967
|
import { toStoryReportWithIndex } from "executable-stories-core/converters/story-report";
|
|
968
968
|
|
|
@@ -1610,50 +1610,50 @@ function scenarioLines(scenario) {
|
|
|
1610
1610
|
if (scenario.errorMessage) lines.push(indent(`error: ${scenario.errorMessage}`, " "));
|
|
1611
1611
|
return lines;
|
|
1612
1612
|
}
|
|
1613
|
-
function docLines(entry,
|
|
1614
|
-
const lines = ownDocLines(entry,
|
|
1615
|
-
for (const child of entry.children ?? []) lines.push(...docLines(child,
|
|
1613
|
+
function docLines(entry, pad2) {
|
|
1614
|
+
const lines = ownDocLines(entry, pad2);
|
|
1615
|
+
for (const child of entry.children ?? []) lines.push(...docLines(child, pad2 + " "));
|
|
1616
1616
|
return lines;
|
|
1617
1617
|
}
|
|
1618
|
-
function ownDocLines(entry,
|
|
1618
|
+
function ownDocLines(entry, pad2) {
|
|
1619
1619
|
switch (entry.kind) {
|
|
1620
1620
|
case "note":
|
|
1621
|
-
return [indent(entry.text,
|
|
1621
|
+
return [indent(entry.text, pad2)];
|
|
1622
1622
|
case "tag":
|
|
1623
1623
|
return [];
|
|
1624
1624
|
// already on the scenario's tag line
|
|
1625
1625
|
case "kv":
|
|
1626
|
-
return [`${
|
|
1626
|
+
return [`${pad2}${entry.label}: ${compact(entry.value)}`];
|
|
1627
1627
|
case "code":
|
|
1628
|
-
return [`${
|
|
1628
|
+
return [`${pad2}code ${entry.label}${entry.lang ? ` (${entry.lang})` : ""}:`, indent(entry.content, pad2 + " ")];
|
|
1629
1629
|
case "table":
|
|
1630
1630
|
return [
|
|
1631
|
-
`${
|
|
1632
|
-
...entry.rows.map((row) => `${
|
|
1631
|
+
`${pad2}table ${entry.label}: ${entry.columns.join(" | ")}`,
|
|
1632
|
+
...entry.rows.map((row) => `${pad2} ${row.join(" | ")}`)
|
|
1633
1633
|
];
|
|
1634
1634
|
case "link":
|
|
1635
|
-
return [`${
|
|
1635
|
+
return [`${pad2}link ${entry.label}: ${entry.url}`];
|
|
1636
1636
|
case "section":
|
|
1637
|
-
return [`${
|
|
1637
|
+
return [`${pad2}section ${entry.title}:`, indent(entry.markdown, pad2 + " ")];
|
|
1638
1638
|
case "mermaid":
|
|
1639
|
-
return [`${
|
|
1639
|
+
return [`${pad2}mermaid${entry.title ? ` ${entry.title}` : ""}:`, indent(entry.code, pad2 + " ")];
|
|
1640
1640
|
case "screenshot":
|
|
1641
|
-
return [`${
|
|
1641
|
+
return [`${pad2}screenshot ${entry.path}${entry.alt ? ` \u2014 ${entry.alt}` : ""}`];
|
|
1642
1642
|
case "video":
|
|
1643
|
-
return [`${
|
|
1643
|
+
return [`${pad2}video ${entry.path}${entry.caption ? ` \u2014 ${entry.caption}` : ""}`];
|
|
1644
1644
|
case "html":
|
|
1645
|
-
return [`${
|
|
1645
|
+
return [`${pad2}html ${entry.title ?? entry.path ?? entry.url ?? "(inline)"}`];
|
|
1646
1646
|
case "state":
|
|
1647
|
-
return [`${
|
|
1647
|
+
return [`${pad2}state${entry.label ? ` ${entry.label}` : ""}: ${compact(entry.value)}`];
|
|
1648
1648
|
case "custom":
|
|
1649
|
-
return [`${
|
|
1649
|
+
return [`${pad2}custom ${entry.type}: ${compact(entry.data)}`];
|
|
1650
1650
|
}
|
|
1651
1651
|
}
|
|
1652
1652
|
function compact(value) {
|
|
1653
1653
|
return typeof value === "string" ? value : JSON.stringify(value);
|
|
1654
1654
|
}
|
|
1655
|
-
function indent(text2,
|
|
1656
|
-
return text2.split("\n").map((line) =>
|
|
1655
|
+
function indent(text2, pad2) {
|
|
1656
|
+
return text2.split("\n").map((line) => pad2 + line).join("\n");
|
|
1657
1657
|
}
|
|
1658
1658
|
|
|
1659
1659
|
// src/formatters/junit-xml.ts
|
|
@@ -2556,9 +2556,9 @@ var MAX_FUZZ = 2;
|
|
|
2556
2556
|
var normalize = (line) => line.trim();
|
|
2557
2557
|
var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@ ?(.*)$/;
|
|
2558
2558
|
function stripPathPrefix(raw) {
|
|
2559
|
-
const
|
|
2560
|
-
if (
|
|
2561
|
-
return
|
|
2559
|
+
const path20 = raw.split(" ")[0].trim();
|
|
2560
|
+
if (path20 === "/dev/null") return void 0;
|
|
2561
|
+
return path20.replace(/^[ab]\//, "");
|
|
2562
2562
|
}
|
|
2563
2563
|
function parseUnifiedDiff(patch) {
|
|
2564
2564
|
const files = [];
|
|
@@ -2638,7 +2638,7 @@ function createAnchor(args) {
|
|
|
2638
2638
|
};
|
|
2639
2639
|
}
|
|
2640
2640
|
function changedRunCandidates(anchor, file, fileIndex) {
|
|
2641
|
-
const
|
|
2641
|
+
const path20 = file.newPath ?? file.oldPath ?? "";
|
|
2642
2642
|
const out = [];
|
|
2643
2643
|
file.hunks.forEach((hunk, hunkIndex) => {
|
|
2644
2644
|
outer: for (let i = 0; i + anchor.changed.length <= hunk.lines.length; i++) {
|
|
@@ -2649,7 +2649,7 @@ function changedRunCandidates(anchor, file, fileIndex) {
|
|
|
2649
2649
|
continue outer;
|
|
2650
2650
|
}
|
|
2651
2651
|
}
|
|
2652
|
-
out.push({ fileIndex, file:
|
|
2652
|
+
out.push({ fileIndex, file: path20, hunkIndex, lineIndex: i, lines: hunk.lines });
|
|
2653
2653
|
}
|
|
2654
2654
|
});
|
|
2655
2655
|
return out;
|
|
@@ -2746,18 +2746,18 @@ function deriveChangeType(tags) {
|
|
|
2746
2746
|
}
|
|
2747
2747
|
return "unknown";
|
|
2748
2748
|
}
|
|
2749
|
-
function extensionOf(
|
|
2750
|
-
const base =
|
|
2749
|
+
function extensionOf(path20) {
|
|
2750
|
+
const base = path20.split("/").pop() ?? path20;
|
|
2751
2751
|
const dot = base.lastIndexOf(".");
|
|
2752
2752
|
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
2753
2753
|
}
|
|
2754
|
-
function isTestFile(
|
|
2755
|
-
return TEST_INFIX.test(
|
|
2754
|
+
function isTestFile(path20) {
|
|
2755
|
+
return TEST_INFIX.test(path20);
|
|
2756
2756
|
}
|
|
2757
|
-
function isReviewableSource(
|
|
2758
|
-
if (isTestFile(
|
|
2759
|
-
if (
|
|
2760
|
-
return CODE_EXTENSIONS.has(extensionOf(
|
|
2757
|
+
function isReviewableSource(path20) {
|
|
2758
|
+
if (isTestFile(path20)) return false;
|
|
2759
|
+
if (path20.endsWith(".d.ts")) return false;
|
|
2760
|
+
return CODE_EXTENSIONS.has(extensionOf(path20));
|
|
2761
2761
|
}
|
|
2762
2762
|
function testBaseKey(testFile) {
|
|
2763
2763
|
return testFile.replace(TEST_INFIX, "");
|
|
@@ -2861,7 +2861,7 @@ function toClaim(testCase, changedSourcePaths) {
|
|
|
2861
2861
|
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
2862
2862
|
const key = testBaseKey(testCase.sourceFile);
|
|
2863
2863
|
const coversFiles = changedSourcePaths.filter(
|
|
2864
|
-
(
|
|
2864
|
+
(path20) => sourceBaseKey(path20) === key
|
|
2865
2865
|
);
|
|
2866
2866
|
return {
|
|
2867
2867
|
id: testCase.id,
|
|
@@ -3033,14 +3033,14 @@ var TraceabilityMatrixFormatter = class {
|
|
|
3033
3033
|
lines.push("");
|
|
3034
3034
|
lines.push(`Status: ${renderRequirementStatus(req.status)}`);
|
|
3035
3035
|
if (req.covers.length > 0) {
|
|
3036
|
-
lines.push(`Covers: ${req.covers.map((
|
|
3036
|
+
lines.push(`Covers: ${req.covers.map((path20) => `\`${path20}\``).join(", ")}`);
|
|
3037
3037
|
}
|
|
3038
3038
|
lines.push("");
|
|
3039
3039
|
lines.push("| Status | Scenario | Source | Covers |");
|
|
3040
3040
|
lines.push("| --- | --- | --- | --- |");
|
|
3041
3041
|
for (const scenario of req.scenarios) {
|
|
3042
3042
|
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
3043
|
-
const covers = scenario.covers.length > 0 ? scenario.covers.map((
|
|
3043
|
+
const covers = scenario.covers.length > 0 ? scenario.covers.map((path20) => `\`${path20}\``).join(", ") : "";
|
|
3044
3044
|
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
|
|
3045
3045
|
}
|
|
3046
3046
|
lines.push("");
|
|
@@ -3805,8 +3805,8 @@ function extractDocAttachments(step) {
|
|
|
3805
3805
|
}
|
|
3806
3806
|
return attachments;
|
|
3807
3807
|
}
|
|
3808
|
-
function guessMediaType(
|
|
3809
|
-
const lower =
|
|
3808
|
+
function guessMediaType(path20) {
|
|
3809
|
+
const lower = path20.toLowerCase();
|
|
3810
3810
|
if (lower.endsWith(".png")) return "image/png";
|
|
3811
3811
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
3812
3812
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -3947,11 +3947,11 @@ var CucumberHtmlFormatter = class {
|
|
|
3947
3947
|
for (const envelope of envelopes) {
|
|
3948
3948
|
const accepted = htmlStream.write(envelope);
|
|
3949
3949
|
if (!accepted) {
|
|
3950
|
-
await new Promise((
|
|
3950
|
+
await new Promise((resolve12) => htmlStream.once("drain", resolve12));
|
|
3951
3951
|
}
|
|
3952
3952
|
}
|
|
3953
|
-
await new Promise((
|
|
3954
|
-
collector.on("finish",
|
|
3953
|
+
await new Promise((resolve12, reject) => {
|
|
3954
|
+
collector.on("finish", resolve12);
|
|
3955
3955
|
collector.on("error", reject);
|
|
3956
3956
|
htmlStream.end();
|
|
3957
3957
|
});
|
|
@@ -6157,230 +6157,1611 @@ function replaceAssetRefInData(html, original, replacement) {
|
|
|
6157
6157
|
|
|
6158
6158
|
// src/index.ts
|
|
6159
6159
|
import { STORY_META_KEY } from "executable-stories-core/types/story";
|
|
6160
|
-
import {
|
|
6161
|
-
STORY_REPORT_SCHEMA_VERSION,
|
|
6162
|
-
STORY_REPORT_SCHEMA_MAJOR
|
|
6163
|
-
} from "executable-stories-core/types/story-report";
|
|
6164
|
-
import { ES_THEME_TOKENS_CSS as ES_THEME_TOKENS_CSS2, ES_THEME_TOKEN_VALUES } from "executable-stories-core/theme/tokens";
|
|
6165
|
-
import { canonicalizeRun as canonicalizeRun3 } from "executable-stories-core/converters/acl/index";
|
|
6166
|
-
import { normalizeStatus } from "executable-stories-core/converters/acl/index";
|
|
6167
|
-
import { generateTestCaseId } from "executable-stories-core/converters/acl/index";
|
|
6168
|
-
import { generateRunId } from "executable-stories-core/converters/acl/index";
|
|
6169
|
-
import { slugify as slugify2 } from "executable-stories-core/converters/acl/index";
|
|
6170
|
-
import { deriveStepResults } from "executable-stories-core/converters/acl/index";
|
|
6171
|
-
import { mergeStepResults } from "executable-stories-core/converters/acl/index";
|
|
6172
|
-
import { resolveAttachment } from "executable-stories-core/converters/acl/index";
|
|
6173
|
-
import { resolveAttachments } from "executable-stories-core/converters/acl/index";
|
|
6174
|
-
import {
|
|
6175
|
-
validateCanonicalRun,
|
|
6176
|
-
assertValidRun
|
|
6177
|
-
} from "executable-stories-core/converters/acl/validate";
|
|
6178
6160
|
|
|
6179
|
-
// src/
|
|
6161
|
+
// src/sync/engine.ts
|
|
6162
|
+
import { behaviourFingerprint as behaviourFingerprint2, behaviourSimilarity as behaviourSimilarity2 } from "executable-stories-core/converters/acl/ids";
|
|
6163
|
+
|
|
6164
|
+
// src/sync/lockfile.ts
|
|
6180
6165
|
import * as fs4 from "fs";
|
|
6181
6166
|
import * as path5 from "path";
|
|
6182
|
-
import {
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
|
|
6194
|
-
const generator = new ReportGenerator({
|
|
6195
|
-
formats: options.formats,
|
|
6196
|
-
outputDir: options.outputDir,
|
|
6197
|
-
outputName: options.outputName
|
|
6167
|
+
import { createHash as createHash5 } from "crypto";
|
|
6168
|
+
var DEFAULT_LOCKFILE_PATH = ".executable-stories/sync.lock.json";
|
|
6169
|
+
var LOCKFILE_VERSION = 1;
|
|
6170
|
+
function emptyLockfile() {
|
|
6171
|
+
return { version: LOCKFILE_VERSION, providers: {} };
|
|
6172
|
+
}
|
|
6173
|
+
function hashCaseBody(body) {
|
|
6174
|
+
const canonical = JSON.stringify({
|
|
6175
|
+
title: body.title.trim(),
|
|
6176
|
+
steps: body.steps.map((s) => `${s.keyword.toLowerCase()}:${s.text.trim()}`),
|
|
6177
|
+
description: body.description.trim()
|
|
6198
6178
|
});
|
|
6199
|
-
|
|
6200
|
-
return { files: [...result.values()].flat(), run };
|
|
6201
|
-
}
|
|
6202
|
-
async function regenerateArtifacts(options, deps = {}) {
|
|
6203
|
-
return (await regenerateRun(options, deps)).files;
|
|
6204
|
-
}
|
|
6205
|
-
function startWatch(options, deps = {}) {
|
|
6206
|
-
const log = deps.log ?? ((message) => console.log(message));
|
|
6207
|
-
const regenerate = deps.regenerate ?? ((input) => regenerateArtifacts({ ...options, input }, deps));
|
|
6208
|
-
const watchFn = deps.watch ?? ((filePath, listener) => fs4.watch(filePath, listener));
|
|
6209
|
-
const debounceMs = options.debounceMs ?? 150;
|
|
6210
|
-
let timer;
|
|
6211
|
-
let running = false;
|
|
6212
|
-
let pending = false;
|
|
6213
|
-
const run = async () => {
|
|
6214
|
-
if (running) {
|
|
6215
|
-
pending = true;
|
|
6216
|
-
return;
|
|
6217
|
-
}
|
|
6218
|
-
running = true;
|
|
6219
|
-
try {
|
|
6220
|
-
const files = await regenerate(options.input);
|
|
6221
|
-
log(`Regenerated ${files.length} artifact file(s) from ${options.input}`);
|
|
6222
|
-
} catch (error) {
|
|
6223
|
-
log(`Watch regeneration failed: ${error.message}`);
|
|
6224
|
-
} finally {
|
|
6225
|
-
running = false;
|
|
6226
|
-
if (pending) {
|
|
6227
|
-
pending = false;
|
|
6228
|
-
trigger();
|
|
6229
|
-
}
|
|
6230
|
-
}
|
|
6231
|
-
};
|
|
6232
|
-
const trigger = () => {
|
|
6233
|
-
if (timer) clearTimeout(timer);
|
|
6234
|
-
timer = setTimeout(() => void run(), debounceMs);
|
|
6235
|
-
};
|
|
6236
|
-
trigger();
|
|
6237
|
-
const watcher = watchFn(path5.resolve(options.input), trigger);
|
|
6238
|
-
return {
|
|
6239
|
-
close: () => {
|
|
6240
|
-
if (timer) clearTimeout(timer);
|
|
6241
|
-
watcher.close();
|
|
6242
|
-
}
|
|
6243
|
-
};
|
|
6179
|
+
return createHash5("sha1").update(canonical).digest("hex").slice(0, 16);
|
|
6244
6180
|
}
|
|
6245
|
-
|
|
6246
|
-
// src/index.ts
|
|
6247
|
-
import { advanceState, initialRunState } from "executable-stories-core";
|
|
6248
|
-
import { toStoryReport as toStoryReport6 } from "executable-stories-core/converters/story-report";
|
|
6249
|
-
|
|
6250
|
-
// src/publishers/confluence.ts
|
|
6251
|
-
function parseAdf(adf) {
|
|
6181
|
+
function parseLockfile(contents, label) {
|
|
6252
6182
|
let parsed;
|
|
6253
6183
|
try {
|
|
6254
|
-
parsed = JSON.parse(
|
|
6184
|
+
parsed = JSON.parse(contents);
|
|
6255
6185
|
} catch (err) {
|
|
6256
6186
|
throw new Error(
|
|
6257
|
-
`
|
|
6187
|
+
`Sync lockfile at ${label} is not valid JSON: ${err.message}
|
|
6188
|
+
Fix or delete it \u2014 deleting orphans every existing case binding, so prefer fixing.`
|
|
6258
6189
|
);
|
|
6259
6190
|
}
|
|
6260
|
-
if (!parsed || typeof parsed !== "object" ||
|
|
6191
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
6192
|
+
throw new Error(`Sync lockfile at ${label} must contain an object.`);
|
|
6193
|
+
}
|
|
6194
|
+
const lock = parsed;
|
|
6195
|
+
if (lock.version !== LOCKFILE_VERSION) {
|
|
6261
6196
|
throw new Error(
|
|
6262
|
-
`
|
|
6197
|
+
`Sync lockfile at ${label} has version ${String(lock.version)}, expected ${LOCKFILE_VERSION}.`
|
|
6263
6198
|
);
|
|
6264
6199
|
}
|
|
6265
|
-
return
|
|
6200
|
+
return { version: LOCKFILE_VERSION, providers: lock.providers ?? {} };
|
|
6266
6201
|
}
|
|
6267
|
-
function
|
|
6268
|
-
const
|
|
6269
|
-
const
|
|
6270
|
-
|
|
6202
|
+
function serializeLockfile(lock) {
|
|
6203
|
+
const providers = {};
|
|
6204
|
+
for (const provider of Object.keys(lock.providers).sort()) {
|
|
6205
|
+
const entries = lock.providers[provider] ?? {};
|
|
6206
|
+
const sorted = {};
|
|
6207
|
+
for (const key of Object.keys(entries).sort()) sorted[key] = entries[key];
|
|
6208
|
+
providers[provider] = sorted;
|
|
6209
|
+
}
|
|
6210
|
+
return `${JSON.stringify({ version: LOCKFILE_VERSION, providers }, null, 2)}
|
|
6211
|
+
`;
|
|
6271
6212
|
}
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6213
|
+
function entriesFor(lock, provider) {
|
|
6214
|
+
return lock.providers[provider] ?? {};
|
|
6215
|
+
}
|
|
6216
|
+
function setEntry(lock, provider, fingerprint, entry) {
|
|
6217
|
+
lock.providers[provider] ??= {};
|
|
6218
|
+
lock.providers[provider][fingerprint] = entry;
|
|
6219
|
+
}
|
|
6220
|
+
|
|
6221
|
+
// src/sync/engine.ts
|
|
6222
|
+
var DEFAULT_DUPLICATE_THRESHOLD = 0.7;
|
|
6223
|
+
var PARTIAL_RUN_ORPHAN_RATIO = 0.25;
|
|
6224
|
+
function normalizeTitle(text2) {
|
|
6225
|
+
return text2.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
|
|
6226
|
+
}
|
|
6227
|
+
function renderDocs(docs, depth = 0) {
|
|
6228
|
+
if (!docs || docs.length === 0) return "";
|
|
6229
|
+
const lines = [];
|
|
6230
|
+
for (const doc of docs) {
|
|
6231
|
+
switch (doc.kind) {
|
|
6232
|
+
case "note":
|
|
6233
|
+
lines.push(doc.text);
|
|
6234
|
+
break;
|
|
6235
|
+
case "kv":
|
|
6236
|
+
lines.push(`**${doc.label}:** ${formatValue(doc.value)}`);
|
|
6237
|
+
break;
|
|
6238
|
+
case "state":
|
|
6239
|
+
lines.push(`**${doc.label ?? "State"}:** ${formatValue(doc.value)}`);
|
|
6240
|
+
break;
|
|
6241
|
+
case "code":
|
|
6242
|
+
lines.push(`**${doc.label}**`, "", "```" + (doc.lang ?? ""), doc.content, "```");
|
|
6243
|
+
break;
|
|
6244
|
+
case "table":
|
|
6245
|
+
lines.push(
|
|
6246
|
+
`**${doc.label}**`,
|
|
6247
|
+
"",
|
|
6248
|
+
`| ${doc.columns.join(" | ")} |`,
|
|
6249
|
+
`| ${doc.columns.map(() => "---").join(" | ")} |`,
|
|
6250
|
+
...doc.rows.map((row) => `| ${row.join(" | ")} |`)
|
|
6251
|
+
);
|
|
6252
|
+
break;
|
|
6253
|
+
case "link":
|
|
6254
|
+
lines.push(`[${doc.label}](${doc.url})`);
|
|
6255
|
+
break;
|
|
6256
|
+
case "section":
|
|
6257
|
+
lines.push(`${"#".repeat(Math.min(6, depth + 3))} ${doc.title}`, "", doc.markdown);
|
|
6258
|
+
break;
|
|
6259
|
+
case "mermaid":
|
|
6260
|
+
lines.push(...doc.title ? [`**${doc.title}**`, ""] : [], "```mermaid", doc.code, "```");
|
|
6261
|
+
break;
|
|
6262
|
+
case "screenshot":
|
|
6263
|
+
lines.push(`_Screenshot: ${doc.alt ?? doc.path}_`);
|
|
6264
|
+
break;
|
|
6265
|
+
case "video":
|
|
6266
|
+
lines.push(`_Video: ${doc.caption ?? doc.path}_`);
|
|
6267
|
+
break;
|
|
6268
|
+
case "html":
|
|
6269
|
+
lines.push(`_Embedded: ${doc.title ?? doc.url ?? doc.path ?? "html"}_`);
|
|
6270
|
+
break;
|
|
6271
|
+
case "custom":
|
|
6272
|
+
lines.push(`_${doc.type}_: ${formatValue(doc.data)}`);
|
|
6273
|
+
break;
|
|
6274
|
+
case "tag":
|
|
6275
|
+
break;
|
|
6276
|
+
}
|
|
6277
|
+
const children = renderDocs(doc.children, depth + 1);
|
|
6278
|
+
if (children) lines.push(children);
|
|
6278
6279
|
}
|
|
6280
|
+
return lines.join("\n");
|
|
6279
6281
|
}
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6282
|
+
function formatValue(value) {
|
|
6283
|
+
if (typeof value === "string") return value;
|
|
6284
|
+
return JSON.stringify(value);
|
|
6285
|
+
}
|
|
6286
|
+
function scenarioUrl(config, tc) {
|
|
6287
|
+
if (!config.reportUrl) return void 0;
|
|
6288
|
+
const base = config.reportUrl.replace(/\/$/, "");
|
|
6289
|
+
const anchor = config.scenarioAnchor?.(tc);
|
|
6290
|
+
return anchor ? `${base}#${anchor}` : base;
|
|
6291
|
+
}
|
|
6292
|
+
function toCaseBody(tc, config) {
|
|
6293
|
+
const sections = [];
|
|
6294
|
+
const docs = renderDocs(tc.story.docs);
|
|
6295
|
+
if (docs) sections.push(docs);
|
|
6296
|
+
const tickets = tc.story.tickets ?? [];
|
|
6297
|
+
if (tickets.length > 0) {
|
|
6298
|
+
sections.push(
|
|
6299
|
+
`**Requirements:** ${tickets.map((t) => t.url ? `[${t.id}](${t.url})` : t.id).join(", ")}`
|
|
6285
6300
|
);
|
|
6286
6301
|
}
|
|
6287
|
-
if (
|
|
6288
|
-
|
|
6302
|
+
if (tc.story.covers?.length) {
|
|
6303
|
+
sections.push(`**Covers:** ${tc.story.covers.join(", ")}`);
|
|
6289
6304
|
}
|
|
6290
|
-
|
|
6291
|
-
const
|
|
6292
|
-
|
|
6293
|
-
|
|
6305
|
+
sections.push(`_Generated from ${tc.sourceFile}:${tc.sourceLine} by executable-stories. Edit the test, not this case._`);
|
|
6306
|
+
const links = [];
|
|
6307
|
+
const report = scenarioUrl(config, tc);
|
|
6308
|
+
if (report) links.push({ label: "Living documentation", url: report });
|
|
6309
|
+
for (const ticket of tickets) {
|
|
6310
|
+
if (ticket.url) links.push({ label: ticket.id, url: ticket.url });
|
|
6294
6311
|
}
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6312
|
+
return {
|
|
6313
|
+
title: tc.story.scenario,
|
|
6314
|
+
steps: tc.story.steps.map((s) => ({ keyword: s.keyword, text: s.text })),
|
|
6315
|
+
description: sections.join("\n\n"),
|
|
6316
|
+
links
|
|
6299
6317
|
};
|
|
6300
|
-
if (args.pageId) {
|
|
6301
|
-
return updatePage(args, base, headers, fetchFn);
|
|
6302
|
-
}
|
|
6303
|
-
return createPage(args, base, headers, fetchFn);
|
|
6304
6318
|
}
|
|
6305
|
-
|
|
6306
|
-
const
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
);
|
|
6313
|
-
}
|
|
6314
|
-
const current = await getResp.json();
|
|
6315
|
-
const nextVersion = current.version.number + 1;
|
|
6316
|
-
const title = args.title ?? current.title;
|
|
6317
|
-
const putUrl = `${base}/api/v2/pages/${encodeURIComponent(args.pageId)}`;
|
|
6318
|
-
const putResp = await fetchFn(putUrl, {
|
|
6319
|
-
method: "PUT",
|
|
6320
|
-
headers,
|
|
6321
|
-
body: JSON.stringify({
|
|
6322
|
-
id: args.pageId,
|
|
6323
|
-
status: "current",
|
|
6324
|
-
title,
|
|
6325
|
-
body: {
|
|
6326
|
-
representation: "atlas_doc_format",
|
|
6327
|
-
value: args.adf
|
|
6328
|
-
},
|
|
6329
|
-
version: { number: nextVersion }
|
|
6319
|
+
function projectBehaviours(run, config) {
|
|
6320
|
+
const fingerprints = run.testCases.map(
|
|
6321
|
+
(tc) => behaviourFingerprint2({
|
|
6322
|
+
scenario: tc.story.scenario,
|
|
6323
|
+
sourceFile: tc.sourceFile,
|
|
6324
|
+
steps: tc.story.steps.map((s) => ({ keyword: s.keyword, text: s.text })),
|
|
6325
|
+
covers: tc.story.covers
|
|
6330
6326
|
})
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
`PUT ${putUrl} failed with ${putResp.status} ${putResp.statusText}${body ? `: ${body}` : ""}`
|
|
6336
|
-
);
|
|
6327
|
+
);
|
|
6328
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6329
|
+
for (const fp of fingerprints) {
|
|
6330
|
+
if (fp) counts.set(fp, (counts.get(fp) ?? 0) + 1);
|
|
6337
6331
|
}
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6332
|
+
return run.testCases.map((tc, index) => {
|
|
6333
|
+
const fp = fingerprints[index];
|
|
6334
|
+
const unique = fp !== "" && counts.get(fp) === 1;
|
|
6335
|
+
return {
|
|
6336
|
+
fingerprint: unique ? fp : tc.id,
|
|
6337
|
+
testCase: tc,
|
|
6338
|
+
body: toCaseBody(tc, config)
|
|
6339
|
+
};
|
|
6340
|
+
});
|
|
6346
6341
|
}
|
|
6347
|
-
|
|
6348
|
-
const
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6342
|
+
function roleFor(attachment) {
|
|
6343
|
+
const type = attachment.mediaType.toLowerCase();
|
|
6344
|
+
if (type.startsWith("image/")) return "screenshot";
|
|
6345
|
+
if (type.startsWith("video/")) return "video";
|
|
6346
|
+
if (attachment.name.toLowerCase().includes("trace")) return "trace";
|
|
6347
|
+
return "log";
|
|
6348
|
+
}
|
|
6349
|
+
function decode(attachment) {
|
|
6350
|
+
return attachment.contentEncoding === "BASE64" ? Uint8Array.from(Buffer.from(attachment.body, "base64")) : new TextEncoder().encode(attachment.body);
|
|
6351
|
+
}
|
|
6352
|
+
function collectAttachments(args) {
|
|
6353
|
+
const { testCase, policy, maxBytes } = args;
|
|
6354
|
+
if (policy === "none") return { attachments: [], oversized: [] };
|
|
6355
|
+
if (policy === "failed" && testCase.status !== "failed") return { attachments: [], oversized: [] };
|
|
6356
|
+
const attachments = [];
|
|
6357
|
+
const oversized = [];
|
|
6358
|
+
for (const raw of testCase.attachments) {
|
|
6359
|
+
const body = decode(raw);
|
|
6360
|
+
if (maxBytes !== void 0 && body.byteLength > maxBytes) {
|
|
6361
|
+
oversized.push({ filename: raw.name, bytes: body.byteLength, limit: maxBytes });
|
|
6362
|
+
continue;
|
|
6355
6363
|
}
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
body: JSON.stringify(body)
|
|
6363
|
-
});
|
|
6364
|
-
if (!resp.ok) {
|
|
6365
|
-
const errBody = await parseErrorBody(resp);
|
|
6366
|
-
throw new Error(
|
|
6367
|
-
`POST ${postUrl} failed with ${resp.status} ${resp.statusText}${errBody ? `: ${errBody}` : ""}`
|
|
6368
|
-
);
|
|
6364
|
+
attachments.push({
|
|
6365
|
+
filename: raw.name,
|
|
6366
|
+
mediaType: raw.mediaType,
|
|
6367
|
+
body,
|
|
6368
|
+
role: roleFor(raw)
|
|
6369
|
+
});
|
|
6369
6370
|
}
|
|
6370
|
-
|
|
6371
|
+
return { attachments, oversized };
|
|
6372
|
+
}
|
|
6373
|
+
function toCaseResult(args) {
|
|
6374
|
+
const { behaviour, caseId, provider, config } = args;
|
|
6375
|
+
const tc = behaviour.testCase;
|
|
6376
|
+
if (tc.status === "pending") return void 0;
|
|
6377
|
+
const { attachments, oversized } = collectAttachments({
|
|
6378
|
+
testCase: tc,
|
|
6379
|
+
policy: config.attach ?? "failed",
|
|
6380
|
+
maxBytes: provider.maxAttachmentBytes
|
|
6381
|
+
});
|
|
6371
6382
|
return {
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6383
|
+
result: {
|
|
6384
|
+
caseId,
|
|
6385
|
+
status: tc.status,
|
|
6386
|
+
durationMs: tc.durationMs,
|
|
6387
|
+
message: tc.errorMessage,
|
|
6388
|
+
url: scenarioUrl(config, tc),
|
|
6389
|
+
attachments: attachments.length > 0 ? attachments : void 0
|
|
6390
|
+
},
|
|
6391
|
+
oversized
|
|
6377
6392
|
};
|
|
6378
6393
|
}
|
|
6379
|
-
function
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
return
|
|
6394
|
+
function ticketBinding(tc, config) {
|
|
6395
|
+
const prefix = config.ticketPrefix;
|
|
6396
|
+
if (!prefix) return void 0;
|
|
6397
|
+
const match = (tc.story.tickets ?? []).find((t) => t.id.startsWith(prefix));
|
|
6398
|
+
if (!match) return void 0;
|
|
6399
|
+
return config.ticketPrefixStrip === false ? match.id : match.id.slice(prefix.length);
|
|
6400
|
+
}
|
|
6401
|
+
async function analyzeSync(args) {
|
|
6402
|
+
const { run, provider, lockfile, config } = args;
|
|
6403
|
+
const local = projectBehaviours(run, config);
|
|
6404
|
+
const remoteCases = await provider.listCases();
|
|
6405
|
+
const remoteById = new Map(remoteCases.map((c) => [c.id, c]));
|
|
6406
|
+
const locked = entriesFor(lockfile, provider.name);
|
|
6407
|
+
const create = [];
|
|
6408
|
+
const update = [];
|
|
6409
|
+
const unchanged = [];
|
|
6410
|
+
const adopted = [];
|
|
6411
|
+
const skipped = [];
|
|
6412
|
+
const results = [];
|
|
6413
|
+
const oversized = [];
|
|
6414
|
+
const boundCaseIds = /* @__PURE__ */ new Set();
|
|
6415
|
+
let driftUncheckable = 0;
|
|
6416
|
+
for (const behaviour of local) {
|
|
6417
|
+
const entry = locked[behaviour.fingerprint];
|
|
6418
|
+
const caseId = entry?.caseId ?? ticketBinding(behaviour.testCase, config);
|
|
6419
|
+
const remote2 = caseId ? remoteById.get(caseId) : void 0;
|
|
6420
|
+
if (!caseId) {
|
|
6421
|
+
create.push({
|
|
6422
|
+
fingerprint: behaviour.fingerprint,
|
|
6423
|
+
scenario: behaviour.body.title,
|
|
6424
|
+
body: behaviour.body
|
|
6425
|
+
});
|
|
6426
|
+
continue;
|
|
6427
|
+
}
|
|
6428
|
+
if (!remote2) {
|
|
6429
|
+
skipped.push({
|
|
6430
|
+
fingerprint: behaviour.fingerprint,
|
|
6431
|
+
caseId,
|
|
6432
|
+
url: entry?.url ?? "",
|
|
6433
|
+
title: entry?.title ?? behaviour.body.title,
|
|
6434
|
+
reason: "case-missing"
|
|
6435
|
+
});
|
|
6436
|
+
continue;
|
|
6437
|
+
}
|
|
6438
|
+
boundCaseIds.add(caseId);
|
|
6439
|
+
const pending = toCaseResult({ behaviour, caseId, provider, config });
|
|
6440
|
+
if (pending) {
|
|
6441
|
+
results.push(pending.result);
|
|
6442
|
+
oversized.push(...pending.oversized);
|
|
6443
|
+
}
|
|
6444
|
+
const planned = {
|
|
6445
|
+
fingerprint: behaviour.fingerprint,
|
|
6446
|
+
caseId,
|
|
6447
|
+
url: remote2.url,
|
|
6448
|
+
scenario: behaviour.body.title,
|
|
6449
|
+
body: behaviour.body
|
|
6450
|
+
};
|
|
6451
|
+
if (!entry?.owned) {
|
|
6452
|
+
adopted.push(planned);
|
|
6453
|
+
continue;
|
|
6454
|
+
}
|
|
6455
|
+
const remoteHash = remote2.body ? hashCaseBody(remote2.body) : void 0;
|
|
6456
|
+
if (remoteHash !== void 0 && remoteHash !== entry.hash) {
|
|
6457
|
+
skipped.push({
|
|
6458
|
+
fingerprint: behaviour.fingerprint,
|
|
6459
|
+
caseId,
|
|
6460
|
+
url: remote2.url,
|
|
6461
|
+
title: remote2.title,
|
|
6462
|
+
reason: "remote-edited"
|
|
6463
|
+
});
|
|
6464
|
+
continue;
|
|
6465
|
+
}
|
|
6466
|
+
const baseline = remoteHash ?? entry.hash;
|
|
6467
|
+
if (baseline !== "" && hashCaseBody(behaviour.body) === baseline) {
|
|
6468
|
+
unchanged.push(planned);
|
|
6469
|
+
} else {
|
|
6470
|
+
update.push(planned);
|
|
6471
|
+
}
|
|
6472
|
+
if (remoteHash === void 0) driftUncheckable += 1;
|
|
6473
|
+
}
|
|
6474
|
+
const byNormalizedTitle = /* @__PURE__ */ new Map();
|
|
6475
|
+
for (const behaviour of local) byNormalizedTitle.set(normalizeTitle(behaviour.body.title), behaviour);
|
|
6476
|
+
const threshold = config.duplicateThreshold ?? DEFAULT_DUPLICATE_THRESHOLD;
|
|
6477
|
+
const remote = remoteCases.map((remoteCase) => {
|
|
6478
|
+
if (boundCaseIds.has(remoteCase.id)) {
|
|
6479
|
+
return { case: remoteCase, classification: "automated" };
|
|
6480
|
+
}
|
|
6481
|
+
const titleMatch = byNormalizedTitle.get(normalizeTitle(remoteCase.title));
|
|
6482
|
+
if (titleMatch) {
|
|
6483
|
+
return {
|
|
6484
|
+
case: remoteCase,
|
|
6485
|
+
classification: "duplicated",
|
|
6486
|
+
resembles: titleMatch.body.title
|
|
6487
|
+
};
|
|
6488
|
+
}
|
|
6489
|
+
if (remoteCase.body && remoteCase.body.steps.length > 0) {
|
|
6490
|
+
let best;
|
|
6491
|
+
for (const behaviour of local) {
|
|
6492
|
+
const score = behaviourSimilarity2(
|
|
6493
|
+
{
|
|
6494
|
+
scenario: remoteCase.title,
|
|
6495
|
+
sourceFile: "",
|
|
6496
|
+
steps: remoteCase.body.steps
|
|
6497
|
+
},
|
|
6498
|
+
{
|
|
6499
|
+
scenario: behaviour.body.title,
|
|
6500
|
+
sourceFile: behaviour.testCase.sourceFile,
|
|
6501
|
+
steps: behaviour.body.steps
|
|
6502
|
+
}
|
|
6503
|
+
);
|
|
6504
|
+
if (!best || score > best.score) best = { behaviour, score };
|
|
6505
|
+
}
|
|
6506
|
+
if (best && best.score >= threshold) {
|
|
6507
|
+
return {
|
|
6508
|
+
case: remoteCase,
|
|
6509
|
+
classification: "possible-duplicate",
|
|
6510
|
+
resembles: best.behaviour.body.title,
|
|
6511
|
+
similarity: Number(best.score.toFixed(2))
|
|
6512
|
+
};
|
|
6513
|
+
}
|
|
6514
|
+
}
|
|
6515
|
+
return { case: remoteCase, classification: "manual-only" };
|
|
6516
|
+
});
|
|
6517
|
+
const localFingerprints = new Set(local.map((b) => b.fingerprint));
|
|
6518
|
+
const orphaned = Object.entries(locked).filter(([fingerprint]) => !localFingerprints.has(fingerprint)).map(([fingerprint, entry]) => ({
|
|
6519
|
+
fingerprint,
|
|
6520
|
+
caseId: entry.caseId,
|
|
6521
|
+
url: entry.url,
|
|
6522
|
+
title: entry.title
|
|
6523
|
+
}));
|
|
6524
|
+
const lockedCount = Object.keys(locked).length;
|
|
6525
|
+
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;
|
|
6526
|
+
const unsupported = [];
|
|
6527
|
+
if (create.length > 0 && !provider.createCase) unsupported.push("createCase");
|
|
6528
|
+
if (update.length > 0 && !provider.updateCase) unsupported.push("updateCase");
|
|
6529
|
+
if (results.length > 0 && !provider.recordResults) unsupported.push("recordResults");
|
|
6530
|
+
const byRole = {};
|
|
6531
|
+
let files = 0;
|
|
6532
|
+
let bytes = 0;
|
|
6533
|
+
for (const result of results) {
|
|
6534
|
+
for (const attachment of result.attachments ?? []) {
|
|
6535
|
+
files += 1;
|
|
6536
|
+
bytes += attachment.body.byteLength;
|
|
6537
|
+
const role = attachment.role ?? "log";
|
|
6538
|
+
byRole[role] = (byRole[role] ?? 0) + 1;
|
|
6539
|
+
}
|
|
6540
|
+
}
|
|
6541
|
+
return {
|
|
6542
|
+
provider: provider.name,
|
|
6543
|
+
target: provider.describeTarget?.(),
|
|
6544
|
+
local,
|
|
6545
|
+
remote,
|
|
6546
|
+
create,
|
|
6547
|
+
update,
|
|
6548
|
+
unchanged,
|
|
6549
|
+
adopted,
|
|
6550
|
+
skipped,
|
|
6551
|
+
orphaned,
|
|
6552
|
+
results,
|
|
6553
|
+
attachments: { files, bytes, oversized, byRole },
|
|
6554
|
+
unsupported,
|
|
6555
|
+
driftUncheckable,
|
|
6556
|
+
partialRunWarning
|
|
6557
|
+
};
|
|
6558
|
+
}
|
|
6559
|
+
async function applySync(args, deps) {
|
|
6560
|
+
const { analysis, provider, lockfile, config } = args;
|
|
6561
|
+
const result = {
|
|
6562
|
+
created: [],
|
|
6563
|
+
updated: [],
|
|
6564
|
+
resultsRecorded: 0,
|
|
6565
|
+
resultsSkipped: [],
|
|
6566
|
+
attachmentsUploaded: 0,
|
|
6567
|
+
errors: []
|
|
6568
|
+
};
|
|
6569
|
+
const byFingerprint = new Map(analysis.local.map((b) => [b.fingerprint, b]));
|
|
6570
|
+
const results = [...analysis.results];
|
|
6571
|
+
if (analysis.create.length > 0 && provider.createCase) {
|
|
6572
|
+
for (const planned of analysis.create) {
|
|
6573
|
+
try {
|
|
6574
|
+
const created = await provider.createCase(planned.body);
|
|
6575
|
+
setEntry(lockfile, provider.name, planned.fingerprint, {
|
|
6576
|
+
caseId: created.id,
|
|
6577
|
+
url: created.url,
|
|
6578
|
+
hash: hashCaseBody(created.body ?? planned.body),
|
|
6579
|
+
title: created.title,
|
|
6580
|
+
owned: true
|
|
6581
|
+
});
|
|
6582
|
+
result.created.push({ scenario: planned.scenario, caseId: created.id, url: created.url });
|
|
6583
|
+
const behaviour = byFingerprint.get(planned.fingerprint);
|
|
6584
|
+
if (behaviour) {
|
|
6585
|
+
const pending = toCaseResult({ behaviour, caseId: created.id, provider, config });
|
|
6586
|
+
if (pending) results.push(pending.result);
|
|
6587
|
+
}
|
|
6588
|
+
} catch (err) {
|
|
6589
|
+
result.errors.push(`create "${planned.scenario}": ${err.message}`);
|
|
6590
|
+
}
|
|
6591
|
+
}
|
|
6592
|
+
}
|
|
6593
|
+
if (analysis.update.length > 0 && provider.updateCase) {
|
|
6594
|
+
for (const planned of analysis.update) {
|
|
6595
|
+
try {
|
|
6596
|
+
const updated = await provider.updateCase(planned.caseId, planned.body);
|
|
6597
|
+
setEntry(lockfile, provider.name, planned.fingerprint, {
|
|
6598
|
+
caseId: updated.id,
|
|
6599
|
+
url: updated.url,
|
|
6600
|
+
hash: hashCaseBody(updated.body ?? planned.body),
|
|
6601
|
+
title: updated.title,
|
|
6602
|
+
owned: true
|
|
6603
|
+
});
|
|
6604
|
+
result.updated.push({ scenario: planned.scenario, caseId: updated.id, url: updated.url });
|
|
6605
|
+
} catch (err) {
|
|
6606
|
+
result.errors.push(`update "${planned.scenario}" (${planned.caseId}): ${err.message}`);
|
|
6607
|
+
}
|
|
6608
|
+
}
|
|
6609
|
+
}
|
|
6610
|
+
for (const planned of analysis.unchanged) {
|
|
6611
|
+
const existing = entriesFor(lockfile, provider.name)[planned.fingerprint];
|
|
6612
|
+
if (existing) {
|
|
6613
|
+
setEntry(lockfile, provider.name, planned.fingerprint, { ...existing, url: planned.url });
|
|
6614
|
+
}
|
|
6615
|
+
}
|
|
6616
|
+
for (const planned of analysis.adopted) {
|
|
6617
|
+
setEntry(lockfile, provider.name, planned.fingerprint, {
|
|
6618
|
+
caseId: planned.caseId,
|
|
6619
|
+
url: planned.url,
|
|
6620
|
+
hash: "",
|
|
6621
|
+
title: planned.scenario,
|
|
6622
|
+
owned: false
|
|
6623
|
+
});
|
|
6624
|
+
}
|
|
6625
|
+
if (results.length > 0 && provider.recordResults) {
|
|
6626
|
+
try {
|
|
6627
|
+
const summary = await provider.recordResults(results);
|
|
6628
|
+
result.resultsRecorded = summary.recorded;
|
|
6629
|
+
result.resultsSkipped = summary.skipped;
|
|
6630
|
+
result.attachmentsUploaded = summary.attachmentsUploaded;
|
|
6631
|
+
result.runUrl = summary.runUrl;
|
|
6632
|
+
} catch (err) {
|
|
6633
|
+
result.errors.push(`record results: ${err.message}`);
|
|
6634
|
+
}
|
|
6635
|
+
} else if (results.length > 0) {
|
|
6636
|
+
deps.logger.warn(
|
|
6637
|
+
`${provider.name} does not support recording results \u2014 ${results.length} execution(s) not pushed.`
|
|
6638
|
+
);
|
|
6639
|
+
}
|
|
6640
|
+
return result;
|
|
6641
|
+
}
|
|
6642
|
+
|
|
6643
|
+
// src/sync/report.ts
|
|
6644
|
+
var MANUAL_ONLY_LIMIT = 50;
|
|
6645
|
+
function summarize2(analysis) {
|
|
6646
|
+
const count2 = (kind) => analysis.remote.filter((c) => c.classification === kind).length;
|
|
6647
|
+
return {
|
|
6648
|
+
provider: analysis.provider,
|
|
6649
|
+
target: analysis.target,
|
|
6650
|
+
totalCases: analysis.remote.length,
|
|
6651
|
+
automated: count2("automated"),
|
|
6652
|
+
duplicated: count2("duplicated"),
|
|
6653
|
+
possibleDuplicate: count2("possible-duplicate"),
|
|
6654
|
+
manualOnly: count2("manual-only"),
|
|
6655
|
+
untracked: analysis.create.length,
|
|
6656
|
+
adopted: analysis.adopted.length
|
|
6657
|
+
};
|
|
6658
|
+
}
|
|
6659
|
+
function sectionBreakdown(analysis) {
|
|
6660
|
+
const sections = /* @__PURE__ */ new Map();
|
|
6661
|
+
for (const entry of analysis.remote) {
|
|
6662
|
+
const name = entry.case.section ?? "(no section)";
|
|
6663
|
+
const bucket2 = sections.get(name) ?? { total: 0, automated: 0 };
|
|
6664
|
+
bucket2.total += 1;
|
|
6665
|
+
if (entry.classification === "automated") bucket2.automated += 1;
|
|
6666
|
+
sections.set(name, bucket2);
|
|
6667
|
+
}
|
|
6668
|
+
return [...sections.entries()].map(([name, value]) => ({ name, ...value })).sort((a, b) => b.automated - a.automated || b.total - a.total);
|
|
6669
|
+
}
|
|
6670
|
+
function renderCoverageText(analysis) {
|
|
6671
|
+
const summary = summarize2(analysis);
|
|
6672
|
+
const lines = [];
|
|
6673
|
+
lines.push(`${analysis.provider}: ${analysis.target ?? "(target not described)"} (${summary.totalCases} cases)`);
|
|
6674
|
+
lines.push("");
|
|
6675
|
+
lines.push(` ${pad(summary.automated)} automated already covered by a story`);
|
|
6676
|
+
lines.push(` ${pad(summary.duplicated)} duplicated manual case duplicates an automated story`);
|
|
6677
|
+
if (summary.possibleDuplicate > 0) {
|
|
6678
|
+
lines.push(` ${pad(summary.possibleDuplicate)} possible dupe similar to a story, needs a human to confirm`);
|
|
6679
|
+
}
|
|
6680
|
+
lines.push(` ${pad(summary.manualOnly)} manual only no automated equivalent`);
|
|
6681
|
+
lines.push(` ${pad(summary.untracked)} untracked story with no case`);
|
|
6682
|
+
if (summary.adopted > 0) {
|
|
6683
|
+
lines.push(` ${pad(summary.adopted)} linked story bound to a hand-authored case`);
|
|
6684
|
+
}
|
|
6685
|
+
const sections = sectionBreakdown(analysis).filter((s) => s.automated > 0);
|
|
6686
|
+
if (sections.length > 0) {
|
|
6687
|
+
const top = sections[0];
|
|
6688
|
+
lines.push("");
|
|
6689
|
+
lines.push(`Biggest overlap: "${top.name}" section, ${top.automated} of ${top.total} automated.`);
|
|
6690
|
+
}
|
|
6691
|
+
if (analysis.orphaned.length > 0) {
|
|
6692
|
+
lines.push("");
|
|
6693
|
+
lines.push(`${analysis.orphaned.length} case(s) bound to a story that no longer exists. Nothing was removed.`);
|
|
6694
|
+
}
|
|
6695
|
+
if (analysis.partialRunWarning) {
|
|
6696
|
+
lines.push("");
|
|
6697
|
+
lines.push(`Note: ${analysis.partialRunWarning}`);
|
|
6698
|
+
}
|
|
6699
|
+
return lines.join("\n");
|
|
6700
|
+
}
|
|
6701
|
+
function pad(value) {
|
|
6702
|
+
return String(value).padStart(4, " ");
|
|
6703
|
+
}
|
|
6704
|
+
function renderCoverageMarkdown(analysis) {
|
|
6705
|
+
const summary = summarize2(analysis);
|
|
6706
|
+
const lines = [];
|
|
6707
|
+
lines.push(`# Test coverage vs ${analysis.provider}`);
|
|
6708
|
+
lines.push("");
|
|
6709
|
+
if (analysis.target) lines.push(`**Target:** ${analysis.target}`, "");
|
|
6710
|
+
lines.push("| Cases | Count | Meaning |");
|
|
6711
|
+
lines.push("| --- | ---: | --- |");
|
|
6712
|
+
lines.push(`| Automated | ${summary.automated} | Already covered by a story |`);
|
|
6713
|
+
lines.push(`| Duplicated | ${summary.duplicated} | Manual case duplicates an automated story |`);
|
|
6714
|
+
lines.push(`| Possible duplicate | ${summary.possibleDuplicate} | Similar to a story, needs review |`);
|
|
6715
|
+
lines.push(`| Manual only | ${summary.manualOnly} | No automated equivalent |`);
|
|
6716
|
+
lines.push(`| Untracked stories | ${summary.untracked} | Story with no case |`);
|
|
6717
|
+
lines.push("");
|
|
6718
|
+
const duplicates = analysis.remote.filter(
|
|
6719
|
+
(c) => c.classification === "duplicated" || c.classification === "possible-duplicate"
|
|
6720
|
+
);
|
|
6721
|
+
if (duplicates.length > 0) {
|
|
6722
|
+
lines.push("## Retire these first");
|
|
6723
|
+
lines.push("");
|
|
6724
|
+
lines.push("Manual cases an automated story already covers.");
|
|
6725
|
+
lines.push("");
|
|
6726
|
+
lines.push("| Case | Title | Covered by | Confidence |");
|
|
6727
|
+
lines.push("| --- | --- | --- | --- |");
|
|
6728
|
+
for (const entry of duplicates) {
|
|
6729
|
+
const confidence = entry.classification === "duplicated" ? "exact title" : `similarity ${entry.similarity}`;
|
|
6730
|
+
lines.push(
|
|
6731
|
+
`| [${entry.case.id}](${entry.case.url}) | ${escapeCell2(entry.case.title)} | ${escapeCell2(entry.resembles ?? "")} | ${confidence} |`
|
|
6732
|
+
);
|
|
6733
|
+
}
|
|
6734
|
+
lines.push("");
|
|
6735
|
+
}
|
|
6736
|
+
const manualOnly = analysis.remote.filter((c) => c.classification === "manual-only");
|
|
6737
|
+
if (manualOnly.length > 0) {
|
|
6738
|
+
lines.push("## Not automated yet");
|
|
6739
|
+
lines.push("");
|
|
6740
|
+
lines.push("Cases with no automated equivalent. This is the backlog.");
|
|
6741
|
+
lines.push("");
|
|
6742
|
+
lines.push("| Case | Title | Section |");
|
|
6743
|
+
lines.push("| --- | --- | --- |");
|
|
6744
|
+
for (const entry of manualOnly.slice(0, MANUAL_ONLY_LIMIT)) {
|
|
6745
|
+
lines.push(
|
|
6746
|
+
`| [${entry.case.id}](${entry.case.url}) | ${escapeCell2(entry.case.title)} | ${escapeCell2(entry.case.section ?? "")} |`
|
|
6747
|
+
);
|
|
6748
|
+
}
|
|
6749
|
+
if (manualOnly.length > MANUAL_ONLY_LIMIT) {
|
|
6750
|
+
lines.push("");
|
|
6751
|
+
lines.push(
|
|
6752
|
+
`_${manualOnly.length - MANUAL_ONLY_LIMIT} more not listed here. The JSON artifact has all ${manualOnly.length}._`
|
|
6753
|
+
);
|
|
6754
|
+
}
|
|
6755
|
+
lines.push("");
|
|
6756
|
+
}
|
|
6757
|
+
if (analysis.create.length > 0) {
|
|
6758
|
+
lines.push("## Stories with no case");
|
|
6759
|
+
lines.push("");
|
|
6760
|
+
for (const planned of analysis.create) lines.push(`- ${planned.scenario}`);
|
|
6761
|
+
lines.push("");
|
|
6762
|
+
}
|
|
6763
|
+
if (analysis.orphaned.length > 0) {
|
|
6764
|
+
lines.push("## Cases with no story");
|
|
6765
|
+
lines.push("");
|
|
6766
|
+
lines.push("Bound to a story that has since been deleted. Nothing was removed automatically.");
|
|
6767
|
+
lines.push("");
|
|
6768
|
+
for (const orphan of analysis.orphaned) {
|
|
6769
|
+
lines.push(`- [${orphan.caseId}](${orphan.url}) ${escapeCell2(orphan.title)}`);
|
|
6770
|
+
}
|
|
6771
|
+
lines.push("");
|
|
6772
|
+
}
|
|
6773
|
+
const sections = sectionBreakdown(analysis);
|
|
6774
|
+
if (sections.length > 1) {
|
|
6775
|
+
lines.push("## By section");
|
|
6776
|
+
lines.push("");
|
|
6777
|
+
lines.push("| Section | Automated | Total |");
|
|
6778
|
+
lines.push("| --- | ---: | ---: |");
|
|
6779
|
+
for (const section of sections) {
|
|
6780
|
+
lines.push(`| ${escapeCell2(section.name)} | ${section.automated} | ${section.total} |`);
|
|
6781
|
+
}
|
|
6782
|
+
lines.push("");
|
|
6783
|
+
}
|
|
6784
|
+
if (analysis.partialRunWarning) {
|
|
6785
|
+
lines.push(`> ${analysis.partialRunWarning}`, "");
|
|
6786
|
+
}
|
|
6787
|
+
return lines.join("\n");
|
|
6788
|
+
}
|
|
6789
|
+
function escapeCell2(text2) {
|
|
6790
|
+
return text2.replace(/\|/g, "\\|");
|
|
6791
|
+
}
|
|
6792
|
+
function buildCoverageJson(analysis) {
|
|
6793
|
+
return {
|
|
6794
|
+
schema: "executable-stories/sync-coverage/v1",
|
|
6795
|
+
...summarize2(analysis),
|
|
6796
|
+
cases: analysis.remote.map((entry) => ({
|
|
6797
|
+
id: entry.case.id,
|
|
6798
|
+
url: entry.case.url,
|
|
6799
|
+
title: entry.case.title,
|
|
6800
|
+
section: entry.case.section,
|
|
6801
|
+
classification: entry.classification,
|
|
6802
|
+
resembles: entry.resembles,
|
|
6803
|
+
similarity: entry.similarity
|
|
6804
|
+
})),
|
|
6805
|
+
untrackedScenarios: analysis.create.map((c) => c.scenario),
|
|
6806
|
+
orphaned: analysis.orphaned.map((o) => ({ caseId: o.caseId, url: o.url, title: o.title })),
|
|
6807
|
+
sections: sectionBreakdown(analysis)
|
|
6808
|
+
};
|
|
6809
|
+
}
|
|
6810
|
+
function formatBytes(bytes) {
|
|
6811
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
6812
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
6813
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
6814
|
+
}
|
|
6815
|
+
function renderPlan(analysis, opts) {
|
|
6816
|
+
const lines = [];
|
|
6817
|
+
lines.push(`${analysis.provider}: ${analysis.target ?? "(target not described)"}`);
|
|
6818
|
+
lines.push("");
|
|
6819
|
+
lines.push(` + create ${pad(analysis.create.length)} cases`);
|
|
6820
|
+
lines.push(` ~ update ${pad(analysis.update.length)} cases`);
|
|
6821
|
+
lines.push(` = unchanged ${pad(analysis.unchanged.length)} cases`);
|
|
6822
|
+
if (analysis.adopted.length > 0) {
|
|
6823
|
+
lines.push(` \xB7 linked ${pad(analysis.adopted.length)} cases (hand-authored, results only)`);
|
|
6824
|
+
}
|
|
6825
|
+
if (analysis.skipped.length > 0) {
|
|
6826
|
+
const edited = analysis.skipped.filter((s) => s.reason === "remote-edited").length;
|
|
6827
|
+
const missing = analysis.skipped.filter((s) => s.reason === "case-missing").length;
|
|
6828
|
+
if (edited > 0) {
|
|
6829
|
+
lines.push(` ! skipped ${pad(edited)} cases (edited in ${analysis.provider} since last sync)`);
|
|
6830
|
+
}
|
|
6831
|
+
if (missing > 0) {
|
|
6832
|
+
lines.push(` ! skipped ${pad(missing)} cases (bound case no longer exists)`);
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
if (analysis.orphaned.length > 0) {
|
|
6836
|
+
lines.push(` ? orphaned ${pad(analysis.orphaned.length)} cases (story deleted from codebase, never removed)`);
|
|
6837
|
+
}
|
|
6838
|
+
lines.push(` \u2192 results ${pad(analysis.results.length)} executions`);
|
|
6839
|
+
if (analysis.attachments.files > 0) {
|
|
6840
|
+
const roles = Object.entries(analysis.attachments.byRole).map(([role, count2]) => `${count2} ${role}${count2 === 1 ? "" : "s"}`).join(", ");
|
|
6841
|
+
lines.push(
|
|
6842
|
+
` \u2191 upload ${pad(analysis.attachments.files)} attachments (${roles}, ${formatBytes(analysis.attachments.bytes)})`
|
|
6843
|
+
);
|
|
6844
|
+
}
|
|
6845
|
+
for (const oversized of analysis.attachments.oversized) {
|
|
6846
|
+
lines.push(
|
|
6847
|
+
` ! oversized ${oversized.filename} (${formatBytes(oversized.bytes)}, provider limit ${formatBytes(oversized.limit)})`
|
|
6848
|
+
);
|
|
6849
|
+
}
|
|
6850
|
+
for (const capability of analysis.unsupported) {
|
|
6851
|
+
lines.push(` ! ${analysis.provider} does not support ${capability} \u2014 those changes are not applied`);
|
|
6852
|
+
}
|
|
6853
|
+
if (analysis.driftUncheckable > 0) {
|
|
6854
|
+
lines.push(
|
|
6855
|
+
` ! ${analysis.driftUncheckable} case(s): ${analysis.provider} returned no body, so a hand edit to them cannot be detected`
|
|
6856
|
+
);
|
|
6857
|
+
}
|
|
6858
|
+
if (analysis.partialRunWarning) {
|
|
6859
|
+
lines.push("");
|
|
6860
|
+
lines.push(`Note: ${analysis.partialRunWarning}`);
|
|
6861
|
+
}
|
|
6862
|
+
if (analysis.skipped.some((s) => s.reason === "remote-edited")) {
|
|
6863
|
+
lines.push("");
|
|
6864
|
+
lines.push("Skipped cases were edited by hand after we last wrote them. Nothing overwrites them.");
|
|
6865
|
+
for (const skip of analysis.skipped.filter((s) => s.reason === "remote-edited")) {
|
|
6866
|
+
lines.push(` ${skip.caseId} ${skip.title} ${skip.url}`);
|
|
6867
|
+
}
|
|
6868
|
+
}
|
|
6869
|
+
if (opts.dryRun && hasWork(analysis)) {
|
|
6870
|
+
lines.push("");
|
|
6871
|
+
lines.push("Nothing was written. Run the same command with --apply to make these changes.");
|
|
6872
|
+
}
|
|
6873
|
+
return lines.join("\n");
|
|
6874
|
+
}
|
|
6875
|
+
function hasWork(analysis) {
|
|
6876
|
+
return analysis.create.length > 0 || analysis.update.length > 0 || analysis.results.length > 0;
|
|
6877
|
+
}
|
|
6878
|
+
function renderApplyResult(result) {
|
|
6879
|
+
const lines = [];
|
|
6880
|
+
for (const created of result.created) {
|
|
6881
|
+
lines.push(` + ${created.caseId} ${created.scenario} ${created.url}`);
|
|
6882
|
+
}
|
|
6883
|
+
for (const updated of result.updated) {
|
|
6884
|
+
lines.push(` ~ ${updated.caseId} ${updated.scenario}`);
|
|
6885
|
+
}
|
|
6886
|
+
lines.push("");
|
|
6887
|
+
lines.push(
|
|
6888
|
+
`Created ${result.created.length}, updated ${result.updated.length}, recorded ${result.resultsRecorded} execution(s), uploaded ${result.attachmentsUploaded} attachment(s).`
|
|
6889
|
+
);
|
|
6890
|
+
if (result.runUrl) lines.push(`Run: ${result.runUrl}`);
|
|
6891
|
+
for (const skipped of result.resultsSkipped) {
|
|
6892
|
+
lines.push(` ! result for case ${skipped.caseId} not recorded: ${skipped.reason}`);
|
|
6893
|
+
}
|
|
6894
|
+
for (const error of result.errors) {
|
|
6895
|
+
lines.push(` \u2717 ${error}`);
|
|
6896
|
+
}
|
|
6897
|
+
return lines.join("\n");
|
|
6898
|
+
}
|
|
6899
|
+
|
|
6900
|
+
// src/sync/adapters/case-text.ts
|
|
6901
|
+
var BDD_KEYWORDS = ["Given", "When", "Then", "And", "But"];
|
|
6902
|
+
function encodeStepText(step) {
|
|
6903
|
+
const keyword = step.keyword.trim();
|
|
6904
|
+
return keyword ? `${keyword} ${step.text}` : step.text;
|
|
6905
|
+
}
|
|
6906
|
+
function decodeStepText(content) {
|
|
6907
|
+
const trimmed = content.trim();
|
|
6908
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
6909
|
+
if (firstSpace > 0) {
|
|
6910
|
+
const head = trimmed.slice(0, firstSpace);
|
|
6911
|
+
if (BDD_KEYWORDS.some((keyword) => keyword.toLowerCase() === head.toLowerCase())) {
|
|
6912
|
+
return { keyword: head, text: trimmed.slice(firstSpace + 1) };
|
|
6913
|
+
}
|
|
6914
|
+
}
|
|
6915
|
+
return { keyword: "", text: trimmed };
|
|
6916
|
+
}
|
|
6917
|
+
function encodeDescription(body) {
|
|
6918
|
+
if (body.links.length === 0) return body.description;
|
|
6919
|
+
const links = body.links.map((link2) => `- [${link2.label}](${link2.url})`).join("\n");
|
|
6920
|
+
return `${body.description}
|
|
6921
|
+
|
|
6922
|
+
${links}`;
|
|
6923
|
+
}
|
|
6924
|
+
function decodeDescription(raw) {
|
|
6925
|
+
const links = [];
|
|
6926
|
+
const lines = raw.split("\n");
|
|
6927
|
+
let cut = lines.length;
|
|
6928
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
6929
|
+
const line = lines[index].trim();
|
|
6930
|
+
if (line === "") continue;
|
|
6931
|
+
const match = /^- \[([^\]]+)\]\(([^)]+)\)$/.exec(line);
|
|
6932
|
+
if (!match) break;
|
|
6933
|
+
links.unshift({ label: match[1], url: match[2] });
|
|
6934
|
+
cut = index;
|
|
6935
|
+
}
|
|
6936
|
+
return { description: lines.slice(0, cut).join("\n").trim(), links };
|
|
6937
|
+
}
|
|
6938
|
+
|
|
6939
|
+
// src/sync/adapters/testrail.ts
|
|
6940
|
+
var DEFAULT_STEPS_FIELD = "custom_steps_separated";
|
|
6941
|
+
var DEFAULT_DESCRIPTION_FIELD = "custom_preconds";
|
|
6942
|
+
var DEFAULT_MAX_ATTACHMENT_BYTES = 64 * 1024 * 1024;
|
|
6943
|
+
var PAGE_LIMIT = 250;
|
|
6944
|
+
var MAX_ATTEMPTS = 3;
|
|
6945
|
+
function unwrapList(payload, key) {
|
|
6946
|
+
if (Array.isArray(payload)) return payload;
|
|
6947
|
+
if (payload && typeof payload === "object") {
|
|
6948
|
+
const list = payload[key];
|
|
6949
|
+
if (Array.isArray(list)) return list;
|
|
6950
|
+
}
|
|
6951
|
+
return [];
|
|
6952
|
+
}
|
|
6953
|
+
function encodeElapsed(durationMs) {
|
|
6954
|
+
const seconds = Math.floor(durationMs / 1e3);
|
|
6955
|
+
if (seconds < 1) return void 0;
|
|
6956
|
+
if (seconds < 60) return `${seconds}s`;
|
|
6957
|
+
const minutes = Math.floor(seconds / 60);
|
|
6958
|
+
const rest = seconds % 60;
|
|
6959
|
+
return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
|
|
6960
|
+
}
|
|
6961
|
+
function authHint(status) {
|
|
6962
|
+
if (status === 401) {
|
|
6963
|
+
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.";
|
|
6964
|
+
}
|
|
6965
|
+
if (status === 403) {
|
|
6966
|
+
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.";
|
|
6967
|
+
}
|
|
6968
|
+
return "";
|
|
6969
|
+
}
|
|
6970
|
+
function createTestRailProvider(config, auth, deps) {
|
|
6971
|
+
const base = config.url.replace(/\/$/, "");
|
|
6972
|
+
const stepsField = config.fields?.steps ?? DEFAULT_STEPS_FIELD;
|
|
6973
|
+
const descriptionField = config.fields?.description ?? DEFAULT_DESCRIPTION_FIELD;
|
|
6974
|
+
const basicAuth = Buffer.from(`${auth.username}:${auth.apiKey}`).toString("base64");
|
|
6975
|
+
let projectName;
|
|
6976
|
+
let suiteName;
|
|
6977
|
+
const sectionNames = /* @__PURE__ */ new Map();
|
|
6978
|
+
async function api(method, init) {
|
|
6979
|
+
const url = `${base}/index.php?/api/v2/${method}`;
|
|
6980
|
+
const headers = {
|
|
6981
|
+
Authorization: `Basic ${basicAuth}`
|
|
6982
|
+
};
|
|
6983
|
+
let body;
|
|
6984
|
+
if (init?.form) {
|
|
6985
|
+
body = init.form;
|
|
6986
|
+
} else if (init?.body !== void 0) {
|
|
6987
|
+
headers["Content-Type"] = "application/json";
|
|
6988
|
+
body = JSON.stringify(init.body);
|
|
6989
|
+
}
|
|
6990
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
6991
|
+
const response = await deps.fetch(url, {
|
|
6992
|
+
method: body === void 0 ? "GET" : "POST",
|
|
6993
|
+
headers,
|
|
6994
|
+
body
|
|
6995
|
+
});
|
|
6996
|
+
if (response.status === 429 && attempt < MAX_ATTEMPTS - 1) {
|
|
6997
|
+
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
|
|
6998
|
+
deps.logger.warn(`TestRail rate limit hit, retrying in ${retryAfter}s`);
|
|
6999
|
+
await new Promise((resolve12) => setTimeout(resolve12, Math.max(1, retryAfter) * 1e3));
|
|
7000
|
+
continue;
|
|
7001
|
+
}
|
|
7002
|
+
const text2 = await response.text();
|
|
7003
|
+
if (text2.length === 0) {
|
|
7004
|
+
if (response.ok) return void 0;
|
|
7005
|
+
throw new Error(
|
|
7006
|
+
`TestRail ${method} failed (${response.status}) with an empty response${authHint(response.status)}`
|
|
7007
|
+
);
|
|
7008
|
+
}
|
|
7009
|
+
let parsed;
|
|
7010
|
+
try {
|
|
7011
|
+
parsed = JSON.parse(text2);
|
|
7012
|
+
} catch {
|
|
7013
|
+
throw new Error(
|
|
7014
|
+
`TestRail ${method} returned HTML rather than JSON (status ${response.status}).
|
|
7015
|
+
Check that sync.testrail.url is the instance root, e.g. https://acme.testrail.io, with no path after it.`
|
|
7016
|
+
);
|
|
7017
|
+
}
|
|
7018
|
+
if (!response.ok) {
|
|
7019
|
+
const detail = parsed.error ?? text2;
|
|
7020
|
+
throw new Error(
|
|
7021
|
+
`TestRail ${method} failed (${response.status}): ${detail}${authHint(response.status)}`
|
|
7022
|
+
);
|
|
7023
|
+
}
|
|
7024
|
+
return parsed;
|
|
7025
|
+
}
|
|
7026
|
+
throw new Error(`TestRail ${method} failed: rate limited after ${MAX_ATTEMPTS} attempts`);
|
|
7027
|
+
}
|
|
7028
|
+
async function paginate(method, key) {
|
|
7029
|
+
const all = [];
|
|
7030
|
+
for (let offset = 0; ; offset += PAGE_LIMIT) {
|
|
7031
|
+
const page = await api(`${method}&limit=${PAGE_LIMIT}&offset=${offset}`);
|
|
7032
|
+
const items = unwrapList(page, key);
|
|
7033
|
+
all.push(...items);
|
|
7034
|
+
if (items.length < PAGE_LIMIT) return all;
|
|
7035
|
+
}
|
|
7036
|
+
}
|
|
7037
|
+
function caseUrl(id) {
|
|
7038
|
+
return `${base}/index.php?/cases/view/${id}`;
|
|
7039
|
+
}
|
|
7040
|
+
function toRemoteCase(raw, context) {
|
|
7041
|
+
if (raw?.id === void 0) {
|
|
7042
|
+
throw new Error(
|
|
7043
|
+
`TestRail ${context} returned no case id. The response was: ${JSON.stringify(raw)?.slice(0, 200)}`
|
|
7044
|
+
);
|
|
7045
|
+
}
|
|
7046
|
+
const title = typeof raw.title === "string" ? raw.title : "";
|
|
7047
|
+
const steps = Array.isArray(raw[stepsField]) ? raw[stepsField] : [];
|
|
7048
|
+
const rawDescription = typeof raw[descriptionField] === "string" ? raw[descriptionField] : "";
|
|
7049
|
+
const { description, links } = decodeDescription(rawDescription);
|
|
7050
|
+
return {
|
|
7051
|
+
id: String(raw.id),
|
|
7052
|
+
url: caseUrl(raw.id),
|
|
7053
|
+
title,
|
|
7054
|
+
section: raw.section_id === void 0 ? void 0 : sectionNames.get(raw.section_id),
|
|
7055
|
+
body: {
|
|
7056
|
+
title,
|
|
7057
|
+
steps: steps.map((step) => decodeStepText(step.content ?? "")),
|
|
7058
|
+
description,
|
|
7059
|
+
links
|
|
7060
|
+
}
|
|
7061
|
+
};
|
|
7062
|
+
}
|
|
7063
|
+
function bodyToPayload(body) {
|
|
7064
|
+
const payload = {
|
|
7065
|
+
title: body.title,
|
|
7066
|
+
[stepsField]: body.steps.map((step) => ({ content: encodeStepText(step), expected: "" })),
|
|
7067
|
+
[descriptionField]: encodeDescription(body)
|
|
7068
|
+
};
|
|
7069
|
+
if (config.templateId !== void 0) payload["template_id"] = config.templateId;
|
|
7070
|
+
return payload;
|
|
7071
|
+
}
|
|
7072
|
+
const suiteQuery = config.suiteId === void 0 ? "" : `&suite_id=${config.suiteId}`;
|
|
7073
|
+
return {
|
|
7074
|
+
name: "testrail",
|
|
7075
|
+
maxAttachmentBytes: config.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES,
|
|
7076
|
+
describeTarget() {
|
|
7077
|
+
const project = projectName ?? `project ${config.projectId}`;
|
|
7078
|
+
const suite = suiteName ?? (config.suiteId === void 0 ? void 0 : `suite ${config.suiteId}`);
|
|
7079
|
+
return suite ? `${project} / ${suite}` : project;
|
|
7080
|
+
},
|
|
7081
|
+
async listCases() {
|
|
7082
|
+
try {
|
|
7083
|
+
const project = await api(`get_project/${config.projectId}`);
|
|
7084
|
+
projectName = project?.name;
|
|
7085
|
+
if (config.suiteId !== void 0) {
|
|
7086
|
+
const suite = await api(`get_suite/${config.suiteId}`);
|
|
7087
|
+
suiteName = suite?.name;
|
|
7088
|
+
}
|
|
7089
|
+
const sections = await paginate(
|
|
7090
|
+
`get_sections/${config.projectId}${suiteQuery}`,
|
|
7091
|
+
"sections"
|
|
7092
|
+
);
|
|
7093
|
+
for (const section of sections) sectionNames.set(section.id, section.name);
|
|
7094
|
+
} catch (err) {
|
|
7095
|
+
deps.logger.warn(`TestRail metadata lookup failed, continuing without names: ${err.message}`);
|
|
7096
|
+
}
|
|
7097
|
+
const cases = await paginate(`get_cases/${config.projectId}${suiteQuery}`, "cases");
|
|
7098
|
+
return cases.map((raw) => toRemoteCase(raw, "get_cases"));
|
|
7099
|
+
},
|
|
7100
|
+
async createCase(body) {
|
|
7101
|
+
if (config.sectionId === void 0) {
|
|
7102
|
+
throw new Error(
|
|
7103
|
+
"TestRail needs a sectionId to create cases. Set sync.testrail.sectionId to the section new cases should land in."
|
|
7104
|
+
);
|
|
7105
|
+
}
|
|
7106
|
+
const created = await api(`add_case/${config.sectionId}`, {
|
|
7107
|
+
body: bodyToPayload(body)
|
|
7108
|
+
});
|
|
7109
|
+
return toRemoteCase(created, `add_case/${config.sectionId}`);
|
|
7110
|
+
},
|
|
7111
|
+
async updateCase(id, body) {
|
|
7112
|
+
const updated = await api(`update_case/${id}`, { body: bodyToPayload(body) });
|
|
7113
|
+
return toRemoteCase(updated, `update_case/${id}`);
|
|
7114
|
+
},
|
|
7115
|
+
async recordResults(results) {
|
|
7116
|
+
const statusIds = {
|
|
7117
|
+
passed: config.statusIds?.passed ?? 1,
|
|
7118
|
+
failed: config.statusIds?.failed ?? 5,
|
|
7119
|
+
skipped: config.statusIds?.skipped
|
|
7120
|
+
};
|
|
7121
|
+
const skipped = [];
|
|
7122
|
+
const sendable = [];
|
|
7123
|
+
for (const result of results) {
|
|
7124
|
+
const caseId = Number(result.caseId);
|
|
7125
|
+
if (!Number.isFinite(caseId)) {
|
|
7126
|
+
skipped.push({ caseId: result.caseId, reason: "case id is not numeric" });
|
|
7127
|
+
continue;
|
|
7128
|
+
}
|
|
7129
|
+
const statusId = result.status === "skipped" ? statusIds.skipped : statusIds[result.status];
|
|
7130
|
+
if (statusId === void 0) {
|
|
7131
|
+
skipped.push({
|
|
7132
|
+
caseId: result.caseId,
|
|
7133
|
+
reason: "no TestRail status id configured for skipped (set sync.testrail.statusIds.skipped)"
|
|
7134
|
+
});
|
|
7135
|
+
continue;
|
|
7136
|
+
}
|
|
7137
|
+
sendable.push({ result, caseId, statusId });
|
|
7138
|
+
}
|
|
7139
|
+
if (sendable.length === 0) {
|
|
7140
|
+
return { recorded: 0, skipped, attachmentsUploaded: 0 };
|
|
7141
|
+
}
|
|
7142
|
+
let runId = config.runId;
|
|
7143
|
+
if (runId === void 0) {
|
|
7144
|
+
const name = `${config.runName ?? "executable-stories"} ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
7145
|
+
const run = await api(`add_run/${config.projectId}`, {
|
|
7146
|
+
body: {
|
|
7147
|
+
...config.suiteId === void 0 ? {} : { suite_id: config.suiteId },
|
|
7148
|
+
name,
|
|
7149
|
+
include_all: false,
|
|
7150
|
+
// Deduplicated: two stories can carry the same ticket id, and
|
|
7151
|
+
// TestRail rejects a run whose case list repeats one.
|
|
7152
|
+
case_ids: [...new Set(sendable.map((s) => s.caseId))]
|
|
7153
|
+
}
|
|
7154
|
+
});
|
|
7155
|
+
runId = run.id;
|
|
7156
|
+
}
|
|
7157
|
+
const runUrl = `${base}/index.php?/runs/view/${runId}`;
|
|
7158
|
+
const payload = sendable.map(({ result, caseId, statusId }) => {
|
|
7159
|
+
const elapsed = encodeElapsed(result.durationMs);
|
|
7160
|
+
const comment = [result.message, result.url ? `Living documentation: ${result.url}` : void 0].filter(Boolean).join("\n\n");
|
|
7161
|
+
return {
|
|
7162
|
+
case_id: caseId,
|
|
7163
|
+
status_id: statusId,
|
|
7164
|
+
...comment ? { comment } : {},
|
|
7165
|
+
...elapsed ? { elapsed } : {}
|
|
7166
|
+
};
|
|
7167
|
+
});
|
|
7168
|
+
const recorded = unwrapList(
|
|
7169
|
+
await api(`add_results_for_cases/${runId}`, { body: { results: payload } }),
|
|
7170
|
+
"results"
|
|
7171
|
+
);
|
|
7172
|
+
let attachmentsUploaded = 0;
|
|
7173
|
+
for (const [index, entry] of recorded.entries()) {
|
|
7174
|
+
const source = sendable[index]?.result;
|
|
7175
|
+
for (const attachment of source?.attachments ?? []) {
|
|
7176
|
+
try {
|
|
7177
|
+
const form = new FormData();
|
|
7178
|
+
const bytes = attachment.body.buffer.slice(
|
|
7179
|
+
attachment.body.byteOffset,
|
|
7180
|
+
attachment.body.byteOffset + attachment.body.byteLength
|
|
7181
|
+
);
|
|
7182
|
+
form.append(
|
|
7183
|
+
"attachment",
|
|
7184
|
+
new Blob([bytes], { type: attachment.mediaType }),
|
|
7185
|
+
attachment.filename
|
|
7186
|
+
);
|
|
7187
|
+
await api(`add_attachment_to_result/${entry.id}`, { form });
|
|
7188
|
+
attachmentsUploaded += 1;
|
|
7189
|
+
} catch (err) {
|
|
7190
|
+
deps.logger.warn(
|
|
7191
|
+
`TestRail attachment "${attachment.filename}" failed: ${err.message}`
|
|
7192
|
+
);
|
|
7193
|
+
}
|
|
7194
|
+
}
|
|
7195
|
+
}
|
|
7196
|
+
if (config.closeRun && config.runId === void 0) {
|
|
7197
|
+
await api(`close_run/${runId}`, { body: {} });
|
|
7198
|
+
}
|
|
7199
|
+
return {
|
|
7200
|
+
runId: String(runId),
|
|
7201
|
+
runUrl,
|
|
7202
|
+
recorded: recorded.length,
|
|
7203
|
+
skipped,
|
|
7204
|
+
attachmentsUploaded
|
|
7205
|
+
};
|
|
7206
|
+
}
|
|
7207
|
+
};
|
|
7208
|
+
}
|
|
7209
|
+
|
|
7210
|
+
// src/sync/adapters/xray.ts
|
|
7211
|
+
var DEFAULT_XRAY_BASE = "https://xray.cloud.getxray.app";
|
|
7212
|
+
var DEFAULT_TEST_TYPE = "Manual";
|
|
7213
|
+
var DEFAULT_MAX_ATTACHMENT_BYTES2 = 32 * 1024 * 1024;
|
|
7214
|
+
var PAGE_LIMIT2 = 100;
|
|
7215
|
+
function toAdf(text2) {
|
|
7216
|
+
const paragraphs = text2.split("\n\n").filter((block) => block.trim() !== "");
|
|
7217
|
+
return {
|
|
7218
|
+
version: 1,
|
|
7219
|
+
type: "doc",
|
|
7220
|
+
content: paragraphs.length === 0 ? [{ type: "paragraph", content: [] }] : paragraphs.map((block) => ({
|
|
7221
|
+
type: "paragraph",
|
|
7222
|
+
content: [{ type: "text", text: block }]
|
|
7223
|
+
}))
|
|
7224
|
+
};
|
|
7225
|
+
}
|
|
7226
|
+
function fromAdf(value) {
|
|
7227
|
+
if (typeof value === "string") return value;
|
|
7228
|
+
if (!value || typeof value !== "object") return "";
|
|
7229
|
+
const blocks = [];
|
|
7230
|
+
const walk = (node, collected) => {
|
|
7231
|
+
if (!node || typeof node !== "object") return;
|
|
7232
|
+
const typed = node;
|
|
7233
|
+
if (typed.type === "text" && typeof typed.text === "string") collected.push(typed.text);
|
|
7234
|
+
for (const child of typed.content ?? []) walk(child, collected);
|
|
7235
|
+
};
|
|
7236
|
+
for (const node of value.content ?? []) {
|
|
7237
|
+
const collected = [];
|
|
7238
|
+
walk(node, collected);
|
|
7239
|
+
blocks.push(collected.join(""));
|
|
7240
|
+
}
|
|
7241
|
+
return blocks.join("\n\n").trim();
|
|
7242
|
+
}
|
|
7243
|
+
function createXrayProvider(config, auth, deps) {
|
|
7244
|
+
const xrayBase = (config.xrayBaseUrl ?? DEFAULT_XRAY_BASE).replace(/\/$/, "");
|
|
7245
|
+
const jiraBase = config.jiraBaseUrl.replace(/\/$/, "");
|
|
7246
|
+
const jql = config.jql ?? `project = "${config.projectKey}" AND issuetype = Test`;
|
|
7247
|
+
const issueIds = /* @__PURE__ */ new Map();
|
|
7248
|
+
let token;
|
|
7249
|
+
async function authenticate() {
|
|
7250
|
+
if (token) return token;
|
|
7251
|
+
const response = await deps.fetch(`${xrayBase}/api/v2/authenticate`, {
|
|
7252
|
+
method: "POST",
|
|
7253
|
+
headers: { "Content-Type": "application/json" },
|
|
7254
|
+
body: JSON.stringify({ client_id: auth.clientId, client_secret: auth.clientSecret })
|
|
7255
|
+
});
|
|
7256
|
+
const text2 = await response.text();
|
|
7257
|
+
if (!response.ok) {
|
|
7258
|
+
throw new Error(
|
|
7259
|
+
`Xray authentication failed (${response.status}): ${text2}
|
|
7260
|
+
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.`
|
|
7261
|
+
);
|
|
7262
|
+
}
|
|
7263
|
+
token = JSON.parse(text2).replace(/^"|"$/g, "");
|
|
7264
|
+
return token;
|
|
7265
|
+
}
|
|
7266
|
+
async function graphql(query, variables) {
|
|
7267
|
+
const bearer = await authenticate();
|
|
7268
|
+
const response = await deps.fetch(`${xrayBase}/api/v2/graphql`, {
|
|
7269
|
+
method: "POST",
|
|
7270
|
+
headers: {
|
|
7271
|
+
Authorization: `Bearer ${bearer}`,
|
|
7272
|
+
"Content-Type": "application/json"
|
|
7273
|
+
},
|
|
7274
|
+
body: JSON.stringify({ query, variables })
|
|
7275
|
+
});
|
|
7276
|
+
const text2 = await response.text();
|
|
7277
|
+
if (!response.ok) throw new Error(`Xray GraphQL failed (${response.status}): ${text2}`);
|
|
7278
|
+
const payload = JSON.parse(text2);
|
|
7279
|
+
if (payload.errors?.length) {
|
|
7280
|
+
throw new Error(`Xray GraphQL error: ${payload.errors.map((e) => e.message).join("; ")}`);
|
|
7281
|
+
}
|
|
7282
|
+
return payload.data;
|
|
7283
|
+
}
|
|
7284
|
+
function issueUrl(key) {
|
|
7285
|
+
return `${jiraBase}/browse/${key}`;
|
|
7286
|
+
}
|
|
7287
|
+
function toRemoteCase(test) {
|
|
7288
|
+
const key = test.jira?.key ?? test.issueId;
|
|
7289
|
+
if (!key) {
|
|
7290
|
+
throw new Error(
|
|
7291
|
+
`Xray returned a test with neither a Jira key nor an issue id: ${JSON.stringify(test)?.slice(0, 200)}`
|
|
7292
|
+
);
|
|
7293
|
+
}
|
|
7294
|
+
if (test.jira?.key) issueIds.set(test.jira.key, test.issueId);
|
|
7295
|
+
const { description, links } = decodeDescription(fromAdf(test.jira?.description));
|
|
7296
|
+
return {
|
|
7297
|
+
id: key,
|
|
7298
|
+
url: issueUrl(key),
|
|
7299
|
+
title: test.jira?.summary ?? key,
|
|
7300
|
+
body: {
|
|
7301
|
+
title: test.jira?.summary ?? key,
|
|
7302
|
+
steps: (test.steps ?? []).map((step) => decodeStepText(step.action ?? "")),
|
|
7303
|
+
description,
|
|
7304
|
+
links
|
|
7305
|
+
}
|
|
7306
|
+
};
|
|
7307
|
+
}
|
|
7308
|
+
async function jiraUpdate(key, fields) {
|
|
7309
|
+
if (!auth.jiraEmail || !auth.jiraToken) {
|
|
7310
|
+
deps.logger.warn(
|
|
7311
|
+
`Xray: summary/description for ${key} left unchanged. Set JIRA_EMAIL and JIRA_TOKEN to update Jira fields (steps are updated either way).`
|
|
7312
|
+
);
|
|
7313
|
+
return;
|
|
7314
|
+
}
|
|
7315
|
+
const basic = Buffer.from(`${auth.jiraEmail}:${auth.jiraToken}`).toString("base64");
|
|
7316
|
+
const response = await deps.fetch(`${jiraBase}/rest/api/3/issue/${key}`, {
|
|
7317
|
+
method: "PUT",
|
|
7318
|
+
headers: { Authorization: `Basic ${basic}`, "Content-Type": "application/json" },
|
|
7319
|
+
body: JSON.stringify({ fields })
|
|
7320
|
+
});
|
|
7321
|
+
if (!response.ok) {
|
|
7322
|
+
throw new Error(`Jira update of ${key} failed (${response.status}): ${await response.text()}`);
|
|
7323
|
+
}
|
|
7324
|
+
}
|
|
7325
|
+
return {
|
|
7326
|
+
name: "xray",
|
|
7327
|
+
maxAttachmentBytes: config.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES2,
|
|
7328
|
+
describeTarget() {
|
|
7329
|
+
return `${config.projectKey} (${jiraBase})`;
|
|
7330
|
+
},
|
|
7331
|
+
async listCases() {
|
|
7332
|
+
const tests = [];
|
|
7333
|
+
for (let start = 0; ; start += PAGE_LIMIT2) {
|
|
7334
|
+
const data = await graphql(
|
|
7335
|
+
`query($jql: String!, $limit: Int!, $start: Int!) {
|
|
7336
|
+
getTests(jql: $jql, limit: $limit, start: $start) {
|
|
7337
|
+
total
|
|
7338
|
+
results {
|
|
7339
|
+
issueId
|
|
7340
|
+
jira(fields: ["key", "summary", "description"])
|
|
7341
|
+
steps { id action data result }
|
|
7342
|
+
}
|
|
7343
|
+
}
|
|
7344
|
+
}`,
|
|
7345
|
+
{ jql, limit: PAGE_LIMIT2, start }
|
|
7346
|
+
);
|
|
7347
|
+
const page = data.getTests?.results ?? [];
|
|
7348
|
+
tests.push(...page);
|
|
7349
|
+
if (page.length < PAGE_LIMIT2) break;
|
|
7350
|
+
}
|
|
7351
|
+
return tests.map(toRemoteCase);
|
|
7352
|
+
},
|
|
7353
|
+
async createCase(body) {
|
|
7354
|
+
const data = await graphql(
|
|
7355
|
+
`mutation($testType: UpdateTestTypeInput!, $steps: [CreateStepInput], $jira: JSON!) {
|
|
7356
|
+
createTest(testType: $testType, steps: $steps, jira: $jira) {
|
|
7357
|
+
test { issueId jira(fields: ["key", "summary", "description"]) }
|
|
7358
|
+
warnings
|
|
7359
|
+
}
|
|
7360
|
+
}`,
|
|
7361
|
+
{
|
|
7362
|
+
testType: { name: config.testType ?? DEFAULT_TEST_TYPE },
|
|
7363
|
+
steps: body.steps.map((step) => ({ action: encodeStepText(step), result: "" })),
|
|
7364
|
+
jira: {
|
|
7365
|
+
fields: {
|
|
7366
|
+
summary: body.title,
|
|
7367
|
+
description: toAdf(encodeDescription(body)),
|
|
7368
|
+
project: { key: config.projectKey }
|
|
7369
|
+
}
|
|
7370
|
+
}
|
|
7371
|
+
}
|
|
7372
|
+
);
|
|
7373
|
+
const created = data.createTest?.test;
|
|
7374
|
+
if (!created) throw new Error(`Xray createTest returned no test for "${body.title}"`);
|
|
7375
|
+
for (const warning of data.createTest?.warnings ?? []) deps.logger.warn(`Xray: ${warning}`);
|
|
7376
|
+
return toRemoteCase(created);
|
|
7377
|
+
},
|
|
7378
|
+
async updateCase(id, body) {
|
|
7379
|
+
const issueId = issueIds.get(id);
|
|
7380
|
+
if (!issueId) {
|
|
7381
|
+
throw new Error(`Xray: no internal issue id cached for ${id}. Run listCases first.`);
|
|
7382
|
+
}
|
|
7383
|
+
const current = await graphql(
|
|
7384
|
+
`query($issueId: String!) { getTest(issueId: $issueId) { steps { id } } }`,
|
|
7385
|
+
{ issueId }
|
|
7386
|
+
);
|
|
7387
|
+
const existing = current.getTest?.steps ?? [];
|
|
7388
|
+
for (const [index, step] of body.steps.entries()) {
|
|
7389
|
+
const action = encodeStepText(step);
|
|
7390
|
+
const target = existing[index];
|
|
7391
|
+
if (target) {
|
|
7392
|
+
await graphql(
|
|
7393
|
+
`mutation($stepId: String!, $step: UpdateStepInput!) {
|
|
7394
|
+
updateTestStep(stepId: $stepId, step: $step)
|
|
7395
|
+
}`,
|
|
7396
|
+
{ stepId: target.id, step: { action, result: "" } }
|
|
7397
|
+
);
|
|
7398
|
+
} else {
|
|
7399
|
+
await graphql(
|
|
7400
|
+
`mutation($issueId: String!, $step: CreateStepInput!) {
|
|
7401
|
+
addTestStep(issueId: $issueId, step: $step) { id }
|
|
7402
|
+
}`,
|
|
7403
|
+
{ issueId, step: { action, result: "" } }
|
|
7404
|
+
);
|
|
7405
|
+
}
|
|
7406
|
+
}
|
|
7407
|
+
for (const surplus of existing.slice(body.steps.length).reverse()) {
|
|
7408
|
+
await graphql(`mutation($stepId: String!) { removeTestStep(stepId: $stepId) }`, {
|
|
7409
|
+
stepId: surplus.id
|
|
7410
|
+
});
|
|
7411
|
+
}
|
|
7412
|
+
await jiraUpdate(id, {
|
|
7413
|
+
summary: body.title,
|
|
7414
|
+
description: toAdf(encodeDescription(body))
|
|
7415
|
+
});
|
|
7416
|
+
return {
|
|
7417
|
+
id,
|
|
7418
|
+
url: issueUrl(id),
|
|
7419
|
+
title: body.title,
|
|
7420
|
+
body
|
|
7421
|
+
};
|
|
7422
|
+
},
|
|
7423
|
+
async recordResults(results) {
|
|
7424
|
+
const statuses = {
|
|
7425
|
+
passed: config.statuses?.passed ?? "PASSED",
|
|
7426
|
+
failed: config.statuses?.failed ?? "FAILED",
|
|
7427
|
+
skipped: config.statuses?.skipped ?? "TODO"
|
|
7428
|
+
};
|
|
7429
|
+
let attachmentsUploaded = 0;
|
|
7430
|
+
const tests = results.map((result) => {
|
|
7431
|
+
const evidence = (result.attachments ?? []).map((attachment) => {
|
|
7432
|
+
attachmentsUploaded += 1;
|
|
7433
|
+
return {
|
|
7434
|
+
data: Buffer.from(attachment.body).toString("base64"),
|
|
7435
|
+
filename: attachment.filename,
|
|
7436
|
+
contentType: attachment.mediaType
|
|
7437
|
+
};
|
|
7438
|
+
});
|
|
7439
|
+
const comment = [result.message, result.url ? `Living documentation: ${result.url}` : void 0].filter(Boolean).join("\n\n");
|
|
7440
|
+
return {
|
|
7441
|
+
testKey: result.caseId,
|
|
7442
|
+
status: statuses[result.status],
|
|
7443
|
+
...comment ? { comment } : {},
|
|
7444
|
+
...evidence.length > 0 ? { evidence } : {}
|
|
7445
|
+
};
|
|
7446
|
+
});
|
|
7447
|
+
const bearer = await authenticate();
|
|
7448
|
+
const payload = {
|
|
7449
|
+
...config.testExecutionKey ? { testExecutionKey: config.testExecutionKey } : {},
|
|
7450
|
+
info: {
|
|
7451
|
+
summary: `${config.executionSummary ?? "executable-stories"} ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
7452
|
+
project: config.projectKey,
|
|
7453
|
+
...config.testPlanKey ? { testPlanKey: config.testPlanKey } : {}
|
|
7454
|
+
},
|
|
7455
|
+
tests
|
|
7456
|
+
};
|
|
7457
|
+
const response = await deps.fetch(`${xrayBase}/api/v2/import/execution`, {
|
|
7458
|
+
method: "POST",
|
|
7459
|
+
headers: { Authorization: `Bearer ${bearer}`, "Content-Type": "application/json" },
|
|
7460
|
+
body: JSON.stringify(payload)
|
|
7461
|
+
});
|
|
7462
|
+
const text2 = await response.text();
|
|
7463
|
+
if (!response.ok) {
|
|
7464
|
+
throw new Error(`Xray import execution failed (${response.status}): ${text2}`);
|
|
7465
|
+
}
|
|
7466
|
+
const imported = JSON.parse(text2);
|
|
7467
|
+
return {
|
|
7468
|
+
runId: imported.key,
|
|
7469
|
+
runUrl: imported.key ? issueUrl(imported.key) : void 0,
|
|
7470
|
+
recorded: tests.length,
|
|
7471
|
+
skipped: [],
|
|
7472
|
+
attachmentsUploaded
|
|
7473
|
+
};
|
|
7474
|
+
}
|
|
7475
|
+
};
|
|
7476
|
+
}
|
|
7477
|
+
|
|
7478
|
+
// src/sync/adapters/index.ts
|
|
7479
|
+
var PROVIDER_NAMES = ["testrail", "xray"];
|
|
7480
|
+
function isProviderName(value) {
|
|
7481
|
+
return PROVIDER_NAMES.includes(value);
|
|
7482
|
+
}
|
|
7483
|
+
function required(env, name, hint) {
|
|
7484
|
+
const value = env[name];
|
|
7485
|
+
if (!value) throw new Error(`Missing ${name}. ${hint}`);
|
|
7486
|
+
return value;
|
|
7487
|
+
}
|
|
7488
|
+
function buildProvider(args, deps) {
|
|
7489
|
+
const { name, targets, env } = args;
|
|
7490
|
+
if (name === "testrail") {
|
|
7491
|
+
const config2 = targets.testrail;
|
|
7492
|
+
if (!config2) {
|
|
7493
|
+
throw new Error(
|
|
7494
|
+
'No TestRail config found. Add a `sync: { testrail: { url, projectId } }` block to executable-stories.config.mjs, or run "executable-stories sync testrail --init".'
|
|
7495
|
+
);
|
|
7496
|
+
}
|
|
7497
|
+
const provider2 = createTestRailProvider(
|
|
7498
|
+
config2,
|
|
7499
|
+
{
|
|
7500
|
+
username: required(env, "TESTRAIL_USERNAME", "This is the login email of the TestRail account."),
|
|
7501
|
+
apiKey: required(
|
|
7502
|
+
env,
|
|
7503
|
+
"TESTRAIL_API_KEY",
|
|
7504
|
+
"Generate one in TestRail under My Settings -> API Keys. A password will not work when the instance requires API keys."
|
|
7505
|
+
)
|
|
7506
|
+
},
|
|
7507
|
+
deps
|
|
7508
|
+
);
|
|
7509
|
+
return {
|
|
7510
|
+
provider: provider2,
|
|
7511
|
+
// TestRail ids are numeric and conventionally written "C1234" in a ticket,
|
|
7512
|
+
// so the prefix is decoration and gets stripped.
|
|
7513
|
+
engineDefaults: { ticketPrefix: "C", ticketPrefixStrip: true }
|
|
7514
|
+
};
|
|
7515
|
+
}
|
|
7516
|
+
const config = targets.xray;
|
|
7517
|
+
if (!config) {
|
|
7518
|
+
throw new Error(
|
|
7519
|
+
'No Xray config found. Add a `sync: { xray: { jiraBaseUrl, projectKey } }` block to executable-stories.config.mjs, or run "executable-stories sync xray --init".'
|
|
7520
|
+
);
|
|
7521
|
+
}
|
|
7522
|
+
const provider = createXrayProvider(
|
|
7523
|
+
config,
|
|
7524
|
+
{
|
|
7525
|
+
clientId: required(env, "XRAY_CLIENT_ID", "Create an API key pair in Jira under Apps -> Xray -> API Keys."),
|
|
7526
|
+
clientSecret: required(env, "XRAY_CLIENT_SECRET", "This is the secret half of the Xray API key pair."),
|
|
7527
|
+
jiraEmail: env["JIRA_EMAIL"],
|
|
7528
|
+
jiraToken: env["JIRA_TOKEN"]
|
|
7529
|
+
},
|
|
7530
|
+
deps
|
|
7531
|
+
);
|
|
7532
|
+
return {
|
|
7533
|
+
provider,
|
|
7534
|
+
// An Xray case id is a Jira issue key, so the project prefix is part of the
|
|
7535
|
+
// id and must survive.
|
|
7536
|
+
engineDefaults: { ticketPrefix: `${config.projectKey}-`, ticketPrefixStrip: false }
|
|
7537
|
+
};
|
|
7538
|
+
}
|
|
7539
|
+
|
|
7540
|
+
// src/index.ts
|
|
7541
|
+
import {
|
|
7542
|
+
STORY_REPORT_SCHEMA_VERSION,
|
|
7543
|
+
STORY_REPORT_SCHEMA_MAJOR
|
|
7544
|
+
} from "executable-stories-core/types/story-report";
|
|
7545
|
+
import { ES_THEME_TOKENS_CSS as ES_THEME_TOKENS_CSS2, ES_THEME_TOKEN_VALUES } from "executable-stories-core/theme/tokens";
|
|
7546
|
+
import { canonicalizeRun as canonicalizeRun3 } from "executable-stories-core/converters/acl/index";
|
|
7547
|
+
import { normalizeStatus } from "executable-stories-core/converters/acl/index";
|
|
7548
|
+
import { generateTestCaseId } from "executable-stories-core/converters/acl/index";
|
|
7549
|
+
import { generateRunId } from "executable-stories-core/converters/acl/index";
|
|
7550
|
+
import { slugify as slugify2 } from "executable-stories-core/converters/acl/index";
|
|
7551
|
+
import { deriveStepResults } from "executable-stories-core/converters/acl/index";
|
|
7552
|
+
import { mergeStepResults } from "executable-stories-core/converters/acl/index";
|
|
7553
|
+
import { resolveAttachment } from "executable-stories-core/converters/acl/index";
|
|
7554
|
+
import { resolveAttachments } from "executable-stories-core/converters/acl/index";
|
|
7555
|
+
import {
|
|
7556
|
+
validateCanonicalRun,
|
|
7557
|
+
assertValidRun
|
|
7558
|
+
} from "executable-stories-core/converters/acl/validate";
|
|
7559
|
+
|
|
7560
|
+
// src/watch.ts
|
|
7561
|
+
import * as fs5 from "fs";
|
|
7562
|
+
import * as path6 from "path";
|
|
7563
|
+
import { canonicalizeRun } from "executable-stories-core/converters/acl/index";
|
|
7564
|
+
import { synthesizeStories } from "executable-stories-core/converters/synthesize";
|
|
7565
|
+
function toRun(data, inputType, synthesize) {
|
|
7566
|
+
if (inputType === "canonical") return data;
|
|
7567
|
+
let raw = data;
|
|
7568
|
+
if (synthesize) raw = synthesizeStories(raw);
|
|
7569
|
+
return canonicalizeRun(raw);
|
|
7570
|
+
}
|
|
7571
|
+
async function regenerateRun(options, deps = {}) {
|
|
7572
|
+
const read = deps.readFile ?? ((filePath) => fs5.readFileSync(filePath, "utf8"));
|
|
7573
|
+
const data = JSON.parse(read(path6.resolve(options.input)));
|
|
7574
|
+
const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
|
|
7575
|
+
const generator = new ReportGenerator({
|
|
7576
|
+
formats: options.formats,
|
|
7577
|
+
outputDir: options.outputDir,
|
|
7578
|
+
outputName: options.outputName
|
|
7579
|
+
});
|
|
7580
|
+
const result = await generator.generate(run);
|
|
7581
|
+
return { files: [...result.values()].flat(), run };
|
|
7582
|
+
}
|
|
7583
|
+
async function regenerateArtifacts(options, deps = {}) {
|
|
7584
|
+
return (await regenerateRun(options, deps)).files;
|
|
7585
|
+
}
|
|
7586
|
+
function startWatch(options, deps = {}) {
|
|
7587
|
+
const log = deps.log ?? ((message) => console.log(message));
|
|
7588
|
+
const regenerate = deps.regenerate ?? ((input) => regenerateArtifacts({ ...options, input }, deps));
|
|
7589
|
+
const watchFn = deps.watch ?? ((filePath, listener) => fs5.watch(filePath, listener));
|
|
7590
|
+
const debounceMs = options.debounceMs ?? 150;
|
|
7591
|
+
let timer;
|
|
7592
|
+
let running = false;
|
|
7593
|
+
let pending = false;
|
|
7594
|
+
const run = async () => {
|
|
7595
|
+
if (running) {
|
|
7596
|
+
pending = true;
|
|
7597
|
+
return;
|
|
7598
|
+
}
|
|
7599
|
+
running = true;
|
|
7600
|
+
try {
|
|
7601
|
+
const files = await regenerate(options.input);
|
|
7602
|
+
log(`Regenerated ${files.length} artifact file(s) from ${options.input}`);
|
|
7603
|
+
} catch (error) {
|
|
7604
|
+
log(`Watch regeneration failed: ${error.message}`);
|
|
7605
|
+
} finally {
|
|
7606
|
+
running = false;
|
|
7607
|
+
if (pending) {
|
|
7608
|
+
pending = false;
|
|
7609
|
+
trigger();
|
|
7610
|
+
}
|
|
7611
|
+
}
|
|
7612
|
+
};
|
|
7613
|
+
const trigger = () => {
|
|
7614
|
+
if (timer) clearTimeout(timer);
|
|
7615
|
+
timer = setTimeout(() => void run(), debounceMs);
|
|
7616
|
+
};
|
|
7617
|
+
trigger();
|
|
7618
|
+
const watcher = watchFn(path6.resolve(options.input), trigger);
|
|
7619
|
+
return {
|
|
7620
|
+
close: () => {
|
|
7621
|
+
if (timer) clearTimeout(timer);
|
|
7622
|
+
watcher.close();
|
|
7623
|
+
}
|
|
7624
|
+
};
|
|
7625
|
+
}
|
|
7626
|
+
|
|
7627
|
+
// src/index.ts
|
|
7628
|
+
import { advanceState, initialRunState } from "executable-stories-core";
|
|
7629
|
+
import { toStoryReport as toStoryReport6 } from "executable-stories-core/converters/story-report";
|
|
7630
|
+
|
|
7631
|
+
// src/publishers/confluence.ts
|
|
7632
|
+
function parseAdf(adf) {
|
|
7633
|
+
let parsed;
|
|
7634
|
+
try {
|
|
7635
|
+
parsed = JSON.parse(adf);
|
|
7636
|
+
} catch (err) {
|
|
7637
|
+
throw new Error(
|
|
7638
|
+
`ADF payload is not valid JSON: ${err.message}`
|
|
7639
|
+
);
|
|
7640
|
+
}
|
|
7641
|
+
if (!parsed || typeof parsed !== "object" || parsed.type !== "doc" || !Array.isArray(parsed.content)) {
|
|
7642
|
+
throw new Error(
|
|
7643
|
+
`ADF payload must be an object with { version, type: "doc", content: [...] }`
|
|
7644
|
+
);
|
|
7645
|
+
}
|
|
7646
|
+
return parsed;
|
|
7647
|
+
}
|
|
7648
|
+
function basicAuthHeader(auth) {
|
|
7649
|
+
const raw = `${auth.email}:${auth.token}`;
|
|
7650
|
+
const encoded = typeof Buffer !== "undefined" ? Buffer.from(raw, "utf8").toString("base64") : btoa(raw);
|
|
7651
|
+
return `Basic ${encoded}`;
|
|
7652
|
+
}
|
|
7653
|
+
async function parseErrorBody(response) {
|
|
7654
|
+
try {
|
|
7655
|
+
const body = await response.text();
|
|
7656
|
+
return body ? body.slice(0, 800) : "";
|
|
7657
|
+
} catch {
|
|
7658
|
+
return "";
|
|
7659
|
+
}
|
|
7660
|
+
}
|
|
7661
|
+
async function publishConfluencePage(args, deps) {
|
|
7662
|
+
parseAdf(args.adf);
|
|
7663
|
+
if (!args.pageId && !args.spaceId) {
|
|
7664
|
+
throw new Error(
|
|
7665
|
+
"publishConfluencePage requires either pageId (update) or spaceId (create)"
|
|
7666
|
+
);
|
|
7667
|
+
}
|
|
7668
|
+
if (!args.pageId && !args.title) {
|
|
7669
|
+
throw new Error("Creating a new page requires a title");
|
|
7670
|
+
}
|
|
7671
|
+
const base = args.baseUrl.replace(/\/$/, "");
|
|
7672
|
+
const fetchFn = deps.fetch ?? globalThis.fetch;
|
|
7673
|
+
if (!fetchFn) {
|
|
7674
|
+
throw new Error("No fetch implementation available (Node >= 22 expected)");
|
|
7675
|
+
}
|
|
7676
|
+
const headers = {
|
|
7677
|
+
Authorization: basicAuthHeader(deps.auth),
|
|
7678
|
+
Accept: "application/json",
|
|
7679
|
+
"Content-Type": "application/json"
|
|
7680
|
+
};
|
|
7681
|
+
if (args.pageId) {
|
|
7682
|
+
return updatePage(args, base, headers, fetchFn);
|
|
7683
|
+
}
|
|
7684
|
+
return createPage(args, base, headers, fetchFn);
|
|
7685
|
+
}
|
|
7686
|
+
async function updatePage(args, base, headers, fetchFn) {
|
|
7687
|
+
const getUrl = `${base}/api/v2/pages/${encodeURIComponent(args.pageId)}`;
|
|
7688
|
+
const getResp = await fetchFn(getUrl, { method: "GET", headers });
|
|
7689
|
+
if (!getResp.ok) {
|
|
7690
|
+
const body = await parseErrorBody(getResp);
|
|
7691
|
+
throw new Error(
|
|
7692
|
+
`GET ${getUrl} failed with ${getResp.status} ${getResp.statusText}${body ? `: ${body}` : ""}`
|
|
7693
|
+
);
|
|
7694
|
+
}
|
|
7695
|
+
const current = await getResp.json();
|
|
7696
|
+
const nextVersion = current.version.number + 1;
|
|
7697
|
+
const title = args.title ?? current.title;
|
|
7698
|
+
const putUrl = `${base}/api/v2/pages/${encodeURIComponent(args.pageId)}`;
|
|
7699
|
+
const putResp = await fetchFn(putUrl, {
|
|
7700
|
+
method: "PUT",
|
|
7701
|
+
headers,
|
|
7702
|
+
body: JSON.stringify({
|
|
7703
|
+
id: args.pageId,
|
|
7704
|
+
status: "current",
|
|
7705
|
+
title,
|
|
7706
|
+
body: {
|
|
7707
|
+
representation: "atlas_doc_format",
|
|
7708
|
+
value: args.adf
|
|
7709
|
+
},
|
|
7710
|
+
version: { number: nextVersion }
|
|
7711
|
+
})
|
|
7712
|
+
});
|
|
7713
|
+
if (!putResp.ok) {
|
|
7714
|
+
const body = await parseErrorBody(putResp);
|
|
7715
|
+
throw new Error(
|
|
7716
|
+
`PUT ${putUrl} failed with ${putResp.status} ${putResp.statusText}${body ? `: ${body}` : ""}`
|
|
7717
|
+
);
|
|
7718
|
+
}
|
|
7719
|
+
const updated = await putResp.json();
|
|
7720
|
+
return {
|
|
7721
|
+
id: updated.id,
|
|
7722
|
+
title: updated.title,
|
|
7723
|
+
version: updated.version.number,
|
|
7724
|
+
url: buildPageUrl(base, updated._links?.webui, updated.id),
|
|
7725
|
+
action: "updated"
|
|
7726
|
+
};
|
|
7727
|
+
}
|
|
7728
|
+
async function createPage(args, base, headers, fetchFn) {
|
|
7729
|
+
const body = {
|
|
7730
|
+
spaceId: args.spaceId,
|
|
7731
|
+
status: "current",
|
|
7732
|
+
title: args.title,
|
|
7733
|
+
body: {
|
|
7734
|
+
representation: "atlas_doc_format",
|
|
7735
|
+
value: args.adf
|
|
7736
|
+
}
|
|
7737
|
+
};
|
|
7738
|
+
if (args.parentId) body.parentId = args.parentId;
|
|
7739
|
+
const postUrl = `${base}/api/v2/pages`;
|
|
7740
|
+
const resp = await fetchFn(postUrl, {
|
|
7741
|
+
method: "POST",
|
|
7742
|
+
headers,
|
|
7743
|
+
body: JSON.stringify(body)
|
|
7744
|
+
});
|
|
7745
|
+
if (!resp.ok) {
|
|
7746
|
+
const errBody = await parseErrorBody(resp);
|
|
7747
|
+
throw new Error(
|
|
7748
|
+
`POST ${postUrl} failed with ${resp.status} ${resp.statusText}${errBody ? `: ${errBody}` : ""}`
|
|
7749
|
+
);
|
|
7750
|
+
}
|
|
7751
|
+
const created = await resp.json();
|
|
7752
|
+
return {
|
|
7753
|
+
id: created.id,
|
|
7754
|
+
title: created.title,
|
|
7755
|
+
version: created.version.number,
|
|
7756
|
+
url: buildPageUrl(base, created._links?.webui, created.id),
|
|
7757
|
+
action: "created"
|
|
7758
|
+
};
|
|
7759
|
+
}
|
|
7760
|
+
function buildPageUrl(base, webui, id) {
|
|
7761
|
+
if (webui) {
|
|
7762
|
+
return webui.startsWith("http") ? webui : `${base}${webui}`;
|
|
7763
|
+
}
|
|
7764
|
+
return `${base}/pages/${id}`;
|
|
6384
7765
|
}
|
|
6385
7766
|
|
|
6386
7767
|
// src/publishers/jira.ts
|
|
@@ -7513,7 +8894,7 @@ function statusIcon2(status) {
|
|
|
7513
8894
|
return "\u2022";
|
|
7514
8895
|
}
|
|
7515
8896
|
}
|
|
7516
|
-
function
|
|
8897
|
+
function escapeCell3(value) {
|
|
7517
8898
|
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
7518
8899
|
}
|
|
7519
8900
|
function intentSummary(intent) {
|
|
@@ -7542,7 +8923,7 @@ function renderWeakBand(lines, files) {
|
|
|
7542
8923
|
lines.push(`## \u{1F7E1} Changed code with weak evidence (${weak.length})`);
|
|
7543
8924
|
lines.push("");
|
|
7544
8925
|
for (const file of weak) {
|
|
7545
|
-
const covered = file.claims.map((c) => `${
|
|
8926
|
+
const covered = file.claims.map((c) => `${escapeCell3(c.scenario)} (${c.strength})`).join(", ");
|
|
7546
8927
|
lines.push(`- \`${file.path}\` _(${file.changeKind})_ \u2014 only: ${covered}`);
|
|
7547
8928
|
}
|
|
7548
8929
|
lines.push("");
|
|
@@ -7567,7 +8948,7 @@ function renderClaim(lines, claim) {
|
|
|
7567
8948
|
);
|
|
7568
8949
|
}
|
|
7569
8950
|
if (claim.intent) {
|
|
7570
|
-
lines.push(`- Why: ${
|
|
8951
|
+
lines.push(`- Why: ${escapeCell3(intentSummary(claim.intent))}`);
|
|
7571
8952
|
}
|
|
7572
8953
|
lines.push("");
|
|
7573
8954
|
}
|
|
@@ -7624,7 +9005,7 @@ function renderCodeDiff(lines, evidence) {
|
|
|
7624
9005
|
} else {
|
|
7625
9006
|
for (const ref of annotation.scenarios) {
|
|
7626
9007
|
lines.push(
|
|
7627
|
-
ref.resolved && ref.status ? `- ${statusIcon2(ref.status)} ${
|
|
9008
|
+
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)`
|
|
7628
9009
|
);
|
|
7629
9010
|
}
|
|
7630
9011
|
}
|
|
@@ -7843,9 +9224,9 @@ function renderDiffHunk(file, hunk, anchoredStart, anchoredCount) {
|
|
|
7843
9224
|
const sign = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
|
|
7844
9225
|
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>`;
|
|
7845
9226
|
});
|
|
7846
|
-
const
|
|
9227
|
+
const path20 = file.newPath ?? file.oldPath ?? "";
|
|
7847
9228
|
return `<div class="diff-hunk">
|
|
7848
|
-
<div class="diff-file-header"><code>${escapeHtml3(
|
|
9229
|
+
<div class="diff-file-header"><code>${escapeHtml3(path20)}</code> <span class="subtle">@@ -${hunk.oldStart} +${hunk.newStart} @@ ${escapeHtml3(hunk.header)}</span></div>
|
|
7849
9230
|
<table class="diff-table"><tbody>${rows.join("")}</tbody></table>
|
|
7850
9231
|
</div>`;
|
|
7851
9232
|
}
|
|
@@ -8080,8 +9461,8 @@ applyTheme(getEffectiveTheme());` : "";
|
|
|
8080
9461
|
};
|
|
8081
9462
|
|
|
8082
9463
|
// src/deploy/ledger.ts
|
|
8083
|
-
import * as
|
|
8084
|
-
import * as
|
|
9464
|
+
import * as fs6 from "fs";
|
|
9465
|
+
import * as path7 from "path";
|
|
8085
9466
|
function createEmptyLedger() {
|
|
8086
9467
|
return {
|
|
8087
9468
|
deployments: [],
|
|
@@ -8089,12 +9470,12 @@ function createEmptyLedger() {
|
|
|
8089
9470
|
};
|
|
8090
9471
|
}
|
|
8091
9472
|
function loadLedger(ledgerPath) {
|
|
8092
|
-
const resolved =
|
|
8093
|
-
if (!
|
|
9473
|
+
const resolved = path7.resolve(ledgerPath);
|
|
9474
|
+
if (!fs6.existsSync(resolved)) {
|
|
8094
9475
|
return createEmptyLedger();
|
|
8095
9476
|
}
|
|
8096
9477
|
try {
|
|
8097
|
-
const raw = JSON.parse(
|
|
9478
|
+
const raw = JSON.parse(fs6.readFileSync(resolved, "utf8"));
|
|
8098
9479
|
if (raw.schemaVersion !== 1) {
|
|
8099
9480
|
throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
|
|
8100
9481
|
}
|
|
@@ -8105,10 +9486,10 @@ function loadLedger(ledgerPath) {
|
|
|
8105
9486
|
}
|
|
8106
9487
|
}
|
|
8107
9488
|
function saveLedger(ledger, ledgerPath) {
|
|
8108
|
-
const resolved =
|
|
8109
|
-
const dir =
|
|
8110
|
-
|
|
8111
|
-
|
|
9489
|
+
const resolved = path7.resolve(ledgerPath);
|
|
9490
|
+
const dir = path7.dirname(resolved);
|
|
9491
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
9492
|
+
fs6.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
|
|
8112
9493
|
}
|
|
8113
9494
|
function getLatestDeployment(ledger, environment) {
|
|
8114
9495
|
return [...ledger.deployments].reverse().find((d) => d.environment === environment);
|
|
@@ -8235,11 +9616,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
8235
9616
|
const ext = FORMAT_EXTENSIONS[format];
|
|
8236
9617
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
8237
9618
|
if (mode === "aggregated") {
|
|
8238
|
-
return toPosix(
|
|
9619
|
+
return toPosix(path8.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
|
|
8239
9620
|
}
|
|
8240
9621
|
const normalizedSource = toPosix(sourceFile);
|
|
8241
|
-
const dirOfSource =
|
|
8242
|
-
let baseName =
|
|
9622
|
+
const dirOfSource = path8.posix.dirname(normalizedSource);
|
|
9623
|
+
let baseName = path8.posix.basename(normalizedSource);
|
|
8243
9624
|
for (const testExt of TEST_EXTENSIONS) {
|
|
8244
9625
|
if (baseName.endsWith(testExt)) {
|
|
8245
9626
|
baseName = baseName.slice(0, -testExt.length);
|
|
@@ -8248,12 +9629,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
8248
9629
|
}
|
|
8249
9630
|
const fileName = `${baseName}.${effectiveName}${ext}`;
|
|
8250
9631
|
if (colocatedStyle === "adjacent") {
|
|
8251
|
-
return toPosix(
|
|
9632
|
+
return toPosix(path8.posix.join(dirOfSource, fileName));
|
|
8252
9633
|
}
|
|
8253
9634
|
if (colocatedStyle === "flat") {
|
|
8254
|
-
return toPosix(
|
|
9635
|
+
return toPosix(path8.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
|
|
8255
9636
|
}
|
|
8256
|
-
return toPosix(
|
|
9637
|
+
return toPosix(path8.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
8257
9638
|
}
|
|
8258
9639
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
8259
9640
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -8473,8 +9854,8 @@ var ReportGenerator = class {
|
|
|
8473
9854
|
if (astroPaths) {
|
|
8474
9855
|
for (const mdPath of astroPaths) {
|
|
8475
9856
|
const content = await fsPromises.readFile(mdPath, "utf8");
|
|
8476
|
-
const mdDir =
|
|
8477
|
-
const assetsDir =
|
|
9857
|
+
const mdDir = path8.dirname(mdPath);
|
|
9858
|
+
const assetsDir = path8.resolve(this.options.astro.assetsDir);
|
|
8478
9859
|
const result = copyMarkdownAssets({
|
|
8479
9860
|
markdown: content,
|
|
8480
9861
|
markdownDir: mdDir,
|
|
@@ -8522,16 +9903,16 @@ var ReportGenerator = class {
|
|
|
8522
9903
|
bySourceFile.set(sourceFile, outputPath);
|
|
8523
9904
|
}
|
|
8524
9905
|
if (bySourceFile.size === 0) return void 0;
|
|
8525
|
-
const indexPath = toPosix(
|
|
9906
|
+
const indexPath = toPosix(path8.join(this.options.outputDir, "index.html"));
|
|
8526
9907
|
if (htmlPaths.some((p) => toPosix(p) === indexPath)) {
|
|
8527
9908
|
this.deps.logger.warn?.(
|
|
8528
9909
|
`Skipping colocated index: a report already occupies ${indexPath}.`
|
|
8529
9910
|
);
|
|
8530
9911
|
return void 0;
|
|
8531
9912
|
}
|
|
8532
|
-
const entries = buildIndexEntries(run, bySourceFile,
|
|
9913
|
+
const entries = buildIndexEntries(run, bySourceFile, path8.dirname(indexPath));
|
|
8533
9914
|
const html = renderColocatedIndex(entries, this.options.html.title);
|
|
8534
|
-
await fsPromises.mkdir(
|
|
9915
|
+
await fsPromises.mkdir(path8.dirname(indexPath), { recursive: true });
|
|
8535
9916
|
await this.deps.writeFile(indexPath, html);
|
|
8536
9917
|
return indexPath;
|
|
8537
9918
|
}
|
|
@@ -8550,9 +9931,9 @@ var ReportGenerator = class {
|
|
|
8550
9931
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
8551
9932
|
const ext = FORMAT_EXTENSIONS[format];
|
|
8552
9933
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
8553
|
-
const outputPath = toPosix(
|
|
9934
|
+
const outputPath = toPosix(path8.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
|
|
8554
9935
|
const content = await this.formatContent(run, format);
|
|
8555
|
-
const dir =
|
|
9936
|
+
const dir = path8.dirname(outputPath);
|
|
8556
9937
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
8557
9938
|
await this.deps.writeFile(outputPath, content);
|
|
8558
9939
|
return [outputPath];
|
|
@@ -8564,7 +9945,7 @@ var ReportGenerator = class {
|
|
|
8564
9945
|
testCases
|
|
8565
9946
|
};
|
|
8566
9947
|
const content = await this.formatContent(groupRun, format);
|
|
8567
|
-
const dir =
|
|
9948
|
+
const dir = path8.dirname(outputPath);
|
|
8568
9949
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
8569
9950
|
await this.deps.writeFile(outputPath, content);
|
|
8570
9951
|
writtenPaths.push(outputPath);
|
|
@@ -8752,7 +10133,7 @@ async function generateRunComparison(args) {
|
|
|
8752
10133
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
8753
10134
|
for (const format of args.formats) {
|
|
8754
10135
|
const ext = format === "html" ? ".html" : format === "changelog" ? ".changelog.md" : ".md";
|
|
8755
|
-
const outputPath = toPosix(
|
|
10136
|
+
const outputPath = toPosix(path8.join(outputDir, `${outputName}${ext}`));
|
|
8756
10137
|
const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : format === "changelog" ? new RunDiffChangelogFormatter().format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
|
|
8757
10138
|
await fsPromises.writeFile(outputPath, content, "utf8");
|
|
8758
10139
|
files.push(outputPath);
|
|
@@ -8765,8 +10146,8 @@ import { parseNdjson as parseNdjson2 } from "executable-stories-core/converters/
|
|
|
8765
10146
|
import { toCIInfo as toCIInfo2 } from "executable-stories-core/types/ci";
|
|
8766
10147
|
|
|
8767
10148
|
// src/artifacts-readme.ts
|
|
8768
|
-
import * as
|
|
8769
|
-
import * as
|
|
10149
|
+
import * as fs7 from "fs";
|
|
10150
|
+
import * as path9 from "path";
|
|
8770
10151
|
var README = `# executable-stories artifacts
|
|
8771
10152
|
|
|
8772
10153
|
Generated by your test reporter and the \`executable-stories\` CLI. Safe to
|
|
@@ -8792,10 +10173,10 @@ Useful commands (all take \`raw-run.json\`):
|
|
|
8792
10173
|
`;
|
|
8793
10174
|
function writeArtifactsReadme(outputDir) {
|
|
8794
10175
|
try {
|
|
8795
|
-
const target =
|
|
8796
|
-
if (
|
|
8797
|
-
|
|
8798
|
-
|
|
10176
|
+
const target = path9.join(outputDir, "README.md");
|
|
10177
|
+
if (fs7.existsSync(target)) return false;
|
|
10178
|
+
fs7.mkdirSync(outputDir, { recursive: true });
|
|
10179
|
+
fs7.writeFileSync(target, README, "utf8");
|
|
8799
10180
|
return true;
|
|
8800
10181
|
} catch {
|
|
8801
10182
|
return false;
|
|
@@ -8803,8 +10184,8 @@ function writeArtifactsReadme(outputDir) {
|
|
|
8803
10184
|
}
|
|
8804
10185
|
|
|
8805
10186
|
// src/explainers.ts
|
|
8806
|
-
import * as
|
|
8807
|
-
import * as
|
|
10187
|
+
import * as fs9 from "fs";
|
|
10188
|
+
import * as path11 from "path";
|
|
8808
10189
|
import Ajv2 from "ajv/dist/2020.js";
|
|
8809
10190
|
import { parse as parseYaml } from "yaml";
|
|
8810
10191
|
import {
|
|
@@ -8863,16 +10244,16 @@ var explainer_v1_default = {
|
|
|
8863
10244
|
};
|
|
8864
10245
|
|
|
8865
10246
|
// src/utils/markdown-files.ts
|
|
8866
|
-
import * as
|
|
8867
|
-
import * as
|
|
10247
|
+
import * as fs8 from "fs";
|
|
10248
|
+
import * as path10 from "path";
|
|
8868
10249
|
function collectMarkdownFiles(target) {
|
|
8869
|
-
if (!
|
|
8870
|
-
if (
|
|
10250
|
+
if (!fs8.existsSync(target)) return [];
|
|
10251
|
+
if (fs8.statSync(target).isFile()) return [target];
|
|
8871
10252
|
const out = [];
|
|
8872
10253
|
const walk = (dir) => {
|
|
8873
|
-
for (const entry of
|
|
10254
|
+
for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
|
|
8874
10255
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
8875
|
-
const full =
|
|
10256
|
+
const full = path10.join(dir, entry.name);
|
|
8876
10257
|
if (entry.isDirectory()) walk(full);
|
|
8877
10258
|
else if (/\.mdx?$/u.test(entry.name)) out.push(full);
|
|
8878
10259
|
}
|
|
@@ -8924,9 +10305,9 @@ function buildExplainersReport(args, _deps = {}) {
|
|
|
8924
10305
|
const scenarios = report.features.flatMap((feature) => feature.scenarios);
|
|
8925
10306
|
const explainers = [];
|
|
8926
10307
|
for (const file of collectMarkdownFiles(args.dir).sort()) {
|
|
8927
|
-
const parsed = parseExplainerDoc(
|
|
10308
|
+
const parsed = parseExplainerDoc(fs9.readFileSync(file, "utf8"));
|
|
8928
10309
|
if (!parsed) continue;
|
|
8929
|
-
const rel =
|
|
10310
|
+
const rel = path11.relative(args.dir, file);
|
|
8930
10311
|
if (!parsed.explainer) {
|
|
8931
10312
|
explainers.push({ file: rel, status: "invalid", errors: parsed.errors });
|
|
8932
10313
|
continue;
|
|
@@ -8995,13 +10376,13 @@ function renderExplainersReport(report, format) {
|
|
|
8995
10376
|
}
|
|
8996
10377
|
|
|
8997
10378
|
// src/run-file.ts
|
|
8998
|
-
import
|
|
8999
|
-
import
|
|
10379
|
+
import fs10 from "fs";
|
|
10380
|
+
import path12 from "path";
|
|
9000
10381
|
var DEFAULT_RUN_FILES = [".executable-stories/raw-run.json", "reports/raw-run.json"];
|
|
9001
10382
|
var SUPPORTED_RAW_RUN_SCHEMA = 1;
|
|
9002
10383
|
function findDefaultRunFile(cwd = process.cwd()) {
|
|
9003
10384
|
for (const candidate of DEFAULT_RUN_FILES) {
|
|
9004
|
-
if (
|
|
10385
|
+
if (fs10.existsSync(path12.resolve(cwd, candidate))) return candidate;
|
|
9005
10386
|
}
|
|
9006
10387
|
return void 0;
|
|
9007
10388
|
}
|
|
@@ -9027,8 +10408,8 @@ function diagnoseRunFile(file, cwd = process.cwd()) {
|
|
|
9027
10408
|
});
|
|
9028
10409
|
return { checks, healthy: false };
|
|
9029
10410
|
}
|
|
9030
|
-
const abs =
|
|
9031
|
-
if (!
|
|
10411
|
+
const abs = path12.resolve(cwd, resolved);
|
|
10412
|
+
if (!fs10.existsSync(abs)) {
|
|
9032
10413
|
checks.push({
|
|
9033
10414
|
label: "run file",
|
|
9034
10415
|
status: "fail",
|
|
@@ -9037,7 +10418,7 @@ function diagnoseRunFile(file, cwd = process.cwd()) {
|
|
|
9037
10418
|
});
|
|
9038
10419
|
return { checks, healthy: false };
|
|
9039
10420
|
}
|
|
9040
|
-
const stat =
|
|
10421
|
+
const stat = fs10.statSync(abs);
|
|
9041
10422
|
checks.push({
|
|
9042
10423
|
label: "run file",
|
|
9043
10424
|
status: "ok",
|
|
@@ -9045,7 +10426,7 @@ function diagnoseRunFile(file, cwd = process.cwd()) {
|
|
|
9045
10426
|
});
|
|
9046
10427
|
let parsed;
|
|
9047
10428
|
try {
|
|
9048
|
-
parsed = JSON.parse(
|
|
10429
|
+
parsed = JSON.parse(fs10.readFileSync(abs, "utf8"));
|
|
9049
10430
|
} catch (err) {
|
|
9050
10431
|
checks.push({
|
|
9051
10432
|
label: "json",
|
|
@@ -9177,6 +10558,8 @@ var COMPLETION_SUBCOMMANDS = [
|
|
|
9177
10558
|
["new", "Scaffold a docs page from a template"],
|
|
9178
10559
|
["check-links", "Scan docs for broken links"],
|
|
9179
10560
|
["push", "Send a run to Executable Stories Cloud"],
|
|
10561
|
+
["coverage", "Compare stories against a test-management system (read-only)"],
|
|
10562
|
+
["sync", "Push cases, executions, and evidence to TestRail or Xray"],
|
|
9180
10563
|
["import-openapi", "Generate API doc pages from an OpenAPI spec"],
|
|
9181
10564
|
["publish-confluence", "Publish an ADF JSON file to Confluence"],
|
|
9182
10565
|
["publish-jira", "Publish an ADF JSON file to a Jira issue"],
|
|
@@ -9198,8 +10581,16 @@ var COMMON_FLAGS = [
|
|
|
9198
10581
|
"--baseline",
|
|
9199
10582
|
"--baseline-dir",
|
|
9200
10583
|
"--emit-canonical",
|
|
10584
|
+
"--apply",
|
|
10585
|
+
"--attach",
|
|
10586
|
+
"--report-url",
|
|
9201
10587
|
"--help"
|
|
9202
10588
|
];
|
|
10589
|
+
var SUBCOMMAND_VALUES = {
|
|
10590
|
+
completion: ["bash", "zsh", "fish"],
|
|
10591
|
+
sync: [...PROVIDER_NAMES],
|
|
10592
|
+
coverage: [...PROVIDER_NAMES]
|
|
10593
|
+
};
|
|
9203
10594
|
var FLAG_VALUES = {
|
|
9204
10595
|
"--format": [
|
|
9205
10596
|
"html",
|
|
@@ -9221,12 +10612,13 @@ var FLAG_VALUES = {
|
|
|
9221
10612
|
"--preset": ["agent", "ci", "docs"],
|
|
9222
10613
|
"--input-type": ["raw", "canonical", "ndjson"],
|
|
9223
10614
|
"--list-format": ["text", "json", "csv", "markdown-table"],
|
|
9224
|
-
"--check-format": ["text", "json"]
|
|
10615
|
+
"--check-format": ["text", "json"],
|
|
10616
|
+
"--attach": ["failed", "all", "none"]
|
|
9225
10617
|
};
|
|
9226
10618
|
function bashScript() {
|
|
9227
10619
|
const subcommands = COMPLETION_SUBCOMMANDS.map(([name]) => name).join(" ");
|
|
9228
10620
|
const flags = COMMON_FLAGS.join(" ");
|
|
9229
|
-
const valueCases = Object.entries(FLAG_VALUES).map(([
|
|
10621
|
+
const valueCases = [...Object.entries(FLAG_VALUES), ...Object.entries(SUBCOMMAND_VALUES)].map(([word, values]) => ` ${word})
|
|
9230
10622
|
COMPREPLY=( $(compgen -W "${values.join(" ")}" -- "$cur") ); return 0 ;;`).join("\n");
|
|
9231
10623
|
return `# executable-stories bash completion
|
|
9232
10624
|
_executable_stories() {
|
|
@@ -9236,8 +10628,6 @@ _executable_stories() {
|
|
|
9236
10628
|
|
|
9237
10629
|
case "$prev" in
|
|
9238
10630
|
${valueCases}
|
|
9239
|
-
completion)
|
|
9240
|
-
COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") ); return 0 ;;
|
|
9241
10631
|
esac
|
|
9242
10632
|
|
|
9243
10633
|
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
@@ -9257,8 +10647,8 @@ function zshScript() {
|
|
|
9257
10647
|
const subcommands = COMPLETION_SUBCOMMANDS.map(([name, desc]) => ` '${name}:${desc.replaceAll("'", "")}'`).join(
|
|
9258
10648
|
"\n"
|
|
9259
10649
|
);
|
|
9260
|
-
const valueCases = Object.entries(FLAG_VALUES).map(([
|
|
9261
|
-
_values '${
|
|
10650
|
+
const valueCases = [...Object.entries(FLAG_VALUES), ...Object.entries(SUBCOMMAND_VALUES)].map(([word, values]) => ` ${word})
|
|
10651
|
+
_values '${word.replace(/^--/, "")}' ${values.join(" ")} ;;`).join("\n");
|
|
9262
10652
|
return `#compdef executable-stories
|
|
9263
10653
|
# executable-stories zsh completion
|
|
9264
10654
|
|
|
@@ -9270,8 +10660,6 @@ ${subcommands}
|
|
|
9270
10660
|
|
|
9271
10661
|
case "\${words[CURRENT-1]}" in
|
|
9272
10662
|
${valueCases}
|
|
9273
|
-
completion)
|
|
9274
|
-
_values 'shell' bash zsh fish ;;
|
|
9275
10663
|
*)
|
|
9276
10664
|
if (( CURRENT == 2 )); then
|
|
9277
10665
|
_describe 'subcommand' subcommands
|
|
@@ -9300,6 +10688,11 @@ function fishScript() {
|
|
|
9300
10688
|
`complete -c executable-stories -l ${flag.slice(2)} -x -a '${values.join(" ")}'`
|
|
9301
10689
|
);
|
|
9302
10690
|
}
|
|
10691
|
+
for (const [subcommand, values] of Object.entries(SUBCOMMAND_VALUES)) {
|
|
10692
|
+
lines.push(
|
|
10693
|
+
`complete -c executable-stories -n '__fish_seen_subcommand_from ${subcommand}' -x -a '${values.join(" ")}'`
|
|
10694
|
+
);
|
|
10695
|
+
}
|
|
9303
10696
|
lines.push("complete -c executable-stories -l open -d 'Open the HTML report when done'");
|
|
9304
10697
|
lines.push("complete -c executable-stories -l help -d 'Show help'");
|
|
9305
10698
|
return `# executable-stories fish completion
|
|
@@ -9334,7 +10727,7 @@ function runCompletion(args) {
|
|
|
9334
10727
|
|
|
9335
10728
|
// src/open-report.ts
|
|
9336
10729
|
import { spawn } from "child_process";
|
|
9337
|
-
import
|
|
10730
|
+
import path13 from "path";
|
|
9338
10731
|
function openCommand(platform) {
|
|
9339
10732
|
if (platform === "darwin") return { command: "open", args: [] };
|
|
9340
10733
|
if (platform === "win32") return { command: "cmd", args: ["/c", "start", ""] };
|
|
@@ -9350,7 +10743,7 @@ function openInBrowser(file, platform = process.platform) {
|
|
|
9350
10743
|
}
|
|
9351
10744
|
const { command, args } = openCommand(platform);
|
|
9352
10745
|
try {
|
|
9353
|
-
const child = spawn(command, [...args,
|
|
10746
|
+
const child = spawn(command, [...args, path13.resolve(file)], { stdio: "ignore", detached: true });
|
|
9354
10747
|
child.on("error", (err) => {
|
|
9355
10748
|
console.error(`--open: could not open ${file}: ${err.message}`);
|
|
9356
10749
|
});
|
|
@@ -9378,14 +10771,14 @@ function summaryLine(counts, files, durationMs) {
|
|
|
9378
10771
|
|
|
9379
10772
|
// src/init-astro.ts
|
|
9380
10773
|
import { spawnSync } from "child_process";
|
|
9381
|
-
import * as
|
|
9382
|
-
import * as
|
|
10774
|
+
import * as fs11 from "fs";
|
|
10775
|
+
import * as path14 from "path";
|
|
9383
10776
|
import { fileURLToPath } from "url";
|
|
9384
|
-
var __dirname =
|
|
10777
|
+
var __dirname = path14.dirname(fileURLToPath(import.meta.url));
|
|
9385
10778
|
function detectPackageManager(cwd = process.cwd()) {
|
|
9386
|
-
if (
|
|
9387
|
-
if (
|
|
9388
|
-
if (
|
|
10779
|
+
if (fs11.existsSync(path14.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
10780
|
+
if (fs11.existsSync(path14.join(cwd, "yarn.lock"))) return "yarn";
|
|
10781
|
+
if (fs11.existsSync(path14.join(cwd, "bun.lock")) || fs11.existsSync(path14.join(cwd, "bun.lockb"))) {
|
|
9389
10782
|
return "bun";
|
|
9390
10783
|
}
|
|
9391
10784
|
return "npm";
|
|
@@ -9406,7 +10799,7 @@ function runDocsDev(siteDir) {
|
|
|
9406
10799
|
return { kind: "not-scaffolded" };
|
|
9407
10800
|
}
|
|
9408
10801
|
const pm = detectPackageManager();
|
|
9409
|
-
if (!
|
|
10802
|
+
if (!fs11.existsSync(path14.join(siteDir, "node_modules"))) {
|
|
9410
10803
|
console.log(`Installing docs site dependencies with ${pm}\u2026`);
|
|
9411
10804
|
if (!installScaffoldDependencies(siteDir, pm)) {
|
|
9412
10805
|
return { kind: "install-failed", pm };
|
|
@@ -9417,14 +10810,14 @@ function runDocsDev(siteDir) {
|
|
|
9417
10810
|
}
|
|
9418
10811
|
var SCAFFOLD_MARKER = "executable-stories.config.mjs";
|
|
9419
10812
|
function isScaffoldedAstroSite(dir) {
|
|
9420
|
-
return
|
|
10813
|
+
return fs11.existsSync(path14.join(dir, SCAFFOLD_MARKER));
|
|
9421
10814
|
}
|
|
9422
10815
|
function initAstro(options = {}) {
|
|
9423
10816
|
const targetDir = options.targetDir ?? "./story-docs";
|
|
9424
10817
|
const force = options.force ?? false;
|
|
9425
10818
|
const update = options.update ?? false;
|
|
9426
|
-
const templateDir =
|
|
9427
|
-
if (!
|
|
10819
|
+
const templateDir = path14.resolve(__dirname, "..", "templates", "astro-thin");
|
|
10820
|
+
if (!fs11.existsSync(templateDir)) {
|
|
9428
10821
|
throw new Error(
|
|
9429
10822
|
`Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
|
|
9430
10823
|
);
|
|
@@ -9432,8 +10825,8 @@ function initAstro(options = {}) {
|
|
|
9432
10825
|
if (update) {
|
|
9433
10826
|
return updateScaffoldDeps(templateDir, targetDir);
|
|
9434
10827
|
}
|
|
9435
|
-
if (
|
|
9436
|
-
const entries =
|
|
10828
|
+
if (fs11.existsSync(targetDir)) {
|
|
10829
|
+
const entries = fs11.readdirSync(targetDir);
|
|
9437
10830
|
if (entries.length > 0 && !force) {
|
|
9438
10831
|
throw new Error(
|
|
9439
10832
|
`Directory "${targetDir}" already exists and is not empty. Use --force to overlay the template (existing files are kept; same-path template files are overwritten), or --update to refresh framework files only.`
|
|
@@ -9453,11 +10846,11 @@ function updateScaffoldDeps(templateDir, targetDir) {
|
|
|
9453
10846
|
return { targetDir };
|
|
9454
10847
|
}
|
|
9455
10848
|
function mergeDependencies(templateDir, targetDir) {
|
|
9456
|
-
const tmplPkgPath =
|
|
9457
|
-
const userPkgPath =
|
|
9458
|
-
if (!
|
|
9459
|
-
const tmpl = JSON.parse(
|
|
9460
|
-
const user = JSON.parse(
|
|
10849
|
+
const tmplPkgPath = path14.join(templateDir, "package.json");
|
|
10850
|
+
const userPkgPath = path14.join(targetDir, "package.json");
|
|
10851
|
+
if (!fs11.existsSync(tmplPkgPath) || !fs11.existsSync(userPkgPath)) return;
|
|
10852
|
+
const tmpl = JSON.parse(fs11.readFileSync(tmplPkgPath, "utf8"));
|
|
10853
|
+
const user = JSON.parse(fs11.readFileSync(userPkgPath, "utf8"));
|
|
9461
10854
|
user.dependencies = user.dependencies ?? {};
|
|
9462
10855
|
let changed = false;
|
|
9463
10856
|
for (const [name, version] of Object.entries(tmpl.dependencies ?? {})) {
|
|
@@ -9467,28 +10860,28 @@ function mergeDependencies(templateDir, targetDir) {
|
|
|
9467
10860
|
}
|
|
9468
10861
|
}
|
|
9469
10862
|
if (changed) {
|
|
9470
|
-
|
|
10863
|
+
fs11.writeFileSync(userPkgPath, `${JSON.stringify(user, null, 2)}
|
|
9471
10864
|
`, "utf8");
|
|
9472
10865
|
}
|
|
9473
10866
|
}
|
|
9474
10867
|
function copyDirRecursive(src, dest) {
|
|
9475
|
-
|
|
9476
|
-
const entries =
|
|
10868
|
+
fs11.mkdirSync(dest, { recursive: true });
|
|
10869
|
+
const entries = fs11.readdirSync(src, { withFileTypes: true });
|
|
9477
10870
|
for (const entry of entries) {
|
|
9478
|
-
const srcPath =
|
|
10871
|
+
const srcPath = path14.join(src, entry.name);
|
|
9479
10872
|
const destName = entry.name === "gitignore" ? ".gitignore" : entry.name;
|
|
9480
|
-
const destPath =
|
|
10873
|
+
const destPath = path14.join(dest, destName);
|
|
9481
10874
|
if (entry.isDirectory()) {
|
|
9482
10875
|
copyDirRecursive(srcPath, destPath);
|
|
9483
10876
|
} else {
|
|
9484
|
-
|
|
10877
|
+
fs11.copyFileSync(srcPath, destPath);
|
|
9485
10878
|
}
|
|
9486
10879
|
}
|
|
9487
10880
|
}
|
|
9488
10881
|
|
|
9489
10882
|
// src/scaffold-doc.ts
|
|
9490
|
-
import * as
|
|
9491
|
-
import * as
|
|
10883
|
+
import * as fs12 from "fs";
|
|
10884
|
+
import * as path15 from "path";
|
|
9492
10885
|
var TEMPLATES = [
|
|
9493
10886
|
"adr",
|
|
9494
10887
|
"runbook",
|
|
@@ -9505,7 +10898,7 @@ function isoDate(today) {
|
|
|
9505
10898
|
function nextSeq(dir) {
|
|
9506
10899
|
let max = 0;
|
|
9507
10900
|
try {
|
|
9508
|
-
for (const entry of
|
|
10901
|
+
for (const entry of fs12.readdirSync(dir)) {
|
|
9509
10902
|
const match = /^(\d{1,4})-/.exec(entry);
|
|
9510
10903
|
if (match) max = Math.max(max, Number.parseInt(match[1], 10));
|
|
9511
10904
|
}
|
|
@@ -9662,12 +11055,12 @@ function scaffoldDoc(options) {
|
|
|
9662
11055
|
);
|
|
9663
11056
|
}
|
|
9664
11057
|
const spec = TEMPLATE_SPECS[template];
|
|
9665
|
-
const baseDir = options.baseDir ??
|
|
11058
|
+
const baseDir = options.baseDir ?? path15.join("src", "content", "docs");
|
|
9666
11059
|
const today = options.today ?? /* @__PURE__ */ new Date();
|
|
9667
11060
|
const name = (options.name ?? "").trim() || defaultName(template);
|
|
9668
11061
|
const slug2 = slugify3(name);
|
|
9669
11062
|
const scenarioId = normalizeScenarioId(options.scenarioId);
|
|
9670
|
-
const dir =
|
|
11063
|
+
const dir = path15.join(baseDir, spec.subdir);
|
|
9671
11064
|
if (template === "scenario-note" && !scenarioId) {
|
|
9672
11065
|
throw new Error(`Template "scenario-note" requires --scenario-id.`);
|
|
9673
11066
|
}
|
|
@@ -9679,14 +11072,14 @@ function scaffoldDoc(options) {
|
|
|
9679
11072
|
seq: nextSeq(dir)
|
|
9680
11073
|
};
|
|
9681
11074
|
const filename = `${spec.filename(slug2, ctx)}.mdx`;
|
|
9682
|
-
const filePath =
|
|
9683
|
-
if (
|
|
11075
|
+
const filePath = path15.join(dir, filename);
|
|
11076
|
+
if (fs12.existsSync(filePath) && !options.force) {
|
|
9684
11077
|
throw new Error(
|
|
9685
11078
|
`File "${filePath}" already exists. Use --force to overwrite.`
|
|
9686
11079
|
);
|
|
9687
11080
|
}
|
|
9688
|
-
|
|
9689
|
-
|
|
11081
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
11082
|
+
fs12.writeFileSync(filePath, spec.content(ctx), "utf8");
|
|
9690
11083
|
return { template, path: filePath, title: titleFor2(template, ctx) };
|
|
9691
11084
|
}
|
|
9692
11085
|
function defaultName(template) {
|
|
@@ -9727,8 +11120,8 @@ function normalizeScenarioId(input) {
|
|
|
9727
11120
|
}
|
|
9728
11121
|
|
|
9729
11122
|
// src/check-links.ts
|
|
9730
|
-
import * as
|
|
9731
|
-
import * as
|
|
11123
|
+
import * as fs13 from "fs";
|
|
11124
|
+
import * as path16 from "path";
|
|
9732
11125
|
function stripCode(markdown) {
|
|
9733
11126
|
let out = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
|
|
9734
11127
|
out = out.replace(/(`+)(?:(?!\1).)+\1/g, "");
|
|
@@ -9742,8 +11135,8 @@ function extractLinks(markdown) {
|
|
|
9742
11135
|
while ((match = mdRe.exec(stripped)) !== null) {
|
|
9743
11136
|
found.push(match[1].trim());
|
|
9744
11137
|
}
|
|
9745
|
-
const
|
|
9746
|
-
while ((match =
|
|
11138
|
+
const attrRe = /\b(?:href|src)\s*=\s*["']([^"']+)["']/gi;
|
|
11139
|
+
while ((match = attrRe.exec(stripped)) !== null) {
|
|
9747
11140
|
found.push(match[1].trim());
|
|
9748
11141
|
}
|
|
9749
11142
|
return found.filter(Boolean);
|
|
@@ -9755,21 +11148,37 @@ function classifyLink(link2) {
|
|
|
9755
11148
|
if (link2.startsWith("/")) return "root";
|
|
9756
11149
|
return "internal";
|
|
9757
11150
|
}
|
|
9758
|
-
function resolutionCandidates(
|
|
9759
|
-
const
|
|
9760
|
-
if (!withoutAnchor) return [];
|
|
9761
|
-
const base = path15.resolve(path15.dirname(fromFile), withoutAnchor);
|
|
11151
|
+
function resolutionCandidates(fromDir, linkPath) {
|
|
11152
|
+
const base = path16.resolve(fromDir, linkPath);
|
|
9762
11153
|
const candidates = [base];
|
|
9763
|
-
if (!
|
|
11154
|
+
if (!path16.extname(base)) {
|
|
9764
11155
|
candidates.push(`${base}.md`, `${base}.mdx`);
|
|
9765
|
-
candidates.push(
|
|
11156
|
+
candidates.push(path16.join(base, "index.md"), path16.join(base, "index.mdx"));
|
|
9766
11157
|
}
|
|
9767
11158
|
return candidates;
|
|
9768
11159
|
}
|
|
9769
|
-
function
|
|
9770
|
-
return
|
|
9771
|
-
|
|
9772
|
-
|
|
11160
|
+
function linkPathOf(link2) {
|
|
11161
|
+
return link2.split("#")[0].split("?")[0];
|
|
11162
|
+
}
|
|
11163
|
+
function existsAsFile(candidate) {
|
|
11164
|
+
return fs13.existsSync(candidate) && fs13.statSync(candidate).isFile();
|
|
11165
|
+
}
|
|
11166
|
+
function resolvesFrom(dirs, linkPath) {
|
|
11167
|
+
return dirs.some((dir) => resolutionCandidates(dir, linkPath).some(existsAsFile));
|
|
11168
|
+
}
|
|
11169
|
+
function detectAssetRoots(target) {
|
|
11170
|
+
let dir = fs13.existsSync(target) && fs13.statSync(target).isDirectory() ? path16.resolve(target) : path16.dirname(path16.resolve(target));
|
|
11171
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
11172
|
+
const hasConfig = ["mjs", "js", "ts", "mts"].some(
|
|
11173
|
+
(ext) => fs13.existsSync(path16.join(dir, `astro.config.${ext}`))
|
|
11174
|
+
);
|
|
11175
|
+
const publicDir = path16.join(dir, "public");
|
|
11176
|
+
if (hasConfig && fs13.existsSync(publicDir)) return [publicDir];
|
|
11177
|
+
const parent = path16.dirname(dir);
|
|
11178
|
+
if (parent === dir) break;
|
|
11179
|
+
dir = parent;
|
|
11180
|
+
}
|
|
11181
|
+
return [];
|
|
9773
11182
|
}
|
|
9774
11183
|
async function isExternalAlive(url, timeoutMs) {
|
|
9775
11184
|
const attempt = async (method) => {
|
|
@@ -9794,9 +11203,11 @@ async function isExternalAlive(url, timeoutMs) {
|
|
|
9794
11203
|
}
|
|
9795
11204
|
async function checkLinks(options) {
|
|
9796
11205
|
const { target, checkExternal = false, externalTimeoutMs = 8e3 } = options;
|
|
9797
|
-
if (!
|
|
11206
|
+
if (!fs13.existsSync(target)) {
|
|
9798
11207
|
throw new Error(`Path not found: ${target}`);
|
|
9799
11208
|
}
|
|
11209
|
+
const siteRoot = path16.resolve(options.siteRoot ?? target);
|
|
11210
|
+
const rootDirs = [siteRoot, ...(options.assetRoots ?? detectAssetRoots(target)).map((d) => path16.resolve(d))];
|
|
9800
11211
|
const files = collectMarkdownFiles(target);
|
|
9801
11212
|
const broken = [];
|
|
9802
11213
|
let linksChecked = 0;
|
|
@@ -9804,13 +11215,25 @@ async function checkLinks(options) {
|
|
|
9804
11215
|
let skipped = 0;
|
|
9805
11216
|
const externalCache = /* @__PURE__ */ new Map();
|
|
9806
11217
|
for (const file of files) {
|
|
9807
|
-
const content =
|
|
11218
|
+
const content = fs13.readFileSync(file, "utf8");
|
|
9808
11219
|
for (const link2 of extractLinks(content)) {
|
|
9809
11220
|
const kind = classifyLink(link2);
|
|
9810
|
-
if (kind === "anchor" || kind === "mail"
|
|
11221
|
+
if (kind === "anchor" || kind === "mail") {
|
|
9811
11222
|
skipped += 1;
|
|
9812
11223
|
continue;
|
|
9813
11224
|
}
|
|
11225
|
+
if (kind === "root") {
|
|
11226
|
+
const linkPath2 = linkPathOf(link2).replace(/^\/+/, "");
|
|
11227
|
+
if (linkPath2 === "") {
|
|
11228
|
+
skipped += 1;
|
|
11229
|
+
continue;
|
|
11230
|
+
}
|
|
11231
|
+
linksChecked += 1;
|
|
11232
|
+
if (!resolvesFrom(rootDirs, linkPath2)) {
|
|
11233
|
+
broken.push({ file, link: link2, reason: "target file not found" });
|
|
11234
|
+
}
|
|
11235
|
+
continue;
|
|
11236
|
+
}
|
|
9814
11237
|
if (kind === "external") {
|
|
9815
11238
|
if (!checkExternal) {
|
|
9816
11239
|
skipped += 1;
|
|
@@ -9828,8 +11251,13 @@ async function checkLinks(options) {
|
|
|
9828
11251
|
}
|
|
9829
11252
|
continue;
|
|
9830
11253
|
}
|
|
11254
|
+
const linkPath = linkPathOf(link2);
|
|
11255
|
+
if (linkPath === "") {
|
|
11256
|
+
skipped += 1;
|
|
11257
|
+
continue;
|
|
11258
|
+
}
|
|
9831
11259
|
linksChecked += 1;
|
|
9832
|
-
if (!
|
|
11260
|
+
if (!resolvesFrom([path16.dirname(file)], linkPath)) {
|
|
9833
11261
|
broken.push({ file, link: link2, reason: "target file not found" });
|
|
9834
11262
|
}
|
|
9835
11263
|
}
|
|
@@ -9861,7 +11289,7 @@ function formatLinkReport(report) {
|
|
|
9861
11289
|
|
|
9862
11290
|
// src/push.ts
|
|
9863
11291
|
import { execFileSync } from "child_process";
|
|
9864
|
-
import * as
|
|
11292
|
+
import * as fs14 from "fs";
|
|
9865
11293
|
import { parseArgs } from "util";
|
|
9866
11294
|
import { canonicalizeRun as canonicalizeRun4 } from "executable-stories-core/converters/acl/index";
|
|
9867
11295
|
import { toStoryReport as toStoryReport8 } from "executable-stories-core/converters/story-report";
|
|
@@ -9891,7 +11319,7 @@ Options:
|
|
|
9891
11319
|
Exit codes: 0 pushed, 1 push rejected/failed, 4 usage error.`;
|
|
9892
11320
|
function defaultDeps() {
|
|
9893
11321
|
return {
|
|
9894
|
-
readFile: (filePath) =>
|
|
11322
|
+
readFile: (filePath) => fs14.readFileSync(filePath, "utf8"),
|
|
9895
11323
|
fetchFn: fetch,
|
|
9896
11324
|
git: (args) => {
|
|
9897
11325
|
try {
|
|
@@ -10020,9 +11448,334 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
10020
11448
|
return EXIT_SUCCESS;
|
|
10021
11449
|
}
|
|
10022
11450
|
|
|
11451
|
+
// src/sync/run.ts
|
|
11452
|
+
import * as fs15 from "fs";
|
|
11453
|
+
import * as path17 from "path";
|
|
11454
|
+
import { parseArgs as parseArgs2 } from "util";
|
|
11455
|
+
import { canonicalizeRun as canonicalizeRun5 } from "executable-stories-core/converters/acl/index";
|
|
11456
|
+
import { synthesizeStories as synthesizeStories3 } from "executable-stories-core/converters/synthesize";
|
|
11457
|
+
|
|
11458
|
+
// src/config.ts
|
|
11459
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
|
|
11460
|
+
import { resolve as resolve9 } from "path";
|
|
11461
|
+
var CONFIG_CANDIDATES = [
|
|
11462
|
+
"executable-stories.config.mjs",
|
|
11463
|
+
"executable-stories.config.js",
|
|
11464
|
+
"executable-stories.config.json"
|
|
11465
|
+
];
|
|
11466
|
+
async function loadConfig(configPath) {
|
|
11467
|
+
let resolved;
|
|
11468
|
+
if (configPath) {
|
|
11469
|
+
resolved = resolve9(configPath);
|
|
11470
|
+
} else {
|
|
11471
|
+
const present = CONFIG_CANDIDATES.map((name) => resolve9(process.cwd(), name)).filter(existsSync11);
|
|
11472
|
+
if (present.length > 1) {
|
|
11473
|
+
throw new Error(
|
|
11474
|
+
`Multiple config files found in this directory:
|
|
11475
|
+
` + present.map((p) => ` - ${p}`).join("\n") + `
|
|
11476
|
+
Keep only one, or pass --config <path> to choose which to load.`
|
|
11477
|
+
);
|
|
11478
|
+
}
|
|
11479
|
+
resolved = present[0];
|
|
11480
|
+
}
|
|
11481
|
+
if (!resolved || !existsSync11(resolved)) return {};
|
|
11482
|
+
const isJson = resolved.endsWith(".json");
|
|
11483
|
+
let config;
|
|
11484
|
+
if (isJson) {
|
|
11485
|
+
try {
|
|
11486
|
+
config = JSON.parse(readFileSync10(resolved, "utf8"));
|
|
11487
|
+
} catch (err) {
|
|
11488
|
+
throw new Error(`Config file at ${resolved} is not valid JSON: ${err.message}`);
|
|
11489
|
+
}
|
|
11490
|
+
} else {
|
|
11491
|
+
config = (await import(resolved)).default;
|
|
11492
|
+
}
|
|
11493
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
11494
|
+
throw new Error(
|
|
11495
|
+
isJson ? `Config file at ${resolved} must contain a JSON object. Got: ${Array.isArray(config) ? "array" : typeof config}` : `Config file at ${resolved} must export a default object. Got: ${typeof config}`
|
|
11496
|
+
);
|
|
11497
|
+
}
|
|
11498
|
+
const { formatters, sync } = config;
|
|
11499
|
+
return {
|
|
11500
|
+
...formatters === void 0 ? {} : { formatters },
|
|
11501
|
+
...sync === void 0 ? {} : { sync }
|
|
11502
|
+
};
|
|
11503
|
+
}
|
|
11504
|
+
|
|
11505
|
+
// src/sync/run.ts
|
|
11506
|
+
var EXIT_SUCCESS2 = 0;
|
|
11507
|
+
var EXIT_FAILED = 1;
|
|
11508
|
+
var EXIT_USAGE2 = 4;
|
|
11509
|
+
var COVERAGE_HELP = `Usage:
|
|
11510
|
+
executable-stories coverage <provider> <run.json> [options]
|
|
11511
|
+
|
|
11512
|
+
Compares what your tests cover against what a test-management system holds.
|
|
11513
|
+
Read-only: needs nothing but a read-scoped API key, and writes nothing remote.
|
|
11514
|
+
|
|
11515
|
+
Providers: ${PROVIDER_NAMES.join(", ")}
|
|
11516
|
+
|
|
11517
|
+
Options:
|
|
11518
|
+
--config <path> Config file (default: executable-stories.config.mjs, .js, or .json)
|
|
11519
|
+
--output-dir <dir> Where the JSON and Markdown land (default: reports)
|
|
11520
|
+
--report-url <url> Published report URL, used for deep links
|
|
11521
|
+
--lockfile <path> Default: ${DEFAULT_LOCKFILE_PATH}
|
|
11522
|
+
--quiet Write the artifacts, skip the stdout summary
|
|
11523
|
+
-h, --help Show this help
|
|
11524
|
+
|
|
11525
|
+
Credentials come from the environment:
|
|
11526
|
+
TestRail TESTRAIL_USERNAME, TESTRAIL_API_KEY
|
|
11527
|
+
Xray XRAY_CLIENT_ID, XRAY_CLIENT_SECRET (JIRA_EMAIL/JIRA_TOKEN to edit Jira fields)
|
|
11528
|
+
|
|
11529
|
+
Exit codes: 0 report produced, 1 provider unreachable, 4 usage error.`;
|
|
11530
|
+
var SYNC_HELP = `Usage:
|
|
11531
|
+
executable-stories sync <provider> <run.json> [options]
|
|
11532
|
+
|
|
11533
|
+
Pushes stories into a test-management system: case bodies authored from the
|
|
11534
|
+
test, executions recorded against them, evidence attached.
|
|
11535
|
+
|
|
11536
|
+
Prints the plan and changes nothing unless --apply is passed.
|
|
11537
|
+
|
|
11538
|
+
Providers: ${PROVIDER_NAMES.join(", ")}
|
|
11539
|
+
|
|
11540
|
+
Options:
|
|
11541
|
+
--apply Actually write. Without it, this is a dry run.
|
|
11542
|
+
--attach <policy> failed (default), all, or none
|
|
11543
|
+
--config <path> Config file (default: executable-stories.config.mjs, .js, or .json)
|
|
11544
|
+
--lockfile <path> Default: ${DEFAULT_LOCKFILE_PATH}
|
|
11545
|
+
--report-url <url> Published report URL, used for deep links
|
|
11546
|
+
--output-dir <dir> Where coverage artifacts land (default: reports)
|
|
11547
|
+
--continue-on-error Exit 0 even when some writes failed
|
|
11548
|
+
--init Print a config block for this provider and exit
|
|
11549
|
+
-h, --help Show this help
|
|
11550
|
+
|
|
11551
|
+
The lockfile binds each story to its case. Commit it: the diff shows up in the
|
|
11552
|
+
pull request that created the case.
|
|
11553
|
+
|
|
11554
|
+
Exit codes: 0 applied (or planned), 1 some writes failed, 4 usage error.`;
|
|
11555
|
+
function defaultDeps2() {
|
|
11556
|
+
return {
|
|
11557
|
+
readFile: (filePath) => fs15.readFileSync(filePath, "utf8"),
|
|
11558
|
+
fileExists: (filePath) => fs15.existsSync(filePath),
|
|
11559
|
+
writeFile: (filePath, contents) => {
|
|
11560
|
+
fs15.mkdirSync(path17.dirname(path17.resolve(filePath)), { recursive: true });
|
|
11561
|
+
fs15.writeFileSync(filePath, contents, "utf8");
|
|
11562
|
+
},
|
|
11563
|
+
fetchFn: globalThis.fetch,
|
|
11564
|
+
env: process.env,
|
|
11565
|
+
log: console.log,
|
|
11566
|
+
error: console.error,
|
|
11567
|
+
loadConfigFn: loadConfig
|
|
11568
|
+
};
|
|
11569
|
+
}
|
|
11570
|
+
var CONFIG_TEMPLATES = {
|
|
11571
|
+
testrail: `export default {
|
|
11572
|
+
sync: {
|
|
11573
|
+
testrail: {
|
|
11574
|
+
url: "https://acme.testrail.io",
|
|
11575
|
+
projectId: 1,
|
|
11576
|
+
suiteId: 1,
|
|
11577
|
+
// Section that newly created cases land in. Without it, creation is refused.
|
|
11578
|
+
sectionId: 1,
|
|
11579
|
+
// TestRail ships no "skipped" status; set one to record skipped tests.
|
|
11580
|
+
// statusIds: { skipped: 6 },
|
|
11581
|
+
// Only needed if this instance uses a customised case template.
|
|
11582
|
+
// fields: { steps: "custom_steps_separated", description: "custom_preconds" },
|
|
11583
|
+
},
|
|
11584
|
+
},
|
|
11585
|
+
};
|
|
11586
|
+
|
|
11587
|
+
// Environment: TESTRAIL_USERNAME (login email), TESTRAIL_API_KEY (My Settings -> API Keys)`,
|
|
11588
|
+
xray: `export default {
|
|
11589
|
+
sync: {
|
|
11590
|
+
xray: {
|
|
11591
|
+
jiraBaseUrl: "https://acme.atlassian.net",
|
|
11592
|
+
projectKey: "PROJ",
|
|
11593
|
+
// testPlanKey: "PROJ-100",
|
|
11594
|
+
},
|
|
11595
|
+
},
|
|
11596
|
+
};
|
|
11597
|
+
|
|
11598
|
+
// Environment: XRAY_CLIENT_ID, XRAY_CLIENT_SECRET (Jira -> Apps -> Xray -> API Keys)
|
|
11599
|
+
// Optional: JIRA_EMAIL, JIRA_TOKEN (needed to update an existing test's summary/description)`
|
|
11600
|
+
};
|
|
11601
|
+
var JSON_CONFIG_TEMPLATES = {
|
|
11602
|
+
testrail: JSON.stringify(
|
|
11603
|
+
{ sync: { testrail: { url: "https://acme.testrail.io", projectId: 1, suiteId: 1, sectionId: 1 } } },
|
|
11604
|
+
null,
|
|
11605
|
+
2
|
|
11606
|
+
),
|
|
11607
|
+
xray: JSON.stringify(
|
|
11608
|
+
{ sync: { xray: { jiraBaseUrl: "https://acme.atlassian.net", projectKey: "PROJ" } } },
|
|
11609
|
+
null,
|
|
11610
|
+
2
|
|
11611
|
+
)
|
|
11612
|
+
};
|
|
11613
|
+
function isStoryReport2(data) {
|
|
11614
|
+
return typeof data.schemaVersion === "string";
|
|
11615
|
+
}
|
|
11616
|
+
function loadRun(inputPath, deps) {
|
|
11617
|
+
const data = JSON.parse(deps.readFile(inputPath));
|
|
11618
|
+
if (isStoryReport2(data)) {
|
|
11619
|
+
throw new Error(
|
|
11620
|
+
`${inputPath} is a StoryReport, which has already dropped the attachment bodies and source paths this needs. Point at the raw run instead (e.g. reports/raw-run.json).`
|
|
11621
|
+
);
|
|
11622
|
+
}
|
|
11623
|
+
return canonicalizeRun5(synthesizeStories3(data));
|
|
11624
|
+
}
|
|
11625
|
+
async function runSyncCommand(mode, rawArgs, depsOverride = {}) {
|
|
11626
|
+
const deps = { ...defaultDeps2(), ...depsOverride };
|
|
11627
|
+
const help = mode === "sync" ? SYNC_HELP : COVERAGE_HELP;
|
|
11628
|
+
let parsed;
|
|
11629
|
+
try {
|
|
11630
|
+
parsed = parseArgs2({
|
|
11631
|
+
args: rawArgs,
|
|
11632
|
+
allowPositionals: true,
|
|
11633
|
+
options: {
|
|
11634
|
+
apply: { type: "boolean", default: false },
|
|
11635
|
+
attach: { type: "string" },
|
|
11636
|
+
config: { type: "string" },
|
|
11637
|
+
lockfile: { type: "string" },
|
|
11638
|
+
"report-url": { type: "string" },
|
|
11639
|
+
"output-dir": { type: "string" },
|
|
11640
|
+
"continue-on-error": { type: "boolean", default: false },
|
|
11641
|
+
init: { type: "boolean", default: false },
|
|
11642
|
+
quiet: { type: "boolean", default: false },
|
|
11643
|
+
help: { type: "boolean", short: "h", default: false }
|
|
11644
|
+
}
|
|
11645
|
+
});
|
|
11646
|
+
} catch (err) {
|
|
11647
|
+
deps.error(err instanceof Error ? err.message : String(err));
|
|
11648
|
+
deps.error(help);
|
|
11649
|
+
return EXIT_USAGE2;
|
|
11650
|
+
}
|
|
11651
|
+
if (parsed.values.help) {
|
|
11652
|
+
deps.log(help);
|
|
11653
|
+
return EXIT_SUCCESS2;
|
|
11654
|
+
}
|
|
11655
|
+
const providerName = parsed.positionals[0];
|
|
11656
|
+
if (!providerName || !isProviderName(providerName)) {
|
|
11657
|
+
deps.error(
|
|
11658
|
+
providerName ? `Unknown provider "${providerName}". Available: ${PROVIDER_NAMES.join(", ")}.` : `${mode} needs a provider: executable-stories ${mode} <${PROVIDER_NAMES.join("|")}> <run.json>`
|
|
11659
|
+
);
|
|
11660
|
+
deps.error(help);
|
|
11661
|
+
return EXIT_USAGE2;
|
|
11662
|
+
}
|
|
11663
|
+
if (parsed.values.init) {
|
|
11664
|
+
deps.log(CONFIG_TEMPLATES[providerName]);
|
|
11665
|
+
deps.log("");
|
|
11666
|
+
deps.log(
|
|
11667
|
+
`Save the block above as executable-stories.config.mjs (or merge the \`sync\` key into the one you have), then run:
|
|
11668
|
+
executable-stories coverage ${providerName} reports/raw-run.json`
|
|
11669
|
+
);
|
|
11670
|
+
deps.log("");
|
|
11671
|
+
deps.log(
|
|
11672
|
+
`Not a JavaScript project? Put the same \`sync\` object in executable-stories.config.json instead:`
|
|
11673
|
+
);
|
|
11674
|
+
deps.log(JSON_CONFIG_TEMPLATES[providerName]);
|
|
11675
|
+
return EXIT_SUCCESS2;
|
|
11676
|
+
}
|
|
11677
|
+
const inputPath = parsed.positionals[1];
|
|
11678
|
+
if (!inputPath) {
|
|
11679
|
+
deps.error(`${mode} needs a run file: executable-stories ${mode} ${providerName} <run.json>`);
|
|
11680
|
+
deps.error(help);
|
|
11681
|
+
return EXIT_USAGE2;
|
|
11682
|
+
}
|
|
11683
|
+
const attach = parsed.values.attach;
|
|
11684
|
+
if (attach && !["failed", "all", "none"].includes(attach)) {
|
|
11685
|
+
deps.error(`--attach must be one of: failed, all, none (got "${attach}")`);
|
|
11686
|
+
return EXIT_USAGE2;
|
|
11687
|
+
}
|
|
11688
|
+
let run;
|
|
11689
|
+
try {
|
|
11690
|
+
run = loadRun(inputPath, deps);
|
|
11691
|
+
} catch (err) {
|
|
11692
|
+
deps.error(`Could not read ${inputPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
11693
|
+
return EXIT_USAGE2;
|
|
11694
|
+
}
|
|
11695
|
+
const logger = { warn: (message) => deps.error(`Warning: ${message}`) };
|
|
11696
|
+
let targets;
|
|
11697
|
+
try {
|
|
11698
|
+
const config = await deps.loadConfigFn(parsed.values.config);
|
|
11699
|
+
targets = config.sync ?? {};
|
|
11700
|
+
} catch (err) {
|
|
11701
|
+
deps.error(err instanceof Error ? err.message : String(err));
|
|
11702
|
+
return EXIT_USAGE2;
|
|
11703
|
+
}
|
|
11704
|
+
let built;
|
|
11705
|
+
try {
|
|
11706
|
+
built = buildProvider(
|
|
11707
|
+
{ name: providerName, targets, env: deps.env },
|
|
11708
|
+
{ fetch: deps.fetchFn, logger }
|
|
11709
|
+
);
|
|
11710
|
+
} catch (err) {
|
|
11711
|
+
deps.error(err instanceof Error ? err.message : String(err));
|
|
11712
|
+
return EXIT_USAGE2;
|
|
11713
|
+
}
|
|
11714
|
+
const targetConfig = targets[providerName] ?? {};
|
|
11715
|
+
const engineConfig = {
|
|
11716
|
+
...built.engineDefaults,
|
|
11717
|
+
...targetConfig,
|
|
11718
|
+
...parsed.values["report-url"] ? { reportUrl: parsed.values["report-url"] } : {},
|
|
11719
|
+
...attach ? { attach } : {}
|
|
11720
|
+
};
|
|
11721
|
+
const lockfilePath = parsed.values.lockfile ?? DEFAULT_LOCKFILE_PATH;
|
|
11722
|
+
const outputDir = parsed.values["output-dir"] ?? "reports";
|
|
11723
|
+
let lockfile;
|
|
11724
|
+
try {
|
|
11725
|
+
lockfile = deps.fileExists(lockfilePath) ? parseLockfile(deps.readFile(lockfilePath), lockfilePath) : emptyLockfile();
|
|
11726
|
+
} catch (err) {
|
|
11727
|
+
deps.error(err instanceof Error ? err.message : String(err));
|
|
11728
|
+
return EXIT_USAGE2;
|
|
11729
|
+
}
|
|
11730
|
+
let analysis;
|
|
11731
|
+
try {
|
|
11732
|
+
analysis = await analyzeSync({
|
|
11733
|
+
run,
|
|
11734
|
+
provider: built.provider,
|
|
11735
|
+
lockfile,
|
|
11736
|
+
config: engineConfig
|
|
11737
|
+
});
|
|
11738
|
+
} catch (err) {
|
|
11739
|
+
deps.error(`Could not read from ${providerName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
11740
|
+
return EXIT_FAILED;
|
|
11741
|
+
}
|
|
11742
|
+
const jsonPath = path17.join(outputDir, `sync-coverage.${providerName}.json`);
|
|
11743
|
+
const markdownPath = path17.join(outputDir, `sync-coverage.${providerName}.md`);
|
|
11744
|
+
deps.writeFile(jsonPath, `${JSON.stringify(buildCoverageJson(analysis), null, 2)}
|
|
11745
|
+
`);
|
|
11746
|
+
deps.writeFile(markdownPath, `${renderCoverageMarkdown(analysis)}
|
|
11747
|
+
`);
|
|
11748
|
+
if (mode === "coverage") {
|
|
11749
|
+
if (!parsed.values.quiet) {
|
|
11750
|
+
deps.log(renderCoverageText(analysis));
|
|
11751
|
+
deps.log("");
|
|
11752
|
+
}
|
|
11753
|
+
deps.log(`Wrote ${jsonPath} and ${markdownPath}`);
|
|
11754
|
+
return EXIT_SUCCESS2;
|
|
11755
|
+
}
|
|
11756
|
+
const dryRun = !parsed.values.apply;
|
|
11757
|
+
deps.log(renderPlan(analysis, { dryRun }));
|
|
11758
|
+
if (dryRun) return EXIT_SUCCESS2;
|
|
11759
|
+
const applied = await applySync(
|
|
11760
|
+
{ analysis, provider: built.provider, lockfile, config: engineConfig },
|
|
11761
|
+
{ logger }
|
|
11762
|
+
);
|
|
11763
|
+
deps.writeFile(lockfilePath, serializeLockfile(lockfile));
|
|
11764
|
+
deps.log("");
|
|
11765
|
+
deps.log(renderApplyResult(applied));
|
|
11766
|
+
if (applied.errors.length > 0 && !parsed.values["continue-on-error"]) {
|
|
11767
|
+
deps.error(
|
|
11768
|
+
`
|
|
11769
|
+
${applied.errors.length} write(s) failed. A stale system of record is worse than a red build, so this exits non-zero. Pass --continue-on-error to treat it as advisory.`
|
|
11770
|
+
);
|
|
11771
|
+
return EXIT_FAILED;
|
|
11772
|
+
}
|
|
11773
|
+
return EXIT_SUCCESS2;
|
|
11774
|
+
}
|
|
11775
|
+
|
|
10023
11776
|
// src/import-openapi.ts
|
|
10024
|
-
import * as
|
|
10025
|
-
import * as
|
|
11777
|
+
import * as fs16 from "fs";
|
|
11778
|
+
import * as path18 from "path";
|
|
10026
11779
|
import { parse as parseYamlString } from "yaml";
|
|
10027
11780
|
var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
|
|
10028
11781
|
function parseYaml2(raw, specPath) {
|
|
@@ -10035,9 +11788,9 @@ function parseYaml2(raw, specPath) {
|
|
|
10035
11788
|
}
|
|
10036
11789
|
}
|
|
10037
11790
|
function parseSpec(specPath) {
|
|
10038
|
-
if (!
|
|
10039
|
-
const raw =
|
|
10040
|
-
const ext =
|
|
11791
|
+
if (!fs16.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
|
|
11792
|
+
const raw = fs16.readFileSync(specPath, "utf8");
|
|
11793
|
+
const ext = path18.extname(specPath).toLowerCase();
|
|
10041
11794
|
if (ext === ".json") return JSON.parse(raw);
|
|
10042
11795
|
if (ext === ".yaml" || ext === ".yml") return parseYaml2(raw, specPath);
|
|
10043
11796
|
try {
|
|
@@ -10068,8 +11821,8 @@ function extractEndpoints(spec) {
|
|
|
10068
11821
|
}
|
|
10069
11822
|
function loadScenarios(runFile) {
|
|
10070
11823
|
if (!runFile) return [];
|
|
10071
|
-
if (!
|
|
10072
|
-
const report = JSON.parse(
|
|
11824
|
+
if (!fs16.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
|
|
11825
|
+
const report = JSON.parse(fs16.readFileSync(runFile, "utf8"));
|
|
10073
11826
|
return (report.features ?? []).flatMap((f) => f.scenarios ?? []);
|
|
10074
11827
|
}
|
|
10075
11828
|
function endpointRefs(endpoint) {
|
|
@@ -10176,25 +11929,25 @@ async function importOpenApi(options) {
|
|
|
10176
11929
|
list.push(item);
|
|
10177
11930
|
groups.set(item.endpoint.tag, list);
|
|
10178
11931
|
}
|
|
10179
|
-
const outputDir = options.outputDir ??
|
|
10180
|
-
if (
|
|
10181
|
-
const entries =
|
|
11932
|
+
const outputDir = options.outputDir ?? path18.join("src", "content", "docs", "api");
|
|
11933
|
+
if (fs16.existsSync(outputDir) && !options.force) {
|
|
11934
|
+
const entries = fs16.readdirSync(outputDir);
|
|
10182
11935
|
if (entries.length > 0) {
|
|
10183
11936
|
throw new Error(`Output directory "${outputDir}" is not empty. Use --force to overwrite.`);
|
|
10184
11937
|
}
|
|
10185
11938
|
}
|
|
10186
|
-
|
|
11939
|
+
fs16.mkdirSync(outputDir, { recursive: true });
|
|
10187
11940
|
const coveredCount = coverage.filter((c) => c.status === "covered").length;
|
|
10188
11941
|
const uncoveredCount = coverage.filter((c) => c.status === "uncovered").length;
|
|
10189
|
-
|
|
10190
|
-
|
|
11942
|
+
fs16.writeFileSync(
|
|
11943
|
+
path18.join(outputDir, "index.mdx"),
|
|
10191
11944
|
renderIndex(groups, hasRun, { endpointCount: endpoints.length, coveredCount, uncoveredCount }),
|
|
10192
11945
|
"utf8"
|
|
10193
11946
|
);
|
|
10194
11947
|
for (const [tag, rows] of groups) {
|
|
10195
|
-
const dir =
|
|
10196
|
-
|
|
10197
|
-
|
|
11948
|
+
const dir = path18.join(outputDir, slug(tag));
|
|
11949
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
11950
|
+
fs16.writeFileSync(path18.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
|
|
10198
11951
|
}
|
|
10199
11952
|
return {
|
|
10200
11953
|
outputDir,
|
|
@@ -10205,43 +11958,12 @@ async function importOpenApi(options) {
|
|
|
10205
11958
|
};
|
|
10206
11959
|
}
|
|
10207
11960
|
|
|
10208
|
-
// src/config.ts
|
|
10209
|
-
import { existsSync as existsSync11 } from "fs";
|
|
10210
|
-
import { resolve as resolve8 } from "path";
|
|
10211
|
-
var CONFIG_CANDIDATES = ["executable-stories.config.mjs", "executable-stories.config.js"];
|
|
10212
|
-
async function loadConfig(configPath) {
|
|
10213
|
-
let resolved;
|
|
10214
|
-
if (configPath) {
|
|
10215
|
-
resolved = resolve8(configPath);
|
|
10216
|
-
} else {
|
|
10217
|
-
const present = CONFIG_CANDIDATES.map((name) => resolve8(process.cwd(), name)).filter(existsSync11);
|
|
10218
|
-
if (present.length > 1) {
|
|
10219
|
-
throw new Error(
|
|
10220
|
-
`Multiple config files found in this directory:
|
|
10221
|
-
` + present.map((p) => ` - ${p}`).join("\n") + `
|
|
10222
|
-
Keep only one, or pass --config <path> to choose which to load.`
|
|
10223
|
-
);
|
|
10224
|
-
}
|
|
10225
|
-
resolved = present[0];
|
|
10226
|
-
}
|
|
10227
|
-
if (!resolved || !existsSync11(resolved)) return {};
|
|
10228
|
-
const mod = await import(resolved);
|
|
10229
|
-
const config = mod.default;
|
|
10230
|
-
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
10231
|
-
throw new Error(
|
|
10232
|
-
`Config file at ${resolved} must export a default object. Got: ${typeof config}`
|
|
10233
|
-
);
|
|
10234
|
-
}
|
|
10235
|
-
const { formatters } = config;
|
|
10236
|
-
return formatters === void 0 ? {} : { formatters };
|
|
10237
|
-
}
|
|
10238
|
-
|
|
10239
11961
|
// src/cli.ts
|
|
10240
|
-
var
|
|
11962
|
+
var EXIT_SUCCESS3 = 0;
|
|
10241
11963
|
var EXIT_SCHEMA_VALIDATION = 1;
|
|
10242
11964
|
var EXIT_CANONICAL_VALIDATION = 2;
|
|
10243
11965
|
var EXIT_GENERATION = 3;
|
|
10244
|
-
var
|
|
11966
|
+
var EXIT_USAGE3 = 4;
|
|
10245
11967
|
var EXIT_COMPARE_GATE = 5;
|
|
10246
11968
|
var EXIT_REVIEW_GATE = 5;
|
|
10247
11969
|
var EXIT_AGENT_GATE = 5;
|
|
@@ -10270,6 +11992,8 @@ USAGE
|
|
|
10270
11992
|
executable-stories new <template> "<name>" [options]
|
|
10271
11993
|
executable-stories check-links <dir> [options]
|
|
10272
11994
|
executable-stories push <run.json> [--key <es_...>] [--url <base>] [--repo <org/name>]
|
|
11995
|
+
executable-stories coverage <testrail|xray> <run.json> [options]
|
|
11996
|
+
executable-stories sync <testrail|xray> <run.json> [--apply] [options]
|
|
10273
11997
|
executable-stories import-openapi <spec> [options]
|
|
10274
11998
|
executable-stories publish-confluence <file.adf.json> [options]
|
|
10275
11999
|
executable-stories publish-jira <file.adf.json> [options]
|
|
@@ -10295,6 +12019,8 @@ SUBCOMMANDS
|
|
|
10295
12019
|
new Scaffold a docs page from a template (adr, runbook, decision-log, incident, scenario-note)
|
|
10296
12020
|
check-links Scan docs for broken internal/external links (CI-friendly exit code)
|
|
10297
12021
|
push Send a run (StoryReport or raw run JSON) to Executable Stories Cloud
|
|
12022
|
+
coverage Compare your stories against a test-management system (read-only)
|
|
12023
|
+
sync Push cases, executions, and evidence to TestRail or Xray (dry run by default)
|
|
10298
12024
|
import-openapi Generate API doc pages from an OpenAPI spec, linked to verifying stories
|
|
10299
12025
|
publish-confluence Publish an ADF JSON file to a Confluence page via REST API
|
|
10300
12026
|
publish-jira Publish an ADF JSON file to a Jira issue (as comment or description)
|
|
@@ -10487,18 +12213,19 @@ EXIT CODES
|
|
|
10487
12213
|
function parseTextJsonFormat(flag, value) {
|
|
10488
12214
|
if (value !== "text" && value !== "json") {
|
|
10489
12215
|
console.error(`Error: ${flag} must be "text" or "json", got "${value}".`);
|
|
10490
|
-
process.exit(
|
|
12216
|
+
process.exit(EXIT_USAGE3);
|
|
10491
12217
|
}
|
|
10492
12218
|
return value;
|
|
10493
12219
|
}
|
|
10494
12220
|
async function parseCliArgs(argv) {
|
|
10495
12221
|
const args = argv.slice(2);
|
|
10496
|
-
|
|
12222
|
+
const SELF_DOCUMENTING = /* @__PURE__ */ new Set(["sync", "coverage"]);
|
|
12223
|
+
if (args.length === 0 || (args.includes("--help") || args.includes("-h")) && !SELF_DOCUMENTING.has(args[0] ?? "")) {
|
|
10497
12224
|
console.log(HELP_TEXT);
|
|
10498
|
-
process.exit(
|
|
12225
|
+
process.exit(EXIT_SUCCESS3);
|
|
10499
12226
|
}
|
|
10500
12227
|
const subcommand = args[0];
|
|
10501
|
-
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "check" && subcommand !== "check-explainers" && subcommand !== "goal" && subcommand !== "triage" && subcommand !== "validate" && subcommand !== "doctor" && subcommand !== "completion" && subcommand !== "dev" && subcommand !== "init-astro" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "push" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
12228
|
+
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "check" && subcommand !== "check-explainers" && subcommand !== "goal" && subcommand !== "triage" && subcommand !== "validate" && subcommand !== "doctor" && subcommand !== "completion" && subcommand !== "dev" && subcommand !== "init-astro" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "push" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira" && subcommand !== "sync" && subcommand !== "coverage") {
|
|
10502
12229
|
if (subcommand === "serve" || subcommand === "build-docs") {
|
|
10503
12230
|
console.error(
|
|
10504
12231
|
`The "${subcommand}" subcommand was removed. Living docs are now an Astro site, rendered live from the run JSON (no Markdown generation step):
|
|
@@ -10507,12 +12234,12 @@ async function parseCliArgs(argv) {
|
|
|
10507
12234
|
3. run \`executable-stories dev\` in another \u2014 it hot-reloads the docs.
|
|
10508
12235
|
See: https://github.com/jagreehal/executable-stories (executable-stories-astro).`
|
|
10509
12236
|
);
|
|
10510
|
-
process.exit(
|
|
12237
|
+
process.exit(EXIT_USAGE3);
|
|
10511
12238
|
}
|
|
10512
12239
|
console.error(
|
|
10513
|
-
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "check", "check-explainers", "goal", "triage", "validate", "doctor", "completion", "dev", "init-astro", "new", "check-links", "push", "import-openapi", "publish-confluence", or "publish-jira".`
|
|
12240
|
+
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "check", "check-explainers", "goal", "triage", "validate", "doctor", "completion", "dev", "init-astro", "new", "check-links", "push", "sync", "coverage", "import-openapi", "publish-confluence", or "publish-jira".`
|
|
10514
12241
|
);
|
|
10515
|
-
process.exit(
|
|
12242
|
+
process.exit(EXIT_USAGE3);
|
|
10516
12243
|
}
|
|
10517
12244
|
if (subcommand === "completion") {
|
|
10518
12245
|
process.exit(runCompletion(args.slice(1)));
|
|
@@ -10526,15 +12253,15 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10526
12253
|
} else {
|
|
10527
12254
|
console.log(formatDoctorReport(report));
|
|
10528
12255
|
}
|
|
10529
|
-
process.exit(report.healthy ?
|
|
12256
|
+
process.exit(report.healthy ? EXIT_SUCCESS3 : EXIT_USAGE3);
|
|
10530
12257
|
}
|
|
10531
12258
|
if (subcommand === "publish-confluence") {
|
|
10532
12259
|
await runPublishConfluence(args.slice(1));
|
|
10533
|
-
process.exit(
|
|
12260
|
+
process.exit(EXIT_SUCCESS3);
|
|
10534
12261
|
}
|
|
10535
12262
|
if (subcommand === "publish-jira") {
|
|
10536
12263
|
await runPublishJira(args.slice(1));
|
|
10537
|
-
process.exit(
|
|
12264
|
+
process.exit(EXIT_SUCCESS3);
|
|
10538
12265
|
}
|
|
10539
12266
|
if (subcommand === "deploy") {
|
|
10540
12267
|
process.exit(await runDeploy(args.slice(1)));
|
|
@@ -10548,7 +12275,7 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10548
12275
|
`No docs site found at ${siteDir}. Create one (scaffold + install) with:
|
|
10549
12276
|
npx executable-stories init-astro --install`
|
|
10550
12277
|
);
|
|
10551
|
-
process.exit(
|
|
12278
|
+
process.exit(EXIT_USAGE3);
|
|
10552
12279
|
}
|
|
10553
12280
|
if (dev.kind === "install-failed") {
|
|
10554
12281
|
console.error(`"${dev.pm} install" failed in ${siteDir} \u2014 run it manually, then retry.`);
|
|
@@ -10567,7 +12294,7 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10567
12294
|
if (update) {
|
|
10568
12295
|
console.log(`Updated ${result.targetDir} (content + config left untouched)`);
|
|
10569
12296
|
console.log(" Framework updates come via: pnpm update executable-stories-astro");
|
|
10570
|
-
process.exit(
|
|
12297
|
+
process.exit(EXIT_SUCCESS3);
|
|
10571
12298
|
}
|
|
10572
12299
|
console.log(`Scaffolded Astro docs site at ${result.targetDir}`);
|
|
10573
12300
|
const pm = detectPackageManager();
|
|
@@ -10594,17 +12321,19 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10594
12321
|
console.log("");
|
|
10595
12322
|
console.log("Everything is configured in one file: executable-stories.config.mjs");
|
|
10596
12323
|
console.log(" \u2014 sources, scenario selection (include/exclude), grouping (groupBy), docs, and theme.");
|
|
10597
|
-
process.exit(
|
|
12324
|
+
process.exit(EXIT_SUCCESS3);
|
|
10598
12325
|
} catch (err) {
|
|
10599
12326
|
console.error(`Error: ${err.message}`);
|
|
10600
|
-
process.exit(
|
|
12327
|
+
process.exit(EXIT_USAGE3);
|
|
10601
12328
|
}
|
|
10602
12329
|
}
|
|
10603
12330
|
if (subcommand === "new") process.exit(runNew(args.slice(1)));
|
|
10604
12331
|
if (subcommand === "check-links") process.exit(await runCheckLinks(args.slice(1)));
|
|
10605
12332
|
if (subcommand === "push") process.exit(await runPush(args.slice(1)));
|
|
12333
|
+
if (subcommand === "sync") process.exit(await runSyncCommand("sync", args.slice(1)));
|
|
12334
|
+
if (subcommand === "coverage") process.exit(await runSyncCommand("coverage", args.slice(1)));
|
|
10606
12335
|
if (subcommand === "import-openapi") process.exit(await runImportOpenApi(args.slice(1)));
|
|
10607
|
-
const { values, positionals } =
|
|
12336
|
+
const { values, positionals } = parseArgs3({
|
|
10608
12337
|
args: args.slice(1),
|
|
10609
12338
|
options: {
|
|
10610
12339
|
format: { type: "string", default: "html" },
|
|
@@ -10682,7 +12411,7 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10682
12411
|
});
|
|
10683
12412
|
if (values.help) {
|
|
10684
12413
|
console.log(HELP_TEXT);
|
|
10685
|
-
process.exit(
|
|
12414
|
+
process.exit(EXIT_SUCCESS3);
|
|
10686
12415
|
}
|
|
10687
12416
|
const userSetFormat = args.slice(1).some((a) => a === "--format" || a.startsWith("--format="));
|
|
10688
12417
|
const preset = expandPreset(
|
|
@@ -10692,7 +12421,7 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10692
12421
|
);
|
|
10693
12422
|
if (preset.error) {
|
|
10694
12423
|
console.error(`Error: ${preset.error}`);
|
|
10695
|
-
process.exit(
|
|
12424
|
+
process.exit(EXIT_USAGE3);
|
|
10696
12425
|
}
|
|
10697
12426
|
const useStdin = values.stdin;
|
|
10698
12427
|
const baselineValue = values.baseline;
|
|
@@ -10704,15 +12433,15 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10704
12433
|
if (isCompareLike) {
|
|
10705
12434
|
if (useStdin) {
|
|
10706
12435
|
console.error(`Error: ${subcommand} does not support --stdin. Pass baseline and current files.`);
|
|
10707
|
-
process.exit(
|
|
12436
|
+
process.exit(EXIT_USAGE3);
|
|
10708
12437
|
}
|
|
10709
12438
|
if (!currentFile) {
|
|
10710
12439
|
console.error(`Error: ${subcommand} requires <current-file>, and either <baseline-file> or --baseline auto.`);
|
|
10711
|
-
process.exit(
|
|
12440
|
+
process.exit(EXIT_USAGE3);
|
|
10712
12441
|
}
|
|
10713
12442
|
if (baselineMode === "explicit" && !baselineFile) {
|
|
10714
12443
|
console.error(`Error: ${subcommand} requires <baseline-file> and <current-file>, or use --baseline auto.`);
|
|
10715
|
-
process.exit(
|
|
12444
|
+
process.exit(EXIT_USAGE3);
|
|
10716
12445
|
}
|
|
10717
12446
|
}
|
|
10718
12447
|
let resolvedInputFile = inputFile;
|
|
@@ -10727,13 +12456,13 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10727
12456
|
Pass a path, use --stdin, or run your tests first (non-JS adapters write
|
|
10728
12457
|
${DEFAULT_RUN_FILES[0]}; set rawRunPath in a JS reporter to write ${DEFAULT_RUN_FILES[1]}).`
|
|
10729
12458
|
);
|
|
10730
|
-
process.exit(
|
|
12459
|
+
process.exit(EXIT_USAGE3);
|
|
10731
12460
|
}
|
|
10732
12461
|
}
|
|
10733
12462
|
const inputType = values["input-type"];
|
|
10734
12463
|
if (inputType !== "raw" && inputType !== "canonical" && inputType !== "ndjson") {
|
|
10735
12464
|
console.error(`Error: --input-type must be "raw", "canonical", or "ndjson", got "${inputType}".`);
|
|
10736
|
-
process.exit(
|
|
12465
|
+
process.exit(EXIT_USAGE3);
|
|
10737
12466
|
}
|
|
10738
12467
|
const pluginConfig = await loadConfig(values["config"]);
|
|
10739
12468
|
const customFormatterNames = new Set(Object.keys(pluginConfig.formatters ?? {}));
|
|
@@ -10752,7 +12481,7 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10752
12481
|
if (unknownFormats.length > 0) {
|
|
10753
12482
|
const knownCustom = customFormatterNames.size > 0 ? `, ${[...customFormatterNames].join(", ")}` : "";
|
|
10754
12483
|
console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: agent-text, astro-markdown, behavior-manifest-json, confluence, html, markdown, release-manifest, traceability-matrix, traceability-csv, junit, cucumber-json, cucumber-messages, cucumber-html, scenario-index-json, story-report-json${knownCustom}.`);
|
|
10755
|
-
process.exit(
|
|
12484
|
+
process.exit(EXIT_USAGE3);
|
|
10756
12485
|
}
|
|
10757
12486
|
const formats = builtInRequested;
|
|
10758
12487
|
const noSynthesize = values["no-synthesize-stories"];
|
|
@@ -10761,19 +12490,19 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10761
12490
|
const validNotifyConditions = /* @__PURE__ */ new Set(["always", "on-failure", "never"]);
|
|
10762
12491
|
if (!validNotifyConditions.has(notifyValue)) {
|
|
10763
12492
|
console.error(`Error: --notify must be "always", "on-failure", or "never", got "${notifyValue}".`);
|
|
10764
|
-
process.exit(
|
|
12493
|
+
process.exit(EXIT_USAGE3);
|
|
10765
12494
|
}
|
|
10766
12495
|
const maxFailedTestsStr = values["max-failed-tests"];
|
|
10767
12496
|
const maxFailedTests = maxFailedTestsStr ? parseInt(maxFailedTestsStr, 10) : 5;
|
|
10768
12497
|
if (maxFailedTestsStr && (isNaN(maxFailedTests) || maxFailedTests < 0)) {
|
|
10769
12498
|
console.error(`Error: --max-failed-tests must be a non-negative integer, got "${maxFailedTestsStr}".`);
|
|
10770
|
-
process.exit(
|
|
12499
|
+
process.exit(EXIT_USAGE3);
|
|
10771
12500
|
}
|
|
10772
12501
|
const htmlStaleAfterDaysStr = values["html-stale-after-days"];
|
|
10773
12502
|
const htmlStaleAfterDays = htmlStaleAfterDaysStr ? parseInt(htmlStaleAfterDaysStr, 10) : 7;
|
|
10774
12503
|
if (htmlStaleAfterDaysStr && (isNaN(htmlStaleAfterDays) || htmlStaleAfterDays < 0)) {
|
|
10775
12504
|
console.error(`Error: --html-stale-after-days must be a non-negative integer, got "${htmlStaleAfterDaysStr}".`);
|
|
10776
|
-
process.exit(
|
|
12505
|
+
process.exit(EXIT_USAGE3);
|
|
10777
12506
|
}
|
|
10778
12507
|
const slackWebhook = values["slack-webhook"];
|
|
10779
12508
|
const teamsWebhook = values["teams-webhook"];
|
|
@@ -10800,7 +12529,7 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10800
12529
|
const upper = webhookMethodRaw.toUpperCase();
|
|
10801
12530
|
if (upper !== "POST" && upper !== "PUT") {
|
|
10802
12531
|
console.error(`Error: --webhook-method must be "POST" or "PUT", got "${webhookMethodRaw}".`);
|
|
10803
|
-
process.exit(
|
|
12532
|
+
process.exit(EXIT_USAGE3);
|
|
10804
12533
|
}
|
|
10805
12534
|
webhookMethod = upper;
|
|
10806
12535
|
}
|
|
@@ -10808,36 +12537,36 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10808
12537
|
const maxHistoryRuns = maxHistoryRunsStr ? parseInt(maxHistoryRunsStr, 10) : 10;
|
|
10809
12538
|
if (maxHistoryRunsStr && (isNaN(maxHistoryRuns) || maxHistoryRuns < 1)) {
|
|
10810
12539
|
console.error(`Error: --max-history-runs must be a positive integer, got "${maxHistoryRunsStr}".`);
|
|
10811
|
-
process.exit(
|
|
12540
|
+
process.exit(EXIT_USAGE3);
|
|
10812
12541
|
}
|
|
10813
12542
|
const maxRegressionsStr = values["max-regressions"];
|
|
10814
12543
|
const maxRegressions = maxRegressionsStr !== void 0 ? parseInt(maxRegressionsStr, 10) : void 0;
|
|
10815
12544
|
if (maxRegressionsStr !== void 0 && (isNaN(maxRegressions) || maxRegressions < 0)) {
|
|
10816
12545
|
console.error(`Error: --max-regressions must be a non-negative integer, got "${maxRegressionsStr}".`);
|
|
10817
|
-
process.exit(
|
|
12546
|
+
process.exit(EXIT_USAGE3);
|
|
10818
12547
|
}
|
|
10819
12548
|
const sortTestCasesRaw = values["sort-test-cases"];
|
|
10820
12549
|
const validSortModes = /* @__PURE__ */ new Set(["id", "source", "none"]);
|
|
10821
12550
|
if (!validSortModes.has(sortTestCasesRaw)) {
|
|
10822
12551
|
console.error(`Error: --sort-test-cases must be id, source, or none, got "${sortTestCasesRaw}".`);
|
|
10823
|
-
process.exit(
|
|
12552
|
+
process.exit(EXIT_USAGE3);
|
|
10824
12553
|
}
|
|
10825
12554
|
const assetModeRaw = values["asset-mode"];
|
|
10826
12555
|
const validAssetModes = /* @__PURE__ */ new Set(["none", "copy"]);
|
|
10827
12556
|
if (!validAssetModes.has(assetModeRaw)) {
|
|
10828
12557
|
console.error(`Error: --asset-mode must be "none" or "copy", got "${assetModeRaw}".`);
|
|
10829
|
-
process.exit(
|
|
12558
|
+
process.exit(EXIT_USAGE3);
|
|
10830
12559
|
}
|
|
10831
12560
|
const failOnRaw = values["fail-on"];
|
|
10832
12561
|
if (failOnRaw !== void 0 && failOnRaw !== "uncovered" && failOnRaw !== "weak") {
|
|
10833
12562
|
console.error(`Error: --fail-on must be "uncovered" or "weak", got "${failOnRaw}".`);
|
|
10834
|
-
process.exit(
|
|
12563
|
+
process.exit(EXIT_USAGE3);
|
|
10835
12564
|
}
|
|
10836
12565
|
const minEvidenceRaw = values["min-evidence"];
|
|
10837
12566
|
const validMinEvidence = /* @__PURE__ */ new Set(["weak", "moderate", "strong"]);
|
|
10838
12567
|
if (minEvidenceRaw !== void 0 && !validMinEvidence.has(minEvidenceRaw)) {
|
|
10839
12568
|
console.error(`Error: --min-evidence must be "weak", "moderate", or "strong", got "${minEvidenceRaw}".`);
|
|
10840
|
-
process.exit(
|
|
12569
|
+
process.exit(EXIT_USAGE3);
|
|
10841
12570
|
}
|
|
10842
12571
|
const checkFormat = parseTextJsonFormat("--check-format", values["check-format"]);
|
|
10843
12572
|
const goalFormat = parseTextJsonFormat("--goal-format", values["goal-format"]);
|
|
@@ -10921,27 +12650,27 @@ async function readInput(args) {
|
|
|
10921
12650
|
if (args.stdin) {
|
|
10922
12651
|
return readStdin();
|
|
10923
12652
|
}
|
|
10924
|
-
const filePath =
|
|
10925
|
-
if (!
|
|
12653
|
+
const filePath = path19.resolve(args.inputFile);
|
|
12654
|
+
if (!fs17.existsSync(filePath)) {
|
|
10926
12655
|
console.error(`Error: File not found: ${filePath}`);
|
|
10927
|
-
process.exit(
|
|
12656
|
+
process.exit(EXIT_USAGE3);
|
|
10928
12657
|
}
|
|
10929
|
-
return
|
|
12658
|
+
return fs17.readFileSync(filePath, "utf8");
|
|
10930
12659
|
}
|
|
10931
12660
|
function readFileInput(filePath) {
|
|
10932
|
-
const resolved =
|
|
10933
|
-
if (!
|
|
12661
|
+
const resolved = path19.resolve(filePath);
|
|
12662
|
+
if (!fs17.existsSync(resolved)) {
|
|
10934
12663
|
console.error(`Error: File not found: ${resolved}`);
|
|
10935
|
-
process.exit(
|
|
12664
|
+
process.exit(EXIT_USAGE3);
|
|
10936
12665
|
}
|
|
10937
|
-
return
|
|
12666
|
+
return fs17.readFileSync(resolved, "utf8");
|
|
10938
12667
|
}
|
|
10939
12668
|
function readStdin() {
|
|
10940
|
-
return new Promise((
|
|
12669
|
+
return new Promise((resolve12, reject) => {
|
|
10941
12670
|
const chunks = [];
|
|
10942
12671
|
process.stdin.setEncoding("utf8");
|
|
10943
12672
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
10944
|
-
process.stdin.on("end", () =>
|
|
12673
|
+
process.stdin.on("end", () => resolve12(chunks.join("")));
|
|
10945
12674
|
process.stdin.on("error", reject);
|
|
10946
12675
|
});
|
|
10947
12676
|
}
|
|
@@ -10951,7 +12680,7 @@ function parseJson(text2) {
|
|
|
10951
12680
|
} catch (err) {
|
|
10952
12681
|
const msg = err instanceof Error ? err.message : String(err);
|
|
10953
12682
|
console.error(`Error: Invalid JSON \u2014 ${msg}`);
|
|
10954
|
-
process.exit(
|
|
12683
|
+
process.exit(EXIT_USAGE3);
|
|
10955
12684
|
}
|
|
10956
12685
|
}
|
|
10957
12686
|
function tryParseJson(text2) {
|
|
@@ -10989,13 +12718,13 @@ ${msg}`);
|
|
|
10989
12718
|
let raw = data;
|
|
10990
12719
|
let droppedMissingStory = 0;
|
|
10991
12720
|
if (args.synthesizeStories) {
|
|
10992
|
-
raw =
|
|
12721
|
+
raw = synthesizeStories4(raw);
|
|
10993
12722
|
} else {
|
|
10994
12723
|
const before = raw.testCases.length;
|
|
10995
12724
|
const withStory = raw.testCases.filter((tc) => tc.story != null).length;
|
|
10996
12725
|
droppedMissingStory = before - withStory;
|
|
10997
12726
|
}
|
|
10998
|
-
const canonical =
|
|
12727
|
+
const canonical = canonicalizeRun6(raw);
|
|
10999
12728
|
try {
|
|
11000
12729
|
assertValidRun2(canonical);
|
|
11001
12730
|
} catch (err) {
|
|
@@ -11076,9 +12805,9 @@ function tryNormalizeRunFromText(text2, args) {
|
|
|
11076
12805
|
if (!schemaResult.valid) return void 0;
|
|
11077
12806
|
let raw = data;
|
|
11078
12807
|
if (args.synthesizeStories) {
|
|
11079
|
-
raw =
|
|
12808
|
+
raw = synthesizeStories4(raw);
|
|
11080
12809
|
}
|
|
11081
|
-
const canonical =
|
|
12810
|
+
const canonical = canonicalizeRun6(raw);
|
|
11082
12811
|
try {
|
|
11083
12812
|
assertValidRun2(canonical);
|
|
11084
12813
|
return canonical;
|
|
@@ -11087,14 +12816,14 @@ function tryNormalizeRunFromText(text2, args) {
|
|
|
11087
12816
|
}
|
|
11088
12817
|
}
|
|
11089
12818
|
function listBaselineCandidates(currentFile, args) {
|
|
11090
|
-
const baselineDir =
|
|
11091
|
-
const currentResolved =
|
|
11092
|
-
if (!
|
|
12819
|
+
const baselineDir = path19.resolve(args.baselineDir ?? path19.dirname(currentFile));
|
|
12820
|
+
const currentResolved = path19.resolve(currentFile);
|
|
12821
|
+
if (!fs17.existsSync(baselineDir)) {
|
|
11093
12822
|
console.error(`Error: baseline directory not found: ${baselineDir}`);
|
|
11094
|
-
process.exit(
|
|
12823
|
+
process.exit(EXIT_USAGE3);
|
|
11095
12824
|
}
|
|
11096
|
-
const entries =
|
|
11097
|
-
return entries.filter((entry) => entry.isFile()).map((entry) =>
|
|
12825
|
+
const entries = fs17.readdirSync(baselineDir, { withFileTypes: true });
|
|
12826
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => path19.join(baselineDir, entry.name)).filter((candidate) => path19.resolve(candidate) !== currentResolved).filter(
|
|
11098
12827
|
(candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
|
|
11099
12828
|
);
|
|
11100
12829
|
}
|
|
@@ -11102,21 +12831,21 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
|
|
|
11102
12831
|
const candidates = listBaselineCandidates(currentFile, args);
|
|
11103
12832
|
const comparable = [];
|
|
11104
12833
|
for (const candidate of candidates) {
|
|
11105
|
-
const run = tryNormalizeRunFromText(
|
|
12834
|
+
const run = tryNormalizeRunFromText(fs17.readFileSync(candidate, "utf8"), args);
|
|
11106
12835
|
if (run) {
|
|
11107
12836
|
comparable.push({ file: candidate, run });
|
|
11108
12837
|
}
|
|
11109
12838
|
}
|
|
11110
12839
|
if (comparable.length === 0) {
|
|
11111
12840
|
console.error(
|
|
11112
|
-
`Error: no compatible baseline files found in ${
|
|
12841
|
+
`Error: no compatible baseline files found in ${path19.resolve(args.baselineDir ?? path19.dirname(currentFile))}.`
|
|
11113
12842
|
);
|
|
11114
|
-
process.exit(
|
|
12843
|
+
process.exit(EXIT_USAGE3);
|
|
11115
12844
|
}
|
|
11116
12845
|
const picked = pickAutoBaseline(currentRun, comparable);
|
|
11117
12846
|
if (!picked) {
|
|
11118
12847
|
console.error("Error: unable to choose an automatic baseline.");
|
|
11119
|
-
process.exit(
|
|
12848
|
+
process.exit(EXIT_USAGE3);
|
|
11120
12849
|
}
|
|
11121
12850
|
return picked.file;
|
|
11122
12851
|
}
|
|
@@ -11126,7 +12855,7 @@ function resolveBaselineRun(args, currentRun) {
|
|
|
11126
12855
|
if (args.baselineArg === "auto") {
|
|
11127
12856
|
if (!args.inputFile) {
|
|
11128
12857
|
console.error("Error: --baseline auto requires a current input file (not --stdin).");
|
|
11129
|
-
process.exit(
|
|
12858
|
+
process.exit(EXIT_USAGE3);
|
|
11130
12859
|
}
|
|
11131
12860
|
baselineFile = resolveBaselineAuto(args.inputFile, currentRun, args);
|
|
11132
12861
|
} else {
|
|
@@ -11172,7 +12901,7 @@ async function runCompare(ctx) {
|
|
|
11172
12901
|
}
|
|
11173
12902
|
process.exit(EXIT_COMPARE_GATE);
|
|
11174
12903
|
}
|
|
11175
|
-
process.exit(
|
|
12904
|
+
process.exit(EXIT_SUCCESS3);
|
|
11176
12905
|
} catch (err) {
|
|
11177
12906
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11178
12907
|
console.error(`Comparison failed: ${msg}`);
|
|
@@ -11217,7 +12946,7 @@ async function runGateRelease(ctx) {
|
|
|
11217
12946
|
process.exit(EXIT_RELEASE_GATE);
|
|
11218
12947
|
}
|
|
11219
12948
|
console.error("Release gate passed: RC matches dev baseline.");
|
|
11220
|
-
process.exit(
|
|
12949
|
+
process.exit(EXIT_SUCCESS3);
|
|
11221
12950
|
} catch (err) {
|
|
11222
12951
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11223
12952
|
console.error(`Release gate check failed: ${msg}`);
|
|
@@ -11249,7 +12978,7 @@ async function runReview(ctx) {
|
|
|
11249
12978
|
}
|
|
11250
12979
|
process.exit(EXIT_REVIEW_GATE);
|
|
11251
12980
|
}
|
|
11252
|
-
process.exit(
|
|
12981
|
+
process.exit(EXIT_SUCCESS3);
|
|
11253
12982
|
} catch (err) {
|
|
11254
12983
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11255
12984
|
console.error(`Review failed: ${msg}`);
|
|
@@ -11265,7 +12994,7 @@ async function runList(ctx) {
|
|
|
11265
12994
|
const validListFormats = /* @__PURE__ */ new Set(["text", "json", "csv", "markdown-table"]);
|
|
11266
12995
|
if (!validListFormats.has(resolvedFormat)) {
|
|
11267
12996
|
console.error(`Error: Unknown list format "${resolvedFormat}". Valid: text, json, csv, markdown-table.`);
|
|
11268
|
-
process.exit(
|
|
12997
|
+
process.exit(EXIT_USAGE3);
|
|
11269
12998
|
}
|
|
11270
12999
|
const output = listScenarios(
|
|
11271
13000
|
{
|
|
@@ -11276,7 +13005,7 @@ async function runList(ctx) {
|
|
|
11276
13005
|
{}
|
|
11277
13006
|
);
|
|
11278
13007
|
console.log(output);
|
|
11279
|
-
process.exit(
|
|
13008
|
+
process.exit(EXIT_SUCCESS3);
|
|
11280
13009
|
}
|
|
11281
13010
|
async function runCheck(ctx) {
|
|
11282
13011
|
const { args } = ctx;
|
|
@@ -11291,17 +13020,17 @@ async function runCheck(ctx) {
|
|
|
11291
13020
|
if (report.summary.failed > 0 && !args.noFail) {
|
|
11292
13021
|
process.exit(EXIT_AGENT_GATE);
|
|
11293
13022
|
}
|
|
11294
|
-
process.exit(
|
|
13023
|
+
process.exit(EXIT_SUCCESS3);
|
|
11295
13024
|
}
|
|
11296
13025
|
async function runCheckExplainers(ctx) {
|
|
11297
13026
|
const { args } = ctx;
|
|
11298
13027
|
if (!args.explainersDir) {
|
|
11299
13028
|
console.error("Error: check-explainers requires --explainers-dir <dir> (the folder of explainer markdown).");
|
|
11300
|
-
process.exit(
|
|
13029
|
+
process.exit(EXIT_USAGE3);
|
|
11301
13030
|
}
|
|
11302
|
-
if (!
|
|
13031
|
+
if (!fs17.existsSync(args.explainersDir) || !fs17.statSync(args.explainersDir).isDirectory()) {
|
|
11303
13032
|
console.error(`Error: --explainers-dir "${args.explainersDir}" is not a directory.`);
|
|
11304
|
-
process.exit(
|
|
13033
|
+
process.exit(EXIT_USAGE3);
|
|
11305
13034
|
}
|
|
11306
13035
|
const text2 = await readInput(args);
|
|
11307
13036
|
const run = applySelection(normalizeRunFromText(text2, args).run, args);
|
|
@@ -11310,7 +13039,7 @@ async function runCheckExplainers(ctx) {
|
|
|
11310
13039
|
if (explainersGateFailed(report) && !args.noFail) {
|
|
11311
13040
|
process.exit(EXIT_AGENT_GATE);
|
|
11312
13041
|
}
|
|
11313
|
-
process.exit(
|
|
13042
|
+
process.exit(EXIT_SUCCESS3);
|
|
11314
13043
|
}
|
|
11315
13044
|
async function runGoal(ctx) {
|
|
11316
13045
|
const { args } = ctx;
|
|
@@ -11331,7 +13060,7 @@ async function runGoal(ctx) {
|
|
|
11331
13060
|
{}
|
|
11332
13061
|
);
|
|
11333
13062
|
console.log(renderGoal(report, args.goalFormat));
|
|
11334
|
-
process.exit(report.met ?
|
|
13063
|
+
process.exit(report.met ? EXIT_SUCCESS3 : EXIT_AGENT_GATE);
|
|
11335
13064
|
}
|
|
11336
13065
|
async function runTriage(ctx) {
|
|
11337
13066
|
const { args } = ctx;
|
|
@@ -11343,13 +13072,13 @@ async function runTriage(ctx) {
|
|
|
11343
13072
|
{}
|
|
11344
13073
|
);
|
|
11345
13074
|
console.log(renderTriage(report, args.triageFormat));
|
|
11346
|
-
process.exit(
|
|
13075
|
+
process.exit(EXIT_SUCCESS3);
|
|
11347
13076
|
}
|
|
11348
13077
|
async function runWatch(ctx) {
|
|
11349
13078
|
const { args } = ctx;
|
|
11350
13079
|
if (!args.inputFile) {
|
|
11351
13080
|
console.error("Error: watch requires an input file (the raw-run JSON the framework writes).");
|
|
11352
|
-
process.exit(
|
|
13081
|
+
process.exit(EXIT_USAGE3);
|
|
11353
13082
|
}
|
|
11354
13083
|
console.log(
|
|
11355
13084
|
`Watching ${args.inputFile} \u2192 regenerating [${args.formats.join(", ")}] into ${args.outputDir}/ (Ctrl+C to stop)`
|
|
@@ -11398,7 +13127,7 @@ async function runFormatOrValidate(ctx) {
|
|
|
11398
13127
|
}
|
|
11399
13128
|
}
|
|
11400
13129
|
console.log(`Valid NDJSON (${lines.length} envelopes).`);
|
|
11401
|
-
process.exit(
|
|
13130
|
+
process.exit(EXIT_SUCCESS3);
|
|
11402
13131
|
}
|
|
11403
13132
|
let run;
|
|
11404
13133
|
try {
|
|
@@ -11409,9 +13138,9 @@ async function runFormatOrValidate(ctx) {
|
|
|
11409
13138
|
process.exit(EXIT_SCHEMA_VALIDATION);
|
|
11410
13139
|
}
|
|
11411
13140
|
if (args.emitCanonical) {
|
|
11412
|
-
const outPath =
|
|
11413
|
-
|
|
11414
|
-
|
|
13141
|
+
const outPath = path19.resolve(args.emitCanonical);
|
|
13142
|
+
fs17.mkdirSync(path19.dirname(outPath), { recursive: true });
|
|
13143
|
+
fs17.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
11415
13144
|
}
|
|
11416
13145
|
try {
|
|
11417
13146
|
const history = runHistoryPipeline(run, args);
|
|
@@ -11419,7 +13148,7 @@ async function runFormatOrValidate(ctx) {
|
|
|
11419
13148
|
runCustomFormatters(run, customRequested, pluginConfig.formatters ?? {}, args);
|
|
11420
13149
|
await dispatchNotifications(run, args);
|
|
11421
13150
|
printResult(result, args, startMs);
|
|
11422
|
-
process.exit(
|
|
13151
|
+
process.exit(EXIT_SUCCESS3);
|
|
11423
13152
|
} catch (err) {
|
|
11424
13153
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11425
13154
|
console.error(`Generation failed: ${msg}`);
|
|
@@ -11433,7 +13162,7 @@ async function runFormatOrValidate(ctx) {
|
|
|
11433
13162
|
assertValidRun2(data);
|
|
11434
13163
|
warnLargeStateDocs(data.testCases);
|
|
11435
13164
|
console.log("Valid canonical TestRunResult.");
|
|
11436
|
-
process.exit(
|
|
13165
|
+
process.exit(EXIT_SUCCESS3);
|
|
11437
13166
|
} catch (err) {
|
|
11438
13167
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11439
13168
|
console.error(msg);
|
|
@@ -11457,7 +13186,7 @@ async function runFormatOrValidate(ctx) {
|
|
|
11457
13186
|
}
|
|
11458
13187
|
warnLargeStateDocs(data.testCases);
|
|
11459
13188
|
console.log("Valid RawRun (schemaVersion 1).");
|
|
11460
|
-
process.exit(
|
|
13189
|
+
process.exit(EXIT_SUCCESS3);
|
|
11461
13190
|
}
|
|
11462
13191
|
if (args.inputType === "canonical") {
|
|
11463
13192
|
try {
|
|
@@ -11470,9 +13199,9 @@ ${msg}`);
|
|
|
11470
13199
|
}
|
|
11471
13200
|
const run = data;
|
|
11472
13201
|
if (args.emitCanonical) {
|
|
11473
|
-
const outPath =
|
|
11474
|
-
|
|
11475
|
-
|
|
13202
|
+
const outPath = path19.resolve(args.emitCanonical);
|
|
13203
|
+
fs17.mkdirSync(path19.dirname(outPath), { recursive: true });
|
|
13204
|
+
fs17.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
11476
13205
|
}
|
|
11477
13206
|
try {
|
|
11478
13207
|
const history = runHistoryPipeline(run, args);
|
|
@@ -11480,7 +13209,7 @@ ${msg}`);
|
|
|
11480
13209
|
runCustomFormatters(run, customRequested, pluginConfig.formatters ?? {}, args);
|
|
11481
13210
|
await dispatchNotifications(run, args);
|
|
11482
13211
|
printResult(result, args, startMs);
|
|
11483
|
-
process.exit(
|
|
13212
|
+
process.exit(EXIT_SUCCESS3);
|
|
11484
13213
|
} catch (err) {
|
|
11485
13214
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11486
13215
|
console.error(`Generation failed: ${msg}`);
|
|
@@ -11505,7 +13234,7 @@ ${msg}`);
|
|
|
11505
13234
|
let raw = data;
|
|
11506
13235
|
let droppedMissingStory = 0;
|
|
11507
13236
|
if (args.synthesizeStories) {
|
|
11508
|
-
raw =
|
|
13237
|
+
raw = synthesizeStories4(raw);
|
|
11509
13238
|
} else {
|
|
11510
13239
|
const before = raw.testCases.length;
|
|
11511
13240
|
const withStory = raw.testCases.filter(
|
|
@@ -11518,7 +13247,7 @@ ${msg}`);
|
|
|
11518
13247
|
);
|
|
11519
13248
|
}
|
|
11520
13249
|
}
|
|
11521
|
-
const canonical =
|
|
13250
|
+
const canonical = canonicalizeRun6(raw);
|
|
11522
13251
|
try {
|
|
11523
13252
|
assertValidRun2(canonical);
|
|
11524
13253
|
} catch (err) {
|
|
@@ -11528,9 +13257,9 @@ ${msg}`);
|
|
|
11528
13257
|
process.exit(EXIT_CANONICAL_VALIDATION);
|
|
11529
13258
|
}
|
|
11530
13259
|
if (args.emitCanonical) {
|
|
11531
|
-
const outPath =
|
|
11532
|
-
|
|
11533
|
-
|
|
13260
|
+
const outPath = path19.resolve(args.emitCanonical);
|
|
13261
|
+
fs17.mkdirSync(path19.dirname(outPath), { recursive: true });
|
|
13262
|
+
fs17.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
|
|
11534
13263
|
}
|
|
11535
13264
|
try {
|
|
11536
13265
|
const history = runHistoryPipeline(canonical, args);
|
|
@@ -11538,7 +13267,7 @@ ${msg}`);
|
|
|
11538
13267
|
runCustomFormatters(canonical, customRequested, pluginConfig.formatters ?? {}, args);
|
|
11539
13268
|
await dispatchNotifications(canonical, args);
|
|
11540
13269
|
printResult(result, args, startMs, droppedMissingStory);
|
|
11541
|
-
process.exit(
|
|
13270
|
+
process.exit(EXIT_SUCCESS3);
|
|
11542
13271
|
} catch (err) {
|
|
11543
13272
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11544
13273
|
console.error(`Generation failed: ${msg}`);
|
|
@@ -11555,9 +13284,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
|
|
|
11555
13284
|
const ext = formatter.fileExtension ?? formatName;
|
|
11556
13285
|
const baseName = args.outputName ?? "report";
|
|
11557
13286
|
const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
|
|
11558
|
-
const filepath =
|
|
11559
|
-
|
|
11560
|
-
|
|
13287
|
+
const filepath = path19.join(outputDir, filename);
|
|
13288
|
+
fs17.mkdirSync(outputDir, { recursive: true });
|
|
13289
|
+
fs17.writeFileSync(filepath, content, "utf8");
|
|
11561
13290
|
console.log(`Generated: ${filepath}`);
|
|
11562
13291
|
} catch (err) {
|
|
11563
13292
|
console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -11607,13 +13336,13 @@ async function dispatchNotifications(run, args) {
|
|
|
11607
13336
|
}
|
|
11608
13337
|
function runHistoryPipeline(run, args) {
|
|
11609
13338
|
if (!args.historyFile) return void 0;
|
|
11610
|
-
const historyPath =
|
|
13339
|
+
const historyPath = path19.resolve(args.historyFile);
|
|
11611
13340
|
const store = loadHistory(
|
|
11612
13341
|
{ filePath: historyPath },
|
|
11613
13342
|
{
|
|
11614
13343
|
readFile: (p) => {
|
|
11615
13344
|
try {
|
|
11616
|
-
return
|
|
13345
|
+
return fs17.readFileSync(p, "utf8");
|
|
11617
13346
|
} catch {
|
|
11618
13347
|
return void 0;
|
|
11619
13348
|
}
|
|
@@ -11626,11 +13355,11 @@ function runHistoryPipeline(run, args) {
|
|
|
11626
13355
|
run,
|
|
11627
13356
|
maxRuns: args.maxHistoryRuns
|
|
11628
13357
|
});
|
|
11629
|
-
const dir =
|
|
11630
|
-
|
|
13358
|
+
const dir = path19.dirname(historyPath);
|
|
13359
|
+
fs17.mkdirSync(dir, { recursive: true });
|
|
11631
13360
|
saveHistory(
|
|
11632
13361
|
{ filePath: historyPath, store: updated },
|
|
11633
|
-
{ writeFile: (p, content) =>
|
|
13362
|
+
{ writeFile: (p, content) => fs17.writeFileSync(p, content, "utf8") }
|
|
11634
13363
|
);
|
|
11635
13364
|
let metricsCount = 0;
|
|
11636
13365
|
for (const testId of Object.keys(updated.tests)) {
|
|
@@ -11781,7 +13510,7 @@ function loadReviewContext(args) {
|
|
|
11781
13510
|
console.error(
|
|
11782
13511
|
"Error: --code-diff requires --patch <file> (generate it with: git diff --histogram > changes.patch)."
|
|
11783
13512
|
);
|
|
11784
|
-
process.exit(
|
|
13513
|
+
process.exit(EXIT_USAGE3);
|
|
11785
13514
|
}
|
|
11786
13515
|
const sidecar = JSON.parse(readFileInput(args.codeDiffPath));
|
|
11787
13516
|
const patch = readFileInput(args.patchPath);
|
|
@@ -11801,11 +13530,11 @@ function writeReviewReport(review, args) {
|
|
|
11801
13530
|
const outputDir = args.outputDir ?? "reports";
|
|
11802
13531
|
const baseName = args.outputName ?? "evidence-review";
|
|
11803
13532
|
const suffix = args.outputNameTimestamp ? `-${Math.floor(review.run.startedAtMs / 1e3)}` : "";
|
|
11804
|
-
|
|
11805
|
-
const mdPath =
|
|
11806
|
-
const htmlPath =
|
|
11807
|
-
|
|
11808
|
-
|
|
13533
|
+
fs17.mkdirSync(outputDir, { recursive: true });
|
|
13534
|
+
const mdPath = path19.join(outputDir, `${baseName}${suffix}.md`);
|
|
13535
|
+
const htmlPath = path19.join(outputDir, `${baseName}${suffix}.html`);
|
|
13536
|
+
fs17.writeFileSync(mdPath, markdown, "utf8");
|
|
13537
|
+
fs17.writeFileSync(htmlPath, html, "utf8");
|
|
11809
13538
|
return [mdPath, htmlPath];
|
|
11810
13539
|
}
|
|
11811
13540
|
function evaluateReviewGate(review, args) {
|
|
@@ -11860,9 +13589,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
|
|
|
11860
13589
|
function printCompareResult(result, args, startMs) {
|
|
11861
13590
|
const durationMs = Date.now() - startMs;
|
|
11862
13591
|
if (result.prSummary && args.prSummaryFile) {
|
|
11863
|
-
const outputPath =
|
|
11864
|
-
|
|
11865
|
-
|
|
13592
|
+
const outputPath = path19.resolve(args.prSummaryFile);
|
|
13593
|
+
fs17.mkdirSync(path19.dirname(outputPath), { recursive: true });
|
|
13594
|
+
fs17.writeFileSync(outputPath, result.prSummary, "utf8");
|
|
11866
13595
|
}
|
|
11867
13596
|
if (args.jsonSummary) {
|
|
11868
13597
|
console.log(
|
|
@@ -11896,13 +13625,13 @@ function printCompareResult(result, args, startMs) {
|
|
|
11896
13625
|
}
|
|
11897
13626
|
}
|
|
11898
13627
|
function loadReleasePolicy(policyPath) {
|
|
11899
|
-
const resolved =
|
|
11900
|
-
if (!
|
|
13628
|
+
const resolved = path19.resolve(policyPath);
|
|
13629
|
+
if (!fs17.existsSync(resolved)) {
|
|
11901
13630
|
console.error(`Error: release policy file not found: ${resolved}`);
|
|
11902
|
-
process.exit(
|
|
13631
|
+
process.exit(EXIT_USAGE3);
|
|
11903
13632
|
}
|
|
11904
13633
|
try {
|
|
11905
|
-
const raw = JSON.parse(
|
|
13634
|
+
const raw = JSON.parse(fs17.readFileSync(resolved, "utf8"));
|
|
11906
13635
|
return {
|
|
11907
13636
|
allowedOmissions: Array.isArray(raw.allowedOmissions) ? raw.allowedOmissions : [],
|
|
11908
13637
|
allowedRegressions: Array.isArray(raw.allowedRegressions) ? raw.allowedRegressions : [],
|
|
@@ -11911,7 +13640,7 @@ function loadReleasePolicy(policyPath) {
|
|
|
11911
13640
|
} catch (err) {
|
|
11912
13641
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11913
13642
|
console.error(`Error reading release policy: ${msg}`);
|
|
11914
|
-
process.exit(
|
|
13643
|
+
process.exit(EXIT_USAGE3);
|
|
11915
13644
|
}
|
|
11916
13645
|
}
|
|
11917
13646
|
function applyReleasePolicy(result, policy) {
|
|
@@ -11963,7 +13692,7 @@ function evaluateCompareGate(result, args) {
|
|
|
11963
13692
|
return failures;
|
|
11964
13693
|
}
|
|
11965
13694
|
async function runPublishConfluence(rawArgs) {
|
|
11966
|
-
const { values, positionals } =
|
|
13695
|
+
const { values, positionals } = parseArgs3({
|
|
11967
13696
|
args: rawArgs,
|
|
11968
13697
|
options: {
|
|
11969
13698
|
"page-id": { type: "string" },
|
|
@@ -11999,16 +13728,16 @@ Optional:
|
|
|
11999
13728
|
--help Show this help
|
|
12000
13729
|
|
|
12001
13730
|
Generate an API token at https://id.atlassian.com/manage-profile/security/api-tokens`);
|
|
12002
|
-
process.exit(
|
|
13731
|
+
process.exit(EXIT_SUCCESS3);
|
|
12003
13732
|
}
|
|
12004
13733
|
const inputFile = positionals[0];
|
|
12005
13734
|
if (!inputFile) {
|
|
12006
13735
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
12007
|
-
process.exit(
|
|
13736
|
+
process.exit(EXIT_USAGE3);
|
|
12008
13737
|
}
|
|
12009
|
-
if (!
|
|
13738
|
+
if (!fs17.existsSync(inputFile)) {
|
|
12010
13739
|
console.error(`Error: file not found: ${inputFile}`);
|
|
12011
|
-
process.exit(
|
|
13740
|
+
process.exit(EXIT_USAGE3);
|
|
12012
13741
|
}
|
|
12013
13742
|
const baseUrl = values["base-url"] ?? process.env.CONFLUENCE_BASE_URL;
|
|
12014
13743
|
const email = values.email ?? process.env.CONFLUENCE_EMAIL;
|
|
@@ -12022,19 +13751,19 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
12022
13751
|
console.error(
|
|
12023
13752
|
"Error: --base-url or CONFLUENCE_BASE_URL is required (e.g. https://acme.atlassian.net/wiki)"
|
|
12024
13753
|
);
|
|
12025
|
-
process.exit(
|
|
13754
|
+
process.exit(EXIT_USAGE3);
|
|
12026
13755
|
}
|
|
12027
13756
|
if (!pageId && !spaceId) {
|
|
12028
13757
|
console.error(
|
|
12029
13758
|
"Error: specify either --page-id (to update) or --space-id (to create)"
|
|
12030
13759
|
);
|
|
12031
|
-
process.exit(
|
|
13760
|
+
process.exit(EXIT_USAGE3);
|
|
12032
13761
|
}
|
|
12033
13762
|
if (!pageId && !title) {
|
|
12034
13763
|
console.error("Error: --title is required when creating a new page");
|
|
12035
|
-
process.exit(
|
|
13764
|
+
process.exit(EXIT_USAGE3);
|
|
12036
13765
|
}
|
|
12037
|
-
const adf =
|
|
13766
|
+
const adf = fs17.readFileSync(path19.resolve(inputFile), "utf8");
|
|
12038
13767
|
if (dryRun) {
|
|
12039
13768
|
console.log(
|
|
12040
13769
|
JSON.stringify(
|
|
@@ -12051,13 +13780,13 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
12051
13780
|
2
|
|
12052
13781
|
)
|
|
12053
13782
|
);
|
|
12054
|
-
process.exit(
|
|
13783
|
+
process.exit(EXIT_SUCCESS3);
|
|
12055
13784
|
}
|
|
12056
13785
|
if (!email || !token) {
|
|
12057
13786
|
console.error(
|
|
12058
13787
|
"Error: --email/CONFLUENCE_EMAIL and --token/CONFLUENCE_TOKEN are required unless --dry-run is set"
|
|
12059
13788
|
);
|
|
12060
|
-
process.exit(
|
|
13789
|
+
process.exit(EXIT_USAGE3);
|
|
12061
13790
|
}
|
|
12062
13791
|
try {
|
|
12063
13792
|
const result = await publishConfluencePage(
|
|
@@ -12067,14 +13796,14 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
12067
13796
|
console.log(
|
|
12068
13797
|
`${result.action === "created" ? "Created" : "Updated"} "${result.title}" (v${result.version}) \u2192 ${result.url}`
|
|
12069
13798
|
);
|
|
12070
|
-
process.exit(
|
|
13799
|
+
process.exit(EXIT_SUCCESS3);
|
|
12071
13800
|
} catch (err) {
|
|
12072
13801
|
console.error(`Error: ${err.message}`);
|
|
12073
13802
|
process.exit(EXIT_GENERATION);
|
|
12074
13803
|
}
|
|
12075
13804
|
}
|
|
12076
13805
|
async function runPublishJira(rawArgs) {
|
|
12077
|
-
const { values, positionals } =
|
|
13806
|
+
const { values, positionals } = parseArgs3({
|
|
12078
13807
|
args: rawArgs,
|
|
12079
13808
|
options: {
|
|
12080
13809
|
issue: { type: "string" },
|
|
@@ -12106,16 +13835,16 @@ Optional:
|
|
|
12106
13835
|
--help Show this help
|
|
12107
13836
|
|
|
12108
13837
|
Generate an API token at https://id.atlassian.com/manage-profile/security/api-tokens`);
|
|
12109
|
-
process.exit(
|
|
13838
|
+
process.exit(EXIT_SUCCESS3);
|
|
12110
13839
|
}
|
|
12111
13840
|
const inputFile = positionals[0];
|
|
12112
13841
|
if (!inputFile) {
|
|
12113
13842
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
12114
|
-
process.exit(
|
|
13843
|
+
process.exit(EXIT_USAGE3);
|
|
12115
13844
|
}
|
|
12116
|
-
if (!
|
|
13845
|
+
if (!fs17.existsSync(inputFile)) {
|
|
12117
13846
|
console.error(`Error: file not found: ${inputFile}`);
|
|
12118
|
-
process.exit(
|
|
13847
|
+
process.exit(EXIT_USAGE3);
|
|
12119
13848
|
}
|
|
12120
13849
|
const baseUrl = values["base-url"] ?? process.env.JIRA_BASE_URL;
|
|
12121
13850
|
const email = values.email ?? process.env.JIRA_EMAIL;
|
|
@@ -12127,20 +13856,20 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
12127
13856
|
console.error(
|
|
12128
13857
|
"Error: --base-url or JIRA_BASE_URL is required (e.g. https://acme.atlassian.net)"
|
|
12129
13858
|
);
|
|
12130
|
-
process.exit(
|
|
13859
|
+
process.exit(EXIT_USAGE3);
|
|
12131
13860
|
}
|
|
12132
13861
|
if (!issueKey) {
|
|
12133
13862
|
console.error("Error: --issue <KEY> is required (e.g. --issue PROJ-123)");
|
|
12134
|
-
process.exit(
|
|
13863
|
+
process.exit(EXIT_USAGE3);
|
|
12135
13864
|
}
|
|
12136
13865
|
if (modeRaw !== "comment" && modeRaw !== "description") {
|
|
12137
13866
|
console.error(
|
|
12138
13867
|
`Error: --mode must be "comment" or "description" (got "${modeRaw}")`
|
|
12139
13868
|
);
|
|
12140
|
-
process.exit(
|
|
13869
|
+
process.exit(EXIT_USAGE3);
|
|
12141
13870
|
}
|
|
12142
13871
|
const mode = modeRaw;
|
|
12143
|
-
const adf =
|
|
13872
|
+
const adf = fs17.readFileSync(path19.resolve(inputFile), "utf8");
|
|
12144
13873
|
if (dryRun) {
|
|
12145
13874
|
console.log(
|
|
12146
13875
|
JSON.stringify(
|
|
@@ -12155,13 +13884,13 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
12155
13884
|
2
|
|
12156
13885
|
)
|
|
12157
13886
|
);
|
|
12158
|
-
process.exit(
|
|
13887
|
+
process.exit(EXIT_SUCCESS3);
|
|
12159
13888
|
}
|
|
12160
13889
|
if (!email || !token) {
|
|
12161
13890
|
console.error(
|
|
12162
13891
|
"Error: --email/JIRA_EMAIL and --token/JIRA_TOKEN are required unless --dry-run is set"
|
|
12163
13892
|
);
|
|
12164
|
-
process.exit(
|
|
13893
|
+
process.exit(EXIT_USAGE3);
|
|
12165
13894
|
}
|
|
12166
13895
|
try {
|
|
12167
13896
|
const result = await publishJiraIssue(
|
|
@@ -12175,14 +13904,14 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
12175
13904
|
} else {
|
|
12176
13905
|
console.log(`Updated description for ${result.issueKey} \u2192 ${result.url}`);
|
|
12177
13906
|
}
|
|
12178
|
-
process.exit(
|
|
13907
|
+
process.exit(EXIT_SUCCESS3);
|
|
12179
13908
|
} catch (err) {
|
|
12180
13909
|
console.error(`Error: ${err.message}`);
|
|
12181
13910
|
process.exit(EXIT_GENERATION);
|
|
12182
13911
|
}
|
|
12183
13912
|
}
|
|
12184
13913
|
function runNew(rawArgs) {
|
|
12185
|
-
const { values, positionals } =
|
|
13914
|
+
const { values, positionals } = parseArgs3({
|
|
12186
13915
|
args: rawArgs,
|
|
12187
13916
|
options: {
|
|
12188
13917
|
dir: { type: "string" },
|
|
@@ -12199,7 +13928,7 @@ function runNew(rawArgs) {
|
|
|
12199
13928
|
`Usage: executable-stories new <template> "<name>" [--dir <docs-dir>] [--scenario-id <id>] [--force]`
|
|
12200
13929
|
);
|
|
12201
13930
|
console.error(`Templates: ${TEMPLATES.join(", ")}`);
|
|
12202
|
-
return
|
|
13931
|
+
return EXIT_USAGE3;
|
|
12203
13932
|
}
|
|
12204
13933
|
try {
|
|
12205
13934
|
const result = scaffoldDoc({
|
|
@@ -12213,36 +13942,41 @@ function runNew(rawArgs) {
|
|
|
12213
13942
|
console.log(` Title: ${result.title}`);
|
|
12214
13943
|
console.log("");
|
|
12215
13944
|
console.log("Next: fill in the content and link verifying stories in `verifiedBy`.");
|
|
12216
|
-
return
|
|
13945
|
+
return EXIT_SUCCESS3;
|
|
12217
13946
|
} catch (err) {
|
|
12218
13947
|
console.error(`Error: ${err.message}`);
|
|
12219
|
-
return
|
|
13948
|
+
return EXIT_USAGE3;
|
|
12220
13949
|
}
|
|
12221
13950
|
}
|
|
12222
13951
|
async function runCheckLinks(rawArgs) {
|
|
12223
|
-
const { values, positionals } =
|
|
13952
|
+
const { values, positionals } = parseArgs3({
|
|
12224
13953
|
args: rawArgs,
|
|
12225
13954
|
options: {
|
|
12226
13955
|
external: { type: "boolean", default: false },
|
|
12227
|
-
json: { type: "boolean", default: false }
|
|
13956
|
+
json: { type: "boolean", default: false },
|
|
13957
|
+
"site-root": { type: "string" },
|
|
13958
|
+
assets: { type: "string", multiple: true }
|
|
12228
13959
|
},
|
|
12229
13960
|
allowPositionals: true,
|
|
12230
13961
|
strict: true
|
|
12231
13962
|
});
|
|
12232
13963
|
try {
|
|
13964
|
+
const assets = values.assets;
|
|
12233
13965
|
const report = await checkLinks({
|
|
12234
13966
|
target: positionals[0] ?? ".",
|
|
12235
|
-
checkExternal: values.external
|
|
13967
|
+
checkExternal: values.external,
|
|
13968
|
+
...values["site-root"] ? { siteRoot: values["site-root"] } : {},
|
|
13969
|
+
...assets && assets.length > 0 ? { assetRoots: assets } : {}
|
|
12236
13970
|
});
|
|
12237
13971
|
console.log(values.json ? JSON.stringify(report, null, 2) : formatLinkReport(report));
|
|
12238
|
-
return report.brokenCount > 0 ? EXIT_GENERATION :
|
|
13972
|
+
return report.brokenCount > 0 ? EXIT_GENERATION : EXIT_SUCCESS3;
|
|
12239
13973
|
} catch (err) {
|
|
12240
13974
|
console.error(`Error: ${err.message}`);
|
|
12241
|
-
return
|
|
13975
|
+
return EXIT_USAGE3;
|
|
12242
13976
|
}
|
|
12243
13977
|
}
|
|
12244
13978
|
async function runImportOpenApi(rawArgs) {
|
|
12245
|
-
const { values, positionals } =
|
|
13979
|
+
const { values, positionals } = parseArgs3({
|
|
12246
13980
|
args: rawArgs,
|
|
12247
13981
|
options: {
|
|
12248
13982
|
"output-dir": { type: "string" },
|
|
@@ -12255,7 +13989,7 @@ async function runImportOpenApi(rawArgs) {
|
|
|
12255
13989
|
const spec = positionals[0];
|
|
12256
13990
|
if (!spec) {
|
|
12257
13991
|
console.error(`Usage: executable-stories import-openapi <spec.json|yaml> [--output-dir <dir>] [--run <story-report.json>] [--force]`);
|
|
12258
|
-
return
|
|
13992
|
+
return EXIT_USAGE3;
|
|
12259
13993
|
}
|
|
12260
13994
|
try {
|
|
12261
13995
|
const result = await importOpenApi({
|
|
@@ -12269,10 +14003,10 @@ async function runImportOpenApi(rawArgs) {
|
|
|
12269
14003
|
if (result.uncoveredCount > 0) {
|
|
12270
14004
|
console.log(` \u26A0 ${result.uncoveredCount} endpoint(s) have no verifying story`);
|
|
12271
14005
|
}
|
|
12272
|
-
return
|
|
14006
|
+
return EXIT_SUCCESS3;
|
|
12273
14007
|
} catch (err) {
|
|
12274
14008
|
console.error(`Error: ${err.message}`);
|
|
12275
|
-
return
|
|
14009
|
+
return EXIT_USAGE3;
|
|
12276
14010
|
}
|
|
12277
14011
|
}
|
|
12278
14012
|
async function runDeploy(rawArgs) {
|
|
@@ -12282,9 +14016,9 @@ async function runDeploy(rawArgs) {
|
|
|
12282
14016
|
console.error(" deploy record <file> --env <env> [--tag <tag>] [--ledger <path>]");
|
|
12283
14017
|
console.error(" deploy status [--ledger <path>] [--json]");
|
|
12284
14018
|
console.error(" deploy diff <env-a> <env-b> [--ledger <path>] [--json]");
|
|
12285
|
-
return
|
|
14019
|
+
return EXIT_USAGE3;
|
|
12286
14020
|
}
|
|
12287
|
-
const { values, positionals } =
|
|
14021
|
+
const { values, positionals } = parseArgs3({
|
|
12288
14022
|
args: rawArgs.slice(1),
|
|
12289
14023
|
options: {
|
|
12290
14024
|
env: { type: "string" },
|
|
@@ -12309,19 +14043,19 @@ OPTIONS
|
|
|
12309
14043
|
--tag <tag> Optional Git tag for this deployment (e.g. v1.2.3)
|
|
12310
14044
|
--ledger <path> Path to deployment ledger JSON (default: .executable-stories/deployments.json)
|
|
12311
14045
|
--json Output as JSON instead of text`);
|
|
12312
|
-
return
|
|
14046
|
+
return EXIT_SUCCESS3;
|
|
12313
14047
|
}
|
|
12314
14048
|
const ledgerPath = values.ledger;
|
|
12315
14049
|
if (mode === "record") {
|
|
12316
14050
|
const inputFile = positionals[0];
|
|
12317
14051
|
if (!inputFile) {
|
|
12318
14052
|
console.error("Error: deploy record requires an input file.");
|
|
12319
|
-
return
|
|
14053
|
+
return EXIT_USAGE3;
|
|
12320
14054
|
}
|
|
12321
14055
|
const env = values.env;
|
|
12322
14056
|
if (!env) {
|
|
12323
14057
|
console.error("Error: deploy record requires --env <environment>.");
|
|
12324
|
-
return
|
|
14058
|
+
return EXIT_USAGE3;
|
|
12325
14059
|
}
|
|
12326
14060
|
const text2 = readFileInput(inputFile);
|
|
12327
14061
|
const { run } = normalizeRunFromText(text2, {
|
|
@@ -12347,14 +14081,14 @@ OPTIONS
|
|
|
12347
14081
|
console.error(` Tag: ${result.entry.tag}`);
|
|
12348
14082
|
}
|
|
12349
14083
|
console.error(` Ledger: ${result.ledgerPath}`);
|
|
12350
|
-
return
|
|
14084
|
+
return EXIT_SUCCESS3;
|
|
12351
14085
|
}
|
|
12352
14086
|
if (mode === "status") {
|
|
12353
14087
|
const status = getDeploymentStatus(ledgerPath);
|
|
12354
14088
|
const envs = Object.keys(status.environments);
|
|
12355
14089
|
if (envs.length === 0) {
|
|
12356
14090
|
console.error("No deployments recorded yet.");
|
|
12357
|
-
return
|
|
14091
|
+
return EXIT_SUCCESS3;
|
|
12358
14092
|
}
|
|
12359
14093
|
if (values.json) {
|
|
12360
14094
|
console.log(JSON.stringify(status, null, 2));
|
|
@@ -12380,14 +14114,14 @@ OPTIONS
|
|
|
12380
14114
|
}
|
|
12381
14115
|
console.log(`Ledger: ${ledgerPath}`);
|
|
12382
14116
|
}
|
|
12383
|
-
return
|
|
14117
|
+
return EXIT_SUCCESS3;
|
|
12384
14118
|
}
|
|
12385
14119
|
if (mode === "diff") {
|
|
12386
14120
|
const envA = positionals[0];
|
|
12387
14121
|
const envB = positionals[1];
|
|
12388
14122
|
if (!envA || !envB) {
|
|
12389
14123
|
console.error("Error: deploy diff requires two environment names.");
|
|
12390
|
-
return
|
|
14124
|
+
return EXIT_USAGE3;
|
|
12391
14125
|
}
|
|
12392
14126
|
try {
|
|
12393
14127
|
const drift = getEnvironmentDrift(ledgerPath, envA, envB);
|
|
@@ -12433,11 +14167,11 @@ OPTIONS
|
|
|
12433
14167
|
}
|
|
12434
14168
|
} catch (err) {
|
|
12435
14169
|
console.error(`Error: ${err.message}`);
|
|
12436
|
-
return
|
|
14170
|
+
return EXIT_USAGE3;
|
|
12437
14171
|
}
|
|
12438
|
-
return
|
|
14172
|
+
return EXIT_SUCCESS3;
|
|
12439
14173
|
}
|
|
12440
|
-
return
|
|
14174
|
+
return EXIT_USAGE3;
|
|
12441
14175
|
}
|
|
12442
14176
|
function createDefaultCliArgs() {
|
|
12443
14177
|
return {
|
|
@@ -12493,6 +14227,6 @@ function createDefaultCliArgs() {
|
|
|
12493
14227
|
}
|
|
12494
14228
|
main().catch((err) => {
|
|
12495
14229
|
console.error(err);
|
|
12496
|
-
process.exit(
|
|
14230
|
+
process.exit(EXIT_USAGE3);
|
|
12497
14231
|
});
|
|
12498
14232
|
//# sourceMappingURL=cli.js.map
|