@@ -562,3 +562,193 @@ describe('JSONConsumer flush and end', () => {
562562 consumer . end ( ) ;
563563 } ) ;
564564} ) ;
565+
566+ describe ( 'LogConsumer attach/detach' , ( ) => {
567+ it ( 'should be idempotent when attach() is called twice' , ( ) => {
568+ const stream = new TestStream ( ) ;
569+ const consumer = new JSONConsumer ( { stream, level : 'info' } ) ;
570+ const logger = new Logger ( { level : 'info' } ) ;
571+
572+ consumer . attach ( ) ;
573+ consumer . attach ( ) ;
574+ try {
575+ logger . info ( 'once' ) ;
576+ consumer . flushSync ( ) ;
577+ assert . strictEqual ( stream . logs . length , 1 ) ;
578+ } finally {
579+ consumer . detach ( ) ;
580+ }
581+ } ) ;
582+
583+ it ( 'should fully detach after a double-attach' , ( ) => {
584+ const stream = new TestStream ( ) ;
585+ const consumer = new JSONConsumer ( { stream, level : 'info' } ) ;
586+ const logger = new Logger ( { level : 'info' } ) ;
587+
588+ consumer . attach ( ) ;
589+ consumer . attach ( ) ;
590+ consumer . detach ( ) ;
591+
592+ logger . info ( 'should not be captured' ) ;
593+ consumer . flushSync ( ) ;
594+ assert . strictEqual ( stream . logs . length , 0 ) ;
595+ } ) ;
596+
597+ it ( 'should no-op on detach() without prior attach()' , ( ) => {
598+ const stream = new TestStream ( ) ;
599+ const consumer = new JSONConsumer ( { stream, level : 'info' } ) ;
600+ consumer . detach ( ) ;
601+ } ) ;
602+ } ) ;
603+
604+ describe ( 'JSONConsumer undefined handling' , ( ) => {
605+ it ( 'should skip undefined consumer fields to keep JSON valid' , ( ) => {
606+ const stream = new TestStream ( ) ;
607+ const consumer = new JSONConsumer ( {
608+ stream,
609+ level : 'info' ,
610+ fields : { service : 'api' , region : undefined } ,
611+ } ) ;
612+ consumer . attach ( ) ;
613+ const logger = new Logger ( { level : 'info' } ) ;
614+ try {
615+ logger . info ( 'ok' ) ;
616+ consumer . flushSync ( ) ;
617+ assert . strictEqual ( stream . logs . length , 1 ) ;
618+ assert . strictEqual ( stream . logs [ 0 ] . service , 'api' ) ;
619+ assert . ok ( ! ( 'region' in stream . logs [ 0 ] ) ) ;
620+ } finally {
621+ consumer . detach ( ) ;
622+ }
623+ } ) ;
624+
625+ it ( 'should skip undefined bindings to keep JSON valid' , ( ) => {
626+ const stream = new TestStream ( ) ;
627+ const consumer = new JSONConsumer ( { stream, level : 'info' } ) ;
628+ consumer . attach ( ) ;
629+ const logger = new Logger ( {
630+ level : 'info' ,
631+ bindings : { requestId : 'abc' , traceId : undefined } ,
632+ } ) ;
633+ try {
634+ logger . info ( 'ok' ) ;
635+ consumer . flushSync ( ) ;
636+ assert . strictEqual ( stream . logs . length , 1 ) ;
637+ assert . strictEqual ( stream . logs [ 0 ] . requestId , 'abc' ) ;
638+ assert . ok ( ! ( 'traceId' in stream . logs [ 0 ] ) ) ;
639+ } finally {
640+ consumer . detach ( ) ;
641+ }
642+ } ) ;
643+
644+ it ( 'should skip undefined log fields to keep JSON valid' , ( ) => {
645+ const stream = new TestStream ( ) ;
646+ const consumer = new JSONConsumer ( { stream, level : 'info' } ) ;
647+ consumer . attach ( ) ;
648+ const logger = new Logger ( { level : 'info' } ) ;
649+ try {
650+ logger . info ( 'ok' , { userId : 1 , nickname : undefined } ) ;
651+ consumer . flushSync ( ) ;
652+ assert . strictEqual ( stream . logs . length , 1 ) ;
653+ assert . strictEqual ( stream . logs [ 0 ] . userId , 1 ) ;
654+ assert . ok ( ! ( 'nickname' in stream . logs [ 0 ] ) ) ;
655+ } finally {
656+ consumer . detach ( ) ;
657+ }
658+ } ) ;
659+ } ) ;
660+
661+ describe ( 'JSONConsumer reserved keys' , ( ) => {
662+ it ( 'should not let consumer fields override level/time/msg' , ( ) => {
663+ const stream = new TestStream ( ) ;
664+ const consumer = new JSONConsumer ( {
665+ stream,
666+ level : 'info' ,
667+ fields : { level : 'fake' , time : 0 , msg : 'fake' , service : 'api' } ,
668+ } ) ;
669+ consumer . attach ( ) ;
670+ const logger = new Logger ( { level : 'info' } ) ;
671+ try {
672+ logger . info ( 'real' ) ;
673+ consumer . flushSync ( ) ;
674+ const log = stream . logs [ 0 ] ;
675+ assert . strictEqual ( log . level , 'info' ) ;
676+ assert . strictEqual ( log . msg , 'real' ) ;
677+ assert . strictEqual ( typeof log . time , 'number' ) ;
678+ assert . notStrictEqual ( log . time , 0 ) ;
679+ assert . strictEqual ( log . service , 'api' ) ;
680+ } finally {
681+ consumer . detach ( ) ;
682+ }
683+ } ) ;
684+
685+ it ( 'should not let bindings override level/time/msg' , ( ) => {
686+ const stream = new TestStream ( ) ;
687+ const consumer = new JSONConsumer ( { stream, level : 'info' } ) ;
688+ consumer . attach ( ) ;
689+ const logger = new Logger ( {
690+ level : 'info' ,
691+ bindings : { level : 'fake' , time : 0 , msg : 'fake' , requestId : 'r1' } ,
692+ } ) ;
693+ try {
694+ logger . info ( 'real' ) ;
695+ consumer . flushSync ( ) ;
696+ const log = stream . logs [ 0 ] ;
697+ assert . strictEqual ( log . level , 'info' ) ;
698+ assert . strictEqual ( log . msg , 'real' ) ;
699+ assert . notStrictEqual ( log . time , 0 ) ;
700+ assert . strictEqual ( log . requestId , 'r1' ) ;
701+ } finally {
702+ consumer . detach ( ) ;
703+ }
704+ } ) ;
705+
706+ it ( 'should not let log fields override level/time/msg' , ( ) => {
707+ const stream = new TestStream ( ) ;
708+ const consumer = new JSONConsumer ( { stream, level : 'info' } ) ;
709+ consumer . attach ( ) ;
710+ const logger = new Logger ( { level : 'info' } ) ;
711+ try {
712+ logger . info ( 'real' , { level : 'fake' , time : 0 , msg : 'fake' , k : 1 } ) ;
713+ consumer . flushSync ( ) ;
714+ const log = stream . logs [ 0 ] ;
715+ assert . strictEqual ( log . level , 'info' ) ;
716+ assert . strictEqual ( log . msg , 'real' ) ;
717+ assert . notStrictEqual ( log . time , 0 ) ;
718+ assert . strictEqual ( log . k , 1 ) ;
719+ } finally {
720+ consumer . detach ( ) ;
721+ }
722+ } ) ;
723+ } ) ;
724+
725+ describe ( 'Logger Error-branch fields validation' , ( ) => {
726+ it ( 'should throw when fields is not an object for Error input' , ( ) => {
727+ const stream = new TestStream ( ) ;
728+ const consumer = new JSONConsumer ( { stream, level : 'info' } ) ;
729+ consumer . attach ( ) ;
730+ const logger = new Logger ( { level : 'info' } ) ;
731+ try {
732+ assert . throws ( ( ) => {
733+ logger . error ( new Error ( 'boom' ) , 'not-an-object' ) ;
734+ } , { code : 'ERR_INVALID_ARG_TYPE' } ) ;
735+ } finally {
736+ consumer . detach ( ) ;
737+ }
738+ } ) ;
739+ } ) ;
740+
741+ describe ( 'JSONConsumer stream interface' , ( ) => {
742+ it ( 'should throw when object stream is missing flush/flushSync/end' , ( ) => {
743+ const badStream = { write ( ) { } , end ( ) { } } ;
744+ assert . throws ( ( ) => {
745+ new JSONConsumer ( { stream : badStream , level : 'info' } ) ;
746+ } , { code : 'ERR_INVALID_ARG_TYPE' } ) ;
747+ } ) ;
748+
749+ it ( 'should accept object stream implementing full interface' , ( ) => {
750+ const stream = new TestStream ( ) ;
751+ const consumer = new JSONConsumer ( { stream, level : 'info' } ) ;
752+ assert . ok ( consumer ) ;
753+ } ) ;
754+ } ) ;
0 commit comments