mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-04-17 15:20:58 +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
221 lines
8.2 KiB
JavaScript
221 lines
8.2 KiB
JavaScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
buildRegistry,
|
|
getCurrentRegistry,
|
|
loadModules,
|
|
resetRegistry,
|
|
} from "../../src/modules/registry.js";
|
|
import { makeFakeKv } from "../fakes/fake-kv-namespace.js";
|
|
import { makeCommand, makeFakeImportMap, makeModule } from "../fakes/fake-modules.js";
|
|
|
|
const makeEnv = (modules) => ({ MODULES: modules, KV: makeFakeKv() });
|
|
|
|
describe("registry", () => {
|
|
beforeEach(() => resetRegistry());
|
|
|
|
describe("loadModules", () => {
|
|
it("loads modules listed in env.MODULES", async () => {
|
|
const map = makeFakeImportMap({
|
|
a: makeModule("a", [makeCommand("foo", "public")]),
|
|
b: makeModule("b", [makeCommand("bar", "public")]),
|
|
});
|
|
const mods = await loadModules({ MODULES: "a,b" }, map);
|
|
expect(mods.map((m) => m.name)).toEqual(["a", "b"]);
|
|
});
|
|
|
|
it("trims, dedupes, skips empty", async () => {
|
|
const map = makeFakeImportMap({
|
|
a: makeModule("a", [makeCommand("foo", "public")]),
|
|
});
|
|
const mods = await loadModules({ MODULES: " a , a , " }, map);
|
|
expect(mods).toHaveLength(1);
|
|
});
|
|
|
|
it("throws on empty MODULES", async () => {
|
|
await expect(loadModules({ MODULES: "" }, {})).rejects.toThrow(/empty/);
|
|
await expect(loadModules({}, {})).rejects.toThrow(/empty/);
|
|
});
|
|
|
|
it("throws on unknown module name", async () => {
|
|
await expect(loadModules({ MODULES: "ghost" }, {})).rejects.toThrow(/unknown module: ghost/);
|
|
});
|
|
|
|
it("throws when default export name mismatches key", async () => {
|
|
const map = makeFakeImportMap({ a: makeModule("b", []) });
|
|
await expect(loadModules({ MODULES: "a" }, map)).rejects.toThrow(/mismatched name/);
|
|
});
|
|
|
|
it("throws when module has no commands array", async () => {
|
|
const map = {
|
|
a: async () => ({ default: { name: "a" } }),
|
|
};
|
|
await expect(loadModules({ MODULES: "a" }, map)).rejects.toThrow(/commands array/);
|
|
});
|
|
|
|
it("runs validateCommand on each command", async () => {
|
|
const badMap = makeFakeImportMap({
|
|
a: makeModule("a", [
|
|
{ name: "/bad", visibility: "public", description: "x", handler: async () => {} },
|
|
]),
|
|
});
|
|
await expect(loadModules({ MODULES: "a" }, badMap)).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("buildRegistry", () => {
|
|
it("partitions commands across visibility maps + flat allCommands", async () => {
|
|
const map = makeFakeImportMap({
|
|
a: makeModule("a", [
|
|
makeCommand("pub", "public"),
|
|
makeCommand("prot", "protected"),
|
|
makeCommand("priv", "private"),
|
|
]),
|
|
});
|
|
const reg = await buildRegistry(makeEnv("a"), map);
|
|
expect([...reg.publicCommands.keys()]).toEqual(["pub"]);
|
|
expect([...reg.protectedCommands.keys()]).toEqual(["prot"]);
|
|
expect([...reg.privateCommands.keys()]).toEqual(["priv"]);
|
|
expect([...reg.allCommands.keys()].sort()).toEqual(["priv", "prot", "pub"]);
|
|
});
|
|
|
|
it("throws on same-visibility conflict", async () => {
|
|
const map = makeFakeImportMap({
|
|
a: makeModule("a", [makeCommand("foo", "public")]),
|
|
b: makeModule("b", [makeCommand("foo", "public")]),
|
|
});
|
|
await expect(buildRegistry(makeEnv("a,b"), map)).rejects.toThrow(/conflict.*foo.*a.*b/);
|
|
});
|
|
|
|
it("throws on CROSS-visibility conflict (unified namespace)", async () => {
|
|
const map = makeFakeImportMap({
|
|
a: makeModule("a", [makeCommand("foo", "public")]),
|
|
b: makeModule("b", [makeCommand("foo", "private")]),
|
|
});
|
|
await expect(buildRegistry(makeEnv("a,b"), map)).rejects.toThrow(/conflict.*foo/);
|
|
});
|
|
|
|
it("calls init({db, env}) exactly once per module, in order", async () => {
|
|
const calls = [];
|
|
const makeInit =
|
|
(name) =>
|
|
async ({ db, env }) => {
|
|
calls.push({ name, hasDb: !!db, hasEnv: !!env });
|
|
await db.put("sentinel", "x");
|
|
};
|
|
const map = makeFakeImportMap({
|
|
a: makeModule("a", [makeCommand("one", "public")], makeInit("a")),
|
|
b: makeModule("b", [makeCommand("two", "public")], makeInit("b")),
|
|
});
|
|
const env = makeEnv("a,b");
|
|
await buildRegistry(env, map);
|
|
expect(calls).toEqual([
|
|
{ name: "a", hasDb: true, hasEnv: true },
|
|
{ name: "b", hasDb: true, hasEnv: true },
|
|
]);
|
|
// Assert prefixing: keys landed as "a:sentinel" and "b:sentinel".
|
|
expect(env.KV.store.get("a:sentinel")).toBe("x");
|
|
expect(env.KV.store.get("b:sentinel")).toBe("x");
|
|
});
|
|
|
|
it("wraps init errors with module name", async () => {
|
|
const map = makeFakeImportMap({
|
|
a: makeModule("a", [makeCommand("foo", "public")], async () => {
|
|
throw new Error("boom");
|
|
}),
|
|
});
|
|
await expect(buildRegistry(makeEnv("a"), map)).rejects.toThrow(
|
|
/module "a" init failed.*boom/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("cron collection", () => {
|
|
const makeCron = (name, schedule = "0 1 * * *") => ({
|
|
name,
|
|
schedule,
|
|
handler: async () => {},
|
|
});
|
|
|
|
it("registry.crons is empty when no modules declare crons", async () => {
|
|
const map = makeFakeImportMap({ a: makeModule("a", [makeCommand("foo", "public")]) });
|
|
const reg = await buildRegistry(makeEnv("a"), map);
|
|
expect(reg.crons).toEqual([]);
|
|
});
|
|
|
|
it("collects crons from modules that declare them", async () => {
|
|
const modWithCron = {
|
|
...makeModule("a", [makeCommand("foo", "public")]),
|
|
crons: [makeCron("nightly")],
|
|
};
|
|
const map = makeFakeImportMap({ a: modWithCron });
|
|
const reg = await buildRegistry(makeEnv("a"), map);
|
|
expect(reg.crons).toHaveLength(1);
|
|
expect(reg.crons[0].name).toBe("nightly");
|
|
expect(reg.crons[0].schedule).toBe("0 1 * * *");
|
|
expect(reg.crons[0].module.name).toBe("a");
|
|
});
|
|
|
|
it("fan-out: two modules with same schedule both appear in registry.crons", async () => {
|
|
const modA = {
|
|
...makeModule("a", [makeCommand("ca", "public")]),
|
|
crons: [makeCron("tick", "*/5 * * * *")],
|
|
};
|
|
const modB = {
|
|
...makeModule("b", [makeCommand("cb", "public")]),
|
|
crons: [makeCron("tick", "*/5 * * * *")],
|
|
};
|
|
const map = makeFakeImportMap({ a: modA, b: modB });
|
|
const reg = await buildRegistry(makeEnv("a,b"), map);
|
|
expect(reg.crons).toHaveLength(2);
|
|
expect(reg.crons.map((c) => c.module.name).sort()).toEqual(["a", "b"]);
|
|
});
|
|
|
|
it("throws on duplicate cron name within the same module", async () => {
|
|
const mod = {
|
|
...makeModule("a", [makeCommand("foo", "public")]),
|
|
crons: [makeCron("dup"), makeCron("dup")],
|
|
};
|
|
const map = makeFakeImportMap({ a: mod });
|
|
await expect(buildRegistry(makeEnv("a"), map)).rejects.toThrow(/duplicate cron name "dup"/);
|
|
});
|
|
|
|
it("throws when crons is not an array", async () => {
|
|
const mod = { ...makeModule("a", [makeCommand("foo", "public")]), crons: "bad" };
|
|
const map = makeFakeImportMap({ a: mod });
|
|
await expect(buildRegistry(makeEnv("a"), map)).rejects.toThrow(/crons must be an array/);
|
|
});
|
|
|
|
it("throws when a cron entry fails validation", async () => {
|
|
const mod = {
|
|
...makeModule("a", [makeCommand("foo", "public")]),
|
|
crons: [{ name: "bad!", schedule: "0 1 * * *", handler: async () => {} }],
|
|
};
|
|
const map = makeFakeImportMap({ a: mod });
|
|
await expect(buildRegistry(makeEnv("a"), map)).rejects.toThrow(/name must match/);
|
|
});
|
|
});
|
|
|
|
describe("getCurrentRegistry / resetRegistry", () => {
|
|
it("getCurrentRegistry throws before build", () => {
|
|
expect(() => getCurrentRegistry()).toThrow(/not built/);
|
|
});
|
|
|
|
it("getCurrentRegistry returns the same instance after build", async () => {
|
|
const map = makeFakeImportMap({
|
|
a: makeModule("a", [makeCommand("foo", "public")]),
|
|
});
|
|
const reg = await buildRegistry(makeEnv("a"), map);
|
|
expect(getCurrentRegistry()).toBe(reg);
|
|
});
|
|
|
|
it("resetRegistry clears the memoized registry", async () => {
|
|
const map = makeFakeImportMap({
|
|
a: makeModule("a", [makeCommand("foo", "public")]),
|
|
});
|
|
await buildRegistry(makeEnv("a"), map);
|
|
resetRegistry();
|
|
expect(() => getCurrentRegistry()).toThrow(/not built/);
|
|
});
|
|
});
|
|
});
|