@elench/testkit 0.1.150 → 0.1.151
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 +21 -9
- package/lib/cli/assistant/view-model.mjs +1 -1
- package/lib/cli/components/blocks/run-tree.mjs +1 -1
- package/lib/cli/renderers/run/events.mjs +4 -3
- package/lib/cli/renderers/run/failure.mjs +2 -2
- package/lib/cli/renderers/run/inline-detail.mjs +2 -2
- package/lib/cli/renderers/run/interactive.mjs +2 -2
- package/lib/cli/renderers/run/text-reporter.mjs +9 -9
- package/lib/cli/state/run/model.mjs +7 -7
- package/lib/cli/state/run/state.mjs +3 -3
- package/lib/cli/terminal/colors.mjs +1 -1
- package/lib/config/runtime.mjs +130 -0
- package/lib/config-api/index.d.ts +22 -0
- package/lib/database/cleanup.mjs +76 -1
- package/lib/database/index.mjs +6 -0
- package/lib/database/local-postgres.mjs +95 -4
- package/lib/database/naming.mjs +7 -0
- package/lib/database/state-files.mjs +12 -0
- package/lib/docker-compat/matrix.mjs +5 -3
- package/lib/kiln/client.mjs +8 -0
- package/lib/local/kiln-driver.mjs +96 -69
- package/lib/ownership/docker.mjs +67 -1
- package/lib/regressions/github-transport.mjs +178 -4
- package/lib/regressions/github.mjs +52 -16
- package/lib/regressions/index.d.ts +56 -28
- package/lib/regressions/index.mjs +122 -47
- package/lib/regressions/workflow.mjs +266 -0
- package/lib/results/artifacts.mjs +8 -7
- package/lib/runner/formatting.mjs +17 -16
- package/lib/runner/orchestrator.mjs +5 -4
- package/lib/runner/regressions.mjs +175 -33
- package/lib/runner/run-finalization.mjs +34 -4
- 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/node_modules/es-toolkit/CHANGELOG.md +801 -0
- package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +1 -0
- package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +3 -0
- package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +4 -0
- package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +4 -0
- package/node_modules/esprima/ChangeLog +235 -0
- package/package.json +6 -5
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +0 -188
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +0 -1
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +0 -293
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +0 -1
- package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +0 -25
|
@@ -19,13 +19,26 @@ export function parseGitHubRepoSlug(remoteUrl) {
|
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export async function createDefaultGitHubIssueTransport(env = process.env) {
|
|
22
|
+
export async function createDefaultGitHubIssueTransport(env = process.env, options = {}) {
|
|
23
23
|
const token = env.GH_TOKEN || env.GITHUB_TOKEN || null;
|
|
24
|
+
const apiBaseUrl = normalizeOptionalString(options.apiBaseUrl || env.TESTKIT_GITHUB_API_URL) || "https://api.github.com";
|
|
24
25
|
if (token) {
|
|
25
26
|
return {
|
|
26
27
|
type: "token",
|
|
27
28
|
async fetchRepoIssues(repo, numbers) {
|
|
28
|
-
return fetchRepoIssuesViaToken(repo, numbers, token);
|
|
29
|
+
return fetchRepoIssuesViaToken(repo, numbers, token, { apiBaseUrl });
|
|
30
|
+
},
|
|
31
|
+
async fetchOpenBugIssues(repo, options = {}) {
|
|
32
|
+
return fetchOpenBugIssuesViaRest(repo, token, { apiBaseUrl, ...options });
|
|
33
|
+
},
|
|
34
|
+
async createIssue(repo, input) {
|
|
35
|
+
return createIssueViaRest(repo, input, token, { apiBaseUrl });
|
|
36
|
+
},
|
|
37
|
+
async updateIssueState(repo, number, state) {
|
|
38
|
+
return updateIssueStateViaRest(repo, number, state, token, { apiBaseUrl });
|
|
39
|
+
},
|
|
40
|
+
async createIssueComment(repo, number, body) {
|
|
41
|
+
return createIssueCommentViaRest(repo, number, body, token, { apiBaseUrl });
|
|
29
42
|
},
|
|
30
43
|
};
|
|
31
44
|
}
|
|
@@ -41,6 +54,18 @@ export async function createDefaultGitHubIssueTransport(env = process.env) {
|
|
|
41
54
|
async fetchRepoIssues(repo, numbers) {
|
|
42
55
|
return fetchRepoIssuesViaGh(repo, numbers, env);
|
|
43
56
|
},
|
|
57
|
+
async fetchOpenBugIssues(repo, options = {}) {
|
|
58
|
+
return fetchOpenBugIssuesViaGh(repo, options, env);
|
|
59
|
+
},
|
|
60
|
+
async createIssue(repo, input) {
|
|
61
|
+
return createIssueViaGh(repo, input, env);
|
|
62
|
+
},
|
|
63
|
+
async updateIssueState(repo, number, state) {
|
|
64
|
+
return updateIssueStateViaGh(repo, number, state, env);
|
|
65
|
+
},
|
|
66
|
+
async createIssueComment(repo, number, body) {
|
|
67
|
+
return createIssueCommentViaGh(repo, number, body, env);
|
|
68
|
+
},
|
|
44
69
|
};
|
|
45
70
|
} catch {
|
|
46
71
|
return null;
|
|
@@ -65,9 +90,9 @@ export async function fetchIssuesByRepo(client, issueNumbersByRepo) {
|
|
|
65
90
|
return issuesByRepo;
|
|
66
91
|
}
|
|
67
92
|
|
|
68
|
-
async function fetchRepoIssuesViaToken(repo, numbers, token) {
|
|
93
|
+
async function fetchRepoIssuesViaToken(repo, numbers, token, { apiBaseUrl = "https://api.github.com" } = {}) {
|
|
69
94
|
const query = buildIssueQuery(repo, numbers);
|
|
70
|
-
const response = await fetch("
|
|
95
|
+
const response = await fetch(`${apiBaseUrl.replace(/\/+$/u, "")}/graphql`, {
|
|
71
96
|
method: "POST",
|
|
72
97
|
headers: {
|
|
73
98
|
"Content-Type": "application/json",
|
|
@@ -90,6 +115,61 @@ async function fetchRepoIssuesViaToken(repo, numbers, token) {
|
|
|
90
115
|
return normalizeGraphqlIssuesResponse(repo, numbers, payload?.data);
|
|
91
116
|
}
|
|
92
117
|
|
|
118
|
+
async function fetchOpenBugIssuesViaRest(repo, token, { apiBaseUrl = "https://api.github.com", labels = ["bug"] } = {}) {
|
|
119
|
+
const labelQuery = labels.length > 0 ? `&labels=${encodeURIComponent(labels.join(","))}` : "";
|
|
120
|
+
const response = await fetch(`${apiBaseUrl.replace(/\/+$/u, "")}/repos/${repo}/issues?state=open${labelQuery}`, {
|
|
121
|
+
headers: githubRestHeaders(token),
|
|
122
|
+
});
|
|
123
|
+
const payload = await response.json().catch(() => null);
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
throw new Error(`GitHub issue list failed with ${response.status}${payload?.message ? `: ${payload.message}` : ""}`);
|
|
126
|
+
}
|
|
127
|
+
return normalizeRestIssues(repo, Array.isArray(payload) ? payload : []);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function createIssueViaRest(repo, input, token, { apiBaseUrl = "https://api.github.com" } = {}) {
|
|
131
|
+
const response = await fetch(`${apiBaseUrl.replace(/\/+$/u, "")}/repos/${repo}/issues`, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: githubRestHeaders(token),
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
title: input.title,
|
|
136
|
+
body: input.body,
|
|
137
|
+
labels: input.labels || [],
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
const payload = await response.json().catch(() => null);
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
throw new Error(`GitHub issue create failed with ${response.status}${payload?.message ? `: ${payload.message}` : ""}`);
|
|
143
|
+
}
|
|
144
|
+
return normalizeRestIssue(repo, payload);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function updateIssueStateViaRest(repo, number, state, token, { apiBaseUrl = "https://api.github.com" } = {}) {
|
|
148
|
+
const response = await fetch(`${apiBaseUrl.replace(/\/+$/u, "")}/repos/${repo}/issues/${number}`, {
|
|
149
|
+
method: "PATCH",
|
|
150
|
+
headers: githubRestHeaders(token),
|
|
151
|
+
body: JSON.stringify({ state }),
|
|
152
|
+
});
|
|
153
|
+
const payload = await response.json().catch(() => null);
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
throw new Error(`GitHub issue update failed with ${response.status}${payload?.message ? `: ${payload.message}` : ""}`);
|
|
156
|
+
}
|
|
157
|
+
return normalizeRestIssue(repo, payload);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function createIssueCommentViaRest(repo, number, body, token, { apiBaseUrl = "https://api.github.com" } = {}) {
|
|
161
|
+
const response = await fetch(`${apiBaseUrl.replace(/\/+$/u, "")}/repos/${repo}/issues/${number}/comments`, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: githubRestHeaders(token),
|
|
164
|
+
body: JSON.stringify({ body }),
|
|
165
|
+
});
|
|
166
|
+
const payload = await response.json().catch(() => null);
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
throw new Error(`GitHub issue comment failed with ${response.status}${payload?.message ? `: ${payload.message}` : ""}`);
|
|
169
|
+
}
|
|
170
|
+
return payload;
|
|
171
|
+
}
|
|
172
|
+
|
|
93
173
|
async function fetchRepoIssuesViaGh(repo, numbers, env) {
|
|
94
174
|
const query = buildIssueQuery(repo, numbers);
|
|
95
175
|
const { stdout } = await execFileAsync("gh", ["api", "graphql", "-f", `query=${query}`], {
|
|
@@ -104,6 +184,65 @@ async function fetchRepoIssuesViaGh(repo, numbers, env) {
|
|
|
104
184
|
return normalizeGraphqlIssuesResponse(repo, numbers, payload?.data);
|
|
105
185
|
}
|
|
106
186
|
|
|
187
|
+
async function fetchOpenBugIssuesViaGh(repo, { labels = ["bug"] } = {}, env) {
|
|
188
|
+
const args = ["issue", "list", "--repo", repo, "--state", "open", "--json", "number,title,state,url,labels"];
|
|
189
|
+
for (const label of labels) args.push("--label", label);
|
|
190
|
+
const { stdout } = await execFileAsync("gh", args, {
|
|
191
|
+
encoding: "utf8",
|
|
192
|
+
env,
|
|
193
|
+
maxBuffer: 1024 * 1024,
|
|
194
|
+
});
|
|
195
|
+
return normalizeRestIssues(repo, JSON.parse(stdout || "[]"));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function createIssueViaGh(repo, input, env) {
|
|
199
|
+
const args = ["issue", "create", "--repo", repo, "--title", input.title, "--body", input.body || ""];
|
|
200
|
+
for (const label of input.labels || []) args.push("--label", label);
|
|
201
|
+
const { stdout } = await execFileAsync("gh", args, {
|
|
202
|
+
encoding: "utf8",
|
|
203
|
+
env,
|
|
204
|
+
maxBuffer: 1024 * 1024,
|
|
205
|
+
});
|
|
206
|
+
const url = stdout.trim();
|
|
207
|
+
const number = Number(url.match(/\/issues\/(\d+)$/u)?.[1]);
|
|
208
|
+
return {
|
|
209
|
+
repo,
|
|
210
|
+
number,
|
|
211
|
+
exists: Number.isInteger(number),
|
|
212
|
+
title: input.title,
|
|
213
|
+
state: "OPEN",
|
|
214
|
+
url,
|
|
215
|
+
labels: input.labels || [],
|
|
216
|
+
source: "github",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function updateIssueStateViaGh(repo, number, state, env) {
|
|
221
|
+
await execFileAsync("gh", ["issue", state === "open" ? "reopen" : "close", String(number), "--repo", repo], {
|
|
222
|
+
encoding: "utf8",
|
|
223
|
+
env,
|
|
224
|
+
maxBuffer: 1024 * 1024,
|
|
225
|
+
});
|
|
226
|
+
return {
|
|
227
|
+
repo,
|
|
228
|
+
number,
|
|
229
|
+
exists: true,
|
|
230
|
+
title: null,
|
|
231
|
+
state: state.toUpperCase(),
|
|
232
|
+
url: `https://github.com/${repo}/issues/${number}`,
|
|
233
|
+
source: "github",
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function createIssueCommentViaGh(repo, number, body, env) {
|
|
238
|
+
await execFileAsync("gh", ["issue", "comment", String(number), "--repo", repo, "--body", body], {
|
|
239
|
+
encoding: "utf8",
|
|
240
|
+
env,
|
|
241
|
+
maxBuffer: 1024 * 1024,
|
|
242
|
+
});
|
|
243
|
+
return { ok: true };
|
|
244
|
+
}
|
|
245
|
+
|
|
107
246
|
function buildIssueQuery(repo, numbers) {
|
|
108
247
|
const [owner, name] = repo.split("/");
|
|
109
248
|
return `
|
|
@@ -159,6 +298,41 @@ function normalizeGraphqlIssuesResponse(repo, numbers, data) {
|
|
|
159
298
|
return map;
|
|
160
299
|
}
|
|
161
300
|
|
|
301
|
+
function normalizeRestIssues(repo, issues) {
|
|
302
|
+
const map = new Map();
|
|
303
|
+
for (const issue of issues) {
|
|
304
|
+
const normalized = normalizeRestIssue(repo, issue);
|
|
305
|
+
if (normalized?.number) map.set(normalized.number, normalized);
|
|
306
|
+
}
|
|
307
|
+
return map;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function normalizeRestIssue(repo, issue) {
|
|
311
|
+
if (!issue || typeof issue !== "object") return null;
|
|
312
|
+
return {
|
|
313
|
+
repo,
|
|
314
|
+
number: Number(issue.number),
|
|
315
|
+
exists: true,
|
|
316
|
+
title: normalizeOptionalString(issue.title),
|
|
317
|
+
state: normalizeOptionalString(issue.state)?.toUpperCase() || null,
|
|
318
|
+
url: normalizeOptionalString(issue.html_url || issue.url),
|
|
319
|
+
labels: Array.isArray(issue.labels)
|
|
320
|
+
? issue.labels.map((label) => typeof label === "string" ? label : label?.name).filter(Boolean)
|
|
321
|
+
: [],
|
|
322
|
+
checkedAt: null,
|
|
323
|
+
source: "github",
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function githubRestHeaders(token) {
|
|
328
|
+
return {
|
|
329
|
+
"Content-Type": "application/json",
|
|
330
|
+
Accept: "application/vnd.github+json",
|
|
331
|
+
Authorization: `Bearer ${token}`,
|
|
332
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
162
336
|
function normalizeOptionalString(value) {
|
|
163
337
|
if (typeof value !== "string") return null;
|
|
164
338
|
const normalized = value.trim();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { findMatchingRegressionCases } from "./index.mjs";
|
|
2
2
|
import {
|
|
3
3
|
applyStaleCacheFallback,
|
|
4
4
|
loadIssueCache,
|
|
@@ -40,11 +40,13 @@ export function normalizeRegressionSyncConfig(value) {
|
|
|
40
40
|
value.cacheTtlSeconds == null
|
|
41
41
|
? DEFAULT_CACHE_TTL_SECONDS
|
|
42
42
|
: normalizePositiveInteger(value.cacheTtlSeconds, "testkit.config.ts regressions.sync.cacheTtlSeconds");
|
|
43
|
+
const apiBaseUrl = normalizeOptionalString(value.apiBaseUrl);
|
|
43
44
|
|
|
44
45
|
return {
|
|
45
46
|
provider,
|
|
46
47
|
mode,
|
|
47
48
|
cacheTtlSeconds,
|
|
49
|
+
apiBaseUrl,
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -62,7 +64,8 @@ export async function validateRegressionIssues({
|
|
|
62
64
|
if (!document || !normalizedConfig || normalizedConfig.mode === "off") return null;
|
|
63
65
|
|
|
64
66
|
const checks = collectObservedRegressionEntries(document, runArtifact, statusArtifact);
|
|
65
|
-
const
|
|
67
|
+
const cases = document.cases || [];
|
|
68
|
+
const issueNumbersByRepo = groupIssueNumbersByRepo(cases);
|
|
66
69
|
const repoSlug = gitMetadata?.repoSlug || null;
|
|
67
70
|
const remoteUrl = gitMetadata?.remoteUrl || null;
|
|
68
71
|
const cache = loadIssueCache(productDir, CACHE_PATH, CACHE_SCHEMA_VERSION);
|
|
@@ -71,7 +74,9 @@ export async function validateRegressionIssues({
|
|
|
71
74
|
let issuesByRepo = cacheResolution.issuesByRepo;
|
|
72
75
|
|
|
73
76
|
if (cacheResolution.missingByRepo.size > 0) {
|
|
74
|
-
const client = transport || (await createDefaultGitHubIssueTransport(
|
|
77
|
+
const client = transport || (await createDefaultGitHubIssueTransport(process.env, {
|
|
78
|
+
apiBaseUrl: normalizedConfig.apiBaseUrl,
|
|
79
|
+
}));
|
|
75
80
|
if (client) {
|
|
76
81
|
try {
|
|
77
82
|
const fetchedIssues = await fetchIssuesByRepo(client, cacheResolution.missingByRepo);
|
|
@@ -105,9 +110,11 @@ export async function validateRegressionIssues({
|
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
const
|
|
113
|
+
const caseResults = cases.map((entry) => {
|
|
109
114
|
const observed = checks.get(entry.id) || createObservedCheck(entry);
|
|
110
|
-
const issueData =
|
|
115
|
+
const issueData = entry.issue
|
|
116
|
+
? issuesByRepo.get(entry.issue.repo)?.get(entry.issue.number) || null
|
|
117
|
+
: null;
|
|
111
118
|
return buildIssueValidationEntry({ entry, observed, issueData });
|
|
112
119
|
});
|
|
113
120
|
|
|
@@ -117,12 +124,12 @@ export async function validateRegressionIssues({
|
|
|
117
124
|
code: "detected_repo_mismatch",
|
|
118
125
|
severity: normalizedConfig.mode === "error" ? "error" : "warning",
|
|
119
126
|
message:
|
|
120
|
-
`Regression
|
|
127
|
+
`Regression case store issueRepo ${document.issueRepo} does not match detected repo ${repoSlug}` +
|
|
121
128
|
(remoteUrl ? ` (${remoteUrl})` : ""),
|
|
122
129
|
});
|
|
123
130
|
}
|
|
124
131
|
|
|
125
|
-
const summary = buildIssueValidationSummary(
|
|
132
|
+
const summary = buildIssueValidationSummary(caseResults, globalFindings);
|
|
126
133
|
return {
|
|
127
134
|
schemaVersion: 2,
|
|
128
135
|
provider: "github",
|
|
@@ -139,7 +146,7 @@ export async function validateRegressionIssues({
|
|
|
139
146
|
},
|
|
140
147
|
findings: globalFindings,
|
|
141
148
|
summary,
|
|
142
|
-
|
|
149
|
+
cases: caseResults,
|
|
143
150
|
};
|
|
144
151
|
}
|
|
145
152
|
|
|
@@ -182,17 +189,18 @@ export function buildRegressionSyncSummaryLines(result) {
|
|
|
182
189
|
if (parts.length === 0) return [];
|
|
183
190
|
return [
|
|
184
191
|
"",
|
|
185
|
-
"
|
|
192
|
+
"Case store issues:",
|
|
186
193
|
...parts.map((part) => ` - ${part}`),
|
|
187
194
|
];
|
|
188
195
|
}
|
|
189
196
|
|
|
190
197
|
function collectObservedRegressionEntries(document, runArtifact, statusArtifact) {
|
|
191
198
|
const observedTests = collectObservedTests(runArtifact, statusArtifact);
|
|
192
|
-
const
|
|
199
|
+
const cases = document.cases || [];
|
|
200
|
+
const checks = new Map(cases.map((entry) => [entry.id, createObservedCheck(entry)]));
|
|
193
201
|
|
|
194
202
|
for (const test of observedTests) {
|
|
195
|
-
const matchedEntries =
|
|
203
|
+
const matchedEntries = findMatchingRegressionCases(document, test);
|
|
196
204
|
for (const entry of matchedEntries) {
|
|
197
205
|
const observed = checks.get(entry.id) || createObservedCheck(entry);
|
|
198
206
|
observed.matchedTests += 1;
|
|
@@ -252,6 +260,7 @@ function createObservedCheck(entry) {
|
|
|
252
260
|
function groupIssueNumbersByRepo(entries) {
|
|
253
261
|
const map = new Map();
|
|
254
262
|
for (const entry of entries) {
|
|
263
|
+
if (!entry.issue) continue;
|
|
255
264
|
if (!map.has(entry.issue.repo)) {
|
|
256
265
|
map.set(entry.issue.repo, []);
|
|
257
266
|
}
|
|
@@ -271,11 +280,38 @@ function buildIssueValidationEntry({ entry, observed, issueData }) {
|
|
|
271
280
|
const findings = [];
|
|
272
281
|
const normalizedIssueState = normalizeIssueState(issueData?.state);
|
|
273
282
|
|
|
283
|
+
if (!entry.issue) {
|
|
284
|
+
return {
|
|
285
|
+
id: entry.id,
|
|
286
|
+
summary: entry.summary,
|
|
287
|
+
issue: null,
|
|
288
|
+
observed: {
|
|
289
|
+
matchedTests: observed.matchedTests,
|
|
290
|
+
executedTests: observed.executedTests,
|
|
291
|
+
passedTests: observed.passedTests,
|
|
292
|
+
failedTests: observed.failedTests,
|
|
293
|
+
skippedTests: observed.skippedTests,
|
|
294
|
+
notRunTests: observed.notRunTests,
|
|
295
|
+
reproduced: observed.failedTests > 0,
|
|
296
|
+
},
|
|
297
|
+
github: {
|
|
298
|
+
exists: null,
|
|
299
|
+
title: null,
|
|
300
|
+
state: null,
|
|
301
|
+
url: null,
|
|
302
|
+
checkedAt: null,
|
|
303
|
+
cached: false,
|
|
304
|
+
},
|
|
305
|
+
findings,
|
|
306
|
+
status: observed.matchedTests > 0 ? "unlinked" : "not_observed",
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
274
310
|
if (issueData && issueData.exists === false) {
|
|
275
311
|
findings.push({
|
|
276
312
|
code: "issue_missing",
|
|
277
313
|
severity: "error",
|
|
278
|
-
message: `Regression
|
|
314
|
+
message: `Regression case ${entry.id} references missing issue #${entry.issue.number} in ${entry.issue.repo}`,
|
|
279
315
|
});
|
|
280
316
|
}
|
|
281
317
|
|
|
@@ -369,7 +405,7 @@ function resolveIssueValidationStatus(issueData, observed) {
|
|
|
369
405
|
return "not_observed";
|
|
370
406
|
}
|
|
371
407
|
|
|
372
|
-
function buildIssueValidationSummary(
|
|
408
|
+
function buildIssueValidationSummary(caseResults, globalFindings) {
|
|
373
409
|
const byCode = {};
|
|
374
410
|
const byStatus = {};
|
|
375
411
|
let errors = 0;
|
|
@@ -381,7 +417,7 @@ function buildIssueValidationSummary(entries, globalFindings) {
|
|
|
381
417
|
if (finding.severity === "warning") warnings += 1;
|
|
382
418
|
}
|
|
383
419
|
|
|
384
|
-
for (const entry of
|
|
420
|
+
for (const entry of caseResults) {
|
|
385
421
|
byStatus[entry.status] = (byStatus[entry.status] || 0) + 1;
|
|
386
422
|
for (const finding of entry.findings) {
|
|
387
423
|
byCode[finding.code] = (byCode[finding.code] || 0) + 1;
|
|
@@ -391,8 +427,8 @@ function buildIssueValidationSummary(entries, globalFindings) {
|
|
|
391
427
|
}
|
|
392
428
|
|
|
393
429
|
return {
|
|
394
|
-
|
|
395
|
-
|
|
430
|
+
cases: caseResults.length,
|
|
431
|
+
observedCases: caseResults.filter((entry) => entry.observed.matchedTests > 0).length,
|
|
396
432
|
errors,
|
|
397
433
|
warnings,
|
|
398
434
|
byCode,
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
export type RegressionClassification =
|
|
2
2
|
| "expected_failure"
|
|
3
|
+
| "flaky"
|
|
4
|
+
| "harness_bug"
|
|
3
5
|
| "infra"
|
|
4
6
|
| "product_bug"
|
|
5
7
|
| "stale_test"
|
|
6
8
|
| "test_bug";
|
|
7
9
|
|
|
10
|
+
export type RegressionCaseState =
|
|
11
|
+
| "active"
|
|
12
|
+
| "coverage_missing"
|
|
13
|
+
| "fixed_pending_verification"
|
|
14
|
+
| "fix_claimed"
|
|
15
|
+
| "resolved"
|
|
16
|
+
| "untriaged";
|
|
17
|
+
|
|
8
18
|
export interface RegressionIssueRef {
|
|
9
19
|
repo: string;
|
|
10
20
|
number: number;
|
|
@@ -19,27 +29,31 @@ export interface RegressionFingerprint {
|
|
|
19
29
|
errorIncludes?: string;
|
|
20
30
|
}
|
|
21
31
|
|
|
22
|
-
export interface
|
|
32
|
+
export interface RegressionCase {
|
|
23
33
|
id: string;
|
|
34
|
+
state: RegressionCaseState;
|
|
24
35
|
classification: RegressionClassification;
|
|
25
|
-
|
|
36
|
+
owner?: string;
|
|
37
|
+
issue: RegressionIssueRef | null;
|
|
26
38
|
summary: string;
|
|
27
39
|
cause: string;
|
|
28
40
|
lastReviewedAt: string;
|
|
29
41
|
fingerprints: RegressionFingerprint[];
|
|
42
|
+
lifecycle: Record<string, unknown>;
|
|
43
|
+
coverage: Record<string, unknown>;
|
|
30
44
|
}
|
|
31
45
|
|
|
32
|
-
export interface
|
|
33
|
-
schemaVersion:
|
|
46
|
+
export interface RegressionCaseStoreDocument {
|
|
47
|
+
schemaVersion: 2;
|
|
34
48
|
issueRepo?: string | null;
|
|
35
|
-
|
|
49
|
+
cases: RegressionCase[];
|
|
36
50
|
}
|
|
37
51
|
|
|
38
|
-
export interface
|
|
52
|
+
export interface RegressionCaseStoreValidationResult {
|
|
39
53
|
errors: string[];
|
|
40
54
|
warnings: string[];
|
|
41
55
|
stats: {
|
|
42
|
-
|
|
56
|
+
cases: number;
|
|
43
57
|
fingerprints: number;
|
|
44
58
|
failedTests: number;
|
|
45
59
|
diagnosedFailedTests: number;
|
|
@@ -51,6 +65,7 @@ export interface RegressionSyncConfig {
|
|
|
51
65
|
provider?: "github";
|
|
52
66
|
mode?: "off" | "warn" | "error";
|
|
53
67
|
cacheTtlSeconds?: number;
|
|
68
|
+
apiBaseUrl?: string;
|
|
54
69
|
}
|
|
55
70
|
|
|
56
71
|
export interface RegressionSyncFinding {
|
|
@@ -62,7 +77,7 @@ export interface RegressionSyncFinding {
|
|
|
62
77
|
export interface RegressionSyncEntry {
|
|
63
78
|
id: string;
|
|
64
79
|
summary: string;
|
|
65
|
-
issue: RegressionIssueRef;
|
|
80
|
+
issue: RegressionIssueRef | null;
|
|
66
81
|
observed: {
|
|
67
82
|
matchedTests: number;
|
|
68
83
|
executedTests: number;
|
|
@@ -100,41 +115,52 @@ export interface RegressionSyncResult {
|
|
|
100
115
|
};
|
|
101
116
|
findings: RegressionSyncFinding[];
|
|
102
117
|
summary: {
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
cases: number;
|
|
119
|
+
observedCases: number;
|
|
105
120
|
errors: number;
|
|
106
121
|
warnings: number;
|
|
107
122
|
byCode: Record<string, number>;
|
|
108
123
|
byStatus: Record<string, number>;
|
|
109
124
|
};
|
|
110
|
-
|
|
125
|
+
cases: RegressionSyncEntry[];
|
|
111
126
|
}
|
|
112
127
|
|
|
113
|
-
export declare function
|
|
128
|
+
export declare function loadRegressionCaseStoreConfig(
|
|
114
129
|
productDir: string,
|
|
115
130
|
config: { file?: string | null } | null
|
|
116
|
-
):
|
|
117
|
-
export declare function
|
|
131
|
+
): RegressionCaseStoreDocument | null;
|
|
132
|
+
export declare function loadRegressionCaseStoreDocument(
|
|
118
133
|
filePath: string,
|
|
119
134
|
relativePath?: string
|
|
120
|
-
):
|
|
121
|
-
export declare function
|
|
135
|
+
): RegressionCaseStoreDocument;
|
|
136
|
+
export declare function writeRegressionCaseStoreDocument(
|
|
137
|
+
filePath: string,
|
|
138
|
+
document: RegressionCaseStoreDocument
|
|
139
|
+
): void;
|
|
140
|
+
export declare function serializeRegressionCaseStore(
|
|
141
|
+
document: RegressionCaseStoreDocument
|
|
142
|
+
): {
|
|
143
|
+
schemaVersion: 2;
|
|
144
|
+
issueRepo?: string | null;
|
|
145
|
+
cases: RegressionCase[];
|
|
146
|
+
};
|
|
147
|
+
export declare function normalizeRegressionCaseStoreDocument(
|
|
122
148
|
document: unknown,
|
|
123
149
|
relativePath?: string
|
|
124
|
-
):
|
|
125
|
-
export declare function
|
|
126
|
-
document:
|
|
150
|
+
): RegressionCaseStoreDocument;
|
|
151
|
+
export declare function validateRegressionCaseStoreDocument(
|
|
152
|
+
document: RegressionCaseStoreDocument,
|
|
127
153
|
options?: {
|
|
128
154
|
productDir?: string;
|
|
129
155
|
statusArtifactPath?: string;
|
|
130
156
|
statusArtifact?: unknown;
|
|
131
157
|
}
|
|
132
|
-
):
|
|
133
|
-
export declare function
|
|
134
|
-
document:
|
|
158
|
+
): RegressionCaseStoreValidationResult;
|
|
159
|
+
export declare function renderRegressionCaseStoreMarkdown(
|
|
160
|
+
document: RegressionCaseStoreDocument
|
|
135
161
|
): string;
|
|
136
|
-
export declare function
|
|
137
|
-
document:
|
|
162
|
+
export declare function findMatchingRegressionCases(
|
|
163
|
+
document: RegressionCaseStoreDocument,
|
|
138
164
|
fileSummary: {
|
|
139
165
|
service?: string;
|
|
140
166
|
type?: string;
|
|
@@ -143,9 +169,9 @@ export declare function findMatchingRegressionEntries(
|
|
|
143
169
|
error?: string | null;
|
|
144
170
|
failureDetails?: Array<{ key?: string; title?: string }>;
|
|
145
171
|
}
|
|
146
|
-
):
|
|
147
|
-
export declare function
|
|
148
|
-
entry:
|
|
172
|
+
): RegressionCase[];
|
|
173
|
+
export declare function matchesRegressionCase(
|
|
174
|
+
entry: RegressionCase,
|
|
149
175
|
fileSummary: {
|
|
150
176
|
service?: string;
|
|
151
177
|
type?: string;
|
|
@@ -159,6 +185,7 @@ export declare function buildRegressionFileIdentity(
|
|
|
159
185
|
type: string,
|
|
160
186
|
filePath: string
|
|
161
187
|
): string;
|
|
188
|
+
export declare function issueRefToString(issue: RegressionIssueRef | null): string | null;
|
|
162
189
|
|
|
163
190
|
export declare function normalizeRegressionSyncConfig(
|
|
164
191
|
value: unknown
|
|
@@ -166,10 +193,11 @@ export declare function normalizeRegressionSyncConfig(
|
|
|
166
193
|
provider: "github";
|
|
167
194
|
mode: "off" | "warn" | "error";
|
|
168
195
|
cacheTtlSeconds: number;
|
|
196
|
+
apiBaseUrl: string | null;
|
|
169
197
|
} | null;
|
|
170
198
|
export declare function validateRegressionIssues(options: {
|
|
171
199
|
productDir: string;
|
|
172
|
-
document:
|
|
200
|
+
document: RegressionCaseStoreDocument | null;
|
|
173
201
|
runArtifact?: unknown;
|
|
174
202
|
statusArtifact?: unknown;
|
|
175
203
|
config?: RegressionSyncConfig | null;
|