11import * as system from './system.js'
2- import { execa , execaCommand } from 'execa'
2+ import { execa } from 'execa'
33import { describe , expect , test , vi } from 'vitest'
44import which from 'which'
55import { Readable } from 'stream'
@@ -206,16 +206,19 @@ describe('captureCommandWithExitCode', () => {
206206describe ( 'execCommand' , ( ) => {
207207 test ( 'runs command successfully without throwing' , async ( ) => {
208208 // Given
209- vi . mocked ( execaCommand ) . mockResolvedValueOnce ( { } as any )
209+ vi . mocked ( which . sync ) . mockReturnValueOnce ( '/system/echo' )
210+ vi . mocked ( execa ) . mockResolvedValueOnce ( { } as any )
210211
211212 // When/Then
212213 await expect ( system . execCommand ( 'echo hello' ) ) . resolves . toBeUndefined ( )
214+ expect ( execa ) . toHaveBeenCalledWith ( 'echo' , [ 'hello' ] , expect . anything ( ) )
213215 } )
214216
215217 test ( 'throws ExternalError on command failure' , async ( ) => {
216218 // Given
217219 const error = new Error ( 'command not found' )
218- vi . mocked ( execaCommand ) . mockRejectedValueOnce ( error )
220+ vi . mocked ( which . sync ) . mockReturnValueOnce ( '/system/nonexistent' )
221+ vi . mocked ( execa ) . mockRejectedValueOnce ( error )
219222
220223 // When/Then
221224 await expect ( system . execCommand ( 'nonexistent' ) ) . rejects . toThrow ( 'command not found' )
@@ -224,7 +227,8 @@ describe('execCommand', () => {
224227 test ( 'calls custom error handler when provided' , async ( ) => {
225228 // Given
226229 const error = new Error ( 'custom error' )
227- vi . mocked ( execaCommand ) . mockRejectedValueOnce ( error )
230+ vi . mocked ( which . sync ) . mockReturnValueOnce ( '/system/failing' )
231+ vi . mocked ( execa ) . mockRejectedValueOnce ( error )
228232 const customHandler = vi . fn ( )
229233
230234 // When
@@ -234,37 +238,42 @@ describe('execCommand', () => {
234238 expect ( customHandler ) . toHaveBeenCalledWith ( error )
235239 } )
236240
237- test ( 'handles command with spaces in arguments' , async ( ) => {
241+ test ( 'handles command with spaces in arguments (quoted strings) ' , async ( ) => {
238242 // Given
239- vi . mocked ( execaCommand ) . mockResolvedValueOnce ( { } as any )
243+ vi . mocked ( which . sync ) . mockReturnValueOnce ( '/system/touch' )
244+ vi . mocked ( execa ) . mockResolvedValueOnce ( { } as any )
240245
241246 // When
242247 await system . execCommand ( 'touch "my file.txt"' )
243248
244249 // Then
245- expect ( execaCommand ) . toHaveBeenCalledWith ( 'touch "my file.txt"' , expect . anything ( ) )
250+ // The quoted argument is parsed into a single argument without quotes,
251+ // and the executable launched matches the executable that was safety-checked.
252+ expect ( execa ) . toHaveBeenCalledWith ( 'touch' , [ 'my file.txt' ] , expect . anything ( ) )
246253 } )
247254
248255 test ( 'uses provided cwd option' , async ( ) => {
249256 // Given
250- vi . mocked ( execaCommand ) . mockResolvedValueOnce ( { } as any )
257+ vi . mocked ( which . sync ) . mockReturnValueOnce ( '/system/pwd' )
258+ vi . mocked ( execa ) . mockResolvedValueOnce ( { } as any )
251259
252260 // When
253261 await system . execCommand ( 'pwd' , { cwd : '/some/dir' } )
254262
255263 // Then
256- expect ( execaCommand ) . toHaveBeenCalledWith ( 'pwd' , expect . objectContaining ( { cwd : '/some/dir' } ) )
264+ expect ( execa ) . toHaveBeenCalledWith ( 'pwd' , [ ] , expect . objectContaining ( { cwd : '/some/dir' } ) )
257265 } )
258266
259- test ( 'passes stdin option to execaCommand ' , async ( ) => {
267+ test ( 'passes stdin option to execa ' , async ( ) => {
260268 // Given
261- vi . mocked ( execaCommand ) . mockResolvedValueOnce ( { } as any )
269+ vi . mocked ( which . sync ) . mockReturnValueOnce ( '/system/cat' )
270+ vi . mocked ( execa ) . mockResolvedValueOnce ( { } as any )
262271
263272 // When
264273 await system . execCommand ( 'cat' , { stdin : 'inherit' } )
265274
266275 // Then
267- expect ( execaCommand ) . toHaveBeenCalledWith ( 'cat' , expect . objectContaining ( { stdin : 'inherit' } ) )
276+ expect ( execa ) . toHaveBeenCalledWith ( 'cat' , [ ] , expect . objectContaining ( { stdin : 'inherit' } ) )
268277 } )
269278
270279 test ( 'raises an error if the command to run is found in the current directory' , async ( ) => {
@@ -277,6 +286,23 @@ describe('execCommand', () => {
277286 // Then
278287 await expect ( got ) . rejects . toThrowError ( 'Skipped run of unsecure binary command found in the current directory.' )
279288 } )
289+
290+ test ( 'safety check and execution agree on the binary (no parser mismatch bypass)' , async ( ) => {
291+ // Given
292+ // Whatever token the safety check approves must be exactly what execa launches.
293+ // Previously, parseCommand() could approve one token while execaCommand() launched
294+ // a different one (e.g. via backslash-escaped spaces), bypassing checkCommandSafety.
295+ vi . mocked ( which . sync ) . mockReturnValueOnce ( '/system/some-binary' )
296+ vi . mocked ( execa ) . mockResolvedValueOnce ( { } as any )
297+
298+ // When
299+ await system . execCommand ( 'some-binary arg1 arg2' )
300+
301+ // Then
302+ const checkedCommand = vi . mocked ( which . sync ) . mock . calls [ 0 ] ?. [ 0 ]
303+ const launchedCommand = vi . mocked ( execa ) . mock . calls [ 0 ] ?. [ 0 ]
304+ expect ( launchedCommand ) . toBe ( checkedCommand )
305+ } )
280306} )
281307
282308describe ( 'isStdinPiped' , ( ) => {
0 commit comments