@elench/testkit 0.1.96 → 0.1.98

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 (79) hide show
  1. package/lib/app/browser-bridge.mjs +1 -1
  2. package/lib/cli/assistant/app.mjs +49 -12
  3. package/lib/cli/assistant/composer.mjs +19 -1
  4. package/lib/cli/assistant/context-pack.mjs +9 -8
  5. package/lib/cli/assistant/interactive.mjs +1 -1
  6. package/lib/cli/assistant/model-discovery.mjs +243 -0
  7. package/lib/cli/assistant/prompt-builder.mjs +2 -5
  8. package/lib/cli/{agents → assistant}/providers/claude.mjs +41 -3
  9. package/lib/cli/{agents → assistant}/providers/codex.mjs +33 -14
  10. package/lib/cli/{agents → assistant/providers}/index.mjs +3 -3
  11. package/lib/cli/{agents → assistant}/providers/shared.mjs +6 -2
  12. package/lib/cli/assistant/session.mjs +31 -6
  13. package/lib/cli/assistant/slash-commands.mjs +30 -3
  14. package/lib/cli/assistant/state.mjs +237 -71
  15. package/lib/cli/assistant/tool-registry.mjs +325 -39
  16. package/lib/cli/assistant/view-model.mjs +1 -1
  17. package/lib/cli/commands/assistant.mjs +4 -3
  18. package/lib/cli/commands/browser/serve.mjs +5 -23
  19. package/lib/cli/commands/cleanup.mjs +8 -2
  20. package/lib/cli/commands/db/snapshot/capture.mjs +8 -4
  21. package/lib/cli/commands/destroy.mjs +8 -2
  22. package/lib/cli/commands/discover.mjs +5 -27
  23. package/lib/cli/commands/doctor.mjs +5 -5
  24. package/lib/cli/commands/flags.mjs +61 -0
  25. package/lib/cli/commands/run.mjs +10 -2
  26. package/lib/cli/commands/status.mjs +10 -2
  27. package/lib/cli/commands/typecheck.mjs +5 -5
  28. package/lib/cli/{tui/inspect-app.mjs → components/blocks/run-tree.mjs} +29 -54
  29. package/lib/cli/{tui → components/primitives}/filter-bar.mjs +1 -1
  30. package/lib/cli/{presentation → components/primitives}/summary-box.mjs +1 -1
  31. package/lib/cli/config.mjs +63 -0
  32. package/lib/cli/operations/browser/serve/operation.mjs +23 -0
  33. package/lib/cli/operations/cleanup/operation.mjs +8 -0
  34. package/lib/cli/{db.mjs → operations/db/snapshot/capture/operation.mjs} +15 -9
  35. package/lib/cli/operations/destroy/operation.mjs +12 -0
  36. package/lib/cli/operations/discover/operation.mjs +32 -0
  37. package/lib/cli/operations/doctor/operation.mjs +5 -0
  38. package/lib/cli/operations/run/operation.mjs +129 -0
  39. package/lib/cli/operations/status/operation.mjs +7 -0
  40. package/lib/cli/operations/typecheck/operation.mjs +5 -0
  41. package/lib/cli/renderers/browser-serve/text.mjs +6 -0
  42. package/lib/cli/renderers/cleanup/text.mjs +3 -0
  43. package/lib/cli/renderers/db-snapshot-capture/text.mjs +3 -0
  44. package/lib/cli/renderers/destroy/text.mjs +3 -0
  45. package/lib/cli/{presentation/discovery-reporter.mjs → renderers/discover/report.mjs} +3 -3
  46. package/lib/cli/renderers/discover/text.mjs +7 -0
  47. package/lib/cli/renderers/doctor/text.mjs +7 -0
  48. package/lib/cli/{presentation/failure-presentation.mjs → renderers/run/failure.mjs} +6 -6
  49. package/lib/cli/renderers/run/interactive.mjs +119 -0
  50. package/lib/cli/{presentation/run-reporter.mjs → renderers/run/text-reporter.mjs} +5 -5
  51. package/lib/cli/renderers/status/text.mjs +7 -0
  52. package/lib/cli/renderers/typecheck/text.mjs +7 -0
  53. package/lib/cli/{tui/inspect-model.mjs → state/run/model.mjs} +11 -26
  54. package/lib/cli/{tui/inspect-state.mjs → state/run/state.mjs} +11 -18
  55. package/lib/cli/{tui → state/tree}/fuzzy-match.mjs +1 -1
  56. package/lib/cli/terminal/capabilities.mjs +33 -0
  57. package/lib/database/index.mjs +9 -21
  58. package/lib/database/template-steps.mjs +3 -3
  59. package/lib/{cli/viewer.mjs → results/artifacts.mjs} +1 -1
  60. package/lib/{cli/context-resources.mjs → results/context.mjs} +1 -1
  61. package/lib/runner/maintenance.mjs +25 -14
  62. package/lib/runner/readiness.mjs +5 -4
  63. package/lib/runner/runtime-preparation.mjs +36 -0
  64. package/lib/runner/state-io.mjs +10 -4
  65. package/lib/runner/template.mjs +24 -3
  66. package/node_modules/@elench/next-analysis/package.json +1 -1
  67. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  68. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  69. package/node_modules/@elench/ts-analysis/package.json +1 -1
  70. package/package.json +5 -5
  71. package/lib/cli/assistant/command-plan.mjs +0 -227
  72. package/lib/cli/command-helpers.mjs +0 -191
  73. package/lib/cli/presentation/tree-reporter.mjs +0 -96
  74. package/lib/cli/tui/inspect-artifact-adapter.mjs +0 -3
  75. package/lib/cli/tui/inspect-live-adapter.mjs +0 -15
  76. /package/lib/cli/{presentation/events-reporter.mjs → renderers/run/events.mjs} +0 -0
  77. /package/lib/cli/{presentation → terminal}/colors.mjs +0 -0
  78. /package/lib/cli/{presentation/terminal-layout.mjs → terminal/layout.mjs} +0 -0
  79. /package/lib/{cli/presentation → results}/code-frames.mjs +0 -0
