@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.
Files changed (54) 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/command-observer.mjs +2 -1
  7. package/lib/cli/assistant/command-results.mjs +2 -1
  8. package/lib/cli/assistant/context-pack.mjs +2 -2
  9. package/lib/cli/assistant/prompt-builder.mjs +2 -2
  10. package/lib/cli/command-flags.mjs +2 -1
  11. package/lib/cli/commands/cleanup.mjs +13 -2
  12. package/lib/cli/commands/discover.mjs +2 -1
  13. package/lib/cli/commands/run.mjs +3 -2
  14. package/lib/cli/entrypoint.mjs +3 -1
  15. package/lib/cli/operations/cleanup/operation.mjs +6 -1
  16. package/lib/cli/operations/status/operation.mjs +2 -2
  17. package/lib/cli/renderers/discover/report.mjs +6 -8
  18. package/lib/cli/renderers/run/failure.mjs +1 -1
  19. package/lib/cli/renderers/run/text-reporter.mjs +1 -1
  20. package/lib/cli/renderers/status/text.mjs +101 -1
  21. package/lib/config/discovery.mjs +10 -1
  22. package/lib/config-api/index.mjs +2 -2
  23. package/lib/config-api/next-runtime-tsconfig.mjs +2 -1
  24. package/lib/coverage/graph-builder.mjs +2 -4
  25. package/lib/coverage/routing.mjs +1 -1
  26. package/lib/coverage/shared.mjs +1 -2
  27. package/lib/discovery/index.d.ts +5 -8
  28. package/lib/discovery/index.mjs +15 -24
  29. package/lib/domain/test-types.mjs +44 -0
  30. package/lib/history/index.d.ts +3 -4
  31. package/lib/history/index.mjs +6 -14
  32. package/lib/runner/formatting.mjs +2 -3
  33. package/lib/runner/maintenance.mjs +136 -35
  34. package/lib/runner/planning.mjs +1 -1
  35. package/lib/runner/results.mjs +0 -6
  36. package/lib/runner/status-model.mjs +520 -0
  37. package/lib/runner/suite-selection.mjs +20 -11
  38. package/lib/runner/template-steps.mjs +2 -2
  39. package/lib/runner/template.mjs +4 -0
  40. package/lib/ui/index.d.ts +1 -0
  41. package/lib/ui/index.mjs +1 -0
  42. package/lib/vitest/index.mjs +2 -1
  43. package/node_modules/@elench/next-analysis/package.json +1 -1
  44. package/node_modules/@elench/testkit-bridge/dist/index.js +9 -11
  45. package/node_modules/@elench/testkit-bridge/dist/index.js.map +1 -1
  46. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  47. package/node_modules/@elench/testkit-protocol/dist/index.d.ts +1 -3
  48. package/node_modules/@elench/testkit-protocol/dist/index.d.ts.map +1 -1
  49. package/node_modules/@elench/testkit-protocol/dist/index.js +3 -6
  50. package/node_modules/@elench/testkit-protocol/dist/index.js.map +1 -1
  51. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  52. package/node_modules/@elench/ts-analysis/dist/requests.js +1 -1
  53. package/node_modules/@elench/ts-analysis/package.json +1 -1
  54. package/package.json +9 -9
package/README.md CHANGED
@@ -35,7 +35,7 @@ npx @elench/testkit --type int
35
35
  npx @elench/testkit --type dal
36
36
  npx @elench/testkit --type e2e
37
37
  npx @elench/testkit --type int,e2e,dal
38
- npx @elench/testkit --type pw
38
+ npx @elench/testkit --type ui
39
39
 
40
40
  # Parallel file execution
41
41
  npx @elench/testkit --workers 8
@@ -44,7 +44,7 @@ npx @elench/testkit --workers 8
44
44
  npx @elench/testkit --file-timeout-seconds 60
45
45
 
46
46
  # Specific service / suite
47
- npx @elench/testkit --service frontend --type pw -s navigation
47
+ npx @elench/testkit --service frontend --type ui -s navigation
48
48
  npx @elench/testkit --service api --type int -s health
49
49
  npx @elench/testkit --type int,e2e,dal -s dal:queries
50
50
 
