executable-stories-formatters 0.11.0 → 0.11.1

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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import "fs";
3
- import * as path10 from "path";
3
+ import * as path11 from "path";
4
4
  import * as fsPromises from "fs/promises";
5
5
 
6
6
  // src/converters/acl/status.ts
@@ -889,23 +889,23 @@ function buildFeature(relSourceFile, group) {
889
889
  function ensureUniqueFeatureIds(features) {
890
890
  const seen = /* @__PURE__ */ new Map();
891
891
  for (const f of features) {
892
- const count = seen.get(f.id) ?? 0;
893
- if (count > 0) f.id = `${f.id}-${count + 1}`;
894
- seen.set(f.id, count + 1);
892
+ const count2 = seen.get(f.id) ?? 0;
893
+ if (count2 > 0) f.id = `${f.id}-${count2 + 1}`;
894
+ seen.set(f.id, count2 + 1);
895
895
  }
896
896
  }
897
897
  function ensureUniqueScenarioIds(feature) {
898
898
  const seen = /* @__PURE__ */ new Map();
899
899
  for (const s of feature.scenarios) {
900
- const count = seen.get(s.id) ?? 0;
901
- if (count > 0) {
902
- const newId = `${s.id}-${count + 1}`;
900
+ const count2 = seen.get(s.id) ?? 0;
901
+ if (count2 > 0) {
902
+ const newId = `${s.id}-${count2 + 1}`;
903
903
  for (const step of s.steps) {
904
904
  step.id = step.id.replace(s.id, newId);
905
905
  }
906
906
  s.id = newId;
907
907
  }
908
- seen.set(s.id, count + 1);
908
+ seen.set(s.id, count2 + 1);
909
909
  }
910
910
  }
911
911
  function toStoryReport(run) {
@@ -15437,6 +15437,88 @@ function groupBy6(items, keyFn) {
15437
15437
  return map;
15438
15438
  }
15439
15439
 
15440
+ // src/formatters/release-manifest.ts
15441
+ import { createHash as createHash2 } from "crypto";
15442
+ var ReleaseManifestFormatter = class {
15443
+ format(run) {
15444
+ const manifest = toReleaseManifest(run);
15445
+ const lines = [];
15446
+ lines.push("# Release Manifest");
15447
+ lines.push("");
15448
+ lines.push(`Generated: ${manifest.generatedAt}`);
15449
+ lines.push(`Run: ${manifest.run.startedAt} to ${manifest.run.finishedAt}`);
15450
+ if (manifest.run.branch) lines.push(`Branch: ${manifest.run.branch}`);
15451
+ if (manifest.run.gitSha) lines.push(`Commit: ${manifest.run.gitSha}`);
15452
+ lines.push(`Tested-together hash: \`${manifest.testedTogetherHash}\``);
15453
+ lines.push("");
15454
+ lines.push("| Scenarios | Passed | Failed | Skipped | Pending |");
15455
+ lines.push("| ---: | ---: | ---: | ---: | ---: |");
15456
+ lines.push(`| ${manifest.run.total} | ${manifest.run.passed} | ${manifest.run.failed} | ${manifest.run.skipped} | ${manifest.run.pending} |`);
15457
+ lines.push("");
15458
+ lines.push("## Scenarios");
15459
+ lines.push("");
15460
+ lines.push("| Status | Scenario | Source | Tags |");
15461
+ lines.push("| --- | --- | --- | --- |");
15462
+ for (const scenario of manifest.scenarios) {
15463
+ const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
15464
+ const tags = scenario.tags.length > 0 ? scenario.tags.map((tag) => `\`${tag}\``).join(", ") : "";
15465
+ lines.push(`| ${renderStatus(scenario.status)} | ${escapePipe(scenario.title)} | \`${source}\` | ${tags} |`);
15466
+ }
15467
+ return lines.join("\n");
15468
+ }
15469
+ };
15470
+ function toReleaseManifest(run) {
15471
+ const scenarios = [...run.testCases].sort((a, b) => a.id.localeCompare(b.id)).map((tc) => ({
15472
+ id: tc.id,
15473
+ title: tc.story.scenario,
15474
+ status: tc.status,
15475
+ sourceFile: tc.sourceFile,
15476
+ sourceLine: tc.sourceLine,
15477
+ tags: tc.tags
15478
+ }));
15479
+ const fingerprint = scenarios.map((scenario) => `${scenario.id}:${scenario.status}`).join("\n");
15480
+ return {
15481
+ schemaVersion: "1.0",
15482
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
15483
+ run: {
15484
+ startedAt: new Date(run.startedAtMs).toISOString(),
15485
+ finishedAt: new Date(run.finishedAtMs).toISOString(),
15486
+ gitSha: run.gitSha,
15487
+ branch: getBranch(run),
15488
+ total: run.testCases.length,
15489
+ passed: count(run.testCases, "passed"),
15490
+ failed: count(run.testCases, "failed"),
15491
+ skipped: count(run.testCases, "skipped"),
15492
+ pending: count(run.testCases, "pending")
15493
+ },
15494
+ testedTogetherHash: createHash2("sha256").update(fingerprint).digest("hex"),
15495
+ scenarios
15496
+ };
15497
+ }
15498
+ function getBranch(run) {
15499
+ return run.ci?.branch;
15500
+ }
15501
+ function count(testCases, status) {
15502
+ return testCases.filter((tc) => tc.status === status).length;
15503
+ }
15504
+ function renderStatus(status) {
15505
+ switch (status) {
15506
+ case "passed":
15507
+ return "passed";
15508
+ case "failed":
15509
+ return "failed";
15510
+ case "skipped":
15511
+ return "skipped";
15512
+ case "pending":
15513
+ return "pending";
15514
+ default:
15515
+ return status;
15516
+ }
15517
+ }
15518
+ function escapePipe(value) {
15519
+ return value.replace(/\|/g, "\\|");
15520
+ }
15521
+
15440
15522
  // src/formatters/cucumber-messages/synthesize-feature.ts
15441
15523
  function extractFeatureName(testCases, uri) {
15442
15524
  for (const tc of testCases) {
@@ -15503,7 +15585,7 @@ function synthesizeFeature(uri, testCases) {
15503
15585
  }
15504
15586
 
15505
15587
  // src/utils/cucumber-messages.ts
15506
- import { createHash as createHash2 } from "crypto";
15588
+ import { createHash as createHash3 } from "crypto";
15507
15589
  function msToTimestamp(ms) {
15508
15590
  const seconds = Math.floor(ms / 1e3);
15509
15591
  const nanos = Math.round(ms % 1e3 * 1e6);
@@ -15569,7 +15651,7 @@ function statusToCucumberStatus(status) {
15569
15651
  }
15570
15652
  function deterministicId(kind, salt, ...parts) {
15571
15653
  const input = [salt, kind, ...parts].join("::");
15572
- return createHash2("sha1").update(input).digest("hex").slice(0, 36);
15654
+ return createHash3("sha1").update(input).digest("hex").slice(0, 36);
15573
15655
  }
15574
15656
 
15575
15657
  // src/formatters/cucumber-messages/build-gherkin-document.ts
@@ -16057,8 +16139,8 @@ function extractDocAttachments(step) {
16057
16139
  }
16058
16140
  return attachments;
16059
16141
  }
16060
- function guessMediaType(path11) {
16061
- const lower = path11.toLowerCase();
16142
+ function guessMediaType(path12) {
16143
+ const lower = path12.toLowerCase();
16062
16144
  if (lower.endsWith(".png")) return "image/png";
16063
16145
  if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
16064
16146
  if (lower.endsWith(".gif")) return "image/gif";
@@ -16199,11 +16281,11 @@ var CucumberHtmlFormatter = class {
16199
16281
  for (const envelope of envelopes) {
16200
16282
  const accepted = htmlStream.write(envelope);
16201
16283
  if (!accepted) {
16202
- await new Promise((resolve8) => htmlStream.once("drain", resolve8));
16284
+ await new Promise((resolve9) => htmlStream.once("drain", resolve9));
16203
16285
  }
16204
16286
  }
16205
- await new Promise((resolve8, reject) => {
16206
- collector.on("finish", resolve8);
16287
+ await new Promise((resolve9, reject) => {
16288
+ collector.on("finish", resolve9);
16207
16289
  collector.on("error", reject);
16208
16290
  htmlStream.end();
16209
16291
  });
@@ -17267,8 +17349,8 @@ ${body}`;
17267
17349
  }
17268
17350
  buildFrontmatter(run) {
17269
17351
  const badge = _AstroFormatter.computeBadge(run.testCases);
17270
- const count = run.testCases.length;
17271
- const description = `${count} scenario${count !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
17352
+ const count2 = run.testCases.length;
17353
+ const description = `${count2} scenario${count2 !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
17272
17354
  const lines = [
17273
17355
  "---",
17274
17356
  `title: ${yamlScalar(this.deriveTitle(run))}`,
@@ -18266,14 +18348,14 @@ ${result.errors.join("\n")}`);
18266
18348
  }
18267
18349
 
18268
18350
  // src/coverage-index.ts
18269
- function normalizePath(path11) {
18270
- return path11.replace(/^\.\//, "");
18351
+ function normalizePath(path12) {
18352
+ return path12.replace(/^\.\//, "");
18271
18353
  }
18272
18354
  function scenariosCoveringPaths(index, paths) {
18273
18355
  const queries = paths.map(normalizePath);
18274
18356
  return index.scenarios.filter(
18275
18357
  (scenario) => scenario.covers.some(
18276
- (glob) => queries.some((path11) => matchesPattern(normalizePath(glob), path11))
18358
+ (glob) => queries.some((path12) => matchesPattern(normalizePath(glob), path12))
18277
18359
  )
18278
18360
  );
18279
18361
  }
@@ -20190,18 +20272,18 @@ function deriveChangeType(tags) {
20190
20272
  }
20191
20273
  return "unknown";
20192
20274
  }
20193
- function extensionOf(path11) {
20194
- const base = path11.split("/").pop() ?? path11;
20275
+ function extensionOf(path12) {
20276
+ const base = path12.split("/").pop() ?? path12;
20195
20277
  const dot = base.lastIndexOf(".");
20196
20278
  return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
20197
20279
  }
20198
- function isTestFile(path11) {
20199
- return TEST_INFIX.test(path11);
20280
+ function isTestFile(path12) {
20281
+ return TEST_INFIX.test(path12);
20200
20282
  }
20201
- function isReviewableSource(path11) {
20202
- if (isTestFile(path11)) return false;
20203
- if (path11.endsWith(".d.ts")) return false;
20204
- return CODE_EXTENSIONS.has(extensionOf(path11));
20283
+ function isReviewableSource(path12) {
20284
+ if (isTestFile(path12)) return false;
20285
+ if (path12.endsWith(".d.ts")) return false;
20286
+ return CODE_EXTENSIONS.has(extensionOf(path12));
20205
20287
  }
20206
20288
  function testBaseKey(testFile) {
20207
20289
  return testFile.replace(TEST_INFIX, "");
@@ -20305,7 +20387,7 @@ function toClaim(testCase, changedSourcePaths) {
20305
20387
  const { strength, reasons } = gradeEvidence(testCase, audience);
20306
20388
  const key = testBaseKey(testCase.sourceFile);
20307
20389
  const coversFiles = changedSourcePaths.filter(
20308
- (path11) => sourceBaseKey(path11) === key
20390
+ (path12) => sourceBaseKey(path12) === key
20309
20391
  );
20310
20392
  return {
20311
20393
  id: testCase.id,
@@ -20835,11 +20917,116 @@ applyTheme(getEffectiveTheme());` : "";
20835
20917
  }
20836
20918
  };
20837
20919
 
20920
+ // src/deploy/ledger.ts
20921
+ import * as fs9 from "fs";
20922
+ import * as path10 from "path";
20923
+ function createEmptyLedger() {
20924
+ return {
20925
+ deployments: [],
20926
+ schemaVersion: 1
20927
+ };
20928
+ }
20929
+ function loadLedger(ledgerPath) {
20930
+ const resolved = path10.resolve(ledgerPath);
20931
+ if (!fs9.existsSync(resolved)) {
20932
+ return createEmptyLedger();
20933
+ }
20934
+ try {
20935
+ const raw = JSON.parse(fs9.readFileSync(resolved, "utf8"));
20936
+ if (raw.schemaVersion !== 1) {
20937
+ throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
20938
+ }
20939
+ return raw;
20940
+ } catch (err) {
20941
+ const msg = err instanceof Error ? err.message : String(err);
20942
+ throw new Error(`Failed to load deployment ledger at ${resolved}: ${msg}`);
20943
+ }
20944
+ }
20945
+ function saveLedger(ledger, ledgerPath) {
20946
+ const resolved = path10.resolve(ledgerPath);
20947
+ const dir = path10.dirname(resolved);
20948
+ fs9.mkdirSync(dir, { recursive: true });
20949
+ fs9.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
20950
+ }
20951
+ function getLatestDeployment(ledger, environment) {
20952
+ return [...ledger.deployments].reverse().find((d) => d.environment === environment);
20953
+ }
20954
+
20955
+ // src/deploy/index.ts
20956
+ function recordDeployment(args) {
20957
+ const ledger = loadLedger(args.ledgerPath);
20958
+ const previous = getLatestDeployment(ledger, args.environment);
20959
+ const entry = {
20960
+ environment: args.environment,
20961
+ tag: args.tag,
20962
+ sha: args.run.gitSha,
20963
+ runFile: args.runFilePath,
20964
+ scenarioIds: args.run.testCases.map((tc) => tc.id),
20965
+ scenarioStatuses: Object.fromEntries(args.run.testCases.map((tc) => [tc.id, tc.status])),
20966
+ timestamp: new Date(args.run.finishedAtMs).toISOString(),
20967
+ summary: countStatuses(args.run)
20968
+ };
20969
+ ledger.deployments.push(entry);
20970
+ if (previous) {
20971
+ const previousIds = new Set(previous.scenarioIds);
20972
+ const added = entry.scenarioIds.filter((id) => !previousIds.has(id)).length;
20973
+ const removed = previous.scenarioIds.filter((id) => !entry.scenarioIds.includes(id)).length;
20974
+ if (added > 0 || removed > 0) {
20975
+ }
20976
+ }
20977
+ saveLedger(ledger, args.ledgerPath);
20978
+ return { entry, ledgerPath: args.ledgerPath };
20979
+ }
20980
+ function getDeploymentStatus(ledgerPath) {
20981
+ const ledger = loadLedger(ledgerPath);
20982
+ const environments = {};
20983
+ for (const entry of ledger.deployments) {
20984
+ environments[entry.environment] = {
20985
+ latest: entry,
20986
+ previousDeployment: environments[entry.environment]?.latest
20987
+ };
20988
+ }
20989
+ return { environments, ledgerPath };
20990
+ }
20991
+ function getEnvironmentDrift(ledgerPath, envA, envB) {
20992
+ const ledger = loadLedger(ledgerPath);
20993
+ const aEntry = getLatestDeployment(ledger, envA);
20994
+ const bEntry = getLatestDeployment(ledger, envB);
20995
+ if (!aEntry) {
20996
+ throw new Error(`No deployment found for environment "${envA}"`);
20997
+ }
20998
+ if (!bEntry) {
20999
+ throw new Error(`No deployment found for environment "${envB}"`);
21000
+ }
21001
+ const aIds = new Set(aEntry.scenarioIds);
21002
+ const bIds = new Set(bEntry.scenarioIds);
21003
+ const onlyInA = aEntry.scenarioIds.filter((id) => !bIds.has(id));
21004
+ const onlyInB = bEntry.scenarioIds.filter((id) => !aIds.has(id));
21005
+ const inBoth = aEntry.scenarioIds.filter((id) => bIds.has(id));
21006
+ const statusChanged = inBoth.map((id) => ({
21007
+ id,
21008
+ statusA: aEntry.scenarioStatuses?.[id] ?? "unknown",
21009
+ statusB: bEntry.scenarioStatuses?.[id] ?? "unknown"
21010
+ })).filter((item) => item.statusA !== item.statusB);
21011
+ return { environmentA: envA, environmentB: envB, onlyInA, onlyInB, inBoth, statusChanged, aEntry, bEntry };
21012
+ }
21013
+ function countStatuses(run) {
21014
+ const summary = { total: 0, passed: 0, failed: 0, skipped: 0, pending: 0 };
21015
+ for (const tc of run.testCases) {
21016
+ summary.total++;
21017
+ if (tc.status in summary) {
21018
+ summary[tc.status]++;
21019
+ }
21020
+ }
21021
+ return summary;
21022
+ }
21023
+
20838
21024
  // src/index.ts
20839
21025
  var FORMAT_EXTENSIONS = {
20840
21026
  astro: ".md",
20841
21027
  "behavior-manifest-json": ".behavior-manifest.json",
20842
21028
  markdown: ".md",
21029
+ "release-manifest": ".release-manifest.md",
20843
21030
  html: ".html",
20844
21031
  "cucumber-html": ".cucumber.html",
20845
21032
  junit: ".junit.xml",
@@ -20878,11 +21065,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
20878
21065
  const ext = FORMAT_EXTENSIONS[format];
20879
21066
  const effectiveName = outputName + (outputNameSuffix ?? "");
20880
21067
  if (mode === "aggregated") {
20881
- return toPosix(path10.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
21068
+ return toPosix(path11.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
20882
21069
  }
20883
21070
  const normalizedSource = toPosix(sourceFile);
20884
- const dirOfSource = path10.posix.dirname(normalizedSource);
20885
- let baseName = path10.posix.basename(normalizedSource);
21071
+ const dirOfSource = path11.posix.dirname(normalizedSource);
21072
+ let baseName = path11.posix.basename(normalizedSource);
20886
21073
  for (const testExt of TEST_EXTENSIONS) {
20887
21074
  if (baseName.endsWith(testExt)) {
20888
21075
  baseName = baseName.slice(0, -testExt.length);
@@ -20891,12 +21078,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
20891
21078
  }
20892
21079
  const fileName = `${baseName}.${effectiveName}${ext}`;
20893
21080
  if (colocatedStyle === "adjacent") {
20894
- return toPosix(path10.posix.join(dirOfSource, fileName));
21081
+ return toPosix(path11.posix.join(dirOfSource, fileName));
20895
21082
  }
20896
21083
  if (colocatedStyle === "flat") {
20897
- return toPosix(path10.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
21084
+ return toPosix(path11.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
20898
21085
  }
20899
- return toPosix(path10.posix.join(baseOutputDir, dirOfSource, fileName));
21086
+ return toPosix(path11.posix.join(baseOutputDir, dirOfSource, fileName));
20900
21087
  }
20901
21088
  function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
20902
21089
  const groups = /* @__PURE__ */ new Map();
@@ -21103,8 +21290,8 @@ var ReportGenerator = class {
21103
21290
  if (astroPaths) {
21104
21291
  for (const mdPath of astroPaths) {
21105
21292
  const content = await fsPromises.readFile(mdPath, "utf8");
21106
- const mdDir = path10.dirname(mdPath);
21107
- const assetsDir = path10.resolve(this.options.astro.assetsDir);
21293
+ const mdDir = path11.dirname(mdPath);
21294
+ const assetsDir = path11.resolve(this.options.astro.assetsDir);
21108
21295
  const result = copyMarkdownAssets({
21109
21296
  markdown: content,
21110
21297
  markdownDir: mdDir,
@@ -21135,9 +21322,9 @@ var ReportGenerator = class {
21135
21322
  if (groups.size === 0 && this.options.output.mode === "aggregated") {
21136
21323
  const ext = FORMAT_EXTENSIONS[format];
21137
21324
  const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
21138
- const outputPath = toPosix(path10.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
21325
+ const outputPath = toPosix(path11.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
21139
21326
  const content = await this.formatContent(run, format);
21140
- const dir = path10.dirname(outputPath);
21327
+ const dir = path11.dirname(outputPath);
21141
21328
  await fsPromises.mkdir(dir, { recursive: true });
21142
21329
  await this.deps.writeFile(outputPath, content);
21143
21330
  return [outputPath];
@@ -21149,7 +21336,7 @@ var ReportGenerator = class {
21149
21336
  testCases
21150
21337
  };
21151
21338
  const content = await this.formatContent(groupRun, format);
21152
- const dir = path10.dirname(outputPath);
21339
+ const dir = path11.dirname(outputPath);
21153
21340
  await fsPromises.mkdir(dir, { recursive: true });
21154
21341
  await this.deps.writeFile(outputPath, content);
21155
21342
  writtenPaths.push(outputPath);
@@ -21258,6 +21445,10 @@ var ReportGenerator = class {
21258
21445
  });
21259
21446
  return formatter.format(run);
21260
21447
  }
21448
+ case "release-manifest": {
21449
+ const formatter = new ReleaseManifestFormatter();
21450
+ return formatter.format(run);
21451
+ }
21261
21452
  case "story-report-json": {
21262
21453
  const formatter = new StoryReportJsonFormatter({
21263
21454
  pretty: this.options.storyReportJson.pretty
@@ -21292,7 +21483,7 @@ async function generateRunComparison(args) {
21292
21483
  await fsPromises.mkdir(outputDir, { recursive: true });
21293
21484
  for (const format of args.formats) {
21294
21485
  const ext = format === "html" ? ".html" : ".md";
21295
- const outputPath = toPosix(path10.join(outputDir, `${outputName}${ext}`));
21486
+ const outputPath = toPosix(path11.join(outputDir, `${outputName}${ext}`));
21296
21487
  const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
21297
21488
  await fsPromises.writeFile(outputPath, content, "utf8");
21298
21489
  files.push(outputPath);
@@ -21326,6 +21517,7 @@ export {
21326
21517
  MIN_METRIC_SAMPLES,
21327
21518
  MIN_PERF_SAMPLES,
21328
21519
  MarkdownFormatter,
21520
+ ReleaseManifestFormatter,
21329
21521
  ReportGenerator,
21330
21522
  ReviewHtmlFormatter,
21331
21523
  ReviewMarkdownFormatter,
@@ -21365,6 +21557,8 @@ export {
21365
21557
  generateTestCaseId,
21366
21558
  getAvailableThemes,
21367
21559
  getCssOnlyThemes,
21560
+ getDeploymentStatus,
21561
+ getEnvironmentDrift,
21368
21562
  gradeEvidence,
21369
21563
  hasSufficientHistory,
21370
21564
  isReviewableSource,
@@ -21386,6 +21580,7 @@ export {
21386
21580
  readBranchName,
21387
21581
  readGitSha,
21388
21582
  readPackageVersion,
21583
+ recordDeployment,
21389
21584
  regenerateArtifacts,
21390
21585
  resolveAttachment,
21391
21586
  resolveAttachments,
@@ -21405,6 +21600,7 @@ export {
21405
21600
  toBehaviorManifest,
21406
21601
  toCIInfo,
21407
21602
  toRawCIInfo,
21603
+ toReleaseManifest,
21408
21604
  toScenarioIndex,
21409
21605
  toStoryReport,
21410
21606
  tryGetActiveOtelContext,