@@ -41,6 +41,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
4141 storePath : string ;
4242 target ?: "telegram" | "none" ;
4343 isolatedSession ?: boolean ;
44+ activeHours ?: boolean ;
4445 } ) : Promise < { cfg : OpenClawConfig ; sessionKey : string } > => {
4546 const cfg : OpenClawConfig = {
4647 agents : {
@@ -50,6 +51,9 @@ describe("Ghost reminder bug (issue #13317)", () => {
5051 every : "5m" ,
5152 target : params . target ?? "telegram" ,
5253 ...( params . isolatedSession === true ? { isolatedSession : true } : { } ) ,
54+ ...( params . activeHours === true
55+ ? { activeHours : { start : "08:00" , end : "24:00" , timezone : "user" as const } }
56+ : { } ) ,
5357 } ,
5458 } ,
5559 } ,
@@ -196,6 +200,8 @@ describe("Ghost reminder bug (issue #13317)", () => {
196200 owningCronLaneTaskMarker ?: CommandLaneTaskMarker ;
197201 cronLaneDepth ?: number ;
198202 cronNestedLaneDepth ?: number ;
203+ activeHours ?: boolean ;
204+ nowMs ?: number ;
199205 } ) : Promise < {
200206 result : Awaited < ReturnType < typeof runHeartbeatOnce > > ;
201207 sendTelegram : ReturnType < typeof vi . fn > ;
@@ -215,6 +221,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
215221 storePath,
216222 target : params . target ,
217223 isolatedSession : params . isolatedSession ,
224+ activeHours : params . activeHours ,
218225 } ) ;
219226 params . enqueue ( sessionKey ) ;
220227 const owningCronJobMarker = params . owningCronJobId
@@ -244,6 +251,7 @@ describe("Ghost reminder bug (issue #13317)", () => {
244251 deps : {
245252 getReplyFromConfig : getReplySpy ,
246253 telegram : sendTelegram ,
254+ nowMs : ( ) => params . nowMs ?? Date . now ( ) ,
247255 ...( params . cronLaneDepth === undefined && params . cronNestedLaneDepth === undefined
248256 ? { }
249257 : {
@@ -310,6 +318,30 @@ describe("Ghost reminder bug (issue #13317)", () => {
310318 expect ( sendTelegram ) . toHaveBeenCalled ( ) ;
311319 } ) ;
312320
321+ it ( "runs the tagged cron payload outside heartbeat active hours" , async ( ) => {
322+ const reminderText = "Reminder: Send the overnight report" ;
323+ const { result, sendTelegram, calledCtx, replyCallCount } = await runHeartbeatCase ( {
324+ tmpPrefix : "openclaw-cron-quiet-hours-" ,
325+ replyText : "Overnight report sent" ,
326+ reason : "cron:overnight-report" ,
327+ source : "cron" ,
328+ intent : "immediate" ,
329+ activeHours : true ,
330+ nowMs : Date . UTC ( 2025 , 0 , 1 , 7 , 0 , 0 ) ,
331+ enqueue : ( sessionKey ) => {
332+ enqueueSystemEvent ( reminderText , {
333+ sessionKey,
334+ contextKey : "cron:overnight-report" ,
335+ } ) ;
336+ } ,
337+ } ) ;
338+
339+ expect ( result . status ) . toBe ( "ran" ) ;
340+ expect ( replyCallCount ) . toBe ( 1 ) ;
341+ expectCronEventPrompt ( calledCtx , reminderText ) ;
342+ expect ( sendTelegram ) . toHaveBeenCalled ( ) ;
343+ } ) ;
344+
313345 it ( "uses CRON_EVENT_PROMPT when cron events are mixed with heartbeat noise" , async ( ) => {
314346 const { result, sendTelegram, calledCtx } = await runCronReminderCase (
315347 "openclaw-cron-mixed-" ,
0 commit comments