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/
105 lines
3.2 KiB
JavaScript
105 lines
3.2 KiB
JavaScript
import { describe, expect, it } from "vitest";
|
|
import { renderHelp } from "../../../src/modules/util/help-command.js";
|
|
|
|
const noop = async () => {};
|
|
|
|
/**
|
|
* Build a synthetic registry directly — no loader involved. Lets us test
|
|
* the pure renderer with exact inputs.
|
|
*/
|
|
function makeRegistry(modules) {
|
|
const publicCommands = new Map();
|
|
const protectedCommands = new Map();
|
|
const privateCommands = new Map();
|
|
const allCommands = new Map();
|
|
|
|
for (const mod of modules) {
|
|
for (const cmd of mod.commands) {
|
|
const entry = { module: mod, cmd, visibility: cmd.visibility };
|
|
allCommands.set(cmd.name, entry);
|
|
if (cmd.visibility === "public") publicCommands.set(cmd.name, entry);
|
|
else if (cmd.visibility === "protected") protectedCommands.set(cmd.name, entry);
|
|
else privateCommands.set(cmd.name, entry);
|
|
}
|
|
}
|
|
|
|
return {
|
|
publicCommands,
|
|
protectedCommands,
|
|
privateCommands,
|
|
allCommands,
|
|
modules,
|
|
};
|
|
}
|
|
|
|
const cmd = (name, visibility, description) => ({
|
|
name,
|
|
visibility,
|
|
description,
|
|
handler: noop,
|
|
});
|
|
|
|
describe("renderHelp", () => {
|
|
it("groups commands by module in env.MODULES order", () => {
|
|
const reg = makeRegistry([
|
|
{ name: "a", commands: [cmd("one", "public", "A-one")] },
|
|
{ name: "b", commands: [cmd("two", "public", "B-two")] },
|
|
]);
|
|
const out = renderHelp(reg);
|
|
const aIdx = out.indexOf("<b>a</b>");
|
|
const bIdx = out.indexOf("<b>b</b>");
|
|
expect(aIdx).toBeGreaterThanOrEqual(0);
|
|
expect(bIdx).toBeGreaterThanOrEqual(0);
|
|
expect(aIdx).toBeLessThan(bIdx);
|
|
});
|
|
|
|
it("appends (protected) suffix to protected commands", () => {
|
|
const reg = makeRegistry([{ name: "a", commands: [cmd("admin", "protected", "Admin tool")] }]);
|
|
const out = renderHelp(reg);
|
|
expect(out).toContain("/admin — Admin tool (protected)");
|
|
});
|
|
|
|
it("hides modules whose only commands are private", () => {
|
|
const reg = makeRegistry([
|
|
{ name: "a", commands: [cmd("visible", "public", "V")] },
|
|
{ name: "b", commands: [cmd("hidden", "private", "H")] },
|
|
]);
|
|
const out = renderHelp(reg);
|
|
expect(out).toContain("<b>a</b>");
|
|
expect(out).not.toContain("<b>b</b>");
|
|
expect(out).not.toContain("hidden");
|
|
});
|
|
|
|
it("NEVER leaks private command names into output", () => {
|
|
const reg = makeRegistry([
|
|
{
|
|
name: "a",
|
|
commands: [cmd("show", "public", "Public"), cmd("secret", "private", "Secret")],
|
|
},
|
|
]);
|
|
const out = renderHelp(reg);
|
|
expect(out).toContain("/show");
|
|
expect(out).not.toContain("/secret");
|
|
expect(out).not.toContain("Secret");
|
|
});
|
|
|
|
it("HTML-escapes module name and description", () => {
|
|
const reg = makeRegistry([
|
|
{
|
|
name: "a&b",
|
|
commands: [cmd("foo", "public", "runs <script>")],
|
|
},
|
|
]);
|
|
const out = renderHelp(reg);
|
|
expect(out).toContain("<b>a&b</b>");
|
|
expect(out).toContain("runs <script>");
|
|
// The literal unescaped sequence must NOT appear in output.
|
|
expect(out).not.toContain("<script>");
|
|
});
|
|
|
|
it("returns a placeholder when no commands are visible", () => {
|
|
const reg = makeRegistry([{ name: "a", commands: [cmd("hidden", "private", "H")] }]);
|
|
expect(renderHelp(reg)).toBe("no commands registered");
|
|
});
|
|
});
|