@@ -96,11 +96,11 @@ export async function isPortInUse({ host, port }) {
96
96
  });
97
97
  }
98
98
 
99
- export function printRunStatus(productDir) {
99
+ export function buildRunStatusLines(productDir) {
100
100
  const manifests = listRunManifests(productDir);
101
- if (manifests.length === 0) return;
101
+ if (manifests.length === 0) return [];
102
102
 
103
- console.log(" runs/");
103
+ const lines = [" runs/"];
104
104
  for (const manifest of manifests) {
105
105
  const state = isPidRunning(manifest.pid) ? "active" : "stale";
106
106
  const ports = [
@@ -110,8 +110,9 @@ export function printRunStatus(productDir) {
110
110
  )
111
111
  ),
112
112
  ];
113
- console.log(
113
+ lines.push(
114
114
  ` ${manifest.runId}: ${state} pid=${manifest.pid}${ports.length > 0 ? ` ports=${ports.join(",")}` : ""}`
115
115
  );
116
116
  }
117
+ return lines;
117
118
  }
@@ -127,6 +127,7 @@ export async function computeRuntimePrepareFingerprint(config) {
127
127
  : null,
128
128
  })
129
129
  );
130
+ hash.update(JSON.stringify(collectRuntimeDatabaseFingerprintInputs(config)));
130
131
 
