@elench/testkit 0.1.97 → 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 +25 -1
- package/lib/cli/assistant/composer.mjs +1 -1
- package/lib/cli/assistant/context-pack.mjs +4 -4
- package/lib/cli/assistant/interactive.mjs +1 -1
- package/lib/cli/assistant/prompt-builder.mjs +2 -2
- package/lib/cli/{agents → assistant/providers}/index.mjs +3 -3
- package/lib/cli/assistant/session.mjs +5 -5
- package/lib/cli/assistant/slash-commands.mjs +22 -1
- package/lib/cli/assistant/state.mjs +148 -75
- package/lib/cli/assistant/tool-registry.mjs +305 -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/{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/state-io.mjs +10 -4
- 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 +6 -7
- 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/{agents → assistant}/providers/claude.mjs +0 -0
- /package/lib/cli/{agents → assistant}/providers/codex.mjs +0 -0
- /package/lib/cli/{agents → assistant}/providers/shared.mjs +0 -0
- /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,10 +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 "../
|
|
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";
|
|
8
17
|
|
|
9
18
|
const COMMAND_OUTPUT_LIMIT = 14_000;
|
|
10
19
|
const COMMAND_LINE_LIMIT = 220;
|
|
@@ -12,9 +21,29 @@ const FILE_LINE_LIMIT = 160;
|
|
|
12
21
|
|
|
13
22
|
export function listAssistantTools() {
|
|
14
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
|
+
},
|
|
15
44
|
{
|
|
16
45
|
name: "shell_exec",
|
|
17
|
-
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.",
|
|
18
47
|
},
|
|
19
48
|
{
|
|
20
49
|
name: "read_context",
|
|
@@ -34,6 +63,16 @@ export function listAssistantTools() {
|
|
|
34
63
|
export async function executeAssistantTool(name, argumentsObject, context) {
|
|
35
64
|
const args = argumentsObject && typeof argumentsObject === "object" ? argumentsObject : {};
|
|
36
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);
|
|
37
76
|
case "shell_exec":
|
|
38
77
|
return shellExecTool(args, context);
|
|
39
78
|
case "read_context":
|
|
@@ -50,6 +89,9 @@ export async function executeAssistantTool(name, argumentsObject, context) {
|
|
|
50
89
|
async function shellExecTool(args, context) {
|
|
51
90
|
const command = String(args.command || "").trim();
|
|
52
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
|
+
}
|
|
53
95
|
|
|
54
96
|
const shellCommand = classifyShellCommand(command);
|
|
55
97
|
const commandId = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
@@ -122,22 +164,179 @@ async function shellExecTool(args, context) {
|
|
|
122
164
|
};
|
|
123
165
|
}
|
|
124
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
|
+
|
|
125
324
|
function readContextTool(args, context) {
|
|
126
325
|
if (args.file || args.path) {
|
|
127
326
|
ensureArtifactLoaded(context);
|
|
128
|
-
const artifact = context.
|
|
327
|
+
const artifact = context.runState.getSnapshot().runArtifact;
|
|
129
328
|
const subject = resolveFileSubject(artifact, args.file || args.path, args.service || null);
|
|
130
|
-
context.
|
|
329
|
+
context.runState.revealFile(subject.service.name, subject.file.path);
|
|
131
330
|
} else if (args.service) {
|
|
132
331
|
ensureArtifactLoaded(context);
|
|
133
|
-
if (!context.
|
|
332
|
+
if (!context.runState.revealService(args.service)) {
|
|
134
333
|
throw new Error(`Unknown service "${args.service}"`);
|
|
135
334
|
}
|
|
136
335
|
}
|
|
137
336
|
|
|
138
337
|
const content = readContextContent({
|
|
139
338
|
productDir: context.productDir,
|
|
140
|
-
snapshot: context.
|
|
339
|
+
snapshot: context.runState.getSnapshot(),
|
|
141
340
|
mode: normalizeContextMode(args.mode),
|
|
142
341
|
logTail: args.logTail == null ? 12 : Number(args.logTail),
|
|
143
342
|
});
|
|
@@ -218,30 +417,6 @@ async function searchRepoTool(args, context) {
|
|
|
218
417
|
|
|
219
418
|
function classifyShellCommand(command) {
|
|
220
419
|
const normalized = command.trim();
|
|
221
|
-
if (/^(testkit)\b/.test(normalized)) {
|
|
222
|
-
return {
|
|
223
|
-
command: "testkit",
|
|
224
|
-
display: normalized,
|
|
225
|
-
title: "testkit command",
|
|
226
|
-
testkitRelated: true,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
if (/^(npx)\s+testkit\b/.test(normalized)) {
|
|
230
|
-
return {
|
|
231
|
-
command: "npx testkit",
|
|
232
|
-
display: normalized,
|
|
233
|
-
title: "npx testkit",
|
|
234
|
-
testkitRelated: true,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
if (/^(npm)\s+run\s+testkit\b/.test(normalized) || /^(npm)\s+run\s+testkit:/.test(normalized)) {
|
|
238
|
-
return {
|
|
239
|
-
command: "npm run testkit",
|
|
240
|
-
display: normalized,
|
|
241
|
-
title: "npm testkit script",
|
|
242
|
-
testkitRelated: true,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
420
|
return {
|
|
246
421
|
command: normalized.split(/\s+/)[0] || "command",
|
|
247
422
|
display: normalized,
|
|
@@ -250,6 +425,11 @@ function classifyShellCommand(command) {
|
|
|
250
425
|
};
|
|
251
426
|
}
|
|
252
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
|
+
|
|
253
433
|
function formatCommandResult(command, result, shellCommand) {
|
|
254
434
|
const lines = [`$ ${command}`];
|
|
255
435
|
const stdout = (result.stdout || "").trim();
|
|
@@ -285,6 +465,92 @@ function resolveRepoPath(productDir, file) {
|
|
|
285
465
|
return path.resolve(productDir, file);
|
|
286
466
|
}
|
|
287
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
|
+
|
|
288
554
|
function shellQuote(value) {
|
|
289
555
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
290
556
|
}
|
|
@@ -295,22 +561,22 @@ function normalizeContextMode(mode) {
|
|
|
295
561
|
}
|
|
296
562
|
|
|
297
563
|
function ensureArtifactLoaded(context) {
|
|
298
|
-
const snapshot = context.
|
|
564
|
+
const snapshot = context.runState.getSnapshot();
|
|
299
565
|
if (snapshot.runArtifact) return snapshot.runArtifact;
|
|
300
566
|
try {
|
|
301
|
-
context.
|
|
567
|
+
context.runState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
|
|
302
568
|
} catch {
|
|
303
|
-
context.
|
|
569
|
+
context.runState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
|
|
304
570
|
}
|
|
305
|
-
return context.
|
|
571
|
+
return context.runState.getSnapshot().runArtifact;
|
|
306
572
|
}
|
|
307
573
|
|
|
308
574
|
function refreshArtifactSelection(context) {
|
|
309
575
|
try {
|
|
310
|
-
context.
|
|
576
|
+
context.runState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
|
|
311
577
|
} catch {
|
|
312
578
|
try {
|
|
313
|
-
context.
|
|
579
|
+
context.runState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
|
|
314
580
|
} catch {
|
|
315
581
|
// Ignore missing artifacts.
|
|
316
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;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command, Flags } from "@oclif/core";
|
|
2
|
-
import {
|
|
2
|
+
import { executeDoctorOperation } from "../operations/doctor/operation.mjs";
|
|
3
|
+
import { renderDoctorResult } from "../renderers/doctor/text.mjs";
|
|
3
4
|
|
|
4
5
|
export default class DoctorCommand extends Command {
|
|
5
6
|
static summary = "Run built-in config, discovery, and hygiene checks";
|
|
@@ -19,12 +20,11 @@ export default class DoctorCommand extends Command {
|
|
|
19
20
|
|
|
20
21
|
async run() {
|
|
21
22
|
const { flags } = await this.parse(DoctorCommand);
|
|
22
|
-
const result = await
|
|
23
|
+
const result = await executeDoctorOperation(flags);
|
|
23
24
|
|
|
24
25
|
if (!this.jsonEnabled()) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
this.log(`${check.level.toUpperCase()} ${check.code} ${check.message}`);
|
|
26
|
+
for (const line of renderDoctorResult(result)) {
|
|
27
|
+
this.log(line);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|