@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.
- package/lib/app/browser-bridge.mjs +1 -1
- package/lib/cli/assistant/app.mjs +49 -12
- package/lib/cli/assistant/composer.mjs +19 -1
- package/lib/cli/assistant/context-pack.mjs +9 -8
- package/lib/cli/assistant/interactive.mjs +1 -1
- package/lib/cli/assistant/model-discovery.mjs +243 -0
- package/lib/cli/assistant/prompt-builder.mjs +2 -5
- package/lib/cli/{agents → assistant}/providers/claude.mjs +41 -3
- package/lib/cli/{agents → assistant}/providers/codex.mjs +33 -14
- package/lib/cli/{agents → assistant/providers}/index.mjs +3 -3
- package/lib/cli/{agents → assistant}/providers/shared.mjs +6 -2
- package/lib/cli/assistant/session.mjs +31 -6
- package/lib/cli/assistant/slash-commands.mjs +30 -3
- package/lib/cli/assistant/state.mjs +237 -71
- package/lib/cli/assistant/tool-registry.mjs +325 -39
- package/lib/cli/assistant/view-model.mjs +1 -1
- package/lib/cli/commands/assistant.mjs +4 -3
- package/lib/cli/commands/browser/serve.mjs +5 -23
- package/lib/cli/commands/cleanup.mjs +8 -2
- package/lib/cli/commands/db/snapshot/capture.mjs +8 -4
- package/lib/cli/commands/destroy.mjs +8 -2
- package/lib/cli/commands/discover.mjs +5 -27
- package/lib/cli/commands/doctor.mjs +5 -5
- package/lib/cli/commands/flags.mjs +61 -0
- package/lib/cli/commands/run.mjs +10 -2
- package/lib/cli/commands/status.mjs +10 -2
- package/lib/cli/commands/typecheck.mjs +5 -5
- package/lib/cli/{tui/inspect-app.mjs → components/blocks/run-tree.mjs} +29 -54
- package/lib/cli/{tui → components/primitives}/filter-bar.mjs +1 -1
- package/lib/cli/{presentation → components/primitives}/summary-box.mjs +1 -1
- package/lib/cli/config.mjs +63 -0
- package/lib/cli/operations/browser/serve/operation.mjs +23 -0
- package/lib/cli/operations/cleanup/operation.mjs +8 -0
- package/lib/cli/{db.mjs → operations/db/snapshot/capture/operation.mjs} +15 -9
- package/lib/cli/operations/destroy/operation.mjs +12 -0
- package/lib/cli/operations/discover/operation.mjs +32 -0
- package/lib/cli/operations/doctor/operation.mjs +5 -0
- package/lib/cli/operations/run/operation.mjs +129 -0
- package/lib/cli/operations/status/operation.mjs +7 -0
- package/lib/cli/operations/typecheck/operation.mjs +5 -0
- package/lib/cli/renderers/browser-serve/text.mjs +6 -0
- package/lib/cli/renderers/cleanup/text.mjs +3 -0
- package/lib/cli/renderers/db-snapshot-capture/text.mjs +3 -0
- package/lib/cli/renderers/destroy/text.mjs +3 -0
- package/lib/cli/{presentation/discovery-reporter.mjs → renderers/discover/report.mjs} +3 -3
- package/lib/cli/renderers/discover/text.mjs +7 -0
- package/lib/cli/renderers/doctor/text.mjs +7 -0
- package/lib/cli/{presentation/failure-presentation.mjs → renderers/run/failure.mjs} +6 -6
- package/lib/cli/renderers/run/interactive.mjs +119 -0
- package/lib/cli/{presentation/run-reporter.mjs → renderers/run/text-reporter.mjs} +5 -5
- package/lib/cli/renderers/status/text.mjs +7 -0
- package/lib/cli/renderers/typecheck/text.mjs +7 -0
- package/lib/cli/{tui/inspect-model.mjs → state/run/model.mjs} +11 -26
- package/lib/cli/{tui/inspect-state.mjs → state/run/state.mjs} +11 -18
- package/lib/cli/{tui → state/tree}/fuzzy-match.mjs +1 -1
- package/lib/cli/terminal/capabilities.mjs +33 -0
- package/lib/database/index.mjs +9 -21
- package/lib/database/template-steps.mjs +3 -3
- package/lib/{cli/viewer.mjs → results/artifacts.mjs} +1 -1
- package/lib/{cli/context-resources.mjs → results/context.mjs} +1 -1
- package/lib/runner/maintenance.mjs +25 -14
- package/lib/runner/readiness.mjs +5 -4
- package/lib/runner/runtime-preparation.mjs +36 -0
- package/lib/runner/state-io.mjs +10 -4
- package/lib/runner/template.mjs +24 -3
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +5 -5
- package/lib/cli/assistant/command-plan.mjs +0 -227
- package/lib/cli/command-helpers.mjs +0 -191
- package/lib/cli/presentation/tree-reporter.mjs +0 -96
- package/lib/cli/tui/inspect-artifact-adapter.mjs +0 -3
- package/lib/cli/tui/inspect-live-adapter.mjs +0 -15
- /package/lib/cli/{presentation/events-reporter.mjs → renderers/run/events.mjs} +0 -0
- /package/lib/cli/{presentation → terminal}/colors.mjs +0 -0
- /package/lib/cli/{presentation/terminal-layout.mjs → terminal/layout.mjs} +0 -0
- /package/lib/{cli/presentation → results}/code-frames.mjs +0 -0
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { execaCommand } from "execa";
|
|
4
|
-
import { loadCurrentRunArtifact, loadLatestRunArtifact, resolveFileSubject } from "
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
} from "../
|
|
8
|
-
import {
|
|
4
|
+
import { loadCurrentRunArtifact, loadLatestRunArtifact, resolveFileSubject } from "../../results/artifacts.mjs";
|
|
5
|
+
import { readContextContent } from "../../results/context.mjs";
|
|
6
|
+
import { buildRunRequest, executeRunRequest } from "../operations/run/operation.mjs";
|
|
7
|
+
import { executeDiscoverOperation } from "../operations/discover/operation.mjs";
|
|
8
|
+
import { executeDoctorOperation } from "../operations/doctor/operation.mjs";
|
|
9
|
+
import { executeTypecheckOperation } from "../operations/typecheck/operation.mjs";
|
|
10
|
+
import { executeStatusOperation } from "../operations/status/operation.mjs";
|
|
11
|
+
import { renderDiscoverResult } from "../renderers/discover/text.mjs";
|
|
12
|
+
import { renderDoctorResult } from "../renderers/doctor/text.mjs";
|
|
13
|
+
import { renderTypecheckResult } from "../renderers/typecheck/text.mjs";
|
|
14
|
+
import { renderStatusResult } from "../renderers/status/text.mjs";
|
|
15
|
+
import { createRunSession } from "../renderers/run/interactive.mjs";
|
|
16
|
+
import { loadCliConfig } from "../config.mjs";
|
|
9
17
|
|
|
10
18
|
const COMMAND_OUTPUT_LIMIT = 14_000;
|
|
11
19
|
const COMMAND_LINE_LIMIT = 220;
|
|
@@ -13,9 +21,29 @@ const FILE_LINE_LIMIT = 160;
|
|
|
13
21
|
|
|
14
22
|
export function listAssistantTools() {
|
|
15
23
|
return [
|
|
24
|
+
{
|
|
25
|
+
name: "run_tests",
|
|
26
|
+
description: "Run testkit suites in-process with the shared live run session and tree state.",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "discover_tests",
|
|
30
|
+
description: "Discover testkit suites and files in the current repository.",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "show_status",
|
|
34
|
+
description: "Show local testkit runtime and database state for the current repository.",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "run_doctor",
|
|
38
|
+
description: "Run built-in testkit doctor checks for the current repository.",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "run_typecheck",
|
|
42
|
+
description: "Run testkit typechecking for config, helpers, and suites.",
|
|
43
|
+
},
|
|
16
44
|
{
|
|
17
45
|
name: "shell_exec",
|
|
18
|
-
description: "Execute a shell command inside the repository
|
|
46
|
+
description: "Execute a shell command inside the repository for arbitrary repo work outside first-class testkit actions.",
|
|
19
47
|
},
|
|
20
48
|
{
|
|
21
49
|
name: "read_context",
|
|
@@ -35,6 +63,16 @@ export function listAssistantTools() {
|
|
|
35
63
|
export async function executeAssistantTool(name, argumentsObject, context) {
|
|
36
64
|
const args = argumentsObject && typeof argumentsObject === "object" ? argumentsObject : {};
|
|
37
65
|
switch (name) {
|
|
66
|
+
case "run_tests":
|
|
67
|
+
return runTestsTool(args, context);
|
|
68
|
+
case "discover_tests":
|
|
69
|
+
return discoverTestsTool(args, context);
|
|
70
|
+
case "show_status":
|
|
71
|
+
return showStatusTool(args, context);
|
|
72
|
+
case "run_doctor":
|
|
73
|
+
return doctorTool(args, context);
|
|
74
|
+
case "run_typecheck":
|
|
75
|
+
return typecheckTool(args, context);
|
|
38
76
|
case "shell_exec":
|
|
39
77
|
return shellExecTool(args, context);
|
|
40
78
|
case "read_context":
|
|
@@ -49,10 +87,13 @@ export async function executeAssistantTool(name, argumentsObject, context) {
|
|
|
49
87
|
}
|
|
50
88
|
|
|
51
89
|
async function shellExecTool(args, context) {
|
|
52
|
-
const command =
|
|
90
|
+
const command = String(args.command || "").trim();
|
|
53
91
|
if (!command) throw new Error("shell_exec requires a command string");
|
|
92
|
+
if (isTestkitCommand(command)) {
|
|
93
|
+
throw new Error("Use the dedicated testkit tools for run/discover/status/doctor/typecheck instead of shell_exec.");
|
|
94
|
+
}
|
|
54
95
|
|
|
55
|
-
const shellCommand =
|
|
96
|
+
const shellCommand = classifyShellCommand(command);
|
|
56
97
|
const commandId = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
57
98
|
context.commandLog?.appendCommandLog({
|
|
58
99
|
type: "command_start",
|
|
@@ -60,22 +101,17 @@ async function shellExecTool(args, context) {
|
|
|
60
101
|
commandId,
|
|
61
102
|
cwd: context.productDir,
|
|
62
103
|
raw: command,
|
|
63
|
-
executable: shellCommand.executableCommand,
|
|
64
|
-
normalized: shellCommand.normalized,
|
|
65
104
|
});
|
|
66
105
|
context.onEvent?.({
|
|
67
106
|
type: "tool-start",
|
|
68
107
|
tool: "shell_exec",
|
|
69
|
-
command
|
|
70
|
-
rawCommand: command,
|
|
108
|
+
command,
|
|
71
109
|
title: shellCommand.title,
|
|
72
110
|
testkitRelated: shellCommand.testkitRelated,
|
|
73
|
-
message: shellCommand.
|
|
74
|
-
? `Running ${shellCommand.displayCommand} (${shellCommand.normalizationReason})`
|
|
75
|
-
: `Running ${shellCommand.displayCommand}`,
|
|
111
|
+
message: `Running ${shellCommand.display}`,
|
|
76
112
|
});
|
|
77
113
|
|
|
78
|
-
const result = await execaCommand(
|
|
114
|
+
const result = await execaCommand(command, {
|
|
79
115
|
cwd: context.productDir,
|
|
80
116
|
reject: false,
|
|
81
117
|
shell: true,
|
|
@@ -93,21 +129,18 @@ async function shellExecTool(args, context) {
|
|
|
93
129
|
commandId,
|
|
94
130
|
cwd: context.productDir,
|
|
95
131
|
raw: command,
|
|
96
|
-
executable: shellCommand.executableCommand,
|
|
97
|
-
normalized: shellCommand.normalized,
|
|
98
132
|
code: result.exitCode ?? 0,
|
|
99
133
|
signal: result.signal ?? null,
|
|
100
134
|
});
|
|
101
135
|
context.onEvent?.({
|
|
102
136
|
type: "tool-exit",
|
|
103
137
|
tool: "shell_exec",
|
|
104
|
-
command
|
|
105
|
-
rawCommand: command,
|
|
138
|
+
command,
|
|
106
139
|
title: shellCommand.title,
|
|
107
140
|
testkitRelated: shellCommand.testkitRelated,
|
|
108
141
|
code: result.exitCode ?? 0,
|
|
109
142
|
signal: result.signal ?? null,
|
|
110
|
-
message: `${shellCommand.
|
|
143
|
+
message: `${shellCommand.display} exited ${result.exitCode ?? 0}`,
|
|
111
144
|
});
|
|
112
145
|
|
|
113
146
|
if (shellCommand.testkitRelated) {
|
|
@@ -115,15 +148,13 @@ async function shellExecTool(args, context) {
|
|
|
115
148
|
}
|
|
116
149
|
context.commandLog?.refresh?.();
|
|
117
150
|
|
|
118
|
-
const lines = formatCommandResult(result, shellCommand);
|
|
151
|
+
const lines = formatCommandResult(command, result, shellCommand);
|
|
119
152
|
return {
|
|
120
153
|
ok: (result.exitCode ?? 0) === 0,
|
|
121
154
|
title: shellCommand.title,
|
|
122
155
|
text: lines.join("\n"),
|
|
123
156
|
data: {
|
|
124
157
|
command,
|
|
125
|
-
executableCommand: shellCommand.executableCommand,
|
|
126
|
-
normalizedCommand: shellCommand.normalized,
|
|
127
158
|
stdout: result.stdout || "",
|
|
128
159
|
stderr: result.stderr || "",
|
|
129
160
|
exitCode: result.exitCode ?? 0,
|
|
@@ -133,22 +164,179 @@ async function shellExecTool(args, context) {
|
|
|
133
164
|
};
|
|
134
165
|
}
|
|
135
166
|
|
|
167
|
+
async function runTestsTool(args, context) {
|
|
168
|
+
const invocationId = `run-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
169
|
+
const normalizedArgs = normalizeRunToolArgs(args, context.productDir);
|
|
170
|
+
const rawCommand = buildRunCommandLine(normalizedArgs);
|
|
171
|
+
const request = await buildRunRequest(normalizedArgs, null, context.productDir, context.productDir);
|
|
172
|
+
const runSession = createRunSession({
|
|
173
|
+
productDir: request.productDir,
|
|
174
|
+
stderr: process.stderr,
|
|
175
|
+
config: loadCliConfig(request.productDir),
|
|
176
|
+
});
|
|
177
|
+
context.commandLog?.appendCommandLog({
|
|
178
|
+
type: "command_start",
|
|
179
|
+
command: "testkit",
|
|
180
|
+
commandId: invocationId,
|
|
181
|
+
cwd: context.productDir,
|
|
182
|
+
raw: rawCommand,
|
|
183
|
+
});
|
|
184
|
+
context.onEvent?.({
|
|
185
|
+
type: "tool-start",
|
|
186
|
+
tool: "run_tests",
|
|
187
|
+
invocationId,
|
|
188
|
+
title: buildRunToolTitle(normalizedArgs),
|
|
189
|
+
testkitRelated: true,
|
|
190
|
+
message: `Running ${buildRunDisplay(normalizedArgs)}`,
|
|
191
|
+
});
|
|
192
|
+
context.onEvent?.({
|
|
193
|
+
type: "run-session-start",
|
|
194
|
+
tool: "run_tests",
|
|
195
|
+
invocationId,
|
|
196
|
+
session: runSession,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const previousExitCode = process.exitCode;
|
|
200
|
+
try {
|
|
201
|
+
const result = await executeRunRequest(request, {
|
|
202
|
+
outputMode: "compact",
|
|
203
|
+
terminal: {
|
|
204
|
+
stdout: process.stdout,
|
|
205
|
+
stderr: process.stderr,
|
|
206
|
+
stdin: process.stdin,
|
|
207
|
+
env: context.env,
|
|
208
|
+
},
|
|
209
|
+
attachedRunSession: runSession,
|
|
210
|
+
});
|
|
211
|
+
process.exitCode = previousExitCode;
|
|
212
|
+
refreshArtifactSelection(context);
|
|
213
|
+
context.commandLog?.refresh?.();
|
|
214
|
+
context.commandLog?.appendCommandLog({
|
|
215
|
+
type: "command_exit",
|
|
216
|
+
command: "testkit",
|
|
217
|
+
commandId: invocationId,
|
|
218
|
+
cwd: context.productDir,
|
|
219
|
+
raw: rawCommand,
|
|
220
|
+
code: result.exitCode ?? 0,
|
|
221
|
+
signal: null,
|
|
222
|
+
});
|
|
223
|
+
const text = formatRunSessionText(runSession);
|
|
224
|
+
context.onEvent?.({
|
|
225
|
+
type: "tool-exit",
|
|
226
|
+
tool: "run_tests",
|
|
227
|
+
invocationId,
|
|
228
|
+
title: buildRunToolTitle(normalizedArgs),
|
|
229
|
+
testkitRelated: true,
|
|
230
|
+
code: result.exitCode ?? 0,
|
|
231
|
+
message: `${buildRunDisplay(normalizedArgs)} exited ${result.exitCode ?? 0}`,
|
|
232
|
+
});
|
|
233
|
+
context.onEvent?.({
|
|
234
|
+
type: "run-session-end",
|
|
235
|
+
tool: "run_tests",
|
|
236
|
+
invocationId,
|
|
237
|
+
session: runSession,
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
ok: (result.exitCode ?? 0) === 0,
|
|
241
|
+
title: buildRunToolTitle(normalizedArgs),
|
|
242
|
+
text,
|
|
243
|
+
data: {
|
|
244
|
+
exitCode: result.exitCode ?? 0,
|
|
245
|
+
testkitRelated: true,
|
|
246
|
+
summaryRows: runSession.getSnapshot().summaryData?.rows || [],
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
} finally {
|
|
250
|
+
process.exitCode = previousExitCode;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function discoverTestsTool(args, context) {
|
|
255
|
+
const flags = {
|
|
256
|
+
dir: context.productDir,
|
|
257
|
+
service: normalizeOptionalString(args.service),
|
|
258
|
+
type: normalizeStringArray(args.type),
|
|
259
|
+
suite: normalizeStringArray(args.suite),
|
|
260
|
+
file: normalizeStringArray(args.file || args.path),
|
|
261
|
+
"runnable-only": Boolean(args.runnableOnly || args["runnable-only"]),
|
|
262
|
+
strict: Boolean(args.strict),
|
|
263
|
+
output: normalizeOptionalString(args.output),
|
|
264
|
+
};
|
|
265
|
+
const result = await executeDiscoverOperation(flags, context.productDir);
|
|
266
|
+
return {
|
|
267
|
+
ok: true,
|
|
268
|
+
title: "testkit discover",
|
|
269
|
+
text: renderDiscoverResult(result, { outputMode: args.outputMode || "compact" }).join("\n"),
|
|
270
|
+
data: {
|
|
271
|
+
testkitRelated: true,
|
|
272
|
+
services: result.services?.length || 0,
|
|
273
|
+
files: result.files?.length || 0,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function showStatusTool(args, context) {
|
|
279
|
+
const flags = {
|
|
280
|
+
dir: context.productDir,
|
|
281
|
+
service: normalizeOptionalString(args.service),
|
|
282
|
+
};
|
|
283
|
+
const results = await executeStatusOperation(flags);
|
|
284
|
+
return {
|
|
285
|
+
ok: true,
|
|
286
|
+
title: "testkit status",
|
|
287
|
+
text: results.flatMap(renderStatusResult).join("\n"),
|
|
288
|
+
data: {
|
|
289
|
+
testkitRelated: true,
|
|
290
|
+
services: results.map((result) => result.name),
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function doctorTool(args, context) {
|
|
296
|
+
const result = await executeDoctorOperation({
|
|
297
|
+
dir: context.productDir,
|
|
298
|
+
typecheck: args.typecheck == null ? true : Boolean(args.typecheck),
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
ok: result.ok,
|
|
302
|
+
title: "testkit doctor",
|
|
303
|
+
text: renderDoctorResult(result).join("\n"),
|
|
304
|
+
data: {
|
|
305
|
+
testkitRelated: true,
|
|
306
|
+
ok: result.ok,
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function typecheckTool(_args, context) {
|
|
312
|
+
const result = await executeTypecheckOperation({ dir: context.productDir });
|
|
313
|
+
return {
|
|
314
|
+
ok: true,
|
|
315
|
+
title: "testkit typecheck",
|
|
316
|
+
text: renderTypecheckResult(result).join("\n"),
|
|
317
|
+
data: {
|
|
318
|
+
testkitRelated: true,
|
|
319
|
+
count: result.results.length,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
136
324
|
function readContextTool(args, context) {
|
|
137
325
|
if (args.file || args.path) {
|
|
138
326
|
ensureArtifactLoaded(context);
|
|
139
|
-
const artifact = context.
|
|
327
|
+
const artifact = context.runState.getSnapshot().runArtifact;
|
|
140
328
|
const subject = resolveFileSubject(artifact, args.file || args.path, args.service || null);
|
|
141
|
-
context.
|
|
329
|
+
context.runState.revealFile(subject.service.name, subject.file.path);
|
|
142
330
|
} else if (args.service) {
|
|
143
331
|
ensureArtifactLoaded(context);
|
|
144
|
-
if (!context.
|
|
332
|
+
if (!context.runState.revealService(args.service)) {
|
|
145
333
|
throw new Error(`Unknown service "${args.service}"`);
|
|
146
334
|
}
|
|
147
335
|
}
|
|
148
336
|
|
|
149
337
|
const content = readContextContent({
|
|
150
338
|
productDir: context.productDir,
|
|
151
|
-
snapshot: context.
|
|
339
|
+
snapshot: context.runState.getSnapshot(),
|
|
152
340
|
mode: normalizeContextMode(args.mode),
|
|
153
341
|
logTail: args.logTail == null ? 12 : Number(args.logTail),
|
|
154
342
|
});
|
|
@@ -227,11 +415,23 @@ async function searchRepoTool(args, context) {
|
|
|
227
415
|
};
|
|
228
416
|
}
|
|
229
417
|
|
|
230
|
-
function
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
418
|
+
function classifyShellCommand(command) {
|
|
419
|
+
const normalized = command.trim();
|
|
420
|
+
return {
|
|
421
|
+
command: normalized.split(/\s+/)[0] || "command",
|
|
422
|
+
display: normalized,
|
|
423
|
+
title: "Shell command",
|
|
424
|
+
testkitRelated: false,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function isTestkitCommand(command) {
|
|
429
|
+
const normalized = command.trim();
|
|
430
|
+
return /^(testkit)\b/.test(normalized) || /^(npx)\s+testkit\b/.test(normalized) || /^(npm)\s+run\s+testkit\b/.test(normalized) || /^(npm)\s+run\s+testkit:/.test(normalized);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function formatCommandResult(command, result, shellCommand) {
|
|
434
|
+
const lines = [`$ ${command}`];
|
|
235
435
|
const stdout = (result.stdout || "").trim();
|
|
236
436
|
const stderr = (result.stderr || "").trim();
|
|
237
437
|
const merged = [];
|
|
@@ -265,6 +465,92 @@ function resolveRepoPath(productDir, file) {
|
|
|
265
465
|
return path.resolve(productDir, file);
|
|
266
466
|
}
|
|
267
467
|
|
|
468
|
+
function normalizeOptionalString(value) {
|
|
469
|
+
if (typeof value !== "string") return null;
|
|
470
|
+
const normalized = value.trim();
|
|
471
|
+
return normalized.length > 0 ? normalized : null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function normalizeStringArray(value) {
|
|
475
|
+
if (Array.isArray(value)) return value.flatMap((entry) => String(entry).split(",")).map((entry) => entry.trim()).filter(Boolean);
|
|
476
|
+
if (value == null) return [];
|
|
477
|
+
return String(value).split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function normalizeRunToolArgs(args, productDir) {
|
|
481
|
+
return {
|
|
482
|
+
dir: productDir,
|
|
483
|
+
service: normalizeOptionalString(args.service),
|
|
484
|
+
type: normalizeStringArray(args.type),
|
|
485
|
+
suite: normalizeStringArray(args.suite),
|
|
486
|
+
file: normalizeStringArray(args.file || args.path),
|
|
487
|
+
workers: normalizeOptionalString(args.workers),
|
|
488
|
+
"file-timeout-seconds": normalizeOptionalString(args.fileTimeoutSeconds || args["file-timeout-seconds"]),
|
|
489
|
+
shard: normalizeOptionalString(args.shard),
|
|
490
|
+
seed: normalizeOptionalString(args.seed),
|
|
491
|
+
"write-status": Boolean(args.writeStatus || args["write-status"]),
|
|
492
|
+
"allow-partial-status": Boolean(args.allowPartialStatus || args["allow-partial-status"]),
|
|
493
|
+
"ignore-skip-rules": Boolean(args.ignoreSkipRules || args["ignore-skip-rules"]),
|
|
494
|
+
"output-mode": "compact",
|
|
495
|
+
debug: false,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function buildRunDisplay(args) {
|
|
500
|
+
return buildRunCommandLine(args);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function buildRunToolTitle(args) {
|
|
504
|
+
const types = args.type?.length ? args.type.join(",") : "all";
|
|
505
|
+
return `testkit run ${types}`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function formatRunSessionText(runSession) {
|
|
509
|
+
const snapshot = runSession.getSnapshot();
|
|
510
|
+
const fileLines = collectRunTreeFiles(snapshot.services || [])
|
|
511
|
+
.map((entry) => `${formatRunStatus(entry.status)} ${entry.serviceName} ${entry.type} ${entry.filePath || entry.path}`);
|
|
512
|
+
const rows = snapshot.summaryData?.rows || [];
|
|
513
|
+
const summaryLines = rows.map(([label, value]) => `${label}: ${value}`);
|
|
514
|
+
const lines = [...fileLines, ...summaryLines];
|
|
515
|
+
return lines.length > 0 ? lines.join("\n") : "Run complete.";
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function collectRunTreeFiles(services) {
|
|
519
|
+
const files = [];
|
|
520
|
+
for (const service of services || []) {
|
|
521
|
+
for (const typeNode of service.types || []) {
|
|
522
|
+
for (const suite of typeNode.suites || []) {
|
|
523
|
+
for (const file of suite.files || []) {
|
|
524
|
+
files.push(file);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return files;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function formatRunStatus(status) {
|
|
533
|
+
if (status === "passed") return "PASS";
|
|
534
|
+
if (status === "failed") return "FAIL";
|
|
535
|
+
if (status === "skipped") return "SKIP";
|
|
536
|
+
if (status === "running") return "RUN";
|
|
537
|
+
if (status === "not_run") return "NOT_RUN";
|
|
538
|
+
return "PENDING";
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function buildRunCommandLine(args) {
|
|
542
|
+
const parts = ["testkit", "run"];
|
|
543
|
+
if (args.type?.length === 1) parts.push(args.type[0]);
|
|
544
|
+
parts.push("--dir", ".");
|
|
545
|
+
if (args.type?.length !== 1) {
|
|
546
|
+
for (const type of args.type || []) parts.push("--type", type);
|
|
547
|
+
}
|
|
548
|
+
for (const suite of args.suite || []) parts.push("--suite", suite);
|
|
549
|
+
for (const file of args.file || []) parts.push("--file", file);
|
|
550
|
+
if (args.service) parts.push("--service", args.service);
|
|
551
|
+
return parts.join(" ");
|
|
552
|
+
}
|
|
553
|
+
|
|
268
554
|
function shellQuote(value) {
|
|
269
555
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
270
556
|
}
|
|
@@ -275,22 +561,22 @@ function normalizeContextMode(mode) {
|
|
|
275
561
|
}
|
|
276
562
|
|
|
277
563
|
function ensureArtifactLoaded(context) {
|
|
278
|
-
const snapshot = context.
|
|
564
|
+
const snapshot = context.runState.getSnapshot();
|
|
279
565
|
if (snapshot.runArtifact) return snapshot.runArtifact;
|
|
280
566
|
try {
|
|
281
|
-
context.
|
|
567
|
+
context.runState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
|
|
282
568
|
} catch {
|
|
283
|
-
context.
|
|
569
|
+
context.runState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
|
|
284
570
|
}
|
|
285
|
-
return context.
|
|
571
|
+
return context.runState.getSnapshot().runArtifact;
|
|
286
572
|
}
|
|
287
573
|
|
|
288
574
|
function refreshArtifactSelection(context) {
|
|
289
575
|
try {
|
|
290
|
-
context.
|
|
576
|
+
context.runState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
|
|
291
577
|
} catch {
|
|
292
578
|
try {
|
|
293
|
-
context.
|
|
579
|
+
context.runState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
|
|
294
580
|
} catch {
|
|
295
581
|
// Ignore missing artifacts.
|
|
296
582
|
}
|
|
@@ -23,7 +23,7 @@ export function buildAssistantViewModel(snapshot, { cwd = process.cwd(), termina
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function buildWelcomeModel(snapshot, { cwd = process.cwd(), providerLabel = null } = {}) {
|
|
26
|
-
const summaryRows = snapshot?.
|
|
26
|
+
const summaryRows = snapshot?.run?.summaryData?.rows || snapshot?.summaryData?.rows || [];
|
|
27
27
|
const rowValue = (label) => summaryRows.find(([key]) => key === label)?.[1] || null;
|
|
28
28
|
const contextSelection = snapshot?.context?.selection || {};
|
|
29
29
|
const latestResult = rowValue("Result");
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Command, Flags } from "@oclif/core";
|
|
2
|
-
import {
|
|
2
|
+
import { loadManagedConfigs } from "../../app/configs.mjs";
|
|
3
|
+
import { loadLatestRunArtifact, resolveFileSubject } from "../../results/artifacts.mjs";
|
|
4
|
+
import { sharedFlags } from "./flags.mjs";
|
|
3
5
|
import { createAssistantState } from "../assistant/state.mjs";
|
|
4
6
|
import { runInteractiveAssistant } from "../assistant/interactive.mjs";
|
|
5
7
|
|
|
@@ -44,7 +46,7 @@ export default class AssistantCommand extends Command {
|
|
|
44
46
|
if (flags.message && flags.prompt) {
|
|
45
47
|
this.error("Use either --message or --prompt, not both.");
|
|
46
48
|
}
|
|
47
|
-
const { allConfigs } = await
|
|
49
|
+
const { allConfigs } = await loadManagedConfigs({ dir: flags.dir, service: flags.service });
|
|
48
50
|
const productDir = allConfigs[0]?.productDir || process.cwd();
|
|
49
51
|
const interactive =
|
|
50
52
|
(process.stdout.isTTY || process.env.TESTKIT_FORCE_INTERACTIVE_ASSISTANT === "1") &&
|
|
@@ -70,7 +72,6 @@ export default class AssistantCommand extends Command {
|
|
|
70
72
|
await assistantState.loadLatestArtifact();
|
|
71
73
|
if (flags.file) {
|
|
72
74
|
try {
|
|
73
|
-
const { loadLatestRunArtifact, resolveFileSubject } = await import("../viewer.mjs");
|
|
74
75
|
const artifact = loadLatestRunArtifact(productDir);
|
|
75
76
|
const subject = resolveFileSubject(artifact, flags.file, flags.service || null);
|
|
76
77
|
assistantState.revealFile(subject.service.name, subject.file.path);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command, Flags } from "@oclif/core";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { executeBrowserServeOperation } from "../../operations/browser/serve/operation.mjs";
|
|
3
|
+
import { renderBrowserServeResult } from "../../renderers/browser-serve/text.mjs";
|
|
4
4
|
|
|
5
5
|
export default class BrowserServeCommand extends Command {
|
|
6
6
|
static summary = "Serve the local browser bridge for the current testkit product";
|
|
@@ -23,31 +23,13 @@ export default class BrowserServeCommand extends Command {
|
|
|
23
23
|
|
|
24
24
|
async run() {
|
|
25
25
|
const { flags } = await this.parse(BrowserServeCommand);
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
const adapter = {
|
|
29
|
-
loadProductContext: async () => context,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const serverRef = await startBrowserBridgeServer(adapter, {
|
|
33
|
-
host: flags.host,
|
|
34
|
-
port: flags.port,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const payload = {
|
|
38
|
-
ok: true,
|
|
39
|
-
productDir,
|
|
40
|
-
host: serverRef.host,
|
|
41
|
-
port: serverRef.port,
|
|
42
|
-
url: serverRef.url,
|
|
43
|
-
};
|
|
26
|
+
const result = await executeBrowserServeOperation(flags);
|
|
44
27
|
|
|
45
28
|
if (!this.jsonEnabled()) {
|
|
46
|
-
|
|
47
|
-
this.log(`Listening on ${serverRef.url}`);
|
|
29
|
+
for (const line of renderBrowserServeResult(result)) this.log(line);
|
|
48
30
|
}
|
|
49
31
|
|
|
50
32
|
await new Promise(() => {});
|
|
51
|
-
return
|
|
33
|
+
return result;
|
|
52
34
|
}
|
|
53
35
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Command } from "@oclif/core";
|
|
2
|
-
import {
|
|
2
|
+
import { sharedFlags } from "./flags.mjs";
|
|
3
|
+
import { executeCleanupOperation } from "../operations/cleanup/operation.mjs";
|
|
4
|
+
import { renderCleanupResult } from "../renderers/cleanup/text.mjs";
|
|
3
5
|
|
|
4
6
|
export default class CleanupCommand extends Command {
|
|
5
7
|
static summary = "Clean stale local testkit state";
|
|
@@ -10,6 +12,10 @@ export default class CleanupCommand extends Command {
|
|
|
10
12
|
|
|
11
13
|
async run() {
|
|
12
14
|
const { flags } = await this.parse(CleanupCommand);
|
|
13
|
-
|
|
15
|
+
const result = await executeCleanupOperation(flags);
|
|
16
|
+
if (!this.jsonEnabled()) {
|
|
17
|
+
for (const line of renderCleanupResult(result)) this.log(line);
|
|
18
|
+
}
|
|
19
|
+
return { ok: true, ...result };
|
|
14
20
|
}
|
|
15
21
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command, Flags } from "@oclif/core";
|
|
2
|
-
import { sharedFlags } from "
|
|
3
|
-
import {
|
|
2
|
+
import { sharedFlags } from "../../flags.mjs";
|
|
3
|
+
import { executeDatabaseSnapshotCaptureOperation } from "../../../operations/db/snapshot/capture/operation.mjs";
|
|
4
|
+
import { renderDatabaseSnapshotCaptureResult } from "../../../renderers/db-snapshot-capture/text.mjs";
|
|
4
5
|
|
|
5
6
|
export default class DbSnapshotCaptureCommand extends Command {
|
|
6
7
|
static summary = "Capture a database schema snapshot";
|
|
@@ -16,7 +17,10 @@ export default class DbSnapshotCaptureCommand extends Command {
|
|
|
16
17
|
|
|
17
18
|
async run() {
|
|
18
19
|
const { flags } = await this.parse(DbSnapshotCaptureCommand);
|
|
19
|
-
await
|
|
20
|
-
|
|
20
|
+
const result = await executeDatabaseSnapshotCaptureOperation(flags);
|
|
21
|
+
if (!this.jsonEnabled()) {
|
|
22
|
+
for (const line of renderDatabaseSnapshotCaptureResult(result)) this.log(line);
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
21
25
|
}
|
|
22
26
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Command } from "@oclif/core";
|
|
2
|
-
import {
|
|
2
|
+
import { sharedFlags } from "./flags.mjs";
|
|
3
|
+
import { executeDestroyOperation } from "../operations/destroy/operation.mjs";
|
|
4
|
+
import { renderDestroyResult } from "../renderers/destroy/text.mjs";
|
|
3
5
|
|
|
4
6
|
export default class DestroyCommand extends Command {
|
|
5
7
|
static summary = "Destroy local testkit state";
|
|
@@ -10,6 +12,10 @@ export default class DestroyCommand extends Command {
|
|
|
10
12
|
|
|
11
13
|
async run() {
|
|
12
14
|
const { flags } = await this.parse(DestroyCommand);
|
|
13
|
-
|
|
15
|
+
const results = await executeDestroyOperation(flags);
|
|
16
|
+
if (!this.jsonEnabled()) {
|
|
17
|
+
for (const line of renderDestroyResult(results)) this.log(line);
|
|
18
|
+
}
|
|
19
|
+
return { ok: true, results };
|
|
14
20
|
}
|
|
15
21
|
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
1
|
import { Command, Flags } from "@oclif/core";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { resolveRequestedFiles } from "../args.mjs";
|
|
8
|
-
import { sharedFlags } from "../command-helpers.mjs";
|
|
2
|
+
import { executeDiscoverOperation } from "../operations/discover/operation.mjs";
|
|
3
|
+
import { renderDiscoverResult } from "../renderers/discover/text.mjs";
|
|
4
|
+
import { sharedFlags } from "./flags.mjs";
|
|
9
5
|
|
|
10
6
|
export default class DiscoverCommand extends Command {
|
|
11
7
|
static summary = "Discover managed tests and report their metadata";
|
|
@@ -49,30 +45,12 @@ export default class DiscoverCommand extends Command {
|
|
|
49
45
|
|
|
50
46
|
async run() {
|
|
51
47
|
const { flags } = await this.parse(DiscoverCommand);
|
|
52
|
-
const
|
|
53
|
-
const fileNames = resolveRequestedFiles(flags.file || [], productDir, process.cwd());
|
|
54
|
-
const result = await discoverTests({
|
|
55
|
-
dir: productDir,
|
|
56
|
-
service: flags.service || null,
|
|
57
|
-
type: flags.type || [],
|
|
58
|
-
suite: flags.suite || [],
|
|
59
|
-
file: fileNames,
|
|
60
|
-
runnableOnly: flags["runnable-only"],
|
|
61
|
-
diagnostics: flags.strict ? "error" : "report",
|
|
62
|
-
});
|
|
63
|
-
let outputLabel = null;
|
|
64
|
-
if (flags.output) {
|
|
65
|
-
const outputPath = path.resolve(productDir, flags.output);
|
|
66
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
67
|
-
fs.writeFileSync(outputPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
68
|
-
outputLabel = path.relative(productDir, outputPath) || path.basename(outputPath);
|
|
69
|
-
}
|
|
48
|
+
const result = await executeDiscoverOperation(flags, process.cwd());
|
|
70
49
|
|
|
71
50
|
if (!this.jsonEnabled()) {
|
|
72
|
-
for (const line of
|
|
51
|
+
for (const line of renderDiscoverResult(result, { outputMode: flags["output-mode"] })) {
|
|
73
52
|
this.log(line);
|
|
74
53
|
}
|
|
75
|
-
if (outputLabel) this.log(`Wrote ${outputLabel}`);
|
|
76
54
|
}
|
|
77
55
|
|
|
78
56
|
return result;
|