mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-04-17 13:21:31 +00:00
grammY-based bot with a module plugin system loaded from the MODULES env var. Three command visibility levels (public/protected/private) share a unified command namespace with conflict detection at registry build. - 4 initial modules (util, wordle, loldle, misc); util fully implemented, others are stubs proving the plugin system end-to-end - util: /info (chat/thread/sender ids) + /help (pure renderer over the registry, HTML parse mode, escapes user-influenced strings) - KVStore interface with CFKVStore and a per-module prefixing factory; getJSON/putJSON convenience helpers; other backends drop in via one file - Webhook at POST /webhook with secret-token validation via grammY's webhookCallback; no admin HTTP surface - Post-deploy register script (npm run deploy = wrangler deploy && node --env-file=.env.deploy scripts/register.js) for setWebhook and setMyCommands; --dry-run flag for preview - 56 vitest unit tests across 7 suites covering registry, db wrapper, dispatcher, help renderer, validators, and HTML escaper - biome for lint + format; phased implementation plan under plans/
86 lines
2.9 KiB
JavaScript
86 lines
2.9 KiB
JavaScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { CFKVStore } from "../../src/db/cf-kv-store.js";
|
|
import { makeFakeKv } from "../fakes/fake-kv-namespace.js";
|
|
|
|
describe("CFKVStore", () => {
|
|
let kv;
|
|
let store;
|
|
|
|
beforeEach(() => {
|
|
kv = makeFakeKv();
|
|
store = new CFKVStore(kv);
|
|
});
|
|
|
|
it("throws when given no binding", () => {
|
|
expect(() => new CFKVStore(null)).toThrow(/required/);
|
|
});
|
|
|
|
it("round-trips get/put/delete", async () => {
|
|
expect(await store.get("k")).toBeNull();
|
|
await store.put("k", "v");
|
|
expect(await store.get("k")).toBe("v");
|
|
await store.delete("k");
|
|
expect(await store.get("k")).toBeNull();
|
|
});
|
|
|
|
it("list() returns normalized shape with module keys stripped of wrappers", async () => {
|
|
await store.put("a:1", "x");
|
|
await store.put("a:2", "y");
|
|
await store.put("b:1", "z");
|
|
const res = await store.list({ prefix: "a:" });
|
|
expect(res.keys.sort()).toEqual(["a:1", "a:2"]);
|
|
expect(res.done).toBe(true);
|
|
});
|
|
|
|
it("list() pagination — cursor propagates through", async () => {
|
|
for (let i = 0; i < 5; i++) await store.put(`k${i}`, String(i));
|
|
const page1 = await store.list({ limit: 2 });
|
|
expect(page1.keys.length).toBe(2);
|
|
expect(page1.done).toBe(false);
|
|
expect(page1.cursor).toBeTruthy();
|
|
const page2 = await store.list({ limit: 2, cursor: page1.cursor });
|
|
expect(page2.keys.length).toBe(2);
|
|
expect(page2.done).toBe(false);
|
|
const page3 = await store.list({ limit: 2, cursor: page2.cursor });
|
|
expect(page3.keys.length).toBe(1);
|
|
expect(page3.done).toBe(true);
|
|
expect(page3.cursor).toBeUndefined();
|
|
});
|
|
|
|
it("put() with expirationTtl passes through to binding", async () => {
|
|
await store.put("k", "v", { expirationTtl: 60 });
|
|
expect(kv.putLog).toEqual([{ key: "k", value: "v", opts: { expirationTtl: 60 } }]);
|
|
});
|
|
|
|
it("put() without options does NOT pass an options object", async () => {
|
|
await store.put("k", "v");
|
|
expect(kv.putLog[0]).toEqual({ key: "k", value: "v", opts: undefined });
|
|
});
|
|
|
|
it("getJSON/putJSON round-trip", async () => {
|
|
await store.putJSON("k", { a: 1, b: [2, 3] });
|
|
expect(await store.getJSON("k")).toEqual({ a: 1, b: [2, 3] });
|
|
});
|
|
|
|
it("getJSON returns null on missing key", async () => {
|
|
expect(await store.getJSON("missing")).toBeNull();
|
|
});
|
|
|
|
it("getJSON returns null on corrupt JSON and logs a warning", async () => {
|
|
kv.store.set("bad", "{not json");
|
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
expect(await store.getJSON("bad")).toBeNull();
|
|
expect(warn).toHaveBeenCalled();
|
|
warn.mockRestore();
|
|
});
|
|
|
|
it("putJSON throws on undefined value", async () => {
|
|
await expect(store.putJSON("k", undefined)).rejects.toThrow(/undefined/);
|
|
});
|
|
|
|
it("putJSON passes expirationTtl through", async () => {
|
|
await store.putJSON("k", { a: 1 }, { expirationTtl: 120 });
|
|
expect(kv.putLog[0].opts).toEqual({ expirationTtl: 120 });
|
|
});
|
|
});
|