@elench/testkit 0.1.80 → 0.1.82
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 +78 -56
- package/lib/cli/args.mjs +2 -14
- package/lib/cli/args.test.mjs +1 -17
- package/lib/cli/command-helpers.mjs +1 -20
- package/lib/cli/entrypoint.mjs +0 -4
- package/lib/cli/presentation/colors.mjs +1 -1
- package/lib/cli/presentation/failure-presentation.mjs +4 -4
- package/lib/cli/presentation/run-reporter.mjs +23 -9
- package/lib/cli/presentation/run-reporter.test.mjs +12 -6
- package/lib/cli/presentation/summary-box.test.mjs +4 -4
- package/lib/cli/viewer.mjs +18 -19
- package/lib/config/index.mjs +6 -6
- package/lib/config/runtime.mjs +8 -8
- package/lib/config-api/auth-fixtures.mjs +762 -0
- package/lib/config-api/index.d.ts +96 -112
- package/lib/config-api/index.mjs +22 -12
- package/lib/config-api/index.test.mjs +61 -222
- package/lib/index.d.ts +29 -9
- package/lib/package.test.mjs +4 -4
- package/lib/{known-failures → regressions}/github.mjs +36 -78
- package/lib/regressions/github.test.mjs +324 -0
- package/lib/regressions/index.d.ts +189 -0
- package/lib/{known-failures → regressions}/index.mjs +90 -93
- package/lib/{known-failures → regressions}/index.test.mjs +37 -48
- package/lib/runner/formatting.mjs +49 -34
- package/lib/runner/formatting.test.mjs +16 -15
- package/lib/runner/metadata.mjs +1 -1
- package/lib/runner/orchestrator.mjs +7 -9
- package/lib/runner/regressions.mjs +304 -0
- package/lib/runner/{triage.test.mjs → regressions.test.mjs} +50 -36
- package/lib/runner/reporting.mjs +2 -2
- package/lib/runner/reporting.test.mjs +2 -2
- package/lib/runner/run-finalization.mjs +18 -30
- package/lib/runner/template-steps.mjs +2 -2
- package/lib/runtime/index.d.ts +50 -33
- package/lib/runtime/index.mjs +0 -1
- package/lib/runtime-src/k6/http-suite-runtime.js +147 -0
- package/lib/runtime-src/k6/http.js +80 -41
- package/lib/runtime-src/k6/scenario-suite.js +13 -110
- package/lib/runtime-src/k6/suite.js +13 -107
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +8 -8
- package/lib/cli/commands/known-failures/render.mjs +0 -19
- package/lib/cli/commands/known-failures/validate.mjs +0 -20
- package/lib/cli/known-failures.mjs +0 -164
- package/lib/config-api/profiles.mjs +0 -640
- package/lib/known-failures/github.test.mjs +0 -512
- package/lib/known-failures/index.d.ts +0 -192
- package/lib/runner/triage.mjs +0 -221
- /package/lib/{known-failures → regressions}/github-cache.mjs +0 -0
- /package/lib/{known-failures → regressions}/github-transport.mjs +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { findMatchingRegressionEntries } from "./index.mjs";
|
|
2
2
|
import {
|
|
3
3
|
applyStaleCacheFallback,
|
|
4
4
|
loadIssueCache,
|
|
@@ -12,37 +12,34 @@ import {
|
|
|
12
12
|
fetchIssuesByRepo,
|
|
13
13
|
parseGitHubRepoSlug,
|
|
14
14
|
} from "./github-transport.mjs";
|
|
15
|
+
|
|
15
16
|
export { parseGitHubRepoSlug } from "./github-transport.mjs";
|
|
17
|
+
|
|
16
18
|
const DEFAULT_CACHE_TTL_SECONDS = 15 * 60;
|
|
17
19
|
const CACHE_SCHEMA_VERSION = 1;
|
|
18
|
-
const CACHE_PATH = [".testkit", "
|
|
20
|
+
const CACHE_PATH = [".testkit", "regressions", "github-issues-cache.json"];
|
|
19
21
|
const MODES = new Set(["off", "warn", "error"]);
|
|
20
22
|
|
|
21
|
-
export function
|
|
23
|
+
export function normalizeRegressionSyncConfig(value) {
|
|
22
24
|
if (value == null) return null;
|
|
23
25
|
if (!value || typeof value !== "object") {
|
|
24
|
-
throw new Error("testkit.config.ts
|
|
26
|
+
throw new Error("testkit.config.ts regressions.sync must be an object");
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
const provider = normalizeOptionalString(value.provider) || "github";
|
|
28
30
|
if (provider !== "github") {
|
|
29
|
-
throw new Error('testkit.config.ts
|
|
31
|
+
throw new Error('testkit.config.ts regressions.sync.provider must be "github"');
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
const mode = normalizeOptionalString(value.mode) || "warn";
|
|
33
35
|
if (!MODES.has(mode)) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
'testkit.config.ts reporting.issueValidation.mode must be one of: off, warn, error'
|
|
36
|
-
);
|
|
36
|
+
throw new Error('testkit.config.ts regressions.sync.mode must be one of: off, warn, error');
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const cacheTtlSeconds =
|
|
40
40
|
value.cacheTtlSeconds == null
|
|
41
41
|
? DEFAULT_CACHE_TTL_SECONDS
|
|
42
|
-
: normalizePositiveInteger(
|
|
43
|
-
value.cacheTtlSeconds,
|
|
44
|
-
"testkit.config.ts reporting.issueValidation.cacheTtlSeconds"
|
|
45
|
-
);
|
|
42
|
+
: normalizePositiveInteger(value.cacheTtlSeconds, "testkit.config.ts regressions.sync.cacheTtlSeconds");
|
|
46
43
|
|
|
47
44
|
return {
|
|
48
45
|
provider,
|
|
@@ -51,7 +48,7 @@ export function normalizeKnownFailureIssueValidationConfig(value) {
|
|
|
51
48
|
};
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
export async function
|
|
51
|
+
export async function validateRegressionIssues({
|
|
55
52
|
productDir,
|
|
56
53
|
document,
|
|
57
54
|
runArtifact,
|
|
@@ -61,10 +58,10 @@ export async function validateKnownFailureIssues({
|
|
|
61
58
|
transport = null,
|
|
62
59
|
now = Date.now(),
|
|
63
60
|
}) {
|
|
64
|
-
const normalizedConfig =
|
|
61
|
+
const normalizedConfig = normalizeRegressionSyncConfig(config);
|
|
65
62
|
if (!document || !normalizedConfig || normalizedConfig.mode === "off") return null;
|
|
66
63
|
|
|
67
|
-
const checks =
|
|
64
|
+
const checks = collectObservedRegressionEntries(document, runArtifact, statusArtifact);
|
|
68
65
|
const issueNumbersByRepo = groupIssueNumbersByRepo(document.entries);
|
|
69
66
|
const repoSlug = gitMetadata?.repoSlug || null;
|
|
70
67
|
const remoteUrl = gitMetadata?.remoteUrl || null;
|
|
@@ -87,11 +84,10 @@ export async function validateKnownFailureIssues({
|
|
|
87
84
|
cacheResolution.staleByRepo,
|
|
88
85
|
availabilityFindings
|
|
89
86
|
);
|
|
90
|
-
const severity = normalizedConfig.mode === "error" ? "error" : "warning";
|
|
91
87
|
availabilityFindings.push({
|
|
92
88
|
code: "validation_unavailable",
|
|
93
|
-
severity,
|
|
94
|
-
message: `GitHub issue
|
|
89
|
+
severity: normalizedConfig.mode === "error" ? "error" : "warning",
|
|
90
|
+
message: `GitHub issue sync unavailable: ${formatErrorMessage(error)}`,
|
|
95
91
|
});
|
|
96
92
|
}
|
|
97
93
|
} else {
|
|
@@ -100,12 +96,11 @@ export async function validateKnownFailureIssues({
|
|
|
100
96
|
cacheResolution.staleByRepo,
|
|
101
97
|
availabilityFindings
|
|
102
98
|
);
|
|
103
|
-
const severity = normalizedConfig.mode === "error" ? "error" : "warning";
|
|
104
99
|
availabilityFindings.push({
|
|
105
100
|
code: "validation_unavailable",
|
|
106
|
-
severity,
|
|
101
|
+
severity: normalizedConfig.mode === "error" ? "error" : "warning",
|
|
107
102
|
message:
|
|
108
|
-
"GitHub issue
|
|
103
|
+
"GitHub issue sync unavailable: set GH_TOKEN/GITHUB_TOKEN or install/authenticate gh",
|
|
109
104
|
});
|
|
110
105
|
}
|
|
111
106
|
}
|
|
@@ -113,11 +108,7 @@ export async function validateKnownFailureIssues({
|
|
|
113
108
|
const entries = document.entries.map((entry) => {
|
|
114
109
|
const observed = checks.get(entry.id) || createObservedCheck(entry);
|
|
115
110
|
const issueData = issuesByRepo.get(entry.issue.repo)?.get(entry.issue.number) || null;
|
|
116
|
-
return buildIssueValidationEntry({
|
|
117
|
-
entry,
|
|
118
|
-
observed,
|
|
119
|
-
issueData,
|
|
120
|
-
});
|
|
111
|
+
return buildIssueValidationEntry({ entry, observed, issueData });
|
|
121
112
|
});
|
|
122
113
|
|
|
123
114
|
const globalFindings = [...availabilityFindings];
|
|
@@ -126,7 +117,7 @@ export async function validateKnownFailureIssues({
|
|
|
126
117
|
code: "detected_repo_mismatch",
|
|
127
118
|
severity: normalizedConfig.mode === "error" ? "error" : "warning",
|
|
128
119
|
message:
|
|
129
|
-
`
|
|
120
|
+
`Regression catalog issueRepo ${document.issueRepo} does not match detected repo ${repoSlug}` +
|
|
130
121
|
(remoteUrl ? ` (${remoteUrl})` : ""),
|
|
131
122
|
});
|
|
132
123
|
}
|
|
@@ -152,12 +143,12 @@ export async function validateKnownFailureIssues({
|
|
|
152
143
|
};
|
|
153
144
|
}
|
|
154
145
|
|
|
155
|
-
export function
|
|
146
|
+
export function shouldFailRegressionSync(result) {
|
|
156
147
|
if (!result || result.mode !== "error") return false;
|
|
157
148
|
return (result.summary?.errors || 0) > 0;
|
|
158
149
|
}
|
|
159
150
|
|
|
160
|
-
export function
|
|
151
|
+
export function buildRegressionSyncSummaryParts(result) {
|
|
161
152
|
if (!result) return [];
|
|
162
153
|
const parts = [];
|
|
163
154
|
const byCode = result.summary?.byCode || {};
|
|
@@ -167,23 +158,17 @@ export function buildKnownFailureIssueValidationSummaryParts(result) {
|
|
|
167
158
|
if (byCode.issue_missing) {
|
|
168
159
|
parts.push(`${byCode.issue_missing} missing issue reference${pluralSuffix(byCode.issue_missing)}`);
|
|
169
160
|
}
|
|
170
|
-
if (byCode.open_not_reproduced) {
|
|
171
|
-
parts.push(`${byCode.open_not_reproduced} open issue${pluralSuffix(byCode.open_not_reproduced)} not reproduced`);
|
|
172
|
-
}
|
|
173
161
|
if (byCode.closed_not_reproduced) {
|
|
174
|
-
parts.push(`${byCode.closed_not_reproduced}
|
|
162
|
+
parts.push(`${byCode.closed_not_reproduced} fixed known regression${pluralSuffix(byCode.closed_not_reproduced)}`);
|
|
175
163
|
}
|
|
176
|
-
if (byCode.
|
|
177
|
-
parts.push(`${byCode.
|
|
178
|
-
}
|
|
179
|
-
if (byCode.state_mismatch) {
|
|
180
|
-
parts.push(`${byCode.state_mismatch} state mismatch${pluralSuffix(byCode.state_mismatch)}`);
|
|
164
|
+
if (byCode.open_not_reproduced) {
|
|
165
|
+
parts.push(`${byCode.open_not_reproduced} known regression${pluralSuffix(byCode.open_not_reproduced)} no longer reproducing`);
|
|
181
166
|
}
|
|
182
167
|
if (byCode.detected_repo_mismatch) {
|
|
183
168
|
parts.push(`${byCode.detected_repo_mismatch} repo mismatch${pluralSuffix(byCode.detected_repo_mismatch)}`);
|
|
184
169
|
}
|
|
185
170
|
if (byCode.validation_unavailable) {
|
|
186
|
-
parts.push("GitHub
|
|
171
|
+
parts.push("GitHub issue sync unavailable");
|
|
187
172
|
}
|
|
188
173
|
if (byCode.used_stale_cache) {
|
|
189
174
|
parts.push("used stale GitHub cache");
|
|
@@ -192,24 +177,22 @@ export function buildKnownFailureIssueValidationSummaryParts(result) {
|
|
|
192
177
|
return parts;
|
|
193
178
|
}
|
|
194
179
|
|
|
195
|
-
export function
|
|
196
|
-
const parts =
|
|
180
|
+
export function buildRegressionSyncSummaryLines(result) {
|
|
181
|
+
const parts = buildRegressionSyncSummaryParts(result);
|
|
197
182
|
if (parts.length === 0) return [];
|
|
198
|
-
|
|
199
183
|
return [
|
|
200
184
|
"",
|
|
201
|
-
"
|
|
185
|
+
"Catalog issues:",
|
|
202
186
|
...parts.map((part) => ` - ${part}`),
|
|
203
187
|
];
|
|
204
188
|
}
|
|
205
189
|
|
|
206
|
-
|
|
207
|
-
function collectObservedKnownFailureEntries(document, runArtifact, statusArtifact) {
|
|
190
|
+
function collectObservedRegressionEntries(document, runArtifact, statusArtifact) {
|
|
208
191
|
const observedTests = collectObservedTests(runArtifact, statusArtifact);
|
|
209
192
|
const checks = new Map(document.entries.map((entry) => [entry.id, createObservedCheck(entry)]));
|
|
210
193
|
|
|
211
194
|
for (const test of observedTests) {
|
|
212
|
-
const matchedEntries =
|
|
195
|
+
const matchedEntries = findMatchingRegressionEntries(document, test);
|
|
213
196
|
for (const entry of matchedEntries) {
|
|
214
197
|
const observed = checks.get(entry.id) || createObservedCheck(entry);
|
|
215
198
|
observed.matchedTests += 1;
|
|
@@ -292,23 +275,7 @@ function buildIssueValidationEntry({ entry, observed, issueData }) {
|
|
|
292
275
|
findings.push({
|
|
293
276
|
code: "issue_missing",
|
|
294
277
|
severity: "error",
|
|
295
|
-
message: `
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (issueData?.exists && issueData.title && issueData.title !== entry.title) {
|
|
300
|
-
findings.push({
|
|
301
|
-
code: "title_mismatch",
|
|
302
|
-
severity: "error",
|
|
303
|
-
message: `Known failure ${entry.id} title does not match issue #${entry.issue.number}`,
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (issueData?.exists && normalizedIssueState && normalizedIssueState !== entry.state) {
|
|
308
|
-
findings.push({
|
|
309
|
-
code: "state_mismatch",
|
|
310
|
-
severity: "error",
|
|
311
|
-
message: `Known failure ${entry.id} state ${entry.state} does not match issue #${entry.issue.number} state ${normalizedIssueState}`,
|
|
278
|
+
message: `Regression catalog entry ${entry.id} references missing issue #${entry.issue.number} in ${entry.issue.repo}`,
|
|
312
279
|
});
|
|
313
280
|
}
|
|
314
281
|
|
|
@@ -316,7 +283,7 @@ function buildIssueValidationEntry({ entry, observed, issueData }) {
|
|
|
316
283
|
findings.push({
|
|
317
284
|
code: "closed_but_failing",
|
|
318
285
|
severity: "error",
|
|
319
|
-
message: `
|
|
286
|
+
message: `Regression ${entry.id} still fails but issue #${entry.issue.number} is closed`,
|
|
320
287
|
});
|
|
321
288
|
}
|
|
322
289
|
|
|
@@ -330,20 +297,20 @@ function buildIssueValidationEntry({ entry, observed, issueData }) {
|
|
|
330
297
|
findings.push({
|
|
331
298
|
code: "open_not_reproduced",
|
|
332
299
|
severity: "warning",
|
|
333
|
-
message: `
|
|
300
|
+
message: `Regression ${entry.id} did not reproduce but issue #${entry.issue.number} is open`,
|
|
334
301
|
});
|
|
335
302
|
} else if (normalizedIssueState === "closed") {
|
|
336
303
|
findings.push({
|
|
337
304
|
code: "closed_not_reproduced",
|
|
338
305
|
severity: "warning",
|
|
339
|
-
message: `
|
|
306
|
+
message: `Regression ${entry.id} did not reproduce and issue #${entry.issue.number} is closed`,
|
|
340
307
|
});
|
|
341
308
|
}
|
|
342
309
|
}
|
|
343
310
|
|
|
344
311
|
return {
|
|
345
312
|
id: entry.id,
|
|
346
|
-
|
|
313
|
+
summary: entry.summary,
|
|
347
314
|
issue: entry.issue,
|
|
348
315
|
observed: {
|
|
349
316
|
matchedTests: observed.matchedTests,
|
|
@@ -372,11 +339,11 @@ function buildIssueValidationEntry({ entry, observed, issueData }) {
|
|
|
372
339
|
cached: false,
|
|
373
340
|
},
|
|
374
341
|
findings,
|
|
375
|
-
status: resolveIssueValidationStatus(issueData, observed
|
|
342
|
+
status: resolveIssueValidationStatus(issueData, observed),
|
|
376
343
|
};
|
|
377
344
|
}
|
|
378
345
|
|
|
379
|
-
function resolveIssueValidationStatus(issueData, observed
|
|
346
|
+
function resolveIssueValidationStatus(issueData, observed) {
|
|
380
347
|
if (!issueData) return observed.matchedTests > 0 ? "validation_unavailable" : "not_observed";
|
|
381
348
|
if (issueData.exists === false) return "issue_missing";
|
|
382
349
|
const normalizedState = normalizeIssueState(issueData.state);
|
|
@@ -399,7 +366,6 @@ function resolveIssueValidationStatus(issueData, observed, findings) {
|
|
|
399
366
|
) {
|
|
400
367
|
return "closed_not_reproduced";
|
|
401
368
|
}
|
|
402
|
-
if (findings.length > 0) return "metadata_mismatch";
|
|
403
369
|
return "not_observed";
|
|
404
370
|
}
|
|
405
371
|
|
|
@@ -453,14 +419,6 @@ function normalizePositiveInteger(value, label) {
|
|
|
453
419
|
return value;
|
|
454
420
|
}
|
|
455
421
|
|
|
456
|
-
function chunkValues(values, size) {
|
|
457
|
-
const chunks = [];
|
|
458
|
-
for (let index = 0; index < values.length; index += size) {
|
|
459
|
-
chunks.push(values.slice(index, index + size));
|
|
460
|
-
}
|
|
461
|
-
return chunks;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
422
|
function pluralSuffix(value) {
|
|
465
423
|
return value === 1 ? "" : "s";
|
|
466
424
|
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { normalizeRegressionCatalogDocument } from "./index.mjs";
|
|
6
|
+
import {
|
|
7
|
+
parseGitHubRepoSlug,
|
|
8
|
+
shouldFailRegressionSync,
|
|
9
|
+
validateRegressionIssues,
|
|
10
|
+
} from "./github.mjs";
|
|
11
|
+
|
|
12
|
+
const tempDirs = [];
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
for (const tempDir of tempDirs.splice(0)) {
|
|
16
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("regression GitHub sync", () => {
|
|
21
|
+
it("parses GitHub repo slugs from common origin URLs", () => {
|
|
22
|
+
expect(parseGitHubRepoSlug("https://github.com/acme/repo.git")).toBe("acme/repo");
|
|
23
|
+
expect(parseGitHubRepoSlug("git@github.com:acme/repo.git")).toBe("acme/repo");
|
|
24
|
+
expect(parseGitHubRepoSlug("ssh://git@github.com/acme/repo.git")).toBe("acme/repo");
|
|
25
|
+
expect(parseGitHubRepoSlug("https://gitlab.com/acme/repo.git")).toBe(null);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("fails sync when a linked issue is closed but the regression still reproduces", async () => {
|
|
29
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-regressions-gh-"));
|
|
30
|
+
tempDirs.push(tempDir);
|
|
31
|
+
const document = normalizeRegressionCatalogDocument({
|
|
32
|
+
schemaVersion: 1,
|
|
33
|
+
issueRepo: "acme/repo",
|
|
34
|
+
entries: [
|
|
35
|
+
{
|
|
36
|
+
id: "bad-message",
|
|
37
|
+
classification: "product_bug",
|
|
38
|
+
issue: {
|
|
39
|
+
repo: "acme/repo",
|
|
40
|
+
number: 12,
|
|
41
|
+
},
|
|
42
|
+
summary: "API returns the wrong message",
|
|
43
|
+
cause: "The payload is wrong.",
|
|
44
|
+
lastReviewedAt: "2026-05-04",
|
|
45
|
+
fingerprints: [
|
|
46
|
+
{
|
|
47
|
+
service: "api",
|
|
48
|
+
type: "int",
|
|
49
|
+
path: "src/api/routes/__testkit__/failing.int.testkit.ts",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await validateRegressionIssues({
|
|
57
|
+
productDir: tempDir,
|
|
58
|
+
document,
|
|
59
|
+
statusArtifact: {
|
|
60
|
+
tests: [
|
|
61
|
+
{
|
|
62
|
+
service: "api",
|
|
63
|
+
type: "int",
|
|
64
|
+
path: "src/api/routes/__testkit__/failing.int.testkit.ts",
|
|
65
|
+
status: "failed",
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
config: {
|
|
70
|
+
provider: "github",
|
|
71
|
+
mode: "error",
|
|
72
|
+
cacheTtlSeconds: 60,
|
|
73
|
+
},
|
|
74
|
+
gitMetadata: {
|
|
75
|
+
repoSlug: "acme/repo",
|
|
76
|
+
remoteUrl: "https://github.com/acme/repo.git",
|
|
77
|
+
},
|
|
78
|
+
transport: {
|
|
79
|
+
async fetchRepoIssues(repo, numbers) {
|
|
80
|
+
const map = new Map();
|
|
81
|
+
map.set(numbers[0], {
|
|
82
|
+
repo,
|
|
83
|
+
number: numbers[0],
|
|
84
|
+
exists: true,
|
|
85
|
+
title: "API returns the wrong message",
|
|
86
|
+
state: "CLOSED",
|
|
87
|
+
url: `https://github.com/${repo}/issues/${numbers[0]}`,
|
|
88
|
+
checkedAt: "2026-05-04T00:00:00.000Z",
|
|
89
|
+
source: "github",
|
|
90
|
+
});
|
|
91
|
+
return map;
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(result.summary.byCode.closed_but_failing).toBe(1);
|
|
97
|
+
expect(result.summary.errors).toBeGreaterThan(0);
|
|
98
|
+
expect(result.entries[0].status).toBe("closed_but_failing");
|
|
99
|
+
expect(shouldFailRegressionSync(result)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("allows multiple local entries to share one GitHub issue", async () => {
|
|
103
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-regressions-gh-shared-"));
|
|
104
|
+
tempDirs.push(tempDir);
|
|
105
|
+
const document = normalizeRegressionCatalogDocument({
|
|
106
|
+
schemaVersion: 1,
|
|
107
|
+
issueRepo: "acme/repo",
|
|
108
|
+
entries: [
|
|
109
|
+
{
|
|
110
|
+
id: "route-a-invalid-uuid",
|
|
111
|
+
classification: "product_bug",
|
|
112
|
+
issue: {
|
|
113
|
+
repo: "acme/repo",
|
|
114
|
+
number: 77,
|
|
115
|
+
},
|
|
116
|
+
summary: "Route A leaks UUID parse errors",
|
|
117
|
+
cause: "Route A is missing UUID validation.",
|
|
118
|
+
lastReviewedAt: "2026-05-04",
|
|
119
|
+
fingerprints: [
|
|
120
|
+
{
|
|
121
|
+
service: "api",
|
|
122
|
+
type: "int",
|
|
123
|
+
path: "src/api/routes/__testkit__/route-a.int.testkit.ts",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "route-b-invalid-uuid",
|
|
129
|
+
classification: "product_bug",
|
|
130
|
+
issue: {
|
|
131
|
+
repo: "acme/repo",
|
|
132
|
+
number: 77,
|
|
133
|
+
},
|
|
134
|
+
summary: "Route B leaks UUID parse errors",
|
|
135
|
+
cause: "Route B is missing UUID validation.",
|
|
136
|
+
lastReviewedAt: "2026-05-04",
|
|
137
|
+
fingerprints: [
|
|
138
|
+
{
|
|
139
|
+
service: "api",
|
|
140
|
+
type: "int",
|
|
141
|
+
path: "src/api/routes/__testkit__/route-b.int.testkit.ts",
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const result = await validateRegressionIssues({
|
|
149
|
+
productDir: tempDir,
|
|
150
|
+
document,
|
|
151
|
+
statusArtifact: {
|
|
152
|
+
tests: [
|
|
153
|
+
{
|
|
154
|
+
service: "api",
|
|
155
|
+
type: "int",
|
|
156
|
+
path: "src/api/routes/__testkit__/route-a.int.testkit.ts",
|
|
157
|
+
status: "failed",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
service: "api",
|
|
161
|
+
type: "int",
|
|
162
|
+
path: "src/api/routes/__testkit__/route-b.int.testkit.ts",
|
|
163
|
+
status: "failed",
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
config: {
|
|
168
|
+
provider: "github",
|
|
169
|
+
mode: "error",
|
|
170
|
+
},
|
|
171
|
+
transport: {
|
|
172
|
+
async fetchRepoIssues(repo, numbers) {
|
|
173
|
+
const map = new Map();
|
|
174
|
+
map.set(numbers[0], {
|
|
175
|
+
repo,
|
|
176
|
+
number: numbers[0],
|
|
177
|
+
exists: true,
|
|
178
|
+
title: "UUID path params reach Postgres and return 500 instead of 400",
|
|
179
|
+
state: "OPEN",
|
|
180
|
+
url: `https://github.com/${repo}/issues/${numbers[0]}`,
|
|
181
|
+
checkedAt: "2026-05-04T00:00:00.000Z",
|
|
182
|
+
source: "github",
|
|
183
|
+
});
|
|
184
|
+
return map;
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(result.summary.errors).toBe(0);
|
|
190
|
+
expect(result.entries).toHaveLength(2);
|
|
191
|
+
expect(result.entries[0].findings).toEqual([]);
|
|
192
|
+
expect(result.entries[1].findings).toEqual([]);
|
|
193
|
+
expect(result.entries[0].status).toBe("open_and_failing");
|
|
194
|
+
expect(result.entries[1].status).toBe("open_and_failing");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("warns when an open issue is not reproduced", async () => {
|
|
198
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-regressions-gh-open-"));
|
|
199
|
+
tempDirs.push(tempDir);
|
|
200
|
+
const document = normalizeRegressionCatalogDocument({
|
|
201
|
+
schemaVersion: 1,
|
|
202
|
+
issueRepo: "acme/repo",
|
|
203
|
+
entries: [
|
|
204
|
+
{
|
|
205
|
+
id: "bad-message",
|
|
206
|
+
classification: "product_bug",
|
|
207
|
+
issue: {
|
|
208
|
+
repo: "acme/repo",
|
|
209
|
+
number: 12,
|
|
210
|
+
},
|
|
211
|
+
summary: "API returns the wrong message",
|
|
212
|
+
cause: "The payload is wrong.",
|
|
213
|
+
lastReviewedAt: "2026-05-04",
|
|
214
|
+
fingerprints: [
|
|
215
|
+
{
|
|
216
|
+
service: "api",
|
|
217
|
+
type: "int",
|
|
218
|
+
path: "src/api/routes/__testkit__/failing.int.testkit.ts",
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const result = await validateRegressionIssues({
|
|
226
|
+
productDir: tempDir,
|
|
227
|
+
document,
|
|
228
|
+
statusArtifact: {
|
|
229
|
+
tests: [
|
|
230
|
+
{
|
|
231
|
+
service: "api",
|
|
232
|
+
type: "int",
|
|
233
|
+
path: "src/api/routes/__testkit__/failing.int.testkit.ts",
|
|
234
|
+
status: "passed",
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
},
|
|
238
|
+
config: {
|
|
239
|
+
provider: "github",
|
|
240
|
+
mode: "warn",
|
|
241
|
+
},
|
|
242
|
+
gitMetadata: {
|
|
243
|
+
repoSlug: "acme/repo",
|
|
244
|
+
},
|
|
245
|
+
transport: {
|
|
246
|
+
async fetchRepoIssues(repo, numbers) {
|
|
247
|
+
const map = new Map();
|
|
248
|
+
map.set(numbers[0], {
|
|
249
|
+
repo,
|
|
250
|
+
number: numbers[0],
|
|
251
|
+
exists: true,
|
|
252
|
+
title: "API returns the wrong message",
|
|
253
|
+
state: "OPEN",
|
|
254
|
+
url: `https://github.com/${repo}/issues/${numbers[0]}`,
|
|
255
|
+
checkedAt: "2026-05-04T00:00:00.000Z",
|
|
256
|
+
source: "github",
|
|
257
|
+
});
|
|
258
|
+
return map;
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
expect(result.summary.byCode.open_not_reproduced).toBe(1);
|
|
264
|
+
expect(result.summary.errors).toBe(0);
|
|
265
|
+
expect(result.summary.warnings).toBe(1);
|
|
266
|
+
expect(result.entries[0].status).toBe("open_not_reproduced");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("falls back to warning when sync is unavailable", async () => {
|
|
270
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-regressions-gh-unavailable-"));
|
|
271
|
+
tempDirs.push(tempDir);
|
|
272
|
+
const document = normalizeRegressionCatalogDocument({
|
|
273
|
+
schemaVersion: 1,
|
|
274
|
+
entries: [
|
|
275
|
+
{
|
|
276
|
+
id: "bad-message",
|
|
277
|
+
classification: "product_bug",
|
|
278
|
+
issue: {
|
|
279
|
+
repo: "acme/repo",
|
|
280
|
+
number: 12,
|
|
281
|
+
},
|
|
282
|
+
summary: "API returns the wrong message",
|
|
283
|
+
cause: "The payload is wrong.",
|
|
284
|
+
lastReviewedAt: "2026-05-04",
|
|
285
|
+
fingerprints: [
|
|
286
|
+
{
|
|
287
|
+
service: "api",
|
|
288
|
+
type: "int",
|
|
289
|
+
path: "src/api/routes/__testkit__/failing.int.testkit.ts",
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const result = await validateRegressionIssues({
|
|
297
|
+
productDir: tempDir,
|
|
298
|
+
document,
|
|
299
|
+
statusArtifact: {
|
|
300
|
+
tests: [
|
|
301
|
+
{
|
|
302
|
+
service: "api",
|
|
303
|
+
type: "int",
|
|
304
|
+
path: "src/api/routes/__testkit__/failing.int.testkit.ts",
|
|
305
|
+
status: "failed",
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
config: {
|
|
310
|
+
provider: "github",
|
|
311
|
+
mode: "warn",
|
|
312
|
+
},
|
|
313
|
+
transport: {
|
|
314
|
+
async fetchRepoIssues() {
|
|
315
|
+
throw new Error("boom");
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
expect(result.summary.byCode.validation_unavailable).toBe(1);
|
|
321
|
+
expect(result.summary.errors).toBe(0);
|
|
322
|
+
expect(result.summary.warnings).toBe(1);
|
|
323
|
+
});
|
|
324
|
+
});
|