This commit is contained in:
Patrick Star 2026-01-30 14:09:07 +03:00 committed by GitHub
commit e2dac1a02c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 81 additions and 3 deletions

View File

@ -0,0 +1,74 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { CronService } from "./service.js";
const noopLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
async function makeStorePath() {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "moltbot-cron-"));
return {
storePath: path.join(dir, "cron", "jobs.json"),
cleanup: async () => {
await fs.rm(dir, { recursive: true, force: true });
},
};
}
describe("CronService", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2025-12-13T00:00:00.000Z"));
noopLogger.debug.mockClear();
noopLogger.info.mockClear();
noopLogger.warn.mockClear();
noopLogger.error.mockClear();
});
afterEach(() => {
vi.useRealTimers();
});
it("does not requestHeartbeatNow for main jobs when wakeMode is next-heartbeat", async () => {
const store = await makeStorePath();
const enqueueSystemEvent = vi.fn();
const requestHeartbeatNow = vi.fn();
const cron = new CronService({
storePath: store.storePath,
cronEnabled: true,
log: noopLogger,
enqueueSystemEvent,
requestHeartbeatNow,
runIsolatedAgentJob: vi.fn(async () => ({ status: "ok" as const })),
});
await cron.start();
const atMs = Date.parse("2025-12-13T00:00:02.000Z");
await cron.add({
name: "next-heartbeat main job",
enabled: true,
schedule: { kind: "at", atMs },
sessionTarget: "main",
wakeMode: "next-heartbeat",
payload: { kind: "systemEvent", text: "hello" },
});
vi.setSystemTime(new Date("2025-12-13T00:00:02.000Z"));
await vi.runOnlyPendingTimersAsync();
expect(enqueueSystemEvent).toHaveBeenCalledWith("hello", { agentId: undefined });
expect(requestHeartbeatNow).not.toHaveBeenCalled();
cron.stop();
await store.cleanup();
});
});

View File

@ -80,7 +80,8 @@ describe("CronService", () => {
await cronB.status();
expect(enqueueSystemEvent).toHaveBeenCalledTimes(1);
expect(requestHeartbeatNow).toHaveBeenCalledTimes(1);
// wakeMode=next-heartbeat should not force a heartbeat.
expect(requestHeartbeatNow).toHaveBeenCalledTimes(0);
cronA.stop();
cronB.stop();

View File

@ -183,8 +183,11 @@ export async function executeJob(
await finish("error", heartbeatResult.reason, text);
}
} else {
// wakeMode is "next-heartbeat" or runHeartbeatOnce not available
state.deps.requestHeartbeatNow({ reason: `cron:${job.id}` });
// If wakeMode is "now" but runHeartbeatOnce isn't available, request a heartbeat.
// If wakeMode is "next-heartbeat", do NOT force a heartbeat: that's the whole point.
if (job.wakeMode === "now") {
state.deps.requestHeartbeatNow({ reason: `cron:${job.id}` });
}
await finish("ok", undefined, text);
}
return;