Tests: avoid bundled Discord runtime lookup

This commit is contained in:
Gustavo Madeira Santana
2026-04-17 20:56:17 -04:00
parent c8d722d093
commit 0e4ddf7b38
4 changed files with 107 additions and 59 deletions

View File

@@ -1,17 +1,25 @@
import { ChannelType } from "discord-api-types/v10";
import type { NativeCommandSpec } from "openclaw/plugin-sdk/command-auth";
import { resolveDirectStatusReplyForSession } from "openclaw/plugin-sdk/command-status-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { clearPluginCommands, registerPluginCommand } from "openclaw/plugin-sdk/plugin-runtime";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
clearPluginCommands,
executePluginCommand,
matchPluginCommand,
registerPluginCommand,
} from "openclaw/plugin-sdk/plugin-runtime";
import { dispatchReplyWithDispatcher } from "openclaw/plugin-sdk/reply-dispatch-runtime";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
createTestRegistry,
setActivePluginRegistry,
} from "../../../../test/helpers/plugins/plugin-registry.js";
import { resolveDiscordNativeInteractionRouteState } from "./native-command-route.js";
import {
createMockCommandInteraction,
type MockCommandInteraction,
} from "./native-command.test-helpers.js";
import { createNoopThreadBindingManager } from "./thread-bindings.js";
import { createNoopThreadBindingManager } from "./thread-bindings.manager.js";
let createDiscordNativeCommand: typeof import("./native-command.js").createDiscordNativeCommand;
let discordNativeCommandTesting: typeof import("./native-command.js").__testing;
@@ -22,33 +30,6 @@ const runtimeModuleMocks = vi.hoisted(() => ({
resolveDirectStatusReplyForSession: vi.fn(),
}));
vi.mock("openclaw/plugin-sdk/plugin-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/plugin-runtime")>(
"openclaw/plugin-sdk/plugin-runtime",
);
return {
...actual,
matchPluginCommand: (...args: unknown[]) => runtimeModuleMocks.matchPluginCommand(...args),
executePluginCommand: (...args: unknown[]) => runtimeModuleMocks.executePluginCommand(...args),
};
});
vi.mock("openclaw/plugin-sdk/reply-runtime", async () => {
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/reply-runtime")>(
"openclaw/plugin-sdk/reply-runtime",
);
return {
...actual,
dispatchReplyWithDispatcher: (...args: unknown[]) =>
runtimeModuleMocks.dispatchReplyWithDispatcher(...args),
};
});
vi.mock("openclaw/plugin-sdk/command-status-runtime", () => ({
resolveDirectStatusReplyForSession: (...args: unknown[]) =>
runtimeModuleMocks.resolveDirectStatusReplyForSession(...args),
}));
function createInteraction(params?: {
channelType?: ChannelType;
channelId?: string;
@@ -270,13 +251,16 @@ async function expectPairCommandReply(params: {
cfg: OpenClawConfig;
commandName: string;
interaction: MockCommandInteraction;
expectedRegisteredName?: string;
}) {
const command = await createPluginCommand({
cfg: params.cfg,
name: params.commandName,
});
const dispatchSpy = runtimeModuleMocks.dispatchReplyWithDispatcher;
const executeSpy = runtimeModuleMocks.executePluginCommand.mockResolvedValue({
text: "paired:now",
});
await (command as { run: (interaction: unknown) => Promise<void> }).run(
Object.assign(params.interaction, {
options: {
@@ -288,6 +272,12 @@ async function expectPairCommandReply(params: {
);
expect(dispatchSpy).not.toHaveBeenCalled();
expect(executeSpy).toHaveBeenCalledWith(
expect.objectContaining({
command: expect.objectContaining({ name: params.expectedRegisteredName ?? "pair" }),
args: "now",
}),
);
expect(params.interaction.followUp).toHaveBeenCalledWith(
expect.objectContaining({ content: "paired:now" }),
);
@@ -338,21 +328,28 @@ describe("Discord native plugin command dispatch", () => {
await import("./native-command.js"));
});
beforeEach(async () => {
afterAll(() => {
clearPluginCommands();
setActivePluginRegistry(createTestRegistry());
discordNativeCommandTesting.setMatchPluginCommand(matchPluginCommand);
discordNativeCommandTesting.setExecutePluginCommand(executePluginCommand);
discordNativeCommandTesting.setDispatchReplyWithDispatcher(dispatchReplyWithDispatcher);
discordNativeCommandTesting.setResolveDirectStatusReplyForSession(
resolveDirectStatusReplyForSession,
);
discordNativeCommandTesting.setResolveDiscordNativeInteractionRouteState(
resolveDiscordNativeInteractionRouteState,
);
});
beforeEach(() => {
vi.clearAllMocks();
clearPluginCommands();
setActivePluginRegistry(createTestRegistry());
const actualPluginRuntime = await vi.importActual<
typeof import("openclaw/plugin-sdk/plugin-runtime")
>("openclaw/plugin-sdk/plugin-runtime");
runtimeModuleMocks.matchPluginCommand.mockReset();
runtimeModuleMocks.matchPluginCommand.mockImplementation(
actualPluginRuntime.matchPluginCommand,
);
runtimeModuleMocks.matchPluginCommand.mockImplementation(matchPluginCommand);
runtimeModuleMocks.executePluginCommand.mockReset();
runtimeModuleMocks.executePluginCommand.mockImplementation(
actualPluginRuntime.executePluginCommand,
);
runtimeModuleMocks.executePluginCommand.mockImplementation(executePluginCommand);
runtimeModuleMocks.dispatchReplyWithDispatcher.mockReset();
runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue({
counts: {
@@ -372,7 +369,10 @@ describe("Discord native plugin command dispatch", () => {
runtimeModuleMocks.executePluginCommand as typeof import("openclaw/plugin-sdk/plugin-runtime").executePluginCommand,
);
discordNativeCommandTesting.setDispatchReplyWithDispatcher(
runtimeModuleMocks.dispatchReplyWithDispatcher as typeof import("openclaw/plugin-sdk/reply-runtime").dispatchReplyWithDispatcher,
runtimeModuleMocks.dispatchReplyWithDispatcher as typeof dispatchReplyWithDispatcher,
);
discordNativeCommandTesting.setResolveDirectStatusReplyForSession(
runtimeModuleMocks.resolveDirectStatusReplyForSession as typeof resolveDirectStatusReplyForSession,
);
discordNativeCommandTesting.setResolveDiscordNativeInteractionRouteState(async (params) =>
createUnboundRouteState({
@@ -436,7 +436,6 @@ describe("Discord native plugin command dispatch", () => {
description: "Pair",
acceptsArgs: true,
};
const command = await createNativeCommand(cfg, commandSpec);
const interaction = createInteraction({
channelType: ChannelType.GuildText,
channelId: "234567890123456789",
@@ -455,6 +454,7 @@ describe("Discord native plugin command dispatch", () => {
handler: async ({ args }) => ({ text: `open:${args ?? ""}` }),
}),
).toEqual({ ok: true });
const command = await createNativeCommand(cfg, commandSpec);
const executeSpy = runtimeModuleMocks.executePluginCommand;
const dispatchSpy = runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue(

View File

@@ -94,6 +94,7 @@ const DISCORD_COMMAND_DESCRIPTION_MAX = 100;
let matchPluginCommandImpl = pluginRuntime.matchPluginCommand;
let executePluginCommandImpl = pluginRuntime.executePluginCommand;
let dispatchReplyWithDispatcherImpl = dispatchReplyWithDispatcher;
let resolveDirectStatusReplyForSessionImpl = resolveDirectStatusReplyForSession;
let resolveDiscordNativeInteractionRouteStateImpl = resolveDiscordNativeInteractionRouteState;
export const __testing = {
@@ -118,6 +119,13 @@ export const __testing = {
dispatchReplyWithDispatcherImpl = next;
return previous;
},
setResolveDirectStatusReplyForSession(
next: typeof resolveDirectStatusReplyForSession,
): typeof resolveDirectStatusReplyForSession {
const previous = resolveDirectStatusReplyForSessionImpl;
resolveDirectStatusReplyForSessionImpl = next;
return previous;
},
setResolveDiscordNativeInteractionRouteState(
next: typeof resolveDiscordNativeInteractionRouteState,
): typeof resolveDiscordNativeInteractionRouteState {
@@ -621,6 +629,19 @@ async function safeDiscordInteractionCall<T>(
}
}
function createNativeCommandDefinition(command: NativeCommandSpec): ChatCommandDefinition {
return {
key: command.name,
nativeName: command.name,
description: command.description,
textAliases: [],
acceptsArgs: command.acceptsArgs,
args: command.args,
argsParsing: "none",
scope: "native",
};
}
export function createDiscordNativeCommand(params: {
command: NativeCommandSpec;
cfg: ReturnType<typeof loadConfig>;
@@ -639,18 +660,13 @@ export function createDiscordNativeCommand(params: {
ephemeralDefault,
threadBindings,
} = params;
const fallbackCommandDefinition = createNativeCommandDefinition(command);
const commandDefinition =
findCommandByNativeName(command.name, "discord") ??
({
key: command.name,
nativeName: command.name,
description: command.description,
textAliases: [],
acceptsArgs: command.acceptsArgs,
args: command.args,
argsParsing: "none",
scope: "native",
} satisfies ChatCommandDefinition);
matchPluginCommandImpl(`/${command.name}`) !== null
? fallbackCommandDefinition
: (findCommandByNativeName(command.name, "discord", {
includeBundledChannelFallback: false,
}) ?? fallbackCommandDefinition);
const argDefinitions = commandDefinition.args ?? command.args;
const commandOptions = buildDiscordCommandOptions({
command: commandDefinition,
@@ -1130,7 +1146,7 @@ async function dispatchDiscordCommandInteraction(params: {
});
const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, effectiveRoute.agentId);
if (!suppressReplies && commandName === "status") {
const statusReply = await resolveDirectStatusReplyForSession({
const statusReply = await resolveDirectStatusReplyForSessionImpl({
cfg,
sessionKey: commandTargetSessionKey?.trim() || sessionKey,
channel: "discord",