From ed1744bcaacba502ad06e1bad5d24a09f5cbd2a4 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 12 Apr 2026 17:55:09 +0100 Subject: [PATCH] test(heartbeat): cover isolated cron event consumption --- ...beat-runner.isolated-key-stability.test.ts | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/infra/heartbeat-runner.isolated-key-stability.test.ts b/src/infra/heartbeat-runner.isolated-key-stability.test.ts index 84ac3483057..7aa4b8e1e36 100644 --- a/src/infra/heartbeat-runner.isolated-key-stability.test.ts +++ b/src/infra/heartbeat-runner.isolated-key-stability.test.ts @@ -5,7 +5,11 @@ import type { OpenClawConfig } from "../config/config.js"; import { resolveMainSessionKey } from "../config/sessions.js"; import { runHeartbeatOnce } from "./heartbeat-runner.js"; import { seedSessionStore, withTempHeartbeatSandbox } from "./heartbeat-runner.test-utils.js"; -import { resetSystemEventsForTest } from "./system-events.js"; +import { + enqueueSystemEvent, + peekSystemEventEntries, + resetSystemEventsForTest, +} from "./system-events.js"; vi.mock("./outbound/deliver.js", () => ({ deliverOutboundPayloads: vi.fn().mockResolvedValue(undefined), @@ -197,6 +201,62 @@ describe("runHeartbeatOnce – isolated session key stability (#59493)", () => { }); }); + it("consumes base-session cron events when isolated heartbeat runs on a :heartbeat session", async () => { + await withTempHeartbeatSandbox(async ({ tmpDir, storePath }) => { + const cfg = makeIsolatedHeartbeatConfig(tmpDir, storePath); + const baseSessionKey = resolveMainSessionKey(cfg); + const replySpy = vi.spyOn(replyModule, "getReplyFromConfig"); + replySpy + .mockResolvedValueOnce({ text: "Relay this cron update now" }) + .mockResolvedValueOnce({ text: "HEARTBEAT_OK" }); + + enqueueSystemEvent("Cron: QMD maintenance completed", { + sessionKey: baseSessionKey, + contextKey: "cron:qmd-maintenance", + }); + + await runHeartbeatOnce({ + cfg, + agentId: "main", + reason: "interval", + deps: { + getQueueSize: () => 0, + nowMs: () => 0, + }, + }); + + expect(peekSystemEventEntries(baseSessionKey)).toEqual([]); + + await runHeartbeatOnce({ + cfg, + agentId: "main", + reason: "interval", + deps: { + getQueueSize: () => 0, + nowMs: () => 0, + }, + }); + + expect(replySpy).toHaveBeenCalledTimes(2); + const firstCtx = replySpy.mock.calls[0]?.[0] as { + Body?: string; + Provider?: string; + SessionKey?: string; + }; + const secondCtx = replySpy.mock.calls[1]?.[0] as { + Body?: string; + Provider?: string; + SessionKey?: string; + }; + + expect(firstCtx.SessionKey).toBe(`${baseSessionKey}:heartbeat`); + expect(firstCtx.Provider).toBe("cron-event"); + expect(firstCtx.Body).toContain("Cron: QMD maintenance completed"); + expect(secondCtx.SessionKey).toBe(`${baseSessionKey}:heartbeat`); + expect(secondCtx.Body).not.toContain("Cron: QMD maintenance completed"); + }); + }); + it("stays stable for wake re-entry when the configured base key already ends with :heartbeat", async () => { await withTempHeartbeatSandbox(async ({ tmpDir, storePath }) => { const cfg = makeNamedIsolatedHeartbeatConfig(tmpDir, storePath, "alerts:heartbeat");