Files
ccs/tests/unit/utils/shell-executor.test.ts
T
Tam Nhu Tran ccdd0b6e8e fix(windows): align escaped wrapper shell handling
Use ComSpec-aware shell selection for escaped wrapper launches.

Apply it to the remaining Windows Claude launch paths.

Rewrite the Codex exec regression test to use scoped spies so the full suite stays isolated.
2026-04-19 14:27:27 -04:00

191 lines
6.8 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { stripAnthropicEnv } from '../../../src/utils/shell-executor';
// We need to mock process.platform for cross-platform testing
const originalPlatform = process.platform;
describe('escapeShellArg', () => {
describe('Unix (non-Windows)', () => {
beforeEach(() => {
Object.defineProperty(process, 'platform', { value: 'linux' });
});
afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('wraps argument in double quotes', async () => {
const { escapeShellArg } = await import('../../../src/utils/shell-executor');
expect(escapeShellArg('arg')).toBe('"arg"');
});
it('escapes inner double quotes with backslash', async () => {
const { escapeShellArg } = await import('../../../src/utils/shell-executor');
expect(escapeShellArg('say "hello"')).toBe('"say \\"hello\\""');
});
it('handles paths with spaces', async () => {
const { escapeShellArg } = await import('../../../src/utils/shell-executor');
expect(escapeShellArg('/path/to/my file')).toBe('"/path/to/my file"');
});
it('handles empty string', async () => {
const { escapeShellArg } = await import('../../../src/utils/shell-executor');
expect(escapeShellArg('')).toBe('""');
});
});
describe('Windows (cmd.exe)', () => {
beforeEach(() => {
Object.defineProperty(process, 'platform', { value: 'win32' });
});
afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('wraps argument in double quotes', async () => {
const { escapeShellArg } = await import('../../../src/utils/shell-executor');
expect(escapeShellArg('arg')).toBe('"arg"');
});
it('escapes inner double quotes by doubling them', async () => {
const { escapeShellArg } = await import('../../../src/utils/shell-executor');
expect(escapeShellArg('say "hello"')).toBe('"say ""hello"""');
});
it('escapes percent signs', async () => {
const { escapeShellArg } = await import('../../../src/utils/shell-executor');
expect(escapeShellArg('%PATH%')).toBe('"%%PATH%%"');
});
it('escapes caret characters', async () => {
const { escapeShellArg } = await import('../../../src/utils/shell-executor');
expect(escapeShellArg('a^b')).toBe('"a^^b"');
});
it('replaces newlines with spaces', async () => {
const { escapeShellArg } = await import('../../../src/utils/shell-executor');
expect(escapeShellArg('line1\nline2')).toBe('"line1 line2"');
});
it('replaces tabs with spaces', async () => {
const { escapeShellArg } = await import('../../../src/utils/shell-executor');
expect(escapeShellArg('col1\tcol2')).toBe('"col1 col2"');
});
it('handles Windows paths with spaces', async () => {
const { escapeShellArg } = await import('../../../src/utils/shell-executor');
expect(escapeShellArg('C:\\Program Files\\App')).toBe('"C:\\Program Files\\App"');
});
it('escapes exclamation marks for delayed expansion', async () => {
const { escapeShellArg } = await import('../../../src/utils/shell-executor');
expect(escapeShellArg('hello!')).toBe('"hello^^!"');
});
it('prefers ComSpec when resolving the escaped command shell', async () => {
const originalComSpec = process.env.ComSpec;
const originalCOMSPEC = process.env.COMSPEC;
try {
process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe';
delete process.env.COMSPEC;
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;
}
});
it('falls back to cmd.exe when ComSpec is unavailable', async () => {
const originalComSpec = process.env.ComSpec;
const originalCOMSPEC = process.env.COMSPEC;
try {
delete process.env.ComSpec;
delete process.env.COMSPEC;
const { getWindowsEscapedCommandShell } = await import(
'../../../src/utils/shell-executor'
);
expect(getWindowsEscapedCommandShell()).toBe('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;
}
});
});
});
describe('getWindowsEscapedCommandShell', () => {
afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('returns shell=true outside Windows if called defensively', async () => {
Object.defineProperty(process, 'platform', { value: 'linux' });
const { getWindowsEscapedCommandShell } = await import('../../../src/utils/shell-executor');
expect(getWindowsEscapedCommandShell()).toBe(true);
});
});
describe('stripAnthropicEnv', () => {
it('removes all ANTHROPIC_* keys', () => {
const input = {
PATH: '/usr/bin',
ANTHROPIC_BASE_URL: 'http://localhost:3000',
ANTHROPIC_MODEL: 'claude-3',
HOME: '/home/user',
};
const result = stripAnthropicEnv(input);
expect(result).toEqual({ PATH: '/usr/bin', HOME: '/home/user' });
});
it('preserves non-ANTHROPIC keys', () => {
const input = { FOO: 'bar', ANTHROPIC_KEY: 'secret' };
const result = stripAnthropicEnv(input);
expect(result).toEqual({ FOO: 'bar' });
});
it('handles empty object', () => {
expect(stripAnthropicEnv({})).toEqual({});
});
it('preserves undefined values', () => {
const input: NodeJS.ProcessEnv = { FOO: 'bar', BAZ: undefined };
const result = stripAnthropicEnv(input);
expect(result.FOO).toBe('bar');
expect(result.BAZ).toBeUndefined();
expect('BAZ' in result).toBe(true);
});
it('is case-sensitive (only uppercase ANTHROPIC_)', () => {
const input = {
anthropic_base_url: 'lowercase',
Anthropic_Model: 'mixed',
ANTHROPIC_API_KEY: 'uppercase',
};
const result = stripAnthropicEnv(input);
expect(result).toEqual({
anthropic_base_url: 'lowercase',
Anthropic_Model: 'mixed',
});
});
it('strips all ANTHROPIC_ prefixed vars including nested names', () => {
const input = {
ANTHROPIC_: 'empty suffix',
ANTHROPIC_V2_SETTING: 'v2',
ANTHROPIC_INTERNAL_DEBUG: 'internal',
PATH: '/bin',
};
const result = stripAnthropicEnv(input);
expect(result).toEqual({ PATH: '/bin' });
});
});