131
132
  for (const envFile of config.testkit.envFiles || []) {
132
133
  appendFileToHash(hash, config.productDir, resolveServiceCwd(config.productDir, envFile));
@@ -138,6 +139,41 @@ export async function computeRuntimePrepareFingerprint(config) {
138
139
  return hash.digest("hex");
139
140
  }
140
141
 
142
+ function collectRuntimeDatabaseFingerprintInputs(config) {
143
+ const inputs = [];
144
+ const ownDatabaseUrl = readDatabaseUrl(config.stateDir);
145
+ if (ownDatabaseUrl) {
146
+ inputs.push({ service: config.name, url: ownDatabaseUrl });
147
+ }
148
+
149
+ const referencedServices = new Set();
150
+ for (const value of Object.values(config.testkit.serviceEnv || {})) {
151
+ collectDatabasePlaceholderServices(value, referencedServices, config.name);
152
+ }
153
+ for (const value of Object.values(config.testkit.local?.env || {})) {
154
+ collectDatabasePlaceholderServices(value, referencedServices, config.name);
155
+ }
156
+
157
+ const stateDirByService = config.testkit.templateContext?.stateDirByService;
158
+ for (const serviceName of [...referencedServices].sort()) {
159
+ const stateDir = stateDirByService?.get?.(serviceName);
160
+ const databaseUrl = stateDir ? readDatabaseUrl(stateDir) : null;
161
+ inputs.push({ service: serviceName, url: databaseUrl || null });
162
+ }
163
+
164
+ return inputs;
165
+ }
166
+
167
+ function collectDatabasePlaceholderServices(value, out, defaultServiceName) {
168
+ if (typeof value !== "string") return;
169
+ const matcher = /\{db(?:Url|Host|Port|Name|User|Password)(?::([a-zA-Z0-9_-]+))?\}/g;
170
+ let match = matcher.exec(value);
171
+ while (match) {
172
+ out.add(match[1] || defaultServiceName);
173
+ match = matcher.exec(value);
174
+ }
175
+ }
176
+
141
177
  function appendResolvedInputToHash(hash, productDir, absPath) {
142
178
  const relative = path.relative(productDir, absPath);
143
179
  appendInputToHash(hash, productDir, relative);
@@ -33,16 +33,22 @@ export function readStateValue(filePath) {
33
33
  return fs.readFileSync(filePath, "utf8").trim();
34
34
  }
35
35
 
36
- export function printStateDir(dir, indent) {
36
+ export function collectStateDirLines(dir, indent = "") {
37
+ const lines = [];
38
+ appendStateDirLines(lines, dir, indent);
39
+ return lines;
40
+ }
41
+
42
+ function appendStateDirLines(lines, dir, indent) {
37
43
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
38
44
  const filePath = path.join(dir, entry.name);
39
45
  if (entry.isDirectory()) {
40
- console.log(`${indent}${entry.name}/`);
41
- printStateDir(filePath, `${indent} `);
46
+ lines.push(`${indent}${entry.name}/`);
47
+ appendStateDirLines(lines, filePath, `${indent} `);
42
48
  continue;
43
49
  }
44
50
  const value =
45
51
  entry.name === "password" ? "********" : fs.readFileSync(filePath, "utf8").trim();
46
- console.log(`${indent}${entry.name}: ${value}`);
52
+ lines.push(`${indent}${entry.name}: ${value}`);
47
53
  }
48
54
  }
@@ -204,17 +204,29 @@ export function buildExecutionEnv(config, extraEnv = {}, processEnv = process.en
204
204
  return buildExecutionEnvWithContext(config, null, extraEnv, processEnv);
205
205
  }
206
206
 
207
+ export function buildTemplateExecutionEnv(config, extraEnv = {}, processEnv = process.env) {
208
+ return buildExecutionEnvWithContext(config, null, extraEnv, processEnv, {
209
+ omitRuntimeDatabaseBindings: true,
210
+ });
211
+ }
212
+
207
213
  export function buildTaskExecutionEnv(config, lease, extraEnv = {}, processEnv = process.env) {
208
214
  return buildExecutionEnvWithContext(config, lease, extraEnv, processEnv);
209
215
  }
210
216
 
211
- function buildExecutionEnvWithContext(config, lease, extraEnv, processEnv) {
217
+ function buildExecutionEnvWithContext(config, lease, extraEnv, processEnv, options = {}) {
212
218
  const inheritedEnv = { ...processEnv };
213
219
  const templateContext = buildTemplateContext(config, lease);
220
+ const serviceEnv = options.omitRuntimeDatabaseBindings
221
+ ? omitRuntimeDatabaseBindings(config.testkit.serviceEnv || {})
222
+ : config.testkit.serviceEnv || {};
223
+ const localEnv = options.omitRuntimeDatabaseBindings
224
+ ? omitRuntimeDatabaseBindings(config.testkit.local?.env || {})
225
+ : config.testkit.local?.env || {};
214
226
  const env = {
215
227
  ...inheritedEnv,
216
- ...resolveEnvTemplates(config.testkit.serviceEnv || {}, templateContext),
217
- ...resolveEnvTemplates(config.testkit.local?.env || {}, templateContext),
228
+ ...resolveEnvTemplates(serviceEnv, templateContext),
229
+ ...resolveEnvTemplates(localEnv, templateContext),
218
230
  ...resolveEnvTemplates(extraEnv, templateContext),
219
231
  TESTKIT_ACTIVE: "1",
220
232
  ...(config.runtimeId ? { TESTKIT_RUNTIME_ID: String(config.runtimeId) } : {}),
@@ -340,6 +352,15 @@ function resolveEnvTemplates(values, templateContext) {
340
352
  );
341
353
  }
342
354
 
355
+ function omitRuntimeDatabaseBindings(values = {}) {
356
+ return Object.fromEntries(
357
+ Object.entries(values).filter(([_key, value]) => {
358
+ if (typeof value !== "string") return true;
359
+ return !/\{db(?:Url|Host|Port|Name|User|Password)(?::[a-zA-Z0-9_-]+)?\}/.test(value);
360
+ })
361
+ );
362
+ }
363
+
343
364
  function finalizeRuntimePrepare(prepare, context) {
344
365
  if (!prepare) {
345
366
  return {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/next-analysis",
3
- "version": "0.1.96",
3
+ "version": "0.1.98",
4
4
  "description": "SWC-backed Next.js source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-bridge",
3
- "version": "0.1.96",
3
+ "version": "0.1.98",
4
4
  "description": "Browser bridge helpers for testkit",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "typecheck": "tsc -p tsconfig.json --noEmit"
23
23
  },
24
24
  "dependencies": {
25
- "@elench/testkit-protocol": "0.1.96"
25
+ "@elench/testkit-protocol": "0.1.98"
26
26
  },
27
27
  "private": false
28
28
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit-protocol",
3
- "version": "0.1.96",
3
+ "version": "0.1.98",
4
4
  "description": "Shared browser protocol for testkit bridge and extension consumers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/ts-analysis",
3
- "version": "0.1.96",
3
+ "version": "0.1.98",
4
4
  "description": "TypeScript compiler-backed source analysis primitives for Erench tools",
5
5
  "type": "module",
6
6
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.96",
3
+ "version": "0.1.98",
4
4
  "description": "Assistant-first CLI for running, inspecting, and debugging local testkit suites",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -83,10 +83,10 @@
83
83
  },
84
84
  "dependencies": {
85
85
  "@babel/code-frame": "^7.29.0",
86
- "@elench/next-analysis": "0.1.96",
87
- "@elench/testkit-bridge": "0.1.96",
88
- "@elench/testkit-protocol": "0.1.96",
89
- "@elench/ts-analysis": "0.1.96",
86
+ "@elench/next-analysis": "0.1.98",
87
+ "@elench/testkit-bridge": "0.1.98",
88
+ "@elench/testkit-protocol": "0.1.98",
89
+ "@elench/ts-analysis": "0.1.98",
90
90
  "@oclif/core": "^4.10.6",
91
91
  "esbuild": "^0.25.11",
92
92
  "execa": "^9.5.0",
@@ -1,227 +0,0 @@
1
- const TESTKIT_TYPES = new Set(["int", "e2e", "scenario", "dal", "load", "pw", "all"]);
2
- const TESTKIT_DIR_COMMANDS = new Set(["run", "discover", "status", "doctor", "destroy", "cleanup", "typecheck"]);
3
- const PACKAGE_RUNNERS = new Set(["npx", "pnpm", "npm", "yarn", "bun"]);
4
-
5
- export function extractShellCommand(args = {}) {
6
- if (!args || typeof args !== "object") return "";
7
- const value =
8
- args.command ??
9
- args.cmd ??
10
- args.commandString ??
11
- args.shellCommand ??
12
- args.input ??
13
- args.script ??
14
- "";
15
- if (Array.isArray(value)) return value.map((part) => shellEscapeArg(part)).join(" ");
16
- return String(value || "");
17
- }
18
-
19
- export function planShellCommand(rawCommand) {
20
- const raw = String(rawCommand || "").trim();
21
- if (!raw) {
22
- return {
23
- executableCommand: "",
24
- rawCommand: raw,
25
- displayCommand: raw,
26
- command: "",
27
- title: "Shell command",
28
- testkitRelated: false,
29
- normalized: false,
30
- };
31
- }
32
-
33
- const testkit = planTestkitCommand(raw);
34
- if (testkit) return testkit;
35
-
36
- const testkitScript = planTestkitPackageScript(raw);
37
- if (testkitScript) return testkitScript;
38
-
39
- return {
40
- executableCommand: raw,
41
- rawCommand: raw,
42
- displayCommand: raw,
43
- command: firstCommandToken(raw),
44
- title: "Shell command",
45
- testkitRelated: false,
46
- normalized: false,
47
- };
48
- }
49
-
50
- function planTestkitPackageScript(raw) {
51
- if (containsShellControl(raw)) return null;
52
- const tokens = tokenizeShellWords(raw);
53
- if (!tokens || tokens.length < 3) return null;
54
- if (tokens[0] !== "npm" || tokens[1] !== "run") return null;
55
- if (tokens[2] !== "testkit" && !tokens[2].startsWith("testkit:")) return null;
56
- return {
57
- executableCommand: raw,
58
- rawCommand: raw,
59
- displayCommand: raw,
60
- command: "npm run testkit",
61
- title: "npm testkit script",
62
- testkitRelated: true,
63
- normalized: false,
64
- };
65
- }
66
-
67
- function planTestkitCommand(raw) {
68
- if (containsShellControl(raw)) return null;
69
- const tokens = tokenizeShellWords(raw);
70
- if (!tokens || tokens.length === 0) return null;
71
-
72
- const extracted = extractTestkitInvocation(tokens);
73
- if (!extracted) return null;
74
-
75
- const canonicalArgs = canonicalizeTestkitArgs(extracted.args);
76
- const executableCommand = ["testkit", ...canonicalArgs].map(shellEscapeArg).join(" ");
77
- const wasNormalized = executableCommand !== raw;
78
- return {
79
- executableCommand,
80
- rawCommand: raw,
81
- displayCommand: executableCommand,
82
- command: "testkit",
83
- title: "testkit command",
84
- testkitRelated: true,
85
- normalized: wasNormalized,
86
- normalizationReason: wasNormalized ? extracted.reason : null,
87
- };
88
- }
89
-
90
- function extractTestkitInvocation(tokens) {
91
- if (tokens[0] === "testkit") {
92
- return {
93
- args: tokens.slice(1),
94
- reason: "canonicalized local testkit invocation",
95
- };
96
- }
97
-
98
- if (!PACKAGE_RUNNERS.has(tokens[0])) return null;
99
-
100
- if (tokens[0] === "npm" && ["exec", "x"].includes(tokens[1])) {
101
- const index = findPackageTarget(tokens, 2);
102
- if (index >= 0) return { args: tokens.slice(index + 1), reason: "replaced npm exec testkit with local testkit" };
103
- }
104
-
105
- if (tokens[0] === "npx") {
106
- const index = findPackageTarget(tokens, 1);
107
- if (index >= 0) return { args: tokens.slice(index + 1), reason: "replaced npx testkit with local testkit" };
108
- }
109
-
110
- if (tokens[0] === "pnpm" && ["exec", "dlx"].includes(tokens[1])) {
111
- const index = findPackageTarget(tokens, 2);
112
- if (index >= 0) return { args: tokens.slice(index + 1), reason: "replaced pnpm testkit launcher with local testkit" };
113
- }
114
-
115
- if (tokens[0] === "yarn" && tokens[1] === "testkit") {
116
- return { args: tokens.slice(2), reason: "replaced yarn testkit launcher with local testkit" };
117
- }
118
-
119
- if (tokens[0] === "bun" && ["x", "run"].includes(tokens[1])) {
120
- const index = findPackageTarget(tokens, 2);
121
- if (index >= 0) return { args: tokens.slice(index + 1), reason: "replaced bun testkit launcher with local testkit" };
122
- }
123
-
124
- return null;
125
- }
126
-
127
- function canonicalizeTestkitArgs(inputArgs) {
128
- const args = [...inputArgs];
129
- if (args.length === 0) return [];
130
-
131
- if (TESTKIT_TYPES.has(args[0])) {
132
- return withDir(["run", "--type", args[0], ...args.slice(1)]);
133
- }
134
-
135
- if (!TESTKIT_DIR_COMMANDS.has(args[0])) {
136
- return args;
137
- }
138
-
139
- if (args[0] === "run") {
140
- const runArgs = [...args];
141
- if (TESTKIT_TYPES.has(runArgs[1])) {
142
- const type = runArgs.splice(1, 1)[0];
143
- if (!hasFlag(runArgs, "--type", "-t")) runArgs.splice(1, 0, "--type", type);
144
- }
145
- return withDir(runArgs);
146
- }
147
-
148
- return withDir(args);
149
- }
150
-
151
- function withDir(args) {
152
- if (hasFlag(args, "--dir", "-d") || args.includes("--help") || args.includes("-h")) return args;
153
- const [command, ...rest] = args;
154
- return [command, "--dir", ".", ...rest];
155
- }
156
-
157
- function hasFlag(args, longFlag, shortFlag) {
158
- return args.some((arg) => arg === longFlag || arg.startsWith(`${longFlag}=`) || arg === shortFlag);
159
- }
160
-
161
- function findPackageTarget(tokens, startIndex) {
162
- for (let index = startIndex; index < tokens.length; index += 1) {
163
- const token = tokens[index];
164
- if (token === "--") continue;
165
- if (token === "testkit" || token === "@elench/testkit") return index;
166
- if (!token.startsWith("-")) return -1;
167
- }
168
- return -1;
169
- }
170
-
171
- function firstCommandToken(command) {
172
- const tokens = tokenizeShellWords(command);
173
- return tokens?.[0] || command.split(/\s+/)[0] || "command";
174
- }
175
-
176
- function containsShellControl(command) {
177
- return /[\n;&|<>`]/.test(command);
178
- }
179
-
180
- function tokenizeShellWords(command) {
181
- const words = [];
182
- let current = "";
183
- let quote = null;
184
- let escaping = false;
185
-
186
- for (const char of String(command)) {
187
- if (escaping) {
188
- current += char;
189
- escaping = false;
190
- continue;
191
- }
192
- if (char === "\\") {
193
- escaping = true;
194
- continue;
195
- }
196
- if (quote) {
197
- if (char === quote) {
198
- quote = null;
199
- } else {
200
- current += char;
201
- }
202
- continue;
203
- }
204
- if (char === "'" || char === '"') {
205
- quote = char;
206
- continue;
207
- }
208
- if (/\s/.test(char)) {
209
- if (current) {
210
- words.push(current);
211
- current = "";
212
- }
213
- continue;
214
- }
215
- current += char;
216
- }
217
-
218
- if (escaping || quote) return null;
219
- if (current) words.push(current);
220
- return words;
221
- }
222
-
223
- function shellEscapeArg(value) {
224
- const stringValue = String(value);
225
- if (/^[a-zA-Z0-9._:@/%+=,-]+$/.test(stringValue)) return stringValue;
226
- return `'${stringValue.replace(/'/g, `'\\''`)}'`;
227
- }
@@ -1,191 +0,0 @@
1
- import { Flags } from "@oclif/core";
2
- import path from "path";
3
- import { loadManagedConfigs } from "../app/configs.mjs";
4
- import {
5
- parseFileTimeoutOption,
6
- parseShardOption,
7
- parseSuiteOption,
8
- parseTypeOption,
9
- parseWorkersOption,
10
- resolveRequestedFiles,
11
- } from "./args.mjs";
12
- import * as runner from "../runner/index.mjs";
13
- import { createRunReporter } from "./presentation/run-reporter.mjs";
14
- import { createTreeReporter } from "./presentation/tree-reporter.mjs";
15
- import { createRunEventsReporter } from "./presentation/events-reporter.mjs";
16
-
17
- export const sharedFlags = {
18
- dir: Flags.string({
19
- description: "Explicit product directory",
20
- }),
21
- service: Flags.string({
22
- description: "Limit the operation or assistant context to one service",
23
- }),
24
- };
25
-
26
- export const runFlags = {
27
- ...sharedFlags,
28
- type: Flags.string({
29
- char: "t",
30
- multiple: true,
31
- description: "Run specific suite type(s): int, e2e, scenario, dal, load, pw, all",
32
- }),
33
- suite: Flags.string({
34
- char: "s",
35
- multiple: true,
36
- description: "Run specific suite(s)",
37
- }),
38
- file: Flags.string({
39
- char: "f",
40
- multiple: true,
41
- description: "Run specific file(s)",
42
- }),
43
- workers: Flags.string({
44
- description: "Number of test executors for the whole run",
45
- }),
46
- "file-timeout-seconds": Flags.string({
47
- description: "Per-file wall-clock timeout in seconds",
48
- }),
49
- shard: Flags.string({
50
- description: "Run only shard i of n at suite granularity",
51
- }),
52
- seed: Flags.string({
53
- description: "Deterministic seed for scenario suites",
54
- }),
55
- "write-status": Flags.boolean({
56
- description: "Write a deterministic testkit.status.json snapshot",
57
- default: false,
58
- }),
59
- "allow-partial-status": Flags.boolean({
60
- description: "Allow --write-status for filtered runs",
61
- default: false,
62
- }),
63
- "ignore-skip-rules": Flags.boolean({
64
- description: "Run files even if testkit.config.ts marks them skipped",
65
- default: false,
66
- }),
67
- "output-mode": Flags.string({
68
- description: "Reporter mode",
69
- options: ["compact", "debug", "events"],
70
- }),
71
- debug: Flags.boolean({
72
- description: "Alias for --output-mode debug",
73
- default: false,
74
- }),
75
- };
76
-
77
- export async function resolveConfigsForCommand(flags) {
78
- return loadManagedConfigs({ dir: flags.dir, service: flags.service });
79
- }
80
-
81
- export async function executeRunCommand(command, flags, positionalType = null) {
82
- const request = await buildRunRequest(flags, positionalType, process.cwd(), process.cwd());
83
- const { allConfigs, configs, typeValues, suiteSelectors, productDir } = request;
84
- const outputMode = command.jsonEnabled()
85
- ? "json"
86
- : flags.debug
87
- ? "debug"
88
- : flags["output-mode"] || "compact";
89
-
90
- let reporter;
91
- let finalize = Promise.resolve();
92
- let close = () => {};
93
-
94
- if (outputMode === "compact" && process.stdout.isTTY) {
95
- const tree = createTreeReporter({
96
- stdout: process.stdout,
97
- stderr: process.stderr,
98
- productDir,
99
- });
100
- reporter = tree.reporter;
101
- finalize = tree.finalize;
102
- close = tree.close;
103
- } else if (outputMode === "events") {
104
- reporter = createRunEventsReporter({ stdout: process.stdout, stderr: process.stderr });
105
- } else {
106
- reporter = createRunReporter({ outputMode });
107
- }
108
-
109
- try {
110
- const result = await runner.runAll(
111
- configs,
112
- typeValues,
113
- suiteSelectors,
114
- {
115
- reporter,
116
- ...request.runOptions,
117
- },
118
- allConfigs
119
- );
120
- await finalize;
121
- return {
122
- outputMode,
123
- ...result,
124
- };
125
- } catch (error) {
126
- close();
127
- await finalize.catch(() => {});
128
- throw error;
129
- }
130
- }
131
-
132
- export async function buildRunRequest(flags, positionalType = null, cwd = process.cwd(), invocationCwd = process.cwd()) {
133
- const { allConfigs, configs } = await resolveConfigsForCommand(flags);
134
- const workers = flags.workers == null ? null : parseWorkersOption(flags.workers);
135
- const fileTimeoutSeconds =
136
- flags["file-timeout-seconds"] == null
137
- ? null
138
- : parseFileTimeoutOption(flags["file-timeout-seconds"]);
139
- const shard = parseShardOption(flags.shard);
140
- const typeValues = parseTypeOption(flags.type, positionalType);
141
- const suiteSelectors = parseSuiteOption(flags.suite);
142
- const rawFileNames = Array.isArray(flags.file) ? flags.file : [flags.file].filter(Boolean);
143
- const productDir = allConfigs[0]?.productDir || cwd;
144
- const fileNames = resolveRequestedFiles(rawFileNames, productDir, invocationCwd);
145
-
146
- return {
147
- allConfigs,
148
- configs,
149
- productDir,
150
- typeValues,
151
- suiteSelectors,
152
- runOptions: {
153
- ...flags,
154
- typeValues,
155
- fileNames,
156
- workers,
157
- fileTimeoutSeconds,
158
- shard,
159
- scenarioSeed: flags.seed || null,
160
- serviceFilter: flags.service || null,
161
- writeStatus: flags["write-status"],
162
- allowPartialStatus: flags["allow-partial-status"],
163
- ignoreSkipRules: flags["ignore-skip-rules"],
164
- },
165
- };
166
- }
167
-
168
- export async function runStatusLike(commandName, flags) {
169
- const { allConfigs, configs } = await resolveConfigsForCommand(flags);
170
-
171
- if (commandName === "cleanup") {
172
- await runner.cleanup(allConfigs[0]?.productDir || process.cwd());
173
- return { ok: true };
174
- }
175
-
176
- const productResults = [];
177
- for (const config of configs) {
178
- if (commandName === "status") {
179
- productResults.push(runner.showStatus(config));
180
- continue;
181
- }
182
- await runner.destroy(config);
183
- productResults.push({ name: config.name, destroyed: true });
184
- }
185
-
186
- return { ok: true, results: productResults };
187
- }
188
-
189
- export function relativeToProduct(productDir, targetPath) {
190
- return path.relative(productDir, targetPath);
191
- }