@@ -500,3 +500,93 @@ test('the active cap admits an exact UTF-8 line and refuses a larger line', asyn
500500 await flushSessionEventLogWrites ( eventLogPath ) ;
501501 assert . equal ( fs . readFileSync ( eventLogPath , 'utf8' ) , line ) ;
502502} ) ;
503+
504+ test ( 'a single serialized line at 16 MiB persists and one byte more is refused' , async ( ) => {
505+ const dir = mkdtempForTestSync ( 'event-single-byte-limit-' ) ;
506+ const eventLogPath = resolveSessionEventLogPath ( dir ) ;
507+ const logPath = path . join ( dir , 'diagnostics.ndjson' ) ;
508+ const max = 16 * 1024 * 1024 ;
509+ const env = { AGENT_DEVICE_EVENT_LOG_MAX_BYTES : String ( max * 2 ) } ;
510+ const makeEvent = ( keySize : number ) => ( {
511+ kind : 'request.started' as const ,
512+ ts : 'fixed' ,
513+ details : { [ 'x' . repeat ( keySize ) ] : 1 } ,
514+ } ) ;
515+ const serialize = ( event : ReturnType < typeof makeEvent > ) =>
516+ `${ JSON . stringify ( Object . assign ( { version : 1 , ts : 'fixed' , session : 'default' } , event ) ) } \n` ;
517+ const overhead = Buffer . byteLength ( serialize ( makeEvent ( 1 ) ) ) - 1 ;
518+ const exact = makeEvent ( max - overhead ) ;
519+ const line = serialize ( exact ) ;
520+ assert . equal ( Buffer . byteLength ( line ) , max ) ;
521+ await withDiagnosticsScope ( { debug : true , logPath } , async ( ) => {
522+ appendSessionEvent ( eventLogPath , 'default' , exact , env ) ;
523+ await flushSessionEventLogWrites ( eventLogPath ) ;
524+ appendSessionEvent ( eventLogPath , 'default' , makeEvent ( max - overhead + 1 ) , env ) ;
525+ await flushSessionEventLogWrites ( eventLogPath ) ;
526+ } ) ;
527+ assert . equal ( fs . readFileSync ( eventLogPath , 'utf8' ) , line ) ;
528+ const diagnostics = fs
529+ . readFileSync ( logPath , 'utf8' )
530+ . trim ( )
531+ . split ( '\n' )
532+ . map ( ( entry ) => JSON . parse ( entry ) ) ;
533+ assert . deepEqual (
534+ diagnostics
535+ . filter ( ( event ) => event . phase === 'session_event_log_write_failed' )
536+ . map ( ( event ) => [ event . data . reason , event . data . operation ] ) ,
537+ [ [ 'entry-too-large' , 'admission' ] ] ,
538+ ) ;
539+ } ) ;
540+
541+ test ( 'an active-file rename failure preserves bytes and a later event recovers the window' , async ( ) => {
542+ const dir = mkdtempForTestSync ( 'event-active-rename-' ) ;
543+ const eventLogPath = resolveSessionEventLogPath ( dir ) ;
544+ const logPath = path . join ( dir , 'diagnostics.ndjson' ) ;
545+ const env = { AGENT_DEVICE_EVENT_LOG_MAX_BYTES : '200' } ;
546+ const send = ( summary : string ) =>
547+ appendSessionEvent (
548+ eventLogPath ,
549+ 'default' ,
550+ {
551+ kind : 'request.started' ,
552+ ts : 'fixed' ,
553+ summary,
554+ } ,
555+ env ,
556+ ) ;
557+ send ( 'first' ) ;
558+ send ( 'second' ) ;
559+ await flushSessionEventLogWrites ( eventLogPath ) ;
560+ const before = fs . readFileSync ( eventLogPath ) ;
561+ const rename = fs . promises . rename . bind ( fs . promises ) ;
562+ const fault = vi . spyOn ( fs . promises , 'rename' ) . mockImplementation ( async ( source , destination ) => {
563+ if ( String ( source ) === eventLogPath )
564+ throw Object . assign ( new Error ( 'denied' ) , { code : 'EACCES' } ) ;
565+ return rename ( source , destination ) ;
566+ } ) ;
567+ await withDiagnosticsScope ( { debug : true , logPath } , async ( ) => {
568+ send ( 'failed' ) ;
569+ await flushSessionEventLogWrites ( eventLogPath ) ;
570+ } ) ;
571+ assert . deepEqual ( fs . readFileSync ( eventLogPath ) , before ) ;
572+ assert . ok ( before . length <= 200 ) ;
573+ assert . equal ( fs . existsSync ( resolveRotatedSessionEventLogPath ( eventLogPath ) ) , false ) ;
574+ const diagnostics = fs
575+ . readFileSync ( logPath , 'utf8' )
576+ . trim ( )
577+ . split ( '\n' )
578+ . map ( ( entry ) => JSON . parse ( entry ) ) ;
579+ assert . deepEqual (
580+ diagnostics
581+ . filter ( ( event ) => event . phase === 'session_event_log_write_failed' )
582+ . map ( ( event ) => [ event . data . reason , event . data . operation , event . data . errno ] ) ,
583+ [ [ 'maintenance-failed' , 'rotation' , 'EACCES' ] ] ,
584+ ) ;
585+ fault . mockRestore ( ) ;
586+ send ( 'recovered' ) ;
587+ await flushSessionEventLogWrites ( eventLogPath ) ;
588+ assert . deepEqual ( fs . readFileSync ( resolveRotatedSessionEventLogPath ( eventLogPath ) ) , before ) ;
589+ assert . ok ( fs . statSync ( eventLogPath ) . size <= 200 ) ;
590+ assert . deepEqual ( summaries ( eventLogPath , '0' , 10 ) , [ 'first' , 'second' , 'recovered' ] ) ;
591+ assert . equal ( readSessionEventLog ( eventLogPath , { cursor : '2' } ) . endCursor , '3' ) ;
592+ } ) ;
0 commit comments