11import fs from "node:fs/promises" ;
2- import { describe , expect , it , vi } from "vitest" ;
2+ import { beforeEach , describe , expect , it , vi } from "vitest" ;
33import type { OpenClawConfig } from "../config/config.js" ;
44import { runHeartbeatOnce , type HeartbeatDeps } from "./heartbeat-runner.js" ;
55import { installHeartbeatRunnerTestRuntime } from "./heartbeat-runner.test-harness.js" ;
@@ -9,8 +9,10 @@ import {
99 withTempHeartbeatSandbox ,
1010 withTempTelegramHeartbeatSandbox ,
1111} from "./heartbeat-runner.test-utils.js" ;
12+ import { enqueueSystemEvent , resetSystemEventsForTest } from "./system-events.js" ;
1213
1314installHeartbeatRunnerTestRuntime ( ) ;
15+ beforeEach ( ( ) => resetSystemEventsForTest ( ) ) ;
1416
1517describe ( "runHeartbeatOnce ack handling" , ( ) => {
1618 const WHATSAPP_GROUP = "120363140186826074@g.us" ;
@@ -302,6 +304,102 @@ describe("runHeartbeatOnce ack handling", () => {
302304 } ) ;
303305 } ) ;
304306
307+ it ( "suppresses exact no-op sentinel heartbeat replies" , async ( ) => {
308+ await withTempHeartbeatSandbox ( async ( { tmpDir, storePath, replySpy } ) => {
309+ const cfg = await createSeededWhatsAppHeartbeatConfig ( {
310+ tmpDir,
311+ storePath,
312+ } ) ;
313+
314+ const sendWhatsApp = createMessageSendSpy ( ) ;
315+ const cases = [ "NO_REPLY" , "NO_NEW_AUDIO" , "SESSION_WATCHDOG_OK" ] ;
316+ for ( const replyText of cases ) {
317+ replySpy . mockResolvedValueOnce ( { text : replyText } ) ;
318+ await runHeartbeatOnce ( {
319+ cfg,
320+ deps : {
321+ ...makeWhatsAppDeps ( { sendWhatsApp } ) ,
322+ getReplyFromConfig : replySpy ,
323+ } ,
324+ } ) ;
325+ }
326+
327+ expect ( sendWhatsApp ) . not . toHaveBeenCalled ( ) ;
328+ } ) ;
329+ } ) ;
330+
331+ it ( "suppresses exact HEARTBEAT_OK from exec system event handoff" , async ( ) => {
332+ await withTempHeartbeatSandbox ( async ( { tmpDir, storePath, replySpy } ) => {
333+ const cfg = await createSeededWhatsAppHeartbeatConfig ( {
334+ tmpDir,
335+ storePath,
336+ } ) ;
337+ const sessionKey = await seedMainSessionStore ( storePath , cfg , {
338+ lastChannel : "whatsapp" ,
339+ lastProvider : "whatsapp" ,
340+ lastTo : WHATSAPP_GROUP ,
341+ } ) ;
342+ enqueueSystemEvent ( "Exec completed (abc12345, code 0) :: no output" , {
343+ sessionKey,
344+ contextKey : "exec:abc12345" ,
345+ } ) ;
346+
347+ replySpy . mockResolvedValue ( { text : "HEARTBEAT_OK" } ) ;
348+ const sendWhatsApp = createMessageSendSpy ( ) ;
349+
350+ await runHeartbeatOnce ( {
351+ cfg,
352+ reason : "exec-event" ,
353+ deps : {
354+ ...makeWhatsAppDeps ( { sendWhatsApp } ) ,
355+ getReplyFromConfig : replySpy ,
356+ } ,
357+ } ) ;
358+
359+ expect ( sendWhatsApp ) . not . toHaveBeenCalled ( ) ;
360+ } ) ;
361+ } ) ;
362+
363+ it ( "keeps meaningful exec system event summaries" , async ( ) => {
364+ await withTempHeartbeatSandbox ( async ( { tmpDir, storePath, replySpy } ) => {
365+ const cfg = await createSeededWhatsAppHeartbeatConfig ( {
366+ tmpDir,
367+ storePath,
368+ } ) ;
369+ const sessionKey = await seedMainSessionStore ( storePath , cfg , {
370+ lastChannel : "whatsapp" ,
371+ lastProvider : "whatsapp" ,
372+ lastTo : WHATSAPP_GROUP ,
373+ } ) ;
374+ enqueueSystemEvent ( "Exec completed (abc12345, code 0) :: uploaded report.txt" , {
375+ sessionKey,
376+ contextKey : "exec:abc12345" ,
377+ } ) ;
378+
379+ replySpy . mockResolvedValue ( { text : "Command completed: uploaded report.txt" } ) ;
380+ const sendWhatsApp = createMessageSendSpy ( ) ;
381+
382+ await runHeartbeatOnce ( {
383+ cfg,
384+ reason : "exec-event" ,
385+ deps : {
386+ ...makeWhatsAppDeps ( { sendWhatsApp } ) ,
387+ getReplyFromConfig : replySpy ,
388+ } ,
389+ } ) ;
390+
391+ expect ( sendWhatsApp ) . toHaveBeenCalledTimes ( 1 ) ;
392+ expect ( sendWhatsApp ) . toHaveBeenCalledWith (
393+ WHATSAPP_GROUP ,
394+ "Command completed: uploaded report.txt" ,
395+ expect . any ( Object ) ,
396+ ) ;
397+ const calledCtx = replySpy . mock . calls [ 0 ] ?. [ 0 ] as { Body ?: string ; Provider ?: string } ;
398+ expect ( calledCtx . Provider ) . toBe ( "exec-event" ) ;
399+ expect ( calledCtx . Body ) . toContain ( "Please relay the command output to the user" ) ;
400+ } ) ;
401+ } ) ;
402+
305403 it ( "does not regress updatedAt when restoring heartbeat sessions" , async ( ) => {
306404 await withTempHeartbeatSandbox ( async ( { tmpDir, storePath, replySpy } ) => {
307405 const originalUpdatedAt = 1000 ;
0 commit comments