@elench/testkit 0.1.56 → 0.1.57
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 +59 -0
- package/lib/cli/commands/discover.mjs +80 -0
- package/lib/cli/entrypoint.mjs +1 -0
- package/lib/cli/presentation/colors.mjs +32 -0
- package/lib/cli/presentation/discovery-reporter.mjs +166 -0
- package/lib/config/discovery.mjs +106 -45
- package/lib/config/discovery.test.mjs +75 -1
- package/lib/config/index.mjs +21 -3
- package/lib/discovery/index.d.ts +121 -0
- package/lib/discovery/index.mjs +540 -0
- package/lib/discovery/index.test.mjs +182 -0
- package/lib/history/index.d.ts +46 -0
- package/lib/history/index.mjs +166 -0
- package/lib/history/index.test.mjs +115 -0
- package/lib/package.test.mjs +5 -0
- package/lib/runner/orchestrator.mjs +7 -0
- package/package.json +5 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { discoverTests } from "./index.mjs";
|
|
6
|
+
import { buildHistoryTestId, saveHistory } from "../history/index.mjs";
|
|
7
|
+
|
|
8
|
+
const cleanups = [];
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
while (cleanups.length > 0) {
|
|
12
|
+
cleanups.pop()();
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("public discovery", () => {
|
|
17
|
+
it("returns resolved suites/files with human labels and persisted history", async () => {
|
|
18
|
+
const productDir = createProduct();
|
|
19
|
+
writeFile(
|
|
20
|
+
productDir,
|
|
21
|
+
"testkit.setup.ts",
|
|
22
|
+
`
|
|
23
|
+
import { defineTestkitSetup } from "@elench/testkit/setup";
|
|
24
|
+
|
|
25
|
+
export default defineTestkitSetup({
|
|
26
|
+
services: {
|
|
27
|
+
api: {
|
|
28
|
+
local: {
|
|
29
|
+
cwd: ".",
|
|
30
|
+
start: "node server.js",
|
|
31
|
+
baseUrl: "http://127.0.0.1:3000",
|
|
32
|
+
readyUrl: "http://127.0.0.1:3000"
|
|
33
|
+
},
|
|
34
|
+
requirements: {
|
|
35
|
+
files: [
|
|
36
|
+
{
|
|
37
|
+
path: "src/api/routes/__testkit__/agent-configs-auth-gate.int.testkit.ts",
|
|
38
|
+
locks: ["route-lock"]
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
frontend: {
|
|
44
|
+
local: {
|
|
45
|
+
cwd: "frontend",
|
|
46
|
+
start: "node server.js",
|
|
47
|
+
baseUrl: "http://127.0.0.1:3001",
|
|
48
|
+
readyUrl: "http://127.0.0.1:3001"
|
|
49
|
+
},
|
|
50
|
+
dependsOn: ["api"],
|
|
51
|
+
skip: {
|
|
52
|
+
files: [
|
|
53
|
+
{
|
|
54
|
+
path: "frontend/src/app/login/__testkit__/auth.pw.testkit.ts",
|
|
55
|
+
reason: "Auth is stubbed locally"
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
`
|
|
63
|
+
);
|
|
64
|
+
writeFile(productDir, "src/api/routes/__testkit__/agent-configs-auth-gate.int.testkit.ts");
|
|
65
|
+
writeFile(productDir, "frontend/src/app/login/__testkit__/auth.pw.testkit.ts");
|
|
66
|
+
|
|
67
|
+
saveHistory(productDir, {
|
|
68
|
+
version: 1,
|
|
69
|
+
tests: {
|
|
70
|
+
[buildHistoryTestId("api", "int", "src/api/routes/__testkit__/agent-configs-auth-gate.int.testkit.ts")]: {
|
|
71
|
+
id: buildHistoryTestId("api", "int", "src/api/routes/__testkit__/agent-configs-auth-gate.int.testkit.ts"),
|
|
72
|
+
path: "src/api/routes/__testkit__/agent-configs-auth-gate.int.testkit.ts",
|
|
73
|
+
service: "api",
|
|
74
|
+
suiteName: "routes",
|
|
75
|
+
selectionType: "int",
|
|
76
|
+
framework: "k6",
|
|
77
|
+
firstSeenAt: "2026-04-01T00:00:00.000Z",
|
|
78
|
+
lastSeenAt: "2026-04-02T00:00:00.000Z",
|
|
79
|
+
lastRunAt: "2026-04-02T00:00:00.000Z",
|
|
80
|
+
runCount: 4,
|
|
81
|
+
passCount: 3,
|
|
82
|
+
failCount: 1,
|
|
83
|
+
skipCount: 0,
|
|
84
|
+
notRunCount: 0,
|
|
85
|
+
avgDurationMs: 2500,
|
|
86
|
+
durationCount: 4,
|
|
87
|
+
lastStatus: "passed",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const result = await discoverTests({ dir: productDir });
|
|
93
|
+
expect(result.summary).toMatchObject({
|
|
94
|
+
services: 2,
|
|
95
|
+
suites: 2,
|
|
96
|
+
files: 2,
|
|
97
|
+
activeFiles: 1,
|
|
98
|
+
skippedFiles: 1,
|
|
99
|
+
byType: {
|
|
100
|
+
int: 1,
|
|
101
|
+
pw: 1,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
expect(result.history.available).toBe(true);
|
|
105
|
+
expect(result.services.map((entry) => entry.name)).toEqual(["api", "frontend"]);
|
|
106
|
+
|
|
107
|
+
const apiFile = result.files.find((entry) => entry.service === "api");
|
|
108
|
+
expect(apiFile).toMatchObject({
|
|
109
|
+
displayName: "Agent Configs Auth Gate",
|
|
110
|
+
suiteName: "routes",
|
|
111
|
+
groupLabel: "Routes",
|
|
112
|
+
selectionType: "int",
|
|
113
|
+
skipped: false,
|
|
114
|
+
locks: ["route-lock"],
|
|
115
|
+
});
|
|
116
|
+
expect(apiFile.history).toMatchObject({
|
|
117
|
+
firstSeenAt: "2026-04-01T00:00:00.000Z",
|
|
118
|
+
runCount: 4,
|
|
119
|
+
passCount: 3,
|
|
120
|
+
failCount: 1,
|
|
121
|
+
avgDurationMs: 2500,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const frontendFile = result.files.find((entry) => entry.service === "frontend");
|
|
125
|
+
expect(frontendFile).toMatchObject({
|
|
126
|
+
displayName: "Auth",
|
|
127
|
+
selectionType: "pw",
|
|
128
|
+
skipped: true,
|
|
129
|
+
skipReason: "Auth is stubbed locally",
|
|
130
|
+
dependsOn: ["api"],
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("reports legacy discovery diagnostics without failing in report mode", async () => {
|
|
135
|
+
const productDir = createProduct();
|
|
136
|
+
writeFile(
|
|
137
|
+
productDir,
|
|
138
|
+
"testkit.setup.ts",
|
|
139
|
+
`
|
|
140
|
+
import { defineTestkitSetup } from "@elench/testkit/setup";
|
|
141
|
+
|
|
142
|
+
export default defineTestkitSetup({
|
|
143
|
+
services: {
|
|
144
|
+
api: {
|
|
145
|
+
local: {
|
|
146
|
+
cwd: ".",
|
|
147
|
+
start: "node server.js",
|
|
148
|
+
baseUrl: "http://127.0.0.1:3000",
|
|
149
|
+
readyUrl: "http://127.0.0.1:3000"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
`
|
|
155
|
+
);
|
|
156
|
+
writeFile(productDir, "src/api/routes/__testkit__/health/health.int.testkit.ts");
|
|
157
|
+
writeFile(productDir, "tests/api/integration/legacy.int.testkit.ts");
|
|
158
|
+
|
|
159
|
+
const result = await discoverTests({ dir: productDir, diagnostics: "report" });
|
|
160
|
+
expect(result.files).toHaveLength(1);
|
|
161
|
+
expect(result.diagnostics).toEqual(
|
|
162
|
+
expect.arrayContaining([
|
|
163
|
+
expect.objectContaining({
|
|
164
|
+
code: "legacy_path",
|
|
165
|
+
path: "tests/api/integration/legacy.int.testkit.ts",
|
|
166
|
+
}),
|
|
167
|
+
])
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
function createProduct() {
|
|
173
|
+
const productDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-discovery-public-"));
|
|
174
|
+
cleanups.push(() => fs.rmSync(productDir, { recursive: true, force: true }));
|
|
175
|
+
return productDir;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function writeFile(productDir, relativePath, contents = "export {};\n") {
|
|
179
|
+
const absolutePath = path.join(productDir, relativePath);
|
|
180
|
+
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
181
|
+
fs.writeFileSync(absolutePath, contents);
|
|
182
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface TestHistorySummary {
|
|
2
|
+
firstSeenAt: string | null;
|
|
3
|
+
lastSeenAt: string | null;
|
|
4
|
+
lastRunAt: string | null;
|
|
5
|
+
runCount: number;
|
|
6
|
+
passCount: number;
|
|
7
|
+
failCount: number;
|
|
8
|
+
skipCount: number;
|
|
9
|
+
avgDurationMs: number;
|
|
10
|
+
lastStatus: "passed" | "failed" | "skipped" | "not_run" | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TestHistoryEntry extends TestHistorySummary {
|
|
14
|
+
id: string;
|
|
15
|
+
path: string;
|
|
16
|
+
service: string;
|
|
17
|
+
suiteName: string;
|
|
18
|
+
selectionType: string;
|
|
19
|
+
framework: string;
|
|
20
|
+
notRunCount: number;
|
|
21
|
+
durationCount: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TestHistoryDocument {
|
|
25
|
+
version: number;
|
|
26
|
+
tests: Record<string, TestHistoryEntry>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export declare function historyFilePath(productDir: string): string;
|
|
30
|
+
export declare function createEmptyHistory(): TestHistoryDocument;
|
|
31
|
+
export declare function loadHistory(productDir: string): TestHistoryDocument;
|
|
32
|
+
export declare function saveHistory(productDir: string, history: TestHistoryDocument): void;
|
|
33
|
+
export declare function updateHistoryFromRunArtifact(
|
|
34
|
+
history: TestHistoryDocument,
|
|
35
|
+
runArtifact: unknown,
|
|
36
|
+
recordedAt?: string | null
|
|
37
|
+
): TestHistoryDocument;
|
|
38
|
+
export declare function summarizeHistoryForFiles(
|
|
39
|
+
history: TestHistoryDocument,
|
|
40
|
+
files?: Array<{ id: string; service: string; selectionType: string; path: string }>
|
|
41
|
+
): Map<string, TestHistorySummary>;
|
|
42
|
+
export declare function buildHistoryTestId(
|
|
43
|
+
serviceName: string,
|
|
44
|
+
selectionType: string,
|
|
45
|
+
filePath: string
|
|
46
|
+
): string;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const HISTORY_SCHEMA_VERSION = 1;
|
|
5
|
+
const HISTORY_DIRNAME = "history";
|
|
6
|
+
const TEST_HISTORY_FILENAME = "tests.json";
|
|
7
|
+
|
|
8
|
+
export function historyFilePath(productDir) {
|
|
9
|
+
return path.join(productDir, ".testkit", HISTORY_DIRNAME, TEST_HISTORY_FILENAME);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createEmptyHistory() {
|
|
13
|
+
return {
|
|
14
|
+
version: HISTORY_SCHEMA_VERSION,
|
|
15
|
+
tests: {},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function loadHistory(productDir) {
|
|
20
|
+
const filePath = historyFilePath(productDir);
|
|
21
|
+
if (!fs.existsSync(filePath)) {
|
|
22
|
+
return createEmptyHistory();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
27
|
+
return normalizeHistory(parsed);
|
|
28
|
+
} catch {
|
|
29
|
+
return createEmptyHistory();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function saveHistory(productDir, history) {
|
|
34
|
+
const filePath = historyFilePath(productDir);
|
|
35
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
36
|
+
fs.writeFileSync(filePath, `${JSON.stringify(normalizeHistory(history), null, 2)}\n`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function updateHistoryFromRunArtifact(history, runArtifact, recordedAt = null) {
|
|
40
|
+
const normalized = normalizeHistory(history);
|
|
41
|
+
const seenAt = recordedAt || runArtifact?.generatedAt || new Date().toISOString();
|
|
42
|
+
|
|
43
|
+
for (const service of runArtifact?.services || []) {
|
|
44
|
+
for (const suite of service.suites || []) {
|
|
45
|
+
for (const file of suite.files || []) {
|
|
46
|
+
const id = buildHistoryTestId(service.name, suite.type, file.path);
|
|
47
|
+
const existing = normalized.tests[id];
|
|
48
|
+
const next = existing
|
|
49
|
+
? { ...existing }
|
|
50
|
+
: {
|
|
51
|
+
id,
|
|
52
|
+
path: file.path,
|
|
53
|
+
service: service.name,
|
|
54
|
+
suiteName: suite.name,
|
|
55
|
+
selectionType: suite.type,
|
|
56
|
+
framework: normalizeArtifactFramework(suite.framework),
|
|
57
|
+
firstSeenAt: seenAt,
|
|
58
|
+
lastSeenAt: seenAt,
|
|
59
|
+
lastRunAt: seenAt,
|
|
60
|
+
runCount: 0,
|
|
61
|
+
passCount: 0,
|
|
62
|
+
failCount: 0,
|
|
63
|
+
skipCount: 0,
|
|
64
|
+
notRunCount: 0,
|
|
65
|
+
avgDurationMs: 0,
|
|
66
|
+
durationCount: 0,
|
|
67
|
+
lastStatus: null,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
next.path = file.path;
|
|
71
|
+
next.service = service.name;
|
|
72
|
+
next.suiteName = suite.name;
|
|
73
|
+
next.selectionType = suite.type;
|
|
74
|
+
next.framework = normalizeArtifactFramework(suite.framework);
|
|
75
|
+
next.lastSeenAt = seenAt;
|
|
76
|
+
next.lastRunAt = seenAt;
|
|
77
|
+
next.runCount += 1;
|
|
78
|
+
next.lastStatus = file.status;
|
|
79
|
+
|
|
80
|
+
if (file.status === "passed") next.passCount += 1;
|
|
81
|
+
else if (file.status === "failed") next.failCount += 1;
|
|
82
|
+
else if (file.status === "skipped") next.skipCount += 1;
|
|
83
|
+
else next.notRunCount += 1;
|
|
84
|
+
|
|
85
|
+
if (Number(file.durationMs || 0) > 0 && file.status !== "skipped" && file.status !== "not_run") {
|
|
86
|
+
const durationCount = Number(next.durationCount || 0) + 1;
|
|
87
|
+
next.avgDurationMs = Math.max(
|
|
88
|
+
1,
|
|
89
|
+
Math.round(
|
|
90
|
+
((Number(next.avgDurationMs || 0) * Number(next.durationCount || 0)) + Number(file.durationMs || 0)) /
|
|
91
|
+
durationCount
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
next.durationCount = durationCount;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
normalized.tests[id] = next;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return normalized;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function summarizeHistoryForFiles(history, files = []) {
|
|
106
|
+
const normalized = normalizeHistory(history);
|
|
107
|
+
const byId = new Map();
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
const entry = normalized.tests[file.id] || normalized.tests[buildHistoryTestId(file.service, file.selectionType, file.path)];
|
|
110
|
+
if (!entry) continue;
|
|
111
|
+
byId.set(file.id, {
|
|
112
|
+
firstSeenAt: entry.firstSeenAt || null,
|
|
113
|
+
lastSeenAt: entry.lastSeenAt || null,
|
|
114
|
+
lastRunAt: entry.lastRunAt || null,
|
|
115
|
+
runCount: Number(entry.runCount || 0),
|
|
116
|
+
passCount: Number(entry.passCount || 0),
|
|
117
|
+
failCount: Number(entry.failCount || 0),
|
|
118
|
+
skipCount: Number(entry.skipCount || 0),
|
|
119
|
+
avgDurationMs: Number(entry.avgDurationMs || 0),
|
|
120
|
+
lastStatus: entry.lastStatus || null,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return byId;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildHistoryTestId(serviceName, selectionType, filePath) {
|
|
127
|
+
return [serviceName, selectionType, normalizePath(filePath)].join("|");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeHistory(parsed) {
|
|
131
|
+
const tests = {};
|
|
132
|
+
for (const [id, entry] of Object.entries(parsed?.tests || {})) {
|
|
133
|
+
tests[id] = {
|
|
134
|
+
id,
|
|
135
|
+
path: normalizePath(entry.path || ""),
|
|
136
|
+
service: String(entry.service || ""),
|
|
137
|
+
suiteName: String(entry.suiteName || ""),
|
|
138
|
+
selectionType: String(entry.selectionType || ""),
|
|
139
|
+
framework: normalizeArtifactFramework(entry.framework || "k6"),
|
|
140
|
+
firstSeenAt: entry.firstSeenAt || null,
|
|
141
|
+
lastSeenAt: entry.lastSeenAt || null,
|
|
142
|
+
lastRunAt: entry.lastRunAt || null,
|
|
143
|
+
runCount: Number(entry.runCount || 0),
|
|
144
|
+
passCount: Number(entry.passCount || 0),
|
|
145
|
+
failCount: Number(entry.failCount || 0),
|
|
146
|
+
skipCount: Number(entry.skipCount || 0),
|
|
147
|
+
notRunCount: Number(entry.notRunCount || 0),
|
|
148
|
+
avgDurationMs: Number(entry.avgDurationMs || 0),
|
|
149
|
+
durationCount: Number(entry.durationCount || 0),
|
|
150
|
+
lastStatus: entry.lastStatus || null,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
version: HISTORY_SCHEMA_VERSION,
|
|
155
|
+
tests,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function normalizeArtifactFramework(value) {
|
|
160
|
+
if (value === "default") return "k6";
|
|
161
|
+
return value || "k6";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizePath(filePath) {
|
|
165
|
+
return String(filePath).split(path.sep).join("/").replace(/^\.\/+/, "");
|
|
166
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildHistoryTestId, createEmptyHistory, summarizeHistoryForFiles, updateHistoryFromRunArtifact } from "./index.mjs";
|
|
3
|
+
|
|
4
|
+
describe("test history", () => {
|
|
5
|
+
it("tracks first seen and pass/fail counters across runs", () => {
|
|
6
|
+
const history = createEmptyHistory();
|
|
7
|
+
const first = updateHistoryFromRunArtifact(
|
|
8
|
+
history,
|
|
9
|
+
{
|
|
10
|
+
generatedAt: "2026-04-30T10:00:00.000Z",
|
|
11
|
+
services: [
|
|
12
|
+
{
|
|
13
|
+
name: "api",
|
|
14
|
+
suites: [
|
|
15
|
+
{
|
|
16
|
+
name: "routes",
|
|
17
|
+
type: "int",
|
|
18
|
+
framework: "default",
|
|
19
|
+
files: [
|
|
20
|
+
{
|
|
21
|
+
path: "src/api/routes/__testkit__/health.int.testkit.ts",
|
|
22
|
+
status: "passed",
|
|
23
|
+
durationMs: 2000,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
const second = updateHistoryFromRunArtifact(
|
|
33
|
+
first,
|
|
34
|
+
{
|
|
35
|
+
generatedAt: "2026-05-01T10:00:00.000Z",
|
|
36
|
+
services: [
|
|
37
|
+
{
|
|
38
|
+
name: "api",
|
|
39
|
+
suites: [
|
|
40
|
+
{
|
|
41
|
+
name: "routes",
|
|
42
|
+
type: "int",
|
|
43
|
+
framework: "default",
|
|
44
|
+
files: [
|
|
45
|
+
{
|
|
46
|
+
path: "src/api/routes/__testkit__/health.int.testkit.ts",
|
|
47
|
+
status: "failed",
|
|
48
|
+
durationMs: 4000,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const id = buildHistoryTestId("api", "int", "src/api/routes/__testkit__/health.int.testkit.ts");
|
|
59
|
+
expect(second.tests[id]).toMatchObject({
|
|
60
|
+
firstSeenAt: "2026-04-30T10:00:00.000Z",
|
|
61
|
+
lastSeenAt: "2026-05-01T10:00:00.000Z",
|
|
62
|
+
lastRunAt: "2026-05-01T10:00:00.000Z",
|
|
63
|
+
runCount: 2,
|
|
64
|
+
passCount: 1,
|
|
65
|
+
failCount: 1,
|
|
66
|
+
skipCount: 0,
|
|
67
|
+
avgDurationMs: 3000,
|
|
68
|
+
lastStatus: "failed",
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("summarizes history for discovery file entries", () => {
|
|
73
|
+
const history = {
|
|
74
|
+
version: 1,
|
|
75
|
+
tests: {
|
|
76
|
+
[buildHistoryTestId("api", "int", "src/api/routes/__testkit__/health.int.testkit.ts")]: {
|
|
77
|
+
id: buildHistoryTestId("api", "int", "src/api/routes/__testkit__/health.int.testkit.ts"),
|
|
78
|
+
path: "src/api/routes/__testkit__/health.int.testkit.ts",
|
|
79
|
+
service: "api",
|
|
80
|
+
suiteName: "routes",
|
|
81
|
+
selectionType: "int",
|
|
82
|
+
framework: "k6",
|
|
83
|
+
firstSeenAt: "2026-04-30T10:00:00.000Z",
|
|
84
|
+
lastSeenAt: "2026-05-01T10:00:00.000Z",
|
|
85
|
+
lastRunAt: "2026-05-01T10:00:00.000Z",
|
|
86
|
+
runCount: 2,
|
|
87
|
+
passCount: 1,
|
|
88
|
+
failCount: 1,
|
|
89
|
+
skipCount: 0,
|
|
90
|
+
notRunCount: 0,
|
|
91
|
+
avgDurationMs: 3000,
|
|
92
|
+
durationCount: 2,
|
|
93
|
+
lastStatus: "failed",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const summaries = summarizeHistoryForFiles(history, [
|
|
99
|
+
{
|
|
100
|
+
id: buildHistoryTestId("api", "int", "src/api/routes/__testkit__/health.int.testkit.ts"),
|
|
101
|
+
service: "api",
|
|
102
|
+
selectionType: "int",
|
|
103
|
+
path: "src/api/routes/__testkit__/health.int.testkit.ts",
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
expect(summaries.get(buildHistoryTestId("api", "int", "src/api/routes/__testkit__/health.int.testkit.ts"))).toMatchObject({
|
|
108
|
+
runCount: 2,
|
|
109
|
+
passCount: 1,
|
|
110
|
+
failCount: 1,
|
|
111
|
+
avgDurationMs: 3000,
|
|
112
|
+
lastStatus: "failed",
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
package/lib/package.test.mjs
CHANGED
|
@@ -22,6 +22,10 @@ describe("package metadata", () => {
|
|
|
22
22
|
types: "./lib/runtime/index.d.ts",
|
|
23
23
|
default: "./lib/runtime/index.mjs",
|
|
24
24
|
});
|
|
25
|
+
expect(packageJson.exports["./discovery"]).toEqual({
|
|
26
|
+
types: "./lib/discovery/index.d.ts",
|
|
27
|
+
default: "./lib/discovery/index.mjs",
|
|
28
|
+
});
|
|
25
29
|
expect(packageJson.exports["./known-failures"]).toEqual({
|
|
26
30
|
types: "./lib/known-failures/index.d.ts",
|
|
27
31
|
default: "./lib/known-failures/index.mjs",
|
|
@@ -29,6 +33,7 @@ describe("package metadata", () => {
|
|
|
29
33
|
expect(fs.existsSync(path.join(rootDir, "lib", "index.d.ts"))).toBe(true);
|
|
30
34
|
expect(fs.existsSync(path.join(rootDir, "lib", "setup", "index.d.ts"))).toBe(true);
|
|
31
35
|
expect(fs.existsSync(path.join(rootDir, "lib", "runtime", "index.d.ts"))).toBe(true);
|
|
36
|
+
expect(fs.existsSync(path.join(rootDir, "lib", "discovery", "index.d.ts"))).toBe(true);
|
|
32
37
|
expect(fs.existsSync(path.join(rootDir, "lib", "known-failures", "index.d.ts"))).toBe(true);
|
|
33
38
|
});
|
|
34
39
|
});
|
|
@@ -52,6 +52,7 @@ import { createRuntimeManager } from "./runtime-manager.mjs";
|
|
|
52
52
|
import { createWorker, runWorker } from "./worker-loop.mjs";
|
|
53
53
|
import { findUnmatchedRequestedFiles, isFullRunSelection } from "./selection.mjs";
|
|
54
54
|
import { uploadTelemetryArtifact } from "../telemetry/index.mjs";
|
|
55
|
+
import { loadHistory, saveHistory, updateHistoryFromRunArtifact } from "../history/index.mjs";
|
|
55
56
|
|
|
56
57
|
export async function runAll(configs, typeValues, suiteSelectors, opts, allConfigs = configs) {
|
|
57
58
|
const configMap = new Map(allConfigs.map((config) => [config.name, config]));
|
|
@@ -302,6 +303,12 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
302
303
|
if (opts.writeStatus) {
|
|
303
304
|
writeStatusArtifact(productDir, enrichedArtifacts.statusArtifact);
|
|
304
305
|
}
|
|
306
|
+
const nextHistory = updateHistoryFromRunArtifact(
|
|
307
|
+
loadHistory(productDir),
|
|
308
|
+
enrichedArtifacts.runArtifact,
|
|
309
|
+
enrichedArtifacts.runArtifact.generatedAt
|
|
310
|
+
);
|
|
311
|
+
saveHistory(productDir, nextHistory);
|
|
305
312
|
|
|
306
313
|
reporter?.runSummary?.(results, finishedAt - startedAt, knownFailureIssueValidation);
|
|
307
314
|
await reportTelemetry(telemetry, enrichedArtifacts.runArtifact, reporter);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elench/testkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.57",
|
|
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
|
+
"./discovery": {
|
|
21
|
+
"types": "./lib/discovery/index.d.ts",
|
|
22
|
+
"default": "./lib/discovery/index.mjs"
|
|
23
|
+
},
|
|
20
24
|
"./known-failures": {
|
|
21
25
|
"types": "./lib/known-failures/index.d.ts",
|
|
22
26
|
"default": "./lib/known-failures/index.mjs"
|