@@ -699,6 +699,55 @@ describe("a live turn", () => {
699699 ) ;
700700 } ) ;
701701
702+ test ( "a silent sibling does not clear a coalesced pending shell tail" , async ( ) => {
703+ await withTestRenderer (
704+ async ( h ) => {
705+ const shell = createAppShell ( h . renderer , {
706+ terminal : { columns : 80 , rows : 24 } ,
707+ wireKeys : false ,
708+ run : "idle" ,
709+ } ) ;
710+ const bridge = attachSessionBridge ( shell , createRecordingPort ( ) ) ;
711+ try {
712+ bridge . play ( [
713+ {
714+ type : "inference.tool_call.end" ,
715+ data : {
716+ name : "run_shell" ,
717+ callId : "sh1" ,
718+ arguments : { command : "echo alpha" } ,
719+ } ,
720+ } ,
721+ {
722+ type : "inference.tool_call.end" ,
723+ data : {
724+ name : "run_shell" ,
725+ callId : "sh2" ,
726+ arguments : { command : "sleep 5; echo done" } ,
727+ } ,
728+ } ,
729+ ] ) ;
730+ await h . renderOnce ( ) ;
731+ expect ( shell . streamLog . length ) . toBe ( 1 ) ;
732+ expect ( shell . streamLog [ 0 ] ?. coalesced ) . toBe ( true ) ;
733+ expect ( shell . streamLog [ 0 ] ?. pending ) . toBe ( true ) ;
734+
735+ bridge . syncShellOutputs ( ( callId ) => {
736+ if ( callId === "sh1" ) return liveFeed ( ( ) => "alpha-only\n" ) ;
737+ if ( callId === "sh2" ) return liveFeed ( ( ) => "" ) ;
738+ return undefined ;
739+ } ) ;
740+ await h . renderOnce ( ) ;
741+ expect ( shell . streamLog [ 0 ] ?. previewLines ) . toEqual ( [ "alpha-only" ] ) ;
742+ } finally {
743+ bridge . dispose ( ) ;
744+ shell . dispose ( ) ;
745+ }
746+ } ,
747+ { width : 80 , height : 24 } ,
748+ ) ;
749+ } ) ;
750+
702751 test ( "rollbackAttempt drops shellSnapshots for truncated run_shell calls" , async ( ) => {
703752 await withTestRenderer (
704753 async ( h ) => {
@@ -902,4 +951,54 @@ describe("lane paint", () => {
902951 pushToolResult ( ok , { name : "run_shell" , content : "all good" } ) ;
903952 expect ( ok [ 0 ] ?. stat ) . toBeUndefined ( ) ;
904953 } ) ;
954+
955+ test ( "a coalesced shell lane keeps each call's answer behind the arrow" , ( ) => {
956+ const rows : StreamRow [ ] = [ ] ;
957+ pushToolCall ( rows , {
958+ name : "run_shell" ,
959+ arguments : JSON . stringify ( { command : "echo a" } ) ,
960+ callId : "s1" ,
961+ } ) ;
962+ pushToolCall ( rows , {
963+ name : "run_shell" ,
964+ arguments : JSON . stringify ( { command : "echo b" } ) ,
965+ callId : "s2" ,
966+ } ) ;
967+ pushToolResult ( rows , { name : "run_shell" , content : "a" , callId : "s1" } ) ;
968+ pushToolResult ( rows , { name : "run_shell" , content : "b" , callId : "s2" } ) ;
969+ const answers = ( rows [ 0 ] ?. detail ?? [ ] ) . map ( ( line ) =>
970+ line . map ( ( segment ) => segment . text ) . join ( "" ) ,
971+ ) ;
972+ expect ( answers ) . not . toEqual ( [ "answered" , "answered" ] ) ;
973+ expect ( answers ) . toContain ( "a" ) ;
974+ expect ( rows [ 0 ] ?. resultText ) . toBe ( "b" ) ;
975+ expect ( rows [ 0 ] ?. previewLines ) . toEqual ( [ "b" ] ) ;
976+ } ) ;
977+
978+ test ( "a later zero-exit coalesced shell does not keep a leftover exit stat" , ( ) => {
979+ const rows : StreamRow [ ] = [ ] ;
980+ pushToolCall ( rows , {
981+ name : "run_shell" ,
982+ arguments : JSON . stringify ( { command : "false" } ) ,
983+ callId : "s1" ,
984+ } ) ;
985+ pushToolCall ( rows , {
986+ name : "run_shell" ,
987+ arguments : JSON . stringify ( { command : "true" } ) ,
988+ callId : "s2" ,
989+ } ) ;
990+ pushToolResult ( rows , {
991+ name : "run_shell" ,
992+ content : "exit code 1\nboom" ,
993+ callId : "s1" ,
994+ } ) ;
995+ pushToolResult ( rows , {
996+ name : "run_shell" ,
997+ content : "all good" ,
998+ callId : "s2" ,
999+ } ) ;
1000+ expect ( rows [ 0 ] ?. pending ) . toBeUndefined ( ) ;
1001+ expect ( collapsed ( defined ( rows [ 0 ] ) ) ) . toContain ( "true" ) ;
1002+ expect ( rows [ 0 ] ?. stat ) . not . toBe ( "exit 1" ) ;
1003+ } ) ;
9051004} ) ;
0 commit comments