@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
@@ -12,6 +12,7 @@ export function buildAssistantViewModel(snapshot, { cwd = process.cwd(), termina
12
12
  return {
13
13
  title: `testkit · ${repoName}`,
14
14
  welcome: buildWelcomeModel(snapshot, { cwd, providerLabel }),
15
+ qualitySignal: buildQualitySignal(snapshot),
15
16
  blocks: buildTranscriptBlocks(snapshot.messages || []),
16
17
  composer: {
17
18
  text: snapshot.composer || "",
@@ -25,6 +26,84 @@ export function buildAssistantViewModel(snapshot, { cwd = process.cwd(), termina
25
26
  };
26
27
  }
27
28
 
29
+ export function buildQualitySignal(snapshot = {}) {
30
+ const summaryRows = snapshot?.run?.summaryData?.rows || snapshot?.summaryData?.rows || [];
31
+ const rowValue = (label) => summaryRows.find(([key]) => key === label)?.[1] || null;
32
+ const latestResult = rowValue("Result");
33
+ const summaryFiles = rowValue("Files");
34
+ const plannedFiles = snapshot?.run?.totalCount ? String(snapshot.run.totalCount) : null;
35
+ const files = summaryFiles || plannedFiles;
36
+ const failed = rowValue("Failed");
37
+ const duration = rowValue("Duration");
38
+ const newRegressions = rowValue("New regressions");
39
+ const fixedKnown = rowValue("Fixed known");
40
+ const contextSelection = snapshot?.context?.selection || {};
41
+ const items = [];
42
+
43
+ if (files && files !== "0") {
44
+ items.push({
45
+ id: summaryFiles ? "tested-files" : "planned-files",
46
+ text: `${files} ${summaryFiles ? "tested" : "planned"}`,
47
+ tone: "neutral",
48
+ });
49
+ }
50
+
51
+ if (newRegressions && newRegressions !== "0") {
52
+ items.push({
53
+ id: "new-regressions",
54
+ text: `${newRegressions} new regression${newRegressions === "1" ? "" : "s"}`,
55
+ tone: "danger",
56
+ });
57
+ } else if (fixedKnown && fixedKnown !== "0") {
58
+ items.push({
59
+ id: "fixed-known",
60
+ text: `${fixedKnown} fixed known`,
61
+ tone: "good",
62
+ });
63
+ } else if (latestResult === "PASSED") {
64
+ items.push({
65
+ id: "no-regressions",
66
+ text: "no regressions",
67
+ tone: "good",
68
+ });
69
+ } else if (latestResult === "FAILED" && failed && failed !== "0") {
70
+ items.push({
71
+ id: "failed-files",
72
+ text: `${failed} failed`,
73
+ tone: "danger",
74
+ });
75
+ }
76
+
77
+ if (latestResult) {
78
+ const status = latestResult === "PASSED" ? "passed" : latestResult === "FAILED" ? "failed" : latestResult.toLowerCase();
79
+ items.push({
80
+ id: "latest-status",
81
+ text: duration ? `${status} in ${duration}` : status,
82
+ tone: latestResult === "PASSED" ? "good" : latestResult === "FAILED" ? "danger" : "neutral",
83
+ });
84
+ }
85
+
86
+ if (contextSelection.filePath) {
87
+ items.push({
88
+ id: "focus",
89
+ text: `focus ${path.basename(contextSelection.filePath)}`,
90
+ tone: "progress",
91
+ });
92
+ } else if (!latestResult) {
93
+ items.push({
94
+ id: "ready",
95
+ text: "ready to run",
96
+ tone: "progress",
97
+ });
98
+ }
99
+
100
+ return {
101
+ label: "Quality signal",
102
+ tone: latestResult === "FAILED" ? "danger" : latestResult === "PASSED" ? "good" : "neutral",
103
+ items,
104
+ };
105
+ }
106
+
28
107
  export function buildWelcomeModel(snapshot, { cwd = process.cwd(), providerLabel = null } = {}) {
29
108
  const summaryRows = snapshot?.run?.summaryData?.rows || snapshot?.summaryData?.rows || [];
30
109
  const rowValue = (label) => summaryRows.find(([key]) => key === label)?.[1] || null;
@@ -1,4 +1,5 @@
1
1
  import { Flags } from "@oclif/core";
2
+ import { publicTestTypeListText } from "../domain/test-types.mjs";
2
3
 
3
4
  export const sharedFlags = {
4
5
  dir: Flags.string({
@@ -14,7 +15,7 @@ export const runFlags = {
14
15
  type: Flags.string({
15
16
  char: "t",
16
17
  multiple: true,
17
- description: "Run specific suite type(s): int, e2e, scenario, dal, load, pw, all",
18
+ description: `Run specific suite type(s): ${publicTestTypeListText({ includeAll: true })}`,
18
19
  }),
19
20
  suite: Flags.string({
20
21
  char: "s",
@@ -1,4 +1,4 @@
1
- import { Command } from "@oclif/core";
1
+ import { Command, Flags } from "@oclif/core";
2
2
  import { sharedFlags } from "../command-flags.mjs";
3
3
  import { executeCleanupOperation } from "../operations/cleanup/operation.mjs";
4
4
  import { renderCleanupResult } from "../renderers/cleanup/text.mjs";
@@ -8,7 +8,18 @@ export default class CleanupCommand extends Command {
8
8
 
9
9
  static enableJsonFlag = true;
10
10
 
11
- static flags = sharedFlags;
11
+ static flags = {
12
+ ...sharedFlags,
13
+ "dry-run": Flags.boolean({
14
+ description: "Show cleanup actions without deleting state",
15
+ default: false,
16
+ }),
17
+ cache: Flags.string({
18
+ description: "Clean cache state: runtime, bundles, assistant, or all",
19
+ multiple: true,
20
+ options: ["runtime", "bundles", "assistant", "all"],
21
+ }),
22
+ };
12
23
 
13
24
  async run() {
14
25
  const { flags } = await this.parse(CleanupCommand);
@@ -3,6 +3,7 @@ import { executeDiscoverOperation } from "../operations/discover/operation.mjs";
3
3
  import { renderDiscoverResult } from "../renderers/discover/text.mjs";
4
4
  import { sharedFlags } from "../command-flags.mjs";
5
5
  import { withAssistantCommandResult } from "../assistant/command-results.mjs";
6
+ import { publicTestTypeListText } from "../../domain/test-types.mjs";
6
7
 
7
8
  export default class DiscoverCommand extends Command {
8
9
  static summary = "Discover managed tests and report their metadata";
@@ -14,7 +15,7 @@ export default class DiscoverCommand extends Command {
14
15
  type: Flags.string({
15
16
  char: "t",
16
17
  multiple: true,
17
- description: "Filter by suite type(s): int, e2e, scenario, dal, load, pw, all",
18
+ description: `Filter by suite type(s): ${publicTestTypeListText({ includeAll: true })}`,
18
19
  }),
19
20
  suite: Flags.string({
20
21
  char: "s",
@@ -3,6 +3,7 @@ import { runFlags } from "../command-flags.mjs";
3
3
  import { buildRunRequest, executeRunRequest } from "../operations/run/operation.mjs";
4
4
  import { resolveTerminalCapabilities } from "../terminal/capabilities.mjs";
5
5
  import { withAssistantCommandResult } from "../assistant/command-results.mjs";
6
+ import { publicTestTypeList, publicTestTypeListText } from "../../domain/test-types.mjs";
6
7
 
7
8
  export default class RunCommand extends Command {
8
9
  static summary = "Run test suites";
@@ -11,9 +12,9 @@ export default class RunCommand extends Command {
11
12
 
12
13
  static args = {
13
14
  type: Args.string({
14
- description: "Optional suite type shortcut: int, e2e, scenario, dal, load, pw, all",
15
+ description: `Optional suite type shortcut: ${publicTestTypeListText({ includeAll: true })}`,
15
16
  required: false,
16
- options: ["int", "e2e", "scenario", "dal", "load", "pw", "all"],
17
+ options: publicTestTypeList({ includeAll: true, includeLegacy: true }),
17
18
  }),
18
19
  };
19
20
 
@@ -1,3 +1,5 @@
1
+ import { publicTestTypeList } from "../domain/test-types.mjs";
2
+
1
3
  export function normalizeCliArgs(argv) {
2
4
  if (argv[0] === "help") return normalizeHelpInvocation(argv);
3
5
  if (isOclifBuiltinInvocation(argv)) return argv;
@@ -14,7 +16,7 @@ export function normalizeCliArgs(argv) {
14
16
  "browser",
15
17
  "db",
16
18
  ]);
17
- const runTypeShortcuts = new Set(["int", "e2e", "scenario", "dal", "load", "pw", "all"]);
19
+ const runTypeShortcuts = new Set(publicTestTypeList({ includeAll: true, includeLegacy: true }));
18
20
  const valueFlags = new Set([
19
21
  "--dir",
20
22
  "--service",
@@ -4,5 +4,10 @@ import { loadManagedConfigs } from "../../../app/configs.mjs";
4
4
  export async function executeCleanupOperation(flags = {}) {
5
5
  const { allConfigs } = await loadManagedConfigs({ dir: flags.dir, service: flags.service });
6
6
  const productDir = allConfigs[0]?.productDir || process.cwd();
7
- return runner.cleanup(productDir);
7
+ return runner.cleanup(productDir, {
8
+ allConfigs,
9
+ serviceName: flags.service || null,
10
+ dryRun: flags["dry-run"],
11
+ cache: flags.cache || [],
12
+ });
8
13
  }
@@ -2,6 +2,6 @@ import * as runner from "../../../runner/index.mjs";
2
2
  import { loadManagedConfigs } from "../../../app/configs.mjs";
3
3
 
4
4
  export async function executeStatusOperation(flags = {}) {
5
- const { configs } = await loadManagedConfigs({ dir: flags.dir, service: flags.service });
6
- return configs.map((config) => runner.showStatus(config));
5
+ const { allConfigs, configs } = await loadManagedConfigs({ dir: flags.dir, service: flags.service });
6
+ return configs.map((config) => runner.showStatus(config, { allConfigs }));
7
7
  }
@@ -10,7 +10,7 @@ import {
10
10
  statusLabel,
11
11
  } from "../../terminal/colors.mjs";
12
12
 
13
- const TYPE_ORDER = ["int", "e2e", "scenario", "dal", "load", "pw"];
13
+ const TYPE_ORDER = ["ui", "e2e", "scenario", "int", "dal", "load"];
14
14
 
15
15
  const TREE_BRANCH = "\u251C\u2500\u2500 ";
16
16
  const TREE_LAST = "\u2514\u2500\u2500 ";
@@ -98,13 +98,11 @@ function buildVerboseLines(result) {
98
98
 
99
99
  const suites = result.suites.filter((suite) => suite.service === service.name).sort(compareSuites);
100
100
  for (const suite of suites) {
101
- lines.push(
102
- ` suite ${suite.selectionType}:${suite.name} ${muted(`[${suite.framework}]`)} ${muted(`${suite.fileCount} files`)}`
103
- );
101
+ lines.push(` suite ${suite.type}:${suite.name} ${muted(`${suite.fileCount} files`)}`);
104
102
  if (suite.locks.length > 0) {
105
103
  lines.push(` locks ${suite.locks.join(", ")}`);
106
104
  }
107
- for (const file of result.files.filter((entry) => entry.service === service.name && entry.suiteName === suite.name && entry.selectionType === suite.selectionType)) {
105
+ for (const file of result.files.filter((entry) => entry.service === service.name && entry.suiteName === suite.name && entry.type === suite.type)) {
108
106
  lines.push(` ${file.displayName}`);
109
107
  lines.push(` path ${file.path}`);
110
108
  lines.push(` id ${file.id}`);
@@ -142,9 +140,9 @@ function groupSuitesByServiceAndType(suites) {
142
140
  byType = new Map();
143
141
  grouped.set(suite.service, byType);
144
142
  }
145
- const list = byType.get(suite.selectionType) || [];
143
+ const list = byType.get(suite.type) || [];
146
144
  list.push(suite);
147
- byType.set(suite.selectionType, list.sort(compareSuites));
145
+ byType.set(suite.type, list.sort(compareSuites));
148
146
  }
149
147
  return grouped;
150
148
  }
@@ -152,7 +150,7 @@ function groupSuitesByServiceAndType(suites) {
152
150
  function groupFilesBySuite(files) {
153
151
  const bySuite = new Map();
154
152
  for (const file of files) {
155
- const suiteId = [file.service, file.selectionType, file.framework, file.suiteName].join("|");
153
+ const suiteId = [file.service, file.type, file.suiteName].join("|");
156
154
  const list = bySuite.get(suiteId) || [];
157
155
  list.push(file);
158
156
  bySuite.set(
@@ -25,7 +25,7 @@ export function renderFailureBlock(task, outcome, { width, regressionCatalog } =
25
25
  }
26
26
 
27
27
  function normalizeRegressionType(task) {
28
- if (task.framework === "playwright") return "pw";
28
+ if (task.framework === "playwright" || task.type === "ui") return "ui";
29
29
  if (task.type === "integration") return "int";
30
30
  return task.type;
31
31
  }
@@ -157,7 +157,7 @@ export function createRunReporter({ outputMode = "compact", stdout = process.std
157
157
  }
158
158
 
159
159
  function displayTaskType(task) {
160
- if (task.framework === "playwright") return "pw";
160
+ if (task.framework === "playwright" || task.type === "ui") return "ui";
161
161
  if (task.type === "integration") return "int";
162
162
  return task.type;
163
163
  }
@@ -1,7 +1,107 @@
1
1
  export function renderStatusResult(result) {
2
- return normalizeLines(result?.lines);
2
+ if (Array.isArray(result?.lines)) return normalizeLines(result.lines);
3
+ if (!result) return [];
4
+
5
+ const lines = [
6
+ `Testkit Status: ${result.name || result.product?.selectedService || "service"}`,
7
+ "",
8
+ "Product",
9
+ ` dir: ${result.product?.dir || "unknown"}`,
10
+ ` config: ${result.product?.configFile || "none"}`,
11
+ ` services: ${formatList(result.product?.services)}`,
12
+ ` dependencies: ${formatList(result.product?.dependencies)}`,
13
+ "",
14
+ "Runs",
15
+ ` active: ${result.runs?.active || 0}`,
16
+ ` stale: ${result.runs?.stale || 0}`,
17
+ ];
18
+
19
+ for (const run of (result.runs?.runs || []).slice(0, 5)) {
20
+ const ports = run.ports?.length ? ` ports=${run.ports.join(",")}` : "";
21
+ lines.push(` ${run.runId}: ${run.status} pid=${run.pid}${ports}`);
22
+ }
23
+ if ((result.runs?.runs || []).length > 5) {
24
+ lines.push(` ... ${(result.runs.runs.length - 5)} more run${result.runs.runs.length - 5 === 1 ? "" : "s"}`);
25
+ }
26
+
27
+ lines.push("", "Runtime Graphs");
28
+ if ((result.runtimeGraphs || []).length === 0) {
29
+ lines.push(" none");
30
+ } else {
31
+ for (const graph of result.runtimeGraphs || []) {
32
+ const marker = graph.orphan ? " orphan" : "";
33
+ lines.push(` ${graph.name}${marker}`);
34
+ lines.push(` desired instances: ${graph.desired?.instanceCount ?? "none"}`);
35
+ lines.push(` runtime dirs: ${graph.runtimeDirCount || 0}`);
36
+ lines.push(` current: ${formatRuntimeRange(graph.currentRuntimeDirs?.map((runtime) => runtime.id))}`);
37
+ lines.push(` stale: ${formatRuntimeRange(graph.staleRuntimeDirs?.map((runtime) => runtime.id))}`);
38
+ lines.push(` runtime services: ${formatList(graph.desired?.runtimeServices || graph.actual?.runtimeServices)}`);
39
+ lines.push(` target services: ${formatList(graph.desired?.targetServices || graph.actual?.targetServices)}`);
40
+ }
41
+ }
42
+
43
+ lines.push(
44
+ "",
45
+ "Caches",
46
+ " bundles",
47
+ ` size: ${formatBytes(result.caches?.bundles?.sizeBytes || 0)}`,
48
+ ` files: ${result.caches?.bundles?.fileCount || 0}`,
49
+ ` source files: ${result.caches?.bundles?.sourceFileCount || 0}`,
50
+ ` duplicated sources: ${result.caches?.bundles?.duplicatedSourceCount || 0}`,
51
+ ` inline sourcemaps: ${formatSourcemaps(result.caches?.bundles)}`,
52
+ ` manifest entries: ${result.caches?.bundles?.manifest?.entryCount || 0}`,
53
+ ` unmanaged files: ${result.caches?.bundles?.unmanagedFileCount || 0}`,
54
+ " assistant command results",
55
+ ` size: ${formatBytes(result.caches?.assistant?.sizeBytes || 0)}`,
56
+ ` files: ${result.caches?.assistant?.fileCount || 0}`,
57
+ ` large files: ${result.caches?.assistant?.largeFileCount || 0}`
58
+ );
59
+
60
+ if (result.warnings?.length) {
61
+ lines.push("", "Warnings");
62
+ for (const warning of result.warnings) lines.push(` - ${warning}`);
63
+ }
64
+
65
+ if (!result.hasState) {
66
+ lines.push("", "No state — run tests first.");
67
+ }
68
+
69
+ return lines;
3
70
  }
4
71
 
5
72
  function normalizeLines(lines) {
6
73
  return Array.isArray(lines) ? lines : [];
7
74
  }
75
+
76
+ function formatList(values) {
77
+ const list = Array.isArray(values) ? values.filter(Boolean) : [];
78
+ return list.length > 0 ? list.join(", ") : "none";
79
+ }
80
+
81
+ function formatRuntimeRange(ids) {
82
+ const values = Array.isArray(ids) ? ids : [];
83
+ if (values.length === 0) return "none";
84
+ if (values.length === 1) return values[0];
85
+ return `${values[0]}..${values.at(-1)}`;
86
+ }
87
+
88
+ function formatBytes(value) {
89
+ const bytes = Number(value || 0);
90
+ if (bytes < 1024) return `${bytes}B`;
91
+ const units = ["KB", "MB", "GB", "TB"];
92
+ let amount = bytes / 1024;
93
+ let unitIndex = 0;
94
+ while (amount >= 1024 && unitIndex < units.length - 1) {
95
+ amount /= 1024;
96
+ unitIndex += 1;
97
+ }
98
+ return `${amount >= 10 ? amount.toFixed(0) : amount.toFixed(1)}${units[unitIndex]}`;
99
+ }
100
+
101
+ function formatSourcemaps(bundles = {}) {
102
+ if (bundles.sourcemapFileCount == null) return bundles.sourcemapStatus || "unknown";
103
+ if (bundles.sourcemapStatus && bundles.sourcemapStatus !== "present" && bundles.sourcemapStatus !== "none") {
104
+ return `${bundles.sourcemapFileCount} (${bundles.sourcemapStatus})`;
105
+ }
106
+ return String(bundles.sourcemapFileCount || 0);
107
+ }
@@ -15,7 +15,8 @@ const DISCOVERY_RULES = [
15
15
  { suffix: ".scenario.testkit.ts", type: "scenario", framework: "k6" },
16
16
  { suffix: ".dal.testkit.ts", type: "dal", framework: "k6" },
17
17
  { suffix: ".load.testkit.ts", type: "load", framework: "k6" },
18
- { suffix: ".pw.testkit.ts", type: "e2e", framework: "playwright" },
18
+ { suffix: ".ui.testkit.ts", type: "ui", framework: "playwright" },
19
+ { suffix: ".pw.testkit.ts", type: "ui", framework: "playwright", legacySuffix: true },
19
20
  ];
20
21
 
21
22
  export function discoverProject(productDir, explicitServices = {}, options = {}) {
@@ -30,6 +31,14 @@ export function discoverProject(productDir, explicitServices = {}, options = {})
30
31
  for (const filePath of suiteFiles) {
31
32
  const rule = inferRule(filePath);
32
33
  if (!rule) continue;
34
+ if (rule.legacySuffix) {
35
+ diagnostics.push({
36
+ code: "legacy_ui_suffix",
37
+ severity: "warning",
38
+ message: `Legacy UI test suffix ".pw.testkit.ts" is deprecated. Rename to ".ui.testkit.ts": ${filePath}`,
39
+ path: filePath,
40
+ });
41
+ }
33
42
 
34
43
  const owners = inferOwners(filePath, explicitServices, repoDiscovery);
35
44
  if (owners === null) continue;
@@ -225,9 +225,9 @@ function nextApp(options = {}) {
225
225
  readyTimeoutMs,
226
226
  env: mode === "start"
227
227
  ? {
228
- NEXT_DIST_DIR: normalizedEnv.NEXT_DIST_DIR || ".next-testkit/{runtimeId}/dist",
228
+ NEXT_DIST_DIR: normalizedEnv.NEXT_DIST_DIR || "{productDir}/.next-testkit/{runtimeId}/dist",
229
229
  NEXT_TSCONFIG_PATH:
230
- normalizedEnv.NEXT_TSCONFIG_PATH || ".next-testkit/{runtimeId}/tsconfig.json",
230
+ normalizedEnv.NEXT_TSCONFIG_PATH || "{productDir}/.next-testkit/{runtimeId}/tsconfig.json",
231
231
  ...normalizedEnv,
232
232
  }
233
233
  : normalizedEnv,
@@ -3,12 +3,13 @@ import path from "path";
3
3
 
4
4
  export async function writeNextRuntimeTsconfig(context = {}) {
5
5
  const serviceDir = context.cwd || context.productDir;
6
+ const productDir = context.productDir || serviceDir;
6
7
  const runtimeId = context.runtimeId;
7
8
  if (!serviceDir || !runtimeId) {
8
9
  throw new Error("writeNextRuntimeTsconfig requires cwd and runtimeId");
9
10
  }
10
11
 
11
- const runtimeRoot = path.join(serviceDir, ".next-testkit", String(runtimeId));
12
+ const runtimeRoot = path.join(productDir, ".next-testkit", String(runtimeId));
12
13
  const outputPath = path.join(runtimeRoot, "tsconfig.json");
13
14
  const outputDir = path.dirname(outputPath);
14
15
  const relative = (target) => path.relative(outputDir, target).replaceAll(path.sep, "/");
@@ -70,8 +70,7 @@ export function buildCoverageGraph({ productDir, repoDiscovery = {}, services =
70
70
  confidence: "high",
71
71
  service: entry.serviceName,
72
72
  suiteName: entry.suiteName,
73
- selectionType: toSelectionType(entry.type, entry.framework),
74
- framework: entry.framework,
73
+ type: toSelectionType(entry.type, entry.framework),
75
74
  testFilePath: entry.filePath,
76
75
  coveredNodeIds,
77
76
  details: buildEvidenceDetails(coveredNodeIds, graph, entry, context),
@@ -173,8 +172,7 @@ function createFallbackEvidence(entry, testNodeId) {
173
172
  confidence: "medium",
174
173
  service: entry.serviceName,
175
174
  suiteName: entry.suiteName,
176
- selectionType: toSelectionType(entry.type, entry.framework),
177
- framework: entry.framework,
175
+ type: toSelectionType(entry.type, entry.framework),
178
176
  testFilePath: entry.filePath,
179
177
  coveredNodeIds: [testNodeId],
180
178
  };
@@ -53,7 +53,7 @@ export function apiRouteLookupKey(method, route) {
53
53
  }
54
54
 
55
55
  export function toSelectionType(type, framework) {
56
- if (framework === "playwright") return "pw";
56
+ if (framework === "playwright" || type === "ui") return "ui";
57
57
  if (type === "integration") return "int";
58
58
  return type;
59
59
  }
@@ -67,7 +67,6 @@ export function createTestFileNode(graph, entry) {
67
67
  filePath: entry.filePath,
68
68
  metadata: {
69
69
  suiteName: entry.suiteName,
70
- framework: entry.framework,
71
70
  type: toSelectionType(entry.type, entry.framework),
72
71
  },
73
72
  });
@@ -80,7 +79,7 @@ export function apiRouteLookupKey(method, route) {
80
79
  }
81
80
 
82
81
  export function toSelectionType(type, framework) {
83
- if (framework === "playwright") return "pw";
82
+ if (framework === "playwright" || type === "ui") return "ui";
84
83
  if (type === "integration") return "int";
85
84
  return type;
86
85
  }
@@ -1,8 +1,7 @@
1
1
  import type { CoverageGraph } from "@elench/testkit-protocol";
2
2
 
3
- export type DiscoverySelectionType = "int" | "e2e" | "scenario" | "dal" | "load" | "pw";
4
- export type DiscoveryInternalType = "integration" | "e2e" | "scenario" | "dal" | "load";
5
- export type DiscoveryFramework = "k6" | "playwright";
3
+ export type DiscoveryTestType = "ui" | "e2e" | "scenario" | "int" | "dal" | "load";
4
+ export type DiscoveryInternalType = "ui" | "integration" | "e2e" | "scenario" | "dal" | "load";
6
5
 
7
6
  export interface DiscoveryDiagnostic {
8
7
  code: string;
@@ -28,9 +27,8 @@ export interface DiscoverySuite {
28
27
  service: string;
29
28
  name: string;
30
29
  displayName: string;
31
- selectionType: DiscoverySelectionType;
30
+ type: DiscoveryTestType;
32
31
  internalType: DiscoveryInternalType;
33
- framework: DiscoveryFramework;
34
32
  groupLabel: string;
35
33
  fileCount: number;
36
34
  activeFileCount: number;
@@ -59,9 +57,8 @@ export interface DiscoveryFile {
59
57
  service: string;
60
58
  suiteName: string;
61
59
  groupLabel: string;
62
- selectionType: DiscoverySelectionType;
60
+ type: DiscoveryTestType;
63
61
  internalType: DiscoveryInternalType;
64
- framework: DiscoveryFramework;
65
62
  skipped: boolean;
66
63
  skipReason: string | null;
67
64
  locks: string[];
@@ -79,7 +76,7 @@ export interface DiscoveryResult {
79
76
  configFile: string | null;
80
77
  filters: {
81
78
  service: string | null;
82
- types: DiscoverySelectionType[] | ["all"];
79
+ types: DiscoveryTestType[] | ["all"];
83
80
  suiteSelectors: string[];
84
81
  fileNames: string[];
85
82
  runnableOnly: boolean;
@@ -3,6 +3,7 @@ import { loadConfigContext, resolveProductDir } from "../config/index.mjs";
3
3
  import { discoverProject } from "../config/discovery.mjs";
4
4
  import { loadTestkitConfig } from "../config/config-loader.mjs";
5
5
  import { buildCoverageGraph } from "../coverage/index.mjs";
6
+ import { formatPublicTestType } from "../domain/test-types.mjs";
6
7
  import { historyFilePath, loadHistory, summarizeHistoryForFiles } from "../history/index.mjs";
7
8
  import {
8
9
  matchesSelectedTypes,
@@ -124,12 +125,7 @@ export async function discoverTests(options = {}) {
124
125
 
125
126
  export function formatSelectionTypeLabel(type) {
126
127
  if (type === "int") return "Integration";
127
- if (type === "e2e") return "E2E";
128
- if (type === "scenario") return "Scenario";
129
- if (type === "dal") return "DAL";
130
- if (type === "load") return "Load";
131
- if (type === "pw") return "Playwright";
132
- return type;
128
+ return formatPublicTestType(type);
133
129
  }
134
130
 
135
131
  export function formatDisplayName(value) {
@@ -226,9 +222,8 @@ function buildResolvedSuiteEntries(config, suite, internalType, filters) {
226
222
  service: config.name,
227
223
  suiteName: suite.name,
228
224
  groupLabel: formatDisplayName(suite.name),
229
- selectionType,
225
+ type: selectionType,
230
226
  internalType,
231
- framework,
232
227
  skipped,
233
228
  skipReason,
234
229
  locks: [...new Set([...suiteLocks, ...fileLocks])].sort(),
@@ -239,13 +234,12 @@ function buildResolvedSuiteEntries(config, suite, internalType, filters) {
239
234
  if (visibleFiles.length === 0) return null;
240
235
  const skippedFileCount = visibleFiles.filter((entry) => entry.skipped).length;
241
236
  const suiteEntry = {
242
- id: buildSuiteId(config.name, selectionType, suite.name, framework),
237
+ id: buildSuiteId(config.name, selectionType, suite.name),
243
238
  service: config.name,
244
239
  name: suite.name,
245
240
  displayName: formatDisplayName(suite.name),
246
- selectionType,
241
+ type: selectionType,
247
242
  internalType,
248
- framework,
249
243
  groupLabel: formatDisplayName(suite.name),
250
244
  fileCount: visibleFiles.length,
251
245
  activeFileCount: visibleFiles.length - skippedFileCount,
@@ -308,9 +302,8 @@ function buildRawDiscovery({ rawDiscovery, explicitServices, filters }) {
308
302
  service: entry.serviceName,
309
303
  suiteName: entry.suiteName,
310
304
  groupLabel: formatDisplayName(entry.suiteName),
311
- selectionType,
305
+ type: selectionType,
312
306
  internalType: entry.type,
313
- framework: entry.framework,
314
307
  skipped: false,
315
308
  skipReason: null,
316
309
  locks: [],
@@ -318,7 +311,7 @@ function buildRawDiscovery({ rawDiscovery, explicitServices, filters }) {
318
311
  };
319
312
  files.push(fileEntry);
320
313
 
321
- const suiteId = buildSuiteId(entry.serviceName, selectionType, entry.suiteName, entry.framework);
314
+ const suiteId = buildSuiteId(entry.serviceName, selectionType, entry.suiteName);
322
315
  const existingSuite = suitesById.get(suiteId);
323
316
  if (existingSuite) {
324
317
  existingSuite.filePaths.push(entry.filePath);
@@ -330,9 +323,8 @@ function buildRawDiscovery({ rawDiscovery, explicitServices, filters }) {
330
323
  service: entry.serviceName,
331
324
  name: entry.suiteName,
332
325
  displayName: formatDisplayName(entry.suiteName),
333
- selectionType,
326
+ type: selectionType,
334
327
  internalType: entry.type,
335
- framework: entry.framework,
336
328
  groupLabel: formatDisplayName(entry.suiteName),
337
329
  fileCount: 1,
338
330
  activeFileCount: 1,
@@ -412,7 +404,7 @@ function buildSummary(services, suites, files, diagnostics) {
412
404
 
413
405
  for (const file of files) {
414
406
  byService[file.service] = (byService[file.service] || 0) + 1;
415
- byType[file.selectionType] = (byType[file.selectionType] || 0) + 1;
407
+ byType[file.type] = (byType[file.type] || 0) + 1;
416
408
  }
417
409
 
418
410
  return {
@@ -497,12 +489,12 @@ function normalizePath(filePath) {
497
489
  export function fileDisplayName(filePath) {
498
490
  const base = path.posix
499
491
  .basename(filePath)
500
- .replace(/(\.int|\.e2e|\.scenario|\.dal|\.load|\.pw)\.testkit\.ts$/, "");
492
+ .replace(/(\.int|\.e2e|\.scenario|\.dal|\.load|\.ui|\.pw)\.testkit\.ts$/, "");
501
493
  return formatDisplayName(base);
502
494
  }
503
495
 
504
- function buildSuiteId(serviceName, selectionType, suiteName, framework) {
505
- return [serviceName, selectionType, framework, suiteName].join("|");
496
+ function buildSuiteId(serviceName, selectionType, suiteName) {
497
+ return [serviceName, selectionType, suiteName].join("|");
506
498
  }
507
499
 
508
500
  function buildFileId(serviceName, selectionType, filePath) {
@@ -512,16 +504,15 @@ function buildFileId(serviceName, selectionType, filePath) {
512
504
  function compareSuiteEntries(left, right) {
513
505
  return (
514
506
  left.service.localeCompare(right.service) ||
515
- left.selectionType.localeCompare(right.selectionType) ||
516
- left.name.localeCompare(right.name) ||
517
- left.framework.localeCompare(right.framework)
507
+ left.type.localeCompare(right.type) ||
508
+ left.name.localeCompare(right.name)
518
509
  );
519
510
  }
520
511
 
521
512
  function compareFileEntries(left, right) {
522
513
  return (
523
514
  left.service.localeCompare(right.service) ||
524
- left.selectionType.localeCompare(right.selectionType) ||
515
+ left.type.localeCompare(right.type) ||
525
516
  left.suiteName.localeCompare(right.suiteName) ||
526
517
  left.path.localeCompare(right.path)
527
518
  );