@@ -171,7 +171,7 @@ describe("handleChatEvent", () => {
171171 expect ( state . chatStream ) . toBe ( "Hello" ) ;
172172 } ) ;
173173
174- it ( " ignores NO_REPLY delta updates", ( ) => {
174+ it . each ( [ "NO_REPLY" , "HEARTBEAT_OK" ] ) ( " ignores %s delta updates", ( text ) => {
175175 const state = createState ( {
176176 sessionKey : "main" ,
177177 chatRunId : "run-1" ,
@@ -181,7 +181,7 @@ describe("handleChatEvent", () => {
181181 runId : "run-1" ,
182182 sessionKey : "main" ,
183183 state : "delta" ,
184- message : { role : "assistant" , content : [ { type : "text" , text : "NO_REPLY" } ] } ,
184+ message : { role : "assistant" , content : [ { type : "text" , text } ] } ,
185185 } ;
186186
187187 expect ( handleChatEvent ( state , payload ) ) . toBe ( "delta" ) ;
@@ -212,16 +212,19 @@ describe("handleChatEvent", () => {
212212 expect ( state . chatMessages [ 0 ] ) . toEqual ( payload . message ) ;
213213 } ) ;
214214
215- it ( "drops NO_REPLY final payload from another run without clearing active stream" , ( ) => {
216- const state = createActiveStreamingState ( ) ;
217- const payload = createOtherRunNoReplyFinalPayload ( ) ;
215+ it . each ( [ "NO_REPLY" , "HEARTBEAT_OK" ] ) (
216+ "drops %s final payload from another run without clearing active stream" ,
217+ ( text ) => {
218+ const state = createActiveStreamingState ( ) ;
219+ const payload = createOtherRunSilentFinalPayload ( text ) ;
218220
219- expect ( handleChatEvent ( state , payload ) ) . toBe ( "final" ) ;
220- expect ( state . chatRunId ) . toBe ( "run-user" ) ;
221- expect ( state . chatStream ) . toBe ( "Working..." ) ;
222- expect ( state . chatStreamStartedAt ) . toBe ( 123 ) ;
223- expect ( state . chatMessages ) . toEqual ( [ ] ) ;
224- } ) ;
221+ expect ( handleChatEvent ( state , payload ) ) . toBe ( "final" ) ;
222+ expect ( state . chatRunId ) . toBe ( "run-user" ) ;
223+ expect ( state . chatStream ) . toBe ( "Working..." ) ;
224+ expect ( state . chatStreamStartedAt ) . toBe ( 123 ) ;
225+ expect ( state . chatMessages ) . toEqual ( [ ] ) ;
226+ } ,
227+ ) ;
225228
226229 it . each ( [ "no_reply" , "ANNOUNCE_SKIP" , "REPLY_SKIP" ] ) (
227230 "keeps plain-text %s final payload from another run without clearing active stream" ,
@@ -559,11 +562,11 @@ describe("handleChatEvent", () => {
559562 expect ( state . chatStream ) . toBe ( "Working..." ) ;
560563 } ) ;
561564
562- it ( " drops NO_REPLY final payload from own run", ( ) => {
565+ it . each ( [ "NO_REPLY" , "HEARTBEAT_OK" ] ) ( " drops %s final payload from own run", ( text ) => {
563566 const state = createState ( {
564567 sessionKey : "main" ,
565568 chatRunId : "run-1" ,
566- chatStream : "NO_REPLY" ,
569+ chatStream : text ,
567570 chatStreamStartedAt : 100 ,
568571 } ) ;
569572 const payload : ChatEventPayload = {
@@ -572,7 +575,7 @@ describe("handleChatEvent", () => {
572575 state : "final" ,
573576 message : {
574577 role : "assistant" ,
575- content : [ { type : "text" , text : "NO_REPLY" } ] ,
578+ content : [ { type : "text" , text } ] ,
576579 } ,
577580 } ;
578581
@@ -608,22 +611,25 @@ describe("handleChatEvent", () => {
608611 } ,
609612 ) ;
610613
611- it ( "does not persist NO_REPLY stream text on final without message" , ( ) => {
612- const state = createState ( {
613- sessionKey : "main" ,
614- chatRunId : "run-1" ,
615- chatStream : "NO_REPLY" ,
616- chatStreamStartedAt : 100 ,
617- } ) ;
618- const payload : ChatEventPayload = {
619- runId : "run-1" ,
620- sessionKey : "main" ,
621- state : "final" ,
622- } ;
614+ it . each ( [ "NO_REPLY" , "HEARTBEAT_OK" ] ) (
615+ "does not persist %s stream text on final without message" ,
616+ ( text ) => {
617+ const state = createState ( {
618+ sessionKey : "main" ,
619+ chatRunId : "run-1" ,
620+ chatStream : text ,
621+ chatStreamStartedAt : 100 ,
622+ } ) ;
623+ const payload : ChatEventPayload = {
624+ runId : "run-1" ,
625+ sessionKey : "main" ,
626+ state : "final" ,
627+ } ;
623628
624- expect ( handleChatEvent ( state , payload ) ) . toBe ( "final" ) ;
625- expect ( state . chatMessages ) . toEqual ( [ ] ) ;
626- } ) ;
629+ expect ( handleChatEvent ( state , payload ) ) . toBe ( "final" ) ;
630+ expect ( state . chatMessages ) . toEqual ( [ ] ) ;
631+ } ,
632+ ) ;
627633
628634 it ( "does not persist NO_REPLY stream text on abort" , ( ) => {
629635 const state = createState ( {
@@ -1062,10 +1068,28 @@ describe("loadChatHistory", () => {
10621068 expect ( state . lastError ) . toBeNull ( ) ;
10631069 } ) ;
10641070
1065- it ( "filters heartbeat acknowledgements and internal-only user messages" , async ( ) => {
1071+ it ( "filters heartbeat acknowledgements, heartbeat prompts, and internal-only user messages" , async ( ) => {
10661072 const request = vi . fn ( ) . mockResolvedValue ( {
10671073 messages : [
10681074 { role : "assistant" , content : [ { type : "text" , text : "HEARTBEAT_OK" } ] } ,
1075+ {
1076+ role : "user" ,
1077+ content : [
1078+ {
1079+ type : "text" ,
1080+ text : "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. If nothing needs attention, reply HEARTBEAT_OK." ,
1081+ } ,
1082+ ] ,
1083+ } ,
1084+ {
1085+ role : "user" ,
1086+ content : [
1087+ {
1088+ type : "text" ,
1089+ text : "An async command completion event was triggered, but user delivery is disabled for this run. Handle the result internally and reply HEARTBEAT_OK only." ,
1090+ } ,
1091+ ] ,
1092+ } ,
10691093 {
10701094 role : "user" ,
10711095 content : [
0 commit comments