@@ -424,10 +424,12 @@ describe("SshProcessMonitor", () => {
424424 } ) ;
425425
426426 describe ( "cleanup old network files" , ( ) => {
427- const setOldMtime = ( filePath : string ) => {
428- // Default cleanup is 1 hour; set mtime to 2 hours ago to mark as old
429- const TWO_HOURS_AGO = Date . now ( ) - 2 * 60 * 60 * 1000 ;
430- vol . utimesSync ( filePath , TWO_HOURS_AGO / 1000 , TWO_HOURS_AGO / 1000 ) ;
427+ // Network cleanup: 1 hour threshold
428+ const NETWORK_MAX_AGE_MS = 60 * 60 * 1000 ;
429+
430+ const setMtimeAgo = ( filePath : string , ageMs : number ) => {
431+ const mtime = ( Date . now ( ) - ageMs ) / 1000 ;
432+ vol . utimesSync ( filePath , mtime , mtime ) ;
431433 } ;
432434
433435 it ( "deletes old .json files but preserves recent and non-.json files" , async ( ) => {
@@ -438,8 +440,8 @@ describe("SshProcessMonitor", () => {
438440 "/network/recent.json" : "{}" ,
439441 "/network/old.log" : "{}" ,
440442 } ) ;
441- setOldMtime ( "/network/old.json" ) ;
442- setOldMtime ( "/network/old.log" ) ;
443+ setMtimeAgo ( "/network/old.json" , NETWORK_MAX_AGE_MS * 2 ) ;
444+ setMtimeAgo ( "/network/old.log" , NETWORK_MAX_AGE_MS * 2 ) ;
443445
444446 createMonitor ( {
445447 codeLogDir : "/logs/window1" ,
@@ -477,6 +479,122 @@ describe("SshProcessMonitor", () => {
477479 } ) ;
478480 } ) ;
479481
482+ describe ( "cleanup proxy log files" , ( ) => {
483+ // Proxy log cleanup: 7 day threshold, 20 files max
484+ const LOG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000 ;
485+ const LOG_MAX_FILES = 20 ;
486+
487+ const setMtimeAgo = ( filePath : string , ageMs : number ) => {
488+ const mtime = ( Date . now ( ) - ageMs ) / 1000 ;
489+ vol . utimesSync ( filePath , mtime , mtime ) ;
490+ } ;
491+
492+ const logFileName = ( i : number ) =>
493+ `coder-ssh-${ i . toString ( ) . padStart ( 2 , "0" ) } .log` ;
494+
495+ const setupTest = ( total : number , stale : number ) : string [ ] => {
496+ vol . fromJSON ( {
497+ "/logs/ms-vscode-remote.remote-ssh/1-Remote - SSH.log" :
498+ "-> socksPort 12345 ->" ,
499+ } ) ;
500+ vol . mkdirSync ( "/proxy-logs" , { recursive : true } ) ;
501+
502+ const files = Array . from ( { length : total } , ( _ , i ) => logFileName ( i + 1 ) ) ;
503+ for ( const name of files ) {
504+ vol . writeFileSync ( `/proxy-logs/${ name } ` , "" ) ;
505+ }
506+ for ( let i = 0 ; i < stale ; i ++ ) {
507+ setMtimeAgo ( `/proxy-logs/${ files [ i ] } ` , LOG_MAX_AGE_MS * 2 ) ;
508+ }
509+ return files ;
510+ } ;
511+
512+ interface StaleLogTestCase {
513+ total : number ;
514+ stale : number ;
515+ expected : number ;
516+ desc : string ;
517+ }
518+
519+ it . each < StaleLogTestCase > ( [
520+ { total : 25 , stale : 8 , expected : 20 , desc : "Deletes until limit" } ,
521+ { total : 25 , stale : 3 , expected : 22 , desc : "Only deletes stale" } ,
522+ { total : 25 , stale : 0 , expected : 25 , desc : "Keeps recent files" } ,
523+ { total : 15 , stale : 5 , expected : 15 , desc : "Keeps under limit" } ,
524+ ] ) (
525+ "$desc: $total files, $stale stale → $expected remaining" ,
526+ async ( { total, stale, expected } ) => {
527+ setupTest ( total , stale ) ;
528+
529+ createMonitor ( {
530+ codeLogDir : "/logs/window1" ,
531+ proxyLogDir : "/proxy-logs" ,
532+ } ) ;
533+
534+ await vi . waitFor ( ( ) => {
535+ expect ( vol . readdirSync ( "/proxy-logs" ) ) . toHaveLength ( expected ) ;
536+ } ) ;
537+ } ,
538+ ) ;
539+
540+ it ( "only matches coder-ssh*.log files" , async ( ) => {
541+ const files = setupTest ( 25 , 25 ) ;
542+ // Add non-matching files
543+ const nonMatchingFiles = [
544+ "other.log" ,
545+ "coder-ssh-config.json" ,
546+ "readme.txt" ,
547+ ] ;
548+ for ( const f of nonMatchingFiles ) {
549+ const filePath = `/proxy-logs/${ f } ` ;
550+ vol . writeFileSync ( filePath , "" ) ;
551+ setMtimeAgo ( filePath , LOG_MAX_AGE_MS * 2 ) ;
552+ }
553+
554+ createMonitor ( {
555+ codeLogDir : "/logs/window1" ,
556+ proxyLogDir : "/proxy-logs" ,
557+ } ) ;
558+
559+ await vi . waitFor ( ( ) => {
560+ expect ( vol . readdirSync ( "/proxy-logs" ) ) . toHaveLength (
561+ LOG_MAX_FILES + nonMatchingFiles . length ,
562+ ) ;
563+ } ) ;
564+
565+ const remaining = vol . readdirSync ( "/proxy-logs" ) as string [ ] ;
566+ // Non-matching files preserved
567+ expect ( remaining ) . toContain ( "other.log" ) ;
568+ expect ( remaining ) . toContain ( "coder-ssh-config.json" ) ;
569+ expect ( remaining ) . toContain ( "readme.txt" ) ;
570+ // Oldest matching files deleted
571+ expect ( remaining ) . not . toContain ( files [ 0 ] ) ;
572+ expect ( remaining ) . toContain ( files [ 24 ] ) ;
573+ } ) ;
574+
575+ it ( "does not throw when proxy log directory is missing or empty" , ( ) => {
576+ vol . fromJSON ( {
577+ "/logs/ms-vscode-remote.remote-ssh/1-Remote - SSH.log" :
578+ "-> socksPort 12345 ->" ,
579+ } ) ;
580+ vol . mkdirSync ( "/empty-proxy-logs" , { recursive : true } ) ;
581+
582+ expect ( ( ) =>
583+ createMonitor ( {
584+ codeLogDir : "/logs/window1" ,
585+ proxyLogDir : "/nonexistent-proxy-logs" ,
586+ } ) ,
587+ ) . not . toThrow ( ) ;
588+
589+ expect ( ( ) =>
590+ createMonitor ( {
591+ codeLogDir : "/logs/window1" ,
592+ proxyLogDir : "/empty-proxy-logs" ,
593+ } ) ,
594+ ) . not . toThrow ( ) ;
595+ } ) ;
596+ } ) ;
597+
480598 describe ( "missing file retry logic" , ( ) => {
481599 beforeEach ( ( ) => vi . useFakeTimers ( ) ) ;
482600 afterEach ( ( ) => vi . useRealTimers ( ) ) ;
0 commit comments