@@ -837,5 +837,258 @@ describe("Stream Object Route", () => {
837837 ( objectEvent ?. data as { object : Array < { name : string } > } ) . object ,
838838 ) . toEqual ( [ { name : "Alice" } , { name : "Bob" } ] ) ;
839839 } ) ;
840+
841+ it ( "emits warning event when using markdown-block extraction" , async ( ) => {
842+ const mockProc = createMockChildProcess ( ) ;
843+ mockSpawn . mockReturnValue ( mockProc as never ) ;
844+
845+ const app = createTestApp ( ) ;
846+ const responsePromise = request ( app )
847+ . post ( "/stream-object" )
848+ . send ( { prompt : "Extract" , schema : validSchema } ) ;
849+
850+ afterSpawnCalled ( mockSpawn , ( ) => {
851+ // Claude wraps JSON in markdown - this triggers fallback extraction
852+ mockProc . stdout . emit (
853+ "data" ,
854+ Buffer . from (
855+ `${ createStreamEventDelta ( '```json\n{"name": "FallbackTest", "age": 30}\n```' ) } \n` ,
856+ ) ,
857+ ) ;
858+ mockProc . stdout . emit (
859+ "data" ,
860+ Buffer . from ( `${ createStreamResultMessage ( ) } \n` ) ,
861+ ) ;
862+ mockProc . emit ( "close" , 0 , null ) ;
863+ } ) ;
864+
865+ const res = await responsePromise ;
866+ const events = parseSSEResponse ( res . text ) ;
867+
868+ // Should emit warning about fallback extraction
869+ const warningEvent = events . find ( ( e ) => e . event === "warning" ) ;
870+ expect ( warningEvent ) . toBeDefined ( ) ;
871+ expect ( ( warningEvent ?. data as { code : string } ) . code ) . toBe (
872+ "EXTRACTION_FALLBACK" ,
873+ ) ;
874+ expect ( ( warningEvent ?. data as { message : string } ) . message ) . toContain (
875+ "markdown-block" ,
876+ ) ;
877+
878+ // Object should still be extracted correctly
879+ const objectEvent = events . find ( ( e ) => e . event === "object" ) ;
880+ expect ( objectEvent ) . toBeDefined ( ) ;
881+ expect (
882+ ( objectEvent ?. data as { object : { name : string } } ) . object . name ,
883+ ) . toBe ( "FallbackTest" ) ;
884+ } ) ;
885+
886+ it ( "emits warning event when using object-extraction strategy" , async ( ) => {
887+ const mockProc = createMockChildProcess ( ) ;
888+ mockSpawn . mockReturnValue ( mockProc as never ) ;
889+
890+ const app = createTestApp ( ) ;
891+ const responsePromise = request ( app )
892+ . post ( "/stream-object" )
893+ . send ( { prompt : "Extract" , schema : validSchema } ) ;
894+
895+ afterSpawnCalled ( mockSpawn , ( ) => {
896+ // Text with surrounding content triggers object-extraction fallback
897+ mockProc . stdout . emit (
898+ "data" ,
899+ Buffer . from (
900+ `${ createStreamEventDelta ( 'Sure! Here is the result: {"name": "ObjectExtract", "age": 40} Hope this helps!' ) } \n` ,
901+ ) ,
902+ ) ;
903+ mockProc . stdout . emit (
904+ "data" ,
905+ Buffer . from ( `${ createStreamResultMessage ( ) } \n` ) ,
906+ ) ;
907+ mockProc . emit ( "close" , 0 , null ) ;
908+ } ) ;
909+
910+ const res = await responsePromise ;
911+ const events = parseSSEResponse ( res . text ) ;
912+
913+ // Should emit warning about fallback extraction
914+ const warningEvent = events . find ( ( e ) => e . event === "warning" ) ;
915+ expect ( warningEvent ) . toBeDefined ( ) ;
916+ expect ( ( warningEvent ?. data as { code : string } ) . code ) . toBe (
917+ "EXTRACTION_FALLBACK" ,
918+ ) ;
919+ expect ( ( warningEvent ?. data as { message : string } ) . message ) . toContain (
920+ "object-extraction" ,
921+ ) ;
922+ } ) ;
923+
924+ it ( "emits warning event when using array-extraction strategy" , async ( ) => {
925+ const arraySchema = {
926+ type : "array" ,
927+ items : {
928+ type : "object" ,
929+ properties : { name : { type : "string" } } ,
930+ } ,
931+ } ;
932+
933+ const mockProc = createMockChildProcess ( ) ;
934+ mockSpawn . mockReturnValue ( mockProc as never ) ;
935+
936+ const app = createTestApp ( ) ;
937+ const responsePromise = request ( app )
938+ . post ( "/stream-object" )
939+ . send ( { prompt : "List people" , schema : arraySchema } ) ;
940+
941+ afterSpawnCalled ( mockSpawn , ( ) => {
942+ // Text with surrounding content and array triggers array-extraction
943+ mockProc . stdout . emit (
944+ "data" ,
945+ Buffer . from (
946+ `${ createStreamEventDelta ( 'Here are the people: [{"name": "ArrayTest1"}, {"name": "ArrayTest2"}] Done!' ) } \n` ,
947+ ) ,
948+ ) ;
949+ mockProc . stdout . emit (
950+ "data" ,
951+ Buffer . from ( `${ createStreamResultMessage ( ) } \n` ) ,
952+ ) ;
953+ mockProc . emit ( "close" , 0 , null ) ;
954+ } ) ;
955+
956+ const res = await responsePromise ;
957+ const events = parseSSEResponse ( res . text ) ;
958+
959+ // Should emit warning about fallback extraction
960+ const warningEvent = events . find ( ( e ) => e . event === "warning" ) ;
961+ expect ( warningEvent ) . toBeDefined ( ) ;
962+ expect ( ( warningEvent ?. data as { code : string } ) . code ) . toBe (
963+ "EXTRACTION_FALLBACK" ,
964+ ) ;
965+ expect ( ( warningEvent ?. data as { message : string } ) . message ) . toContain (
966+ "array-extraction" ,
967+ ) ;
968+
969+ // Array should still be extracted correctly
970+ const objectEvent = events . find ( ( e ) => e . event === "object" ) ;
971+ expect ( objectEvent ) . toBeDefined ( ) ;
972+ expect (
973+ ( objectEvent ?. data as { object : Array < { name : string } > } ) . object ,
974+ ) . toEqual ( [ { name : "ArrayTest1" } , { name : "ArrayTest2" } ] ) ;
975+ } ) ;
976+
977+ it ( "emits warning when clean exit has unparseable buffer data" , async ( ) => {
978+ const mockProc = createMockChildProcess ( ) ;
979+ mockSpawn . mockReturnValue ( mockProc as never ) ;
980+
981+ const app = createTestApp ( ) ;
982+ const responsePromise = request ( app )
983+ . post ( "/stream-object" )
984+ . send ( { prompt : "Hello" , schema : validSchema } ) ;
985+
986+ afterSpawnCalled ( mockSpawn , ( ) => {
987+ // Stream valid JSON first
988+ mockProc . stdout . emit (
989+ "data" ,
990+ Buffer . from (
991+ `${ createStreamEventDelta ( '{"name": "Test", "age": 1}' ) } \n` ,
992+ ) ,
993+ ) ;
994+ mockProc . stdout . emit (
995+ "data" ,
996+ Buffer . from ( `${ createStreamResultMessage ( ) } \n` ) ,
997+ ) ;
998+ // Leave garbage in buffer (no newline - stays in buffer)
999+ mockProc . stdout . emit (
1000+ "data" ,
1001+ Buffer . from ( "garbage data without newline" ) ,
1002+ ) ;
1003+ // Clean exit (code=0, signal=null) with garbage in buffer
1004+ mockProc . emit ( "close" , 0 , null ) ;
1005+ } ) ;
1006+
1007+ const res = await responsePromise ;
1008+ const events = parseSSEResponse ( res . text ) ;
1009+
1010+ // Should emit warning about unparseable buffer data
1011+ const warningEvent = events . find ( ( e ) => e . event === "warning" ) ;
1012+ expect ( warningEvent ) . toBeDefined ( ) ;
1013+ expect ( ( warningEvent ?. data as { code : string } ) . code ) . toBe (
1014+ "BUFFER_PARSE_WARNING" ,
1015+ ) ;
1016+ } ) ;
1017+
1018+ it ( "handles interrupt exit code with buffer data gracefully" , async ( ) => {
1019+ const mockProc = createMockChildProcess ( ) ;
1020+ mockSpawn . mockReturnValue ( mockProc as never ) ;
1021+
1022+ const app = createTestApp ( ) ;
1023+ const responsePromise = request ( app )
1024+ . post ( "/stream-object" )
1025+ . send ( { prompt : "Hello" , schema : validSchema } ) ;
1026+
1027+ afterSpawnCalled ( mockSpawn , ( ) => {
1028+ // Stream valid JSON first
1029+ mockProc . stdout . emit (
1030+ "data" ,
1031+ Buffer . from (
1032+ `${ createStreamEventDelta ( '{"name": "Test", "age": 1}' ) } \n` ,
1033+ ) ,
1034+ ) ;
1035+ mockProc . stdout . emit (
1036+ "data" ,
1037+ Buffer . from ( `${ createStreamResultMessage ( ) } \n` ) ,
1038+ ) ;
1039+ // Leave incomplete data in buffer (no newline)
1040+ mockProc . stdout . emit ( "data" , Buffer . from ( "incomplete data" ) ) ;
1041+ // Non-zero exit (interrupt) - buffer warning should NOT be emitted
1042+ mockProc . emit ( "close" , 1 , "SIGTERM" ) ;
1043+ } ) ;
1044+
1045+ const res = await responsePromise ;
1046+ const events = parseSSEResponse ( res . text ) ;
1047+
1048+ // Should NOT emit warning for interrupt (non-zero exit code is expected)
1049+ const warningEvent = events . find (
1050+ ( e ) =>
1051+ e . event === "warning" &&
1052+ ( e . data as { code : string } ) . code === "BUFFER_PARSE_WARNING" ,
1053+ ) ;
1054+ expect ( warningEvent ) . toBeUndefined ( ) ;
1055+
1056+ // Should emit CLI_EXIT_ERROR for non-zero exit
1057+ const errorEvent = events . find ( ( e ) => e . event === "error" ) ;
1058+ expect ( errorEvent ) . toBeDefined ( ) ;
1059+ expect ( ( errorEvent ?. data as { code : string } ) . code ) . toBe (
1060+ "CLI_EXIT_ERROR" ,
1061+ ) ;
1062+ } ) ;
1063+
1064+ it ( "handles parse error on final object in close handler" , async ( ) => {
1065+ const mockProc = createMockChildProcess ( ) ;
1066+ mockSpawn . mockReturnValue ( mockProc as never ) ;
1067+
1068+ const app = createTestApp ( ) ;
1069+ const responsePromise = request ( app )
1070+ . post ( "/stream-object" )
1071+ . send ( { prompt : "Hello" , schema : validSchema } ) ;
1072+
1073+ afterSpawnCalled ( mockSpawn , ( ) => {
1074+ // Stream completely malformed JSON that can't be extracted
1075+ mockProc . stdout . emit (
1076+ "data" ,
1077+ Buffer . from ( `${ createStreamEventDelta ( "not json at all {{{{" ) } \n` ) ,
1078+ ) ;
1079+ // Send result without newline - triggers close handler parsing
1080+ const resultMessage = createStreamResultMessage ( ) ;
1081+ mockProc . stdout . emit ( "data" , Buffer . from ( resultMessage ) ) ;
1082+ mockProc . emit ( "close" , 0 , null ) ;
1083+ } ) ;
1084+
1085+ const res = await responsePromise ;
1086+ const events = parseSSEResponse ( res . text ) ;
1087+
1088+ // Should emit parse error
1089+ const errorEvent = events . find ( ( e ) => e . event === "error" ) ;
1090+ expect ( errorEvent ) . toBeDefined ( ) ;
1091+ expect ( ( errorEvent ?. data as { code : string } ) . code ) . toBe ( "PARSE_ERROR" ) ;
1092+ } ) ;
8401093 } ) ;
8411094} ) ;
0 commit comments