executable-stories-formatters 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -1
- package/dist/adapters.d.cts +1 -1
- package/dist/adapters.d.ts +1 -1
- package/dist/cli.js +810 -6
- package/dist/cli.js.map +1 -1
- package/dist/{index-DyeUWfYK.d.cts → index-C4QO-SVT.d.cts} +33 -8
- package/dist/{index-DyeUWfYK.d.ts → index-C4QO-SVT.d.ts} +33 -8
- package/dist/index.cjs +900 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +403 -5
- package/dist/index.d.ts +403 -5
- package/dist/index.js +881 -17
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/schemas/README.md +11 -2
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,9 @@ __export(src_exports, {
|
|
|
35
35
|
CucumberMessagesFormatter: () => CucumberMessagesFormatter,
|
|
36
36
|
HtmlFormatter: () => HtmlFormatter,
|
|
37
37
|
JUnitFormatter: () => JUnitFormatter,
|
|
38
|
+
MIN_FLAKINESS_SAMPLES: () => MIN_FLAKINESS_SAMPLES,
|
|
39
|
+
MIN_METRIC_SAMPLES: () => MIN_METRIC_SAMPLES,
|
|
40
|
+
MIN_PERF_SAMPLES: () => MIN_PERF_SAMPLES,
|
|
38
41
|
MarkdownFormatter: () => MarkdownFormatter,
|
|
39
42
|
ReportGenerator: () => ReportGenerator,
|
|
40
43
|
STORY_META_KEY: () => STORY_META_KEY,
|
|
@@ -42,15 +45,21 @@ __export(src_exports, {
|
|
|
42
45
|
adaptPlaywrightRun: () => adaptPlaywrightRun,
|
|
43
46
|
adaptVitestRun: () => adaptVitestRun,
|
|
44
47
|
assertValidRun: () => assertValidRun,
|
|
48
|
+
calculateFlakiness: () => calculateFlakiness,
|
|
49
|
+
calculateStability: () => calculateStability,
|
|
45
50
|
canonicalizeRun: () => canonicalizeRun,
|
|
46
51
|
clearVersionCache: () => clearVersionCache,
|
|
52
|
+
computeTestMetrics: () => computeTestMetrics,
|
|
47
53
|
createReportGenerator: () => createReportGenerator,
|
|
48
54
|
deriveStepResults: () => deriveStepResults,
|
|
49
55
|
detectCI: () => detectCI4,
|
|
56
|
+
detectPerformanceTrend: () => detectPerformanceTrend,
|
|
50
57
|
findGitDir: () => findGitDir,
|
|
51
58
|
formatDuration: () => formatDuration2,
|
|
52
59
|
generateRunId: () => generateRunId,
|
|
53
60
|
generateTestCaseId: () => generateTestCaseId,
|
|
61
|
+
hasSufficientHistory: () => hasSufficientHistory,
|
|
62
|
+
loadHistory: () => loadHistory,
|
|
54
63
|
mergeStepResults: () => mergeStepResults,
|
|
55
64
|
msToNanoseconds: () => msToNanoseconds,
|
|
56
65
|
nanosecondsToMs: () => nanosecondsToMs,
|
|
@@ -66,8 +75,18 @@ __export(src_exports, {
|
|
|
66
75
|
resolveAttachment: () => resolveAttachment,
|
|
67
76
|
resolveAttachments: () => resolveAttachments,
|
|
68
77
|
resolveTraceUrl: () => resolveTraceUrl,
|
|
78
|
+
saveHistory: () => saveHistory,
|
|
79
|
+
sendNotifications: () => sendNotifications,
|
|
80
|
+
sendSlackNotification: () => sendSlackNotification,
|
|
81
|
+
sendTeamsNotification: () => sendTeamsNotification,
|
|
82
|
+
sendWebhookNotification: () => sendWebhookNotification,
|
|
83
|
+
signBody: () => signBody,
|
|
69
84
|
slugify: () => slugify,
|
|
85
|
+
stripAnsi: () => stripAnsi,
|
|
86
|
+
toCIInfo: () => toCIInfo,
|
|
87
|
+
toRawCIInfo: () => toRawCIInfo,
|
|
70
88
|
tryGetActiveOtelContext: () => tryGetActiveOtelContext,
|
|
89
|
+
updateHistory: () => updateHistory,
|
|
71
90
|
validateCanonicalRun: () => validateCanonicalRun
|
|
72
91
|
});
|
|
73
92
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -2633,6 +2652,21 @@ body {
|
|
|
2633
2652
|
}
|
|
2634
2653
|
}
|
|
2635
2654
|
|
|
2655
|
+
/* ============================================================================
|
|
2656
|
+
History metric badges
|
|
2657
|
+
============================================================================ */
|
|
2658
|
+
.badge { display: inline-block; padding: 2px 6px; border-radius: 4px; font-size: 0.75em; font-weight: 600; margin-left: 4px; vertical-align: middle; }
|
|
2659
|
+
.badge-grade { color: #fff; }
|
|
2660
|
+
.badge-grade-A { background: var(--success); }
|
|
2661
|
+
.badge-grade-B { background: #2196F3; }
|
|
2662
|
+
.badge-grade-C { background: #FF9800; }
|
|
2663
|
+
.badge-grade-D { background: #f44336; }
|
|
2664
|
+
.badge-grade-F { background: #9E0000; }
|
|
2665
|
+
.badge-flaky { background: #FF9800; color: #fff; }
|
|
2666
|
+
.badge-perf { font-size: 0.7em; }
|
|
2667
|
+
.badge-perf-improving { color: var(--success); }
|
|
2668
|
+
.badge-perf-regressing { color: var(--error); }
|
|
2669
|
+
|
|
2636
2670
|
`;
|
|
2637
2671
|
|
|
2638
2672
|
// src/formatters/html/renderers/status.ts
|
|
@@ -2666,7 +2700,22 @@ function renderMetaInfo(args, deps) {
|
|
|
2666
2700
|
items.push(`<dt>Git:</dt><dd>${deps.escapeHtml(shortSha)}</dd>`);
|
|
2667
2701
|
}
|
|
2668
2702
|
if (args.ciName) {
|
|
2669
|
-
|
|
2703
|
+
if (args.ciUrl && args.ciBuildNumber) {
|
|
2704
|
+
items.push(
|
|
2705
|
+
`<dt>CI:</dt><dd>${deps.escapeHtml(args.ciName)} <a href="${deps.escapeHtml(args.ciUrl)}">#${deps.escapeHtml(args.ciBuildNumber)}</a></dd>`
|
|
2706
|
+
);
|
|
2707
|
+
} else {
|
|
2708
|
+
items.push(`<dt>CI:</dt><dd>${deps.escapeHtml(args.ciName)}</dd>`);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
if (args.ciBranch) {
|
|
2712
|
+
items.push(`<dt>Branch:</dt><dd>${deps.escapeHtml(args.ciBranch)}</dd>`);
|
|
2713
|
+
}
|
|
2714
|
+
if (args.ciCommitSha) {
|
|
2715
|
+
const shortSha = args.ciCommitSha.length > 7 ? args.ciCommitSha.slice(0, 7) : args.ciCommitSha;
|
|
2716
|
+
items.push(
|
|
2717
|
+
`<dt>Commit:</dt><dd title="${deps.escapeHtml(args.ciCommitSha)}">${deps.escapeHtml(shortSha)}</dd>`
|
|
2718
|
+
);
|
|
2670
2719
|
}
|
|
2671
2720
|
return `<dl class="meta-info">${items.join("")}</dl>`;
|
|
2672
2721
|
}
|
|
@@ -2917,6 +2966,14 @@ function highlightStepParams(text, deps) {
|
|
|
2917
2966
|
return result;
|
|
2918
2967
|
}
|
|
2919
2968
|
|
|
2969
|
+
// src/history/sample-policy.ts
|
|
2970
|
+
var MIN_PERF_SAMPLES = 6;
|
|
2971
|
+
var MIN_METRIC_SAMPLES = 5;
|
|
2972
|
+
var MIN_FLAKINESS_SAMPLES = 3;
|
|
2973
|
+
function hasSufficientHistory(entries, min) {
|
|
2974
|
+
return entries.length >= min;
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2920
2977
|
// src/formatters/html/renderers/scenario.ts
|
|
2921
2978
|
function renderScenario(args, deps) {
|
|
2922
2979
|
const { tc } = args;
|
|
@@ -2937,6 +2994,19 @@ function renderScenario(args, deps) {
|
|
|
2937
2994
|
traceBadge = `<span class="tag trace-tag" title="${deps.escapeHtml(otelMeta.traceId)}">${deps.escapeHtml(shortId)}\u2026</span>`;
|
|
2938
2995
|
}
|
|
2939
2996
|
}
|
|
2997
|
+
let metricBadges = "";
|
|
2998
|
+
const { metrics } = args;
|
|
2999
|
+
if (metrics && metrics.sampleSize >= MIN_METRIC_SAMPLES) {
|
|
3000
|
+
const grade = metrics.stabilityGrade;
|
|
3001
|
+
metricBadges += `<span class="badge badge-grade badge-grade-${grade}" title="Pass rate: ${(metrics.passRate * 100).toFixed(0)}% (${metrics.sampleSize} runs)">${grade}</span>`;
|
|
3002
|
+
if (metrics.flakinessLevel !== "stable") {
|
|
3003
|
+
metricBadges += `<span class="badge badge-flaky">${metrics.flakinessLevel}</span>`;
|
|
3004
|
+
}
|
|
3005
|
+
if (metrics.performanceTrend !== "stable") {
|
|
3006
|
+
const arrow = metrics.performanceTrend === "improving" ? "\u2191" : "\u2193";
|
|
3007
|
+
metricBadges += `<span class="badge badge-perf badge-perf-${metrics.performanceTrend}">${arrow} ${metrics.performanceTrend}</span>`;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
2940
3010
|
const storyDocs = deps.renderDocs(tc.story.docs, "story-docs");
|
|
2941
3011
|
const steps = deps.renderSteps(
|
|
2942
3012
|
{ steps: tc.story.steps, stepResults: tc.stepResults },
|
|
@@ -2971,7 +3041,7 @@ function renderScenario(args, deps) {
|
|
|
2971
3041
|
<span class="status-icon ${statusClass}">${statusIcon}</span>
|
|
2972
3042
|
<span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
|
|
2973
3043
|
</div>
|
|
2974
|
-
<div class="scenario-meta">${tags}${traceBadge}</div>
|
|
3044
|
+
<div class="scenario-meta">${tags}${traceBadge}${metricBadges}</div>
|
|
2975
3045
|
</div>
|
|
2976
3046
|
<span class="scenario-duration">${duration}</span>
|
|
2977
3047
|
</div>
|
|
@@ -3183,7 +3253,12 @@ function renderFeature(args, deps) {
|
|
|
3183
3253
|
const featureName = suitePaths.length > 0 && suitePaths[0].length > 0 ? suitePaths[0][0] : file.split("/").pop()?.replace(/\.[^.]+$/, "") ?? file;
|
|
3184
3254
|
const collapsedClass = deps.startCollapsed ? " collapsed" : "";
|
|
3185
3255
|
const ariaExpanded = !deps.startCollapsed;
|
|
3186
|
-
const scenarios = testCases.map(
|
|
3256
|
+
const scenarios = testCases.map(
|
|
3257
|
+
(tc) => deps.renderScenario(
|
|
3258
|
+
{ tc, metrics: args.metricsMap?.get(tc.id) },
|
|
3259
|
+
deps.scenarioDeps
|
|
3260
|
+
)
|
|
3261
|
+
).join("\n");
|
|
3187
3262
|
return `
|
|
3188
3263
|
<div class="feature${collapsedClass}">
|
|
3189
3264
|
<div class="feature-header" role="button" tabindex="0" aria-expanded="${ariaExpanded}">
|
|
@@ -3228,7 +3303,11 @@ function buildBody(args, deps) {
|
|
|
3228
3303
|
durationMs: run.durationMs,
|
|
3229
3304
|
packageVersion: run.packageVersion,
|
|
3230
3305
|
gitSha: run.gitSha,
|
|
3231
|
-
ciName: run.ci?.name
|
|
3306
|
+
ciName: run.ci?.name,
|
|
3307
|
+
ciBranch: run.ci?.branch,
|
|
3308
|
+
ciUrl: run.ci?.url,
|
|
3309
|
+
ciCommitSha: run.ci?.commitSha,
|
|
3310
|
+
ciBuildNumber: run.ci?.buildNumber
|
|
3232
3311
|
},
|
|
3233
3312
|
deps.metaDeps
|
|
3234
3313
|
)
|
|
@@ -3257,7 +3336,10 @@ function buildBody(args, deps) {
|
|
|
3257
3336
|
const byFile = groupBy(run.testCases, (tc) => tc.sourceFile);
|
|
3258
3337
|
for (const [file, testCases] of byFile) {
|
|
3259
3338
|
parts.push(
|
|
3260
|
-
deps.renderFeature(
|
|
3339
|
+
deps.renderFeature(
|
|
3340
|
+
{ file, testCases, metricsMap: args.metricsMap },
|
|
3341
|
+
deps.featureDeps
|
|
3342
|
+
)
|
|
3261
3343
|
);
|
|
3262
3344
|
}
|
|
3263
3345
|
return parts.join("\n");
|
|
@@ -5729,45 +5811,102 @@ function clearVersionCache() {
|
|
|
5729
5811
|
|
|
5730
5812
|
// src/utils/ci-detect.ts
|
|
5731
5813
|
function detectCI4(env = process.env) {
|
|
5814
|
+
if (env.TF_BUILD === "True") {
|
|
5815
|
+
const branch = env.BUILD_SOURCEBRANCH?.replace(/^refs\/heads\//, "");
|
|
5816
|
+
const serverUri = env.SYSTEM_TEAMFOUNDATIONSERVERURI;
|
|
5817
|
+
const teamProject = env.SYSTEM_TEAMPROJECT;
|
|
5818
|
+
const buildId = env.BUILD_BUILDID;
|
|
5819
|
+
const url = serverUri && teamProject && buildId ? `${serverUri}${teamProject}/_build/results?buildId=${buildId}` : void 0;
|
|
5820
|
+
return {
|
|
5821
|
+
name: "azure",
|
|
5822
|
+
provider: "azure",
|
|
5823
|
+
buildNumber: buildId,
|
|
5824
|
+
url,
|
|
5825
|
+
branch,
|
|
5826
|
+
commitSha: env.BUILD_SOURCEVERSION,
|
|
5827
|
+
prNumber: env.SYSTEM_PULLREQUEST_PULLREQUESTID
|
|
5828
|
+
};
|
|
5829
|
+
}
|
|
5830
|
+
if (env.BUILDKITE === "true") {
|
|
5831
|
+
const prRaw = env.BUILDKITE_PULL_REQUEST;
|
|
5832
|
+
const prNumber = prRaw && prRaw !== "false" ? prRaw : void 0;
|
|
5833
|
+
return {
|
|
5834
|
+
name: "buildkite",
|
|
5835
|
+
provider: "buildkite",
|
|
5836
|
+
buildNumber: env.BUILDKITE_BUILD_NUMBER,
|
|
5837
|
+
url: env.BUILDKITE_BUILD_URL,
|
|
5838
|
+
branch: env.BUILDKITE_BRANCH,
|
|
5839
|
+
commitSha: env.BUILDKITE_COMMIT,
|
|
5840
|
+
prNumber
|
|
5841
|
+
};
|
|
5842
|
+
}
|
|
5732
5843
|
if (env.GITHUB_ACTIONS === "true") {
|
|
5733
5844
|
const url = env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` : void 0;
|
|
5845
|
+
const branch = env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME;
|
|
5846
|
+
const prMatch = env.GITHUB_REF?.match(/^refs\/pull\/(\d+)\/(merge|head)$/);
|
|
5847
|
+
const prNumber = prMatch ? prMatch[1] : void 0;
|
|
5734
5848
|
return {
|
|
5735
5849
|
name: "github",
|
|
5850
|
+
provider: "github",
|
|
5736
5851
|
buildNumber: env.GITHUB_RUN_NUMBER,
|
|
5737
|
-
url
|
|
5852
|
+
url,
|
|
5853
|
+
branch,
|
|
5854
|
+
commitSha: env.GITHUB_SHA,
|
|
5855
|
+
prNumber
|
|
5856
|
+
};
|
|
5857
|
+
}
|
|
5858
|
+
if (env.GITLAB_CI === "true") {
|
|
5859
|
+
return {
|
|
5860
|
+
name: "gitlab",
|
|
5861
|
+
provider: "gitlab",
|
|
5862
|
+
buildNumber: env.CI_PIPELINE_IID,
|
|
5863
|
+
url: env.CI_PIPELINE_URL,
|
|
5864
|
+
branch: env.CI_COMMIT_REF_NAME,
|
|
5865
|
+
commitSha: env.CI_COMMIT_SHA,
|
|
5866
|
+
prNumber: env.CI_MERGE_REQUEST_IID
|
|
5738
5867
|
};
|
|
5739
5868
|
}
|
|
5740
5869
|
if (env.CIRCLECI === "true") {
|
|
5870
|
+
const prUrl = env.CIRCLE_PULL_REQUEST;
|
|
5871
|
+
const prMatch = prUrl?.match(/\/(\d+)$/);
|
|
5872
|
+
const prNumber = prMatch ? prMatch[1] : void 0;
|
|
5741
5873
|
return {
|
|
5742
5874
|
name: "circleci",
|
|
5875
|
+
provider: "circleci",
|
|
5743
5876
|
buildNumber: env.CIRCLE_BUILD_NUM,
|
|
5744
|
-
url: env.CIRCLE_BUILD_URL
|
|
5877
|
+
url: env.CIRCLE_BUILD_URL,
|
|
5878
|
+
branch: env.CIRCLE_BRANCH,
|
|
5879
|
+
commitSha: env.CIRCLE_SHA1,
|
|
5880
|
+
prNumber
|
|
5745
5881
|
};
|
|
5746
5882
|
}
|
|
5747
5883
|
if (env.JENKINS_URL !== void 0) {
|
|
5748
5884
|
return {
|
|
5749
5885
|
name: "jenkins",
|
|
5886
|
+
provider: "jenkins",
|
|
5750
5887
|
buildNumber: env.BUILD_NUMBER,
|
|
5751
|
-
url: env.BUILD_URL
|
|
5888
|
+
url: env.BUILD_URL,
|
|
5889
|
+
branch: env.GIT_BRANCH,
|
|
5890
|
+
commitSha: env.GIT_COMMIT
|
|
5752
5891
|
};
|
|
5753
5892
|
}
|
|
5754
5893
|
if (env.TRAVIS === "true") {
|
|
5894
|
+
const prRaw = env.TRAVIS_PULL_REQUEST;
|
|
5895
|
+
const prNumber = prRaw && prRaw !== "false" ? prRaw : void 0;
|
|
5755
5896
|
return {
|
|
5756
5897
|
name: "travis",
|
|
5898
|
+
provider: "travis",
|
|
5757
5899
|
buildNumber: env.TRAVIS_BUILD_NUMBER,
|
|
5758
|
-
url: env.TRAVIS_BUILD_WEB_URL
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
return {
|
|
5763
|
-
name: "gitlab",
|
|
5764
|
-
buildNumber: env.CI_PIPELINE_IID,
|
|
5765
|
-
url: env.CI_PIPELINE_URL
|
|
5900
|
+
url: env.TRAVIS_BUILD_WEB_URL,
|
|
5901
|
+
branch: env.TRAVIS_BRANCH,
|
|
5902
|
+
commitSha: env.TRAVIS_COMMIT,
|
|
5903
|
+
prNumber
|
|
5766
5904
|
};
|
|
5767
5905
|
}
|
|
5768
5906
|
if (env.CI === "true") {
|
|
5769
5907
|
return {
|
|
5770
|
-
name: "ci"
|
|
5908
|
+
name: "ci",
|
|
5909
|
+
provider: "unknown"
|
|
5771
5910
|
};
|
|
5772
5911
|
}
|
|
5773
5912
|
return void 0;
|
|
@@ -5799,6 +5938,731 @@ function resolveTraceUrl(template, traceId) {
|
|
|
5799
5938
|
return template.replace(/\{traceId\}/g, traceId);
|
|
5800
5939
|
}
|
|
5801
5940
|
|
|
5941
|
+
// src/notifiers/ansi-strip.ts
|
|
5942
|
+
function stripAnsi(text) {
|
|
5943
|
+
return text.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
|
|
5944
|
+
}
|
|
5945
|
+
|
|
5946
|
+
// src/notifiers/slack.ts
|
|
5947
|
+
function truncate(text, maxLen) {
|
|
5948
|
+
if (text.length <= maxLen) return text;
|
|
5949
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
5950
|
+
}
|
|
5951
|
+
function formatDuration3(ms) {
|
|
5952
|
+
const seconds = ms / 1e3;
|
|
5953
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
5954
|
+
const minutes = Math.floor(seconds / 60);
|
|
5955
|
+
const remainingSeconds = seconds % 60;
|
|
5956
|
+
return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
|
|
5957
|
+
}
|
|
5958
|
+
function buildSlackPayload(summary, maxFailedTests) {
|
|
5959
|
+
const allPassed = summary.failed === 0;
|
|
5960
|
+
const emoji = allPassed ? ":white_check_mark:" : ":x:";
|
|
5961
|
+
const statusText = allPassed ? "Passed" : "Failed";
|
|
5962
|
+
const blocks = [];
|
|
5963
|
+
blocks.push({
|
|
5964
|
+
type: "header",
|
|
5965
|
+
text: {
|
|
5966
|
+
type: "plain_text",
|
|
5967
|
+
text: `${emoji} Test Results: ${summary.passed} passed, ${summary.failed} failed`,
|
|
5968
|
+
emoji: true
|
|
5969
|
+
}
|
|
5970
|
+
});
|
|
5971
|
+
blocks.push({
|
|
5972
|
+
type: "section",
|
|
5973
|
+
fields: [
|
|
5974
|
+
{ type: "mrkdwn", text: `*Total:* ${summary.total}` },
|
|
5975
|
+
{ type: "mrkdwn", text: `*Passed:* ${summary.passed}` },
|
|
5976
|
+
{ type: "mrkdwn", text: `*Failed:* ${summary.failed}` },
|
|
5977
|
+
{ type: "mrkdwn", text: `*Skipped:* ${summary.skipped}` },
|
|
5978
|
+
{ type: "mrkdwn", text: `*Duration:* ${formatDuration3(summary.durationMs)}` },
|
|
5979
|
+
{ type: "mrkdwn", text: `*Status:* ${statusText}` }
|
|
5980
|
+
]
|
|
5981
|
+
});
|
|
5982
|
+
if (summary.failedTests.length > 0) {
|
|
5983
|
+
const displayedTests = summary.failedTests.slice(0, maxFailedTests);
|
|
5984
|
+
const lines = displayedTests.map((t) => {
|
|
5985
|
+
const name = t.name;
|
|
5986
|
+
if (t.error) {
|
|
5987
|
+
const cleanError = truncate(stripAnsi(t.error), 500);
|
|
5988
|
+
return `*${name}*
|
|
5989
|
+
\`\`\`${cleanError}\`\`\``;
|
|
5990
|
+
}
|
|
5991
|
+
return `*${name}*`;
|
|
5992
|
+
});
|
|
5993
|
+
let text = lines.join("\n\n");
|
|
5994
|
+
if (summary.failedTests.length > maxFailedTests) {
|
|
5995
|
+
text += `
|
|
5996
|
+
|
|
5997
|
+
_...and ${summary.failedTests.length - maxFailedTests} more_`;
|
|
5998
|
+
}
|
|
5999
|
+
blocks.push({
|
|
6000
|
+
type: "section",
|
|
6001
|
+
text: {
|
|
6002
|
+
type: "mrkdwn",
|
|
6003
|
+
text
|
|
6004
|
+
}
|
|
6005
|
+
});
|
|
6006
|
+
}
|
|
6007
|
+
if (summary.ci) {
|
|
6008
|
+
const elements = [];
|
|
6009
|
+
if (summary.ci.displayName) {
|
|
6010
|
+
elements.push({ type: "mrkdwn", text: `*CI:* ${summary.ci.displayName}` });
|
|
6011
|
+
}
|
|
6012
|
+
if (summary.ci.branch) {
|
|
6013
|
+
elements.push({ type: "mrkdwn", text: `*Branch:* ${summary.ci.branch}` });
|
|
6014
|
+
}
|
|
6015
|
+
if (summary.ci.commitSha) {
|
|
6016
|
+
elements.push({ type: "mrkdwn", text: `*Commit:* ${summary.ci.commitSha.slice(0, 7)}` });
|
|
6017
|
+
}
|
|
6018
|
+
if (summary.ci.buildNumber) {
|
|
6019
|
+
elements.push({ type: "mrkdwn", text: `*Build:* #${summary.ci.buildNumber}` });
|
|
6020
|
+
}
|
|
6021
|
+
if (elements.length > 0) {
|
|
6022
|
+
blocks.push({
|
|
6023
|
+
type: "context",
|
|
6024
|
+
elements
|
|
6025
|
+
});
|
|
6026
|
+
}
|
|
6027
|
+
}
|
|
6028
|
+
if (summary.reportUrl) {
|
|
6029
|
+
blocks.push({
|
|
6030
|
+
type: "actions",
|
|
6031
|
+
elements: [
|
|
6032
|
+
{
|
|
6033
|
+
type: "button",
|
|
6034
|
+
text: {
|
|
6035
|
+
type: "plain_text",
|
|
6036
|
+
text: "View Report",
|
|
6037
|
+
emoji: true
|
|
6038
|
+
},
|
|
6039
|
+
url: summary.reportUrl,
|
|
6040
|
+
action_id: "view_report"
|
|
6041
|
+
}
|
|
6042
|
+
]
|
|
6043
|
+
});
|
|
6044
|
+
}
|
|
6045
|
+
return { blocks };
|
|
6046
|
+
}
|
|
6047
|
+
async function sendSlackNotification(args, deps) {
|
|
6048
|
+
const { summary, webhookUrl, maxFailedTests = 5 } = args;
|
|
6049
|
+
const { fetch, logger } = deps;
|
|
6050
|
+
const payload = buildSlackPayload(summary, maxFailedTests);
|
|
6051
|
+
try {
|
|
6052
|
+
const response = await fetch(webhookUrl, {
|
|
6053
|
+
method: "POST",
|
|
6054
|
+
headers: { "Content-Type": "application/json" },
|
|
6055
|
+
body: JSON.stringify(payload)
|
|
6056
|
+
});
|
|
6057
|
+
if (!response.ok) {
|
|
6058
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
6059
|
+
let bodyText = "";
|
|
6060
|
+
try {
|
|
6061
|
+
bodyText = await response.text();
|
|
6062
|
+
} catch {
|
|
6063
|
+
}
|
|
6064
|
+
const truncatedBody = truncate(bodyText, 200);
|
|
6065
|
+
const idPart = requestId ? ` x-request-id=${requestId}` : "";
|
|
6066
|
+
const errorMsg = `Slack notifier failed: HTTP ${response.status}${idPart} ${truncatedBody}`;
|
|
6067
|
+
logger.warn(errorMsg);
|
|
6068
|
+
return { ok: false, error: errorMsg };
|
|
6069
|
+
}
|
|
6070
|
+
return { ok: true };
|
|
6071
|
+
} catch (err) {
|
|
6072
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6073
|
+
const errorMsg = `Slack notifier failed: ${msg}`;
|
|
6074
|
+
logger.warn(errorMsg);
|
|
6075
|
+
return { ok: false, error: errorMsg };
|
|
6076
|
+
}
|
|
6077
|
+
}
|
|
6078
|
+
|
|
6079
|
+
// src/notifiers/teams.ts
|
|
6080
|
+
function truncate2(text, maxLen) {
|
|
6081
|
+
if (text.length <= maxLen) return text;
|
|
6082
|
+
return text.slice(0, maxLen - 3) + "...";
|
|
6083
|
+
}
|
|
6084
|
+
function formatDuration4(ms) {
|
|
6085
|
+
const seconds = ms / 1e3;
|
|
6086
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
6087
|
+
const minutes = Math.floor(seconds / 60);
|
|
6088
|
+
const remainingSeconds = seconds % 60;
|
|
6089
|
+
return `${minutes}m ${remainingSeconds.toFixed(0)}s`;
|
|
6090
|
+
}
|
|
6091
|
+
function buildTeamsPayload(summary, maxFailedTests) {
|
|
6092
|
+
const allPassed = summary.failed === 0;
|
|
6093
|
+
const statusEmoji = allPassed ? "\u2705" : "\u274C";
|
|
6094
|
+
const statusColor = allPassed ? "good" : "attention";
|
|
6095
|
+
const bodyItems = [];
|
|
6096
|
+
bodyItems.push({
|
|
6097
|
+
type: "TextBlock",
|
|
6098
|
+
size: "Large",
|
|
6099
|
+
weight: "Bolder",
|
|
6100
|
+
text: `${statusEmoji} Test Results`,
|
|
6101
|
+
color: statusColor
|
|
6102
|
+
});
|
|
6103
|
+
bodyItems.push({
|
|
6104
|
+
type: "FactSet",
|
|
6105
|
+
facts: [
|
|
6106
|
+
{ title: "Total", value: String(summary.total) },
|
|
6107
|
+
{ title: "Passed", value: String(summary.passed) },
|
|
6108
|
+
{ title: "Failed", value: String(summary.failed) },
|
|
6109
|
+
{ title: "Skipped", value: String(summary.skipped) },
|
|
6110
|
+
{ title: "Duration", value: formatDuration4(summary.durationMs) }
|
|
6111
|
+
]
|
|
6112
|
+
});
|
|
6113
|
+
if (summary.failedTests.length > 0) {
|
|
6114
|
+
const displayedTests = summary.failedTests.slice(0, maxFailedTests);
|
|
6115
|
+
const failedItems = [
|
|
6116
|
+
{
|
|
6117
|
+
type: "TextBlock",
|
|
6118
|
+
text: "Failed Tests",
|
|
6119
|
+
weight: "Bolder",
|
|
6120
|
+
spacing: "Medium"
|
|
6121
|
+
}
|
|
6122
|
+
];
|
|
6123
|
+
for (const t of displayedTests) {
|
|
6124
|
+
failedItems.push({
|
|
6125
|
+
type: "TextBlock",
|
|
6126
|
+
text: `**${t.name}**`,
|
|
6127
|
+
wrap: true
|
|
6128
|
+
});
|
|
6129
|
+
if (t.error) {
|
|
6130
|
+
const cleanError = truncate2(stripAnsi(t.error), 500);
|
|
6131
|
+
failedItems.push({
|
|
6132
|
+
type: "TextBlock",
|
|
6133
|
+
text: cleanError,
|
|
6134
|
+
wrap: true,
|
|
6135
|
+
fontType: "Monospace",
|
|
6136
|
+
size: "Small",
|
|
6137
|
+
color: "Attention"
|
|
6138
|
+
});
|
|
6139
|
+
}
|
|
6140
|
+
}
|
|
6141
|
+
if (summary.failedTests.length > maxFailedTests) {
|
|
6142
|
+
failedItems.push({
|
|
6143
|
+
type: "TextBlock",
|
|
6144
|
+
text: `...and ${summary.failedTests.length - maxFailedTests} more`,
|
|
6145
|
+
isSubtle: true,
|
|
6146
|
+
spacing: "Small"
|
|
6147
|
+
});
|
|
6148
|
+
}
|
|
6149
|
+
bodyItems.push({
|
|
6150
|
+
type: "Container",
|
|
6151
|
+
items: failedItems
|
|
6152
|
+
});
|
|
6153
|
+
}
|
|
6154
|
+
if (summary.ci) {
|
|
6155
|
+
const ciFacts = [];
|
|
6156
|
+
if (summary.ci.displayName) {
|
|
6157
|
+
ciFacts.push({ title: "CI", value: summary.ci.displayName });
|
|
6158
|
+
}
|
|
6159
|
+
if (summary.ci.branch) {
|
|
6160
|
+
ciFacts.push({ title: "Branch", value: summary.ci.branch });
|
|
6161
|
+
}
|
|
6162
|
+
if (summary.ci.commitSha) {
|
|
6163
|
+
ciFacts.push({ title: "Commit", value: summary.ci.commitSha.slice(0, 7) });
|
|
6164
|
+
}
|
|
6165
|
+
if (summary.ci.buildNumber) {
|
|
6166
|
+
ciFacts.push({ title: "Build", value: `#${summary.ci.buildNumber}` });
|
|
6167
|
+
}
|
|
6168
|
+
if (ciFacts.length > 0) {
|
|
6169
|
+
bodyItems.push({
|
|
6170
|
+
type: "FactSet",
|
|
6171
|
+
facts: ciFacts,
|
|
6172
|
+
separator: true
|
|
6173
|
+
});
|
|
6174
|
+
}
|
|
6175
|
+
}
|
|
6176
|
+
const card = {
|
|
6177
|
+
type: "AdaptiveCard",
|
|
6178
|
+
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
6179
|
+
version: "1.4",
|
|
6180
|
+
body: bodyItems
|
|
6181
|
+
};
|
|
6182
|
+
if (summary.reportUrl) {
|
|
6183
|
+
card.actions = [
|
|
6184
|
+
{
|
|
6185
|
+
type: "Action.OpenUrl",
|
|
6186
|
+
title: "View Report",
|
|
6187
|
+
url: summary.reportUrl
|
|
6188
|
+
}
|
|
6189
|
+
];
|
|
6190
|
+
}
|
|
6191
|
+
return {
|
|
6192
|
+
type: "message",
|
|
6193
|
+
attachments: [
|
|
6194
|
+
{
|
|
6195
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
6196
|
+
content: card
|
|
6197
|
+
}
|
|
6198
|
+
]
|
|
6199
|
+
};
|
|
6200
|
+
}
|
|
6201
|
+
async function sendTeamsNotification(args, deps) {
|
|
6202
|
+
const { summary, webhookUrl, maxFailedTests = 5 } = args;
|
|
6203
|
+
const { fetch, logger } = deps;
|
|
6204
|
+
const payload = buildTeamsPayload(summary, maxFailedTests);
|
|
6205
|
+
try {
|
|
6206
|
+
const response = await fetch(webhookUrl, {
|
|
6207
|
+
method: "POST",
|
|
6208
|
+
headers: { "Content-Type": "application/json" },
|
|
6209
|
+
body: JSON.stringify(payload)
|
|
6210
|
+
});
|
|
6211
|
+
if (!response.ok) {
|
|
6212
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
6213
|
+
let bodyText = "";
|
|
6214
|
+
try {
|
|
6215
|
+
bodyText = await response.text();
|
|
6216
|
+
} catch {
|
|
6217
|
+
}
|
|
6218
|
+
const truncatedBody = truncate2(bodyText, 200);
|
|
6219
|
+
const idPart = requestId ? ` x-request-id=${requestId}` : "";
|
|
6220
|
+
const errorMsg = `Teams notifier failed: HTTP ${response.status}${idPart} ${truncatedBody}`;
|
|
6221
|
+
logger.warn(errorMsg);
|
|
6222
|
+
return { ok: false, error: errorMsg };
|
|
6223
|
+
}
|
|
6224
|
+
return { ok: true };
|
|
6225
|
+
} catch (err) {
|
|
6226
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6227
|
+
const errorMsg = `Teams notifier failed: ${msg}`;
|
|
6228
|
+
logger.warn(errorMsg);
|
|
6229
|
+
return { ok: false, error: errorMsg };
|
|
6230
|
+
}
|
|
6231
|
+
}
|
|
6232
|
+
|
|
6233
|
+
// src/notifiers/hmac.ts
|
|
6234
|
+
var import_node_crypto3 = require("crypto");
|
|
6235
|
+
function signBody(args) {
|
|
6236
|
+
let input;
|
|
6237
|
+
let timestamp;
|
|
6238
|
+
if (args.includeTimestamp) {
|
|
6239
|
+
timestamp = args.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
6240
|
+
input = `${timestamp}.${args.body}`;
|
|
6241
|
+
} else {
|
|
6242
|
+
input = args.body;
|
|
6243
|
+
}
|
|
6244
|
+
const hex = (0, import_node_crypto3.createHmac)("sha256", args.secret).update(input, "utf8").digest("hex");
|
|
6245
|
+
return {
|
|
6246
|
+
signature: `sha256=${hex}`,
|
|
6247
|
+
timestamp
|
|
6248
|
+
};
|
|
6249
|
+
}
|
|
6250
|
+
|
|
6251
|
+
// src/notifiers/webhook.ts
|
|
6252
|
+
async function sendWebhookNotification(args, deps) {
|
|
6253
|
+
const { summary, options } = args;
|
|
6254
|
+
const { fetch, logger } = deps;
|
|
6255
|
+
const payload = {
|
|
6256
|
+
schemaVersion: 1,
|
|
6257
|
+
event: "test_run_finished",
|
|
6258
|
+
summary
|
|
6259
|
+
};
|
|
6260
|
+
const body = JSON.stringify(payload);
|
|
6261
|
+
const headers = { "Content-Type": "application/json" };
|
|
6262
|
+
if (options.headers) {
|
|
6263
|
+
for (const [key, value] of Object.entries(options.headers)) {
|
|
6264
|
+
headers[key] = value;
|
|
6265
|
+
}
|
|
6266
|
+
}
|
|
6267
|
+
if (options.signer) {
|
|
6268
|
+
const { secret, header, includeTimestamp, timestampHeader } = options.signer;
|
|
6269
|
+
const result = signBody({ body, secret, includeTimestamp });
|
|
6270
|
+
headers[header] = result.signature;
|
|
6271
|
+
if (result.timestamp) {
|
|
6272
|
+
headers[timestampHeader ?? "X-Timestamp"] = result.timestamp;
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
try {
|
|
6276
|
+
const response = await fetch(options.url, {
|
|
6277
|
+
method: options.method ?? "POST",
|
|
6278
|
+
headers,
|
|
6279
|
+
body
|
|
6280
|
+
});
|
|
6281
|
+
if (!response.ok) {
|
|
6282
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
6283
|
+
let snippet = "";
|
|
6284
|
+
try {
|
|
6285
|
+
snippet = (await response.text()).slice(0, 200);
|
|
6286
|
+
} catch {
|
|
6287
|
+
}
|
|
6288
|
+
const idPart = requestId ? ` x-request-id=${requestId}` : "";
|
|
6289
|
+
const errorMsg = `webhook: HTTP ${response.status}${idPart} ${snippet}`;
|
|
6290
|
+
logger.warn(errorMsg);
|
|
6291
|
+
return { ok: false, error: errorMsg };
|
|
6292
|
+
}
|
|
6293
|
+
return { ok: true };
|
|
6294
|
+
} catch (err) {
|
|
6295
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6296
|
+
const errorMsg = `webhook: ${msg}`;
|
|
6297
|
+
logger.warn(errorMsg);
|
|
6298
|
+
return { ok: false, error: errorMsg };
|
|
6299
|
+
}
|
|
6300
|
+
}
|
|
6301
|
+
|
|
6302
|
+
// src/notifiers/index.ts
|
|
6303
|
+
function buildSummary(run, reportUrl, toCIInfo2) {
|
|
6304
|
+
let passed = 0;
|
|
6305
|
+
let failed = 0;
|
|
6306
|
+
let skipped = 0;
|
|
6307
|
+
const failedTests = [];
|
|
6308
|
+
for (const tc of run.testCases) {
|
|
6309
|
+
switch (tc.status) {
|
|
6310
|
+
case "passed":
|
|
6311
|
+
passed++;
|
|
6312
|
+
break;
|
|
6313
|
+
case "failed":
|
|
6314
|
+
failed++;
|
|
6315
|
+
failedTests.push({
|
|
6316
|
+
testId: tc.id,
|
|
6317
|
+
name: tc.story.scenario,
|
|
6318
|
+
error: tc.errorMessage
|
|
6319
|
+
});
|
|
6320
|
+
break;
|
|
6321
|
+
case "skipped":
|
|
6322
|
+
case "pending":
|
|
6323
|
+
skipped++;
|
|
6324
|
+
break;
|
|
6325
|
+
}
|
|
6326
|
+
}
|
|
6327
|
+
let ci;
|
|
6328
|
+
if (run.ci) {
|
|
6329
|
+
ci = toCIInfo2(run.ci);
|
|
6330
|
+
}
|
|
6331
|
+
return {
|
|
6332
|
+
total: run.testCases.length,
|
|
6333
|
+
passed,
|
|
6334
|
+
failed,
|
|
6335
|
+
skipped,
|
|
6336
|
+
durationMs: run.durationMs,
|
|
6337
|
+
failedTests,
|
|
6338
|
+
ci,
|
|
6339
|
+
reportUrl
|
|
6340
|
+
};
|
|
6341
|
+
}
|
|
6342
|
+
function shouldNotify(condition, failedCount) {
|
|
6343
|
+
if (condition === "never") return false;
|
|
6344
|
+
if (condition === "on-failure" && failedCount === 0) return false;
|
|
6345
|
+
return true;
|
|
6346
|
+
}
|
|
6347
|
+
async function sendNotifications(args, deps) {
|
|
6348
|
+
const { run, notification } = args;
|
|
6349
|
+
const { logger, toCIInfo: toCIInfo2 } = deps;
|
|
6350
|
+
const env = deps.env ?? process.env;
|
|
6351
|
+
if (!deps.fetch) {
|
|
6352
|
+
logger.warn("notifications: skipped (fetch unavailable)");
|
|
6353
|
+
return;
|
|
6354
|
+
}
|
|
6355
|
+
const fetch = deps.fetch;
|
|
6356
|
+
const slackWebhookUrl = notification?.slackWebhookUrl ?? env.SLACK_WEBHOOK_URL;
|
|
6357
|
+
const teamsWebhookUrl = notification?.teamsWebhookUrl ?? env.TEAMS_WEBHOOK_URL;
|
|
6358
|
+
const globalCondition = notification?.condition ?? "on-failure";
|
|
6359
|
+
const reportUrl = notification?.reportUrl;
|
|
6360
|
+
const maxFailedTests = notification?.maxFailedTests ?? 5;
|
|
6361
|
+
const webhooks = notification?.webhooks ?? [];
|
|
6362
|
+
if (!slackWebhookUrl && !teamsWebhookUrl && webhooks.length === 0) {
|
|
6363
|
+
return;
|
|
6364
|
+
}
|
|
6365
|
+
const summary = buildSummary(run, reportUrl, toCIInfo2);
|
|
6366
|
+
const promises = [];
|
|
6367
|
+
if (slackWebhookUrl && shouldNotify(globalCondition, summary.failed)) {
|
|
6368
|
+
promises.push(
|
|
6369
|
+
sendSlackNotification(
|
|
6370
|
+
{ summary, webhookUrl: slackWebhookUrl, maxFailedTests },
|
|
6371
|
+
{ fetch, logger }
|
|
6372
|
+
).then(() => void 0)
|
|
6373
|
+
);
|
|
6374
|
+
}
|
|
6375
|
+
if (teamsWebhookUrl && shouldNotify(globalCondition, summary.failed)) {
|
|
6376
|
+
promises.push(
|
|
6377
|
+
sendTeamsNotification(
|
|
6378
|
+
{ summary, webhookUrl: teamsWebhookUrl, maxFailedTests },
|
|
6379
|
+
{ fetch, logger }
|
|
6380
|
+
).then(() => void 0)
|
|
6381
|
+
);
|
|
6382
|
+
}
|
|
6383
|
+
for (const webhook of webhooks) {
|
|
6384
|
+
const effectiveCondition = webhook.condition ?? globalCondition;
|
|
6385
|
+
if (!shouldNotify(effectiveCondition, summary.failed)) continue;
|
|
6386
|
+
promises.push(
|
|
6387
|
+
sendWebhookNotification(
|
|
6388
|
+
{ summary, options: webhook, maxFailedTests },
|
|
6389
|
+
{ fetch, logger }
|
|
6390
|
+
).then(() => void 0)
|
|
6391
|
+
);
|
|
6392
|
+
}
|
|
6393
|
+
await Promise.allSettled(promises);
|
|
6394
|
+
}
|
|
6395
|
+
|
|
6396
|
+
// src/types/ci.ts
|
|
6397
|
+
var DISPLAY_NAMES = {
|
|
6398
|
+
github: "GitHub Actions",
|
|
6399
|
+
gitlab: "GitLab CI",
|
|
6400
|
+
circleci: "CircleCI",
|
|
6401
|
+
jenkins: "Jenkins",
|
|
6402
|
+
azure: "Azure DevOps",
|
|
6403
|
+
buildkite: "Buildkite",
|
|
6404
|
+
travis: "Travis CI",
|
|
6405
|
+
unknown: "CI"
|
|
6406
|
+
};
|
|
6407
|
+
var NAME_TO_PROVIDER = {
|
|
6408
|
+
github: "github",
|
|
6409
|
+
gitlab: "gitlab",
|
|
6410
|
+
circleci: "circleci",
|
|
6411
|
+
jenkins: "jenkins",
|
|
6412
|
+
azure: "azure",
|
|
6413
|
+
buildkite: "buildkite",
|
|
6414
|
+
travis: "travis",
|
|
6415
|
+
ci: "unknown"
|
|
6416
|
+
};
|
|
6417
|
+
function toCIInfo(raw) {
|
|
6418
|
+
if (!raw) return void 0;
|
|
6419
|
+
const provider = raw.provider ?? NAME_TO_PROVIDER[raw.name] ?? "unknown";
|
|
6420
|
+
return {
|
|
6421
|
+
provider,
|
|
6422
|
+
displayName: DISPLAY_NAMES[provider],
|
|
6423
|
+
url: raw.url,
|
|
6424
|
+
buildNumber: raw.buildNumber,
|
|
6425
|
+
branch: raw.branch,
|
|
6426
|
+
commitSha: raw.commitSha,
|
|
6427
|
+
prNumber: raw.prNumber
|
|
6428
|
+
};
|
|
6429
|
+
}
|
|
6430
|
+
function toRawCIInfo(ci) {
|
|
6431
|
+
if (!ci) return void 0;
|
|
6432
|
+
return {
|
|
6433
|
+
name: ci.provider === "unknown" ? "ci" : ci.provider,
|
|
6434
|
+
provider: ci.provider,
|
|
6435
|
+
url: ci.url,
|
|
6436
|
+
buildNumber: ci.buildNumber,
|
|
6437
|
+
branch: ci.branch,
|
|
6438
|
+
commitSha: ci.commitSha,
|
|
6439
|
+
prNumber: ci.prNumber
|
|
6440
|
+
};
|
|
6441
|
+
}
|
|
6442
|
+
|
|
6443
|
+
// src/history/history-store.ts
|
|
6444
|
+
function emptyStore() {
|
|
6445
|
+
return { version: 1, maxRuns: 10, tests: {}, lastUpdated: 0 };
|
|
6446
|
+
}
|
|
6447
|
+
function loadHistory(args, deps) {
|
|
6448
|
+
const content = deps.readFile(args.filePath);
|
|
6449
|
+
if (content === void 0) {
|
|
6450
|
+
return emptyStore();
|
|
6451
|
+
}
|
|
6452
|
+
let parsed;
|
|
6453
|
+
try {
|
|
6454
|
+
parsed = JSON.parse(content);
|
|
6455
|
+
} catch {
|
|
6456
|
+
deps.logger.warn(`Failed to parse history file: ${args.filePath}`);
|
|
6457
|
+
return emptyStore();
|
|
6458
|
+
}
|
|
6459
|
+
if (typeof parsed !== "object" || parsed === null || parsed.version !== 1) {
|
|
6460
|
+
deps.logger.warn(
|
|
6461
|
+
`Unknown history version in ${args.filePath}, expected version 1`
|
|
6462
|
+
);
|
|
6463
|
+
return emptyStore();
|
|
6464
|
+
}
|
|
6465
|
+
const obj = parsed;
|
|
6466
|
+
if (typeof obj.tests !== "object" || obj.tests === null || Array.isArray(obj.tests)) {
|
|
6467
|
+
deps.logger.warn(
|
|
6468
|
+
`Malformed history store in ${args.filePath}: tests must be a non-null object`
|
|
6469
|
+
);
|
|
6470
|
+
return emptyStore();
|
|
6471
|
+
}
|
|
6472
|
+
return parsed;
|
|
6473
|
+
}
|
|
6474
|
+
function saveHistory(args, deps) {
|
|
6475
|
+
deps.writeFile(args.filePath, JSON.stringify(args.store, null, 2));
|
|
6476
|
+
}
|
|
6477
|
+
function updateHistory(args) {
|
|
6478
|
+
const { store, run, maxRuns } = args;
|
|
6479
|
+
const newTests = { ...store.tests };
|
|
6480
|
+
for (const tc of run.testCases) {
|
|
6481
|
+
const entry = {
|
|
6482
|
+
runId: run.runId,
|
|
6483
|
+
timestamp: run.startedAtMs,
|
|
6484
|
+
status: tc.status,
|
|
6485
|
+
durationMs: tc.durationMs,
|
|
6486
|
+
ci: run.ci ? {
|
|
6487
|
+
provider: void 0,
|
|
6488
|
+
branch: run.ci.branch,
|
|
6489
|
+
commitSha: run.ci.commitSha
|
|
6490
|
+
} : void 0
|
|
6491
|
+
};
|
|
6492
|
+
const existing = newTests[tc.id];
|
|
6493
|
+
if (existing) {
|
|
6494
|
+
const updatedEntries = [...existing.entries, entry];
|
|
6495
|
+
const trimmed = updatedEntries.length > maxRuns ? updatedEntries.slice(updatedEntries.length - maxRuns) : updatedEntries;
|
|
6496
|
+
newTests[tc.id] = {
|
|
6497
|
+
...existing,
|
|
6498
|
+
testName: tc.story.scenario,
|
|
6499
|
+
sourceFile: tc.sourceFile,
|
|
6500
|
+
sourceLine: tc.sourceLine,
|
|
6501
|
+
entries: trimmed
|
|
6502
|
+
};
|
|
6503
|
+
} else {
|
|
6504
|
+
newTests[tc.id] = {
|
|
6505
|
+
testId: tc.id,
|
|
6506
|
+
testName: tc.story.scenario,
|
|
6507
|
+
sourceFile: tc.sourceFile,
|
|
6508
|
+
sourceLine: tc.sourceLine,
|
|
6509
|
+
entries: [entry]
|
|
6510
|
+
};
|
|
6511
|
+
}
|
|
6512
|
+
}
|
|
6513
|
+
return {
|
|
6514
|
+
version: 1,
|
|
6515
|
+
maxRuns,
|
|
6516
|
+
tests: newTests,
|
|
6517
|
+
lastUpdated: Date.now()
|
|
6518
|
+
};
|
|
6519
|
+
}
|
|
6520
|
+
|
|
6521
|
+
// src/history/flakiness.ts
|
|
6522
|
+
function calculateFlakiness(args) {
|
|
6523
|
+
const { entries } = args;
|
|
6524
|
+
const countable = entries.filter(
|
|
6525
|
+
(e) => e.status === "passed" || e.status === "failed"
|
|
6526
|
+
);
|
|
6527
|
+
if (countable.length < MIN_FLAKINESS_SAMPLES) {
|
|
6528
|
+
return {
|
|
6529
|
+
flakinessLevel: "stable",
|
|
6530
|
+
flakinessScore: 0,
|
|
6531
|
+
failureRate: 0,
|
|
6532
|
+
longestPassStreak: countable.length,
|
|
6533
|
+
longestFailStreak: 0
|
|
6534
|
+
};
|
|
6535
|
+
}
|
|
6536
|
+
let transitions = 0;
|
|
6537
|
+
for (let i = 1; i < countable.length; i++) {
|
|
6538
|
+
if (countable[i].status !== countable[i - 1].status) {
|
|
6539
|
+
transitions++;
|
|
6540
|
+
}
|
|
6541
|
+
}
|
|
6542
|
+
const transitionScore = transitions / (countable.length - 1);
|
|
6543
|
+
const failures = countable.filter((e) => e.status === "failed").length;
|
|
6544
|
+
const failureRate = failures / countable.length;
|
|
6545
|
+
let longestPassStreak = 0;
|
|
6546
|
+
let longestFailStreak = 0;
|
|
6547
|
+
let currentPassStreak = 0;
|
|
6548
|
+
let currentFailStreak = 0;
|
|
6549
|
+
for (const e of countable) {
|
|
6550
|
+
if (e.status === "passed") {
|
|
6551
|
+
currentPassStreak++;
|
|
6552
|
+
currentFailStreak = 0;
|
|
6553
|
+
if (currentPassStreak > longestPassStreak) {
|
|
6554
|
+
longestPassStreak = currentPassStreak;
|
|
6555
|
+
}
|
|
6556
|
+
} else {
|
|
6557
|
+
currentFailStreak++;
|
|
6558
|
+
currentPassStreak = 0;
|
|
6559
|
+
if (currentFailStreak > longestFailStreak) {
|
|
6560
|
+
longestFailStreak = currentFailStreak;
|
|
6561
|
+
}
|
|
6562
|
+
}
|
|
6563
|
+
}
|
|
6564
|
+
let flakinessLevel;
|
|
6565
|
+
if (transitionScore > 0.5 || transitionScore > 0.3 && failureRate > 0.2) {
|
|
6566
|
+
flakinessLevel = "flaky";
|
|
6567
|
+
} else if (transitionScore > 0.2 || failureRate > 0.3) {
|
|
6568
|
+
flakinessLevel = "unstable";
|
|
6569
|
+
} else {
|
|
6570
|
+
flakinessLevel = "stable";
|
|
6571
|
+
}
|
|
6572
|
+
return {
|
|
6573
|
+
flakinessLevel,
|
|
6574
|
+
flakinessScore: transitionScore,
|
|
6575
|
+
failureRate,
|
|
6576
|
+
longestPassStreak,
|
|
6577
|
+
longestFailStreak
|
|
6578
|
+
};
|
|
6579
|
+
}
|
|
6580
|
+
|
|
6581
|
+
// src/history/performance.ts
|
|
6582
|
+
function detectPerformanceTrend(args) {
|
|
6583
|
+
const { entries } = args;
|
|
6584
|
+
const countable = entries.filter(
|
|
6585
|
+
(e) => e.status !== "skipped" && e.status !== "pending"
|
|
6586
|
+
);
|
|
6587
|
+
if (countable.length === 0) {
|
|
6588
|
+
return { trend: "stable", avgDurationMs: 0 };
|
|
6589
|
+
}
|
|
6590
|
+
const avgAll = countable.reduce((sum, e) => sum + e.durationMs, 0) / countable.length;
|
|
6591
|
+
if (countable.length < MIN_PERF_SAMPLES) {
|
|
6592
|
+
return { trend: "stable", avgDurationMs: avgAll };
|
|
6593
|
+
}
|
|
6594
|
+
const mid = Math.floor(countable.length / 2);
|
|
6595
|
+
const earlier = countable.slice(0, mid);
|
|
6596
|
+
const recent = countable.slice(mid);
|
|
6597
|
+
const earlierAvg = earlier.reduce((sum, e) => sum + e.durationMs, 0) / earlier.length;
|
|
6598
|
+
const recentAvg = recent.reduce((sum, e) => sum + e.durationMs, 0) / recent.length;
|
|
6599
|
+
let trend;
|
|
6600
|
+
if (earlierAvg === 0) {
|
|
6601
|
+
trend = "stable";
|
|
6602
|
+
} else {
|
|
6603
|
+
const change = (recentAvg - earlierAvg) / earlierAvg;
|
|
6604
|
+
if (change > 0.1) {
|
|
6605
|
+
trend = "regressing";
|
|
6606
|
+
} else if (change < -0.1) {
|
|
6607
|
+
trend = "improving";
|
|
6608
|
+
} else {
|
|
6609
|
+
trend = "stable";
|
|
6610
|
+
}
|
|
6611
|
+
}
|
|
6612
|
+
return { trend, avgDurationMs: avgAll };
|
|
6613
|
+
}
|
|
6614
|
+
|
|
6615
|
+
// src/history/stability.ts
|
|
6616
|
+
function calculateStability(args) {
|
|
6617
|
+
const { passRate, flakinessScore, longestPassStreak, sampleSize } = args;
|
|
6618
|
+
const inverseFlakiness = 1 - flakinessScore;
|
|
6619
|
+
const streakNorm = longestPassStreak / Math.min(sampleSize, 10);
|
|
6620
|
+
const score = passRate * 0.6 + inverseFlakiness * 0.3 + streakNorm * 0.1;
|
|
6621
|
+
if (score >= 0.95) return "A";
|
|
6622
|
+
if (score >= 0.85) return "B";
|
|
6623
|
+
if (score >= 0.7) return "C";
|
|
6624
|
+
if (score >= 0.5) return "D";
|
|
6625
|
+
return "F";
|
|
6626
|
+
}
|
|
6627
|
+
|
|
6628
|
+
// src/history/metrics.ts
|
|
6629
|
+
function computeTestMetrics(args) {
|
|
6630
|
+
const { testId, entries } = args;
|
|
6631
|
+
const flakiness = calculateFlakiness({ entries });
|
|
6632
|
+
const perf = detectPerformanceTrend({ entries });
|
|
6633
|
+
const countable = entries.filter(
|
|
6634
|
+
(e) => e.status === "passed" || e.status === "failed"
|
|
6635
|
+
);
|
|
6636
|
+
const passRate = countable.length > 0 ? countable.filter((e) => e.status === "passed").length / countable.length : 1;
|
|
6637
|
+
const stabilityGrade = calculateStability({
|
|
6638
|
+
passRate,
|
|
6639
|
+
flakinessScore: flakiness.flakinessScore,
|
|
6640
|
+
longestPassStreak: flakiness.longestPassStreak,
|
|
6641
|
+
sampleSize: entries.length
|
|
6642
|
+
});
|
|
6643
|
+
let consecutiveFailures = 0;
|
|
6644
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
6645
|
+
if (entries[i].status === "failed") {
|
|
6646
|
+
consecutiveFailures++;
|
|
6647
|
+
} else {
|
|
6648
|
+
break;
|
|
6649
|
+
}
|
|
6650
|
+
}
|
|
6651
|
+
return {
|
|
6652
|
+
testId,
|
|
6653
|
+
flakinessLevel: flakiness.flakinessLevel,
|
|
6654
|
+
flakinessScore: flakiness.flakinessScore,
|
|
6655
|
+
failureRate: flakiness.failureRate,
|
|
6656
|
+
stabilityGrade,
|
|
6657
|
+
performanceTrend: perf.trend,
|
|
6658
|
+
avgDurationMs: perf.avgDurationMs,
|
|
6659
|
+
passRate,
|
|
6660
|
+
longestPassStreak: flakiness.longestPassStreak,
|
|
6661
|
+
consecutiveFailures,
|
|
6662
|
+
sampleSize: entries.length
|
|
6663
|
+
};
|
|
6664
|
+
}
|
|
6665
|
+
|
|
5802
6666
|
// src/index.ts
|
|
5803
6667
|
var FORMAT_EXTENSIONS = {
|
|
5804
6668
|
markdown: ".md",
|
|
@@ -6148,6 +7012,9 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
6148
7012
|
CucumberMessagesFormatter,
|
|
6149
7013
|
HtmlFormatter,
|
|
6150
7014
|
JUnitFormatter,
|
|
7015
|
+
MIN_FLAKINESS_SAMPLES,
|
|
7016
|
+
MIN_METRIC_SAMPLES,
|
|
7017
|
+
MIN_PERF_SAMPLES,
|
|
6151
7018
|
MarkdownFormatter,
|
|
6152
7019
|
ReportGenerator,
|
|
6153
7020
|
STORY_META_KEY,
|
|
@@ -6155,15 +7022,21 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
6155
7022
|
adaptPlaywrightRun,
|
|
6156
7023
|
adaptVitestRun,
|
|
6157
7024
|
assertValidRun,
|
|
7025
|
+
calculateFlakiness,
|
|
7026
|
+
calculateStability,
|
|
6158
7027
|
canonicalizeRun,
|
|
6159
7028
|
clearVersionCache,
|
|
7029
|
+
computeTestMetrics,
|
|
6160
7030
|
createReportGenerator,
|
|
6161
7031
|
deriveStepResults,
|
|
6162
7032
|
detectCI,
|
|
7033
|
+
detectPerformanceTrend,
|
|
6163
7034
|
findGitDir,
|
|
6164
7035
|
formatDuration,
|
|
6165
7036
|
generateRunId,
|
|
6166
7037
|
generateTestCaseId,
|
|
7038
|
+
hasSufficientHistory,
|
|
7039
|
+
loadHistory,
|
|
6167
7040
|
mergeStepResults,
|
|
6168
7041
|
msToNanoseconds,
|
|
6169
7042
|
nanosecondsToMs,
|
|
@@ -6179,8 +7052,18 @@ function normalizePlaywrightResults(testResults, adapterOptions, canonicalizeOpt
|
|
|
6179
7052
|
resolveAttachment,
|
|
6180
7053
|
resolveAttachments,
|
|
6181
7054
|
resolveTraceUrl,
|
|
7055
|
+
saveHistory,
|
|
7056
|
+
sendNotifications,
|
|
7057
|
+
sendSlackNotification,
|
|
7058
|
+
sendTeamsNotification,
|
|
7059
|
+
sendWebhookNotification,
|
|
7060
|
+
signBody,
|
|
6182
7061
|
slugify,
|
|
7062
|
+
stripAnsi,
|
|
7063
|
+
toCIInfo,
|
|
7064
|
+
toRawCIInfo,
|
|
6183
7065
|
tryGetActiveOtelContext,
|
|
7066
|
+
updateHistory,
|
|
6184
7067
|
validateCanonicalRun
|
|
6185
7068
|
});
|
|
6186
7069
|
//# sourceMappingURL=index.cjs.map
|