@elench/testkit 0.1.108 → 0.1.110

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.
Files changed (69) hide show
  1. package/README.md +9 -9
  2. package/lib/app/doctor.mjs +5 -5
  3. package/lib/app/typecheck.mjs +6 -5
  4. package/lib/bundler/index.mjs +134 -7
  5. package/lib/cli/args.mjs +3 -2
  6. package/lib/cli/assistant/app.mjs +19 -5
  7. package/lib/cli/assistant/command-observer.mjs +2 -1
  8. package/lib/cli/assistant/command-results.mjs +2 -1
  9. package/lib/cli/assistant/context-pack.mjs +2 -2
  10. package/lib/cli/assistant/prompt-builder.mjs +2 -2
  11. package/lib/cli/assistant/quality-signal-strip.mjs +103 -0
  12. package/lib/cli/assistant/transcript-text.mjs +2 -1
  13. package/lib/cli/assistant/view-model.mjs +79 -0
  14. package/lib/cli/command-flags.mjs +2 -1
  15. package/lib/cli/commands/cleanup.mjs +13 -2
  16. package/lib/cli/commands/discover.mjs +2 -1
  17. package/lib/cli/commands/run.mjs +3 -2
  18. package/lib/cli/entrypoint.mjs +3 -1
  19. package/lib/cli/operations/cleanup/operation.mjs +6 -1
  20. package/lib/cli/operations/status/operation.mjs +2 -2
  21. package/lib/cli/renderers/discover/report.mjs +6 -8
  22. package/lib/cli/renderers/run/failure.mjs +1 -1
  23. package/lib/cli/renderers/run/text-reporter.mjs +1 -1
  24. package/lib/cli/renderers/status/text.mjs +101 -1
  25. package/lib/config/discovery.mjs +10 -1
  26. package/lib/config-api/index.mjs +2 -2
  27. package/lib/config-api/next-runtime-tsconfig.mjs +2 -1
  28. package/lib/coverage/graph-builder.mjs +2 -4
  29. package/lib/coverage/routing.mjs +1 -1
  30. package/lib/coverage/shared.mjs +1 -2
  31. package/lib/discovery/index.d.ts +5 -8
  32. package/lib/discovery/index.mjs +15 -24
  33. package/lib/domain/test-types.mjs +44 -0
  34. package/lib/history/index.d.ts +3 -4
  35. package/lib/history/index.mjs +6 -14
  36. package/lib/runner/formatting.mjs +2 -3
  37. package/lib/runner/maintenance.mjs +136 -35
  38. package/lib/runner/planning.mjs +1 -1
  39. package/lib/runner/results.mjs +0 -6
  40. package/lib/runner/status-model.mjs +520 -0
  41. package/lib/runner/suite-selection.mjs +20 -11
  42. package/lib/runner/template-steps.mjs +2 -2
  43. package/lib/runner/template.mjs +4 -0
  44. package/lib/ui/index.d.ts +1 -0
  45. package/lib/ui/index.mjs +1 -0
  46. package/lib/vitest/index.mjs +2 -1
  47. package/node_modules/@elench/next-analysis/package.json +1 -1
  48. package/node_modules/@elench/testkit-bridge/dist/index.js +9 -11
  49. package/node_modules/@elench/testkit-bridge/dist/index.js.map +1 -1
  50. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  51. package/node_modules/@elench/testkit-protocol/dist/index.d.ts +1 -3
  52. package/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +1 -1
  53. package/node_modules/@elench/testkit-protocol/dist/index.js +3 -6
  54. package/node_modules/@elench/testkit-protocol/dist/index.js.map +1 -1
  55. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  56. package/node_modules/@elench/ts-analysis/dist/requests.js +1 -1
  57. package/node_modules/@elench/ts-analysis/package.json +1 -1
  58. package/package.json +9 -9
  59. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts +188 -0
  60. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +1 -0
  61. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js +293 -0
  62. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/dist/index.js.map +1 -0
  63. package/packages/testkit-bridge/node_modules/@elench/testkit-protocol/package.json +25 -0
  64. package/node_modules/es-toolkit/CHANGELOG.md +0 -801
  65. package/node_modules/es-toolkit/src/compat/_internal/Equals.d.ts +0 -1
  66. package/node_modules/es-toolkit/src/compat/_internal/IsWritable.d.ts +0 -3
  67. package/node_modules/es-toolkit/src/compat/_internal/MutableList.d.ts +0 -4
  68. package/node_modules/es-toolkit/src/compat/_internal/RejectReadonly.d.ts +0 -4
  69. package/node_modules/esprima/ChangeLog +0 -235
@@ -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
- const USER_TYPES = new Set(["int", "e2e", "scenario", "dal", "load", "pw", "all"]);
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
- if (!USER_TYPES.has(value)) {
17
+ const normalized = normalizePublicTestType(value);
18
+ if (!RUN_TYPES.has(normalized)) {
11
19
  throw new Error(
12
- `Unknown type "${value}". Expected one of: int, e2e, scenario, dal, load, pw, all.`
20
+ `Unknown type "${value}". Expected one of: ${publicTestTypeListText({ includeAll: true })}.`
13
21
  );
14
22
  }
15
- expanded.push(value);
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
- const order = ["int", "e2e", "scenario", "dal", "load", "pw", "all"];
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
- if (!USER_TYPES.has(type) || type === "all") {
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: int, e2e, scenario, dal, load, pw.`
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: `${type}:${name}`,
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 "pw";
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 PLAYWRIGHT_ENTRY = path.join(PACKAGE_ROOT, "lib", "playwright", "index.mjs");
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 === "/playwright") return PLAYWRIGHT_ENTRY;
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;
@@ -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";
@@ -0,0 +1 @@
1
+ export * from "@playwright/test";
@@ -4,7 +4,8 @@ const TESTKIT_SUITE_GLOBS = [
4
4
  "**/*.scenario.testkit.ts",
5
5
  "**/*.dal.testkit.ts",
6
6
  "**/*.load.testkit.ts",
7
- "**/*.pw.testkit.ts",
7
+ "**/*.ui.testkit.ts",
8
+ "**/*.ui.testkit.ts",
8
9
  ];
9
10
 
10
11
  export function defineConfig(config = {}, options = {}) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/next-analysis",
3
- "version": "0.1.108",
3
+ "version": "0.1.110",
4
4
  "description": "SWC-backed Next.js source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {