2025-07-25 12:25:32 -07:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-10-16 17:25:30 -07:00
import {
expect ,
describe ,
it ,
beforeEach ,
beforeAll ,
vi ,
afterEach ,
} from 'vitest' ;
2025-07-25 12:25:32 -07:00
import {
2025-08-17 00:02:54 -04:00
escapeShellArg ,
2025-07-25 12:25:32 -07:00
getCommandRoots ,
2025-08-17 00:02:54 -04:00
getShellConfiguration ,
2025-10-16 17:25:30 -07:00
initializeShellParsers ,
2026-01-19 20:07:28 -08:00
parseCommandDetails ,
2025-07-25 12:25:32 -07:00
stripShellWrapper ,
2026-01-02 11:36:59 -08:00
hasRedirection ,
2026-01-16 09:55:29 -08:00
resolveExecutable ,
2025-07-25 12:25:32 -07:00
} from './shell-utils.js' ;
2026-01-16 09:55:29 -08:00
import path from 'node:path' ;
2025-07-25 12:25:32 -07:00
2025-08-17 00:02:54 -04:00
const mockPlatform = vi . hoisted ( ( ) = > vi . fn ( ) ) ;
2025-08-27 02:17:43 +10:00
const mockHomedir = vi . hoisted ( ( ) = > vi . fn ( ) ) ;
2025-08-17 00:02:54 -04:00
vi . mock ( 'os' , ( ) = > ( {
default : {
platform : mockPlatform ,
2025-08-27 02:17:43 +10:00
homedir : mockHomedir ,
2025-08-17 00:02:54 -04:00
} ,
platform : mockPlatform ,
2025-08-27 02:17:43 +10:00
homedir : mockHomedir ,
2025-08-17 00:02:54 -04:00
} ) ) ;
2026-01-16 09:55:29 -08:00
const mockAccess = vi . hoisted ( ( ) = > vi . fn ( ) ) ;
vi . mock ( 'node:fs' , ( ) = > ( {
default : {
promises : {
access : mockAccess ,
} ,
constants : { X_OK : 1 } ,
} ,
promises : {
access : mockAccess ,
} ,
constants : { X_OK : 1 } ,
} ) ) ;
2026-01-02 11:36:59 -08:00
const mockSpawnSync = vi . hoisted ( ( ) = > vi . fn ( ) ) ;
vi . mock ( 'node:child_process' , ( ) = > ( {
spawnSync : mockSpawnSync ,
spawn : vi.fn ( ) ,
} ) ) ;
2025-08-17 00:02:54 -04:00
const mockQuote = vi . hoisted ( ( ) = > vi . fn ( ) ) ;
vi . mock ( 'shell-quote' , ( ) = > ( {
quote : mockQuote ,
} ) ) ;
2026-01-14 16:48:02 -08:00
const mockDebugLogger = vi . hoisted ( ( ) = > ( {
error : vi.fn ( ) ,
debug : vi.fn ( ) ,
log : vi.fn ( ) ,
warn : vi.fn ( ) ,
} ) ) ;
vi . mock ( './debugLogger.js' , ( ) = > ( {
debugLogger : mockDebugLogger ,
} ) ) ;
2025-10-16 17:25:30 -07:00
const isWindowsRuntime = process . platform === 'win32' ;
const describeWindowsOnly = isWindowsRuntime ? describe : describe.skip ;
beforeAll ( async ( ) = > {
mockPlatform . mockReturnValue ( 'linux' ) ;
await initializeShellParsers ( ) ;
} ) ;
2025-07-25 12:25:32 -07:00
2025-07-27 02:00:26 -04:00
beforeEach ( ( ) = > {
2025-08-17 00:02:54 -04:00
mockPlatform . mockReturnValue ( 'linux' ) ;
mockQuote . mockImplementation ( ( args : string [ ] ) = >
args . map ( ( arg ) = > ` ' ${ arg } ' ` ) . join ( ' ' ) ,
) ;
2026-01-02 11:36:59 -08:00
mockSpawnSync . mockReturnValue ( {
stdout : Buffer.from ( '' ) ,
stderr : Buffer.from ( '' ) ,
status : 0 ,
error : undefined ,
} ) ;
2025-07-27 02:00:26 -04:00
} ) ;
2025-07-25 12:25:32 -07:00
2025-08-17 00:02:54 -04:00
afterEach ( ( ) = > {
vi . clearAllMocks ( ) ;
} ) ;
2026-01-02 15:20:26 -08:00
const mockPowerShellResult = (
commands : Array < { name : string ; text : string } > ,
hasRedirection : boolean ,
) = > {
mockSpawnSync . mockReturnValue ( {
stdout : Buffer.from (
JSON . stringify ( {
success : true ,
commands ,
hasRedirection ,
} ) ,
) ,
stderr : Buffer.from ( '' ) ,
status : 0 ,
error : undefined ,
} ) ;
} ;
2025-07-25 12:25:32 -07:00
describe ( 'getCommandRoots' , ( ) = > {
it ( 'should return a single command' , ( ) = > {
2025-07-27 02:00:26 -04:00
expect ( getCommandRoots ( 'ls -l' ) ) . toEqual ( [ 'ls' ] ) ;
2025-07-25 12:25:32 -07:00
} ) ;
2025-07-27 02:00:26 -04:00
it ( 'should handle paths and return the binary name' , ( ) = > {
expect ( getCommandRoots ( '/usr/local/bin/node script.js' ) ) . toEqual ( [ 'node' ] ) ;
2025-07-25 12:25:32 -07:00
} ) ;
2025-07-27 02:00:26 -04:00
it ( 'should return an empty array for an empty string' , ( ) = > {
expect ( getCommandRoots ( '' ) ) . toEqual ( [ ] ) ;
2025-07-25 12:25:32 -07:00
} ) ;
it ( 'should handle a mix of operators' , ( ) = > {
2025-07-27 02:00:26 -04:00
const result = getCommandRoots ( 'a;b|c&&d||e&f' ) ;
expect ( result ) . toEqual ( [ 'a' , 'b' , 'c' , 'd' , 'e' , 'f' ] ) ;
2025-07-25 12:25:32 -07:00
} ) ;
2025-07-27 02:00:26 -04:00
it ( 'should correctly parse a chained command with quotes' , ( ) = > {
const result = getCommandRoots ( 'echo "hello" && git commit -m "feat"' ) ;
expect ( result ) . toEqual ( [ 'echo' , 'git' ] ) ;
2025-07-25 12:25:32 -07:00
} ) ;
2025-10-16 17:25:30 -07:00
it ( 'should include nested command substitutions' , ( ) = > {
const result = getCommandRoots ( 'echo $(badCommand --danger)' ) ;
expect ( result ) . toEqual ( [ 'echo' , 'badCommand' ] ) ;
} ) ;
it ( 'should include process substitutions' , ( ) = > {
const result = getCommandRoots ( 'diff <(ls) <(ls -a)' ) ;
expect ( result ) . toEqual ( [ 'diff' , 'ls' , 'ls' ] ) ;
} ) ;
it ( 'should include backtick substitutions' , ( ) = > {
const result = getCommandRoots ( 'echo `badCommand --danger`' ) ;
expect ( result ) . toEqual ( [ 'echo' , 'badCommand' ] ) ;
} ) ;
2025-11-04 11:11:29 -05:00
it ( 'should treat parameter expansions with prompt transformations as unsafe' , ( ) = > {
const roots = getCommandRoots (
'echo "${var1=aa\\140 env| ls -l\\140}${var1@P}"' ,
) ;
expect ( roots ) . toEqual ( [ ] ) ;
} ) ;
it ( 'should not return roots for prompt transformation expansions' , ( ) = > {
const roots = getCommandRoots ( 'echo ${foo@P}' ) ;
expect ( roots ) . toEqual ( [ ] ) ;
} ) ;
2026-01-02 11:36:59 -08:00
it ( 'should include nested command substitutions in redirected statements' , ( ) = > {
const result = getCommandRoots ( 'echo $(cat secret) > output.txt' ) ;
expect ( result ) . toEqual ( [ 'echo' , 'cat' ] ) ;
} ) ;
2026-01-19 20:07:28 -08:00
it ( 'should correctly identify input redirection with explicit file descriptor' , ( ) = > {
const result = parseCommandDetails ( 'ls 2< input.txt' ) ;
const redirection = result ? . details . find ( ( d ) = >
d . name . startsWith ( 'redirection' ) ,
) ;
expect ( redirection ? . name ) . toBe ( 'redirection (<)' ) ;
} ) ;
it ( 'should filter out all redirections from getCommandRoots' , ( ) = > {
expect ( getCommandRoots ( 'cat < input.txt' ) ) . toEqual ( [ 'cat' ] ) ;
expect ( getCommandRoots ( 'ls 2> error.log' ) ) . toEqual ( [ 'ls' ] ) ;
expect ( getCommandRoots ( 'exec 3<&0' ) ) . toEqual ( [ 'exec' ] ) ;
} ) ;
2026-01-02 11:36:59 -08:00
it ( 'should handle parser initialization failures gracefully' , async ( ) = > {
// Reset modules to clear singleton state
vi . resetModules ( ) ;
// Mock fileUtils to fail Wasm loading
vi . doMock ( './fileUtils.js' , ( ) = > ( {
loadWasmBinary : vi.fn ( ) . mockRejectedValue ( new Error ( 'Wasm load failed' ) ) ,
} ) ) ;
// Re-import shell-utils with mocked dependencies
const shellUtils = await import ( './shell-utils.js' ) ;
// Should catch the error and not throw
await expect ( shellUtils . initializeShellParsers ( ) ) . resolves . not . toThrow ( ) ;
// Fallback: splitting commands depends on parser, so if parser fails, it returns empty
const roots = shellUtils . getCommandRoots ( 'ls -la' ) ;
expect ( roots ) . toEqual ( [ ] ) ;
} ) ;
2026-01-14 16:48:02 -08:00
it ( 'should handle bash parser timeouts' , ( ) = > {
const nowSpy = vi . spyOn ( performance , 'now' ) ;
// Mock performance.now() to trigger timeout:
// 1st call: start time = 0. deadline = 0 + 1000ms.
// 2nd call (and onwards): inside progressCallback, return 2000ms.
nowSpy . mockReturnValueOnce ( 0 ) . mockReturnValue ( 2000 ) ;
// Use a very complex command to ensure progressCallback is triggered at least once
const complexCommand =
'ls -la && ' + Array ( 100 ) . fill ( 'echo "hello"' ) . join ( ' && ' ) ;
const roots = getCommandRoots ( complexCommand ) ;
expect ( roots ) . toEqual ( [ ] ) ;
expect ( nowSpy ) . toHaveBeenCalled ( ) ;
expect ( mockDebugLogger . error ) . toHaveBeenCalledWith (
'Bash command parsing timed out for command:' ,
complexCommand ,
) ;
nowSpy . mockRestore ( ) ;
} ) ;
2026-01-02 11:36:59 -08:00
} ) ;
describe ( 'hasRedirection' , ( ) = > {
it ( 'should detect output redirection' , ( ) = > {
expect ( hasRedirection ( 'echo hello > world' ) ) . toBe ( true ) ;
} ) ;
it ( 'should detect input redirection' , ( ) = > {
expect ( hasRedirection ( 'cat < input' ) ) . toBe ( true ) ;
} ) ;
2026-01-19 20:07:28 -08:00
it ( 'should detect redirection with explicit file descriptor' , ( ) = > {
expect ( hasRedirection ( 'ls 2> error.log' ) ) . toBe ( true ) ;
expect ( hasRedirection ( 'exec 3<&0' ) ) . toBe ( true ) ;
} ) ;
2026-01-02 11:36:59 -08:00
it ( 'should detect append redirection' , ( ) = > {
expect ( hasRedirection ( 'echo hello >> world' ) ) . toBe ( true ) ;
} ) ;
it ( 'should detect heredoc' , ( ) = > {
expect ( hasRedirection ( 'cat <<EOF\nhello\nEOF' ) ) . toBe ( true ) ;
} ) ;
it ( 'should detect herestring' , ( ) = > {
expect ( hasRedirection ( 'cat <<< "hello"' ) ) . toBe ( true ) ;
} ) ;
it ( 'should return false for simple commands' , ( ) = > {
expect ( hasRedirection ( 'ls -la' ) ) . toBe ( false ) ;
} ) ;
it ( 'should return false for pipes (pipes are not redirections in this context)' , ( ) = > {
// Note: pipes are often handled separately by splitCommands, but checking here confirms they don't trigger "redirection" flag if we don't want them to.
// However, the current implementation checks for 'redirected_statement' nodes.
// A pipe is a 'pipeline' node.
expect ( hasRedirection ( 'echo hello | cat' ) ) . toBe ( false ) ;
} ) ;
2026-01-19 20:07:28 -08:00
it ( 'should return false when redirection characters are inside quotes in bash' , ( ) = > {
mockPlatform . mockReturnValue ( 'linux' ) ;
expect ( hasRedirection ( 'echo "a > b"' ) ) . toBe ( false ) ;
} ) ;
2025-10-16 17:25:30 -07:00
} ) ;
describeWindowsOnly ( 'PowerShell integration' , ( ) = > {
const originalComSpec = process . env [ 'ComSpec' ] ;
beforeEach ( ( ) = > {
mockPlatform . mockReturnValue ( 'win32' ) ;
const systemRoot = process . env [ 'SystemRoot' ] || 'C:\\\\Windows' ;
process . env [ 'ComSpec' ] =
` ${ systemRoot } \\ \\ System32 \\ \\ WindowsPowerShell \\ \\ v1.0 \\ \\ powershell.exe ` ;
} ) ;
afterEach ( ( ) = > {
if ( originalComSpec === undefined ) {
delete process . env [ 'ComSpec' ] ;
} else {
process . env [ 'ComSpec' ] = originalComSpec ;
}
} ) ;
it ( 'should return command roots using PowerShell AST output' , ( ) = > {
2026-01-02 15:20:26 -08:00
mockPowerShellResult (
[
{ name : 'Get-ChildItem' , text : 'Get-ChildItem' } ,
{ name : 'Select-Object' , text : 'Select-Object Name' } ,
] ,
false ,
) ;
2025-10-16 17:25:30 -07:00
const roots = getCommandRoots ( 'Get-ChildItem | Select-Object Name' ) ;
expect ( roots . length ) . toBeGreaterThan ( 0 ) ;
expect ( roots ) . toContain ( 'Get-ChildItem' ) ;
} ) ;
2025-07-25 12:25:32 -07:00
} ) ;
describe ( 'stripShellWrapper' , ( ) = > {
2025-07-27 02:00:26 -04:00
it ( 'should strip sh -c with quotes' , ( ) = > {
expect ( stripShellWrapper ( 'sh -c "ls -l"' ) ) . toEqual ( 'ls -l' ) ;
2025-07-25 12:25:32 -07:00
} ) ;
2025-07-27 02:00:26 -04:00
it ( 'should strip bash -c with extra whitespace' , ( ) = > {
expect ( stripShellWrapper ( ' bash -c "ls -l" ' ) ) . toEqual ( 'ls -l' ) ;
2025-07-25 12:25:32 -07:00
} ) ;
2025-07-27 02:00:26 -04:00
it ( 'should strip zsh -c without quotes' , ( ) = > {
expect ( stripShellWrapper ( 'zsh -c ls -l' ) ) . toEqual ( 'ls -l' ) ;
2025-07-25 12:25:32 -07:00
} ) ;
2025-07-27 02:00:26 -04:00
it ( 'should strip cmd.exe /c' , ( ) = > {
expect ( stripShellWrapper ( 'cmd.exe /c "dir"' ) ) . toEqual ( 'dir' ) ;
2025-07-25 12:25:32 -07:00
} ) ;
2025-10-16 17:25:30 -07:00
it ( 'should strip powershell.exe -Command with optional -NoProfile' , ( ) = > {
expect (
stripShellWrapper ( 'powershell.exe -NoProfile -Command "Get-ChildItem"' ) ,
) . toEqual ( 'Get-ChildItem' ) ;
expect (
stripShellWrapper ( 'powershell.exe -Command "Get-ChildItem"' ) ,
) . toEqual ( 'Get-ChildItem' ) ;
} ) ;
it ( 'should strip pwsh -Command wrapper' , ( ) = > {
expect (
stripShellWrapper ( 'pwsh -NoProfile -Command "Get-ChildItem"' ) ,
) . toEqual ( 'Get-ChildItem' ) ;
} ) ;
2025-07-27 02:00:26 -04:00
it ( 'should not strip anything if no wrapper is present' , ( ) = > {
expect ( stripShellWrapper ( 'ls -l' ) ) . toEqual ( 'ls -l' ) ;
2025-07-25 12:25:32 -07:00
} ) ;
} ) ;
2025-08-17 00:02:54 -04:00
describe ( 'escapeShellArg' , ( ) = > {
describe ( 'POSIX (bash)' , ( ) = > {
it ( 'should use shell-quote for escaping' , ( ) = > {
mockQuote . mockReturnValueOnce ( "'escaped value'" ) ;
const result = escapeShellArg ( 'raw value' , 'bash' ) ;
expect ( mockQuote ) . toHaveBeenCalledWith ( [ 'raw value' ] ) ;
expect ( result ) . toBe ( "'escaped value'" ) ;
} ) ;
it ( 'should handle empty strings' , ( ) = > {
const result = escapeShellArg ( '' , 'bash' ) ;
expect ( result ) . toBe ( '' ) ;
expect ( mockQuote ) . not . toHaveBeenCalled ( ) ;
} ) ;
} ) ;
describe ( 'Windows' , ( ) = > {
describe ( 'when shell is cmd.exe' , ( ) = > {
it ( 'should wrap simple arguments in double quotes' , ( ) = > {
const result = escapeShellArg ( 'search term' , 'cmd' ) ;
expect ( result ) . toBe ( '"search term"' ) ;
} ) ;
it ( 'should escape internal double quotes by doubling them' , ( ) = > {
const result = escapeShellArg ( 'He said "Hello"' , 'cmd' ) ;
expect ( result ) . toBe ( '"He said ""Hello"""' ) ;
} ) ;
it ( 'should handle empty strings' , ( ) = > {
const result = escapeShellArg ( '' , 'cmd' ) ;
expect ( result ) . toBe ( '' ) ;
} ) ;
} ) ;
describe ( 'when shell is PowerShell' , ( ) = > {
it ( 'should wrap simple arguments in single quotes' , ( ) = > {
const result = escapeShellArg ( 'search term' , 'powershell' ) ;
expect ( result ) . toBe ( "'search term'" ) ;
} ) ;
it ( 'should escape internal single quotes by doubling them' , ( ) = > {
const result = escapeShellArg ( "It's a test" , 'powershell' ) ;
expect ( result ) . toBe ( "'It''s a test'" ) ;
} ) ;
it ( 'should handle double quotes without escaping them' , ( ) = > {
const result = escapeShellArg ( 'He said "Hello"' , 'powershell' ) ;
expect ( result ) . toBe ( '\'He said "Hello"\'' ) ;
} ) ;
it ( 'should handle empty strings' , ( ) = > {
const result = escapeShellArg ( '' , 'powershell' ) ;
expect ( result ) . toBe ( '' ) ;
} ) ;
} ) ;
} ) ;
} ) ;
describe ( 'getShellConfiguration' , ( ) = > {
const originalEnv = { . . . process . env } ;
afterEach ( ( ) = > {
process . env = originalEnv ;
} ) ;
it ( 'should return bash configuration on Linux' , ( ) = > {
mockPlatform . mockReturnValue ( 'linux' ) ;
const config = getShellConfiguration ( ) ;
expect ( config . executable ) . toBe ( 'bash' ) ;
expect ( config . argsPrefix ) . toEqual ( [ '-c' ] ) ;
expect ( config . shell ) . toBe ( 'bash' ) ;
} ) ;
it ( 'should return bash configuration on macOS (darwin)' , ( ) = > {
mockPlatform . mockReturnValue ( 'darwin' ) ;
const config = getShellConfiguration ( ) ;
expect ( config . executable ) . toBe ( 'bash' ) ;
expect ( config . argsPrefix ) . toEqual ( [ '-c' ] ) ;
expect ( config . shell ) . toBe ( 'bash' ) ;
} ) ;
describe ( 'on Windows' , ( ) = > {
beforeEach ( ( ) = > {
mockPlatform . mockReturnValue ( 'win32' ) ;
} ) ;
2025-10-16 17:25:30 -07:00
it ( 'should return PowerShell configuration by default' , ( ) = > {
2025-08-17 12:43:21 -04:00
delete process . env [ 'ComSpec' ] ;
2025-08-17 00:02:54 -04:00
const config = getShellConfiguration ( ) ;
2025-10-16 17:25:30 -07:00
expect ( config . executable ) . toBe ( 'powershell.exe' ) ;
expect ( config . argsPrefix ) . toEqual ( [ '-NoProfile' , '-Command' ] ) ;
expect ( config . shell ) . toBe ( 'powershell' ) ;
2025-08-17 00:02:54 -04:00
} ) ;
2025-10-16 17:25:30 -07:00
it ( 'should ignore ComSpec when pointing to cmd.exe' , ( ) = > {
2025-08-17 00:02:54 -04:00
const cmdPath = 'C:\\WINDOWS\\system32\\cmd.exe' ;
2025-08-17 12:43:21 -04:00
process . env [ 'ComSpec' ] = cmdPath ;
2025-08-17 00:02:54 -04:00
const config = getShellConfiguration ( ) ;
2025-10-16 17:25:30 -07:00
expect ( config . executable ) . toBe ( 'powershell.exe' ) ;
expect ( config . argsPrefix ) . toEqual ( [ '-NoProfile' , '-Command' ] ) ;
expect ( config . shell ) . toBe ( 'powershell' ) ;
2025-08-17 00:02:54 -04:00
} ) ;
it ( 'should return PowerShell configuration if ComSpec points to powershell.exe' , ( ) = > {
const psPath =
'C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' ;
2025-08-17 12:43:21 -04:00
process . env [ 'ComSpec' ] = psPath ;
2025-08-17 00:02:54 -04:00
const config = getShellConfiguration ( ) ;
expect ( config . executable ) . toBe ( psPath ) ;
expect ( config . argsPrefix ) . toEqual ( [ '-NoProfile' , '-Command' ] ) ;
expect ( config . shell ) . toBe ( 'powershell' ) ;
} ) ;
it ( 'should return PowerShell configuration if ComSpec points to pwsh.exe' , ( ) = > {
const pwshPath = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' ;
2025-08-17 12:43:21 -04:00
process . env [ 'ComSpec' ] = pwshPath ;
2025-08-17 00:02:54 -04:00
const config = getShellConfiguration ( ) ;
expect ( config . executable ) . toBe ( pwshPath ) ;
expect ( config . argsPrefix ) . toEqual ( [ '-NoProfile' , '-Command' ] ) ;
expect ( config . shell ) . toBe ( 'powershell' ) ;
} ) ;
it ( 'should be case-insensitive when checking ComSpec' , ( ) = > {
2025-08-17 12:43:21 -04:00
process . env [ 'ComSpec' ] = 'C:\\Path\\To\\POWERSHELL.EXE' ;
2025-08-17 00:02:54 -04:00
const config = getShellConfiguration ( ) ;
expect ( config . executable ) . toBe ( 'C:\\Path\\To\\POWERSHELL.EXE' ) ;
expect ( config . argsPrefix ) . toEqual ( [ '-NoProfile' , '-Command' ] ) ;
expect ( config . shell ) . toBe ( 'powershell' ) ;
} ) ;
} ) ;
} ) ;
2026-01-02 11:36:59 -08:00
describe ( 'hasRedirection (PowerShell via mock)' , ( ) = > {
beforeEach ( ( ) = > {
mockPlatform . mockReturnValue ( 'win32' ) ;
process . env [ 'ComSpec' ] = 'powershell.exe' ;
} ) ;
it ( 'should return true when PowerShell parser detects redirection' , ( ) = > {
mockPowerShellResult ( [ { name : 'echo' , text : 'echo hello' } ] , true ) ;
expect ( hasRedirection ( 'echo hello > file.txt' ) ) . toBe ( true ) ;
} ) ;
it ( 'should return false when PowerShell parser does not detect redirection' , ( ) = > {
mockPowerShellResult ( [ { name : 'echo' , text : 'echo hello' } ] , false ) ;
expect ( hasRedirection ( 'echo hello' ) ) . toBe ( false ) ;
} ) ;
it ( 'should return false when quoted redirection chars are used but not actual redirection' , ( ) = > {
mockPowerShellResult (
[ { name : 'echo' , text : 'echo "-> arrow"' } ] ,
false , // Parser says NO redirection
) ;
expect ( hasRedirection ( 'echo "-> arrow"' ) ) . toBe ( false ) ;
} ) ;
it ( 'should fallback to regex if parsing fails (simulating safety)' , ( ) = > {
mockSpawnSync . mockReturnValue ( {
stdout : Buffer.from ( 'invalid json' ) ,
status : 0 ,
} ) ;
// Fallback regex sees '>' in arrow
expect ( hasRedirection ( 'echo "-> arrow"' ) ) . toBe ( true ) ;
} ) ;
} ) ;
2026-01-16 09:55:29 -08:00
describe ( 'resolveExecutable' , ( ) = > {
const originalEnv = process . env ;
beforeEach ( ( ) = > {
process . env = { . . . originalEnv } ;
mockAccess . mockReset ( ) ;
} ) ;
afterEach ( ( ) = > {
process . env = originalEnv ;
} ) ;
it ( 'should return the absolute path if it exists and is executable' , async ( ) = > {
const absPath = path . resolve ( '/usr/bin/git' ) ;
mockAccess . mockResolvedValue ( undefined ) ; // success
expect ( await resolveExecutable ( absPath ) ) . toBe ( absPath ) ;
expect ( mockAccess ) . toHaveBeenCalledWith ( absPath , 1 ) ;
} ) ;
it ( 'should return undefined for absolute path if it does not exist' , async ( ) = > {
const absPath = path . resolve ( '/usr/bin/nonexistent' ) ;
mockAccess . mockRejectedValue ( new Error ( 'ENOENT' ) ) ;
expect ( await resolveExecutable ( absPath ) ) . toBeUndefined ( ) ;
} ) ;
it ( 'should resolve executable in PATH' , async ( ) = > {
const binDir = path . resolve ( '/bin' ) ;
const usrBinDir = path . resolve ( '/usr/bin' ) ;
process . env [ 'PATH' ] = ` ${ binDir } ${ path . delimiter } ${ usrBinDir } ` ;
mockPlatform . mockReturnValue ( 'linux' ) ;
const targetPath = path . join ( usrBinDir , 'ls' ) ;
mockAccess . mockImplementation ( async ( p : string ) = > {
if ( p === targetPath ) return undefined ;
throw new Error ( 'ENOENT' ) ;
} ) ;
expect ( await resolveExecutable ( 'ls' ) ) . toBe ( targetPath ) ;
} ) ;
it ( 'should try extensions on Windows' , async ( ) = > {
const sys32 = path . resolve ( 'C:\\Windows\\System32' ) ;
process . env [ 'PATH' ] = sys32 ;
mockPlatform . mockReturnValue ( 'win32' ) ;
mockAccess . mockImplementation ( async ( p : string ) = > {
// Use includes because on Windows path separators might differ
if ( p . includes ( 'cmd.exe' ) ) return undefined ;
throw new Error ( 'ENOENT' ) ;
} ) ;
expect ( await resolveExecutable ( 'cmd' ) ) . toContain ( 'cmd.exe' ) ;
} ) ;
it ( 'should return undefined if not found in PATH' , async ( ) = > {
process . env [ 'PATH' ] = path . resolve ( '/bin' ) ;
mockPlatform . mockReturnValue ( 'linux' ) ;
mockAccess . mockRejectedValue ( new Error ( 'ENOENT' ) ) ;
expect ( await resolveExecutable ( 'unknown' ) ) . toBeUndefined ( ) ;
} ) ;
} ) ;