fix(windows): pin cmd shell for wrapper launches (#1229)

This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-05-12 18:13:13 -04:00
1 parent 2e5439290e
commit 9c5d4976c5
8 files changed
+215 -46

No files matched your search

@@ -0,0 +1,66 @@
import { afterEach, describe, expect, it, spyOn } from 'bun:test';
import * as childProcess from 'child_process';
import { EventEmitter } from 'events';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { HealthCheck } from '../../../../src/management/checks/types';
const originalPlatform = process.platform;
let originalClaudePath: string | undefined;
function createMockChild(): EventEmitter & {
stdout: EventEmitter;
stderr: EventEmitter;
} {
const child = new EventEmitter() as EventEmitter & {
stdout: EventEmitter;
stderr: EventEmitter;
};
child.stdout = new EventEmitter();
child.stderr = new EventEmitter();
setTimeout(() => {
child.stdout.emit('data', Buffer.from('1.2.3'));
child.emit('close', 0);
}, 0);
return child;
}
afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform });
if (originalClaudePath === undefined) delete process.env.CCS_CLAUDE_PATH;
else process.env.CCS_CLAUDE_PATH = originalClaudePath;
});
describe('ClaudeCliChecker', () => {
it('uses the pinned Windows shell for cmd wrapper health checks', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-system-check-'));
const fakeClaude = path.join(tmpDir, 'claude.cmd');
fs.writeFileSync(fakeClaude, '');
originalClaudePath = process.env.CCS_CLAUDE_PATH;
process.env.CCS_CLAUDE_PATH = fakeClaude;
Object.defineProperty(process, 'platform', { value: 'win32' });
const spawnSpy = spyOn(childProcess, 'spawn').mockImplementation(
() => createMockChild() as unknown as ReturnType<typeof childProcess.spawn>
);
try {
const { ClaudeCliChecker } = await import('../../../../src/management/checks/system-check');
const results = new HealthCheck();
await new ClaudeCliChecker().run(results);
expect(spawnSpy).toHaveBeenCalledTimes(1);
const [, options] = spawnSpy.mock.calls[0] as [
string,
Record<string, unknown> | undefined,
];
expect(options?.shell).toBe('C:\\Windows\\System32\\cmd.exe');
expect(results.details['Claude CLI']?.status).toBe('OK');
} finally {
spawnSpy.mockRestore();
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
});
@@ -94,7 +94,7 @@ describe('codex-adapter exec', () => {
string,
Record<string, unknown> | undefined,
];
expect(options?.shell).toBe('cmd.exe');
expect(options?.shell).toBe('C:\\Windows\\System32\\cmd.exe');
expect(command).toContain(fakeCodex);
expect(command).toContain('mcp_servers.ccs_browser.args=');
expect(command).toContain('@playwright/mcp@0.0.70');
+31 -23
View File
@@ -81,7 +81,7 @@ describe('codex-detector', () => {
expect(spawnSyncSpy).toHaveBeenCalled();
expect(cmdWrapperProbeCall).toBeDefined();
expect((cmdWrapperProbeCall?.[1] as Record<string, unknown> | undefined)?.shell).toBe(
'cmd.exe'
'C:\\Windows\\System32\\cmd.exe'
);
expect(info?.needsShell).toBe(true);
expect(info?.features).toContain('config-overrides');
@@ -96,7 +96,9 @@ describe('codex-detector', () => {
fs.writeFileSync(fakePsCodex, '');
Object.defineProperty(process, 'platform', { value: 'win32' });
const execSyncSpy = spyOn(childProcess, 'execSync').mockImplementation(() => `${fakeCmdCodex}\n`);
const execSyncSpy = spyOn(childProcess, 'execSync').mockImplementation(
() => `${fakeCmdCodex}\n`
);
expect(detectCodexCli()).toBe(fakeCmdCodex);
@@ -110,7 +112,9 @@ describe('codex-detector', () => {
fs.writeFileSync(fakePsCodex, '');
Object.defineProperty(process, 'platform', { value: 'win32' });
const execSyncSpy = spyOn(childProcess, 'execSync').mockImplementation(() => `${fakePsCodex}\n`);
const execSyncSpy = spyOn(childProcess, 'execSync').mockImplementation(
() => `${fakePsCodex}\n`
);
expect(detectCodexCli()).toBe(fakeCmdCodex);
@@ -122,19 +126,21 @@ describe('codex-detector', () => {
fs.writeFileSync(fakeCodex, '');
process.env.CCS_CODEX_PATH = fakeCodex;
const execFileSyncSpy = spyOn(childProcess, 'execFileSync').mockImplementation((command, args) => {
const joinedArgs = Array.isArray(args) ? args.join(' ') : '';
const execFileSyncSpy = spyOn(childProcess, 'execFileSync').mockImplementation(
(command, args) => {
const joinedArgs = Array.isArray(args) ? args.join(' ') : '';
if (joinedArgs.includes('--help')) {
return 'Codex CLI\n';
}
if (joinedArgs.includes('--help')) {
return 'Codex CLI\n';
}
if (joinedArgs.includes('-c') && joinedArgs.includes('--version')) {
return 'codex-cli 0.119.0-alpha.1';
}
if (joinedArgs.includes('-c') && joinedArgs.includes('--version')) {
return 'codex-cli 0.119.0-alpha.1';
}
return 'codex-cli 0.119.0-alpha.1';
});
);
const info = getCodexBinaryInfo();
@@ -148,19 +154,21 @@ describe('codex-detector', () => {
fs.writeFileSync(fakeCodex, '');
process.env.CCS_CODEX_PATH = fakeCodex;
const execFileSyncSpy = spyOn(childProcess, 'execFileSync').mockImplementation((command, args) => {
const joinedArgs = Array.isArray(args) ? args.join(' ') : '';
const execFileSyncSpy = spyOn(childProcess, 'execFileSync').mockImplementation(
(command, args) => {
const joinedArgs = Array.isArray(args) ? args.join(' ') : '';
if (joinedArgs.includes('--help')) {
return 'Codex CLI\n -c, --config <CONFIG_OVERRIDE>\n';
if (joinedArgs.includes('--help')) {
return 'Codex CLI\n -c, --config <CONFIG_OVERRIDE>\n';
}
if (joinedArgs.includes('-c') && joinedArgs.includes('--version')) {
throw new Error('unsupported');
}
return 'codex-cli 0.119.0-alpha.1';
}
if (joinedArgs.includes('-c') && joinedArgs.includes('--version')) {
throw new Error('unsupported');
}
return 'codex-cli 0.119.0-alpha.1';
});
);
const info = getCodexBinaryInfo();
@@ -405,7 +405,7 @@ describe('CLAUDECODE environment stripping', () => {
expect(spawnCalls.length).toBeGreaterThan(0);
const env = spawnCalls[0].options?.env as NodeJS.ProcessEnv;
expect(Object.keys(env).map((k) => k.toUpperCase())).not.toContain('CLAUDECODE');
expect(spawnCalls[0].options?.shell).toBe('cmd.exe');
expect(spawnCalls[0].options?.shell).toBe('C:\\Windows\\System32\\cmd.exe');
});
it('execClaude sets DISABLE_AUTOUPDATER=1 when preferences.auto_update is false', () => {
@@ -827,9 +827,9 @@ describe('CLAUDECODE environment stripping', () => {
expect(persistedLaunchSettings.env?.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
expect(persistedLaunchSettings.env?.ANTHROPIC_API_KEY).toBeUndefined();
expect(persistedLaunchSettings.env?.ANTHROPIC_MODEL).toBe('gpt-5.4');
expect(
persistedLaunchSettings.hooks?.PreToolUse?.[0]?.hooks?.[0]?.command
).toBe('echo headless-bridge-hook');
expect(persistedLaunchSettings.hooks?.PreToolUse?.[0]?.hooks?.[0]?.command).toBe(
'echo headless-bridge-hook'
);
expect(fs.existsSync(launchSettingsPath)).toBe(false);
});
+73 -11
View File
@@ -82,41 +82,103 @@ describe('escapeShellArg', () => {
expect(escapeShellArg('hello!')).toBe('"hello^^!"');
});
it('prefers ComSpec when resolving the escaped command shell', async () => {
it('accepts ComSpec only when it resolves to the trusted system cmd.exe', async () => {
const originalComSpec = process.env.ComSpec;
const originalCOMSPEC = process.env.COMSPEC;
const originalSystemRoot = process.env.SystemRoot;
try {
process.env.SystemRoot = 'C:\\Windows';
process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe';
delete process.env.COMSPEC;
const { getWindowsEscapedCommandShell } = await import(
'../../../src/utils/shell-executor'
);
const { getWindowsEscapedCommandShell } = await import('../../../src/utils/shell-executor');
expect(getWindowsEscapedCommandShell()).toBe('C:\\Windows\\System32\\cmd.exe');
} finally {
if (originalComSpec === undefined) delete process.env.ComSpec;
else process.env.ComSpec = originalComSpec;
if (originalCOMSPEC === undefined) delete process.env.COMSPEC;
else process.env.COMSPEC = originalCOMSPEC;
if (originalSystemRoot === undefined) delete process.env.SystemRoot;
else process.env.SystemRoot = originalSystemRoot;
}
});
it('falls back to cmd.exe when ComSpec is unavailable', async () => {
it('rejects relative or non-system ComSpec values', async () => {
const originalComSpec = process.env.ComSpec;
const originalCOMSPEC = process.env.COMSPEC;
const originalSystemRoot = process.env.SystemRoot;
try {
delete process.env.ComSpec;
delete process.env.COMSPEC;
const { getWindowsEscapedCommandShell } = await import(
'../../../src/utils/shell-executor'
);
expect(getWindowsEscapedCommandShell()).toBe('cmd.exe');
process.env.SystemRoot = 'C:\\Windows';
process.env.ComSpec = 'cmd.exe';
process.env.COMSPEC = 'C:\\Temp\\cmd.exe';
const { getWindowsEscapedCommandShell } = await import('../../../src/utils/shell-executor');
expect(getWindowsEscapedCommandShell()).toBe('C:\\Windows\\System32\\cmd.exe');
} finally {
if (originalComSpec === undefined) delete process.env.ComSpec;
else process.env.ComSpec = originalComSpec;
if (originalCOMSPEC === undefined) delete process.env.COMSPEC;
else process.env.COMSPEC = originalCOMSPEC;
if (originalSystemRoot === undefined) delete process.env.SystemRoot;
else process.env.SystemRoot = originalSystemRoot;
}
});
it('rejects relative Windows root environment values', async () => {
const originalComSpec = process.env.ComSpec;
const originalCOMSPEC = process.env.COMSPEC;
const originalSystemRoot = process.env.SystemRoot;
const originalSYSTEMROOT = process.env.SYSTEMROOT;
const originalWindir = process.env.windir;
try {
process.env.SystemRoot = 'Windows';
delete process.env.SYSTEMROOT;
delete process.env.windir;
process.env.ComSpec = 'Windows\\System32\\cmd.exe';
process.env.COMSPEC = 'C:\\Temp\\cmd.exe';
const { getWindowsEscapedCommandShell } = await import('../../../src/utils/shell-executor');
expect(getWindowsEscapedCommandShell()).toBe('C:\\Windows\\System32\\cmd.exe');
} finally {
if (originalComSpec === undefined) delete process.env.ComSpec;
else process.env.ComSpec = originalComSpec;
if (originalCOMSPEC === undefined) delete process.env.COMSPEC;
else process.env.COMSPEC = originalCOMSPEC;
if (originalSystemRoot === undefined) delete process.env.SystemRoot;
else process.env.SystemRoot = originalSystemRoot;
if (originalSYSTEMROOT === undefined) delete process.env.SYSTEMROOT;
else process.env.SYSTEMROOT = originalSYSTEMROOT;
if (originalWindir === undefined) delete process.env.windir;
else process.env.windir = originalWindir;
}
});
it('falls back to the absolute default system cmd.exe when Windows root env is unavailable', async () => {
const originalComSpec = process.env.ComSpec;
const originalCOMSPEC = process.env.COMSPEC;
const originalSystemRoot = process.env.SystemRoot;
const originalSYSTEMROOT = process.env.SYSTEMROOT;
const originalWindir = process.env.windir;
try {
delete process.env.ComSpec;
delete process.env.COMSPEC;
delete process.env.SystemRoot;
delete process.env.SYSTEMROOT;
delete process.env.windir;
const { getWindowsEscapedCommandShell } = await import('../../../src/utils/shell-executor');
expect(getWindowsEscapedCommandShell()).toBe('C:\\Windows\\System32\\cmd.exe');
} finally {
if (originalComSpec === undefined) delete process.env.ComSpec;
else process.env.ComSpec = originalComSpec;
if (originalCOMSPEC === undefined) delete process.env.COMSPEC;
else process.env.COMSPEC = originalCOMSPEC;
if (originalSystemRoot === undefined) delete process.env.SystemRoot;
else process.env.SystemRoot = originalSystemRoot;
if (originalSYSTEMROOT === undefined) delete process.env.SYSTEMROOT;
else process.env.SYSTEMROOT = originalSYSTEMROOT;
if (originalWindir === undefined) delete process.env.windir;
else process.env.windir = originalWindir;
}
});
});