refactor(discord): remove obsolete action runtime wrapper (#127123)

This commit is contained in:
Peter Steinberger
2026-08-21 02:07:48 -07:00
committed by GitHub
parent be2f7c6a3d
commit 782a7d7aed
6 changed files with 22 additions and 135 deletions
@@ -1,7 +1,7 @@
// Discord tests cover channel actions.contract plugin behavior.
import { installChannelActionsContractSuite } from "openclaw/plugin-sdk/channel-test-helpers";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { describe } from "vitest";
import { describe, expect, it } from "vitest";
import { discordPlugin } from "../api.js";
describe("discord actions contract", () => {
@@ -39,9 +39,13 @@ describe("discord actions contract", () => {
},
} as OpenClawConfig,
expectedActions: ["send", "poll", "react", "reactions", "emoji-list"],
expectedCanonicalOutboundActions: ["poll"],
expectedCapabilities: ["presentation"],
},
],
});
it("declines poll actions so canonical outbound delivery owns them", () => {
expect(discordPlugin.actions?.supportsAction?.({ action: "poll" })).toBe(false);
expect(discordPlugin.outbound?.sendPoll).toBeTypeOf("function");
});
});
@@ -176,11 +176,13 @@ function describeDiscordMessageTool({
}
export const discordMessageActions: ChannelMessageActionAdapter = {
providerOwnedReadGates: true,
// Credential-only Discord actions run in the gateway when one is available.
// Send/file-style actions stay local because core owns their thread, media,
// component, and client-local payload semantics.
resolveExecutionMode: resolveDiscordActionExecutionMode,
describeMessageTool: describeDiscordMessageTool,
supportsAction: ({ action }) => action !== "poll",
requiresTrustedRequesterSender: ({ action, toolContext }) =>
Boolean(toolContext) && isTrustedRequesterGuildAdminAction(action),
extractToolSend: ({ args }) => {
+3 -26
View File
@@ -116,16 +116,12 @@ async function expectDiscordStartupDelay(
}
function installDiscordRuntime(
discord: Record<string, unknown>,
openKeyedStore: (options: Record<string, unknown>) => unknown = vi.fn(() => ({
lookup: vi.fn(async () => undefined),
register: vi.fn(async () => undefined),
})),
) {
setDiscordRuntime({
channel: {
discord,
},
logging: {
shouldLogVerbose: () => false,
},
@@ -167,7 +163,7 @@ afterEach(() => {
beforeEach(async () => {
vi.useRealTimers();
installDiscordRuntime({});
installDiscordRuntime();
});
beforeAll(async () => {
@@ -453,12 +449,6 @@ describe("discordPlugin outbound", () => {
});
it("uses direct Discord probe helpers for status probes", async () => {
const runtimeProbeDiscord = vi.fn(async () => {
throw new Error("runtime Discord probe should not be used");
});
installDiscordRuntime({
probeDiscord: runtimeProbeDiscord,
});
probeDiscordMock.mockResolvedValue({
ok: true,
bot: { username: "Bob" },
@@ -487,7 +477,6 @@ describe("discordPlugin outbound", () => {
const forwardedTimeoutMs = Number(argAt(probeDiscordMock, 0, 1));
expect(forwardedTimeoutMs).toBeGreaterThan(0);
expect(forwardedTimeoutMs).toBeLessThanOrEqual(5_000);
expect(runtimeProbeDiscord).not.toHaveBeenCalled();
});
it("subtracts lazy probe loading from the status budget", async () => {
@@ -577,16 +566,6 @@ describe("discordPlugin outbound", () => {
});
it("uses direct Discord startup helpers for async startup enrichment", async () => {
const runtimeProbeDiscord = vi.fn(async () => {
throw new Error("runtime Discord probe should not be used");
});
const runtimeMonitorDiscordProvider = vi.fn(async () => {
throw new Error("runtime Discord monitor should not be used");
});
installDiscordRuntime({
probeDiscord: runtimeProbeDiscord,
monitorDiscordProvider: runtimeMonitorDiscordProvider,
});
probeDiscordMock.mockResolvedValue({
ok: true,
bot: { username: "Bob" },
@@ -613,8 +592,6 @@ describe("discordPlugin outbound", () => {
expect(monitorParams.token).toBe("discord-token");
expect(monitorParams.accountId).toBe("default");
expect(sleepWithAbortMock).not.toHaveBeenCalled();
expect(runtimeProbeDiscord).not.toHaveBeenCalled();
expect(runtimeMonitorDiscordProvider).not.toHaveBeenCalled();
});
it("fails loudly before provider startup when a token SecretRef is configured but unresolved", async () => {
@@ -705,7 +682,7 @@ describe("discordPlugin outbound", () => {
register: vi.fn(async () => undefined),
};
const openKeyedStore = vi.fn(() => commandDeployHashStore);
installDiscordRuntime({}, openKeyedStore);
installDiscordRuntime(openKeyedStore);
await startDiscordAccount(createCfg());
@@ -721,7 +698,7 @@ describe("discordPlugin outbound", () => {
it("continues Discord startup when the command deployment cache cannot open", async () => {
prepareDiscordStartupMocks();
installDiscordRuntime({}, () => {
installDiscordRuntime(() => {
throw new Error("SQLite unavailable");
});
+1 -60
View File
@@ -4,10 +4,6 @@ import {
createAccountScopedAllowlistNameResolver,
createNestedAllowlistOverrideResolver,
} from "openclaw/plugin-sdk/allowlist-config-edit";
import type {
ChannelMessageActionAdapter,
ChannelMessageToolDiscovery,
} from "openclaw/plugin-sdk/channel-contract";
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
import { createChannelMessageAdapterFromOutbound } from "openclaw/plugin-sdk/channel-outbound";
import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing";
@@ -31,7 +27,7 @@ import {
} from "./accounts.js";
import { getDiscordApprovalCapability } from "./approval-native.js";
import { resolveRequiredDiscordChannelPermissions } from "./audit-core.js";
import { discordMessageActions as discordMessageActionsImpl } from "./channel-actions.js";
import { discordMessageActions } from "./channel-actions.js";
import {
buildTokenChannelStatusSummary,
DEFAULT_ACCOUNT_ID,
@@ -177,61 +173,6 @@ function shouldTreatDiscordDeliveredTextAsVisible(params: {
);
}
function resolveRuntimeDiscordMessageActions() {
try {
return getDiscordRuntime().channel?.discord?.messageActions ?? null;
} catch {
return null;
}
}
const discordMessageActions: ChannelMessageActionAdapter = {
providerOwnedReadGates: true,
resolveExecutionMode: (
ctx: Parameters<NonNullable<ChannelMessageActionAdapter["resolveExecutionMode"]>>[0],
) =>
resolveRuntimeDiscordMessageActions()?.resolveExecutionMode?.(ctx) ??
discordMessageActionsImpl.resolveExecutionMode?.(ctx) ??
"local",
describeMessageTool: (
ctx: Parameters<NonNullable<ChannelMessageActionAdapter["describeMessageTool"]>>[0],
): ChannelMessageToolDiscovery | null =>
resolveRuntimeDiscordMessageActions()?.describeMessageTool?.(ctx) ??
discordMessageActionsImpl.describeMessageTool?.(ctx) ??
null,
requiresTrustedRequesterSender: (
ctx: Parameters<NonNullable<ChannelMessageActionAdapter["requiresTrustedRequesterSender"]>>[0],
) =>
resolveRuntimeDiscordMessageActions()?.requiresTrustedRequesterSender?.(ctx) ??
discordMessageActionsImpl.requiresTrustedRequesterSender?.(ctx) ??
false,
extractToolSend: (
ctx: Parameters<NonNullable<ChannelMessageActionAdapter["extractToolSend"]>>[0],
) =>
resolveRuntimeDiscordMessageActions()?.extractToolSend?.(ctx) ??
discordMessageActionsImpl.extractToolSend?.(ctx) ??
null,
prepareSendPayload: (
ctx: Parameters<NonNullable<ChannelMessageActionAdapter["prepareSendPayload"]>>[0],
) =>
resolveRuntimeDiscordMessageActions()?.prepareSendPayload?.(ctx) ??
discordMessageActionsImpl.prepareSendPayload?.(ctx) ??
null,
supportsAction: ({ action }) => action !== "poll",
handleAction: async (
ctx: Parameters<NonNullable<ChannelMessageActionAdapter["handleAction"]>>[0],
) => {
const runtimeHandleAction = resolveRuntimeDiscordMessageActions()?.handleAction;
if (runtimeHandleAction) {
return await runtimeHandleAction(ctx);
}
if (!discordMessageActionsImpl.handleAction) {
throw new Error("Discord message actions not available");
}
return await discordMessageActionsImpl.handleAction(ctx);
},
};
function resolveDiscordStartupDelayMs(cfg: OpenClawConfig, accountId: string): number {
const startupAccountIds = listDiscordStartupAccountIds(cfg);
const startupIndex = startupAccountIds.findIndex((candidateId) => candidateId === accountId);
+1 -12
View File
@@ -2,22 +2,11 @@
import type { PluginRuntime } from "openclaw/plugin-sdk/channel-core";
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
type DiscordChannelRuntime = {
messageActions?: typeof import("./channel-actions.js").discordMessageActions;
sendMessageDiscord?: typeof import("./send.js").sendMessageDiscord;
};
type DiscordRuntime = PluginRuntime & {
channel: PluginRuntime["channel"] & {
discord?: DiscordChannelRuntime;
};
};
const {
setRuntime: setDiscordRuntime,
tryGetRuntime: getOptionalDiscordRuntime,
getRuntime: getDiscordRuntime,
} = createPluginRuntimeStore<DiscordRuntime>({
} = createPluginRuntimeStore<PluginRuntime>({
pluginId: "discord",
errorMessage: "Discord runtime not initialized",
});
@@ -66,23 +66,12 @@ type ChannelActionsContractCase = {
name: string;
cfg: OpenClawConfig;
expectedActions: readonly ChannelMessageActionName[];
expectedCanonicalOutboundActions?: readonly ChannelMessageActionName[];
expectedCapabilities?: readonly ChannelMessageCapability[];
beforeTest?: () => void;
};
function hasCanonicalOutboundAction(
plugin: Pick<ChannelPlugin, "outbound">,
action: ChannelMessageActionName,
) {
if (action !== "poll") {
return false;
}
return Boolean(plugin.outbound?.sendPoll);
}
export function installChannelActionsContractSuite(params: {
plugin: Pick<ChannelPlugin, "id" | "actions" | "outbound">;
plugin: Pick<ChannelPlugin, "id" | "actions">;
cases: readonly ChannelActionsContractCase[];
unsupportedAction?: ChannelMessageActionName;
}) {
@@ -107,29 +96,14 @@ export function installChannelActionsContractSuite(params: {
expect(sortStrings(actions)).toEqual(sortStrings(testCase.expectedActions));
expect(sortStrings(capabilities)).toEqual(sortStrings(testCase.expectedCapabilities ?? []));
const canonicalOutboundActions = new Set(testCase.expectedCanonicalOutboundActions ?? []);
for (const action of canonicalOutboundActions) {
expect(actions).toContain(action);
expect(hasCanonicalOutboundAction(params.plugin, action)).toBe(true);
expect(params.plugin.actions?.supportsAction).toBeTypeOf("function");
expect(params.plugin.actions?.supportsAction?.({ action })).toBe(false);
}
if (params.plugin.actions?.supportsAction) {
for (const action of testCase.expectedActions) {
if (canonicalOutboundActions.has(action)) {
continue;
}
expect(params.plugin.actions.supportsAction({ action })).toBe(true);
}
if (
params.unsupportedAction &&
!testCase.expectedActions.includes(params.unsupportedAction)
) {
expect(params.plugin.actions.supportsAction({ action: params.unsupportedAction })).toBe(
false,
);
}
if (
params.plugin.actions?.supportsAction &&
params.unsupportedAction &&
!testCase.expectedActions.includes(params.unsupportedAction)
) {
expect(params.plugin.actions.supportsAction({ action: params.unsupportedAction })).toBe(
false,
);
}
});
}