Files
openclaw/src/gateway/server-cron-lazy.test.ts
Peter Steinberger 694ca50e97 Revert "refactor: move runtime state to SQLite"
This reverts commit f91de52f0d.
2026-05-13 13:33:38 +01:00

159 lines
5.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { CliDeps } from "../cli/deps.types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { CronServiceContract } from "../cron/service-contract.js";
import type { GatewayCronState } from "./server-cron.js";
const hoisted = vi.hoisted(() => {
let state: unknown;
return {
buildGatewayCronService: vi.fn(() => state),
setState(nextState: unknown) {
state = nextState;
},
};
});
vi.mock("./server-cron.js", () => ({
buildGatewayCronService: hoisted.buildGatewayCronService,
}));
const { createLazyGatewayCronState } = await import("./server-cron-lazy.js");
describe("createLazyGatewayCronState", () => {
beforeEach(() => {
vi.unstubAllEnvs();
hoisted.buildGatewayCronService.mockClear();
});
it("does not build the heavy cron service until an async cron operation needs it", async () => {
const cron = createCronService();
const state = createCronState(cron);
hoisted.setState(state);
const lazy = createLazyGatewayCronState(createParams());
expect(hoisted.buildGatewayCronService).not.toHaveBeenCalled();
expect(lazy.cron.getJob("demo")).toBeUndefined();
expect(lazy.cron.getDefaultAgentId()).toBeUndefined();
await lazy.cron.status();
expect(hoisted.buildGatewayCronService).toHaveBeenCalledTimes(1);
expect(cron.status).toHaveBeenCalledTimes(1);
});
it("loads the cron service for direct job reads", async () => {
const cron = createCronService();
hoisted.setState(createCronState(cron));
const lazy = createLazyGatewayCronState(createParams());
await lazy.cron.readJob("demo");
expect(hoisted.buildGatewayCronService).toHaveBeenCalledTimes(1);
expect(cron.readJob).toHaveBeenCalledWith("demo");
});
it("starts the loaded cron service once", async () => {
const cron = createCronService();
hoisted.setState(createCronState(cron));
const lazy = createLazyGatewayCronState(createParams());
await lazy.cron.start();
await lazy.cron.start();
expect(hoisted.buildGatewayCronService).toHaveBeenCalledTimes(1);
expect(cron.start).toHaveBeenCalledTimes(1);
});
it("does not start cron after stop wins the lazy startup race", async () => {
const cron = createCronService();
hoisted.setState(createCronState(cron));
const lazy = createLazyGatewayCronState(createParams());
const startPromise = lazy.cron.start();
lazy.cron.stop();
await startPromise;
expect(cron.start).not.toHaveBeenCalled();
expect(cron.stop).toHaveBeenCalledTimes(1);
});
it("allows a stopped loaded cron service to start again", async () => {
const cron = createCronService();
hoisted.setState(createCronState(cron));
const lazy = createLazyGatewayCronState(createParams());
await lazy.cron.start();
lazy.cron.stop();
await lazy.cron.start();
expect(hoisted.buildGatewayCronService).toHaveBeenCalledTimes(1);
expect(cron.stop).toHaveBeenCalledTimes(1);
expect(cron.start).toHaveBeenCalledTimes(2);
});
it("keeps synchronous wake non-blocking before the cron service is loaded", async () => {
const cron = createCronService();
hoisted.setState(createCronState(cron));
const lazy = createLazyGatewayCronState(createParams());
expect(lazy.cron.wake({ mode: "now", text: "ping" })).toEqual({ ok: false });
await vi.waitFor(() => {
expect(hoisted.buildGatewayCronService).toHaveBeenCalledTimes(1);
});
expect(cron.wake).not.toHaveBeenCalled();
});
it("preserves the startup cron enabled flag without loading cron runtime", () => {
vi.stubEnv("OPENCLAW_SKIP_CRON", "1");
const lazy = createLazyGatewayCronState(createParams());
expect(lazy.cronEnabled).toBe(false);
expect(hoisted.buildGatewayCronService).not.toHaveBeenCalled();
});
});
function createParams(overrides: Partial<OpenClawConfig> = {}) {
return {
cfg: {
...overrides,
} as OpenClawConfig,
deps: {} as CliDeps,
broadcast: vi.fn(),
};
}
function createCronState(cron: CronServiceContract): GatewayCronState {
return {
cron,
storePath: "/tmp/openclaw-cron.json",
cronEnabled: true,
} as GatewayCronState;
}
function createCronService(): CronServiceContract {
return {
start: vi.fn(async () => undefined),
stop: vi.fn(),
status: vi.fn(async () => ({ enabled: true }) as never),
list: vi.fn(async () => [] as never),
listPage: vi.fn(async () => ({ items: [], total: 0 }) as never),
add: vi.fn(async () => ({ ok: true }) as never),
update: vi.fn(async () => ({ ok: true }) as never),
remove: vi.fn(async () => ({ ok: true }) as never),
run: vi.fn(async () => ({ ok: true, ran: false, reason: "invalid-spec" }) as never),
enqueueRun: vi.fn(async () => ({ ok: true, ran: false, reason: "invalid-spec" }) as never),
getJob: vi.fn(() => undefined),
readJob: vi.fn(async () => undefined),
getDefaultAgentId: vi.fn(() => "default"),
wake: vi.fn(() => ({ ok: true })),
};
}