@@ -289,4 +289,176 @@ describe("runClaudeCode", () => {
289289 consoleErrorSpy . mockRestore ( )
290290 await generator . return ( undefined )
291291 } )
292+
293+ test ( "should handle ENOENT errors during process spawn with helpful error message" , async ( ) => {
294+ const { runClaudeCode } = await import ( "../run" )
295+
296+ // Mock execa to throw ENOENT error
297+ const enoentError = new Error ( "spawn claude ENOENT" )
298+ ; ( enoentError as any ) . code = "ENOENT"
299+ mockExeca . mockImplementationOnce ( ( ) => {
300+ throw enoentError
301+ } )
302+
303+ const options = {
304+ systemPrompt : "You are a helpful assistant" ,
305+ messages : [ { role : "user" as const , content : "Hello" } ] ,
306+ }
307+
308+ const generator = runClaudeCode ( options )
309+
310+ // Should throw enhanced ENOENT error
311+ await expect ( generator . next ( ) ) . rejects . toThrow ( / C l a u d e C o d e e x e c u t a b l e ' c l a u d e ' n o t f o u n d / )
312+ await expect ( generator . next ( ) ) . rejects . toThrow ( / P l e a s e i n s t a l l C l a u d e C o d e C L I / )
313+ await expect ( generator . next ( ) ) . rejects . toThrow ( / O r i g i n a l e r r o r : s p a w n c l a u d e E N O E N T / )
314+ } )
315+
316+ test ( "should handle ENOENT errors during process execution with helpful error message" , async ( ) => {
317+ const { runClaudeCode } = await import ( "../run" )
318+
319+ // Create a mock process that emits ENOENT error
320+ const mockProcessWithError = createMockProcess ( )
321+ const enoentError = new Error ( "spawn claude ENOENT" )
322+ ; ( enoentError as any ) . code = "ENOENT"
323+
324+ mockProcessWithError . on = vi . fn ( ( event , callback ) => {
325+ if ( event === "error" ) {
326+ // Emit ENOENT error
327+ setTimeout ( ( ) => callback ( enoentError ) , 5 )
328+ } else if ( event === "close" ) {
329+ // Don't emit close event in this test
330+ }
331+ } )
332+
333+ mockExeca . mockReturnValueOnce ( mockProcessWithError )
334+
335+ const options = {
336+ systemPrompt : "You are a helpful assistant" ,
337+ messages : [ { role : "user" as const , content : "Hello" } ] ,
338+ }
339+
340+ const generator = runClaudeCode ( options )
341+
342+ // Should throw enhanced ENOENT error
343+ await expect ( generator . next ( ) ) . rejects . toThrow ( / C l a u d e C o d e e x e c u t a b l e ' c l a u d e ' n o t f o u n d / )
344+ await expect ( generator . next ( ) ) . rejects . toThrow ( / P l e a s e i n s t a l l C l a u d e C o d e C L I / )
345+ await expect ( generator . next ( ) ) . rejects . toThrow ( / O r i g i n a l e r r o r : s p a w n c l a u d e E N O E N T / )
346+ } )
347+
348+ test ( "should handle ENOENT errors with custom claude path" , async ( ) => {
349+ const { runClaudeCode } = await import ( "../run" )
350+
351+ const customPath = "/custom/path/to/claude"
352+ const enoentError = new Error ( `spawn ${ customPath } ENOENT` )
353+ ; ( enoentError as any ) . code = "ENOENT"
354+ mockExeca . mockImplementationOnce ( ( ) => {
355+ throw enoentError
356+ } )
357+
358+ const options = {
359+ systemPrompt : "You are a helpful assistant" ,
360+ messages : [ { role : "user" as const , content : "Hello" } ] ,
361+ path : customPath ,
362+ }
363+
364+ const generator = runClaudeCode ( options )
365+
366+ // Should throw enhanced ENOENT error with custom path
367+ await expect ( generator . next ( ) ) . rejects . toThrow ( `Claude Code executable '${ customPath } ' not found` )
368+ } )
369+
370+ test ( "should preserve non-ENOENT errors during process spawn" , async ( ) => {
371+ const { runClaudeCode } = await import ( "../run" )
372+
373+ // Mock execa to throw non-ENOENT error
374+ const otherError = new Error ( "Permission denied" )
375+ mockExeca . mockImplementationOnce ( ( ) => {
376+ throw otherError
377+ } )
378+
379+ const options = {
380+ systemPrompt : "You are a helpful assistant" ,
381+ messages : [ { role : "user" as const , content : "Hello" } ] ,
382+ }
383+
384+ const generator = runClaudeCode ( options )
385+
386+ // Should throw original error, not enhanced ENOENT error
387+ await expect ( generator . next ( ) ) . rejects . toThrow ( "Permission denied" )
388+ await expect ( generator . next ( ) ) . rejects . not . toThrow ( / C l a u d e C o d e e x e c u t a b l e / )
389+ } )
390+
391+ test ( "should preserve non-ENOENT errors during process execution" , async ( ) => {
392+ const { runClaudeCode } = await import ( "../run" )
393+
394+ // Create a mock process that emits non-ENOENT error
395+ const mockProcessWithError = createMockProcess ( )
396+ const otherError = new Error ( "Permission denied" )
397+
398+ mockProcessWithError . on = vi . fn ( ( event , callback ) => {
399+ if ( event === "error" ) {
400+ // Emit non-ENOENT error
401+ setTimeout ( ( ) => callback ( otherError ) , 5 )
402+ } else if ( event === "close" ) {
403+ // Don't emit close event in this test
404+ }
405+ } )
406+
407+ mockExeca . mockReturnValueOnce ( mockProcessWithError )
408+
409+ const options = {
410+ systemPrompt : "You are a helpful assistant" ,
411+ messages : [ { role : "user" as const , content : "Hello" } ] ,
412+ }
413+
414+ const generator = runClaudeCode ( options )
415+
416+ // Should throw original error, not enhanced ENOENT error
417+ await expect ( generator . next ( ) ) . rejects . toThrow ( "Permission denied" )
418+ await expect ( generator . next ( ) ) . rejects . not . toThrow ( / C l a u d e C o d e e x e c u t a b l e / )
419+ } )
420+
421+ test ( "should prioritize ClaudeCodeNotFoundError over generic exit code errors" , async ( ) => {
422+ const { runClaudeCode } = await import ( "../run" )
423+
424+ // Create a mock process that emits ENOENT error and then exits with non-zero code
425+ const mockProcessWithError = createMockProcess ( )
426+ const enoentError = new Error ( "spawn claude ENOENT" )
427+ ; ( enoentError as any ) . code = "ENOENT"
428+
429+ let resolveProcess : ( value : { exitCode : number } ) => void
430+ const processPromise = new Promise < { exitCode : number } > ( ( resolve ) => {
431+ resolveProcess = resolve
432+ } )
433+
434+ mockProcessWithError . on = vi . fn ( ( event , callback ) => {
435+ if ( event === "error" ) {
436+ // Emit ENOENT error
437+ setTimeout ( ( ) => callback ( enoentError ) , 5 )
438+ } else if ( event === "close" ) {
439+ // Emit non-zero exit code
440+ setTimeout ( ( ) => {
441+ callback ( 1 )
442+ resolveProcess ( { exitCode : 1 } )
443+ } , 10 )
444+ }
445+ } )
446+
447+ mockProcessWithError . then = processPromise . then . bind ( processPromise )
448+ mockProcessWithError . catch = processPromise . catch . bind ( processPromise )
449+ mockProcessWithError . finally = processPromise . finally . bind ( processPromise )
450+
451+ mockExeca . mockReturnValueOnce ( mockProcessWithError )
452+
453+ const options = {
454+ systemPrompt : "You are a helpful assistant" ,
455+ messages : [ { role : "user" as const , content : "Hello" } ] ,
456+ }
457+
458+ const generator = runClaudeCode ( options )
459+
460+ // Should throw ClaudeCodeNotFoundError, not generic exit code error
461+ await expect ( generator . next ( ) ) . rejects . toThrow ( / C l a u d e C o d e e x e c u t a b l e ' c l a u d e ' n o t f o u n d / )
462+ await expect ( generator . next ( ) ) . rejects . not . toThrow ( / p r o c e s s e x i t e d w i t h c o d e / )
463+ } )
292464} )
0 commit comments