@elench/testkit 0.1.108 → 0.1.109
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 +9 -9
- package/lib/app/doctor.mjs +5 -5
- package/lib/app/typecheck.mjs +6 -5
- package/lib/bundler/index.mjs +134 -7
- package/lib/cli/args.mjs +3 -2
- package/lib/cli/assistant/command-observer.mjs +2 -1
- package/lib/cli/assistant/command-results.mjs +2 -1
- package/lib/cli/assistant/context-pack.mjs +2 -2
- package/lib/cli/assistant/prompt-builder.mjs +2 -2
- package/lib/cli/command-flags.mjs +2 -1
- package/lib/cli/commands/cleanup.mjs +13 -2
- package/lib/cli/commands/discover.mjs +2 -1
- package/lib/cli/commands/run.mjs +3 -2
- package/lib/cli/entrypoint.mjs +3 -1
- package/lib/cli/operations/cleanup/operation.mjs +6 -1
- package/lib/cli/operations/status/operation.mjs +2 -2
- package/lib/cli/renderers/discover/report.mjs +6 -8
- package/lib/cli/renderers/run/failure.mjs +1 -1
- package/lib/cli/renderers/run/text-reporter.mjs +1 -1
- package/lib/cli/renderers/status/text.mjs +101 -1
- package/lib/config/discovery.mjs +10 -1
- package/lib/config-api/index.mjs +2 -2
- package/lib/config-api/next-runtime-tsconfig.mjs +2 -1
- package/lib/coverage/graph-builder.mjs +2 -4
- package/lib/coverage/routing.mjs +1 -1
- package/lib/coverage/shared.mjs +1 -2
- package/lib/discovery/index.d.ts +5 -8
- package/lib/discovery/index.mjs +15 -24
- package/lib/domain/test-types.mjs +44 -0
- package/lib/history/index.d.ts +3 -4
- package/lib/history/index.mjs +6 -14
- package/lib/runner/formatting.mjs +2 -3
- package/lib/runner/maintenance.mjs +136 -35
- package/lib/runner/planning.mjs +1 -1
- package/lib/runner/results.mjs +0 -6
- package/lib/runner/status-model.mjs +520 -0
- package/lib/runner/suite-selection.mjs +20 -11
- package/lib/runner/template-steps.mjs +2 -2
- package/lib/runner/template.mjs +4 -0
- package/lib/ui/index.d.ts +1 -0
- package/lib/ui/index.mjs +1 -0
- package/lib/vitest/index.mjs +2 -1
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/dist/index.js +9 -11
- package/node_modules/@elench/testkit-bridge/dist/index.js.map +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/dist/index.d.ts +1 -3
- package/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +1 -1
- package/node_modules/@elench/testkit-protocol/dist/index.js +3 -6
- package/node_modules/@elench/testkit-protocol/dist/index.js.map +1 -1
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/dist/requests.js +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +9 -9
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { buildRuntimeIds } from "./execution-config.mjs";
|
|
4
|
+
import { buildGraphDirName, resolveRuntimeConfigs } from "./planning.mjs";
|
|
5
|
+
import { isPidRunning, listRunManifests } from "./lifecycle.mjs";
|
|
6
|
+
import { readGraphMetadata } from "./state.mjs";
|
|
7
|
+
|
|
8
|
+
const BUNDLE_MANIFEST = "manifest.json";
|
|
9
|
+
const ASSISTANT_LARGE_RESULT_BYTES = 10 * 1024 * 1024;
|
|
10
|
+
const ASSISTANT_RESULT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
export function collectStatusModel(config, { allConfigs = [config] } = {}) {
|
|
13
|
+
const productDir = config.productDir;
|
|
14
|
+
const desiredGraphs = collectDesiredRuntimeGraphs(allConfigs);
|
|
15
|
+
const runtimeGraphs = collectRuntimeGraphStatus(productDir, {
|
|
16
|
+
desiredGraphs,
|
|
17
|
+
serviceName: config.name,
|
|
18
|
+
});
|
|
19
|
+
const serviceState = collectDirectorySummary(config.stateDir);
|
|
20
|
+
const runs = collectRunStatus(productDir);
|
|
21
|
+
const bundles = collectBundleCacheStatus(productDir, config.name);
|
|
22
|
+
const assistant = collectAssistantResultStatus(productDir);
|
|
23
|
+
const warnings = collectWarnings({ runtimeGraphs, bundles, assistant });
|
|
24
|
+
const hasState =
|
|
25
|
+
serviceState.exists ||
|
|
26
|
+
runtimeGraphs.some((graph) => graph.exists) ||
|
|
27
|
+
runs.total > 0 ||
|
|
28
|
+
bundles.exists ||
|
|
29
|
+
assistant.exists;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
schemaVersion: 1,
|
|
33
|
+
name: config.name,
|
|
34
|
+
product: {
|
|
35
|
+
dir: productDir,
|
|
36
|
+
configFile: config.configFile || null,
|
|
37
|
+
services: allConfigs.map((entry) => entry.name).sort(),
|
|
38
|
+
selectedService: config.name,
|
|
39
|
+
dependencies: [...(config.testkit?.dependsOn || [])].sort(),
|
|
40
|
+
},
|
|
41
|
+
hasState,
|
|
42
|
+
runs,
|
|
43
|
+
serviceState,
|
|
44
|
+
runtimeGraphs,
|
|
45
|
+
caches: {
|
|
46
|
+
bundles,
|
|
47
|
+
assistant,
|
|
48
|
+
},
|
|
49
|
+
warnings,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function collectCleanupTargets(productDir, { allConfigs = [], serviceName = null, cache = [] } = {}) {
|
|
54
|
+
const desiredGraphs = collectDesiredRuntimeGraphs(allConfigs);
|
|
55
|
+
const runtimeGraphs = collectRuntimeGraphStatus(productDir, { desiredGraphs, serviceName });
|
|
56
|
+
const cacheSet = normalizeCacheSelection(cache);
|
|
57
|
+
return {
|
|
58
|
+
runtime: runtimeGraphs.flatMap((graph) =>
|
|
59
|
+
graph.staleRuntimeDirs
|
|
60
|
+
.filter((runtime) => !runtime.active)
|
|
61
|
+
.map((runtime) => ({
|
|
62
|
+
graph: graph.name,
|
|
63
|
+
runtimeId: runtime.id,
|
|
64
|
+
path: runtime.path,
|
|
65
|
+
}))
|
|
66
|
+
),
|
|
67
|
+
bundles: cacheSet.has("bundles") ? collectBundleCleanupTargets(productDir, { allConfigs, serviceName }) : [],
|
|
68
|
+
assistant: cacheSet.has("assistant") ? collectAssistantCleanupTargets(productDir) : [],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function collectDesiredRuntimeGraphs(allConfigs = []) {
|
|
73
|
+
const configMap = new Map(allConfigs.map((config) => [config.name, config]));
|
|
74
|
+
const graphByKey = new Map();
|
|
75
|
+
|
|
76
|
+
for (const config of allConfigs) {
|
|
77
|
+
const runtimeConfigs = resolveRuntimeConfigs(config, configMap);
|
|
78
|
+
const runtimeNames = runtimeConfigs.map((runtimeConfig) => runtimeConfig.name).sort();
|
|
79
|
+
const key = runtimeNames.join("|");
|
|
80
|
+
const existing = graphByKey.get(key);
|
|
81
|
+
if (existing) {
|
|
82
|
+
existing.targetNames.push(config.name);
|
|
83
|
+
existing.targetNames.sort();
|
|
84
|
+
existing.instanceCount = Math.max(existing.instanceCount, config.testkit.runtime.instances);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
graphByKey.set(key, {
|
|
88
|
+
key,
|
|
89
|
+
dirName: buildGraphDirName(runtimeNames),
|
|
90
|
+
runtimeNames,
|
|
91
|
+
targetNames: [config.name],
|
|
92
|
+
instanceCount: config.testkit.runtime.instances,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return [...graphByKey.values()].sort((left, right) => left.dirName.localeCompare(right.dirName));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function collectRuntimeGraphStatus(productDir, { desiredGraphs = [], serviceName = null } = {}) {
|
|
100
|
+
const graphsRoot = path.join(productDir, ".testkit", "_graphs");
|
|
101
|
+
const desiredByName = new Map(desiredGraphs.map((graph) => [graph.dirName, graph]));
|
|
102
|
+
const actualByName = new Map();
|
|
103
|
+
|
|
104
|
+
if (fs.existsSync(graphsRoot)) {
|
|
105
|
+
for (const entry of fs.readdirSync(graphsRoot, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
106
|
+
if (!entry.isDirectory()) continue;
|
|
107
|
+
const graphDir = path.join(graphsRoot, entry.name);
|
|
108
|
+
actualByName.set(entry.name, {
|
|
109
|
+
name: entry.name,
|
|
110
|
+
path: graphDir,
|
|
111
|
+
metadata: readGraphMetadata(graphDir),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const activeRuntimeDirs = collectActiveRuntimeDirs(productDir);
|
|
117
|
+
const names = new Set([...desiredByName.keys(), ...actualByName.keys()]);
|
|
118
|
+
return [...names]
|
|
119
|
+
.sort()
|
|
120
|
+
.map((name) => {
|
|
121
|
+
const desired = desiredByName.get(name) || null;
|
|
122
|
+
const actual = actualByName.get(name) || null;
|
|
123
|
+
const graphDir = actual?.path || path.join(graphsRoot, name);
|
|
124
|
+
const runtimeDirs = listRuntimeDirs(graphDir);
|
|
125
|
+
const desiredRuntimeIds = desired ? buildRuntimeIds(desired.instanceCount) : [];
|
|
126
|
+
const desiredRuntimeSet = new Set(desiredRuntimeIds);
|
|
127
|
+
const staleRuntimeDirs = runtimeDirs
|
|
128
|
+
.filter((runtime) => !desiredRuntimeSet.has(runtime.id))
|
|
129
|
+
.map((runtime) => ({
|
|
130
|
+
...runtime,
|
|
131
|
+
active: activeRuntimeDirs.has(runtime.path),
|
|
132
|
+
}));
|
|
133
|
+
const currentRuntimeDirs = runtimeDirs.filter((runtime) => desiredRuntimeSet.has(runtime.id));
|
|
134
|
+
return {
|
|
135
|
+
name,
|
|
136
|
+
path: graphDir,
|
|
137
|
+
exists: Boolean(actual),
|
|
138
|
+
desired: desired
|
|
139
|
+
? {
|
|
140
|
+
instanceCount: desired.instanceCount,
|
|
141
|
+
runtimeIds: desiredRuntimeIds,
|
|
142
|
+
runtimeServices: desired.runtimeNames,
|
|
143
|
+
targetServices: desired.targetNames,
|
|
144
|
+
}
|
|
145
|
+
: null,
|
|
146
|
+
actual: actual
|
|
147
|
+
? {
|
|
148
|
+
runtimeServices: [...(actual.metadata?.runtimeServices || [])].sort(),
|
|
149
|
+
targetServices: [...(actual.metadata?.targetServices || [])].sort(),
|
|
150
|
+
}
|
|
151
|
+
: null,
|
|
152
|
+
runtimeDirCount: runtimeDirs.length,
|
|
153
|
+
currentRuntimeDirs,
|
|
154
|
+
staleRuntimeDirs,
|
|
155
|
+
orphan: !desired,
|
|
156
|
+
};
|
|
157
|
+
})
|
|
158
|
+
.filter((graph) => {
|
|
159
|
+
if (!serviceName) return true;
|
|
160
|
+
const namesForGraph = new Set([
|
|
161
|
+
...(graph.desired?.runtimeServices || []),
|
|
162
|
+
...(graph.desired?.targetServices || []),
|
|
163
|
+
...(graph.actual?.runtimeServices || []),
|
|
164
|
+
...(graph.actual?.targetServices || []),
|
|
165
|
+
]);
|
|
166
|
+
return namesForGraph.has(serviceName);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function collectBundleCacheStatus(productDir, serviceName = "shared") {
|
|
171
|
+
const dir = path.join(productDir, ".testkit", "_bundles", serviceName || "shared");
|
|
172
|
+
const summary = collectDirectorySummary(dir);
|
|
173
|
+
const files = listFiles(dir);
|
|
174
|
+
const jsFiles = files.filter((file) => file.path.endsWith(".js"));
|
|
175
|
+
const entryFiles = files.filter((file) => file.path.endsWith(".entry.mjs"));
|
|
176
|
+
const manifest = readBundleManifest(dir);
|
|
177
|
+
const manifestFiles = new Set(
|
|
178
|
+
manifest.entries.flatMap((entry) => [entry.outputFile, entry.entryFile].filter(Boolean).map((filePath) => path.resolve(filePath)))
|
|
179
|
+
);
|
|
180
|
+
const inferredSources = inferBundleSources(entryFiles);
|
|
181
|
+
const sourcesWithDuplicates = [...inferredSources.values()].filter((entries) => entries.length > 1).length;
|
|
182
|
+
const unmanagedFiles = files.filter((file) => path.basename(file.path) !== BUNDLE_MANIFEST && !manifestFiles.has(path.resolve(file.path)));
|
|
183
|
+
const sourcemaps = collectInlineSourcemapStatus(jsFiles, manifest);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
serviceName,
|
|
187
|
+
path: dir,
|
|
188
|
+
exists: summary.exists,
|
|
189
|
+
sizeBytes: summary.sizeBytes,
|
|
190
|
+
fileCount: summary.fileCount,
|
|
191
|
+
jsFileCount: jsFiles.length,
|
|
192
|
+
entryFileCount: entryFiles.length,
|
|
193
|
+
sourceFileCount: inferredSources.size || manifest.sourceFileCount,
|
|
194
|
+
duplicatedSourceCount: sourcesWithDuplicates,
|
|
195
|
+
sourcemapFileCount: sourcemaps.count,
|
|
196
|
+
sourcemapStatus: sourcemaps.status,
|
|
197
|
+
manifest: {
|
|
198
|
+
exists: manifest.exists,
|
|
199
|
+
entryCount: manifest.entries.length,
|
|
200
|
+
},
|
|
201
|
+
unmanagedFileCount: unmanagedFiles.length,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function collectAssistantResultStatus(productDir) {
|
|
206
|
+
const dir = path.join(productDir, ".testkit", "assistant", "command-results");
|
|
207
|
+
const files = listFiles(dir).sort((left, right) => right.size - left.size || left.path.localeCompare(right.path));
|
|
208
|
+
return {
|
|
209
|
+
path: dir,
|
|
210
|
+
exists: fs.existsSync(dir),
|
|
211
|
+
sizeBytes: files.reduce((sum, file) => sum + file.size, 0),
|
|
212
|
+
fileCount: files.length,
|
|
213
|
+
largeFileCount: files.filter((file) => file.size >= ASSISTANT_LARGE_RESULT_BYTES).length,
|
|
214
|
+
largestFiles: files.slice(0, 3).map((file) => ({
|
|
215
|
+
path: file.path,
|
|
216
|
+
sizeBytes: file.size,
|
|
217
|
+
modifiedAt: new Date(file.mtimeMs).toISOString(),
|
|
218
|
+
})),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function collectBundleCleanupTargets(productDir, { allConfigs = [], serviceName = null } = {}) {
|
|
223
|
+
const serviceNames = serviceName ? [serviceName] : allConfigs.map((config) => config.name).sort();
|
|
224
|
+
const targets = [];
|
|
225
|
+
for (const name of serviceNames) {
|
|
226
|
+
const dir = path.join(productDir, ".testkit", "_bundles", name || "shared");
|
|
227
|
+
const files = listFiles(dir);
|
|
228
|
+
if (files.length === 0) continue;
|
|
229
|
+
const manifest = readBundleManifest(dir);
|
|
230
|
+
const manifestTargets = collectManifestBundleCleanupTargets(manifest.entries);
|
|
231
|
+
if (manifestTargets.length > 0) {
|
|
232
|
+
targets.push(...manifestTargets.map((target) => ({ ...target, serviceName: name })));
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
targets.push(...collectLegacyBundleCleanupTargets(dir).map((target) => ({ ...target, serviceName: name })));
|
|
236
|
+
}
|
|
237
|
+
return dedupeCleanupFiles(targets);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function collectAssistantCleanupTargets(productDir) {
|
|
241
|
+
const now = Date.now();
|
|
242
|
+
const dir = path.join(productDir, ".testkit", "assistant", "command-results");
|
|
243
|
+
return listFiles(dir)
|
|
244
|
+
.filter((file) => file.size >= ASSISTANT_LARGE_RESULT_BYTES || now - file.mtimeMs >= ASSISTANT_RESULT_TTL_MS)
|
|
245
|
+
.map((file) => ({
|
|
246
|
+
path: file.path,
|
|
247
|
+
reason: file.size >= ASSISTANT_LARGE_RESULT_BYTES ? "large" : "expired",
|
|
248
|
+
sizeBytes: file.size,
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function collectRunStatus(productDir) {
|
|
253
|
+
const manifests = listRunManifests(productDir);
|
|
254
|
+
const runs = manifests.map((manifest) => ({
|
|
255
|
+
runId: manifest.runId,
|
|
256
|
+
pid: manifest.pid,
|
|
257
|
+
status: isPidRunning(manifest.pid) ? "active" : "stale",
|
|
258
|
+
startedAt: manifest.startedAt || null,
|
|
259
|
+
ports: collectRunPorts(manifest),
|
|
260
|
+
}));
|
|
261
|
+
return {
|
|
262
|
+
total: runs.length,
|
|
263
|
+
active: runs.filter((run) => run.status === "active").length,
|
|
264
|
+
stale: runs.filter((run) => run.status === "stale").length,
|
|
265
|
+
runs,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function collectRunPorts(manifest) {
|
|
270
|
+
const ports = [];
|
|
271
|
+
for (const service of manifest.services || []) {
|
|
272
|
+
for (const socket of service.ports || []) {
|
|
273
|
+
if (socket?.host && socket?.port) ports.push(`${socket.host}:${socket.port}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return [...new Set(ports)].sort();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function collectActiveRuntimeDirs(productDir) {
|
|
280
|
+
const active = new Set();
|
|
281
|
+
for (const manifest of listRunManifests(productDir)) {
|
|
282
|
+
if (!isPidRunning(manifest.pid)) continue;
|
|
283
|
+
for (const runtimeDir of manifest.runtimeDirs || []) {
|
|
284
|
+
active.add(path.resolve(runtimeDir));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return active;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function listRuntimeDirs(graphDir) {
|
|
291
|
+
const runtimesDir = path.join(graphDir, "runtimes");
|
|
292
|
+
if (!fs.existsSync(runtimesDir)) return [];
|
|
293
|
+
return fs.readdirSync(runtimesDir, { withFileTypes: true })
|
|
294
|
+
.filter((entry) => entry.isDirectory() && /^runtime-\d+$/.test(entry.name))
|
|
295
|
+
.map((entry) => ({
|
|
296
|
+
id: entry.name,
|
|
297
|
+
path: path.resolve(runtimesDir, entry.name),
|
|
298
|
+
}))
|
|
299
|
+
.sort((left, right) => runtimeIndex(left.id) - runtimeIndex(right.id) || left.id.localeCompare(right.id));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function runtimeIndex(runtimeId) {
|
|
303
|
+
return Number(String(runtimeId).match(/^runtime-(\d+)$/)?.[1] || Number.MAX_SAFE_INTEGER);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function collectDirectorySummary(dir) {
|
|
307
|
+
const files = listFiles(dir);
|
|
308
|
+
return {
|
|
309
|
+
path: dir,
|
|
310
|
+
exists: fs.existsSync(dir),
|
|
311
|
+
fileCount: files.length,
|
|
312
|
+
sizeBytes: files.reduce((sum, file) => sum + file.size, 0),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function listFiles(dir) {
|
|
317
|
+
const files = [];
|
|
318
|
+
const visit = (current) => {
|
|
319
|
+
if (!fs.existsSync(current)) return;
|
|
320
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
321
|
+
const filePath = path.join(current, entry.name);
|
|
322
|
+
if (entry.isDirectory()) {
|
|
323
|
+
visit(filePath);
|
|
324
|
+
} else if (entry.isFile()) {
|
|
325
|
+
const stat = fs.statSync(filePath);
|
|
326
|
+
files.push({
|
|
327
|
+
path: path.resolve(filePath),
|
|
328
|
+
size: stat.size,
|
|
329
|
+
mtimeMs: stat.mtimeMs,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
visit(dir);
|
|
335
|
+
return files;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function readBundleManifest(dir) {
|
|
339
|
+
const manifestPath = path.join(dir, BUNDLE_MANIFEST);
|
|
340
|
+
if (!fs.existsSync(manifestPath)) {
|
|
341
|
+
return {
|
|
342
|
+
exists: false,
|
|
343
|
+
entries: [],
|
|
344
|
+
sourceFileCount: 0,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
349
|
+
const entries = Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
350
|
+
return {
|
|
351
|
+
exists: true,
|
|
352
|
+
entries,
|
|
353
|
+
sourceFileCount: new Set(entries.map((entry) => entry.sourceFile).filter(Boolean)).size,
|
|
354
|
+
};
|
|
355
|
+
} catch {
|
|
356
|
+
return {
|
|
357
|
+
exists: true,
|
|
358
|
+
entries: [],
|
|
359
|
+
sourceFileCount: 0,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function inferBundleSources(entryFiles) {
|
|
365
|
+
const bySource = new Map();
|
|
366
|
+
for (const file of entryFiles) {
|
|
367
|
+
const sourceFile = readEntrySourceFile(file.path);
|
|
368
|
+
if (!sourceFile) continue;
|
|
369
|
+
const entries = bySource.get(sourceFile) || [];
|
|
370
|
+
entries.push(file);
|
|
371
|
+
bySource.set(sourceFile, entries);
|
|
372
|
+
}
|
|
373
|
+
return bySource;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function readEntrySourceFile(filePath) {
|
|
377
|
+
try {
|
|
378
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
379
|
+
const match = text.match(/import \* as suiteModule from ("(?:\\.|[^"])+");/);
|
|
380
|
+
return match ? JSON.parse(match[1]) : null;
|
|
381
|
+
} catch {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function collectInlineSourcemapStatus(files, manifest) {
|
|
387
|
+
if (manifest.entries.length > 0) {
|
|
388
|
+
const count = manifest.entries.filter((entry) => entry.hasSourcemap).length;
|
|
389
|
+
return {
|
|
390
|
+
count,
|
|
391
|
+
status: count > 0 ? "present" : "none",
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const sample = files.slice(0, 25);
|
|
396
|
+
const detected = sample.some((file) => fileContainsInlineSourcemap(file.path));
|
|
397
|
+
if (detected) {
|
|
398
|
+
return {
|
|
399
|
+
count: null,
|
|
400
|
+
status: "detected",
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
count: 0,
|
|
405
|
+
status: files.length > sample.length ? "not-detected-in-sample" : "none",
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function fileContainsInlineSourcemap(filePath) {
|
|
410
|
+
try {
|
|
411
|
+
return /sourceMappingURL=data:application\/json/.test(fs.readFileSync(filePath, "utf8"));
|
|
412
|
+
} catch {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function collectManifestBundleCleanupTargets(entries) {
|
|
418
|
+
if (entries.length === 0) return [];
|
|
419
|
+
const now = Date.now();
|
|
420
|
+
const bySource = groupBy(entries, (entry) => entry.sourceFile || entry.cacheKey || entry.outputFile);
|
|
421
|
+
const targets = [];
|
|
422
|
+
for (const sourceEntries of bySource.values()) {
|
|
423
|
+
const sorted = [...sourceEntries].sort(compareBundleEntryRecentFirst);
|
|
424
|
+
const kept = new Set(
|
|
425
|
+
sorted
|
|
426
|
+
.filter((entry, index) => index < 2 || now - Date.parse(entry.lastUsedAt || entry.createdAt || 0) < ASSISTANT_RESULT_TTL_MS)
|
|
427
|
+
.map((entry) => entry.cacheKey || entry.outputFile)
|
|
428
|
+
);
|
|
429
|
+
for (const entry of sorted) {
|
|
430
|
+
const key = entry.cacheKey || entry.outputFile;
|
|
431
|
+
if (kept.has(key)) continue;
|
|
432
|
+
for (const filePath of [entry.outputFile, entry.entryFile].filter(Boolean)) {
|
|
433
|
+
targets.push({
|
|
434
|
+
path: path.resolve(filePath),
|
|
435
|
+
reason: "stale-bundle-version",
|
|
436
|
+
sourceFile: entry.sourceFile || null,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return targets;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function collectLegacyBundleCleanupTargets(dir) {
|
|
445
|
+
const entryFiles = listFiles(dir).filter((file) => file.path.endsWith(".entry.mjs"));
|
|
446
|
+
const bySource = inferBundleSources(entryFiles);
|
|
447
|
+
const targets = [];
|
|
448
|
+
for (const [sourceFile, files] of bySource.entries()) {
|
|
449
|
+
const sorted = [...files].sort((left, right) => right.mtimeMs - left.mtimeMs || left.path.localeCompare(right.path));
|
|
450
|
+
for (const file of sorted.slice(2)) {
|
|
451
|
+
const outputFile = file.path.replace(/\.entry\.mjs$/, ".js");
|
|
452
|
+
targets.push({ path: file.path, reason: "stale-legacy-bundle-version", sourceFile });
|
|
453
|
+
if (fs.existsSync(outputFile)) {
|
|
454
|
+
targets.push({ path: outputFile, reason: "stale-legacy-bundle-version", sourceFile });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return targets;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function compareBundleEntryRecentFirst(left, right) {
|
|
462
|
+
return Date.parse(right.lastUsedAt || right.createdAt || 0) - Date.parse(left.lastUsedAt || left.createdAt || 0) ||
|
|
463
|
+
String(left.outputFile || "").localeCompare(String(right.outputFile || ""));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function groupBy(entries, keyFn) {
|
|
467
|
+
const grouped = new Map();
|
|
468
|
+
for (const entry of entries) {
|
|
469
|
+
const key = keyFn(entry);
|
|
470
|
+
const values = grouped.get(key) || [];
|
|
471
|
+
values.push(entry);
|
|
472
|
+
grouped.set(key, values);
|
|
473
|
+
}
|
|
474
|
+
return grouped;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function dedupeCleanupFiles(targets) {
|
|
478
|
+
const byPath = new Map();
|
|
479
|
+
for (const target of targets) {
|
|
480
|
+
if (!target.path) continue;
|
|
481
|
+
byPath.set(path.resolve(target.path), {
|
|
482
|
+
...target,
|
|
483
|
+
path: path.resolve(target.path),
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function normalizeCacheSelection(cache) {
|
|
490
|
+
const values = Array.isArray(cache) ? cache : [cache].filter(Boolean);
|
|
491
|
+
const selected = new Set(values.map(String));
|
|
492
|
+
if (selected.has("all")) {
|
|
493
|
+
selected.add("runtime");
|
|
494
|
+
selected.add("bundles");
|
|
495
|
+
selected.add("assistant");
|
|
496
|
+
}
|
|
497
|
+
return selected;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function collectWarnings({ runtimeGraphs, bundles, assistant }) {
|
|
501
|
+
const warnings = [];
|
|
502
|
+
const staleRuntimeCount = runtimeGraphs.reduce((sum, graph) => sum + graph.staleRuntimeDirs.length, 0);
|
|
503
|
+
if (staleRuntimeCount > 0) {
|
|
504
|
+
warnings.push(`${staleRuntimeCount} stale runtime ${staleRuntimeCount === 1 ? "directory" : "directories"} found outside the configured runtime set.`);
|
|
505
|
+
}
|
|
506
|
+
if (bundles.duplicatedSourceCount > 0) {
|
|
507
|
+
warnings.push(`${bundles.duplicatedSourceCount} source file${bundles.duplicatedSourceCount === 1 ? "" : "s"} have duplicate bundle versions.`);
|
|
508
|
+
}
|
|
509
|
+
if (bundles.unmanagedFileCount > 0) {
|
|
510
|
+
warnings.push(`${bundles.unmanagedFileCount} bundle cache file${bundles.unmanagedFileCount === 1 ? "" : "s"} are unmanaged by the manifest.`);
|
|
511
|
+
}
|
|
512
|
+
if (bundles.sourcemapFileCount > 0 || bundles.sourcemapStatus === "detected") {
|
|
513
|
+
const count = bundles.sourcemapFileCount == null ? "Legacy bundle files" : `${bundles.sourcemapFileCount} bundle file${bundles.sourcemapFileCount === 1 ? "" : "s"}`;
|
|
514
|
+
warnings.push(`${count} contain inline sourcemaps.`);
|
|
515
|
+
}
|
|
516
|
+
if (assistant.largeFileCount > 0) {
|
|
517
|
+
warnings.push(`${assistant.largeFileCount} assistant command result file${assistant.largeFileCount === 1 ? "" : "s"} exceed the large-file threshold.`);
|
|
518
|
+
}
|
|
519
|
+
return warnings;
|
|
520
|
+
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import {
|
|
2
|
+
RUN_TYPE_ORDER,
|
|
3
|
+
RUN_TYPES,
|
|
4
|
+
TEST_TYPE_ORDER,
|
|
5
|
+
TEST_TYPES,
|
|
6
|
+
normalizePublicTestType,
|
|
7
|
+
publicTestTypeListText,
|
|
8
|
+
} from "../domain/test-types.mjs";
|
|
2
9
|
|
|
3
10
|
export function normalizeTypeValues(values = []) {
|
|
4
11
|
const expanded = [];
|
|
@@ -7,12 +14,13 @@ export function normalizeTypeValues(values = []) {
|
|
|
7
14
|
for (const part of String(rawValue).split(",")) {
|
|
8
15
|
const value = part.trim();
|
|
9
16
|
if (!value) continue;
|
|
10
|
-
|
|
17
|
+
const normalized = normalizePublicTestType(value);
|
|
18
|
+
if (!RUN_TYPES.has(normalized)) {
|
|
11
19
|
throw new Error(
|
|
12
|
-
`Unknown type "${value}". Expected one of:
|
|
20
|
+
`Unknown type "${value}". Expected one of: ${publicTestTypeListText({ includeAll: true })}.`
|
|
13
21
|
);
|
|
14
22
|
}
|
|
15
|
-
expanded.push(
|
|
23
|
+
expanded.push(normalized);
|
|
16
24
|
}
|
|
17
25
|
}
|
|
18
26
|
|
|
@@ -25,8 +33,7 @@ export function normalizeTypeValues(values = []) {
|
|
|
25
33
|
throw new Error(`"--type all" cannot be combined with other types.`);
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
return deduped.sort((left, right) => order.indexOf(left) - order.indexOf(right));
|
|
36
|
+
return deduped.sort((left, right) => RUN_TYPE_ORDER.indexOf(left) - RUN_TYPE_ORDER.indexOf(right));
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
export function isAllTypeSelection(typeValues = []) {
|
|
@@ -50,9 +57,10 @@ export function parseSuiteSelectors(values = []) {
|
|
|
50
57
|
|
|
51
58
|
const type = typeMatch[1];
|
|
52
59
|
const name = typeMatch[2].trim();
|
|
53
|
-
|
|
60
|
+
const normalizedType = normalizePublicTestType(type);
|
|
61
|
+
if (!TEST_TYPES.has(normalizedType)) {
|
|
54
62
|
throw new Error(
|
|
55
|
-
`Unknown suite selector type "${type}". Expected one of:
|
|
63
|
+
`Unknown suite selector type "${type}". Expected one of: ${publicTestTypeListText()}.`
|
|
56
64
|
);
|
|
57
65
|
}
|
|
58
66
|
if (!name) {
|
|
@@ -61,9 +69,9 @@ export function parseSuiteSelectors(values = []) {
|
|
|
61
69
|
|
|
62
70
|
selectors.push({
|
|
63
71
|
kind: "typed",
|
|
64
|
-
type,
|
|
72
|
+
type: normalizedType,
|
|
65
73
|
name,
|
|
66
|
-
raw: `${
|
|
74
|
+
raw: `${normalizedType}:${name}`,
|
|
67
75
|
});
|
|
68
76
|
}
|
|
69
77
|
}
|
|
@@ -72,8 +80,9 @@ export function parseSuiteSelectors(values = []) {
|
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
export function suiteSelectionType(type, framework) {
|
|
75
|
-
if ((framework || "k6") === "playwright") return "
|
|
83
|
+
if (type === "ui" || (framework || "k6") === "playwright") return "ui";
|
|
76
84
|
if (type === "integration") return "int";
|
|
85
|
+
if (TEST_TYPE_ORDER.includes(type)) return type;
|
|
77
86
|
return type;
|
|
78
87
|
}
|
|
79
88
|
|
|
@@ -31,7 +31,7 @@ const CONFIG_NEXT_TSCONFIG_ENTRY = path.join(
|
|
|
31
31
|
);
|
|
32
32
|
const DRIZZLE_ENTRY = path.join(PACKAGE_ROOT, "lib", "drizzle", "index.mjs");
|
|
33
33
|
const ENV_ENTRY = path.join(PACKAGE_ROOT, "lib", "env", "index.mjs");
|
|
34
|
-
const
|
|
34
|
+
const UI_ENTRY = path.join(PACKAGE_ROOT, "lib", "ui", "index.mjs");
|
|
35
35
|
const RUNTIME_ENTRY = path.join(PACKAGE_ROOT, "lib", "runtime", "index.mjs");
|
|
36
36
|
const VITEST_ENTRY = path.join(PACKAGE_ROOT, "lib", "vitest", "index.mjs");
|
|
37
37
|
const DISCOVERY_ENTRY = path.join(PACKAGE_ROOT, "lib", "discovery", "index.mjs");
|
|
@@ -259,7 +259,7 @@ function resolvePackageSubpath(specifier) {
|
|
|
259
259
|
if (subpath === "/config/next-runtime-tsconfig") return CONFIG_NEXT_TSCONFIG_ENTRY;
|
|
260
260
|
if (subpath === "/drizzle") return DRIZZLE_ENTRY;
|
|
261
261
|
if (subpath === "/env") return ENV_ENTRY;
|
|
262
|
-
if (subpath === "/
|
|
262
|
+
if (subpath === "/ui") return UI_ENTRY;
|
|
263
263
|
if (subpath === "/runtime") return RUNTIME_ENTRY;
|
|
264
264
|
if (subpath === "/vitest") return VITEST_ENTRY;
|
|
265
265
|
if (subpath === "/discovery") return DISCOVERY_ENTRY;
|
package/lib/runner/template.mjs
CHANGED
|
@@ -119,6 +119,7 @@ export function resolveRuntimeConfig(
|
|
|
119
119
|
runtimeId,
|
|
120
120
|
runtimeLabel,
|
|
121
121
|
runtimeDir,
|
|
122
|
+
productDir: config.productDir,
|
|
122
123
|
serviceName: config.name,
|
|
123
124
|
serviceStateDir: stateDir,
|
|
124
125
|
prepareDir,
|
|
@@ -258,6 +259,7 @@ function buildTemplateContext(config, lease) {
|
|
|
258
259
|
...baseContext,
|
|
259
260
|
runtimeId: config.runtimeId || baseContext.runtimeId || null,
|
|
260
261
|
runtimeLabel: config.runtimeLabel || baseContext.runtimeLabel || null,
|
|
262
|
+
productDir: config.productDir || baseContext.productDir || null,
|
|
261
263
|
serviceName: config.name,
|
|
262
264
|
serviceStateDir: config.stateDir || baseContext.serviceStateDir || null,
|
|
263
265
|
prepareDir: config.testkit?.prepareDir || baseContext.prepareDir || null,
|
|
@@ -298,6 +300,8 @@ export function resolveTemplateString(value, context) {
|
|
|
298
300
|
return context.serviceName;
|
|
299
301
|
case "stateDir":
|
|
300
302
|
return context.serviceStateDir;
|
|
303
|
+
case "productDir":
|
|
304
|
+
return context.productDir || "";
|
|
301
305
|
case "prepareDir":
|
|
302
306
|
return context.prepareDir || "";
|
|
303
307
|
case "lease":
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@playwright/test";
|
package/lib/ui/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@playwright/test";
|
package/lib/vitest/index.mjs
CHANGED