fix(cli): hide windows console windows for runner/claude subprocesses (#242)

This commit is contained in:
Jlovec
2026-03-04 20:47:16 +08:00
committed by GitHub
parent 5ceab49630
commit 55f08bd562
3 changed files with 101 additions and 2 deletions
+91
View File
@@ -0,0 +1,91 @@
import { beforeAll, afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
import type { SpawnOptions } from 'child_process';
const spawnMock = vi.fn((..._args: any[]) => ({ pid: 12345 } as any));
vi.mock('child_process', async () => {
const actual = await vi.importActual<typeof import('child_process')>('child_process');
return {
...actual,
spawn: spawnMock
};
});
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
function setPlatform(value: string) {
Object.defineProperty(process, 'platform', {
value,
configurable: true
});
}
function getSpawnOptionsOrThrow(): SpawnOptions {
expect(spawnMock).toHaveBeenCalledTimes(1);
const firstCall = spawnMock.mock.calls[0] as unknown[] | undefined;
const options = firstCall?.[2] as SpawnOptions | undefined;
if (!options) {
throw new Error('Expected spawn options to be passed as third argument');
}
return options;
}
describe('spawnHappyCLI windowsHide behavior', () => {
beforeAll(() => {
if (!originalPlatformDescriptor?.configurable) {
throw new Error('process.platform is not configurable in this runtime');
}
});
beforeEach(() => {
vi.clearAllMocks();
});
afterAll(() => {
if (originalPlatformDescriptor) {
Object.defineProperty(process, 'platform', originalPlatformDescriptor);
}
});
it('sets windowsHide=true when platform is win32 and detached=true', async () => {
setPlatform('win32');
const { spawnHappyCLI } = await import('./spawnHappyCLI');
spawnHappyCLI(['runner', 'start-sync'], {
detached: true,
stdio: 'ignore'
});
const options = getSpawnOptionsOrThrow();
expect(options.detached).toBe(true);
expect(options.windowsHide).toBe(true);
});
it('does not set windowsHide when platform is win32 but detached is false', async () => {
setPlatform('win32');
const { spawnHappyCLI } = await import('./spawnHappyCLI');
spawnHappyCLI(['runner', 'start-sync'], {
detached: false,
stdio: 'ignore'
});
const options = getSpawnOptionsOrThrow();
expect(options.detached).toBe(false);
expect('windowsHide' in options).toBe(false);
});
it('does not set windowsHide on non-win32 even when detached=true', async () => {
setPlatform('linux');
const { spawnHappyCLI } = await import('./spawnHappyCLI');
spawnHappyCLI(['runner', 'start-sync'], {
detached: true,
stdio: 'ignore'
});
const options = getSpawnOptionsOrThrow();
expect(options.detached).toBe(true);
expect('windowsHide' in options).toBe(false);
});
});
+7 -1
View File
@@ -105,5 +105,11 @@ export function spawnHappyCLI(args: string[], options: SpawnOptions = {}): Child
}
}
return spawn(spawnCommand, spawnArgs, options);
// On Windows, detached processes allocate a new console window by default.
// windowsHide: true suppresses this to prevent cmd windows from accumulating.
const finalOptions: SpawnOptions = { ...options };
if (process.platform === 'win32' && options.detached) {
finalOptions.windowsHide = true;
}
return spawn(spawnCommand, spawnArgs, finalOptions);
}