@elench/testkit 0.1.43 → 0.1.45

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.
@@ -1,35 +1,10 @@
1
- import fs from "fs";
2
- import path from "path";
1
+ import {
2
+ buildKnownFailureFileIdentity,
3
+ findMatchingKnownFailureEntries,
4
+ loadKnownFailuresConfig,
5
+ } from "../known-failures/index.mjs";
3
6
 
4
- const CLASSIFICATIONS = new Set([
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 = buildFileIdentity(entry.service, entry.type, entry.path);
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 entry of knownFailures.entries) {
59
- for (const fileSummary of fileSummaries.values()) {
60
- if (!matchesKnownFailureEntry(entry, fileSummary)) continue;
61
- const fileKey = buildFileIdentity(fileSummary.service, fileSummary.type, fileSummary.path);
62
- if (!matchesByFileKey.has(fileKey)) {
63
- matchesByFileKey.set(fileKey, []);
64
- }
65
- matchesByFileKey.get(fileKey).push(toArtifactTriageEntry(entry));
66
- if (fileSummary.status === "failed") {
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 = buildFileIdentity(entry.service, entry.type, entry.path);
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
- applyKnownFailuresToArtifacts,
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", () => {
@@ -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.43",
3
+ "version": "0.1.45",
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": {