@@ -183,9 +183,8 @@ export default defineConfig({
183
183
  ```
184
184
 
185
185
  ```ts
186
- // playwright.config.ts
187
- import { defineConfig } from "@elench/testkit/playwright";
188
- import { devices } from "@playwright/test";
186
+ // ui.config.ts
187
+ import { defineConfig, devices } from "@elench/testkit/ui";
189
188
 
190
189
  export default defineConfig({
191
190
  testDir: "./__testkit__",
@@ -589,7 +588,7 @@ Example layouts:
589
588
 
590
589
  - `src/api/routes/__testkit__/auth/me.int.testkit.ts`
591
590
  - `src/db/__testkit__/sessions/count-type.dal.testkit.ts`
592
- - `frontend/__testkit__/navigation/navigation.pw.testkit.ts`
591
+ - `frontend/__testkit__/navigation/navigation.ui.testkit.ts`
593
592
  - `src/internal/handler/__testkit__/repos/crud.int.testkit.ts`
594
593
 
595
594
  `testkit` uses these suffixes automatically:
@@ -599,7 +598,9 @@ Example layouts:
599
598
  - `*.scenario.testkit.ts`
600
599
  - `*.dal.testkit.ts`
601
600
  - `*.load.testkit.ts`
602
- - `*.pw.testkit.ts`
601
+ - `*.ui.testkit.ts`
602
+
603
+ See [docs/test-types.md](docs/test-types.md) for the canonical type model.
603
604
 
604
605
  Ownership is inferred from:
605
606
 
@@ -645,9 +646,8 @@ identifier plus canonical metadata such as:
645
646
  - `path`
646
647
  - `service`
647
648
  - `suiteName`
648
- - `selectionType`
649
+ - `type`
649
650
  - `internalType`
650
- - `framework`
651
651
  - `skipped`
652
652
  - `skipReason`
653
653
  - `locks`
@@ -31,12 +31,12 @@ export async function runDoctor(options = {}) {
31
31
 
32
32
  const playwrightViolations = findPlaywrightRuntimeImportViolations(productDir);
33
33
  checks.push({
34
- code: "playwright-runtime-imports",
34
+ code: "ui-runtime-imports",
35
35
  level: playwrightViolations.length === 0 ? "pass" : "fail",
36
36
  message:
37
37
  playwrightViolations.length === 0
38
- ? "No runtime @playwright/test imports found in testkit Playwright suites"
39
- : `Found ${playwrightViolations.length} Playwright runtime import violation(s)`,
38
+ ? "No runtime @playwright/test imports found in testkit UI suites"
39
+ : `Found ${playwrightViolations.length} UI runtime import violation(s); import from @elench/testkit/ui instead`,
40
40
  details: playwrightViolations,
41
41
  });
42
42
 
@@ -51,7 +51,7 @@ export async function runDoctor(options = {}) {
51
51
  details: configImportViolations,
52
52
  });
53
53
 
54
- const hasBrowserOrNextWork = discovery.files.some((entry) => entry.selectionType === "pw");
54
+ const hasBrowserOrNextWork = discovery.files.some((entry) => entry.type === "ui");
55
55
  if (hasBrowserOrNextWork) {
56
56
  const nodeCount = discovery.coverageGraph?.nodes?.length || 0;
57
57
  checks.push({
@@ -159,7 +159,7 @@ function collectFiles(rootDir, out = []) {
159
159
  collectFiles(absolutePath, out);
160
160
  continue;
161
161
  }
162
- if (entry.isFile() && entry.name.endsWith(".pw.testkit.ts")) {
162
+ if (entry.isFile() && (entry.name.endsWith(".ui.testkit.ts") || entry.name.endsWith(".ui.testkit.ts"))) {
163
163
  out.push(absolutePath);
164
164
  }
165
165
  }
@@ -83,7 +83,7 @@ function writeRootTypecheckConfig({ productDir, configFile, outputDir }) {
83
83
  "**/*.load.testkit.ts",
84
84
  "**/__testkit__/helpers/**/*.ts",
85
85
  ],
86
- exclude: ["node_modules", "dist", ".testkit", "**/*.pw.testkit.ts"],
86
+ exclude: ["node_modules", "dist", ".testkit", "**/*.ui.testkit.ts", "**/*.ui.testkit.ts"],
87
87
  };
88
88
  fs.writeFileSync(tsconfigPath, `${JSON.stringify(config, null, 2)}\n`);
89
89
  return tsconfigPath;
@@ -104,14 +104,15 @@ function writeNextServiceTypecheckConfig({ productDir, cwd, outputDir, serviceNa
104
104
  }),
105
105
  include: [
106
106
  serviceExists(serviceDir, "next-env.d.ts") ? relativeJsonPath(tsconfigPath, path.join(serviceDir, "next-env.d.ts")) : undefined,
107
- relativeGlob(tsconfigPath, serviceDir, "src/**/*.pw.testkit.ts"),
107
+ relativeGlob(tsconfigPath, serviceDir, "src/**/*.ui.testkit.ts"),
108
+ relativeGlob(tsconfigPath, serviceDir, "src/**/*.ui.testkit.ts"),
108
109
  relativeGlob(tsconfigPath, serviceDir, "tests/playwright-fixtures/**/*.ts"),
109
110
  relativeGlob(tsconfigPath, serviceDir, "tests/playwright-fixtures/**/*.tsx"),
110
111
  relativeGlob(tsconfigPath, serviceDir, "tests/playwright-fixtures/**/*.mts"),
111
112
  relativeGlob(tsconfigPath, serviceDir, ".next/types/**/*.ts"),
112
113
  relativeGlob(tsconfigPath, serviceDir, ".next/dev/types/**/*.ts"),
113
- relativeGlob(tsconfigPath, serviceDir, ".next-testkit/**/dist/types/**/*.ts"),
114
- relativeGlob(tsconfigPath, serviceDir, ".next-testkit/**/dist/dev/types/**/*.ts"),
114
+ relativeGlob(tsconfigPath, productDir, ".next-testkit/**/dist/types/**/*.ts"),
115
+ relativeGlob(tsconfigPath, productDir, ".next-testkit/**/dist/dev/types/**/*.ts"),
115
116
  ].filter(Boolean),
116
117
  exclude: ["node_modules", ".next/cache", ".next/dev"],
117
118
  };
@@ -195,7 +196,7 @@ function packageTypePaths(tsconfigPath) {
195
196
  "@elench/testkit/config": [relativeJsonPath(tsconfigPath, path.join(PACKAGE_ROOT, "lib", "config-api", "index.d.ts"))],
196
197
  "@elench/testkit/drizzle": [relativeJsonPath(tsconfigPath, path.join(PACKAGE_ROOT, "lib", "drizzle", "index.d.ts"))],
197
198
  "@elench/testkit/env": [relativeJsonPath(tsconfigPath, path.join(PACKAGE_ROOT, "lib", "env", "index.d.ts"))],
198
- "@elench/testkit/playwright": [relativeJsonPath(tsconfigPath, path.join(PACKAGE_ROOT, "lib", "playwright", "index.d.ts"))],
199
+ "@elench/testkit/ui": [relativeJsonPath(tsconfigPath, path.join(PACKAGE_ROOT, "lib", "ui", "index.d.ts"))],
199
200
  "@elench/testkit/runtime": [relativeJsonPath(tsconfigPath, path.join(PACKAGE_ROOT, "lib", "runtime", "index.d.ts"))],
200
201
  "@elench/testkit/vitest": [relativeJsonPath(tsconfigPath, path.join(PACKAGE_ROOT, "lib", "vitest", "index.d.ts"))],
201
202
  };
@@ -10,6 +10,7 @@ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)),
10
10
  const ROOT_ENTRY = path.join(PACKAGE_ROOT, "lib", "index.mjs");
11
11
  const CONFIG_ENTRY = path.join(PACKAGE_ROOT, "lib", "config-api", "index.mjs");
12
12
  const RUNTIME_ENTRY = path.join(PACKAGE_ROOT, "lib", "runtime", "index.mjs");
13
+ const MANIFEST_FILE = "manifest.json";
13
14
  const bundleCache = new Map();
14
15
 
15
16
  export async function bundleK6File({
@@ -22,9 +23,18 @@ export async function bundleK6File({
22
23
  fs.mkdirSync(bundleDir, { recursive: true });
23
24
 
24
25
  const configFile = findConfigFile(productDir);
25
- const cacheKey = await buildCacheKey(absoluteSource, configFile);
26
+ const metadata = await buildCacheMetadata(absoluteSource, configFile);
27
+ const { cacheKey } = metadata;
26
28
  const cached = bundleCache.get(cacheKey);
27
29
  if (cached && fs.existsSync(cached)) {
30
+ updateBundleManifest(bundleDir, {
31
+ ...metadata,
32
+ serviceName: serviceName || "shared",
33
+ sourceFile: absoluteSource,
34
+ outputFile: cached,
35
+ entryFile: inferEntryFile(cached),
36
+ hasSourcemap: fileHasInlineSourcemap(cached),
37
+ });
28
38
  return cached;
29
39
  }
30
40
 
@@ -36,6 +46,20 @@ export async function bundleK6File({
36
46
  bundleDir,
37
47
  `${path.basename(sourceFile, path.extname(sourceFile))}-${cacheKey.slice(0, 12)}.entry.mjs`
38
48
  );
49
+
50
+ if (fs.existsSync(outputFile) && fs.existsSync(entryFile)) {
51
+ bundleCache.set(cacheKey, outputFile);
52
+ updateBundleManifest(bundleDir, {
53
+ ...metadata,
54
+ serviceName: serviceName || "shared",
55
+ sourceFile: absoluteSource,
56
+ outputFile,
57
+ entryFile,
58
+ hasSourcemap: fileHasInlineSourcemap(outputFile),
59
+ });
60
+ return outputFile;
61
+ }
62
+
39
63
  fs.writeFileSync(entryFile, buildBundleEntryModule({
40
64
  sourceFile: absoluteSource,
41
65
  configFile,
@@ -49,7 +73,7 @@ export async function bundleK6File({
49
73
  legalComments: "none",
50
74
  outfile: outputFile,
51
75
  platform: "neutral",
52
- sourcemap: "inline",
76
+ sourcemap: bundleSourcemapEnabled() ? "inline" : false,
53
77
  target: "es2020",
54
78
  plugins: [testkitPackageAliasPlugin()],
55
79
  external: [
@@ -59,26 +83,44 @@ export async function bundleK6File({
59
83
  });
60
84
 
61
85
  bundleCache.set(cacheKey, outputFile);
86
+ updateBundleManifest(bundleDir, {
87
+ ...metadata,
88
+ serviceName: serviceName || "shared",
89
+ sourceFile: absoluteSource,
90
+ outputFile,
91
+ entryFile,
92
+ hasSourcemap: bundleSourcemapEnabled(),
93
+ });
62
94
  return outputFile;
63
95
  }
64
96
 
65
- async function buildCacheKey(sourceFile, configFile = null) {
97
+ async function buildCacheMetadata(sourceFile, configFile = null) {
66
98
  const source = await fs.promises.readFile(sourceFile, "utf8");
67
- const packageJson = await fs.promises.readFile(path.join(PACKAGE_ROOT, "package.json"), "utf8");
99
+ const packageJsonText = await fs.promises.readFile(path.join(PACKAGE_ROOT, "package.json"), "utf8");
100
+ const packageJson = JSON.parse(packageJsonText);
101
+ const sourceHash = crypto.createHash("sha256").update(source).digest("hex");
68
102
  const hash = crypto
69
103
  .createHash("sha256")
70
104
  .update(sourceFile)
71
105
  .update("\0")
72
106
  .update(source)
73
107
  .update("\0")
74
- .update(packageJson);
108
+ .update(packageJsonText);
109
+ let configHash = null;
75
110
 
76
111
  if (configFile && fs.existsSync(configFile)) {
112
+ const configText = await fs.promises.readFile(configFile, "utf8");
113
+ configHash = crypto.createHash("sha256").update(configFile).update("\0").update(configText).digest("hex");
77
114
  hash.update("\0").update(configFile).update("\0");
78
- hash.update(await fs.promises.readFile(configFile, "utf8"));
115
+ hash.update(configText);
79
116
  }
80
117
 
81
- return hash.digest("hex");
118
+ return {
119
+ cacheKey: hash.digest("hex"),
120
+ sourceHash,
121
+ configHash,
122
+ testkitVersion: packageJson.version || null,
123
+ };
82
124
  }
83
125
 
84
126
  function testkitPackageAliasPlugin() {
@@ -153,3 +195,88 @@ function normalizeTestkitSuite(module) {
153
195
  }
154
196
  `;
155
197
  }
198
+
199
+ function updateBundleManifest(bundleDir, entry) {
200
+ const manifestPath = path.join(bundleDir, MANIFEST_FILE);
201
+ const manifest = readBundleManifest(manifestPath);
202
+ const now = new Date().toISOString();
203
+ const existingIndex = manifest.entries.findIndex((candidate) => candidate.cacheKey === entry.cacheKey);
204
+ const existing = existingIndex >= 0 ? manifest.entries[existingIndex] : null;
205
+ const document = {
206
+ sourceFile: entry.sourceFile,
207
+ cacheKey: entry.cacheKey,
208
+ outputFile: entry.outputFile,
209
+ entryFile: entry.entryFile,
210
+ createdAt: existing?.createdAt || now,
211
+ lastUsedAt: now,
212
+ sizeBytes: safeSize(entry.outputFile) + safeSize(entry.entryFile),
213
+ testkitVersion: entry.testkitVersion,
214
+ configHash: entry.configHash,
215
+ sourceHash: entry.sourceHash,
216
+ hasSourcemap: Boolean(entry.hasSourcemap),
217
+ };
218
+
219
+ if (existingIndex >= 0) {
220
+ manifest.entries[existingIndex] = document;
221
+ } else {
222
+ manifest.entries.push(document);
223
+ }
224
+ manifest.entries.sort((left, right) => String(left.sourceFile).localeCompare(String(right.sourceFile)) || String(left.cacheKey).localeCompare(String(right.cacheKey)));
225
+ writeBundleManifest(manifestPath, manifest);
226
+ }
227
+
228
+ function readBundleManifest(manifestPath) {
229
+ if (!fs.existsSync(manifestPath)) {
230
+ return {
231
+ schemaVersion: 1,
232
+ source: "testkit-bundle-cache",
233
+ entries: [],
234
+ };
235
+ }
236
+ try {
237
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
238
+ return {
239
+ schemaVersion: 1,
240
+ source: "testkit-bundle-cache",
241
+ ...parsed,
242
+ entries: Array.isArray(parsed.entries) ? parsed.entries : [],
243
+ };
244
+ } catch {
245
+ return {
246
+ schemaVersion: 1,
247
+ source: "testkit-bundle-cache",
248
+ entries: [],
249
+ };
250
+ }
251
+ }
252
+
253
+ function writeBundleManifest(manifestPath, manifest) {
254
+ const tempPath = `${manifestPath}.${process.pid}.${Date.now()}.tmp`;
255
+ fs.writeFileSync(tempPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
256
+ fs.renameSync(tempPath, manifestPath);
257
+ }
258
+
259
+ function bundleSourcemapEnabled() {
260
+ return process.env.TESTKIT_BUNDLE_SOURCEMAP === "1";
261
+ }
262
+
263
+ function inferEntryFile(outputFile) {
264
+ return outputFile.replace(/\.js$/, ".entry.mjs");
265
+ }
266
+
267
+ function safeSize(filePath) {
268
+ try {
269
+ return fs.statSync(filePath).size;
270
+ } catch {
271
+ return 0;
272
+ }
273
+ }
274
+
275
+ function fileHasInlineSourcemap(filePath) {
276
+ try {
277
+ const text = fs.readFileSync(filePath, "utf8");
278
+ return /sourceMappingURL=data:application\/json/.test(text);
279
+ } catch {
280
+ return false;
281
+ }
282
+ }
package/lib/cli/args.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  import path from "path";
2
2
  import { normalizeTypeValues, parseSuiteSelectors } from "../runner/suite-selection.mjs";
3
+ import { publicTestTypeList, publicTestTypeListText } from "../domain/test-types.mjs";
3
4
  import {
4
5
  parseFileTimeoutOption,
5
6
  parseWorkersOption,
6
7
  } from "../runner/execution-config.mjs";
7
8
 
8
- export const POSITIONAL_TYPES = new Set(["int", "e2e", "scenario", "dal", "load", "pw", "all"]);
9
+ export const POSITIONAL_TYPES = new Set(publicTestTypeList({ includeAll: true, includeLegacy: true }));
9
10
  export const LIFECYCLE = new Set(["status", "destroy", "cleanup"]);
10
11
  export const RESERVED = new Set([...POSITIONAL_TYPES, ...LIFECYCLE]);
11
12
 
@@ -33,7 +34,7 @@ export function resolveCliSelection({ first, second, third }) {
33
34
  } else if (first) {
34
35
  throw new Error(
35
36
  `Unknown argument "${first}". Expected a lifecycle command (status, destroy, cleanup) ` +
36
- `or suite type (int, e2e, scenario, dal, load, pw, all).`
37
+ `or suite type (${publicTestTypeListText({ includeAll: true })}).`
37
38
  );
38
39
  }
39
40
 
@@ -1,9 +1,10 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { publicTestTypeList } from "../../domain/test-types.mjs";
3
4
 
4
5
  const POLL_INTERVAL_MS = 150;
5
6
  const OBSERVED_KINDS = new Set(["run", "discover", "status", "doctor", "typecheck"]);
6
- const RUN_KINDS = new Set(["run", "int", "e2e", "scenario", "dal", "load", "pw", "all"]);
7
+ const RUN_KINDS = new Set(["run", ...publicTestTypeList({ includeAll: true, includeLegacy: true })]);
7
8
 
8
9
  export function createAssistantCommandObserver({
9
10
  productDir,
@@ -1,5 +1,6 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { publicTestTypeList } from "../../domain/test-types.mjs";
3
4
 
4
5
  export const ASSISTANT_SESSION_ENV = "TESTKIT_ASSISTANT_SESSION_ID";
5
6
  export const ASSISTANT_RESULT_DIR_ENV = "TESTKIT_ASSISTANT_RESULT_DIR";
@@ -7,7 +8,7 @@ export const ASSISTANT_COMMAND_LOG_ENV = "TESTKIT_ASSISTANT_COMMAND_LOG";
7
8
  export const ASSISTANT_COMMAND_ID_ENV = "TESTKIT_ASSISTANT_COMMAND_ID";
8
9
  export const ASSISTANT_WRAPPER_LOGGED_ENV = "TESTKIT_ASSISTANT_WRAPPER_LOGGED";
9
10
 
10
- const RUN_SHORTCUTS = new Set(["int", "e2e", "scenario", "dal", "load", "pw", "all"]);
11
+ const RUN_SHORTCUTS = new Set(publicTestTypeList({ includeAll: true, includeLegacy: true }));
11
12
  const FLAGS_WITH_VALUES = new Set([
12
13
  "--dir",
13
14
  "--service",
@@ -178,7 +178,7 @@ function appendCommandLog(event) {
178
178
  }
179
179
 
180
180
  function inferKind(args) {
181
- const runShortcuts = new Set(["int", "e2e", "scenario", "dal", "load", "pw", "all"]);
181
+ const runShortcuts = new Set(["ui", "e2e", "scenario", "int", "dal", "load", "all"]);
182
182
  const flagsWithValues = new Set([
183
183
  "--dir",
184
184
  "--service",
@@ -248,7 +248,7 @@ function buildContextMarkdown(productDir, snapshot, paths) {
248
248
  "## Guidance",
249
249
  "- Work normally in the repository and run real commands. Testkit observes recognized Testkit commands and renders them in the assistant UI.",
250
250
  "- Prefer `testkit ...` commands when using Testkit directly; `npx testkit ...` and project scripts are also valid.",
251
- "- `testkit run e2e` selects e2e suites. `testkit run pw` selects Playwright/browser suites.",
251
+ "- `testkit run ui` selects UI suites. `testkit run e2e` selects e2e suites.",
252
252
  "- Do not reinterpret CLI syntax after an execution failure unless `testkit run --help` confirms a syntax problem.",
253
253
  "- Use the command log and focused context files before rereading artifacts manually.",
254
254
  "- Prefer repo-local commands over guessing project-specific wrappers.",
@@ -15,8 +15,8 @@ export function buildAssistantPrompt({
15
15
  "You are Testkit Assistant.",
16
16
  "You are running as a coding agent inside the user's repository.",
17
17
  "Work normally: inspect files, edit files, run real shell commands, and iterate until the user's request is handled.",
18
- "When using Testkit, run real commands such as `testkit discover`, `testkit run e2e`, `testkit run pw`, `npx testkit run int`, or the repository's package scripts.",
19
- "`testkit run e2e` selects e2e suites. `testkit run pw` selects Playwright/browser suites. Choose commands from the user's request and the command reference, not from Testkit prompt routing.",
18
+ "When using Testkit, run real commands such as `testkit discover`, `testkit run ui`, `testkit run e2e`, `npx testkit run int`, or the repository's package scripts.",
19
+ "`testkit run ui` selects UI suites. `testkit run e2e` selects e2e suites. Choose commands from the user's request and the command reference, not from Testkit prompt routing.",
20
20
  "Testkit observes recognized Testkit commands and renders rich assistant UI from the real command output, sidecars, and artifacts.",
21
21
  "Do not respond with a JSON tool envelope. Give the user a normal final answer when you are done.",
22
22
  "",
@@ -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
  }