@elench/testkit 0.1.43 → 0.1.44
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 +12 -0
- package/lib/config/index.mjs +4 -0
- package/lib/known-failures/github.mjs +739 -0
- package/lib/known-failures/github.test.mjs +219 -0
- package/lib/known-failures/index.d.ts +185 -0
- package/lib/known-failures/index.mjs +329 -0
- package/lib/known-failures/index.test.mjs +152 -0
- package/lib/package.test.mjs +5 -0
- package/lib/runner/formatting.mjs +10 -1
- package/lib/runner/formatting.test.mjs +36 -0
- package/lib/runner/metadata.mjs +5 -0
- package/lib/runner/orchestrator.mjs +32 -3
- package/lib/runner/reporting.mjs +2 -2
- package/lib/runner/reporting.test.mjs +2 -2
- package/lib/runner/triage.mjs +20 -196
- package/lib/runner/triage.test.mjs +2 -4
- package/lib/setup/index.d.ts +7 -0
- package/package.json +5 -1
package/lib/runner/triage.mjs
CHANGED
|
@@ -1,35 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
buildKnownFailureFileIdentity,
|
|
3
|
+
findMatchingKnownFailureEntries,
|
|
4
|
+
loadKnownFailuresConfig,
|
|
5
|
+
} from "../known-failures/index.mjs";
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
"expected_failure",
|
|
6
|
-
"infra",
|
|
7
|
-
"product_bug",
|
|
8
|
-
"stale_test",
|
|
9
|
-
"test_bug",
|
|
10
|
-
]);
|
|
11
|
-
const STATES = new Set(["closed", "open"]);
|
|
12
|
-
|
|
13
|
-
export function loadKnownFailuresConfig(productDir, config) {
|
|
14
|
-
const relativePath = config?.knownFailuresFile;
|
|
15
|
-
if (!relativePath) return null;
|
|
16
|
-
|
|
17
|
-
const absolutePath = path.resolve(productDir, relativePath);
|
|
18
|
-
if (!fs.existsSync(absolutePath)) {
|
|
19
|
-
throw new Error(`Known failures file not found: ${relativePath}`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let parsed;
|
|
23
|
-
try {
|
|
24
|
-
parsed = JSON.parse(fs.readFileSync(absolutePath, "utf8"));
|
|
25
|
-
} catch (error) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
`Could not parse known failures file ${relativePath}: ${formatErrorMessage(error)}`
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return normalizeKnownFailuresDocument(parsed, relativePath);
|
|
32
|
-
}
|
|
7
|
+
export { loadKnownFailuresConfig };
|
|
33
8
|
|
|
34
9
|
export function applyKnownFailuresToArtifacts(runArtifact, statusArtifact, knownFailures) {
|
|
35
10
|
if (!knownFailures) return { runArtifact, statusArtifact };
|
|
@@ -39,7 +14,7 @@ export function applyKnownFailuresToArtifacts(runArtifact, statusArtifact, known
|
|
|
39
14
|
const fileSummaries = new Map();
|
|
40
15
|
|
|
41
16
|
for (const entry of [...runEntries, ...statusEntries]) {
|
|
42
|
-
const key =
|
|
17
|
+
const key = buildKnownFailureFileIdentity(entry.service, entry.type, entry.path);
|
|
43
18
|
if (!fileSummaries.has(key)) {
|
|
44
19
|
fileSummaries.set(key, {
|
|
45
20
|
service: entry.service,
|
|
@@ -55,22 +30,25 @@ export function applyKnownFailuresToArtifacts(runArtifact, statusArtifact, known
|
|
|
55
30
|
const matchesByFileKey = new Map();
|
|
56
31
|
const matchedByFailedEntryIds = new Set();
|
|
57
32
|
|
|
58
|
-
for (const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
33
|
+
for (const fileSummary of fileSummaries.values()) {
|
|
34
|
+
const matches = findMatchingKnownFailureEntries(knownFailures, fileSummary);
|
|
35
|
+
if (matches.length === 0) continue;
|
|
36
|
+
|
|
37
|
+
const fileKey = buildKnownFailureFileIdentity(
|
|
38
|
+
fileSummary.service,
|
|
39
|
+
fileSummary.type,
|
|
40
|
+
fileSummary.path
|
|
41
|
+
);
|
|
42
|
+
matchesByFileKey.set(fileKey, matches.map((entry) => toArtifactTriageEntry(entry)));
|
|
43
|
+
if (fileSummary.status === "failed") {
|
|
44
|
+
for (const entry of matches) {
|
|
67
45
|
matchedByFailedEntryIds.add(entry.id);
|
|
68
46
|
}
|
|
69
47
|
}
|
|
70
48
|
}
|
|
71
49
|
|
|
72
50
|
for (const entry of [...runEntries, ...statusEntries]) {
|
|
73
|
-
const fileKey =
|
|
51
|
+
const fileKey = buildKnownFailureFileIdentity(entry.service, entry.type, entry.path);
|
|
74
52
|
const matches = matchesByFileKey.get(fileKey) || [];
|
|
75
53
|
if (matches.length === 0) {
|
|
76
54
|
if (entry.status === "failed") {
|
|
@@ -102,129 +80,6 @@ export function applyKnownFailuresToArtifacts(runArtifact, statusArtifact, known
|
|
|
102
80
|
return { runArtifact, statusArtifact };
|
|
103
81
|
}
|
|
104
82
|
|
|
105
|
-
export function normalizeKnownFailuresDocument(document, relativePath = "known failures file") {
|
|
106
|
-
if (!document || typeof document !== "object") {
|
|
107
|
-
throw new Error(`${relativePath} must contain a JSON object`);
|
|
108
|
-
}
|
|
109
|
-
if (document.schemaVersion !== 1) {
|
|
110
|
-
throw new Error(`${relativePath} schemaVersion must be 1`);
|
|
111
|
-
}
|
|
112
|
-
if (!Array.isArray(document.entries)) {
|
|
113
|
-
throw new Error(`${relativePath} entries must be an array`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const ids = new Set();
|
|
117
|
-
const entries = document.entries.map((entry, index) => {
|
|
118
|
-
const normalized = normalizeKnownFailureEntry(entry, `${relativePath} entries[${index}]`);
|
|
119
|
-
if (ids.has(normalized.id)) {
|
|
120
|
-
throw new Error(`${relativePath} has duplicate entry id "${normalized.id}"`);
|
|
121
|
-
}
|
|
122
|
-
ids.add(normalized.id);
|
|
123
|
-
return normalized;
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
schemaVersion: 1,
|
|
128
|
-
issueRepo: normalizeOptionalString(document.issueRepo),
|
|
129
|
-
entries,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function normalizeKnownFailureEntry(entry, label) {
|
|
134
|
-
if (!entry || typeof entry !== "object") {
|
|
135
|
-
throw new Error(`${label} must be an object`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const id = requireNonEmptyString(entry.id, `${label}.id`);
|
|
139
|
-
const title = requireNonEmptyString(entry.title, `${label}.title`);
|
|
140
|
-
const classification = requireEnumValue(
|
|
141
|
-
entry.classification,
|
|
142
|
-
CLASSIFICATIONS,
|
|
143
|
-
`${label}.classification`
|
|
144
|
-
);
|
|
145
|
-
const state = requireEnumValue(entry.state, STATES, `${label}.state`);
|
|
146
|
-
const description = requireNonEmptyString(entry.description, `${label}.description`);
|
|
147
|
-
const whyFailing = requireNonEmptyString(entry.whyFailing, `${label}.whyFailing`);
|
|
148
|
-
const lastReviewedAt = requireNonEmptyString(entry.lastReviewedAt, `${label}.lastReviewedAt`);
|
|
149
|
-
if (!Array.isArray(entry.matches) || entry.matches.length === 0) {
|
|
150
|
-
throw new Error(`${label}.matches must be a non-empty array`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
id,
|
|
155
|
-
title,
|
|
156
|
-
classification,
|
|
157
|
-
state,
|
|
158
|
-
issue: normalizeKnownFailureIssue(entry.issue, `${label}.issue`),
|
|
159
|
-
description,
|
|
160
|
-
whyFailing,
|
|
161
|
-
lastReviewedAt,
|
|
162
|
-
matches: entry.matches.map((match, index) =>
|
|
163
|
-
normalizeKnownFailureMatch(match, `${label}.matches[${index}]`)
|
|
164
|
-
),
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function normalizeKnownFailureIssue(issue, label) {
|
|
169
|
-
if (!issue || typeof issue !== "object") {
|
|
170
|
-
throw new Error(`${label} must be an object`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const repo = requireNonEmptyString(issue.repo, `${label}.repo`);
|
|
174
|
-
const number = issue.number;
|
|
175
|
-
if (!Number.isInteger(number) || number <= 0) {
|
|
176
|
-
throw new Error(`${label}.number must be a positive integer`);
|
|
177
|
-
}
|
|
178
|
-
const url = requireNonEmptyString(issue.url, `${label}.url`);
|
|
179
|
-
|
|
180
|
-
return { repo, number, url };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function normalizeKnownFailureMatch(match, label) {
|
|
184
|
-
if (!match || typeof match !== "object") {
|
|
185
|
-
throw new Error(`${label} must be an object`);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const pathValue = requireNonEmptyString(match.path, `${label}.path`);
|
|
189
|
-
const normalized = {
|
|
190
|
-
path: pathValue,
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const service = normalizeOptionalString(match.service);
|
|
194
|
-
if (service) normalized.service = service;
|
|
195
|
-
|
|
196
|
-
const type = normalizeOptionalString(match.type);
|
|
197
|
-
if (type) normalized.type = type;
|
|
198
|
-
|
|
199
|
-
const failureKey = normalizeOptionalString(match.failureKey);
|
|
200
|
-
if (failureKey) normalized.failureKey = failureKey;
|
|
201
|
-
|
|
202
|
-
const errorIncludes = normalizeOptionalString(match.errorIncludes);
|
|
203
|
-
if (errorIncludes) normalized.errorIncludes = errorIncludes;
|
|
204
|
-
|
|
205
|
-
return normalized;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function matchesKnownFailureEntry(entry, fileSummary) {
|
|
209
|
-
return entry.matches.some((match) => matchesKnownFailureMatch(match, fileSummary));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function matchesKnownFailureMatch(match, fileSummary) {
|
|
213
|
-
if (match.service && match.service !== fileSummary.service) return false;
|
|
214
|
-
if (match.type && match.type !== fileSummary.type) return false;
|
|
215
|
-
if (match.path !== fileSummary.path) return false;
|
|
216
|
-
if (match.failureKey) {
|
|
217
|
-
const failureKeys = Array.isArray(fileSummary.failureDetails)
|
|
218
|
-
? fileSummary.failureDetails.map((detail) => detail.key)
|
|
219
|
-
: [];
|
|
220
|
-
if (!failureKeys.includes(match.failureKey)) return false;
|
|
221
|
-
}
|
|
222
|
-
if (match.errorIncludes && !String(fileSummary.error || "").includes(match.errorIncludes)) {
|
|
223
|
-
return false;
|
|
224
|
-
}
|
|
225
|
-
return true;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
83
|
function toArtifactTriageEntry(entry) {
|
|
229
84
|
return {
|
|
230
85
|
id: entry.id,
|
|
@@ -297,34 +152,3 @@ function setEntryTriage(entry, triage) {
|
|
|
297
152
|
}
|
|
298
153
|
entry.triage = triage;
|
|
299
154
|
}
|
|
300
|
-
|
|
301
|
-
function buildFileIdentity(service, type, filePath) {
|
|
302
|
-
return `${service}::${type}::${filePath}`;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function requireNonEmptyString(value, label) {
|
|
306
|
-
const normalized = normalizeOptionalString(value);
|
|
307
|
-
if (!normalized) {
|
|
308
|
-
throw new Error(`${label} must be a non-empty string`);
|
|
309
|
-
}
|
|
310
|
-
return normalized;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function requireEnumValue(value, allowed, label) {
|
|
314
|
-
const normalized = requireNonEmptyString(value, label);
|
|
315
|
-
if (!allowed.has(normalized)) {
|
|
316
|
-
throw new Error(`${label} must be one of: ${[...allowed].sort().join(", ")}`);
|
|
317
|
-
}
|
|
318
|
-
return normalized;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function normalizeOptionalString(value) {
|
|
322
|
-
if (typeof value !== "string") return null;
|
|
323
|
-
const normalized = value.trim();
|
|
324
|
-
return normalized.length > 0 ? normalized : null;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function formatErrorMessage(error) {
|
|
328
|
-
if (error instanceof Error) return error.message;
|
|
329
|
-
return String(error);
|
|
330
|
-
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
normalizeKnownFailuresDocument,
|
|
5
|
-
} from "./triage.mjs";
|
|
2
|
+
import { normalizeKnownFailuresDocument } from "../known-failures/index.mjs";
|
|
3
|
+
import { applyKnownFailuresToArtifacts } from "./triage.mjs";
|
|
6
4
|
|
|
7
5
|
describe("runner triage", () => {
|
|
8
6
|
it("matches exact failure keys and enriches both artifacts", () => {
|
package/lib/setup/index.d.ts
CHANGED
|
@@ -59,6 +59,12 @@ export interface TestkitExecutionConfig {
|
|
|
59
59
|
fileTimeoutSeconds?: number;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export interface KnownFailureIssueValidationConfig {
|
|
63
|
+
provider?: "github";
|
|
64
|
+
mode?: "off" | "warn" | "error";
|
|
65
|
+
cacheTtlSeconds?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
62
68
|
export interface ServiceConfig {
|
|
63
69
|
database?: LocalDatabaseConfig;
|
|
64
70
|
databaseFrom?: string;
|
|
@@ -92,6 +98,7 @@ export interface TestkitSetup {
|
|
|
92
98
|
};
|
|
93
99
|
reporting?: {
|
|
94
100
|
knownFailuresFile?: string;
|
|
101
|
+
issueValidation?: KnownFailureIssueValidationConfig;
|
|
95
102
|
};
|
|
96
103
|
services?: Record<string, ServiceConfig>;
|
|
97
104
|
telemetry?: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
4
4
|
"description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./lib/index.d.ts",
|
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
"types": "./lib/runtime/index.d.ts",
|
|
18
18
|
"default": "./lib/runtime/index.mjs"
|
|
19
19
|
},
|
|
20
|
+
"./known-failures": {
|
|
21
|
+
"types": "./lib/known-failures/index.d.ts",
|
|
22
|
+
"default": "./lib/known-failures/index.mjs"
|
|
23
|
+
},
|
|
20
24
|
"./package.json": "./package.json"
|
|
21
25
|
},
|
|
22
26
|
"bin": {
|