@@ -334,6 +334,111 @@ printf '%s\n' '{"type":"system","session_id":"session-cli-1"}'
334334 killSpy . mockRestore ( ) ;
335335 } ) ;
336336
337+ it ( 'lists processes without crashing when a tracked work folder has been deleted' , async ( ) => {
338+ const root = mkdtempSync ( join ( tmpdir ( ) , 'ai-cli-cli-service-' ) ) ;
339+ tempDirs . push ( root ) ;
340+ const stateDir = join ( root , 'state' ) ;
341+ const workFolder = join ( root , 'deleted-project' ) ;
342+ mkdirSync ( workFolder , { recursive : true } ) ;
343+
344+ const pid = 45678 ;
345+ const processDir = join ( stateDir , 'cwds' , encodeCwd ( realpathSync ( workFolder ) ) , String ( pid ) ) ;
346+ mkdirSync ( processDir , { recursive : true } ) ;
347+
348+ writeFileSync (
349+ join ( processDir , 'meta.json' ) ,
350+ JSON . stringify ( {
351+ pid,
352+ prompt : 'deleted cwd' ,
353+ workFolder,
354+ toolType : 'claude' ,
355+ startTime : new Date ( ) . toISOString ( ) ,
356+ stdoutPath : join ( processDir , 'stdout.log' ) ,
357+ stderrPath : join ( processDir , 'stderr.log' ) ,
358+ status : 'running' ,
359+ } )
360+ ) ;
361+
362+ rmSync ( workFolder , { recursive : true , force : true } ) ;
363+
364+ const service = new CliProcessService ( {
365+ stateDir,
366+ cliPaths : {
367+ claude : '/bin/sh' ,
368+ codex : '/bin/sh' ,
369+ gemini : '/bin/sh' ,
370+ forge : '/bin/sh' ,
371+ } ,
372+ } ) ;
373+
374+ const killSpy = vi . spyOn ( globalThis . process , 'kill' ) . mockImplementation ( ( target : number , signal ?: string | number ) => {
375+ if ( signal === 0 && target === pid ) {
376+ throw Object . assign ( new Error ( 'not running' ) , { code : 'ESRCH' } ) ;
377+ }
378+ return true ;
379+ } ) ;
380+
381+ const listed = await service . listProcesses ( ) ;
382+
383+ expect ( listed ) . toEqual ( [
384+ {
385+ pid,
386+ agent : 'claude' ,
387+ status : 'completed' ,
388+ } ,
389+ ] ) ;
390+ expect ( JSON . parse ( readFileSync ( join ( processDir , 'meta.json' ) , 'utf-8' ) ) . status ) . toBe ( 'completed' ) ;
391+ killSpy . mockRestore ( ) ;
392+ } ) ;
393+
394+ it ( 'cleans up finished process directories even when their work folder has been deleted' , async ( ) => {
395+ const root = mkdtempSync ( join ( tmpdir ( ) , 'ai-cli-cli-service-' ) ) ;
396+ tempDirs . push ( root ) ;
397+ const stateDir = join ( root , 'state' ) ;
398+ const workFolder = join ( root , 'deleted-finished-project' ) ;
399+ mkdirSync ( workFolder , { recursive : true } ) ;
400+
401+ const pid = 56789 ;
402+ const cwdDir = join ( stateDir , 'cwds' , encodeCwd ( realpathSync ( workFolder ) ) ) ;
403+ const processDir = join ( cwdDir , String ( pid ) ) ;
404+ mkdirSync ( processDir , { recursive : true } ) ;
405+
406+ writeFileSync (
407+ join ( processDir , 'meta.json' ) ,
408+ JSON . stringify ( {
409+ pid,
410+ prompt : 'done' ,
411+ workFolder,
412+ toolType : 'claude' ,
413+ startTime : new Date ( ) . toISOString ( ) ,
414+ stdoutPath : join ( processDir , 'stdout.log' ) ,
415+ stderrPath : join ( processDir , 'stderr.log' ) ,
416+ status : 'completed' ,
417+ } )
418+ ) ;
419+
420+ rmSync ( workFolder , { recursive : true , force : true } ) ;
421+
422+ const service = new CliProcessService ( {
423+ stateDir,
424+ cliPaths : {
425+ claude : '/bin/sh' ,
426+ codex : '/bin/sh' ,
427+ gemini : '/bin/sh' ,
428+ forge : '/bin/sh' ,
429+ } ,
430+ } ) ;
431+
432+ const result = await service . cleanupProcesses ( ) ;
433+
434+ expect ( result ) . toEqual ( {
435+ removed : 1 ,
436+ message : 'Removed 1 processes' ,
437+ } ) ;
438+ expect ( existsSync ( processDir ) ) . toBe ( false ) ;
439+ expect ( existsSync ( cwdDir ) ) . toBe ( false ) ;
440+ } ) ;
441+
337442 it ( 'cleans up completed and failed process directories but preserves running ones' , async ( ) => {
338443 const root = mkdtempSync ( join ( tmpdir ( ) , 'ai-cli-cli-service-' ) ) ;
339444 tempDirs . push ( root ) ;
0 commit comments