File size: 6,878 Bytes
fc93158 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | import { ChannelType } from "discord-api-types/v10";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { NativeCommandSpec } from "../../auto-reply/commands-registry.js";
import * as dispatcherModule from "../../auto-reply/reply/provider-dispatcher.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { DiscordAccountConfig } from "../../config/types.discord.js";
import * as pluginCommandsModule from "../../plugins/commands.js";
import { createDiscordNativeCommand } from "./native-command.js";
import {
createMockCommandInteraction,
type MockCommandInteraction,
} from "./native-command.test-helpers.js";
import { createNoopThreadBindingManager } from "./thread-bindings.js";
function createInteraction(params?: { userId?: string }): MockCommandInteraction {
return createMockCommandInteraction({
userId: params?.userId ?? "123456789012345678",
username: "discord-user",
globalName: "Discord User",
channelType: ChannelType.GuildText,
channelId: "234567890123456789",
guildId: "345678901234567890",
guildName: "Test Guild",
interactionId: "interaction-1",
});
}
function createConfig(): OpenClawConfig {
return {
commands: {
allowFrom: {
discord: ["user:123456789012345678"],
},
},
channels: {
discord: {
groupPolicy: "allowlist",
guilds: {
"345678901234567890": {
channels: {
"234567890123456789": {
allow: true,
requireMention: false,
},
},
},
},
},
},
} as OpenClawConfig;
}
function createCommand(cfg: OpenClawConfig, discordConfig?: DiscordAccountConfig) {
const commandSpec: NativeCommandSpec = {
name: "status",
description: "Status",
acceptsArgs: false,
};
return createDiscordNativeCommand({
command: commandSpec,
cfg,
discordConfig: discordConfig ?? cfg.channels?.discord ?? {},
accountId: "default",
sessionPrefix: "discord:slash",
ephemeralDefault: true,
threadBindings: createNoopThreadBindingManager("default"),
});
}
function createDispatchSpy() {
return vi.spyOn(dispatcherModule, "dispatchReplyWithDispatcher").mockResolvedValue({
counts: {
final: 1,
block: 0,
tool: 0,
},
} as never);
}
async function runGuildSlashCommand(params?: {
userId?: string;
mutateConfig?: (cfg: OpenClawConfig) => void;
runtimeDiscordConfig?: DiscordAccountConfig;
}) {
const cfg = createConfig();
params?.mutateConfig?.(cfg);
const command = createCommand(cfg, params?.runtimeDiscordConfig);
const interaction = createInteraction({ userId: params?.userId });
vi.spyOn(pluginCommandsModule, "matchPluginCommand").mockReturnValue(null);
const dispatchSpy = createDispatchSpy();
await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
return { dispatchSpy, interaction };
}
function expectNotUnauthorizedReply(interaction: MockCommandInteraction) {
expect(interaction.reply).not.toHaveBeenCalledWith(
expect.objectContaining({ content: "You are not authorized to use this command." }),
);
}
function expectUnauthorizedReply(interaction: MockCommandInteraction) {
expect(interaction.reply).toHaveBeenCalledWith(
expect.objectContaining({
content: "You are not authorized to use this command.",
ephemeral: true,
}),
);
}
describe("Discord native slash commands with commands.allowFrom", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it("authorizes guild slash commands when commands.allowFrom.discord matches the sender", async () => {
const { dispatchSpy, interaction } = await runGuildSlashCommand();
expect(dispatchSpy).toHaveBeenCalledTimes(1);
expectNotUnauthorizedReply(interaction);
});
it("authorizes guild slash commands from the global commands.allowFrom list when provider-specific allowFrom is missing", async () => {
const { dispatchSpy, interaction } = await runGuildSlashCommand({
mutateConfig: (cfg) => {
cfg.commands = {
allowFrom: {
"*": ["user:123456789012345678"],
},
};
},
});
expect(dispatchSpy).toHaveBeenCalledTimes(1);
expectNotUnauthorizedReply(interaction);
});
it("authorizes guild slash commands when commands.useAccessGroups is false and commands.allowFrom.discord matches the sender", async () => {
const { dispatchSpy, interaction } = await runGuildSlashCommand({
mutateConfig: (cfg) => {
cfg.commands = {
...cfg.commands,
useAccessGroups: false,
};
},
});
expect(dispatchSpy).toHaveBeenCalledTimes(1);
expectNotUnauthorizedReply(interaction);
});
it("rejects guild slash commands when commands.allowFrom.discord does not match the sender", async () => {
const { dispatchSpy, interaction } = await runGuildSlashCommand({
userId: "999999999999999999",
});
expect(dispatchSpy).not.toHaveBeenCalled();
expectUnauthorizedReply(interaction);
});
it("rejects guild slash commands when commands.useAccessGroups is false and commands.allowFrom.discord does not match the sender", async () => {
const { dispatchSpy, interaction } = await runGuildSlashCommand({
userId: "999999999999999999",
mutateConfig: (cfg) => {
cfg.commands = {
...cfg.commands,
useAccessGroups: false,
};
},
});
expect(dispatchSpy).not.toHaveBeenCalled();
expectUnauthorizedReply(interaction);
});
it("uses the root discord maxLinesPerMessage when runtime discordConfig omits it", async () => {
const longReply = Array.from({ length: 20 }, (_value, index) => `Line ${index + 1}`).join("\n");
const { interaction } = await runGuildSlashCommand({
mutateConfig: (cfg) => {
cfg.channels = {
...cfg.channels,
discord: {
...cfg.channels?.discord,
maxLinesPerMessage: 120,
},
};
},
runtimeDiscordConfig: {
groupPolicy: "allowlist",
guilds: {
"345678901234567890": {
channels: {
"234567890123456789": {
allow: true,
requireMention: false,
},
},
},
},
},
});
const dispatchCall = vi.mocked(dispatcherModule.dispatchReplyWithDispatcher).mock
.calls[0]?.[0] as
| Parameters<typeof dispatcherModule.dispatchReplyWithDispatcher>[0]
| undefined;
await dispatchCall?.dispatcherOptions.deliver({ text: longReply }, { kind: "final" });
expect(interaction.reply).toHaveBeenCalledWith(expect.objectContaining({ content: longReply }));
expect(interaction.followUp).not.toHaveBeenCalled();
});
});
|