diff --git a/src/commands/completion.test.ts b/src/commands/completion.test.ts index c1bbdcf..e33de7e 100644 --- a/src/commands/completion.test.ts +++ b/src/commands/completion.test.ts @@ -10,11 +10,11 @@ const SPEC: CompletionSpec = { }; describe('isShell / detectShell', () => { - it('recognizes the three supported shells', () => { + it('recognizes the four supported shells', () => { expect(isShell('bash')).toBe(true); expect(isShell('zsh')).toBe(true); expect(isShell('fish')).toBe(true); - expect(isShell('powershell')).toBe(false); + expect(isShell('powershell')).toBe(true); }); it('detects the shell from a $SHELL path', () => { @@ -60,6 +60,13 @@ describe('renderCompletion', () => { expect(script).toContain('__fish_seen_subcommand_from auth'); expect(script).toContain('-l output'); }); + + it('powershell script registers an argument completer and lists commands', () => { + const script = renderCompletion('powershell', SPEC); + expect(script).toContain('Register-ArgumentCompleter'); + expect(script).toContain('setup'); + expect(script).toContain('--output'); + }); }); describe('createCompletionCommand', () => { @@ -81,7 +88,7 @@ describe('createCompletionCommand', () => { it('rejects an unsupported shell with VALIDATION_ERROR (exit 5)', async () => { const cmd = createCompletionCommand(() => SPEC, { env: {}, stdout: () => undefined }); - await expect(cmd.parseAsync(['powershell'], { from: 'user' })).rejects.toMatchObject({ + await expect(cmd.parseAsync(['tcsh'], { from: 'user' })).rejects.toMatchObject({ code: 'VALIDATION_ERROR', }); }); diff --git a/src/commands/completion.ts b/src/commands/completion.ts index 42668fa..f74aea4 100644 --- a/src/commands/completion.ts +++ b/src/commands/completion.ts @@ -16,7 +16,7 @@ import { Command } from 'commander'; import { localValidationError } from '../lib/errors.js'; -export const SUPPORTED_SHELLS = ['bash', 'zsh', 'fish'] as const; +export const SUPPORTED_SHELLS = ['bash', 'zsh', 'fish', 'powershell'] as const; export type Shell = (typeof SUPPORTED_SHELLS)[number]; export interface CompletionSpec { @@ -55,6 +55,8 @@ export function renderCompletion(shell: Shell, spec: CompletionSpec): string { return renderZsh(spec); case 'fish': return renderFish(spec); + case 'powershell': + return renderPowerShell(spec); } } @@ -125,15 +127,32 @@ function renderFish(spec: CompletionSpec): string { return lines.join('\n'); } +function renderPowerShell(spec: CompletionSpec): string { + const lines = [ + `# ${spec.program} PowerShell completion. Enable with: testsprite completion powershell | Out-String | Invoke-Expression`, + `Register-ArgumentCompleter -Native -CommandName ${spec.program} -ScriptBlock {`, + ' param($wordToComplete, $commandAst, $cursorPosition)', + ` $commands = @(${spec.commands.map(c => "'" + c + "'").join(', ')})`, + ' if ($wordToComplete -like "-*") {', + ` $flags = @(${spec.globalFlags.map(f => "'" + f + "'").join(', ')})`, + ' $flags | Where-Object { $_ -like "$wordToComplete*" }', + ' return', + ' }', + ' $commands | Where-Object { $_ -like "$wordToComplete*" }', + '}', + ]; + return lines.join('\n'); +} + export function createCompletionCommand( getSpec: () => CompletionSpec, deps: CompletionDeps = {}, ): Command { return new Command('completion') - .description('Print a shell completion script (bash|zsh|fish)') + .description('Print a shell completion script (bash|zsh|fish|powershell)') .argument( '[shell]', - 'Shell to generate for (bash|zsh|fish); auto-detected from $SHELL when omitted', + 'Shell to generate for (bash|zsh|fish|powershell); auto-detected from $SHELL when omitted', ) .addHelpText( 'after', diff --git a/src/commands/test.ts b/src/commands/test.ts index 8bf5ce5..fcb1ca9 100644 --- a/src/commands/test.ts +++ b/src/commands/test.ts @@ -4985,7 +4985,10 @@ async function runBatchRun( return { ...r, dashboardUrl: state.dashboardUrl }; }) : batchRunResults; - out.print({ results: enrichedResults }); + out.print({ + results: enrichedResults, + creation: { results: createResponse.results, summary: createResponse.summary }, + }); } else if (!opts.wait) { // Text mode, no --wait: statuses are non-terminal by design ('queued'), // so a pass/fail summary would misread a successful dispatch as diff --git a/src/lib/interrupt.ts b/src/lib/interrupt.ts index 939a2fd..f291d32 100644 --- a/src/lib/interrupt.ts +++ b/src/lib/interrupt.ts @@ -165,7 +165,7 @@ export class ShutdownController { } class BrokenPipeInterruptError extends InterruptError { - override readonly exitCode = 0; + override readonly exitCode = (process.exitCode as number | undefined) ?? 0; constructor() { // Existing graceful-detach consumers read `.signal` to render their @@ -366,7 +366,7 @@ export function installBrokenPipeGuard(deps: BrokenPipeDeps = {}): void { shutdown.brokenPipe(); return; } - exit(0); + exit((process.exitCode as number | undefined) ?? 0); return; } throw error;