From b0d4e6417042136f2371c3b7e1d5c3fd5f918bc9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 22 Apr 2026 19:38:12 +0100 Subject: [PATCH] refactor(discord): share partial channel test fixtures --- .../src/monitor/message-handler.process.ts | 6 ++-- .../monitor/monitor.threading-utils.test.ts | 14 ++++----- .../native-command.commands-allowfrom.test.ts | 31 +++---------------- .../native-command.model-picker.test.ts | 22 ++++--------- .../native-command.plugin-dispatch.test.ts | 9 ++---- extensions/discord/src/monitor/threading.ts | 8 ++--- .../src/test-support/partial-channel.ts | 26 ++++++++++++++++ extensions/discord/src/voice/command.test.ts | 18 +++-------- 8 files changed, 57 insertions(+), 77 deletions(-) create mode 100644 extensions/discord/src/test-support/partial-channel.ts diff --git a/extensions/discord/src/monitor/message-handler.process.ts b/extensions/discord/src/monitor/message-handler.process.ts index 3dbf4ad916b..7eb35b20e14 100644 --- a/extensions/discord/src/monitor/message-handler.process.ts +++ b/extensions/discord/src/monitor/message-handler.process.ts @@ -275,7 +275,7 @@ export async function processDiscordMessage( const forumParentSlug = isForumParent && threadParentName ? normalizeDiscordSlug(threadParentName) : ""; const threadChannelId = threadChannel?.id; - const threadInheritParent = discordConfig?.thread?.inheritParent ?? false; + const threadParentInheritanceEnabled = discordConfig?.thread?.inheritParent ?? false; const isForumStarter = Boolean(threadChannelId && isForumParent && forumParentSlug) && message.id === threadChannelId; const forumContextLine = isForumStarter ? `[Forum parent: #${forumParentSlug}]` : null; @@ -411,7 +411,7 @@ export async function processDiscordMessage( peer: { kind: "channel", id: threadParentId }, }); } - if (!threadInheritParent) { + if (!threadParentInheritanceEnabled) { parentSessionKey = undefined; } } @@ -438,7 +438,7 @@ export async function processDiscordMessage( agentId: route.agentId, channel: route.channel, cfg, - threadInheritParent, + threadParentInheritanceEnabled, }); const deliverTarget = replyPlan.deliverTarget; const replyTarget = replyPlan.replyTarget; diff --git a/extensions/discord/src/monitor/monitor.threading-utils.test.ts b/extensions/discord/src/monitor/monitor.threading-utils.test.ts index 2fc8ac7f236..050ddd28375 100644 --- a/extensions/discord/src/monitor/monitor.threading-utils.test.ts +++ b/extensions/discord/src/monitor/monitor.threading-utils.test.ts @@ -284,20 +284,20 @@ describe("resolveDiscordAutoThreadContext", () => { name: "no created thread", createdThreadId: undefined, expectedNull: true, - inheritParent: undefined, + parentInheritanceEnabled: undefined, }, { name: "created thread without parent inheritance", createdThreadId: "thread", expectedNull: false, - inheritParent: false, + parentInheritanceEnabled: false, expectedParentSessionKey: undefined, }, { name: "created thread with parent inheritance", createdThreadId: "thread", expectedNull: false, - inheritParent: true, + parentInheritanceEnabled: true, expectedParentSessionKey: buildAgentSessionKey({ agentId: "agent", channel: "discord", @@ -312,7 +312,7 @@ describe("resolveDiscordAutoThreadContext", () => { channel: "discord", messageChannelId: "parent", createdThreadId: testCase.createdThreadId, - inheritParent: testCase.inheritParent, + parentInheritanceEnabled: testCase.parentInheritanceEnabled, }); if (testCase.expectedNull) { @@ -472,7 +472,7 @@ describe("resolveDiscordAutoThreadReplyPlan", () => { client?: Client; channelConfig?: DiscordChannelConfigResolved; threadChannel?: { id: string } | null; - threadInheritParent?: boolean; + threadParentInheritanceEnabled?: boolean; }) { return { client: @@ -492,7 +492,7 @@ describe("resolveDiscordAutoThreadReplyPlan", () => { replyToMode: "all" as const, agentId: "agent", channel: "discord" as const, - threadInheritParent: overrides?.threadInheritParent, + threadParentInheritanceEnabled: overrides?.threadParentInheritanceEnabled, }; } @@ -513,7 +513,7 @@ describe("resolveDiscordAutoThreadReplyPlan", () => { { name: "created thread with parent inheritance", params: { - threadInheritParent: true, + threadParentInheritanceEnabled: true, }, expectedDeliverTarget: "channel:thread", expectedReplyReference: undefined, diff --git a/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts b/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts index 7b1a6eae90f..c44d407a1c7 100644 --- a/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts +++ b/extensions/discord/src/monitor/native-command.commands-allowfrom.test.ts @@ -5,6 +5,7 @@ import type { DiscordAccountConfig } from "openclaw/plugin-sdk/config-runtime"; import * as pluginCommandsModule from "openclaw/plugin-sdk/plugin-runtime"; import * as dispatcherModule from "openclaw/plugin-sdk/reply-dispatch-runtime"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { defineThrowingDiscordChannelGetter } from "../test-support/partial-channel.js"; import { __testing as nativeCommandTesting, createDiscordNativeCommand } from "./native-command.js"; import { createMockCommandInteraction, @@ -157,15 +158,7 @@ describe("Discord native slash commands with commands.allowFrom", () => { it("tolerates partial guild channels whose name getter throws", async () => { const { dispatchSpy, interaction } = await runGuildSlashCommand({ mutateInteraction: (currentInteraction) => { - Object.defineProperty(currentInteraction.channel, "name", { - configurable: true, - enumerable: true, - get() { - throw new Error( - "Cannot access rawData on partial Channel. Use fetch() to populate data.", - ); - }, - }); + defineThrowingDiscordChannelGetter(currentInteraction.channel, "name"); }, }); expect(interaction.defer).toHaveBeenCalledTimes(1); @@ -176,15 +169,7 @@ describe("Discord native slash commands with commands.allowFrom", () => { it("tolerates partial guild channels whose topic getter throws", async () => { const { dispatchSpy, interaction } = await runGuildSlashCommand({ mutateInteraction: (currentInteraction) => { - Object.defineProperty(currentInteraction.channel, "topic", { - configurable: true, - enumerable: true, - get() { - throw new Error( - "Cannot access rawData on partial Channel. Use fetch() to populate data.", - ); - }, - }); + defineThrowingDiscordChannelGetter(currentInteraction.channel, "topic"); }, }); expect(interaction.defer).toHaveBeenCalledTimes(1); @@ -199,15 +184,7 @@ describe("Discord native slash commands with commands.allowFrom", () => { type: ChannelType.PublicThread, id: currentInteraction.channel.id, } as MockCommandInteraction["channel"]; - Object.defineProperty(currentInteraction.channel, "parentId", { - configurable: true, - enumerable: true, - get() { - throw new Error( - "Cannot access rawData on partial Channel. Use fetch() to populate data.", - ); - }, - }); + defineThrowingDiscordChannelGetter(currentInteraction.channel, "parentId"); }, }); expect(interaction.defer).toHaveBeenCalledTimes(1); diff --git a/extensions/discord/src/monitor/native-command.model-picker.test.ts b/extensions/discord/src/monitor/native-command.model-picker.test.ts index eb7e43ca8d6..9c4f1f61247 100644 --- a/extensions/discord/src/monitor/native-command.model-picker.test.ts +++ b/extensions/discord/src/monitor/native-command.model-picker.test.ts @@ -6,6 +6,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import * as globalsModule from "openclaw/plugin-sdk/runtime-env"; import * as commandTextModule from "openclaw/plugin-sdk/text-runtime"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { defineThrowingDiscordChannelGetter } from "../test-support/partial-channel.js"; import { resolveDiscordChannelContext } from "./agent-components-helpers.js"; import * as modelPickerPreferencesModule from "./model-picker-preferences.js"; import * as modelPickerModule from "./model-picker.js"; @@ -105,20 +106,6 @@ function createInteraction(params?: { userId?: string; values?: string[] }): Moc }; } -function makePartialChannelThrow( - target: T, - key: keyof T & string, - message = "Cannot access rawData on partial Channel. Use fetch() to populate data.", -) { - Object.defineProperty(target, key, { - configurable: true, - enumerable: true, - get() { - throw new Error(message); - }, - }); -} - function createDefaultModelPickerData(): ModelsProviderData { return createModelsProviderData({ openai: ["gpt-4.1", "gpt-4o"], @@ -357,7 +344,7 @@ describe("Discord model picker interactions", () => { const dispatchSpy = createDispatchSpy(); const submitInteraction = createInteraction({ userId: "owner" }); - makePartialChannelThrow(submitInteraction.channel, "name"); + defineThrowingDiscordChannelGetter(submitInteraction.channel, "name"); const button = createModelPickerFallbackButton(context, dispatchSpy); await button.run( @@ -395,7 +382,10 @@ describe("Discord model picker interactions", () => { parent?: { id?: string; name?: string }; }; submitInteraction.channel = threadChannel as MockInteraction["channel"]; - makePartialChannelThrow(threadChannel.parent as { id?: string; name?: string }, "name"); + defineThrowingDiscordChannelGetter( + threadChannel.parent as { id?: string; name?: string }, + "name", + ); const button = createModelPickerFallbackButton(context, dispatchSpy); await button.run( diff --git a/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts b/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts index be45d3c65e1..cb235732fc2 100644 --- a/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts +++ b/extensions/discord/src/monitor/native-command.plugin-dispatch.test.ts @@ -14,6 +14,7 @@ import { createTestRegistry, setActivePluginRegistry, } from "../../../../test/helpers/plugins/plugin-registry.js"; +import { defineThrowingDiscordChannelGetter } from "../test-support/partial-channel.js"; import { resolveDiscordNativeInteractionRouteState } from "./native-command-route.js"; import { createMockCommandInteraction as createInteraction, @@ -636,13 +637,7 @@ describe("Discord native plugin command dispatch", () => { guildId: "345678901234567890", guildName: "Test Guild", }); - Object.defineProperty(interaction.channel, "parentId", { - configurable: true, - enumerable: true, - get() { - throw new Error("Cannot access rawData on partial Channel. Use fetch() to populate data."); - }, - }); + defineThrowingDiscordChannelGetter(interaction.channel, "parentId"); (interaction.client as { fetchChannel: ReturnType }).fetchChannel = vi.fn( async (channelId: string) => { if (channelId === "partial-thread-123") { diff --git a/extensions/discord/src/monitor/threading.ts b/extensions/discord/src/monitor/threading.ts index da2752e1c24..062a86e8605 100644 --- a/extensions/discord/src/monitor/threading.ts +++ b/extensions/discord/src/monitor/threading.ts @@ -388,7 +388,7 @@ export function resolveDiscordAutoThreadContext(params: { channel: string; messageChannelId: string; createdThreadId?: string | null; - inheritParent?: boolean; + parentInheritanceEnabled?: boolean; }): DiscordAutoThreadContext | null { const createdThreadId = normalizeOptionalStringifiedId(params.createdThreadId) ?? ""; if (!createdThreadId) { @@ -405,7 +405,7 @@ export function resolveDiscordAutoThreadContext(params: { peer: { kind: "channel", id: createdThreadId }, }); const parentSessionKey = - params.inheritParent === true + params.parentInheritanceEnabled === true ? buildAgentSessionKey({ agentId: params.agentId, channel: params.channel, @@ -451,7 +451,7 @@ export async function resolveDiscordAutoThreadReplyPlan( agentId: string; channel: string; cfg?: OpenClawConfig; - threadInheritParent?: boolean; + threadParentInheritanceEnabled?: boolean; }, ): Promise { const messageChannelId = resolveTrimmedDiscordMessageChannelId(params); @@ -487,7 +487,7 @@ export async function resolveDiscordAutoThreadReplyPlan( channel: params.channel, messageChannelId, createdThreadId, - inheritParent: params.threadInheritParent, + parentInheritanceEnabled: params.threadParentInheritanceEnabled, }) : null; return { ...deliveryPlan, createdThreadId, autoThreadContext }; diff --git a/extensions/discord/src/test-support/partial-channel.ts b/extensions/discord/src/test-support/partial-channel.ts new file mode 100644 index 00000000000..abc656ae5c9 --- /dev/null +++ b/extensions/discord/src/test-support/partial-channel.ts @@ -0,0 +1,26 @@ +export const DISCORD_PARTIAL_CHANNEL_RAW_DATA_ERROR = + "Cannot access rawData on partial Channel. Use fetch() to populate data."; + +export function defineThrowingDiscordChannelGetter( + channel: object, + key: string, + message = DISCORD_PARTIAL_CHANNEL_RAW_DATA_ERROR, +) { + Object.defineProperty(channel, key, { + configurable: true, + enumerable: true, + get() { + throw new Error(message); + }, + }); +} + +export function createPartialDiscordChannelWithThrowingGetters( + channel: T, + keys: readonly string[], +): T { + for (const key of keys) { + defineThrowingDiscordChannelGetter(channel, key); + } + return channel; +} diff --git a/extensions/discord/src/voice/command.test.ts b/extensions/discord/src/voice/command.test.ts index 8225468bc1e..48bea7a83bd 100644 --- a/extensions/discord/src/voice/command.test.ts +++ b/extensions/discord/src/voice/command.test.ts @@ -1,5 +1,6 @@ import type { CommandInteraction, CommandWithSubcommands } from "@buape/carbon"; import { describe, expect, it, vi } from "vitest"; +import { createPartialDiscordChannelWithThrowingGetters } from "../test-support/partial-channel.js"; import { createDiscordVoiceCommand } from "./command.js"; import type { DiscordVoiceManager } from "./manager.js"; @@ -103,19 +104,10 @@ describe("createDiscordVoiceCommand", () => { status: statusSpy, } as unknown as DiscordVoiceManager; const { status } = createVoiceCommandHarness(manager); - const partialChannel = { id: "123456789012345678" }; - Object.defineProperties(partialChannel, { - name: { - get() { - throw new Error("Cannot access rawData on partial Channel"); - }, - }, - parentId: { - get() { - throw new Error("Cannot access rawData on partial Channel"); - }, - }, - }); + const partialChannel = createPartialDiscordChannelWithThrowingGetters( + { id: "123456789012345678" }, + ["name", "parentId"], + ); const { interaction, reply } = createInteraction({ channel: partialChannel as CommandInteraction["channel"], client: { fetchChannel: vi.fn(async () => null) } as unknown as CommandInteraction["client"],