@@ -646,4 +646,315 @@ suite('Kernel Connection Helpers', () => {
646646 assert . strictEqual ( name , '.env (Python 9.8.7)' ) ;
647647 } ) ;
648648 } ) ;
649+
650+ suite ( 'executeSilently' , ( ) => {
651+ interface MockKernelOptions {
652+ status : 'ok' | 'error' ;
653+ messages ?: Array < {
654+ msg_type : 'stream' | 'error' | 'display_data' | 'execute_result' ;
655+ content : any ;
656+ } > ;
657+ errorContent ?: {
658+ ename : string ;
659+ evalue : string ;
660+ traceback : string [ ] ;
661+ } ;
662+ }
663+
664+ function createMockKernel ( options : MockKernelOptions ) {
665+ return {
666+ requestExecute : ( ) => {
667+ let resolvePromise : ( value : any ) => void ;
668+
669+ // Create a promise that will be resolved after IOPub messages are dispatched
670+ const donePromise = new Promise < any > ( ( resolve ) => {
671+ resolvePromise = resolve ;
672+ } ) ;
673+
674+ return {
675+ done : donePromise ,
676+ set onIOPub ( cb : ( msg : any ) => void ) {
677+ // Invoke IOPub callback synchronously with all messages
678+ if ( options . messages && options . messages . length > 0 ) {
679+ options . messages . forEach ( ( msg ) => {
680+ cb ( {
681+ header : { msg_type : msg . msg_type } ,
682+ content : msg . content
683+ } ) ;
684+ } ) ;
685+ }
686+ // Resolve the done promise after messages are dispatched
687+ resolvePromise ( {
688+ content :
689+ options . status === 'ok'
690+ ? { status : 'ok' as const }
691+ : {
692+ status : 'error' as const ,
693+ ...options . errorContent
694+ }
695+ } ) ;
696+ }
697+ } ;
698+ }
699+ } ;
700+ }
701+
702+ test ( 'Returns outputs from kernel execution' , async ( ) => {
703+ const mockKernel = createMockKernel ( {
704+ status : 'ok' ,
705+ messages : [
706+ {
707+ msg_type : 'stream' ,
708+ content : {
709+ name : 'stdout' ,
710+ text : 'hello\n'
711+ }
712+ }
713+ ]
714+ } ) ;
715+
716+ const code = 'print("hello")' ;
717+ const { executeSilently } = await import ( './helpers' ) ;
718+ const result = await executeSilently ( mockKernel as any , code ) ;
719+
720+ // executeSilently should return outputs array with collected stream output
721+ assert . isArray ( result ) ;
722+ assert . equal ( result . length , 1 ) ;
723+ assert . equal ( result [ 0 ] . output_type , 'stream' ) ;
724+ assert . equal ( ( result [ 0 ] as any ) . name , 'stdout' ) ;
725+ assert . equal ( ( result [ 0 ] as any ) . text , 'hello\n' ) ;
726+ } ) ;
727+
728+ test ( 'Collects error outputs' , async ( ) => {
729+ const mockKernel = createMockKernel ( {
730+ status : 'error' ,
731+ errorContent : {
732+ ename : 'NameError' ,
733+ evalue : 'name not defined' ,
734+ traceback : [ 'Traceback...' ]
735+ } ,
736+ messages : [
737+ {
738+ msg_type : 'error' ,
739+ content : {
740+ ename : 'NameError' ,
741+ evalue : 'name not defined' ,
742+ traceback : [ 'Traceback...' ]
743+ }
744+ }
745+ ]
746+ } ) ;
747+
748+ const code = 'undefined_variable' ;
749+ const { executeSilently } = await import ( './helpers' ) ;
750+ const result = await executeSilently ( mockKernel as any , code ) ;
751+
752+ assert . isArray ( result ) ;
753+ assert . equal ( result . length , 1 ) ;
754+ assert . equal ( result [ 0 ] . output_type , 'error' ) ;
755+ assert . equal ( ( result [ 0 ] as any ) . ename , 'NameError' ) ;
756+ assert . equal ( ( result [ 0 ] as any ) . evalue , 'name not defined' ) ;
757+ assert . deepStrictEqual ( ( result [ 0 ] as any ) . traceback , [ 'Traceback...' ] ) ;
758+ } ) ;
759+
760+ test ( 'Collects display_data outputs' , async ( ) => {
761+ const mockKernel = createMockKernel ( {
762+ status : 'ok' ,
763+ messages : [
764+ {
765+ msg_type : 'display_data' ,
766+ content : {
767+ data : {
768+ 'text/plain' : 'some data'
769+ } ,
770+ metadata : { }
771+ }
772+ }
773+ ]
774+ } ) ;
775+
776+ const code = 'display("data")' ;
777+ const { executeSilently } = await import ( './helpers' ) ;
778+ const result = await executeSilently ( mockKernel as any , code ) ;
779+
780+ assert . isArray ( result ) ;
781+ assert . equal ( result . length , 1 ) ;
782+ assert . equal ( result [ 0 ] . output_type , 'display_data' ) ;
783+ assert . deepStrictEqual ( ( result [ 0 ] as any ) . data , { 'text/plain' : 'some data' } ) ;
784+ assert . deepStrictEqual ( ( result [ 0 ] as any ) . metadata , { } ) ;
785+ } ) ;
786+
787+ test ( 'Handles multiple outputs' , async ( ) => {
788+ const mockKernel = createMockKernel ( {
789+ status : 'ok' ,
790+ messages : [
791+ {
792+ msg_type : 'stream' ,
793+ content : {
794+ name : 'stdout' ,
795+ text : 'output 1'
796+ }
797+ } ,
798+ {
799+ msg_type : 'stream' ,
800+ content : {
801+ name : 'stdout' ,
802+ text : 'output 2'
803+ }
804+ }
805+ ]
806+ } ) ;
807+
808+ const code = 'print("1"); print("2")' ;
809+ const { executeSilently } = await import ( './helpers' ) ;
810+ const result = await executeSilently ( mockKernel as any , code ) ;
811+
812+ assert . isArray ( result ) ;
813+ // Consecutive stream messages with the same name are concatenated
814+ assert . equal ( result . length , 1 ) ;
815+ assert . equal ( result [ 0 ] . output_type , 'stream' ) ;
816+ assert . equal ( ( result [ 0 ] as any ) . name , 'stdout' ) ;
817+ assert . equal ( ( result [ 0 ] as any ) . text , 'output 1output 2' ) ;
818+ } ) ;
819+
820+ test ( 'Collects execute_result outputs' , async ( ) => {
821+ const mockKernel = createMockKernel ( {
822+ status : 'ok' ,
823+ messages : [
824+ {
825+ msg_type : 'execute_result' ,
826+ content : {
827+ data : {
828+ 'text/plain' : '42'
829+ } ,
830+ metadata : { } ,
831+ execution_count : 1
832+ }
833+ }
834+ ]
835+ } ) ;
836+
837+ const code = '42' ;
838+ const { executeSilently } = await import ( './helpers' ) ;
839+ const result = await executeSilently ( mockKernel as any , code ) ;
840+
841+ assert . isArray ( result ) ;
842+ assert . equal ( result . length , 1 ) ;
843+ assert . equal ( result [ 0 ] . output_type , 'execute_result' ) ;
844+ assert . deepStrictEqual ( ( result [ 0 ] as any ) . data , { 'text/plain' : '42' } ) ;
845+ assert . deepStrictEqual ( ( result [ 0 ] as any ) . metadata , { } ) ;
846+ assert . equal ( ( result [ 0 ] as any ) . execution_count , 1 ) ;
847+ } ) ;
848+
849+ test ( 'Stream messages with different names produce separate outputs' , async ( ) => {
850+ const mockKernel = createMockKernel ( {
851+ status : 'ok' ,
852+ messages : [
853+ {
854+ msg_type : 'stream' ,
855+ content : {
856+ name : 'stdout' ,
857+ text : 'standard output'
858+ }
859+ } ,
860+ {
861+ msg_type : 'stream' ,
862+ content : {
863+ name : 'stderr' ,
864+ text : 'error output'
865+ }
866+ } ,
867+ {
868+ msg_type : 'stream' ,
869+ content : {
870+ name : 'stdout' ,
871+ text : ' more stdout'
872+ }
873+ }
874+ ]
875+ } ) ;
876+
877+ const code = 'print("test")' ;
878+ const { executeSilently } = await import ( './helpers' ) ;
879+ const result = await executeSilently ( mockKernel as any , code ) ;
880+
881+ assert . isArray ( result ) ;
882+ // Should have 3 outputs: stdout, stderr, stdout (not concatenated because stderr is in between)
883+ assert . equal ( result . length , 3 ) ;
884+ assert . equal ( result [ 0 ] . output_type , 'stream' ) ;
885+ assert . equal ( ( result [ 0 ] as any ) . name , 'stdout' ) ;
886+ assert . equal ( ( result [ 0 ] as any ) . text , 'standard output' ) ;
887+ assert . equal ( result [ 1 ] . output_type , 'stream' ) ;
888+ assert . equal ( ( result [ 1 ] as any ) . name , 'stderr' ) ;
889+ assert . equal ( ( result [ 1 ] as any ) . text , 'error output' ) ;
890+ assert . equal ( result [ 2 ] . output_type , 'stream' ) ;
891+ assert . equal ( ( result [ 2 ] as any ) . name , 'stdout' ) ;
892+ assert . equal ( ( result [ 2 ] as any ) . text , ' more stdout' ) ;
893+ } ) ;
894+
895+ test ( 'errorOptions with traceErrors logs errors' , async ( ) => {
896+ const mockKernel = createMockKernel ( {
897+ status : 'error' ,
898+ errorContent : {
899+ ename : 'ValueError' ,
900+ evalue : 'invalid value' ,
901+ traceback : [ 'Traceback (most recent call last):' , ' File "<stdin>", line 1' ]
902+ } ,
903+ messages : [
904+ {
905+ msg_type : 'error' ,
906+ content : {
907+ ename : 'ValueError' ,
908+ evalue : 'invalid value' ,
909+ traceback : [ 'Traceback (most recent call last):' , ' File "<stdin>", line 1' ]
910+ }
911+ }
912+ ]
913+ } ) ;
914+
915+ const code = 'raise ValueError("invalid value")' ;
916+ const { executeSilently } = await import ( './helpers' ) ;
917+ const result = await executeSilently ( mockKernel as any , code , {
918+ traceErrors : true ,
919+ traceErrorsMessage : 'Custom error message'
920+ } ) ;
921+
922+ assert . isArray ( result ) ;
923+ assert . equal ( result . length , 1 ) ;
924+ assert . equal ( result [ 0 ] . output_type , 'error' ) ;
925+ assert . equal ( ( result [ 0 ] as any ) . ename , 'ValueError' ) ;
926+ } ) ;
927+
928+ test ( 'errorOptions without traceErrors still collects errors' , async ( ) => {
929+ const mockKernel = createMockKernel ( {
930+ status : 'error' ,
931+ errorContent : {
932+ ename : 'RuntimeError' ,
933+ evalue : 'runtime issue' ,
934+ traceback : [ 'Traceback...' ]
935+ } ,
936+ messages : [
937+ {
938+ msg_type : 'error' ,
939+ content : {
940+ ename : 'RuntimeError' ,
941+ evalue : 'runtime issue' ,
942+ traceback : [ 'Traceback...' ]
943+ }
944+ }
945+ ]
946+ } ) ;
947+
948+ const code = 'raise RuntimeError("runtime issue")' ;
949+ const { executeSilently } = await import ( './helpers' ) ;
950+ const result = await executeSilently ( mockKernel as any , code , {
951+ traceErrors : false
952+ } ) ;
953+
954+ assert . isArray ( result ) ;
955+ assert . equal ( result . length , 1 ) ;
956+ assert . equal ( result [ 0 ] . output_type , 'error' ) ;
957+ assert . equal ( ( result [ 0 ] as any ) . ename , 'RuntimeError' ) ;
958+ } ) ;
959+ } ) ;
649960} ) ;
0 commit comments