@elench/testkit 0.1.86 → 0.1.88

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 (38) hide show
  1. package/README.md +19 -12
  2. package/lib/cli/agents/providers/claude.mjs +1 -1
  3. package/lib/cli/agents/providers/codex.mjs +1 -1
  4. package/lib/cli/assistant/prompt-builder.mjs +78 -0
  5. package/lib/cli/assistant/protocol.mjs +67 -0
  6. package/lib/cli/assistant/session.mjs +92 -0
  7. package/lib/cli/assistant/slash-commands.mjs +160 -0
  8. package/lib/cli/assistant/state.mjs +279 -0
  9. package/lib/cli/assistant/tool-registry.mjs +236 -0
  10. package/lib/cli/assistant/tool-run-reporter.mjs +80 -0
  11. package/lib/cli/command-helpers.mjs +40 -24
  12. package/lib/cli/commands/assistant.mjs +84 -0
  13. package/lib/cli/entrypoint.mjs +37 -11
  14. package/lib/cli/presentation/tree-reporter.mjs +34 -28
  15. package/lib/cli/tui/assistant-app.mjs +131 -0
  16. package/lib/cli/tui/detail-pane.mjs +161 -0
  17. package/lib/cli/tui/filter-bar.mjs +12 -0
  18. package/lib/cli/tui/fuzzy-match.mjs +106 -0
  19. package/lib/cli/tui/inspect-app.mjs +306 -0
  20. package/lib/cli/tui/inspect-artifact-adapter.mjs +3 -0
  21. package/lib/cli/tui/inspect-live-adapter.mjs +15 -0
  22. package/lib/cli/tui/inspect-model.mjs +817 -0
  23. package/lib/cli/tui/inspect-state.mjs +321 -0
  24. package/node_modules/@elench/next-analysis/package.json +1 -1
  25. package/node_modules/@elench/testkit-bridge/package.json +2 -2
  26. package/node_modules/@elench/testkit-protocol/package.json +1 -1
  27. package/node_modules/@elench/ts-analysis/package.json +1 -1
  28. package/package.json +6 -6
  29. package/lib/cli/commands/artifacts.mjs +0 -45
  30. package/lib/cli/commands/investigate.mjs +0 -87
  31. package/lib/cli/commands/logs.mjs +0 -47
  32. package/lib/cli/commands/show.mjs +0 -47
  33. package/lib/cli/commands/watch.mjs +0 -23
  34. package/lib/cli/tui/run-app.mjs +0 -1
  35. package/lib/cli/tui/run-session-app.mjs +0 -432
  36. package/lib/cli/tui/run-session-state.mjs +0 -505
  37. package/lib/cli/tui/run-tree-state.mjs +0 -1
  38. package/lib/cli/tui/watch-app.mjs +0 -220
