@elench/testkit 0.1.79 → 0.1.81

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.
Files changed (47) hide show
  1. package/README.md +50 -35
  2. package/lib/cli/args.mjs +2 -14
  3. package/lib/cli/args.test.mjs +1 -17
  4. package/lib/cli/command-helpers.mjs +1 -20
  5. package/lib/cli/entrypoint.mjs +0 -4
  6. package/lib/cli/presentation/colors.mjs +1 -1
  7. package/lib/cli/presentation/failure-presentation.mjs +31 -0
  8. package/lib/cli/presentation/run-reporter.mjs +63 -93
  9. package/lib/cli/presentation/run-reporter.test.mjs +137 -26
  10. package/lib/cli/presentation/summary-box.mjs +45 -0
  11. package/lib/cli/presentation/summary-box.test.mjs +43 -0
  12. package/lib/cli/presentation/terminal-layout.mjs +43 -0
  13. package/lib/cli/presentation/terminal-layout.test.mjs +23 -0
  14. package/lib/cli/viewer.mjs +18 -19
  15. package/lib/config/index.mjs +6 -6
  16. package/lib/config/runtime.mjs +8 -8
  17. package/lib/config-api/index.d.ts +4 -4
  18. package/lib/package.test.mjs +4 -4
  19. package/lib/{known-failures → regressions}/github.mjs +39 -77
  20. package/lib/regressions/github.test.mjs +324 -0
  21. package/lib/regressions/index.d.ts +189 -0
  22. package/lib/{known-failures → regressions}/index.mjs +90 -93
  23. package/lib/{known-failures → regressions}/index.test.mjs +37 -48
  24. package/lib/runner/formatting.mjs +105 -103
  25. package/lib/runner/formatting.test.mjs +94 -131
  26. package/lib/runner/metadata.mjs +1 -1
  27. package/lib/runner/orchestrator.mjs +7 -8
  28. package/lib/runner/regressions.mjs +304 -0
  29. package/lib/runner/{triage.test.mjs → regressions.test.mjs} +50 -36
  30. package/lib/runner/reporting.mjs +2 -2
  31. package/lib/runner/reporting.test.mjs +2 -2
  32. package/lib/runner/run-finalization.mjs +18 -30
  33. package/lib/runner/template-steps.mjs +2 -2
  34. package/lib/runner/worker-loop.mjs +1 -0
  35. package/node_modules/@elench/next-analysis/package.json +1 -1
  36. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  37. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  38. package/node_modules/@elench/ts-analysis/package.json +1 -1
  39. package/package.json +12 -9
  40. package/lib/cli/commands/known-failures/render.mjs +0 -19
  41. package/lib/cli/commands/known-failures/validate.mjs +0 -20
  42. package/lib/cli/known-failures.mjs +0 -164
  43. package/lib/known-failures/github.test.mjs +0 -512
  44. package/lib/known-failures/index.d.ts +0 -192
  45. package/lib/runner/triage.mjs +0 -221
  46. /package/lib/{known-failures → regressions}/github-cache.mjs +0 -0
  47. /package/lib/{known-failures → regressions}/github-transport.mjs +0 -0
@@ -3,10 +3,10 @@ import os from "os";
3
3
  import path from "path";
4
4
  import { afterEach, describe, expect, it } from "vitest";
5
5
  import {
6
- findMatchingKnownFailureEntries,
7
- normalizeKnownFailuresDocument,
8
- renderKnownFailuresMarkdown,
9
- validateKnownFailuresDocument,
6
+ findMatchingRegressionEntries,
7
+ normalizeRegressionCatalogDocument,
8
+ renderRegressionCatalogMarkdown,
9
+ validateRegressionCatalogDocument,
10
10
  } from "./index.mjs";
11
11
 
12
12
  const tempDirs = [];
@@ -17,25 +17,22 @@ afterEach(() => {
17
17
  }
18
18
  });
19
19
 
