import { Command } from "commander"; import { describe, expect, it, vi } from "vitest"; import { createMessageCliHelpers } from "./helpers.js"; import { registerMessageDiscordAdminCommands } from "./register.discord-admin.js"; type AdminCase = { command: string; action: string; required: Record; optional?: Record; }; const cases: AdminCase[] = [ { command: "role info", action: "role-info", required: { guildId: "guild-1" } }, { command: "role add", action: "role-add", required: { guildId: "guild-1", userId: "user-1", roleId: "role-1" }, }, { command: "role remove", action: "role-remove", required: { guildId: "guild-1", userId: "user-1", roleId: "role-1" }, }, { command: "channel info", action: "channel-info", required: { target: "channel:123" } }, { command: "channel list", action: "channel-list", required: { guildId: "guild-1" } }, { command: "member info", action: "member-info", required: { userId: "user-1" }, optional: { guildId: "guild-1", channelId: "channel-1" }, }, { command: "voice status", action: "voice-status", required: { guildId: "guild-1", userId: "user-1" }, }, { command: "event list", action: "event-list", required: { guildId: "guild-1" } }, { command: "event create", action: "event-create", required: { guildId: "guild-1", eventName: "QA event", startTime: "2026-09-11T12:00:00Z" }, optional: { endTime: "2026-09-11T13:00:00Z", desc: "Event description", channelId: "channel-1", location: "QA room", eventType: "external", image: "https://example.com/event.png", }, }, { command: "timeout", action: "timeout", required: { guildId: "guild-1", userId: "user-1" }, optional: { durationMin: "0", until: "2026-09-11T13:00:00Z", reason: "QA reason" }, }, { command: "kick", action: "kick", required: { guildId: "guild-1", userId: "user-1" }, optional: { reason: "QA reason" }, }, { command: "ban", action: "ban", required: { guildId: "guild-1", userId: "user-1" }, optional: { reason: "QA reason", deleteDays: "0" }, }, ]; function flag(key: string) { return "--" + key.replace(/[A-Z]/g, (letter) => "-" + letter.toLowerCase()); } function argumentsFor(options: Record) { return Object.entries(options).flatMap(([key, value]) => [flag(key), value]); } function setup() { const command = new Command() .name("message") .exitOverride() .configureOutput({ writeErr() {}, writeOut() {} }); const runMessageAction = vi.fn(async () => {}); registerMessageDiscordAdminCommands(command, { ...createMessageCliHelpers("discord|matrix|msteams|slack"), runMessageAction, }); return { command, runMessageAction }; } function leaf(command: Command, path: string) { let current = command; for (const name of path.split(" ")) { const next = current.commands.find((candidate) => candidate.name() === name); if (!next) { throw new Error("Missing command " + path); } current = next; } return current; } describe("Discord-admin message registration", () => { it.each(cases)( "$command forwards the exact action, defaults and options", async ({ command: path, action, required, optional = {} }) => { const { command, runMessageAction } = setup(); await command.parseAsync( [ ...path.split(" "), ...argumentsFor(required), ...argumentsFor(optional), "--channel", "discord", ], { from: "user" }, ); expect(runMessageAction).toHaveBeenCalledExactlyOnceWith(action, { json: false, dryRun: false, verbose: false, ...required, ...optional, channel: "discord", }); }, ); it.each(cases)( "$command rejects every missing mandatory identifier before dispatch", async ({ command: path, required }) => { for (const missing of Object.keys(required)) { const { command, runMessageAction } = setup(); const remaining = Object.fromEntries( Object.entries(required).filter(([key]) => key !== missing), ); await expect( command.parseAsync([...path.split(" "), ...argumentsFor(remaining)], { from: "user" }), ).rejects.toMatchObject({ code: "commander.missingMandatoryOptionValue" }); expect(runMessageAction).not.toHaveBeenCalled(); } }, ); it.each(cases)( "$command keeps required, shared and leaf-only flags in help order", ({ command: path, required, optional = {} }) => { const { command } = setup(); expect(leaf(command, path).options.map((option) => option.long)).toEqual([ ...Object.keys(required).map(flag), "--channel", "--account", "--json", "--dry-run", "--verbose", ...Object.keys(optional).map(flag), ]); }, ); it.each([undefined, "discord", "slack"])( "keeps guild and conversation optional for member info on %s", async (channel) => { const { command, runMessageAction } = setup(); const options = { userId: "user-1", ...(channel ? { channel } : {}) }; await command.parseAsync(["member", "info", ...argumentsFor(options)], { from: "user" }); expect(runMessageAction).toHaveBeenCalledExactlyOnceWith("member-info", { json: false, dryRun: false, verbose: false, ...options, }); }, ); });