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

View File

@@ -172,6 +172,24 @@ describe("commands registry", () => {
expect(native.find((spec) => spec.name === "agentstatus")).toBeTruthy();
expect(findCommandByNativeName("agentstatus", "slack")?.key).toBe("status");
expect(findCommandByNativeName("status", "slack")).toBeUndefined();
expect(
findCommandByNativeName("agentstatus", "slack", {
includeBundledChannelFallback: false,
})?.key,
).toBe("status");
expect(
findCommandByNativeName("status", "slack", {
includeBundledChannelFallback: false,
}),
).toBeUndefined();
});
it("can resolve default native command names without loading bundled channel fallbacks", () => {
expect(
findCommandByNativeName("status", "discord", {
includeBundledChannelFallback: false,
})?.key,
).toBe("status");
});
it("keeps discord native command specs within slash-command limits", () => {

View File

@@ -1,7 +1,7 @@
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js";
import { resolveConfiguredModelRef } from "../agents/model-selection.js";
import type { SkillCommandSpec } from "../agents/skills.js";
import { getChannelPlugin } from "../channels/plugins/index.js";
import { getChannelPlugin, getLoadedChannelPlugin } from "../channels/plugins/index.js";
import type { OpenClawConfig } from "../config/types.js";
import {
normalizeLowercaseStringOrEmpty,
@@ -55,15 +55,27 @@ export type {
ShouldHandleTextCommandsParams,
} from "./commands-registry.types.js";
function resolveNativeName(command: ChatCommandDefinition, provider?: string): string | undefined {
type NativeCommandProviderLookupOptions = {
includeBundledChannelFallback?: boolean;
};
function resolveNativeName(
command: ChatCommandDefinition,
provider?: string,
options?: NativeCommandProviderLookupOptions,
): string | undefined {
if (!command.nativeName) {
return undefined;
}
if (!provider) {
return command.nativeName;
}
const channelPlugin =
options?.includeBundledChannelFallback === false
? getLoadedChannelPlugin(provider)
: getChannelPlugin(provider);
return (
getChannelPlugin(provider)?.commands?.resolveNativeCommandName?.({
channelPlugin?.commands?.resolveNativeCommandName?.({
commandKey: command.key,
defaultName: command.nativeName,
}) ?? command.nativeName
@@ -108,6 +120,7 @@ export function listNativeCommandSpecsForConfig(
export function findCommandByNativeName(
name: string,
provider?: string,
options?: NativeCommandProviderLookupOptions,
): ChatCommandDefinition | undefined {
const normalized = normalizeOptionalLowercaseString(name);
if (!normalized) {
@@ -116,7 +129,8 @@ export function findCommandByNativeName(
return getChatCommands().find(
(command) =>
command.scope !== "text" &&
normalizeOptionalLowercaseString(resolveNativeName(command, provider)) === normalized,
normalizeOptionalLowercaseString(resolveNativeName(command, provider, options)) ===
normalized,
);
}