fix(regression): preserve discord thread bindings for plugin commands

This commit is contained in:
Tak Hoffman
2026-03-27 19:48:18 -05:00
parent b1eeca3b00
commit b598cdf968
7 changed files with 108 additions and 1 deletions

View File

@@ -39,6 +39,7 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
function createInteraction(params?: {
channelType?: ChannelType;
channelId?: string;
threadParentId?: string | null;
guildId?: string;
guildName?: string;
}): MockCommandInteraction {
@@ -48,6 +49,7 @@ function createInteraction(params?: {
globalName: "Tester",
channelType: params?.channelType ?? ChannelType.DM,
channelId: params?.channelId ?? "dm-1",
threadParentId: params?.threadParentId,
guildId: params?.guildId ?? null,
guildName: params?.guildName,
interactionId: "interaction-1",
@@ -501,6 +503,73 @@ describe("Discord native plugin command dispatch", () => {
);
});
it("forwards Discord thread metadata into direct plugin command execution", async () => {
const cfg = {
commands: {
useAccessGroups: false,
},
channels: {
discord: {
groupPolicy: "allowlist",
guilds: {
"345678901234567890": {
channels: {
"thread-123": {
allow: true,
requireMention: false,
},
"parent-456": {
allow: true,
requireMention: false,
},
},
},
},
},
},
} as OpenClawConfig;
const commandSpec: NativeCommandSpec = {
name: "cron_jobs",
description: "List cron jobs",
acceptsArgs: false,
};
const interaction = createInteraction({
channelType: ChannelType.PublicThread,
channelId: "thread-123",
threadParentId: "parent-456",
guildId: "345678901234567890",
guildName: "Test Guild",
});
const pluginMatch = {
command: {
name: "cron_jobs",
description: "List cron jobs",
pluginId: "cron-jobs",
acceptsArgs: false,
handler: vi.fn().mockResolvedValue({ text: "jobs" }),
},
args: undefined,
};
runtimeModuleMocks.matchPluginCommand.mockReturnValue(pluginMatch as never);
const executeSpy = runtimeModuleMocks.executePluginCommand.mockResolvedValue({
text: "direct plugin output",
});
const command = await createNativeCommand(cfg, commandSpec);
await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
expect(executeSpy).toHaveBeenCalledWith(
expect.objectContaining({
channel: "discord",
from: "discord:channel:thread-123",
to: "slash:owner",
messageThreadId: "thread-123",
threadParentId: "parent-456",
}),
);
});
it("routes native slash commands through configured ACP Discord channel bindings", async () => {
const { cfg, interaction } = createConfiguredAcpCase({
channelType: ChannelType.GuildText,

View File

@@ -3,7 +3,7 @@ import { vi } from "vitest";
export type MockCommandInteraction = {
user: { id: string; username: string; globalName: string };
channel: { type: ChannelType; id: string };
channel: { type: ChannelType; id: string; parentId?: string | null };
guild: { id: string; name?: string } | null;
rawData: { id: string; member: { roles: string[] } };
options: {
@@ -22,6 +22,7 @@ type CreateMockCommandInteractionParams = {
globalName?: string;
channelType?: ChannelType;
channelId?: string;
threadParentId?: string | null;
guildId?: string | null;
guildName?: string;
interactionId?: string;
@@ -42,6 +43,7 @@ export function createMockCommandInteraction(
channel: {
type: params.channelType ?? ChannelType.DM,
id: params.channelId ?? "dm-1",
parentId: params.threadParentId,
},
guild,
rawData: {

View File

@@ -917,6 +917,13 @@ async function dispatchDiscordCommandInteraction(params: {
return;
}
const channelId = rawChannelId || "unknown";
const isThreadChannel =
interaction.channel?.type === ChannelType.PublicThread ||
interaction.channel?.type === ChannelType.PrivateThread ||
interaction.channel?.type === ChannelType.AnnouncementThread;
const messageThreadId = !isDirectMessage && isThreadChannel ? channelId : undefined;
const threadParentId =
!isDirectMessage && isThreadChannel ? (interaction.channel.parentId ?? undefined) : undefined;
const pluginReply = await executePluginCommandImpl({
command: pluginMatch.command,
args: pluginMatch.args,
@@ -933,6 +940,8 @@ async function dispatchDiscordCommandInteraction(params: {
: `discord:channel:${channelId}`,
to: `slash:${user.id}`,
accountId,
messageThreadId,
threadParentId,
});
if (!hasRenderableReplyPayload(pluginReply)) {
await respond("Done.");