mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 20:58:26 +00:00
fix: prevent heartbeat scheduler death when runOnce throws (#14901)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 022efbfef9
Co-authored-by: joeykrug <5925937+joeykrug@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
98
src/infra/heartbeat-wake.test.ts
Normal file
98
src/infra/heartbeat-wake.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
async function loadWakeModule() {
|
||||
vi.resetModules();
|
||||
return import("./heartbeat-wake.js");
|
||||
}
|
||||
|
||||
describe("heartbeat-wake", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("coalesces multiple wake requests into one run", async () => {
|
||||
vi.useFakeTimers();
|
||||
const wake = await loadWakeModule();
|
||||
const handler = vi.fn().mockResolvedValue({ status: "skipped", reason: "disabled" });
|
||||
wake.setHeartbeatWakeHandler(handler);
|
||||
|
||||
wake.requestHeartbeatNow({ reason: "interval", coalesceMs: 200 });
|
||||
wake.requestHeartbeatNow({ reason: "exec-event", coalesceMs: 200 });
|
||||
wake.requestHeartbeatNow({ reason: "retry", coalesceMs: 200 });
|
||||
|
||||
expect(wake.hasPendingHeartbeatWake()).toBe(true);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(199);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith({ reason: "retry" });
|
||||
expect(wake.hasPendingHeartbeatWake()).toBe(false);
|
||||
});
|
||||
|
||||
it("retries requests-in-flight after the default retry delay", async () => {
|
||||
vi.useFakeTimers();
|
||||
const wake = await loadWakeModule();
|
||||
const handler = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({ status: "skipped", reason: "requests-in-flight" })
|
||||
.mockResolvedValueOnce({ status: "ran", durationMs: 1 });
|
||||
wake.setHeartbeatWakeHandler(handler);
|
||||
|
||||
wake.requestHeartbeatNow({ reason: "interval", coalesceMs: 0 });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
expect(handler).toHaveBeenCalledTimes(2);
|
||||
expect(handler.mock.calls[1]?.[0]).toEqual({ reason: "interval" });
|
||||
});
|
||||
|
||||
it("retries thrown handler errors after the default retry delay", async () => {
|
||||
vi.useFakeTimers();
|
||||
const wake = await loadWakeModule();
|
||||
const handler = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error("boom"))
|
||||
.mockResolvedValueOnce({ status: "skipped", reason: "disabled" });
|
||||
wake.setHeartbeatWakeHandler(handler);
|
||||
|
||||
wake.requestHeartbeatNow({ reason: "exec-event", coalesceMs: 0 });
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
expect(handler).toHaveBeenCalledTimes(2);
|
||||
expect(handler.mock.calls[1]?.[0]).toEqual({ reason: "exec-event" });
|
||||
});
|
||||
|
||||
it("drains pending wake once a handler is registered", async () => {
|
||||
vi.useFakeTimers();
|
||||
const wake = await loadWakeModule();
|
||||
|
||||
wake.requestHeartbeatNow({ reason: "manual", coalesceMs: 0 });
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(wake.hasPendingHeartbeatWake()).toBe(true);
|
||||
|
||||
const handler = vi.fn().mockResolvedValue({ status: "skipped", reason: "disabled" });
|
||||
wake.setHeartbeatWakeHandler(handler);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(249);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler).toHaveBeenCalledWith({ reason: "manual" });
|
||||
expect(wake.hasPendingHeartbeatWake()).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user