@@ -332,6 +332,135 @@ describe("bitsocial logs --stdout/--stderr filtering (synthetic)", () => {
332332 } ) ;
333333} ) ;
334334
335+ describe ( "bitsocial logs -f log file rotation (synthetic)" , ( ) => {
336+ it ( "switches to new log file when one appears" , async ( ) => {
337+ const { logDir } = await createLogDir ( ) ;
338+ const file1 = path . join ( logDir , "bitsocial_cli_daemon_2026-03-01T00-00-00.000Z.log" ) ;
339+ await fsPromise . writeFile ( file1 , buildLogLine ( new Date ( "2026-03-01T00:00:00.000Z" ) , "INITIAL_MARKER" ) + "\n" ) ;
340+
341+ const result = await new Promise < { stdout : string ; stderr : string } > ( ( resolve , reject ) => {
342+ const proc = spawn ( "node" , [ "./bin/run" , "logs" , "--logPath" , logDir , "-f" ] , {
343+ stdio : [ "pipe" , "pipe" , "pipe" ]
344+ } ) ;
345+
346+ let stdout = "" ;
347+ let stderr = "" ;
348+ let createdNewFile = false ;
349+ proc . stdout . on ( "data" , ( data : Buffer ) => {
350+ stdout += data . toString ( ) ;
351+ // Wait for initial output before creating the new file
352+ if ( ! createdNewFile && stdout . includes ( "INITIAL_MARKER" ) ) {
353+ createdNewFile = true ;
354+ const file2 = path . join ( logDir , "bitsocial_cli_daemon_2026-03-01T01-00-00.000Z.log" ) ;
355+ fsPromise . writeFile ( file2 , buildLogLine ( new Date ( "2026-03-01T01:00:00.000Z" ) , "NEW_FILE_MARKER" ) + "\n" ) ;
356+ }
357+ } ) ;
358+ proc . stderr . on ( "data" , ( data : Buffer ) => { stderr += data . toString ( ) ; } ) ;
359+
360+ // Wait long enough for the 3-second new-file check to fire, then kill
361+ const timer = setTimeout ( ( ) => {
362+ proc . kill ( "SIGINT" ) ;
363+ } , 8000 ) ;
364+
365+ proc . on ( "close" , ( ) => {
366+ clearTimeout ( timer ) ;
367+ resolve ( { stdout, stderr } ) ;
368+ } ) ;
369+ proc . on ( "error" , ( err ) => {
370+ clearTimeout ( timer ) ;
371+ reject ( err ) ;
372+ } ) ;
373+ } ) ;
374+
375+ expect ( result . stdout ) . toContain ( "INITIAL_MARKER" ) ;
376+ expect ( result . stdout ) . toContain ( "NEW_FILE_MARKER" ) ;
377+ expect ( result . stderr ) . toContain ( "switched to new log file" ) ;
378+ } ) ;
379+
380+ it ( "applies --stdout filter after switching to new log file" , async ( ) => {
381+ const { logDir } = await createLogDir ( ) ;
382+ const file1 = path . join ( logDir , "bitsocial_cli_daemon_2026-04-01T00-00-00.000Z.log" ) ;
383+ await fsPromise . writeFile ( file1 , buildLogLine ( new Date ( "2026-04-01T00:00:00.000Z" ) , "initial stdout" , "stdout" ) + "\n" ) ;
384+
385+ const result = await new Promise < { stdout : string ; stderr : string } > ( ( resolve , reject ) => {
386+ const proc = spawn ( "node" , [ "./bin/run" , "logs" , "--logPath" , logDir , "--stdout" , "-f" ] , {
387+ stdio : [ "pipe" , "pipe" , "pipe" ]
388+ } ) ;
389+
390+ let stdout = "" ;
391+ let stderr = "" ;
392+ let createdNewFile = false ;
393+ proc . stdout . on ( "data" , ( data : Buffer ) => {
394+ stdout += data . toString ( ) ;
395+ if ( ! createdNewFile && stdout . includes ( "initial stdout" ) ) {
396+ createdNewFile = true ;
397+ const file2 = path . join ( logDir , "bitsocial_cli_daemon_2026-04-01T01-00-00.000Z.log" ) ;
398+ const content = [
399+ buildLogLine ( new Date ( "2026-04-01T01:00:00.000Z" ) , "new stdout msg" , "stdout" ) ,
400+ buildLogLine ( new Date ( "2026-04-01T01:01:00.000Z" ) , "new stderr msg" , "stderr" )
401+ ] . join ( "\n" ) + "\n" ;
402+ fsPromise . writeFile ( file2 , content ) ;
403+ }
404+ } ) ;
405+ proc . stderr . on ( "data" , ( data : Buffer ) => { stderr += data . toString ( ) ; } ) ;
406+
407+ const timer = setTimeout ( ( ) => {
408+ proc . kill ( "SIGINT" ) ;
409+ } , 8000 ) ;
410+
411+ proc . on ( "close" , ( ) => {
412+ clearTimeout ( timer ) ;
413+ resolve ( { stdout, stderr } ) ;
414+ } ) ;
415+ proc . on ( "error" , ( err ) => {
416+ clearTimeout ( timer ) ;
417+ reject ( err ) ;
418+ } ) ;
419+ } ) ;
420+
421+ expect ( result . stdout ) . toContain ( "initial stdout" ) ;
422+ expect ( result . stdout ) . toContain ( "new stdout msg" ) ;
423+ expect ( result . stdout ) . not . toContain ( "new stderr msg" ) ;
424+ } ) ;
425+
426+ it ( "continues watching old file if no new file appears" , async ( ) => {
427+ const { logDir } = await createLogDir ( ) ;
428+ const file1 = path . join ( logDir , "bitsocial_cli_daemon_2026-05-01T00-00-00.000Z.log" ) ;
429+ await fsPromise . writeFile ( file1 , buildLogLine ( new Date ( "2026-05-01T00:00:00.000Z" ) , "initial line" ) + "\n" ) ;
430+
431+ const result = await new Promise < { stdout : string } > ( ( resolve , reject ) => {
432+ const proc = spawn ( "node" , [ "./bin/run" , "logs" , "--logPath" , logDir , "-f" ] , {
433+ stdio : [ "pipe" , "pipe" , "pipe" ]
434+ } ) ;
435+
436+ let stdout = "" ;
437+ proc . stdout . on ( "data" , ( data : Buffer ) => { stdout += data . toString ( ) ; } ) ;
438+
439+ // Append to same file after a short delay
440+ setTimeout ( async ( ) => {
441+ await fsPromise . appendFile ( file1 , buildLogLine ( new Date ( "2026-05-01T00:01:00.000Z" ) , "APPENDED_LINE" ) + "\n" ) ;
442+ } , 500 ) ;
443+
444+ // Wait for the appended data to be picked up, then kill
445+ const timer = setTimeout ( ( ) => {
446+ proc . kill ( "SIGINT" ) ;
447+ } , 2000 ) ;
448+
449+ proc . on ( "close" , ( ) => {
450+ clearTimeout ( timer ) ;
451+ resolve ( { stdout } ) ;
452+ } ) ;
453+ proc . on ( "error" , ( err ) => {
454+ clearTimeout ( timer ) ;
455+ reject ( err ) ;
456+ } ) ;
457+ } ) ;
458+
459+ expect ( result . stdout ) . toContain ( "initial line" ) ;
460+ expect ( result . stdout ) . toContain ( "APPENDED_LINE" ) ;
461+ } ) ;
462+ } ) ;
463+
335464describe ( "bitsocial logs (live daemon tests)" , async ( ) => {
336465 let daemonProcess : ManagedChildProcess ;
337466 let logDir : string ;
0 commit comments