@elench/testkit 0.1.98 → 0.1.99

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.
@@ -1,584 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { execaCommand } from "execa";
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";
17
-
18
- const COMMAND_OUTPUT_LIMIT = 14_000;
19
- const COMMAND_LINE_LIMIT = 220;
20
- const FILE_LINE_LIMIT = 160;
21
-
22
- export function listAssistantTools() {
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
- },
44
- {
45
- name: "shell_exec",
46
- description: "Execute a shell command inside the repository for arbitrary repo work outside first-class testkit actions.",
47
- },
48
- {
49
- name: "read_context",
50
- description: "Read testkit-managed context such as focused detail, logs, artifacts, setup, or run summary.",
51
- },
52
- {
53
- name: "read_file",
54
- description: "Read a local file with optional start and end lines.",
55
- },
56
- {
57
- name: "search_repo",
58
- description: "Search the repository with ripgrep and return matching lines.",
59
- },
60
- ];
61
- }
62
-
63
- export async function executeAssistantTool(name, argumentsObject, context) {
64
- const args = argumentsObject && typeof argumentsObject === "object" ? argumentsObject : {};
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);
76
- case "shell_exec":
77
- return shellExecTool(args, context);
78
- case "read_context":
79
- return readContextTool(args, context);
80
- case "read_file":
81
- return readFileTool(args, context);
82
- case "search_repo":
83
- return searchRepoTool(args, context);
84
- default:
85
- throw new Error(`Unknown assistant tool "${name}"`);
86
- }
87
- }
88
-
89
- async function shellExecTool(args, context) {
90
- const command = String(args.command || "").trim();
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
- }
95
-
96
- const shellCommand = classifyShellCommand(command);
97
- const commandId = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
98
- context.commandLog?.appendCommandLog({
99
- type: "command_start",
100
- command: shellCommand.command,
101
- commandId,
102
- cwd: context.productDir,
103
- raw: command,
104
- });
105
- context.onEvent?.({
106
- type: "tool-start",
107
- tool: "shell_exec",
108
- command,
109
- title: shellCommand.title,
110
- testkitRelated: shellCommand.testkitRelated,
111
- message: `Running ${shellCommand.display}`,
112
- });
113
-
114
- const result = await execaCommand(command, {
115
- cwd: context.productDir,
116
- reject: false,
117
- shell: true,
118
- env: {
119
- ...process.env,
120
- ...context.env,
121
- PATH: [context.commandLog?.binDir, context.env?.PATH, process.env.PATH].filter(Boolean).join(path.delimiter),
122
- TESTKIT_NO_ASSISTANT_DEFAULT: "1",
123
- },
124
- });
125
-
126
- context.commandLog?.appendCommandLog({
127
- type: "command_exit",
128
- command: shellCommand.command,
129
- commandId,
130
- cwd: context.productDir,
131
- raw: command,
132
- code: result.exitCode ?? 0,
133
- signal: result.signal ?? null,
134
- });
135
- context.onEvent?.({
136
- type: "tool-exit",
137
- tool: "shell_exec",
138
- command,
139
- title: shellCommand.title,
140
- testkitRelated: shellCommand.testkitRelated,
141
- code: result.exitCode ?? 0,
142
- signal: result.signal ?? null,
143
- message: `${shellCommand.display} exited ${result.exitCode ?? 0}`,
144
- });
145
-
146
- if (shellCommand.testkitRelated) {
147
- refreshArtifactSelection(context);
148
- }
149
- context.commandLog?.refresh?.();
150
-
151
- const lines = formatCommandResult(command, result, shellCommand);
152
- return {
153
- ok: (result.exitCode ?? 0) === 0,
154
- title: shellCommand.title,
155
- text: lines.join("\n"),
156
- data: {
157
- command,
158
- stdout: result.stdout || "",
159
- stderr: result.stderr || "",
160
- exitCode: result.exitCode ?? 0,
161
- signal: result.signal ?? null,
162
- testkitRelated: shellCommand.testkitRelated,
163
- },
164
- };
165
- }
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
-
324
- function readContextTool(args, context) {
325
- if (args.file || args.path) {
326
- ensureArtifactLoaded(context);
327
- const artifact = context.runState.getSnapshot().runArtifact;
328
- const subject = resolveFileSubject(artifact, args.file || args.path, args.service || null);
329
- context.runState.revealFile(subject.service.name, subject.file.path);
330
- } else if (args.service) {
331
- ensureArtifactLoaded(context);
332
- if (!context.runState.revealService(args.service)) {
333
- throw new Error(`Unknown service "${args.service}"`);
334
- }
335
- }
336
-
337
- const content = readContextContent({
338
- productDir: context.productDir,
339
- snapshot: context.runState.getSnapshot(),
340
- mode: normalizeContextMode(args.mode),
341
- logTail: args.logTail == null ? 12 : Number(args.logTail),
342
- });
343
- context.commandLog?.refresh?.();
344
- return {
345
- ok: true,
346
- title: content.title,
347
- text: content.lines.join("\n"),
348
- data: {
349
- title: content.title,
350
- lines: content.lines,
351
- selection: content.selection,
352
- mode: content.mode,
353
- },
354
- };
355
- }
356
-
357
- function readFileTool(args, context) {
358
- const file = String(args.path || args.file || "").trim();
359
- if (!file) throw new Error("read_file requires a path");
360
- const resolved = resolveRepoPath(context.productDir, file);
361
- if (!resolved.startsWith(path.resolve(context.productDir))) {
362
- throw new Error("read_file only supports paths inside the current repository");
363
- }
364
- if (!fs.existsSync(resolved)) {
365
- throw new Error(`File not found: ${file}`);
366
- }
367
- const startLine = Math.max(1, Number(args.startLine || args.start || 1) || 1);
368
- const requestedEnd = Number(args.endLine || args.end || startLine + FILE_LINE_LIMIT - 1) || startLine + FILE_LINE_LIMIT - 1;
369
- const endLine = Math.max(startLine, Math.min(requestedEnd, startLine + FILE_LINE_LIMIT - 1));
370
- const lines = fs.readFileSync(resolved, "utf8").split(/\r?\n/);
371
- const selected = [];
372
- for (let lineNumber = startLine; lineNumber <= Math.min(endLine, lines.length); lineNumber += 1) {
373
- selected.push(`${lineNumber}: ${lines[lineNumber - 1]}`);
374
- }
375
- const title = `File ${path.relative(context.productDir, resolved) || path.basename(resolved)}`;
376
- return {
377
- ok: true,
378
- title,
379
- text: selected.join("\n"),
380
- data: {
381
- path: resolved,
382
- relativePath: path.relative(context.productDir, resolved),
383
- startLine,
384
- endLine,
385
- lines: selected,
386
- },
387
- };
388
- }
389
-
390
- async function searchRepoTool(args, context) {
391
- const query = String(args.query || args.pattern || "").trim();
392
- if (!query) throw new Error("search_repo requires a query");
393
- const result = await execaCommand(
394
- `rg --line-number --smart-case --hidden --glob '!node_modules' --glob '!.git' ${shellQuote(query)} .`,
395
- {
396
- cwd: context.productDir,
397
- reject: false,
398
- shell: true,
399
- }
400
- );
401
- const combined = truncateLines((result.stdout || "").split(/\r?\n/).filter(Boolean), COMMAND_LINE_LIMIT);
402
- const lines =
403
- combined.length > 0
404
- ? combined
405
- : [`No matches for ${query}`];
406
- return {
407
- ok: (result.exitCode ?? 1) === 0,
408
- title: `Search ${query}`,
409
- text: lines.join("\n"),
410
- data: {
411
- query,
412
- matches: combined,
413
- exitCode: result.exitCode ?? 1,
414
- },
415
- };
416
- }
417
-
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}`];
435
- const stdout = (result.stdout || "").trim();
436
- const stderr = (result.stderr || "").trim();
437
- const merged = [];
438
- if (stdout) merged.push(...stdout.split(/\r?\n/));
439
- if (stderr) merged.push(...stderr.split(/\r?\n/).map((line) => `stderr: ${line}`));
440
- if (merged.length === 0) {
441
- merged.push(`exit ${result.exitCode ?? 0}`);
442
- }
443
- const trimmed = truncateLines(merged, COMMAND_LINE_LIMIT).map((line) => truncateText(line, COMMAND_OUTPUT_LIMIT));
444
- lines.push(...trimmed);
445
- if ((result.exitCode ?? 0) !== 0) {
446
- lines.push(`exit code: ${result.exitCode ?? 0}`);
447
- } else if (!shellCommand.testkitRelated) {
448
- lines.push("exit code: 0");
449
- }
450
- return lines;
451
- }
452
-
453
- function truncateLines(lines, limit) {
454
- if (lines.length <= limit) return lines;
455
- return [...lines.slice(0, limit - 1), `… ${lines.length - limit + 1} more lines omitted`];
456
- }
457
-
458
- function truncateText(value, maxLength) {
459
- const normalized = String(value || "");
460
- if (normalized.length <= maxLength) return normalized;
461
- return `${normalized.slice(0, maxLength - 1)}…`;
462
- }
463
-
464
- function resolveRepoPath(productDir, file) {
465
- return path.resolve(productDir, file);
466
- }
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
-
554
- function shellQuote(value) {
555
- return `'${String(value).replace(/'/g, `'\\''`)}'`;
556
- }
557
-
558
- function normalizeContextMode(mode) {
559
- if (mode === "logs" || mode === "artifacts" || mode === "setup") return mode;
560
- return "detail";
561
- }
562
-
563
- function ensureArtifactLoaded(context) {
564
- const snapshot = context.runState.getSnapshot();
565
- if (snapshot.runArtifact) return snapshot.runArtifact;
566
- try {
567
- context.runState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
568
- } catch {
569
- context.runState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
570
- }
571
- return context.runState.getSnapshot().runArtifact;
572
- }
573
-
574
- function refreshArtifactSelection(context) {
575
- try {
576
- context.runState.hydrateFromArtifact(loadCurrentRunArtifact(context.productDir));
577
- } catch {
578
- try {
579
- context.runState.hydrateFromArtifact(loadLatestRunArtifact(context.productDir));
580
- } catch {
581
- // Ignore missing artifacts.
582
- }
583
- }
584
- }