@@ -6,6 +6,7 @@ import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js";
66import { loadCommitmentStore , saveCommitmentStore } from "../commitments/store.js" ;
77import type { CommitmentRecord , CommitmentStoreFile } from "../commitments/types.js" ;
88import type { OpenClawConfig } from "../config/config.js" ;
9+ import { getLastHeartbeatEvent , resetHeartbeatEventsForTest } from "./heartbeat-events.js" ;
910import {
1011 runHeartbeatOnce ,
1112 setHeartbeatsEnabled ,
@@ -25,6 +26,7 @@ describe("runHeartbeatOnce commitments", () => {
2526 setHeartbeatsEnabled ( true ) ;
2627 vi . useRealTimers ( ) ;
2728 vi . unstubAllEnvs ( ) ;
29+ resetHeartbeatEventsForTest ( ) ;
2830 } ) ;
2931
3032 function buildCommitment ( params : {
@@ -395,6 +397,94 @@ describe("runHeartbeatOnce commitments", () => {
395397 } ) ;
396398 } ) ;
397399
400+ it ( "does not mark suppressed commitment sends as delivered or duplicate-dismiss their retry" , async ( ) => {
401+ await withTempHeartbeatSandbox ( async ( { tmpDir, storePath, replySpy } ) => {
402+ vi . stubEnv ( "OPENCLAW_STATE_DIR" , tmpDir ) ;
403+ const sessionKey = "agent:main:telegram:user-155462274" ;
404+ const cfg : OpenClawConfig = {
405+ agents : {
406+ defaults : {
407+ workspace : tmpDir ,
408+ heartbeat : {
409+ every : "5m" ,
410+ target : "last" ,
411+ } ,
412+ } ,
413+ } ,
414+ channels : { telegram : { allowFrom : [ "*" ] } } ,
415+ session : { store : storePath } ,
416+ commitments : { enabled : true } ,
417+ } ;
418+ await seedSessionStore ( storePath , sessionKey , {
419+ lastChannel : "telegram" ,
420+ lastProvider : "telegram" ,
421+ lastTo : "155462274" ,
422+ } ) ;
423+ await saveCommitmentStore ( undefined , {
424+ version : 1 ,
425+ commitments : [ buildCommitment ( { id : "cm_interview" , sessionKey, to : "155462274" } ) ] ,
426+ } ) ;
427+
428+ const sendTelegram = vi . fn ( ) . mockResolvedValue ( {
429+ messageId : "m2" ,
430+ chatId : "155462274" ,
431+ } ) ;
432+ replySpy
433+ . mockResolvedValueOnce ( { text : "No channel reply." } )
434+ . mockResolvedValueOnce ( { text : "How did the interview go?" } ) ;
435+
436+ const runOnce = async ( ) =>
437+ await runHeartbeatOnce ( {
438+ cfg,
439+ agentId : "main" ,
440+ sessionKey,
441+ deps : {
442+ getReplyFromConfig : replySpy ,
443+ telegram : sendTelegram ,
444+ getQueueSize : ( ) => 0 ,
445+ nowMs : ( ) => nowMs ,
446+ } ,
447+ } ) ;
448+
449+ const first = await runOnce ( ) ;
450+
451+ expect ( first . status ) . toBe ( "ran" ) ;
452+ expect ( sendTelegram ) . not . toHaveBeenCalled ( ) ;
453+ let store = await loadCommitmentStore ( ) ;
454+ expectCommitmentFields ( store . commitments [ 0 ] , {
455+ id : "cm_interview" ,
456+ status : "pending" ,
457+ attempts : 1 ,
458+ lastAttemptAtMs : nowMs ,
459+ } ) ;
460+ expect ( store . commitments [ 0 ] ?. sentAtMs ) . toBeUndefined ( ) ;
461+ const sessionStoreAfterSuppressed = JSON . parse (
462+ await fs . readFile ( storePath , "utf-8" ) ,
463+ ) as Record < string , { lastHeartbeatText ?: string ; lastHeartbeatSentAt ?: number } > ;
464+ expect ( sessionStoreAfterSuppressed [ sessionKey ] ?. lastHeartbeatText ) . toBeUndefined ( ) ;
465+ expect ( sessionStoreAfterSuppressed [ sessionKey ] ?. lastHeartbeatSentAt ) . toBeUndefined ( ) ;
466+ expect ( getLastHeartbeatEvent ( ) ) . toMatchObject ( {
467+ status : "skipped" ,
468+ reason : "no_visible_payload" ,
469+ } ) ;
470+
471+ const second = await runOnce ( ) ;
472+
473+ expect ( second . status ) . toBe ( "ran" ) ;
474+ expect ( sendTelegram ) . toHaveBeenCalledTimes ( 1 ) ;
475+ store = await loadCommitmentStore ( ) ;
476+ expectCommitmentFields ( store . commitments [ 0 ] , {
477+ id : "cm_interview" ,
478+ status : "sent" ,
479+ attempts : 2 ,
480+ sentAtMs : nowMs ,
481+ } ) ;
482+ expect ( getLastHeartbeatEvent ( ) ) . toMatchObject ( {
483+ status : "sent" ,
484+ } ) ;
485+ } ) ;
486+ } ) ;
487+
398488 it ( "tolerates Date-invalid commitment due timestamps in heartbeat prompts" , async ( ) => {
399489 const { result, sendTelegram, store } = await setupCommitmentCase ( {
400490 dueWindow : {
0 commit comments