@elench/testkit 0.1.42 → 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 +23 -0
- package/lib/config/index.mjs +27 -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/reporters/playwright.mjs +34 -5
- package/lib/reporters/playwright.test.mjs +11 -0
- package/lib/runner/default-runtime-runner.mjs +5 -1
- package/lib/runner/failure-details.mjs +91 -0
- package/lib/runner/failure-details.test.mjs +63 -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 +51 -12
- package/lib/runner/playwright-runner.mjs +1 -0
- package/lib/runner/reporting.mjs +8 -2
- package/lib/runner/reporting.test.mjs +2 -2
- package/lib/runner/results.mjs +8 -0
- package/lib/runner/triage.mjs +154 -0
- package/lib/runner/triage.test.mjs +154 -0
- package/lib/runtime/index.mjs +2 -1
- package/lib/runtime-src/k6/checks.js +130 -0
- package/lib/runtime-src/k6/dal-suite.js +12 -1
- package/lib/runtime-src/k6/suite.js +10 -1
- package/lib/setup/index.d.ts +10 -0
- package/package.json +5 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { normalizeKnownFailuresDocument } from "../known-failures/index.mjs";
|
|
3
|
+
import { applyKnownFailuresToArtifacts } from "./triage.mjs";
|
|
4
|
+
|
|
5
|
+
describe("runner triage", () => {
|
|
6
|
+
it("matches exact failure keys and enriches both artifacts", () => {
|
|
7
|
+
const knownFailures = normalizeKnownFailuresDocument({
|
|
8
|
+
schemaVersion: 1,
|
|
9
|
+
issueRepo: "acme/repo",
|
|
10
|
+
entries: [
|
|
11
|
+
{
|
|
12
|
+
id: "bad-message",
|
|
13
|
+
title: "Bad message bug",
|
|
14
|
+
classification: "product_bug",
|
|
15
|
+
state: "open",
|
|
16
|
+
issue: {
|
|
17
|
+
repo: "acme/repo",
|
|
18
|
+
number: 12,
|
|
19
|
+
url: "https://github.com/acme/repo/issues/12",
|
|
20
|
+
},
|
|
21
|
+
description: "The API returns the wrong message.",
|
|
22
|
+
whyFailing: "The endpoint payload is wrong.",
|
|
23
|
+
lastReviewedAt: "2026-04-27",
|
|
24
|
+
matches: [
|
|
25
|
+
{
|
|
26
|
+
service: "api",
|
|
27
|
+
type: "int",
|
|
28
|
+
path: "__testkit__/http/failing.int.testkit.ts",
|
|
29
|
+
failureKey: "returns the wrong message",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const runArtifact = {
|
|
37
|
+
services: [
|
|
38
|
+
{
|
|
39
|
+
name: "api",
|
|
40
|
+
suites: [
|
|
41
|
+
{
|
|
42
|
+
type: "int",
|
|
43
|
+
files: [
|
|
44
|
+
{
|
|
45
|
+
path: "__testkit__/http/failing.int.testkit.ts",
|
|
46
|
+
status: "failed",
|
|
47
|
+
error: "Default runtime thresholds failed: checks(rate==1.0)",
|
|
48
|
+
failureDetails: [
|
|
49
|
+
{
|
|
50
|
+
kind: "k6-check",
|
|
51
|
+
key: "returns the wrong message",
|
|
52
|
+
title: "returns the wrong message",
|
|
53
|
+
count: 1,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
const statusArtifact = {
|
|
64
|
+
tests: [
|
|
65
|
+
{
|
|
66
|
+
service: "api",
|
|
67
|
+
type: "int",
|
|
68
|
+
path: "__testkit__/http/failing.int.testkit.ts",
|
|
69
|
+
status: "failed",
|
|
70
|
+
error: "Default runtime thresholds failed: checks(rate==1.0)",
|
|
71
|
+
failureDetails: [
|
|
72
|
+
{
|
|
73
|
+
kind: "k6-check",
|
|
74
|
+
key: "returns the wrong message",
|
|
75
|
+
title: "returns the wrong message",
|
|
76
|
+
count: 1,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const enriched = applyKnownFailuresToArtifacts(runArtifact, statusArtifact, knownFailures);
|
|
84
|
+
expect(enriched.statusArtifact.tests[0].triage).toMatchObject({
|
|
85
|
+
status: "known_failure",
|
|
86
|
+
classifications: ["product_bug"],
|
|
87
|
+
});
|
|
88
|
+
expect(enriched.runArtifact.services[0].suites[0].files[0].triage.entries[0]).toMatchObject({
|
|
89
|
+
id: "bad-message",
|
|
90
|
+
issue: {
|
|
91
|
+
number: 12,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
expect(enriched.statusArtifact.triageSummary).toEqual({
|
|
95
|
+
failed: {
|
|
96
|
+
total: 1,
|
|
97
|
+
known: 1,
|
|
98
|
+
untriaged: 0,
|
|
99
|
+
byClassification: {
|
|
100
|
+
product_bug: 1,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
entries: {
|
|
104
|
+
total: 1,
|
|
105
|
+
matchedByFailedTests: 1,
|
|
106
|
+
unmatched: 0,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("marks unmatched failed tests as untriaged", () => {
|
|
112
|
+
const enriched = applyKnownFailuresToArtifacts(
|
|
113
|
+
{
|
|
114
|
+
services: [
|
|
115
|
+
{
|
|
116
|
+
name: "api",
|
|
117
|
+
suites: [
|
|
118
|
+
{
|
|
119
|
+
type: "int",
|
|
120
|
+
files: [
|
|
121
|
+
{
|
|
122
|
+
path: "__testkit__/http/failing.int.testkit.ts",
|
|
123
|
+
status: "failed",
|
|
124
|
+
error: "boom",
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
tests: [
|
|
134
|
+
{
|
|
135
|
+
service: "api",
|
|
136
|
+
type: "int",
|
|
137
|
+
path: "__testkit__/http/failing.int.testkit.ts",
|
|
138
|
+
status: "failed",
|
|
139
|
+
error: "boom",
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
normalizeKnownFailuresDocument({
|
|
144
|
+
schemaVersion: 1,
|
|
145
|
+
entries: [],
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(enriched.statusArtifact.tests[0].triage).toEqual({
|
|
150
|
+
status: "untriaged",
|
|
151
|
+
entries: [],
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
package/lib/runtime/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import rawHttp from "k6/http";
|
|
2
2
|
import { Rate, Trend } from "k6/metrics";
|
|
3
|
-
import {
|
|
3
|
+
import { fail, sleep } from "k6";
|
|
4
4
|
import {
|
|
5
5
|
formatWaitForTimeoutError,
|
|
6
6
|
normalizeWaitIntervalSeconds,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
remainingFileTimeoutMs,
|
|
9
9
|
remainingFileTimeoutSeconds,
|
|
10
10
|
} from "../shared/file-timeout.mjs";
|
|
11
|
+
import { check, group } from "../runtime-src/k6/checks.js";
|
|
11
12
|
|
|
12
13
|
export { check, fail, group, sleep };
|
|
13
14
|
export { Rate, Trend };
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { check as k6Check, group as k6Group } from "k6";
|
|
1
2
|
import { Rate } from "k6/metrics";
|
|
3
|
+
import { emitArtifact } from "./artifacts.js";
|
|
2
4
|
|
|
3
5
|
export const runtimeFailures = new Rate("testkit_runtime_failures");
|
|
6
|
+
const failureState = createFailureState();
|
|
4
7
|
|
|
5
8
|
export function singleIterationOptions(overrides = {}) {
|
|
6
9
|
return {
|
|
@@ -16,6 +19,41 @@ export function singleIterationOptions(overrides = {}) {
|
|
|
16
19
|
|
|
17
20
|
export const defaultOptions = singleIterationOptions();
|
|
18
21
|
|
|
22
|
+
export function check(value, checks) {
|
|
23
|
+
let allPassed = true;
|
|
24
|
+
|
|
25
|
+
for (const [name, predicate] of Object.entries(checks || {})) {
|
|
26
|
+
const checkName = normalizeLabel(name, "unnamed check");
|
|
27
|
+
const passed = k6Check(value, { [checkName]: predicate });
|
|
28
|
+
if (!passed) {
|
|
29
|
+
recordFailureDetail({
|
|
30
|
+
kind: "k6-check",
|
|
31
|
+
key: buildFailureKey(failureState.groupStack, checkName),
|
|
32
|
+
title: checkName,
|
|
33
|
+
checkName,
|
|
34
|
+
groupPath: [...failureState.groupStack],
|
|
35
|
+
phase: failureState.phase,
|
|
36
|
+
});
|
|
37
|
+
allPassed = false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return allPassed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function group(name, fn) {
|
|
45
|
+
const groupName = normalizeLabel(name, "unnamed group");
|
|
46
|
+
|
|
47
|
+
return k6Group(groupName, () => {
|
|
48
|
+
failureState.groupStack.push(groupName);
|
|
49
|
+
try {
|
|
50
|
+
return fn();
|
|
51
|
+
} finally {
|
|
52
|
+
failureState.groupStack.pop();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
19
57
|
export function json(res) {
|
|
20
58
|
return JSON.parse(res.body);
|
|
21
59
|
}
|
|
@@ -46,3 +84,95 @@ export function isSorted(rows, field, direction = "asc") {
|
|
|
46
84
|
export function recordRuntimeFailure() {
|
|
47
85
|
runtimeFailures.add(1);
|
|
48
86
|
}
|
|
87
|
+
|
|
88
|
+
export function startFailureCollection(phase) {
|
|
89
|
+
failureState.phase = normalizeLabel(phase, "exec");
|
|
90
|
+
failureState.groupStack = [];
|
|
91
|
+
failureState.detailsByKey = new Map();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function emitFailureCollectionArtifact() {
|
|
95
|
+
const failures = [...failureState.detailsByKey.values()]
|
|
96
|
+
.sort((left, right) => left.key.localeCompare(right.key))
|
|
97
|
+
.map((detail) => ({ ...detail }));
|
|
98
|
+
|
|
99
|
+
if (failures.length > 0) {
|
|
100
|
+
emitArtifact(
|
|
101
|
+
"failure-details",
|
|
102
|
+
{
|
|
103
|
+
phase: failureState.phase,
|
|
104
|
+
failures,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
kind: "testkit.failure-details",
|
|
108
|
+
summary: `${failures.length} failure detail(s)`,
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
startFailureCollection(failureState.phase);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function recordFailureDetail(detail) {
|
|
117
|
+
const normalized = normalizeFailureDetail(detail);
|
|
118
|
+
if (!normalized) return;
|
|
119
|
+
|
|
120
|
+
const existing = failureState.detailsByKey.get(normalized.key);
|
|
121
|
+
if (!existing) {
|
|
122
|
+
failureState.detailsByKey.set(normalized.key, normalized);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
existing.count += normalized.count;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function createFailureState() {
|
|
130
|
+
return {
|
|
131
|
+
phase: "exec",
|
|
132
|
+
groupStack: [],
|
|
133
|
+
detailsByKey: new Map(),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function normalizeFailureDetail(detail) {
|
|
138
|
+
if (!detail || typeof detail !== "object") return null;
|
|
139
|
+
|
|
140
|
+
const kind = normalizeLabel(detail.kind, null);
|
|
141
|
+
const key = normalizeLabel(detail.key, null);
|
|
142
|
+
const title = normalizeLabel(detail.title, null);
|
|
143
|
+
if (!kind || !key || !title) return null;
|
|
144
|
+
|
|
145
|
+
const normalized = {
|
|
146
|
+
kind,
|
|
147
|
+
key,
|
|
148
|
+
title,
|
|
149
|
+
count: 1,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const checkName = normalizeLabel(detail.checkName, null);
|
|
153
|
+
if (checkName) normalized.checkName = checkName;
|
|
154
|
+
|
|
155
|
+
const phase = normalizeLabel(detail.phase, null);
|
|
156
|
+
if (phase) normalized.phase = phase;
|
|
157
|
+
|
|
158
|
+
const message = normalizeLabel(detail.message, null);
|
|
159
|
+
if (message) normalized.message = message;
|
|
160
|
+
|
|
161
|
+
const groupPath = Array.isArray(detail.groupPath)
|
|
162
|
+
? detail.groupPath.map((entry) => normalizeLabel(entry, null)).filter(Boolean)
|
|
163
|
+
: [];
|
|
164
|
+
if (groupPath.length > 0) normalized.groupPath = groupPath;
|
|
165
|
+
|
|
166
|
+
return normalized;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildFailureKey(groupPath, title) {
|
|
170
|
+
if (!Array.isArray(groupPath) || groupPath.length === 0) return title;
|
|
171
|
+
return [...groupPath, title].join(" > ");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function normalizeLabel(value, fallback) {
|
|
175
|
+
if (typeof value !== "string") return fallback;
|
|
176
|
+
const normalized = value.trim();
|
|
177
|
+
return normalized.length > 0 ? normalized : fallback;
|
|
178
|
+
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { fail } from "k6";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
defaultOptions,
|
|
4
|
+
emitFailureCollectionArtifact,
|
|
5
|
+
recordRuntimeFailure,
|
|
6
|
+
startFailureCollection,
|
|
7
|
+
} from "./checks.js";
|
|
3
8
|
import { createDalContext, openDb } from "./dal.js";
|
|
4
9
|
|
|
5
10
|
export function defineDalSuite(configOrRun, maybeRun) {
|
|
@@ -11,14 +16,18 @@ export function defineDalSuite(configOrRun, maybeRun) {
|
|
|
11
16
|
options: config.options || defaultOptions,
|
|
12
17
|
setup() {
|
|
13
18
|
if (typeof config.setup !== "function") return null;
|
|
19
|
+
startFailureCollection("setup");
|
|
14
20
|
try {
|
|
15
21
|
return config.setup({ db, dal });
|
|
16
22
|
} catch (error) {
|
|
17
23
|
recordRuntimeFailure();
|
|
18
24
|
fail(formatFatalSuiteError("setup", error));
|
|
25
|
+
} finally {
|
|
26
|
+
emitFailureCollectionArtifact();
|
|
19
27
|
}
|
|
20
28
|
},
|
|
21
29
|
exec(setupData) {
|
|
30
|
+
startFailureCollection("exec");
|
|
22
31
|
try {
|
|
23
32
|
return run({
|
|
24
33
|
db,
|
|
@@ -28,6 +37,8 @@ export function defineDalSuite(configOrRun, maybeRun) {
|
|
|
28
37
|
} catch (error) {
|
|
29
38
|
recordRuntimeFailure();
|
|
30
39
|
fail(formatFatalSuiteError("exec", error));
|
|
40
|
+
} finally {
|
|
41
|
+
emitFailureCollectionArtifact();
|
|
31
42
|
}
|
|
32
43
|
},
|
|
33
44
|
};
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { fail } from "k6";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
defaultOptions,
|
|
4
|
+
emitFailureCollectionArtifact,
|
|
5
|
+
recordRuntimeFailure,
|
|
6
|
+
startFailureCollection,
|
|
7
|
+
} from "./checks.js";
|
|
3
8
|
import { createHttpClient, getEnv } from "./http.js";
|
|
4
9
|
import {
|
|
5
10
|
clearRuntimeContext,
|
|
@@ -16,6 +21,7 @@ export function defineHttpSuite(configOrRun, maybeRun) {
|
|
|
16
21
|
},
|
|
17
22
|
setup() {
|
|
18
23
|
const resolved = resolveRuntimeConfig(config);
|
|
24
|
+
startFailureCollection("setup");
|
|
19
25
|
try {
|
|
20
26
|
registerRuntimeContext({ env: resolved.env, http: resolved.client.rawHttp || null });
|
|
21
27
|
if (typeof resolved.auth?.setup !== "function") return null;
|
|
@@ -24,11 +30,13 @@ export function defineHttpSuite(configOrRun, maybeRun) {
|
|
|
24
30
|
recordRuntimeFailure();
|
|
25
31
|
fail(formatFatalSuiteError("setup", error));
|
|
26
32
|
} finally {
|
|
33
|
+
emitFailureCollectionArtifact();
|
|
27
34
|
clearRuntimeContext();
|
|
28
35
|
}
|
|
29
36
|
},
|
|
30
37
|
exec(setupData) {
|
|
31
38
|
const resolved = resolveRuntimeConfig(config);
|
|
39
|
+
startFailureCollection("exec");
|
|
32
40
|
try {
|
|
33
41
|
registerRuntimeContext({ env: resolved.env, http: resolved.client.rawHttp || null });
|
|
34
42
|
return run({
|
|
@@ -43,6 +51,7 @@ export function defineHttpSuite(configOrRun, maybeRun) {
|
|
|
43
51
|
recordRuntimeFailure();
|
|
44
52
|
fail(formatFatalSuiteError("exec", error));
|
|
45
53
|
} finally {
|
|
54
|
+
emitFailureCollectionArtifact();
|
|
46
55
|
clearRuntimeContext();
|
|
47
56
|
}
|
|
48
57
|
},
|
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;
|
|
@@ -90,6 +96,10 @@ export interface TestkitSetup {
|
|
|
90
96
|
profiles?: {
|
|
91
97
|
http?: Record<string, HttpSuiteConfig>;
|
|
92
98
|
};
|
|
99
|
+
reporting?: {
|
|
100
|
+
knownFailuresFile?: string;
|
|
101
|
+
issueValidation?: KnownFailureIssueValidationConfig;
|
|
102
|
+
};
|
|
93
103
|
services?: Record<string, ServiceConfig>;
|
|
94
104
|
telemetry?: {
|
|
95
105
|
enabled?: boolean;
|
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": {
|