@elench/testkit 0.1.79 → 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 +31 -0
- package/lib/cli/presentation/run-reporter.mjs +63 -93
- package/lib/cli/presentation/run-reporter.test.mjs +137 -26
- package/lib/cli/presentation/summary-box.mjs +45 -0
- package/lib/cli/presentation/summary-box.test.mjs +43 -0
- package/lib/cli/presentation/terminal-layout.mjs +43 -0
- package/lib/cli/presentation/terminal-layout.test.mjs +23 -0
- 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 +39 -77
- 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 +105 -103
- package/lib/runner/formatting.test.mjs +94 -131
- package/lib/runner/metadata.mjs +1 -1
- package/lib/runner/orchestrator.mjs +7 -8
- 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/runner/worker-loop.mjs +1 -0
- 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 +12 -9
- 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
|
@@ -3,17 +3,29 @@ import figures from "figures";
|
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
4
|
import { createRunReporter } from "./run-reporter.mjs";
|
|
5
5
|
|
|
6
|
+
function createCapture(columns = 100) {
|
|
7
|
+
let stdout = "";
|
|
8
|
+
const stream = new Writable({
|
|
9
|
+
write(chunk, _encoding, callback) {
|
|
10
|
+
stdout += chunk.toString();
|
|
11
|
+
callback();
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
stream.columns = columns;
|
|
15
|
+
return {
|
|
16
|
+
stream,
|
|
17
|
+
read() {
|
|
18
|
+
return stdout;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
6
23
|
describe("run reporter setup output", () => {
|
|
7
24
|
it("prints concise high-level setup summaries in compact mode", () => {
|
|
8
|
-
|
|
25
|
+
const capture = createCapture();
|
|
9
26
|
const reporter = createRunReporter({
|
|
10
27
|
outputMode: "compact",
|
|
11
|
-
stdout:
|
|
12
|
-
write(chunk, _encoding, callback) {
|
|
13
|
-
stdout += chunk.toString();
|
|
14
|
-
callback();
|
|
15
|
-
},
|
|
16
|
-
}),
|
|
28
|
+
stdout: capture.stream,
|
|
17
29
|
});
|
|
18
30
|
|
|
19
31
|
reporter.setupOperationFinished({
|
|
@@ -25,20 +37,15 @@ describe("run reporter setup output", () => {
|
|
|
25
37
|
durationMs: 8_000,
|
|
26
38
|
});
|
|
27
39
|
|
|
28
|
-
expect(
|
|
29
|
-
expect(
|
|
40
|
+
expect(capture.read()).toContain(`${figures.play} RUN SETUP api template rebuild`);
|
|
41
|
+
expect(capture.read()).toContain("8s");
|
|
30
42
|
});
|
|
31
43
|
|
|
32
44
|
it("does not print low-level setup steps in compact mode", () => {
|
|
33
|
-
|
|
45
|
+
const capture = createCapture();
|
|
34
46
|
const reporter = createRunReporter({
|
|
35
47
|
outputMode: "compact",
|
|
36
|
-
stdout:
|
|
37
|
-
write(chunk, _encoding, callback) {
|
|
38
|
-
stdout += chunk.toString();
|
|
39
|
-
callback();
|
|
40
|
-
},
|
|
41
|
-
}),
|
|
48
|
+
stdout: capture.stream,
|
|
42
49
|
});
|
|
43
50
|
|
|
44
51
|
reporter.setupOperationFinished({
|
|
@@ -50,19 +57,14 @@ describe("run reporter setup output", () => {
|
|
|
50
57
|
durationMs: 8_000,
|
|
51
58
|
});
|
|
52
59
|
|
|
53
|
-
expect(
|
|
60
|
+
expect(capture.read()).toBe("");
|
|
54
61
|
});
|
|
55
62
|
|
|
56
63
|
it("prints concise setup failures", () => {
|
|
57
|
-
|
|
64
|
+
const capture = createCapture();
|
|
58
65
|
const reporter = createRunReporter({
|
|
59
66
|
outputMode: "compact",
|
|
60
|
-
stdout:
|
|
61
|
-
write(chunk, _encoding, callback) {
|
|
62
|
-
stdout += chunk.toString();
|
|
63
|
-
callback();
|
|
64
|
-
},
|
|
65
|
-
}),
|
|
67
|
+
stdout: capture.stream,
|
|
66
68
|
});
|
|
67
69
|
|
|
68
70
|
reporter.setupOperationFinished({
|
|
@@ -75,7 +77,116 @@ describe("run reporter setup output", () => {
|
|
|
75
77
|
error: "Command failed with exit code 1: node scripts/fail-prepare.mjs",
|
|
76
78
|
});
|
|
77
79
|
|
|
78
|
-
expect(
|
|
79
|
-
expect(
|
|
80
|
+
expect(capture.read()).toContain(`${figures.cross} FAIL SETUP api runtime:prepare`);
|
|
81
|
+
expect(capture.read()).toContain("Command failed with exit code 1");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("run reporter task output", () => {
|
|
86
|
+
it("prints failure details on lines beneath the failed file result", () => {
|
|
87
|
+
const capture = createCapture(88);
|
|
88
|
+
const reporter = createRunReporter({
|
|
89
|
+
outputMode: "compact",
|
|
90
|
+
stdout: capture.stream,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
reporter.setTotalFileCount(3);
|
|
94
|
+
reporter.taskFinished(
|
|
95
|
+
{
|
|
96
|
+
serviceName: "api",
|
|
97
|
+
type: "integration",
|
|
98
|
+
framework: "k6",
|
|
99
|
+
file: "__testkit__/health/health.int.testkit.ts",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
status: "failed",
|
|
103
|
+
failed: true,
|
|
104
|
+
durationMs: 4_000,
|
|
105
|
+
error: "Default runtime thresholds failed: checks(rate==1.0)",
|
|
106
|
+
failureDetails: [
|
|
107
|
+
{
|
|
108
|
+
kind: "http-assertion",
|
|
109
|
+
title: "status is 200",
|
|
110
|
+
message: "GET /health expected 200, got 404",
|
|
111
|
+
request: {
|
|
112
|
+
method: "GET",
|
|
113
|
+
path: "/health",
|
|
114
|
+
requestId: "req-1",
|
|
115
|
+
},
|
|
116
|
+
response: {
|
|
117
|
+
status: 404,
|
|
118
|
+
bodyPreview: '{"error":"route not found"}',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const lines = capture.read().trimEnd().split("\n");
|
|
126
|
+
expect(lines[0]).toContain("[1/3]");
|
|
127
|
+
expect(lines[0]).toContain(`${figures.cross} FAIL api int __testkit__/health/health.int.testkit.ts 4s`);
|
|
128
|
+
expect(lines[1]).toBe(" GET /health expected 200, got 404");
|
|
129
|
+
expect(lines[2]).toContain(' response: {"error":"route not found"}');
|
|
130
|
+
expect(lines[3]).toBe(" regression: new");
|
|
131
|
+
expect(lines[4]).toBe(" logs: requestId=req-1");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("renders a compact aggregate summary box", () => {
|
|
135
|
+
const capture = createCapture(72);
|
|
136
|
+
const reporter = createRunReporter({
|
|
137
|
+
outputMode: "compact",
|
|
138
|
+
stdout: capture.stream,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
reporter.runSummary(
|
|
142
|
+
[
|
|
143
|
+
{
|
|
144
|
+
name: "api",
|
|
145
|
+
skipped: false,
|
|
146
|
+
failed: true,
|
|
147
|
+
suiteCount: 1,
|
|
148
|
+
completedSuiteCount: 1,
|
|
149
|
+
skippedSuiteCount: 0,
|
|
150
|
+
failedSuiteCount: 1,
|
|
151
|
+
totalFileCount: 3,
|
|
152
|
+
passedFileCount: 2,
|
|
153
|
+
failedFileCount: 1,
|
|
154
|
+
skippedFileCount: 0,
|
|
155
|
+
notRunFileCount: 0,
|
|
156
|
+
durationMs: 10_000,
|
|
157
|
+
suites: [],
|
|
158
|
+
errors: [],
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
20_000,
|
|
162
|
+
{
|
|
163
|
+
summary: {
|
|
164
|
+
newRegressions: 1,
|
|
165
|
+
knownRegressions: 2,
|
|
166
|
+
fixedKnownRegressions: 3,
|
|
167
|
+
catalogStale: 4,
|
|
168
|
+
catalogSyncUnavailable: false,
|
|
169
|
+
usedStaleCache: true,
|
|
170
|
+
},
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const output = capture.read();
|
|
175
|
+
expect(output).toContain("┌");
|
|
176
|
+
expect(output).toContain("│ Result");
|
|
177
|
+
expect(output).toContain("FAILED");
|
|
178
|
+
expect(output).toContain("│ Passed");
|
|
179
|
+
expect(output).toContain("│ Failed");
|
|
180
|
+
expect(output).toContain("│ Skipped");
|
|
181
|
+
expect(output).toContain("│ Not run");
|
|
182
|
+
expect(output).toContain("│ Files");
|
|
183
|
+
expect(output).toContain("│ Duration");
|
|
184
|
+
expect(output).toContain("│ New regressions");
|
|
185
|
+
expect(output).toContain("│ Known regressions");
|
|
186
|
+
expect(output).toContain("│ Fixed known");
|
|
187
|
+
expect(output).toContain("│ Catalog stale");
|
|
188
|
+
expect(output).toContain("│ Catalog sync");
|
|
189
|
+
expect(output).not.toContain("Failures:");
|
|
190
|
+
expect(output).not.toContain("Runtime Errors:");
|
|
80
191
|
});
|
|
81
192
|
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import figures from "figures";
|
|
2
|
+
import { clamp, getTerminalWidth, measureWidth, padEndVisible, wrapText } from "./terminal-layout.mjs";
|
|
3
|
+
|
|
4
|
+
export function renderSummaryBox(
|
|
5
|
+
rows,
|
|
6
|
+
{
|
|
7
|
+
stdout = process.stdout,
|
|
8
|
+
widthRatio = 0.55,
|
|
9
|
+
minWidth = 30,
|
|
10
|
+
maxWidth = 56,
|
|
11
|
+
minValueWidth = 8,
|
|
12
|
+
maxKeyWidth = 12,
|
|
13
|
+
} = {}
|
|
14
|
+
) {
|
|
15
|
+
if (!Array.isArray(rows) || rows.length === 0) return [];
|
|
16
|
+
|
|
17
|
+
const terminalWidth = getTerminalWidth(stdout, 100);
|
|
18
|
+
const preferredWidth = clamp(Math.floor(terminalWidth * widthRatio), minWidth, maxWidth);
|
|
19
|
+
const maxRenderableWidth = Math.max(minWidth, Math.min(preferredWidth, terminalWidth - 1));
|
|
20
|
+
const keyWidth = Math.min(
|
|
21
|
+
maxKeyWidth,
|
|
22
|
+
Math.max(...rows.map(([label]) => measureWidth(label)), 6)
|
|
23
|
+
);
|
|
24
|
+
const minBoxWidth = keyWidth + minValueWidth + 7;
|
|
25
|
+
const boxWidth = Math.max(minBoxWidth, maxRenderableWidth);
|
|
26
|
+
const valueWidth = Math.max(minValueWidth, boxWidth - keyWidth - 7);
|
|
27
|
+
|
|
28
|
+
const top = `${figures.lineDownRight}${figures.line.repeat(boxWidth - 2)}${figures.lineDownLeft}`;
|
|
29
|
+
const bottom = `${figures.lineUpRight}${figures.line.repeat(boxWidth - 2)}${figures.lineUpLeft}`;
|
|
30
|
+
const rendered = [top];
|
|
31
|
+
|
|
32
|
+
for (const [label, value] of rows) {
|
|
33
|
+
const wrappedValueLines = wrapText(value, valueWidth);
|
|
34
|
+
for (let index = 0; index < wrappedValueLines.length; index += 1) {
|
|
35
|
+
const keyCell = index === 0 ? padEndVisible(label, keyWidth) : " ".repeat(keyWidth);
|
|
36
|
+
const valueCell = padEndVisible(wrappedValueLines[index], valueWidth);
|
|
37
|
+
rendered.push(
|
|
38
|
+
`${figures.lineVertical} ${keyCell} ${figures.lineVertical} ${valueCell} ${figures.lineVertical}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
rendered.push(bottom);
|
|
44
|
+
return rendered;
|
|
45
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Writable } from "stream";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { renderSummaryBox } from "./summary-box.mjs";
|
|
4
|
+
|
|
5
|
+
function createStream(columns) {
|
|
6
|
+
const stream = new Writable({
|
|
7
|
+
write(_chunk, _encoding, callback) {
|
|
8
|
+
callback();
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
stream.columns = columns;
|
|
12
|
+
return stream;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("summary box", () => {
|
|
16
|
+
it("renders key/value rows inside a bounded box", () => {
|
|
17
|
+
const lines = renderSummaryBox(
|
|
18
|
+
[
|
|
19
|
+
["Result", "FAILED"],
|
|
20
|
+
["Passed", "203"],
|
|
21
|
+
["Duration", "8m 56s"],
|
|
22
|
+
],
|
|
23
|
+
{ stdout: createStream(80) }
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect(lines[0]).toMatch(/^┌/);
|
|
27
|
+
expect(lines.at(-1)).toMatch(/^└/);
|
|
28
|
+
expect(lines.join("\n")).toContain("Result");
|
|
29
|
+
expect(lines.join("\n")).toContain("FAILED");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("wraps long values instead of widening to content length", () => {
|
|
33
|
+
const lines = renderSummaryBox(
|
|
34
|
+
[["Catalog sync", "Used stale GitHub cache while catalog validation was unavailable"]],
|
|
35
|
+
{ stdout: createStream(40) }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(lines.length).toBeGreaterThan(4);
|
|
39
|
+
expect(lines.join("\n")).toContain("Catalog sync");
|
|
40
|
+
expect(lines.join("\n")).toContain("stale");
|
|
41
|
+
expect(lines.join("\n")).toContain("cache");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import stringWidth from "string-width";
|
|
2
|
+
import stripAnsi from "strip-ansi";
|
|
3
|
+
import wrapAnsi from "wrap-ansi";
|
|
4
|
+
|
|
5
|
+
export function getTerminalWidth(stream = process.stdout, fallback = 100) {
|
|
6
|
+
const columns = Number(stream?.columns);
|
|
7
|
+
if (Number.isFinite(columns) && columns > 0) return columns;
|
|
8
|
+
return fallback;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function measureWidth(text) {
|
|
12
|
+
return stringWidth(String(text ?? ""));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function stripTerminalFormatting(text) {
|
|
16
|
+
return stripAnsi(String(text ?? ""));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function padEndVisible(text, width) {
|
|
20
|
+
const normalized = String(text ?? "");
|
|
21
|
+
const padding = Math.max(0, width - measureWidth(normalized));
|
|
22
|
+
return `${normalized}${" ".repeat(padding)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function wrapText(text, width) {
|
|
26
|
+
const normalized = String(text ?? "");
|
|
27
|
+
if (width <= 0) return [normalized];
|
|
28
|
+
return wrapAnsi(normalized, width, {
|
|
29
|
+
hard: true,
|
|
30
|
+
trim: false,
|
|
31
|
+
wordWrap: true,
|
|
32
|
+
}).split("\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function renderIndentedBlock(text, { width, indent = " " } = {}) {
|
|
36
|
+
const visibleIndent = measureWidth(indent);
|
|
37
|
+
const contentWidth = Math.max(12, width - visibleIndent);
|
|
38
|
+
return wrapText(text, contentWidth).map((line) => `${indent}${line}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function clamp(value, min, max) {
|
|
42
|
+
return Math.max(min, Math.min(max, value));
|
|
43
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getTerminalWidth, renderIndentedBlock, wrapText } from "./terminal-layout.mjs";
|
|
3
|
+
|
|
4
|
+
describe("terminal layout", () => {
|
|
5
|
+
it("falls back to a default width when stream columns are unavailable", () => {
|
|
6
|
+
expect(getTerminalWidth({}, 91)).toBe(91);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("wraps indented blocks to the provided width", () => {
|
|
10
|
+
const lines = renderIndentedBlock("response: abcdefghijklmnopqrstuvwxyz", {
|
|
11
|
+
width: 20,
|
|
12
|
+
indent: " ",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
expect(lines[0].startsWith(" ")).toBe(true);
|
|
16
|
+
expect(lines.length).toBeGreaterThan(1);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("hard-wraps long unbroken values", () => {
|
|
20
|
+
const lines = wrapText("abcdefghijklmnopqrstuvwxyz", 8);
|
|
21
|
+
expect(lines.length).toBeGreaterThan(1);
|
|
22
|
+
});
|
|
23
|
+
});
|
package/lib/cli/viewer.mjs
CHANGED
|
@@ -109,11 +109,11 @@ export function formatFileDetail(productDir, runArtifact, subject, options = {})
|
|
|
109
109
|
for (const line of codeFrame) lines.push(` ${line}`);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
if (subject.file.
|
|
112
|
+
if (subject.file.diagnosis) {
|
|
113
113
|
lines.push("");
|
|
114
|
-
lines.push("
|
|
115
|
-
const
|
|
116
|
-
for (const line of
|
|
114
|
+
lines.push("Diagnosis:");
|
|
115
|
+
const diagnosisLines = formatDiagnosis(subject.file.diagnosis);
|
|
116
|
+
for (const line of diagnosisLines) lines.push(` ${line}`);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
const setupOperations = getSetupOperationsForService(runArtifact, subject.service.name);
|
|
@@ -290,30 +290,29 @@ function formatResponseLine(detail) {
|
|
|
290
290
|
return parts.join(" ");
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
function
|
|
294
|
-
const lines = [`status: ${
|
|
295
|
-
if (
|
|
296
|
-
lines.push(`classification: ${
|
|
293
|
+
function formatDiagnosis(diagnosis) {
|
|
294
|
+
const lines = [`status: ${diagnosis.status}`];
|
|
295
|
+
if (diagnosis.classifications?.length) {
|
|
296
|
+
lines.push(`classification: ${diagnosis.classifications.join(", ")}`);
|
|
297
297
|
}
|
|
298
|
-
|
|
299
|
-
lines.push(
|
|
300
|
-
`validation: ${triage.availability.mode}${triage.availability.reason ? ` (${triage.availability.reason})` : ""}`
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
for (const entry of triage.entries || []) {
|
|
298
|
+
for (const entry of diagnosis.entries || []) {
|
|
304
299
|
lines.push(`issue: ${entry.issue.repo}#${entry.issue.number}`);
|
|
305
|
-
|
|
300
|
+
lines.push(`summary: ${entry.summary}`);
|
|
301
|
+
if (entry.github?.url) lines.push(`url: ${entry.github.url}`);
|
|
306
302
|
if (entry.github?.state) {
|
|
307
303
|
lines.push(`github state: ${entry.github.state}${entry.github.cached ? " (cache)" : ""}`);
|
|
308
304
|
}
|
|
309
|
-
if (entry.
|
|
310
|
-
if (entry.
|
|
311
|
-
for (const finding of entry.
|
|
312
|
-
lines.push(`finding: ${finding.message}`);
|
|
305
|
+
if (entry.syncStatus) lines.push(`catalog status: ${entry.syncStatus}`);
|
|
306
|
+
if (entry.catalogFindings?.length) {
|
|
307
|
+
for (const finding of entry.catalogFindings.slice(0, 3)) {
|
|
308
|
+
lines.push(`catalog finding: ${finding.message}`);
|
|
313
309
|
}
|
|
314
310
|
}
|
|
315
311
|
break;
|
|
316
312
|
}
|
|
313
|
+
if (diagnosis.status === "new_regression") {
|
|
314
|
+
lines.push("next: review .testkit/results/latest.json drafts.newRegressions for a prepared catalog entry");
|
|
315
|
+
}
|
|
317
316
|
return lines;
|
|
318
317
|
}
|
|
319
318
|
|
package/lib/config/index.mjs
CHANGED
|
@@ -14,9 +14,9 @@ import {
|
|
|
14
14
|
detectNextApp,
|
|
15
15
|
inferLocalRuntime,
|
|
16
16
|
normalizeBrowserServiceConfig,
|
|
17
|
+
normalizeRegressionsConfig,
|
|
17
18
|
normalizeOptionalString,
|
|
18
19
|
normalizeRepoExecution,
|
|
19
|
-
normalizeReportingConfig,
|
|
20
20
|
normalizeRuntimeConfig,
|
|
21
21
|
} from "./runtime.mjs";
|
|
22
22
|
import { normalizeServiceRequirements, normalizeSkipConfig } from "./skip-config.mjs";
|
|
@@ -30,7 +30,7 @@ export async function loadConfigContext(opts = {}) {
|
|
|
30
30
|
const configContext = opts.configContext || (await loadTestkitConfig(productDir));
|
|
31
31
|
const { config, configFile } = configContext;
|
|
32
32
|
const execution = normalizeRepoExecution(config.execution);
|
|
33
|
-
const
|
|
33
|
+
const regressions = normalizeRegressionsConfig(config.regressions);
|
|
34
34
|
const toolchains = normalizeToolchainRegistry(config.toolchains);
|
|
35
35
|
const discoveryConfig = normalizeRepoDiscoveryConfig(config.discovery);
|
|
36
36
|
const explicitServices = config.services || {};
|
|
@@ -54,7 +54,7 @@ export async function loadConfigContext(opts = {}) {
|
|
|
54
54
|
configFile,
|
|
55
55
|
execution,
|
|
56
56
|
discovery: discoveryConfig,
|
|
57
|
-
|
|
57
|
+
regressions,
|
|
58
58
|
toolchains,
|
|
59
59
|
explicitService: explicitServices[name] || {},
|
|
60
60
|
discoveredService: discovery.services[name] || null,
|
|
@@ -71,7 +71,7 @@ export async function loadConfigContext(opts = {}) {
|
|
|
71
71
|
configFile,
|
|
72
72
|
execution,
|
|
73
73
|
discovery: discoveryConfig,
|
|
74
|
-
|
|
74
|
+
regressions,
|
|
75
75
|
toolchains,
|
|
76
76
|
explicitServices,
|
|
77
77
|
discovery,
|
|
@@ -102,7 +102,7 @@ function normalizeServiceConfig({
|
|
|
102
102
|
configFile,
|
|
103
103
|
execution,
|
|
104
104
|
discovery,
|
|
105
|
-
|
|
105
|
+
regressions,
|
|
106
106
|
toolchains,
|
|
107
107
|
explicitService,
|
|
108
108
|
discoveredService,
|
|
@@ -156,7 +156,7 @@ function normalizeServiceConfig({
|
|
|
156
156
|
suites,
|
|
157
157
|
testkit: {
|
|
158
158
|
execution,
|
|
159
|
-
|
|
159
|
+
regressions,
|
|
160
160
|
dependsOn: explicitService.dependsOn || [],
|
|
161
161
|
discovery: normalizedDiscovery,
|
|
162
162
|
database,
|
package/lib/config/runtime.mjs
CHANGED
|
@@ -15,23 +15,23 @@ import {
|
|
|
15
15
|
parseModuleSpecifier,
|
|
16
16
|
} from "../shared/configured-steps.mjs";
|
|
17
17
|
import { buildConfigToPrepare, normalizeBuildConfig } from "../shared/build-config.mjs";
|
|
18
|
-
import {
|
|
18
|
+
import { normalizeRegressionSyncConfig } from "../regressions/github.mjs";
|
|
19
19
|
import { normalizeRuntimeToolchain } from "../toolchains/index.mjs";
|
|
20
20
|
import { resolveServiceCwd } from "./paths.mjs";
|
|
21
21
|
|
|
22
|
-
export function
|
|
22
|
+
export function normalizeRegressionsConfig(value) {
|
|
23
23
|
if (!value) return null;
|
|
24
24
|
|
|
25
|
-
const
|
|
26
|
-
if (!
|
|
27
|
-
throw new Error('testkit.config.ts
|
|
25
|
+
const file = normalizeOptionalString(value.file);
|
|
26
|
+
if (!file) {
|
|
27
|
+
throw new Error('testkit.config.ts regressions.file must be a non-empty string');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
const
|
|
30
|
+
const sync = normalizeRegressionSyncConfig(value.sync);
|
|
31
31
|
|
|
32
32
|
return {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
file,
|
|
34
|
+
sync,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -143,7 +143,7 @@ export interface BrowserServiceConfig {
|
|
|
143
143
|
origins?: string[];
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
export interface
|
|
146
|
+
export interface RegressionSyncConfig {
|
|
147
147
|
provider?: "github";
|
|
148
148
|
mode?: "off" | "warn" | "error";
|
|
149
149
|
cacheTtlSeconds?: number;
|
|
@@ -412,9 +412,9 @@ export interface TestkitConfig {
|
|
|
412
412
|
profiles?: {
|
|
413
413
|
http?: Record<string, HttpSuiteConfig<any>>;
|
|
414
414
|
};
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
415
|
+
regressions?: {
|
|
416
|
+
file?: string;
|
|
417
|
+
sync?: RegressionSyncConfig;
|
|
418
418
|
};
|
|
419
419
|
services?: Record<string, ServiceConfig>;
|
|
420
420
|
toolchains?: Record<string, ToolchainConfig>;
|
package/lib/package.test.mjs
CHANGED
|
@@ -42,9 +42,9 @@ describe("package metadata", () => {
|
|
|
42
42
|
types: "./lib/discovery/index.d.ts",
|
|
43
43
|
default: "./lib/discovery/index.mjs",
|
|
44
44
|
});
|
|
45
|
-
expect(packageJson.exports["./
|
|
46
|
-
types: "./lib/
|
|
47
|
-
default: "./lib/
|
|
45
|
+
expect(packageJson.exports["./regressions"]).toEqual({
|
|
46
|
+
types: "./lib/regressions/index.d.ts",
|
|
47
|
+
default: "./lib/regressions/index.mjs",
|
|
48
48
|
});
|
|
49
49
|
expect(fs.existsSync(path.join(rootDir, "lib", "index.d.ts"))).toBe(true);
|
|
50
50
|
expect(fs.existsSync(path.join(rootDir, "lib", "config-api", "index.d.ts"))).toBe(true);
|
|
@@ -54,6 +54,6 @@ describe("package metadata", () => {
|
|
|
54
54
|
expect(fs.existsSync(path.join(rootDir, "lib", "runtime", "index.d.ts"))).toBe(true);
|
|
55
55
|
expect(fs.existsSync(path.join(rootDir, "lib", "vitest", "index.d.ts"))).toBe(true);
|
|
56
56
|
expect(fs.existsSync(path.join(rootDir, "lib", "discovery", "index.d.ts"))).toBe(true);
|
|
57
|
-
expect(fs.existsSync(path.join(rootDir, "lib", "
|
|
57
|
+
expect(fs.existsSync(path.join(rootDir, "lib", "regressions", "index.d.ts"))).toBe(true);
|
|
58
58
|
});
|
|
59
59
|
});
|