mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-04-17 19:22:09 +00:00
- modules may declare crons: [{ schedule, name, handler }]
- handler signature (event, { db, sql, env }) matches init context
- scheduled() export in src/index.js dispatches to matching handlers with fan-out and per-handler error isolation
- registry validates cron entries and collects into registry.crons
- wrangler.toml [triggers] crons must still be populated manually by module author
127 lines
4.2 KiB
JavaScript
127 lines
4.2 KiB
JavaScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { dispatchScheduled } from "../../src/modules/cron-dispatcher.js";
|
|
import { makeFakeKv } from "../fakes/fake-kv-namespace.js";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeFakeRegistry(entries) {
|
|
return { crons: entries };
|
|
}
|
|
|
|
function makeModule(name) {
|
|
return { name };
|
|
}
|
|
|
|
function makeCronEntry(moduleName, schedule, name, handler) {
|
|
return { module: makeModule(moduleName), schedule, name, handler };
|
|
}
|
|
|
|
function makeFakeCtx() {
|
|
const promises = [];
|
|
return {
|
|
ctx: { waitUntil: (p) => promises.push(p) },
|
|
flush: () => Promise.all(promises),
|
|
};
|
|
}
|
|
|
|
function makeFakeEnv() {
|
|
return { KV: makeFakeKv(), DB: null, MODULES: "test" };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("dispatchScheduled", () => {
|
|
it("calls handler for matching schedule", async () => {
|
|
const called = [];
|
|
const handler = vi.fn(async () => called.push("ran"));
|
|
const reg = makeFakeRegistry([makeCronEntry("mod", "0 1 * * *", "nightly", handler)]);
|
|
const { ctx, flush } = makeFakeCtx();
|
|
|
|
dispatchScheduled({ cron: "0 1 * * *" }, makeFakeEnv(), ctx, reg);
|
|
await flush();
|
|
|
|
expect(handler).toHaveBeenCalledOnce();
|
|
expect(called).toEqual(["ran"]);
|
|
});
|
|
|
|
it("does NOT call handler when schedule does not match", async () => {
|
|
const handler = vi.fn();
|
|
const reg = makeFakeRegistry([makeCronEntry("mod", "0 2 * * *", "other", handler)]);
|
|
const { ctx, flush } = makeFakeCtx();
|
|
|
|
dispatchScheduled({ cron: "0 1 * * *" }, makeFakeEnv(), ctx, reg);
|
|
await flush();
|
|
|
|
expect(handler).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fan-out: two modules sharing same schedule both fire", async () => {
|
|
const callLog = [];
|
|
const handlerA = async () => callLog.push("a");
|
|
const handlerB = async () => callLog.push("b");
|
|
const reg = makeFakeRegistry([
|
|
makeCronEntry("mod-a", "*/5 * * * *", "tick-a", handlerA),
|
|
makeCronEntry("mod-b", "*/5 * * * *", "tick-b", handlerB),
|
|
]);
|
|
const { ctx, flush } = makeFakeCtx();
|
|
|
|
dispatchScheduled({ cron: "*/5 * * * *" }, makeFakeEnv(), ctx, reg);
|
|
await flush();
|
|
|
|
expect(callLog.sort()).toEqual(["a", "b"]);
|
|
});
|
|
|
|
it("error isolation: one handler throwing does not prevent others", async () => {
|
|
const callLog = [];
|
|
const failing = async () => {
|
|
throw new Error("boom");
|
|
};
|
|
const surviving = async () => callLog.push("survived");
|
|
const reg = makeFakeRegistry([
|
|
makeCronEntry("mod-a", "0 0 * * *", "fail", failing),
|
|
makeCronEntry("mod-b", "0 0 * * *", "ok", surviving),
|
|
]);
|
|
const { ctx, flush } = makeFakeCtx();
|
|
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
|
|
dispatchScheduled({ cron: "0 0 * * *" }, makeFakeEnv(), ctx, reg);
|
|
await flush();
|
|
|
|
expect(callLog).toEqual(["survived"]);
|
|
expect(consoleSpy).toHaveBeenCalledOnce();
|
|
expect(consoleSpy.mock.calls[0][0]).toMatch(/cron.*fail.*mod-a/);
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it("passes event and { db, sql, env } to handler", async () => {
|
|
let received;
|
|
const handler = async (event, handlerCtx) => {
|
|
received = { event, handlerCtx };
|
|
};
|
|
const env = makeFakeEnv();
|
|
const reg = makeFakeRegistry([makeCronEntry("mod", "0 3 * * *", "ctx-check", handler)]);
|
|
const { ctx, flush } = makeFakeCtx();
|
|
const fakeEvent = { cron: "0 3 * * *", scheduledTime: 123 };
|
|
|
|
dispatchScheduled(fakeEvent, env, ctx, reg);
|
|
await flush();
|
|
|
|
expect(received.event).toBe(fakeEvent);
|
|
expect(typeof received.handlerCtx.db).toBe("object");
|
|
expect(typeof received.handlerCtx.sql).toBe("object");
|
|
expect(received.handlerCtx.env).toBe(env);
|
|
});
|
|
|
|
it("no-op when registry has no cron entries", async () => {
|
|
const reg = makeFakeRegistry([]);
|
|
const { ctx, flush } = makeFakeCtx();
|
|
// Should not throw.
|
|
expect(() => dispatchScheduled({ cron: "0 1 * * *" }, makeFakeEnv(), ctx, reg)).not.toThrow();
|
|
await flush();
|
|
});
|
|
});
|