@elench/testkit 0.1.80 → 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.
- package/README.md +50 -35
- 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/index.d.ts +4 -4
- 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/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/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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
export type RegressionClassification =
|
|
2
|
+
| "expected_failure"
|
|
3
|
+
| "infra"
|
|
4
|
+
| "product_bug"
|
|
5
|
+
| "stale_test"
|
|
6
|
+
| "test_bug";
|
|
7
|
+
|
|
8
|
+
export interface RegressionIssueRef {
|
|
9
|
+
repo: string;
|
|
10
|
+
number: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RegressionFingerprint {
|
|
14
|
+
service?: string;
|
|
15
|
+
type?: string;
|
|
16
|
+
path: string;
|
|
17
|
+
failureKey?: string;
|
|
18
|
+
errorIncludes?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RegressionCatalogEntry {
|
|
22
|
+
id: string;
|
|
23
|
+
classification: RegressionClassification;
|
|
24
|
+
issue: RegressionIssueRef;
|
|
25
|
+
summary: string;
|
|
26
|
+
cause: string;
|
|
27
|
+
lastReviewedAt: string;
|
|
28
|
+
fingerprints: RegressionFingerprint[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RegressionCatalogDocument {
|
|
32
|
+
schemaVersion: 1;
|
|
33
|
+
issueRepo?: string | null;
|
|
34
|
+
entries: RegressionCatalogEntry[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RegressionCatalogValidationResult {
|
|
38
|
+
errors: string[];
|
|
39
|
+
warnings: string[];
|
|
40
|
+
stats: {
|
|
41
|
+
entries: number;
|
|
42
|
+
fingerprints: number;
|
|
43
|
+
failedTests: number;
|
|
44
|
+
diagnosedFailedTests: number;
|
|
45
|
+
newFailedTests: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RegressionSyncConfig {
|
|
50
|
+
provider?: "github";
|
|
51
|
+
mode?: "off" | "warn" | "error";
|
|
52
|
+
cacheTtlSeconds?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RegressionSyncFinding {
|
|
56
|
+
code: string;
|
|
57
|
+
severity: "warning" | "error";
|
|
58
|
+
message: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RegressionSyncEntry {
|
|
62
|
+
id: string;
|
|
63
|
+
summary: string;
|
|
64
|
+
issue: RegressionIssueRef;
|
|
65
|
+
observed: {
|
|
66
|
+
matchedTests: number;
|
|
67
|
+
executedTests: number;
|
|
68
|
+
passedTests: number;
|
|
69
|
+
failedTests: number;
|
|
70
|
+
skippedTests: number;
|
|
71
|
+
notRunTests: number;
|
|
72
|
+
reproduced: boolean;
|
|
73
|
+
};
|
|
74
|
+
github: {
|
|
75
|
+
exists: boolean | null;
|
|
76
|
+
title: string | null;
|
|
77
|
+
state: string | null;
|
|
78
|
+
url: string | null;
|
|
79
|
+
checkedAt: string | null;
|
|
80
|
+
cached: boolean;
|
|
81
|
+
};
|
|
82
|
+
findings: RegressionSyncFinding[];
|
|
83
|
+
status: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface RegressionSyncResult {
|
|
87
|
+
schemaVersion: 2;
|
|
88
|
+
provider: "github";
|
|
89
|
+
mode: "off" | "warn" | "error";
|
|
90
|
+
checkedAt: string;
|
|
91
|
+
repo: {
|
|
92
|
+
detected: string | null;
|
|
93
|
+
remoteUrl: string | null;
|
|
94
|
+
configured: string | null;
|
|
95
|
+
};
|
|
96
|
+
availability: {
|
|
97
|
+
hasFreshData: boolean;
|
|
98
|
+
usedCachedFallback: boolean;
|
|
99
|
+
};
|
|
100
|
+
findings: RegressionSyncFinding[];
|
|
101
|
+
summary: {
|
|
102
|
+
entries: number;
|
|
103
|
+
observedEntries: number;
|
|
104
|
+
errors: number;
|
|
105
|
+
warnings: number;
|
|
106
|
+
byCode: Record<string, number>;
|
|
107
|
+
byStatus: Record<string, number>;
|
|
108
|
+
};
|
|
109
|
+
entries: RegressionSyncEntry[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export declare function loadRegressionCatalogConfig(
|
|
113
|
+
productDir: string,
|
|
114
|
+
config: { file?: string | null } | null
|
|
115
|
+
): RegressionCatalogDocument | null;
|
|
116
|
+
export declare function loadRegressionCatalogDocument(
|
|
117
|
+
filePath: string,
|
|
118
|
+
relativePath?: string
|
|
119
|
+
): RegressionCatalogDocument;
|
|
120
|
+
export declare function normalizeRegressionCatalogDocument(
|
|
121
|
+
document: unknown,
|
|
122
|
+
relativePath?: string
|
|
123
|
+
): RegressionCatalogDocument;
|
|
124
|
+
export declare function validateRegressionCatalogDocument(
|
|
125
|
+
document: RegressionCatalogDocument,
|
|
126
|
+
options?: {
|
|
127
|
+
productDir?: string;
|
|
128
|
+
statusArtifactPath?: string;
|
|
129
|
+
statusArtifact?: unknown;
|
|
130
|
+
}
|
|
131
|
+
): RegressionCatalogValidationResult;
|
|
132
|
+
export declare function renderRegressionCatalogMarkdown(
|
|
133
|
+
document: RegressionCatalogDocument
|
|
134
|
+
): string;
|
|
135
|
+
export declare function findMatchingRegressionEntries(
|
|
136
|
+
document: RegressionCatalogDocument,
|
|
137
|
+
fileSummary: {
|
|
138
|
+
service?: string;
|
|
139
|
+
type?: string;
|
|
140
|
+
path: string;
|
|
141
|
+
status?: string;
|
|
142
|
+
error?: string | null;
|
|
143
|
+
failureDetails?: Array<{ key?: string; title?: string }>;
|
|
144
|
+
}
|
|
145
|
+
): RegressionCatalogEntry[];
|
|
146
|
+
export declare function matchesRegressionEntry(
|
|
147
|
+
entry: RegressionCatalogEntry,
|
|
148
|
+
fileSummary: {
|
|
149
|
+
service?: string;
|
|
150
|
+
type?: string;
|
|
151
|
+
path: string;
|
|
152
|
+
error?: string | null;
|
|
153
|
+
failureDetails?: Array<{ key?: string; title?: string }>;
|
|
154
|
+
}
|
|
155
|
+
): boolean;
|
|
156
|
+
export declare function buildRegressionFileIdentity(
|
|
157
|
+
service: string,
|
|
158
|
+
type: string,
|
|
159
|
+
filePath: string
|
|
160
|
+
): string;
|
|
161
|
+
|
|
162
|
+
export declare function normalizeRegressionSyncConfig(
|
|
163
|
+
value: unknown
|
|
164
|
+
): {
|
|
165
|
+
provider: "github";
|
|
166
|
+
mode: "off" | "warn" | "error";
|
|
167
|
+
cacheTtlSeconds: number;
|
|
168
|
+
} | null;
|
|
169
|
+
export declare function validateRegressionIssues(options: {
|
|
170
|
+
productDir: string;
|
|
171
|
+
document: RegressionCatalogDocument | null;
|
|
172
|
+
runArtifact?: unknown;
|
|
173
|
+
statusArtifact?: unknown;
|
|
174
|
+
config?: RegressionSyncConfig | null;
|
|
175
|
+
gitMetadata?: {
|
|
176
|
+
repoSlug?: string | null;
|
|
177
|
+
remoteUrl?: string | null;
|
|
178
|
+
} | null;
|
|
179
|
+
}): Promise<RegressionSyncResult | null>;
|
|
180
|
+
export declare function shouldFailRegressionSync(
|
|
181
|
+
result: RegressionSyncResult | null
|
|
182
|
+
): boolean;
|
|
183
|
+
export declare function buildRegressionSyncSummaryLines(
|
|
184
|
+
result: RegressionSyncResult | null
|
|
185
|
+
): string[];
|
|
186
|
+
export declare function buildRegressionSyncSummaryParts(
|
|
187
|
+
result: RegressionSyncResult | null
|
|
188
|
+
): string[];
|
|
189
|
+
export declare function parseGitHubRepoSlug(remoteUrl: string | null | undefined): string | null;
|
|
@@ -8,34 +8,33 @@ const CLASSIFICATIONS = new Set([
|
|
|
8
8
|
"stale_test",
|
|
9
9
|
"test_bug",
|
|
10
10
|
]);
|
|
11
|
-
const STATES = new Set(["closed", "open"]);
|
|
12
11
|
|
|
13
|
-
export function
|
|
14
|
-
const relativePath = config?.
|
|
12
|
+
export function loadRegressionCatalogConfig(productDir, config) {
|
|
13
|
+
const relativePath = config?.file;
|
|
15
14
|
if (!relativePath) return null;
|
|
16
15
|
|
|
17
16
|
const absolutePath = path.resolve(productDir, relativePath);
|
|
18
17
|
if (!fs.existsSync(absolutePath)) {
|
|
19
|
-
throw new Error(`
|
|
18
|
+
throw new Error(`Regression catalog not found: ${relativePath}`);
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
return
|
|
21
|
+
return loadRegressionCatalogDocument(absolutePath, relativePath);
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
export function
|
|
24
|
+
export function loadRegressionCatalogDocument(filePath, relativePath = filePath) {
|
|
26
25
|
let parsed;
|
|
27
26
|
try {
|
|
28
27
|
parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
29
28
|
} catch (error) {
|
|
30
29
|
throw new Error(
|
|
31
|
-
`Could not parse
|
|
30
|
+
`Could not parse regression catalog ${relativePath}: ${formatErrorMessage(error)}`
|
|
32
31
|
);
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
return
|
|
34
|
+
return normalizeRegressionCatalogDocument(parsed, relativePath);
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
export function
|
|
37
|
+
export function normalizeRegressionCatalogDocument(document, relativePath = "regression catalog") {
|
|
39
38
|
if (!document || typeof document !== "object") {
|
|
40
39
|
throw new Error(`${relativePath} must contain a JSON object`);
|
|
41
40
|
}
|
|
@@ -48,7 +47,7 @@ export function normalizeKnownFailuresDocument(document, relativePath = "known f
|
|
|
48
47
|
|
|
49
48
|
const ids = new Set();
|
|
50
49
|
const entries = document.entries.map((entry, index) => {
|
|
51
|
-
const normalized =
|
|
50
|
+
const normalized = normalizeRegressionEntry(entry, `${relativePath} entries[${index}]`);
|
|
52
51
|
if (ids.has(normalized.id)) {
|
|
53
52
|
throw new Error(`${relativePath} has duplicate entry id "${normalized.id}"`);
|
|
54
53
|
}
|
|
@@ -63,40 +62,45 @@ export function normalizeKnownFailuresDocument(document, relativePath = "known f
|
|
|
63
62
|
};
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
export function
|
|
65
|
+
export function validateRegressionCatalogDocument(
|
|
67
66
|
document,
|
|
68
67
|
{ productDir, statusArtifactPath, statusArtifact } = {}
|
|
69
68
|
) {
|
|
70
69
|
const errors = [];
|
|
71
70
|
const warnings = [];
|
|
72
|
-
const
|
|
73
|
-
let
|
|
71
|
+
const fingerprintKeys = new Set();
|
|
72
|
+
let fingerprintCount = 0;
|
|
74
73
|
|
|
75
74
|
for (const entry of document.entries) {
|
|
76
|
-
for (const
|
|
77
|
-
|
|
75
|
+
for (const fingerprint of entry.fingerprints) {
|
|
76
|
+
fingerprintCount += 1;
|
|
78
77
|
if (productDir) {
|
|
79
|
-
const absolutePath = path.join(productDir,
|
|
78
|
+
const absolutePath = path.join(productDir, fingerprint.path);
|
|
80
79
|
if (!fs.existsSync(absolutePath)) {
|
|
81
|
-
errors.push(`Missing matched file: ${
|
|
80
|
+
errors.push(`Missing matched file: ${fingerprint.path} (${entry.id})`);
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
const
|
|
84
|
+
const fingerprintKey = [
|
|
86
85
|
entry.id,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
fingerprint.service || "",
|
|
87
|
+
fingerprint.type || "",
|
|
88
|
+
fingerprint.path,
|
|
89
|
+
fingerprint.failureKey || "",
|
|
90
|
+
fingerprint.errorIncludes || "",
|
|
92
91
|
].join("::");
|
|
93
|
-
if (
|
|
94
|
-
errors.push(`Duplicate
|
|
92
|
+
if (fingerprintKeys.has(fingerprintKey)) {
|
|
93
|
+
errors.push(`Duplicate fingerprint selector in ${entry.id}: ${fingerprint.path}`);
|
|
95
94
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (
|
|
99
|
-
|
|
95
|
+
fingerprintKeys.add(fingerprintKey);
|
|
96
|
+
|
|
97
|
+
if (
|
|
98
|
+
!fingerprint.path.includes("__testkit__") ||
|
|
99
|
+
!fingerprint.path.endsWith(".testkit.ts")
|
|
100
|
+
) {
|
|
101
|
+
warnings.push(
|
|
102
|
+
`Fingerprint path does not look like a testkit file: ${fingerprint.path} (${entry.id})`
|
|
103
|
+
);
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
|
|
@@ -105,14 +109,11 @@ export function validateKnownFailuresDocument(
|
|
|
105
109
|
`Entry ${entry.id} uses issue repo ${entry.issue.repo} instead of document issueRepo ${document.issueRepo}`
|
|
106
110
|
);
|
|
107
111
|
}
|
|
108
|
-
if (!entry.issue.url.endsWith(`/issues/${entry.issue.number}`)) {
|
|
109
|
-
errors.push(`Issue URL does not match issue number for ${entry.id}`);
|
|
110
|
-
}
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
let failedTests = 0;
|
|
114
|
-
let
|
|
115
|
-
let
|
|
115
|
+
let diagnosedFailedTests = 0;
|
|
116
|
+
let newFailedTests = 0;
|
|
116
117
|
const parsedStatusArtifact = statusArtifact
|
|
117
118
|
? parseStatusArtifact(statusArtifact)
|
|
118
119
|
: statusArtifactPath && fs.existsSync(statusArtifactPath)
|
|
@@ -122,11 +123,11 @@ export function validateKnownFailuresDocument(
|
|
|
122
123
|
const failed = parsedStatusArtifact.tests.filter((test) => test.status === "failed");
|
|
123
124
|
failedTests = failed.length;
|
|
124
125
|
for (const test of failed) {
|
|
125
|
-
if (
|
|
126
|
-
|
|
126
|
+
if (findMatchingRegressionEntries(document, test).length > 0) {
|
|
127
|
+
diagnosedFailedTests += 1;
|
|
127
128
|
} else {
|
|
128
|
-
|
|
129
|
-
warnings.push(`
|
|
129
|
+
newFailedTests += 1;
|
|
130
|
+
warnings.push(`New failing test not yet in regression catalog: ${test.path}`);
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
|
|
@@ -135,41 +136,39 @@ export function validateKnownFailuresDocument(
|
|
|
135
136
|
warnings,
|
|
136
137
|
stats: {
|
|
137
138
|
entries: document.entries.length,
|
|
138
|
-
|
|
139
|
+
fingerprints: fingerprintCount,
|
|
139
140
|
failedTests,
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
diagnosedFailedTests,
|
|
142
|
+
newFailedTests,
|
|
142
143
|
},
|
|
143
144
|
};
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
export function
|
|
147
|
+
export function renderRegressionCatalogMarkdown(document) {
|
|
147
148
|
const lines = [
|
|
148
|
-
"#
|
|
149
|
+
"# Regression Catalog",
|
|
149
150
|
"",
|
|
150
|
-
"Canonical source: `testkit.
|
|
151
|
+
"Canonical source: `testkit.regressions.json`",
|
|
151
152
|
"",
|
|
152
|
-
`Tracked
|
|
153
|
+
`Tracked regressions: ${document.entries.length}`,
|
|
153
154
|
`Tracked issue references: ${new Set(document.entries.map((entry) => entry.issue.number)).size}`,
|
|
154
155
|
"",
|
|
155
156
|
];
|
|
156
157
|
|
|
157
158
|
document.entries.forEach((entry, index) => {
|
|
158
159
|
lines.push(
|
|
159
|
-
`## ${index + 1}. ${entry.
|
|
160
|
+
`## ${index + 1}. ${entry.summary} — #${entry.issue.number}`,
|
|
160
161
|
"",
|
|
161
162
|
`- Classification: \`${entry.classification}\``,
|
|
162
|
-
`-
|
|
163
|
-
`- Description: ${entry.description}`,
|
|
164
|
-
`- Why failing: ${entry.whyFailing}`,
|
|
163
|
+
`- Cause: ${entry.cause}`,
|
|
165
164
|
`- Last reviewed: ${entry.lastReviewedAt}`,
|
|
166
|
-
"-
|
|
165
|
+
"- Fingerprints:"
|
|
167
166
|
);
|
|
168
167
|
|
|
169
|
-
for (const
|
|
168
|
+
for (const fingerprint of entry.fingerprints) {
|
|
170
169
|
lines.push(
|
|
171
|
-
` - \`${
|
|
172
|
-
|
|
170
|
+
` - \`${fingerprint.path}\`${fingerprint.failureKey ? ` — \`${fingerprint.failureKey}\`` : ""}${
|
|
171
|
+
fingerprint.errorIncludes ? ` — error contains \`${fingerprint.errorIncludes}\`` : ""
|
|
173
172
|
}`
|
|
174
173
|
);
|
|
175
174
|
}
|
|
@@ -180,70 +179,68 @@ export function renderKnownFailuresMarkdown(document) {
|
|
|
180
179
|
return `${lines.join("\n").trimEnd()}\n`;
|
|
181
180
|
}
|
|
182
181
|
|
|
183
|
-
export function
|
|
184
|
-
return document.entries.filter((entry) =>
|
|
182
|
+
export function findMatchingRegressionEntries(document, fileSummary) {
|
|
183
|
+
return document.entries.filter((entry) => matchesRegressionEntry(entry, fileSummary));
|
|
185
184
|
}
|
|
186
185
|
|
|
187
|
-
export function
|
|
188
|
-
return entry.
|
|
186
|
+
export function matchesRegressionEntry(entry, fileSummary) {
|
|
187
|
+
return entry.fingerprints.some((fingerprint) =>
|
|
188
|
+
matchesRegressionFingerprint(fingerprint, fileSummary)
|
|
189
|
+
);
|
|
189
190
|
}
|
|
190
191
|
|
|
191
|
-
export function
|
|
192
|
-
if (
|
|
193
|
-
if (
|
|
194
|
-
if (
|
|
195
|
-
if (
|
|
192
|
+
export function matchesRegressionFingerprint(fingerprint, fileSummary) {
|
|
193
|
+
if (fingerprint.service && fingerprint.service !== fileSummary.service) return false;
|
|
194
|
+
if (fingerprint.type && fingerprint.type !== fileSummary.type) return false;
|
|
195
|
+
if (fingerprint.path !== fileSummary.path) return false;
|
|
196
|
+
if (fingerprint.failureKey) {
|
|
196
197
|
const failureKeys = Array.isArray(fileSummary.failureDetails)
|
|
197
198
|
? fileSummary.failureDetails.flatMap((detail) => [detail?.key, detail?.title].filter(Boolean))
|
|
198
199
|
: [];
|
|
199
|
-
if (!failureKeys.includes(
|
|
200
|
+
if (!failureKeys.includes(fingerprint.failureKey)) return false;
|
|
200
201
|
}
|
|
201
|
-
if (
|
|
202
|
+
if (fingerprint.errorIncludes && !String(fileSummary.error || "").includes(fingerprint.errorIncludes)) {
|
|
202
203
|
return false;
|
|
203
204
|
}
|
|
204
205
|
return true;
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
export function
|
|
208
|
+
export function buildRegressionFileIdentity(service, type, filePath) {
|
|
208
209
|
return `${service}::${type}::${filePath}`;
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
function
|
|
212
|
+
function normalizeRegressionEntry(entry, label) {
|
|
212
213
|
if (!entry || typeof entry !== "object") {
|
|
213
214
|
throw new Error(`${label} must be an object`);
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
const id = requireNonEmptyString(entry.id, `${label}.id`);
|
|
217
|
-
const title = requireNonEmptyString(entry.title, `${label}.title`);
|
|
218
218
|
const classification = requireEnumValue(
|
|
219
219
|
entry.classification,
|
|
220
220
|
CLASSIFICATIONS,
|
|
221
221
|
`${label}.classification`
|
|
222
222
|
);
|
|
223
|
-
const
|
|
224
|
-
const
|
|
225
|
-
const whyFailing = requireNonEmptyString(entry.whyFailing, `${label}.whyFailing`);
|
|
223
|
+
const summary = requireNonEmptyString(entry.summary, `${label}.summary`);
|
|
224
|
+
const cause = requireNonEmptyString(entry.cause, `${label}.cause`);
|
|
226
225
|
const lastReviewedAt = requireNonEmptyString(entry.lastReviewedAt, `${label}.lastReviewedAt`);
|
|
227
|
-
if (!Array.isArray(entry.
|
|
228
|
-
throw new Error(`${label}.
|
|
226
|
+
if (!Array.isArray(entry.fingerprints) || entry.fingerprints.length === 0) {
|
|
227
|
+
throw new Error(`${label}.fingerprints must be a non-empty array`);
|
|
229
228
|
}
|
|
230
229
|
|
|
231
230
|
return {
|
|
232
231
|
id,
|
|
233
|
-
title,
|
|
234
232
|
classification,
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
whyFailing,
|
|
233
|
+
issue: normalizeRegressionIssue(entry.issue, `${label}.issue`),
|
|
234
|
+
summary,
|
|
235
|
+
cause,
|
|
239
236
|
lastReviewedAt,
|
|
240
|
-
|
|
241
|
-
|
|
237
|
+
fingerprints: entry.fingerprints.map((fingerprint, index) =>
|
|
238
|
+
normalizeRegressionFingerprint(fingerprint, `${label}.fingerprints[${index}]`)
|
|
242
239
|
),
|
|
243
240
|
};
|
|
244
241
|
}
|
|
245
242
|
|
|
246
|
-
function
|
|
243
|
+
function normalizeRegressionIssue(issue, label) {
|
|
247
244
|
if (!issue || typeof issue !== "object") {
|
|
248
245
|
throw new Error(`${label} must be an object`);
|
|
249
246
|
}
|
|
@@ -253,31 +250,30 @@ function normalizeKnownFailureIssue(issue, label) {
|
|
|
253
250
|
if (!Number.isInteger(number) || number <= 0) {
|
|
254
251
|
throw new Error(`${label}.number must be a positive integer`);
|
|
255
252
|
}
|
|
256
|
-
const url = requireNonEmptyString(issue.url, `${label}.url`);
|
|
257
253
|
|
|
258
|
-
return { repo, number
|
|
254
|
+
return { repo, number };
|
|
259
255
|
}
|
|
260
256
|
|
|
261
|
-
function
|
|
262
|
-
if (!
|
|
257
|
+
function normalizeRegressionFingerprint(fingerprint, label) {
|
|
258
|
+
if (!fingerprint || typeof fingerprint !== "object") {
|
|
263
259
|
throw new Error(`${label} must be an object`);
|
|
264
260
|
}
|
|
265
261
|
|
|
266
|
-
const pathValue = requireNonEmptyString(
|
|
262
|
+
const pathValue = requireNonEmptyString(fingerprint.path, `${label}.path`);
|
|
267
263
|
const normalized = {
|
|
268
264
|
path: pathValue,
|
|
269
265
|
};
|
|
270
266
|
|
|
271
|
-
const service = normalizeOptionalString(
|
|
267
|
+
const service = normalizeOptionalString(fingerprint.service);
|
|
272
268
|
if (service) normalized.service = service;
|
|
273
269
|
|
|
274
|
-
const type = normalizeOptionalString(
|
|
270
|
+
const type = normalizeOptionalString(fingerprint.type);
|
|
275
271
|
if (type) normalized.type = type;
|
|
276
272
|
|
|
277
|
-
const failureKey = normalizeOptionalString(
|
|
273
|
+
const failureKey = normalizeOptionalString(fingerprint.failureKey);
|
|
278
274
|
if (failureKey) normalized.failureKey = failureKey;
|
|
279
275
|
|
|
280
|
-
const errorIncludes = normalizeOptionalString(
|
|
276
|
+
const errorIncludes = normalizeOptionalString(fingerprint.errorIncludes);
|
|
281
277
|
if (errorIncludes) normalized.errorIncludes = errorIncludes;
|
|
282
278
|
|
|
283
279
|
return normalized;
|
|
@@ -321,9 +317,10 @@ function formatErrorMessage(error) {
|
|
|
321
317
|
}
|
|
322
318
|
|
|
323
319
|
export {
|
|
324
|
-
|
|
325
|
-
|
|
320
|
+
buildRegressionSyncSummaryLines,
|
|
321
|
+
buildRegressionSyncSummaryParts,
|
|
322
|
+
normalizeRegressionSyncConfig,
|
|
326
323
|
parseGitHubRepoSlug,
|
|
327
|
-
|
|
328
|
-
|
|
324
|
+
shouldFailRegressionSync,
|
|
325
|
+
validateRegressionIssues,
|
|
329
326
|
} from "./github.mjs";
|