20
- describe("known failures core", () => {
20
+ describe("regression catalog", () => {
21
21
  it("matches entries by service, type, path, and failure key", () => {
22
- const document = normalizeKnownFailuresDocument({
22
+ const document = normalizeRegressionCatalogDocument({
23
23
  schemaVersion: 1,
24
24
  entries: [
25
25
  {
26
26
  id: "bad-message",
27
- title: "Bad message bug",
28
27
  classification: "product_bug",
29
- state: "open",
30
28
  issue: {
31
29
  repo: "acme/repo",
32
30
  number: 12,
33
- url: "https://github.com/acme/repo/issues/12",
34
31
  },
35
- description: "Wrong message",
36
- whyFailing: "Payload is wrong",
37
- lastReviewedAt: "2026-04-27",
38
- matches: [
32
+ summary: "API returns the wrong message",
33
+ cause: "The payload is wrong.",
34
+ lastReviewedAt: "2026-05-04",
35
+ fingerprints: [
39
36
  {
40
37
  service: "api",
41
38
  type: "int",
@@ -47,7 +44,7 @@ describe("known failures core", () => {
47
44
  ],
48
45
  });
49
46
 
50
- const matches = findMatchingKnownFailureEntries(document, {
47
+ const matches = findMatchingRegressionEntries(document, {
51
48
  service: "api",
52
49
  type: "int",
53
50
  path: "src/api/routes/__testkit__/failing.int.testkit.ts",
@@ -59,24 +56,21 @@ describe("known failures core", () => {
59
56
  expect(matches[0].id).toBe("bad-message");
60
57
  });
61
58
 
62
- it("matches failureKey against a detail title when the key becomes richer", () => {
63
- const document = normalizeKnownFailuresDocument({
59
+ it("matches fingerprint failureKey against detail title", () => {
60
+ const document = normalizeRegressionCatalogDocument({
64
61
  schemaVersion: 1,
65
62
  entries: [
66
63
  {
67
64
  id: "missing-route",
68
- title: "Missing route bug",
69
65
  classification: "product_bug",
70
- state: "open",
71
66
  issue: {
72
67
  repo: "acme/repo",
73
68
  number: 13,
74
- url: "https://github.com/acme/repo/issues/13",
75
69
  },
76
- description: "Wrong status code",
77
- whyFailing: "The route returns 404",
78
- lastReviewedAt: "2026-04-28",
79
- matches: [
70
+ summary: "Missing route returns the wrong status",
71
+ cause: "The route returns 404.",
72
+ lastReviewedAt: "2026-05-04",
73
+ fingerprints: [
80
74
  {
81
75
  service: "api",
82
76
  type: "int",
@@ -88,7 +82,7 @@ describe("known failures core", () => {
88
82
  ],
89
83
  });
90
84
 
91
- const matches = findMatchingKnownFailureEntries(document, {
85
+ const matches = findMatchingRegressionEntries(document, {
92
86
  service: "api",
93
87
  type: "int",
94
88
  path: "__testkit__/health/http-failure.int.testkit.ts",
@@ -105,31 +99,28 @@ describe("known failures core", () => {
105
99
  expect(matches[0].id).toBe("missing-route");
106
100
  });
107
101
 
108
- it("validates status coverage and filesystem matches", () => {
109
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-known-failures-"));
102
+ it("validates status coverage and filesystem fingerprints", () => {
103
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-regressions-"));
110
104
  tempDirs.push(tempDir);
111
105
  const testPath = path.join(tempDir, "src/api/routes/__testkit__/failing.int.testkit.ts");
112
106
  fs.mkdirSync(path.dirname(testPath), { recursive: true });
113
107
  fs.writeFileSync(testPath, "export default {};\n");
114
108
 
115
- const document = normalizeKnownFailuresDocument({
109
+ const document = normalizeRegressionCatalogDocument({
116
110
  schemaVersion: 1,
117
111
  issueRepo: "acme/repo",
118
112
  entries: [
119
113
  {
120
114
  id: "bad-message",
121
- title: "Bad message bug",
122
115
  classification: "product_bug",
123
- state: "open",
124
116
  issue: {
125
117
  repo: "acme/repo",
126
118
  number: 12,
127
- url: "https://github.com/acme/repo/issues/12",
128
119
  },
129
- description: "Wrong message",
130
- whyFailing: "Payload is wrong",
131
- lastReviewedAt: "2026-04-27",
132
- matches: [
120
+ summary: "API returns the wrong message",
121
+ cause: "The payload is wrong.",
122
+ lastReviewedAt: "2026-05-04",
123
+ fingerprints: [
133
124
  {
134
125
  service: "api",
135
126
  type: "int",
@@ -140,7 +131,7 @@ describe("known failures core", () => {
140
131
  ],
141
132
  });
142
133
 
143
- const result = validateKnownFailuresDocument(document, {
134
+ const result = validateRegressionCatalogDocument(document, {
144
135
  productDir: tempDir,
145
136
  statusArtifact: {
146
137
  tests: [
@@ -162,37 +153,35 @@ describe("known failures core", () => {
162
153
 
163
154
  expect(result.errors).toEqual([]);
164
155
  expect(result.stats.failedTests).toBe(2);
165
- expect(result.stats.triagedFailedTests).toBe(1);
156
+ expect(result.stats.diagnosedFailedTests).toBe(1);
166
157
  expect(result.warnings).toContain(
167
- "Untriaged failed test: src/api/routes/__testkit__/other.int.testkit.ts"
158
+ "New failing test not yet in regression catalog: src/api/routes/__testkit__/other.int.testkit.ts"
168
159
  );
169
160
  });
170
161
 
171
162
  it("renders markdown from the canonical document", () => {
172
- const markdown = renderKnownFailuresMarkdown(
173
- normalizeKnownFailuresDocument({
163
+ const markdown = renderRegressionCatalogMarkdown(
164
+ normalizeRegressionCatalogDocument({
174
165
  schemaVersion: 1,
175
166
  entries: [
176
167
  {
177
168
  id: "bad-message",
178
- title: "Bad message bug",
179
169
  classification: "product_bug",
180
- state: "open",
181
170
  issue: {
182
171
  repo: "acme/repo",
183
172
  number: 12,
184
- url: "https://github.com/acme/repo/issues/12",
185
173
  },
186
- description: "Wrong message",
187
- whyFailing: "Payload is wrong",
188
- lastReviewedAt: "2026-04-27",
189
- matches: [{ path: "src/api/routes/__testkit__/failing.int.testkit.ts" }],
174
+ summary: "API returns the wrong message",
175
+ cause: "The payload is wrong.",
176
+ lastReviewedAt: "2026-05-04",
177
+ fingerprints: [{ path: "src/api/routes/__testkit__/failing.int.testkit.ts" }],
190
178
  },
191
179
  ],
192
180
  })
193
181
  );
194
182
 
195
- expect(markdown).toContain("# Bourne Bugs");
196
- expect(markdown).toContain("[#12](https://github.com/acme/repo/issues/12)");
183
+ expect(markdown).toContain("# Regression Catalog");
184
+ expect(markdown).toContain("API returns the wrong message");
185
+ expect(markdown).toContain("#12");
197
186
  });
198
187
  });
@@ -1,4 +1,5 @@
1
- import { buildKnownFailureIssueValidationSummaryLines } from "../known-failures/github.mjs";
1
+ import { findMatchingRegressionEntries } from "../regressions/index.mjs";
2
+ import { buildRegressionSyncSummaryLines } from "../regressions/github.mjs";
2
3
 
3
4
  export function formatDuration(durationMs) {
4
5
  const totalSeconds = Math.max(0, Math.round(durationMs / 1000));
@@ -48,57 +49,75 @@ export function formatSuiteFramework(framework) {
48
49
  return label ? ` [${label}]` : "";
49
50
  }
50
51
 
51
- export function buildRunSummaryLines(results, durationMs, knownFailureIssueValidation = null) {
52
- return buildCompactRunSummaryLines(results, durationMs, knownFailureIssueValidation);
52
+ export function buildRunSummaryLines(results, durationMs, regressionReport = null) {
53
+ return buildCompactRunSummaryLines(results, durationMs, regressionReport);
54
+ }
55
+
56
+ export function buildRunSummaryData(results, durationMs, regressionReport = null) {
57
+ const totals = summarizeResults(results);
58
+ const serviceErrors = collectServiceErrors(results);
59
+ const regressionSummary = regressionReport?.summary || null;
60
+ return {
61
+ result: totals.failedServices > 0 ? "FAILED" : "PASSED",
62
+ totalServices: totals.totalServices,
63
+ failedServices: totals.failedServices,
64
+ passed: totals.passedFiles,
65
+ failed: totals.failedFiles,
66
+ skipped: totals.skippedFiles,
67
+ notRun: totals.notRunFiles,
68
+ files: totals.totalFiles,
69
+ duration: formatDuration(durationMs),
70
+ serviceErrors: serviceErrors.length,
71
+ newRegressions: regressionSummary?.newRegressions || 0,
72
+ knownRegressions: regressionSummary?.knownRegressions || 0,
73
+ fixedKnownRegressions: regressionSummary?.fixedKnownRegressions || 0,
74
+ catalogStale: regressionSummary?.catalogStale || 0,
75
+ catalogSyncUnavailable: Boolean(regressionSummary?.catalogSyncUnavailable),
76
+ usedStaleCache: Boolean(regressionSummary?.usedStaleCache),
77
+ };
53
78
  }
54
79
 
55
80
  export function buildCompactRunSummaryLines(
56
81
  results,
57
82
  durationMs,
58
- knownFailureIssueValidation = null
83
+ regressionReport = null
59
84
  ) {
60
- const totals = summarizeResults(results);
85
+ const summary = buildRunSummaryData(results, durationMs, regressionReport);
61
86
  const lines = [
62
87
  "",
63
- `Summary: ${totals.passedFiles} passed, ${totals.failedFiles} failed, ${totals.skippedFiles} skipped, ${totals.notRunFiles} not run across ${totals.totalFiles} ${pluralize(totals.totalFiles, "file", "files")} in ${formatDuration(durationMs)}`,
88
+ `Summary: ${summary.passed} passed, ${summary.failed} failed, ${summary.skipped} skipped, ${summary.notRun} not run across ${summary.files} ${pluralize(summary.files, "file", "files")} in ${summary.duration}`,
64
89
  ];
65
90
 
66
- const failures = collectFailedFiles(results);
67
- if (failures.length > 0) {
68
- lines.push("", "Failures:");
69
- for (const failure of failures) {
70
- lines.push(` ${failure.file.path}`);
71
- lines.push(` ${failure.primaryMessage}`);
72
- for (const detail of failure.extraLines.slice(0, 3)) {
73
- lines.push(` ${detail}`);
74
- }
75
- }
91
+ if (summary.serviceErrors > 0) {
92
+ lines.push(`Runtime errors: ${summary.serviceErrors}`);
76
93
  }
77
94
 
78
- const serviceErrors = collectServiceErrors(results);
79
- if (serviceErrors.length > 0) {
80
- lines.push("", "Runtime Errors:");
81
- for (const item of serviceErrors) {
82
- lines.push(` ${item.service}`);
83
- lines.push(` ${item.message}`);
84
- }
95
+ if (summary.newRegressions > 0) {
96
+ lines.push(`New regressions: ${summary.newRegressions}`);
85
97
  }
86
-
87
- const knownFailureIssueLines = buildKnownFailureIssueValidationSummaryLines(
88
- knownFailureIssueValidation
89
- );
90
- if (knownFailureIssueLines.length > 0) {
91
- lines.push(...knownFailureIssueLines);
98
+ if (summary.knownRegressions > 0) {
99
+ lines.push(`Known regressions: ${summary.knownRegressions}`);
100
+ }
101
+ if (summary.fixedKnownRegressions > 0) {
102
+ lines.push(`Fixed known regressions: ${summary.fixedKnownRegressions}`);
103
+ }
104
+ if (summary.catalogStale > 0) {
105
+ lines.push(`Catalog stale: ${summary.catalogStale}`);
106
+ }
107
+ if (summary.catalogSyncUnavailable) {
108
+ lines.push("Catalog sync unavailable");
92
109
  }
93
110
 
94
111
  lines.push("");
95
112
  lines.push(
96
- totals.failedServices > 0 ? `Result: FAILED (${totals.failedServices}/${totals.totalServices} services failed)` : "Result: PASSED"
113
+ summary.result === "FAILED"
114
+ ? `Result: FAILED (${summary.failedServices}/${summary.totalServices} services failed)`
115
+ : "Result: PASSED"
97
116
  );
98
117
  return lines;
99
118
  }
100
119
 
101
- export function buildDebugRunSummaryLines(results, durationMs, knownFailureIssueValidation = null) {
120
+ export function buildDebugRunSummaryLines(results, durationMs, regressionReport = null) {
102
121
  const totalServices = results.length;
103
122
  const executedServices = results.filter((result) => !result.skipped);
104
123
  const skippedServices = results.filter((result) => result.skipped);
@@ -167,11 +186,19 @@ export function buildDebugRunSummaryLines(results, durationMs, knownFailureIssue
167
186
  }
168
187
  }
169
188
 
170
- const knownFailureIssueLines = buildKnownFailureIssueValidationSummaryLines(
171
- knownFailureIssueValidation
189
+ const regressionSyncLines = buildRegressionSyncSummaryLines(
190
+ regressionReport?.catalog?.configured ? {
191
+ summary: {
192
+ byCode: {
193
+ closed_but_failing: regressionReport.summary.catalogStale,
194
+ validation_unavailable: regressionReport.summary.catalogSyncUnavailable ? 1 : 0,
195
+ used_stale_cache: regressionReport.summary.usedStaleCache ? 1 : 0,
196
+ },
197
+ },
198
+ } : null
172
199
  );
173
- if (knownFailureIssueLines.length > 0) {
174
- lines.push(...knownFailureIssueLines);
200
+ if (regressionSyncLines.length > 0) {
201
+ lines.push(...regressionSyncLines);
175
202
  }
176
203
 
177
204
  if (failedServices.length > 0) {
@@ -183,6 +210,43 @@ export function buildDebugRunSummaryLines(results, durationMs, knownFailureIssue
183
210
  return lines;
184
211
  }
185
212
 
213
+ export function buildFailurePresentation(fileSummary, regressionCatalog = null) {
214
+ const rankedDetails = rankFailureDetails(fileSummary.failureDetails || []);
215
+ const primaryDetail = rankedDetails[0] || null;
216
+ const fallbackMessages = rankedDetails
217
+ .map((detail) => detail.message || detail.title)
218
+ .filter(Boolean)
219
+ .map((message) => sanitizeErrorMessage(String(message).trim()));
220
+ const details = [];
221
+
222
+ const responseLine = formatFailureResponsePreview(primaryDetail);
223
+ if (responseLine) details.push(responseLine);
224
+
225
+ const regressionLine = formatInlineRegressionLine(fileSummary, regressionCatalog);
226
+ if (regressionLine) details.push(regressionLine);
227
+
228
+ const requestLine = formatFailureRequestHint(primaryDetail);
229
+ if (requestLine) details.push(requestLine);
230
+
231
+ for (const detail of rankedDetails.slice(primaryDetail ? 1 : 0)) {
232
+ const line = sanitizeInline(String(detail.message || detail.title || ""), 220);
233
+ if (line && !details.includes(line)) details.push(line);
234
+ }
235
+
236
+ return {
237
+ primary: sanitizeInline(
238
+ resolvePrimaryFailureMessage(
239
+ fileSummary,
240
+ { error: fileSummary.suiteError || null },
241
+ primaryDetail,
242
+ fallbackMessages
243
+ ),
244
+ 240
245
+ ),
246
+ details,
247
+ };
248
+ }
249
+
186
250
  function sanitizeErrorMessage(message) {
187
251
  return message
188
252
  .replace(/Command failed with exit code (\d+): .*?[\\/]vendor[\\/]k6 run\b/g, "Default runtime failed with exit code $1:")
@@ -203,46 +267,6 @@ function summarizeResults(results) {
203
267
  };
204
268
  }
205
269
 
206
- function collectFailedFiles(results) {
207
- const failures = [];
208
- for (const result of results) {
209
- for (const suite of result.suites || []) {
210
- for (const file of suite.files || []) {
211
- if (file.status !== "failed") continue;
212
- const rankedDetails = rankFailureDetails(file.failureDetails || []);
213
- const primaryDetail = rankedDetails[0] || null;
214
- const fallbackMessages = rankedDetails
215
- .map((detail) => detail.message || detail.title)
216
- .filter(Boolean)
217
- .map((message) => sanitizeErrorMessage(String(message).trim()));
218
- const extraLines = [];
219
- if (primaryDetail) {
220
- const responseLine = formatFailureResponsePreview(primaryDetail);
221
- if (responseLine) extraLines.push(responseLine);
222
- const triageLine = formatTriageLine(file.triage || null);
223
- if (triageLine) extraLines.push(triageLine);
224
- const requestLine = formatFailureRequestHint(primaryDetail);
225
- if (requestLine) extraLines.push(requestLine);
226
- } else {
227
- const triageLine = formatTriageLine(file.triage || null);
228
- if (triageLine) extraLines.push(triageLine);
229
- }
230
-
231
- for (const detail of rankedDetails.slice(primaryDetail ? 1 : 0)) {
232
- const line = sanitizeErrorMessage(String(detail.message || detail.title || "").trim());
233
- if (line && !extraLines.includes(line)) extraLines.push(line);
234
- }
235
- failures.push({
236
- file,
237
- primaryMessage: resolvePrimaryFailureMessage(file, suite, primaryDetail, fallbackMessages),
238
- extraLines,
239
- });
240
- }
241
- }
242
- }
243
- return failures.sort((left, right) => left.file.path.localeCompare(right.file.path));
244
- }
245
-
246
270
  function resolvePrimaryFailureMessage(file, suite, primaryDetail, fallbackMessages) {
247
271
  if (primaryDetail?.message) {
248
272
  return sanitizeErrorMessage(String(primaryDetail.message).trim());
@@ -295,35 +319,13 @@ function formatFailureRequestHint(detail) {
295
319
  return `request: ${method} ${path}`;
296
320
  }
297
321
 
298
- function formatTriageLine(triage) {
299
- if (!triage) return null;
300
- if (triage.status === "untriaged") return "triage: untriaged";
301
-
302
- const entry = triage.entries?.[0];
303
- if (!entry?.issue) {
304
- if (triage.status === "validation_unavailable") {
305
- const reason = triage.availability?.reason ? ` (${triage.availability.reason})` : "";
306
- return `triage: validation unavailable${reason}`;
307
- }
308
- return null;
309
- }
322
+ function formatInlineRegressionLine(fileSummary, regressionCatalog) {
323
+ if (!regressionCatalog) return "regression: new";
324
+ const matches = findMatchingRegressionEntries(regressionCatalog, fileSummary);
325
+ if (matches.length === 0) return "regression: new";
310
326
 
311
- const issueLabel = `#${entry.issue.number}`;
312
- const validationStatus = entry.validationStatus || null;
313
- if (validationStatus === "closed_but_failing") {
314
- return `triage: known issue ${issueLabel} closed but still failing`;
315
- }
316
- if (validationStatus === "validation_unavailable") {
317
- const reason = triage.availability?.mode === "cache" ? "cache" : "validation unavailable";
318
- return `triage: known issue ${issueLabel} (${reason})`;
319
- }
320
- if (entry.github?.state === "open" || entry.state === "open") {
321
- return `triage: known issue ${issueLabel} open`;
322
- }
323
- if (entry.github?.state === "closed" || entry.state === "closed") {
324
- return `triage: known issue ${issueLabel} closed`;
325
- }
326
- return `triage: known issue ${issueLabel}`;
327
+ const entry = matches[0];
328
+ return `regression: known #${entry.issue.number} ${entry.classification}`;
327
329
  }
328
330
 
329
331
  function isThresholdWrapperMessage(message) {