refactor(discord): share partial channel test fixtures

This commit is contained in:
Peter Steinberger
2026-04-22 19:38:12 +01:00
parent ec5d403f5b
commit b0d4e64170
8 changed files with 57 additions and 77 deletions

View File

@@ -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;

View File

@@ -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,

View File

@@ -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);

View File

@@ -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<T extends object>(
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(

View File

@@ -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<typeof vi.fn> }).fetchChannel = vi.fn(
async (channelId: string) => {
if (channelId === "partial-thread-123") {

View File

@@ -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<DiscordAutoThreadReplyPlan> {
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 };

View File

@@ -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<T extends object>(
channel: T,
keys: readonly string[],
): T {
for (const key of keys) {
defineThrowingDiscordChannelGetter(channel, key);
}
return channel;
}

View File

@@ -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"],