From 48aa3cca30b2c5e7ff0b5faff865918759b048d1 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 5 Feb 2026 10:36:55 -0500 Subject: [PATCH] test(shell): add unit tests for escapeShellArg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 11 tests covering Unix and Windows escaping behavior - Tests for quotes, percent signs, carets, newlines, tabs - Fixes comment inaccuracy (backslash → doubling) --- src/utils/shell-executor.ts | 2 +- tests/unit/utils/shell-executor.test.ts | 79 +++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 tests/unit/utils/shell-executor.test.ts diff --git a/src/utils/shell-executor.ts b/src/utils/shell-executor.ts index 40a104fc..c6bf8359 100644 --- a/src/utils/shell-executor.ts +++ b/src/utils/shell-executor.ts @@ -19,7 +19,7 @@ export function escapeShellArg(arg: string): string { const isWindows = process.platform === 'win32'; if (isWindows) { - // cmd.exe: Use double quotes, escape inner double quotes with backslash + // cmd.exe: Use double quotes, escape inner double quotes by doubling them // cmd.exe interprets "" as escaped double quote inside quoted string // Strip newlines/tabs that can break cmd.exe parsing return ( diff --git a/tests/unit/utils/shell-executor.test.ts b/tests/unit/utils/shell-executor.test.ts new file mode 100644 index 00000000..8dbe930e --- /dev/null +++ b/tests/unit/utils/shell-executor.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; + +// 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"'); + }); + }); +});