@@ -0,0 +1,279 @@
1
+ import { loadCurrentRunArtifact, loadLatestRunArtifact } from "../viewer.mjs";
2
+ import { createInspectState } from "../tui/inspect-state.mjs";
3
+ import { parseSlashCommand, formatSlashHelpLines } from "./slash-commands.mjs";
4
+ import { executeAssistantTool } from "./tool-registry.mjs";
5
+ import { runAssistantConversationTurn } from "./session.mjs";
6
+
7
+ export function createAssistantState({
8
+ productDir,
9
+ provider = "auto",
10
+ initialPane = "detail",
11
+ dataSource = "artifact",
12
+ configs = [],
13
+ } = {}) {
14
+ const inspectState = createInspectState({ dataSource });
15
+ inspectState.setPaneMode(initialPane);
16
+
17
+ const listeners = new Set();
18
+ const messages = [];
19
+ let composer = "";
20
+ let notice = null;
21
+ let busy = false;
22
+ let providerName = provider;
23
+ let activeStatus = null;
24
+
25
+ inspectState.subscribe(() => notify());
26
+
27
+ function notify() {
28
+ for (const callback of listeners) callback();
29
+ }
30
+
31
+ function appendMessage(message) {
32
+ messages.push({
33
+ id: `msg-${messages.length + 1}`,
34
+ ...message,
35
+ });
36
+ notify();
37
+ }
38
+
39
+ function setBusy(nextBusy, status = null) {
40
+ busy = Boolean(nextBusy);
41
+ activeStatus = status || null;
42
+ notify();
43
+ }
44
+
45
+ return {
46
+ inspectState,
47
+
48
+ async loadLatestArtifact() {
49
+ try {
50
+ inspectState.hydrateFromArtifact(loadLatestRunArtifact(productDir));
51
+ } catch {
52
+ // No artifact yet.
53
+ }
54
+ },
55
+
56
+ async loadCurrentArtifact() {
57
+ try {
58
+ inspectState.hydrateFromArtifact(loadCurrentRunArtifact(productDir));
59
+ } catch {
60
+ // No artifact yet.
61
+ }
62
+ },
63
+
64
+ revealFile(serviceName, filePath) {
65
+ return inspectState.revealFile(serviceName, filePath);
66
+ },
67
+
68
+ revealService(serviceName) {
69
+ return inspectState.revealService(serviceName);
70
+ },
71
+
72
+ setPaneMode(pane) {
73
+ inspectState.setPaneMode(pane);
74
+ },
75
+
76
+ setComposer(value) {
77
+ composer = String(value || "");
78
+ notify();
79
+ },
80
+
81
+ appendComposer(text) {
82
+ composer += String(text || "");
83
+ notify();
84
+ },
85
+
86
+ backspaceComposer() {
87
+ composer = composer.slice(0, -1);
88
+ notify();
89
+ },
90
+
91
+ clearNotice() {
92
+ notice = null;
93
+ notify();
94
+ },
95
+
96
+ setNotice(message) {
97
+ notice = message ? String(message) : null;
98
+ notify();
99
+ },
100
+
101
+ setProvider(nextProvider) {
102
+ providerName = nextProvider || "auto";
103
+ notify();
104
+ },
105
+
106
+ clearMessages() {
107
+ messages.length = 0;
108
+ notify();
109
+ },
110
+
111
+ async submitCurrentComposer() {
112
+ const value = composer.trim();
113
+ composer = "";
114
+ notify();
115
+ if (!value) return;
116
+ await this.submitInput(value);
117
+ },
118
+
119
+ async submitInput(input) {
120
+ const trimmed = String(input || "").trim();
121
+ if (!trimmed) return;
122
+ appendMessage({ role: "user", text: trimmed });
123
+
124
+ const slash = parseSlashCommandSafe(trimmed);
125
+ if (slash?.type === "__error__") {
126
+ appendMessage({
127
+ role: "system",
128
+ text: slash.error,
129
+ });
130
+ return;
131
+ }
132
+ if (slash) {
133
+ try {
134
+ await executeSlashCommand({
135
+ slash,
136
+ productDir,
137
+ inspectState,
138
+ configs,
139
+ setProvider: (value) => {
140
+ providerName = value;
141
+ },
142
+ appendMessage,
143
+ setNotice: (value) => {
144
+ notice = value;
145
+ },
146
+ clearMessages: () => {
147
+ messages.length = 0;
148
+ },
149
+ });
150
+ } catch (error) {
151
+ appendMessage({
152
+ role: "system",
153
+ text: error instanceof Error ? error.message : String(error),
154
+ });
155
+ }
156
+ notify();
157
+ return;
158
+ }
159
+
160
+ try {
161
+ setBusy(true, "Waiting for assistant...");
162
+ const emitted = await runAssistantConversationTurn({
163
+ productDir,
164
+ inspectState,
165
+ transcript: messages.map((entry) => ({ role: entry.role, text: entry.text })),
166
+ userMessage: trimmed,
167
+ provider: providerName,
168
+ configs,
169
+ onStatus(status) {
170
+ activeStatus = status;
171
+ notify();
172
+ },
173
+ });
174
+ for (const message of emitted) appendMessage(message);
175
+ } catch (error) {
176
+ appendMessage({
177
+ role: "system",
178
+ text: error instanceof Error ? error.message : String(error),
179
+ });
180
+ } finally {
181
+ setBusy(false, null);
182
+ }
183
+ },
184
+
185
+ subscribe(callback) {
186
+ listeners.add(callback);
187
+ return () => listeners.delete(callback);
188
+ },
189
+
190
+ getSnapshot() {
191
+ return {
192
+ workbench: inspectState.getSnapshot(),
193
+ messages: [...messages],
194
+ composer,
195
+ notice,
196
+ busy,
197
+ provider: providerName,
198
+ activeStatus,
199
+ };
200
+ },
201
+ };
202
+ }
203
+
204
+ async function executeSlashCommand({
205
+ slash,
206
+ productDir,
207
+ inspectState,
208
+ configs,
209
+ setProvider,
210
+ appendMessage,
211
+ setNotice,
212
+ clearMessages,
213
+ } = {}) {
214
+ if (slash.type === "help") {
215
+ appendMessage({ role: "assistant", text: formatSlashHelpLines().join("\n") });
216
+ return;
217
+ }
218
+ if (slash.type === "clear") {
219
+ clearMessages?.();
220
+ return;
221
+ }
222
+ if (slash.type === "quit") {
223
+ setNotice?.("Use q or Ctrl+C to quit the interactive assistant.");
224
+ return;
225
+ }
226
+ if (slash.type === "provider") {
227
+ setProvider(slash.provider);
228
+ appendMessage({ role: "assistant", text: `Provider set to ${slash.provider}.` });
229
+ return;
230
+ }
231
+
232
+ const result = await executeSlashTool(slash, {
233
+ productDir,
234
+ inspectState,
235
+ configs,
236
+ });
237
+ appendMessage({
238
+ role: "tool",
239
+ toolName: slash.type,
240
+ text: result.text || result.title || "Done.",
241
+ data: result.data || null,
242
+ });
243
+ }
244
+
245
+ async function executeSlashTool(slash, context) {
246
+ switch (slash.type) {
247
+ case "pane":
248
+ return executeAssistantTool("set_pane", { pane: slash.pane }, context);
249
+ case "file":
250
+ return executeAssistantTool("select_file", { file: slash.file }, context);
251
+ case "inspect":
252
+ return slash.file
253
+ ? executeAssistantTool("select_file", { file: slash.file }, context)
254
+ : executeAssistantTool("inspect_selection", {}, context);
255
+ case "service":
256
+ return executeAssistantTool("select_service", { service: slash.service }, context);
257
+ case "status":
258
+ return executeAssistantTool("show_status", {}, context);
259
+ case "discover":
260
+ return executeAssistantTool("discover_tests", {}, context);
261
+ case "doctor":
262
+ return executeAssistantTool("run_doctor", {}, context);
263
+ case "run":
264
+ return executeAssistantTool("run_tests", slash.options, context);
265
+ default:
266
+ throw new Error(`Unsupported slash command "${slash.type}"`);
267
+ }
268
+ }
269
+
270
+ function parseSlashCommandSafe(input) {
271
+ try {
272
+ return parseSlashCommand(input);
273
+ } catch (error) {
274
+ return {
275
+ type: "__error__",
276
+ error: error instanceof Error ? error.message : String(error),
277
+ };
278
+ }
279
+ }
@@ -0,0 +1,236 @@
1
+ import { discoverTests } from "../../discovery/index.mjs";
2
+ import { runDoctor } from "../../app/doctor.mjs";
3
+ import { resolveProductDir } from "../../config/index.mjs";
4
+ import { resolveRequestedFiles } from "../args.mjs";
5
+ import { buildDiscoveryReportLines } from "../presentation/discovery-reporter.mjs";
6
+ import { loadCurrentRunArtifact, loadLatestRunArtifact, resolveFileSubject } from "../viewer.mjs";
7
+ import { buildInspectPaneContent } from "../tui/detail-pane.mjs";
8
+ import { createAssistantRunReporter } from "./tool-run-reporter.mjs";
9
+ import { buildRunRequest } from "../command-helpers.mjs";
10
+ import * as runner from "../../runner/index.mjs";
11
+
12
+ export function listAssistantTools() {
13
+ return [
14
+ { name: "run_tests", description: "Run testkit-managed tests with optional type/service/file filters." },
15
+ { name: "select_file", description: "Focus a specific test file in the workbench." },
16
+ { name: "select_service", description: "Focus a specific service in the workbench." },
17
+ { name: "set_pane", description: "Switch the workbench pane: detail, artifacts, logs, or setup." },
18
+ { name: "inspect_selection", description: "Render the current selection in the active pane." },
19
+ { name: "discover_tests", description: "Discover managed tests and summarize them." },
20
+ { name: "show_status", description: "Show the current local testkit state for the product." },
21
+ { name: "run_doctor", description: "Run built-in testkit doctor checks." },
22
+ ];
23
+ }
24
+
25
+ export async function executeAssistantTool(name, argumentsObject, context) {
26
+ const args = argumentsObject && typeof argumentsObject === "object" ? argumentsObject : {};
27
+ switch (name) {
28
+ case "run_tests":
29
+ return runTestsTool(args, context);
30
+ case "select_file":
31
+ return selectFileTool(args, context);
32
+ case "select_service":
33
+ return selectServiceTool(args, context);
34
+ case "set_pane":
35
+ return setPaneTool(args, context);
36
+ case "inspect_selection":
37
+ return inspectSelectionTool(args, context);
38
+ case "discover_tests":
39
+ return discoverTestsTool(args, context);
40
+ case "show_status":
41
+ return showStatusTool(args, context);
42
+ case "run_doctor":
43
+ return runDoctorTool(args, context);
44
+ default:
45
+ throw new Error(`Unknown assistant tool "${name}"`);
46
+ }
47
+ }
48
+
49
+ async function runTestsTool(args, context) {
50
+ const request = await buildRunRequest(
51
+ {
52
+ dir: context.productDir,
53
+ service: args.service || null,
54
+ type: normalizeArray(args.type),
55
+ suite: normalizeArray(args.suite),
56
+ file: normalizeArray(args.file),
57
+ workers: args.workers == null ? null : String(args.workers),
58
+ "file-timeout-seconds": args.fileTimeoutSeconds == null ? null : String(args.fileTimeoutSeconds),
59
+ shard: args.shard || null,
60
+ seed: args.seed || null,
61
+ "write-status": Boolean(args.writeStatus),
62
+ "allow-partial-status": Boolean(args.allowPartialStatus),
63
+ "ignore-skip-rules": Boolean(args.ignoreSkipRules),
64
+ },
65
+ null,
66
+ context.productDir,
67
+ process.cwd()
68
+ );
69
+
70
+ context.inspectState.resetForLive();
71
+ const liveReporter = createAssistantRunReporter({
72
+ inspectState: context.inspectState,
73
+ onStatus(message) {
74
+ context.onEvent?.({ type: "tool-status", tool: "run_tests", message });
75
+ },
76
+ });
77
+
78
+ try {
79
+ const result = await runner.runAll(
80
+ request.configs,
81
+ request.typeValues,
82
+ request.suiteSelectors,
83
+ {
84
+ ...request.runOptions,
85
+ reporter: liveReporter,
86
+ },
87
+ request.allConfigs
88
+ );
89
+ await liveReporter.finalize;
90
+
91
+ return {
92
+ ok: result.ok,
93
+ title: "Run complete",
94
+ text: summarizeRunResult(context.inspectState.getSnapshot()),
95
+ data: result,
96
+ };
97
+ } catch (error) {
98
+ liveReporter.close();
99
+ await liveReporter.finalize.catch(() => {});
100
+ throw error;
101
+ }
102
+ }
103
+
104
+ function selectFileTool(args, context) {
105
+ const file = args.file || args.path || null;
106
+ if (!file) throw new Error("select_file requires a file argument");
107
+ ensureArtifactLoaded(context);
108
+ const artifact = context.inspectState.getSnapshot().runArtifact;
109
+ const subject = resolveFileSubject(artifact, file, args.service || null);
110
+ context.inspectState.revealFile(subject.service.name, subject.file.path);
111
+ return inspectSelectionTool({}, context);
112
+ }
113
+
114
+ function selectServiceTool(args, context) {
115
+ const service = args.service || args.name || null;
116
+ if (!service) throw new Error("select_service requires a service argument");
117
+ ensureArtifactLoaded(context);
118
+ if (!context.inspectState.revealService(service)) {
119
+ throw new Error(`Unknown service "${service}"`);
120
+ }
121
+ return inspectSelectionTool({}, context);
122
+ }
123
+
124
+ function setPaneTool(args, context) {
125
+ const pane = args.pane || "detail";
126
+ context.inspectState.setPaneMode(pane);
127
+ return inspectSelectionTool({}, context);
128
+ }
129
+
130
+ function inspectSelectionTool(_args, context) {
131
+ ensureArtifactLoaded(context);
132
+ const snapshot = context.inspectState.getSnapshot();
133
+ const pane = buildInspectPaneContent({
134
+ productDir: context.productDir,
135
+ snapshot,
136
+ paneMode: snapshot.paneMode,
137
+ logTail: 12,
138
+ });
139
+ return {
140
+ ok: true,
141
+ title: pane.title,
142
+ text: pane.lines.join("\n"),
143
+ data: {
144
+ title: pane.title,
145
+ lines: pane.lines,
146
+ selection: snapshot.selectedEntry,
147
+ paneMode: snapshot.paneMode,
148
+ },
149
+ };
150
+ }
151
+
152
+ async function discoverTestsTool(args, context) {
153
+ const productDir = resolveProductDir(process.cwd(), context.productDir);
154
+ const fileNames = resolveRequestedFiles(normalizeArray(args.file), productDir, process.cwd());
155
+ const result = await discoverTests({
156
+ dir: productDir,
157
+ service: args.service || null,
158
+ type: normalizeArray(args.type),
159
+ suite: normalizeArray(args.suite),
160
+ file: fileNames,
161
+ runnableOnly: Boolean(args.runnableOnly),
162
+ diagnostics: args.strict ? "error" : "report",
163
+ });
164
+ const lines = buildDiscoveryReportLines(result, { outputMode: args.outputMode || "compact" });
165
+ return {
166
+ ok: true,
167
+ title: "Discovery",
168
+ text: lines.join("\n"),
169
+ data: result,
170
+ };
171
+ }
172
+
173
+ function showStatusTool(_args, context) {
174
+ const { output } = captureConsoleOutput(() => runner.showStatus(context.configs[0]));
175
+ const lines = output.length > 0 ? output : ["No state"];
176
+ return {
177
+ ok: true,
178
+ title: "Status",
179
+ text: lines.join("\n"),
180
+ data: { lines },
181
+ };
182
+ }
183
+
184
+ async function runDoctorTool(args, context) {
185
+ const result = await runDoctor({
186
+ dir: context.productDir,
187
+ typecheck: args.typecheck ?? true,
188
+ });
189
+ const lines = [
190
+ `testkit doctor ${result.ok ? "passed" : "failed"} for ${result.productDir}`,
191
+ ...result.checks.map((check) => `${String(check.level || "").toUpperCase()} ${check.code} ${check.message}`),
192
+ ];
193
+ return {
194
+ ok: result.ok,
195
+ title: "Doctor",
196
+ text: lines.join("\n"),
197
+ data: result,
198
+ };
199
+ }
200
+
201
+ function ensureArtifactLoaded(context) {
202
+ const snapshot = context.inspectState.getSnapshot();
203
+ if (snapshot.runArtifact) return snapshot.runArtifact;
204
+ try {
205
+ context.inspectState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
206
+ } catch {
207
+ context.inspectState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
208
+ }
209
+ return context.inspectState.getSnapshot().runArtifact;
210
+ }
211
+
212
+ function normalizeArray(value) {
213
+ if (value == null) return [];
214
+ if (Array.isArray(value)) return value.filter(Boolean);
215
+ return [value].filter(Boolean);
216
+ }
217
+
218
+ function summarizeRunResult(snapshot) {
219
+ const rows = snapshot?.summaryData?.rows || [];
220
+ if (rows.length === 0) return "Run finished.";
221
+ return rows.map(([label, value]) => `${label}: ${value}`).join("\n");
222
+ }
223
+
224
+ function captureConsoleOutput(fn) {
225
+ const output = [];
226
+ const originalLog = console.log;
227
+ console.log = (...args) => {
228
+ output.push(args.map((value) => String(value)).join(" "));
229
+ };
230
+ try {
231
+ const result = fn();
232
+ return { result, output };
233
+ } finally {
234
+ console.log = originalLog;
235
+ }
236
+ }
@@ -0,0 +1,80 @@
1
+ import {
2
+ applyReporterPlans,
3
+ applyReporterRunSummary,
4
+ applyReporterTaskFinished,
5
+ applyReporterTaskStarted,
6
+ } from "../tui/inspect-live-adapter.mjs";
7
+ import { suiteSelectionType } from "../../runner/suite-selection.mjs";
8
+
9
+ export function createAssistantRunReporter({ inspectState, onStatus } = {}) {
10
+ return {
11
+ reporter: {
12
+ outputMode: "compact",
13
+
14
+ setServicePlans(plans) {
15
+ applyReporterPlans(inspectState, plans);
16
+ onStatus?.("Planned run.");
17
+ },
18
+
19
+ setTotalFileCount(count) {
20
+ inspectState.setTotalFileCount(count);
21
+ },
22
+
23
+ setRegressionCatalog(document) {
24
+ inspectState.setRegressionCatalog(document);
25
+ },
26
+
27
+ serviceSkipped(config, reason) {
28
+ inspectState.markServiceSkipped(config.name, reason);
29
+ },
30
+
31
+ plannedSkip(entry) {
32
+ inspectState.markPlannedSkip(entry);
33
+ },
34
+
35
+ taskStarted(task) {
36
+ const suiteKey = `${task.displayType || suiteSelectionType(task.type, task.framework)}:${task.suiteName}`;
37
+ applyReporterTaskStarted(inspectState, task, suiteKey);
38
+ },
39
+
40
+ taskFinished(task, outcome) {
41
+ applyReporterTaskFinished(inspectState, task, outcome);
42
+ if (outcome.failed) {
43
+ onStatus?.(`Failed ${task.file}`);
44
+ }
45
+ },
46
+
47
+ runtimeError(task, message) {
48
+ inspectState.markRuntimeError(task, message);
49
+ },
50
+
51
+ setupOperationFinished() {},
52
+
53
+ phaseStarted(label) {
54
+ inspectState.setPhase(label);
55
+ onStatus?.(label);
56
+ },
57
+
58
+ toolchainResolved() {},
59
+ localServiceStarting() {},
60
+ writeLine() {},
61
+ writeDebugLine() {},
62
+ telemetry() {},
63
+
64
+ runSummary(results, durationMs, regressionReport) {
65
+ applyReporterRunSummary(inspectState, results, durationMs, regressionReport);
66
+ onStatus?.("Run finished.");
67
+ },
68
+
69
+ error(message) {
70
+ onStatus?.(String(message));
71
+ },
72
+ },
73
+
74
+ finalize: Promise.resolve(),
75
+
76
+ close() {
77
+ // No long-lived UI resources to close.
78
+ },
79
+ };
80
+ }
@@ -19,7 +19,7 @@ export const sharedFlags = {
19
19
  description: "Explicit product directory",
20
20
  }),
