import { buildTestAnalysisRequest } from "../application/build-test-analysis-request.ts"; import { completeTestCommandArguments, currentCompletionToken, testCommandTailBeforeCursor, } from "../application/complete-test-command-arguments.ts"; import { parseTestCommandTail } from "../application/parse-test-command-tail.ts"; import { runProjectTests } from "../application/run-project-tests.ts"; import { summarizeTestRunResult } from "../application/summarize-test-run-result.ts"; import type { TestRunExecutionResult } from "../application/execute-test-run-plan.ts"; import { NodeProjectProbe } from "../infrastructure/node-project-probe.ts"; import { PiCommandExecutor, type PiExecApi, } from "../infrastructure/pi-command-executor.ts"; type ProjectTestRunnerNotificationType = "info" | "warning" | "error"; export type ProjectTestRunnerCommandContext = { cwd: string; signal?: AbortSignal; isIdle(): boolean; ui: { notify(message: string, type?: ProjectTestRunnerNotificationType): void; }; }; type ProjectTestRunnerArgumentCompletion = { value: string; label: string; description?: string; }; type ProjectTestRunnerCommandRegistration = { description: string; getArgumentCompletions?( argumentPrefix: string, ): ProjectTestRunnerArgumentCompletion[] | null; handler(args: string, ctx: ProjectTestRunnerCommandContext): Promise; }; type ProjectTestRunnerAutocompleteResult = { prefix: string; items: ProjectTestRunnerArgumentCompletion[]; } | null; type ProjectTestRunnerApplyCompletionResult = { lines: string[]; cursorLine: number; cursorCol: number; }; type ProjectTestRunnerAutocompleteProvider = { getSuggestions( lines: string[], cursorLine: number, cursorCol: number, options?: unknown, ): | ProjectTestRunnerAutocompleteResult | Promise; applyCompletion( lines: string[], cursorLine: number, cursorCol: number, item: ProjectTestRunnerArgumentCompletion, prefix: string, ): ProjectTestRunnerApplyCompletionResult; shouldTriggerFileCompletion?( lines: string[], cursorLine: number, cursorCol: number, ): boolean; }; type ProjectTestRunnerAutocompleteProviderFactory = ( current: ProjectTestRunnerAutocompleteProvider, ) => ProjectTestRunnerAutocompleteProvider; type ProjectTestRunnerSessionContext = { ui: { addAutocompleteProvider( factory: ProjectTestRunnerAutocompleteProviderFactory, ): void; }; }; type ProjectTestRunnerMessage = { customType: string; content: string; display: boolean; details?: unknown; }; export type ProjectTestRunnerPiApi = PiExecApi & { on( event: "session_start", handler: ( event: unknown, ctx: ProjectTestRunnerSessionContext, ) => void | Promise, ): void; registerCommand( name: string, command: ProjectTestRunnerCommandRegistration, ): void; sendMessage(message: ProjectTestRunnerMessage): void; }; export function registerProjectTestRunnerExtension( pi: ProjectTestRunnerPiApi, ): void { registerProjectTestRunnerAutocomplete(pi); pi.registerCommand("test", { description: "Run project tests", getArgumentCompletions: completeTestCommandArguments, async handler(args, ctx) { try { const parsed = parseTestCommandTail(args); const result = await runProjectTests({ cwd: ctx.cwd, argsTail: parsed.runArgsTail, probe: new NodeProjectProbe(), executor: new PiCommandExecutor(pi), signal: ctx.signal, }); const summary = summarizeTestRunResult(result); ctx.ui.notify(summary.message, summary.type); if (parsed.showOutput) { pi.sendMessage({ customType: "project-test-runner-output", display: true, content: formatTestRunOutput(result), details: { displayCommand: result.displayCommand, exitCode: result.exitCode, killed: result.killed, }, }); } if (parsed.showComments) { const request = buildTestAnalysisRequest(result); if (ctx.isIdle()) { pi.sendUserMessage(request); } else { pi.sendUserMessage(request, { deliverAs: "followUp" }); } } } catch (error) { ctx.ui.notify(errorMessage(error), "error"); } }, }); } function registerProjectTestRunnerAutocomplete( pi: ProjectTestRunnerPiApi, ): void { pi.on("session_start", (_event, ctx) => { ctx.ui.addAutocompleteProvider((current) => ({ getSuggestions(lines, cursorLine, cursorCol, options) { const argsTail = testCommandTailBeforeCursor( lines[cursorLine] ?? "", cursorCol, ); if (argsTail === undefined) { return current.getSuggestions(lines, cursorLine, cursorCol, options); } const items = completeTestCommandArguments(argsTail); if (items === null) return null; return { prefix: currentCompletionToken(argsTail), items, }; }, applyCompletion(lines, cursorLine, cursorCol, item, prefix) { const currentLine = lines[cursorLine] ?? ""; // Selecting the /test command itself: complete without the built-in // trailing space so the cursor lands right after the command; the // next Tab then offers the flag options. if ( prefix.startsWith("/") && !prefix.includes(" ") && item.value === "test" ) { const newLines = [...lines]; newLines[cursorLine] = `/${item.value}${currentLine.slice(cursorCol)}`; return { lines: newLines, cursorLine, cursorCol: item.value.length + 1, }; } // Completing a /test flag: replace the token before the cursor, // inserting the command/argument separator when the command has no // trailing space yet (avoids "/test--info"). const argsTail = testCommandTailBeforeCursor(currentLine, cursorCol); if (argsTail !== undefined) { const beforePrefix = currentLine.slice( 0, Math.max(0, cursorCol - prefix.length), ); const afterCursor = currentLine.slice(cursorCol); const separator = beforePrefix === "/test" ? " " : ""; const newLine = `${beforePrefix}${separator}${item.value}${afterCursor}`; const newLines = [...lines]; newLines[cursorLine] = newLine; return { lines: newLines, cursorLine, cursorCol: beforePrefix.length + separator.length + item.value.length, }; } return current.applyCompletion( lines, cursorLine, cursorCol, item, prefix, ); }, shouldTriggerFileCompletion(lines, cursorLine, cursorCol) { return ( current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true ); }, })); }); } function formatTestRunOutput(result: TestRunExecutionResult): string { return [ `Command: ${result.displayCommand}`, `Exit code: ${result.exitCode}`, `Killed: ${result.killed}`, "", "stdout:", result.stdout, "", "stderr:", result.stderr, ].join("\n"); } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); }