21
21
  service: Flags.string({
22
- description: "Run or inspect only one service",
22
+ description: "Limit the operation or assistant context to one service",
23
23
  }),
24
24
  };
25
25
 
@@ -79,18 +79,8 @@ export async function resolveConfigsForCommand(flags) {
79
79
  }
80
80
 
81
81
  export async function executeRunCommand(command, flags, positionalType = null) {
82
- const { allConfigs, configs } = await resolveConfigsForCommand(flags);
83
- const workers = flags.workers == null ? null : parseWorkersOption(flags.workers);
84
- const fileTimeoutSeconds =
85
- flags["file-timeout-seconds"] == null
86
- ? null
87
- : parseFileTimeoutOption(flags["file-timeout-seconds"]);
88
- const shard = parseShardOption(flags.shard);
89
- const typeValues = parseTypeOption(flags.type, positionalType);
90
- const suiteSelectors = parseSuiteOption(flags.suite);
91
- const rawFileNames = Array.isArray(flags.file) ? flags.file : [flags.file].filter(Boolean);
92
- const productDir = allConfigs[0]?.productDir || process.cwd();
93
- const fileNames = resolveRequestedFiles(rawFileNames, productDir, process.cwd());
82
+ const request = await buildRunRequest(flags, positionalType, process.cwd(), process.cwd());
83
+ const { allConfigs, configs, typeValues, suiteSelectors, productDir } = request;
94
84
  const outputMode = command.jsonEnabled()
95
85
  ? "json"
96
86
  : flags.debug
@@ -122,18 +112,8 @@ export async function executeRunCommand(command, flags, positionalType = null) {
122
112
  typeValues,
123
113
  suiteSelectors,
124
114
  {
125
- ...flags,
126
- typeValues,
127
- fileNames,
128
- workers,
129
- fileTimeoutSeconds,
130
- shard,
131
- scenarioSeed: flags.seed || null,
132
- serviceFilter: flags.service || null,
133
115
  reporter,
134
- writeStatus: flags["write-status"],
135
- allowPartialStatus: flags["allow-partial-status"],
136
- ignoreSkipRules: flags["ignore-skip-rules"],
116
+ ...request.runOptions,
137
117
  },
138
118
  allConfigs
139
119
  );
@@ -149,6 +129,42 @@ export async function executeRunCommand(command, flags, positionalType = null) {
149
129
  }
150
130
  }
151
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
+
152
168
  export async function runStatusLike(commandName, flags) {
153
169
  const { allConfigs, configs } = await resolveConfigsForCommand(flags);
154
170