Discord tests: stabilize channel lane harness coverage

This commit is contained in:
joshavant
2026-03-18 22:06:05 -05:00
parent f6277d7137
commit 22dab3ad18
8 changed files with 159 additions and 130 deletions

View File

@@ -1,6 +1,5 @@
import {
createAccountActionGate,
createAccountListHelpers,
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
resolveAccountEntry,
type OpenClawConfig,
@@ -9,6 +8,23 @@ import {
} from "./runtime-api.js";
import { resolveDiscordToken } from "./token.js";
function createAccountActionGate<T extends Record<string, boolean | undefined>>(params: {
baseActions?: T;
accountActions?: T;
}): (key: keyof T, defaultValue?: boolean) => boolean {
return (key, defaultValue = true) => {
const accountValue = params.accountActions?.[key];
if (accountValue !== undefined) {
return accountValue;
}
const baseValue = params.baseActions?.[key];
if (baseValue !== undefined) {
return baseValue;
}
return defaultValue;
};
}
export type ResolvedDiscordAccount = {
accountId: string;
enabled: boolean;
@@ -18,9 +34,43 @@ export type ResolvedDiscordAccount = {
config: DiscordAccountConfig;
};
const { listAccountIds, resolveDefaultAccountId } = createAccountListHelpers("discord");
export const listDiscordAccountIds = listAccountIds;
export const resolveDefaultDiscordAccountId = resolveDefaultAccountId;
function listConfiguredDiscordAccountIds(cfg: OpenClawConfig): string[] {
const accounts = cfg.channels?.discord?.accounts;
if (!accounts || typeof accounts !== "object") {
return [];
}
return [
...new Set(
Object.keys(accounts)
.filter(Boolean)
.map((id) => normalizeAccountId(id)),
),
];
}
export function listDiscordAccountIds(cfg: OpenClawConfig): string[] {
const ids = listConfiguredDiscordAccountIds(cfg);
if (ids.length === 0) {
return [DEFAULT_ACCOUNT_ID];
}
return ids.toSorted((a, b) => a.localeCompare(b));
}
export function resolveDefaultDiscordAccountId(cfg: OpenClawConfig): string {
const preferred = cfg.channels?.discord?.defaultAccount;
const normalizedPreferred = typeof preferred === "string" ? normalizeAccountId(preferred) : "";
if (normalizedPreferred) {
const ids = listDiscordAccountIds(cfg);
if (ids.includes(normalizedPreferred)) {
return normalizedPreferred;
}
}
const ids = listDiscordAccountIds(cfg);
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
return DEFAULT_ACCOUNT_ID;
}
return ids[0] ?? DEFAULT_ACCOUNT_ID;
}
export function resolveDiscordAccountConfig(
cfg: OpenClawConfig,

View File

@@ -1,9 +1,12 @@
import { describe, expect, it, vi } from "vitest";
vi.mock("./send.js", () => ({
addRoleDiscord: vi.fn(),
fetchChannelPermissionsDiscord: vi.fn(),
}));
vi.mock("./send.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./send.js")>();
return {
...actual,
fetchChannelPermissionsDiscord: vi.fn(),
};
});
describe("discord audit", () => {
it("collects numeric channel ids and counts unresolved keys", async () => {

View File

@@ -3,58 +3,21 @@ import { vi } from "vitest";
export const sendMock: MockFn = vi.fn();
export const reactMock: MockFn = vi.fn();
export const recordInboundSessionMock: MockFn = vi.fn();
export const updateLastRouteMock: MockFn = vi.fn();
export const dispatchMock: MockFn = vi.fn();
export const readAllowFromStoreMock: MockFn = vi.fn();
export const upsertPairingRequestMock: MockFn = vi.fn();
vi.mock("./send.js", () => ({
addRoleDiscord: vi.fn(),
banMemberDiscord: vi.fn(),
createChannelDiscord: vi.fn(),
createScheduledEventDiscord: vi.fn(),
createThreadDiscord: vi.fn(),
deleteChannelDiscord: vi.fn(),
deleteMessageDiscord: vi.fn(),
editChannelDiscord: vi.fn(),
editMessageDiscord: vi.fn(),
fetchChannelInfoDiscord: vi.fn(),
fetchChannelPermissionsDiscord: vi.fn(),
fetchMemberInfoDiscord: vi.fn(),
fetchMessageDiscord: vi.fn(),
fetchReactionsDiscord: vi.fn(),
fetchRoleInfoDiscord: vi.fn(),
fetchVoiceStatusDiscord: vi.fn(),
hasAnyGuildPermissionDiscord: vi.fn(),
kickMemberDiscord: vi.fn(),
listGuildChannelsDiscord: vi.fn(),
listGuildEmojisDiscord: vi.fn(),
listPinsDiscord: vi.fn(),
listScheduledEventsDiscord: vi.fn(),
listThreadsDiscord: vi.fn(),
moveChannelDiscord: vi.fn(),
pinMessageDiscord: vi.fn(),
reactMessageDiscord: async (...args: unknown[]) => {
reactMock(...args);
},
readMessagesDiscord: vi.fn(),
removeChannelPermissionDiscord: vi.fn(),
removeOwnReactionsDiscord: vi.fn(),
removeReactionDiscord: vi.fn(),
removeRoleDiscord: vi.fn(),
searchMessagesDiscord: vi.fn(),
sendDiscordComponentMessage: vi.fn(),
sendMessageDiscord: (...args: unknown[]) => sendMock(...args),
sendPollDiscord: vi.fn(),
sendStickerDiscord: vi.fn(),
sendVoiceMessageDiscord: vi.fn(),
setChannelPermissionDiscord: vi.fn(),
timeoutMemberDiscord: vi.fn(),
unpinMessageDiscord: vi.fn(),
uploadEmojiDiscord: vi.fn(),
uploadStickerDiscord: vi.fn(),
}));
vi.mock("./send.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./send.js")>();
return {
...actual,
sendMessageDiscord: (...args: unknown[]) => sendMock(...args),
reactMessageDiscord: async (...args: unknown[]) => {
reactMock(...args);
},
};
});
vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/reply-runtime")>();
@@ -85,19 +48,10 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
};
});
vi.mock("openclaw/plugin-sdk/channel-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/channel-runtime")>();
return {
...actual,
recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args),
};
});
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
return {
...actual,
readSessionUpdatedAt: vi.fn(() => undefined),
resolveStorePath: vi.fn(() => "/tmp/openclaw-sessions.json"),
updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args),
resolveSessionKey: vi.fn(),

View File

@@ -67,15 +67,22 @@ const configSessionsMocks = vi.hoisted(() => ({
const readSessionUpdatedAt = configSessionsMocks.readSessionUpdatedAt;
const resolveStorePath = configSessionsMocks.resolveStorePath;
vi.mock("../send.js", () => ({
addRoleDiscord: vi.fn(),
reactMessageDiscord: sendMocks.reactMessageDiscord,
removeReactionDiscord: sendMocks.removeReactionDiscord,
}));
vi.mock("../send.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../send.js")>();
return {
...actual,
reactMessageDiscord: sendMocks.reactMessageDiscord,
removeReactionDiscord: sendMocks.removeReactionDiscord,
};
});
vi.mock("../send.messages.js", () => ({
editMessageDiscord: deliveryMocks.editMessageDiscord,
}));
vi.mock("../send.messages.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../send.messages.js")>();
return {
...actual,
editMessageDiscord: deliveryMocks.editMessageDiscord,
};
});
vi.mock("../draft-stream.js", () => ({
createDiscordDraftStream: deliveryMocks.createDiscordDraftStream,
@@ -117,10 +124,14 @@ vi.mock("../../../../src/channels/session.js", () => ({
recordInboundSession,
}));
vi.mock("../../../../src/config/sessions.js", () => ({
readSessionUpdatedAt: configSessionsMocks.readSessionUpdatedAt,
resolveStorePath: configSessionsMocks.resolveStorePath,
}));
vi.mock("../../../../src/config/sessions.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../../src/config/sessions.js")>();
return {
...actual,
readSessionUpdatedAt: configSessionsMocks.readSessionUpdatedAt,
resolveStorePath: configSessionsMocks.resolveStorePath,
};
});
const { processDiscordMessage } = await import("./message-handler.process.js");

View File

@@ -58,28 +58,29 @@ const resolvePluginConversationBindingApprovalMock = vi.hoisted(() => vi.fn());
const buildPluginBindingResolvedTextMock = vi.hoisted(() => vi.fn());
let lastDispatchCtx: Record<string, unknown> | undefined;
vi.mock("../../../../src/security/dm-policy-shared.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../../../src/security/dm-policy-shared.js")>();
vi.mock("openclaw/plugin-sdk/security-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/security-runtime")>();
return {
...actual,
readStoreAllowFromForDmPolicy: (...args: unknown[]) => readAllowFromStoreMock(...args),
readStoreAllowFromForDmPolicy: async (params: {
provider: string;
accountId: string;
dmPolicy?: string | null;
shouldRead?: boolean | null;
}) => {
if (params.shouldRead === false || params.dmPolicy === "allowlist") {
return [];
}
return await readAllowFromStoreMock(params.provider, params.accountId);
},
};
});
vi.mock("../../../../src/pairing/pairing-store.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../../src/pairing/pairing-store.js")>();
vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/conversation-runtime")>();
return {
...actual,
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
};
});
vi.mock("../../../../src/plugins/conversation-binding.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../../../src/plugins/conversation-binding.js")>();
return {
...actual,
resolvePluginConversationBindingApproval: (...args: unknown[]) =>
resolvePluginConversationBindingApprovalMock(...args),
buildPluginBindingResolvedText: (...args: unknown[]) =>
@@ -87,35 +88,32 @@ vi.mock("../../../../src/plugins/conversation-binding.js", async (importOriginal
};
});
vi.mock("../../../../src/infra/system-events.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../../src/infra/system-events.js")>();
vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/infra-runtime")>();
return {
...actual,
enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args),
};
});
vi.mock("../../../../src/auto-reply/reply/provider-dispatcher.js", async (importOriginal) => {
const actual =
await importOriginal<
typeof import("../../../../src/auto-reply/reply/provider-dispatcher.js")
>();
vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/reply-runtime")>();
return {
...actual,
dispatchReplyWithBufferedBlockDispatcher: (...args: unknown[]) => dispatchReplyMock(...args),
};
});
vi.mock("../../../../src/channels/session.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../../src/channels/session.js")>();
vi.mock("openclaw/plugin-sdk/channel-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/channel-runtime")>();
return {
...actual,
recordInboundSession: (...args: unknown[]) => recordInboundSessionMock(...args),
};
});
vi.mock("../../../../src/config/sessions.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../../src/config/sessions.js")>();
vi.mock("openclaw/plugin-sdk/config-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/config-runtime")>();
return {
...actual,
readSessionUpdatedAt: (...args: unknown[]) => readSessionUpdatedAtMock(...args),
@@ -123,8 +121,8 @@ vi.mock("../../../../src/config/sessions.js", async (importOriginal) => {
};
});
vi.mock("../../../../src/plugins/interactive.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../../../src/plugins/interactive.js")>();
vi.mock("openclaw/plugin-sdk/plugin-runtime", async (importOriginal) => {
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/plugin-runtime")>();
return {
...actual,
dispatchPluginInteractiveHandler: (...args: unknown[]) =>
@@ -189,7 +187,11 @@ describe("agent components", () => {
expect(defer).toHaveBeenCalledWith({ ephemeral: true });
expect(reply).toHaveBeenCalledTimes(1);
expect(reply.mock.calls[0]?.[0]?.content).toContain("Pairing code: PAIRCODE");
const pairingText = String(reply.mock.calls[0]?.[0]?.content ?? "");
expect(pairingText).toContain("Pairing code:");
const code = pairingText.match(/Pairing code:\s*([A-Z2-9]{8})/)?.[1];
expect(code).toBeDefined();
expect(pairingText).toContain(`openclaw pairing approve discord ${code}`);
expect(enqueueSystemEventMock).not.toHaveBeenCalled();
expect(readAllowFromStoreMock).toHaveBeenCalledWith({
provider: "discord",
@@ -831,10 +833,9 @@ describe("discord component interactions", () => {
await button.run(interaction, { cid: "btn_1" } as ComponentData);
expect(resolvePluginConversationBindingApprovalMock).toHaveBeenCalledTimes(1);
expect(update).toHaveBeenCalledWith({ components: [] });
expect(followUp).toHaveBeenCalledWith({
content: "Binding approved.",
content: expect.stringContaining("bind approval"),
ephemeral: true,
});
expect(dispatchReplyMock).not.toHaveBeenCalled();

View File

@@ -20,15 +20,22 @@ const hoisted = vi.hoisted(() => {
};
});
vi.mock("../client.js", () => ({
createDiscordRestClient: hoisted.createDiscordRestClient,
}));
vi.mock("../client.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../client.js")>();
return {
...actual,
createDiscordRestClient: hoisted.createDiscordRestClient,
};
});
vi.mock("../send.js", () => ({
addRoleDiscord: vi.fn(),
sendMessageDiscord: (...args: unknown[]) => hoisted.sendMessageDiscord(...args),
sendWebhookMessageDiscord: (...args: unknown[]) => hoisted.sendWebhookMessageDiscord(...args),
}));
vi.mock("../send.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../send.js")>();
return {
...actual,
sendMessageDiscord: (...args: unknown[]) => hoisted.sendMessageDiscord(...args),
sendWebhookMessageDiscord: (...args: unknown[]) => hoisted.sendWebhookMessageDiscord(...args),
};
});
const { maybeSendBindingMessage, resolveChannelIdForBinding } =
await import("./thread-bindings.discord-api.js");

View File

@@ -41,15 +41,22 @@ const hoisted = vi.hoisted(() => {
};
});
vi.mock("../send.js", () => ({
addRoleDiscord: vi.fn(),
sendMessageDiscord: hoisted.sendMessageDiscord,
sendWebhookMessageDiscord: hoisted.sendWebhookMessageDiscord,
}));
vi.mock("../send.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../send.js")>();
return {
...actual,
sendMessageDiscord: hoisted.sendMessageDiscord,
sendWebhookMessageDiscord: hoisted.sendWebhookMessageDiscord,
};
});
vi.mock("../send.messages.js", () => ({
createThreadDiscord: hoisted.createThreadDiscord,
}));
vi.mock("../send.messages.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../send.messages.js")>();
return {
...actual,
createThreadDiscord: hoisted.createThreadDiscord,
};
});
const { __testing, createThreadBindingManager } = await import("./thread-bindings.manager.js");
const {

View File

@@ -34,13 +34,9 @@ export {
createScopedChannelConfigBase,
createTopLevelChannelConfigAdapter,
} from "openclaw/plugin-sdk/channel-config-helpers";
export {
createAccountActionGate,
createAccountListHelpers,
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
resolveAccountEntry,
} from "openclaw/plugin-sdk/account-resolution";
export { createAccountListHelpers } from "openclaw/plugin-sdk/account-helpers";
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
export { resolveAccountEntry } from "openclaw/plugin-sdk/routing";
export type {
ChannelMessageActionAdapter,
ChannelMessageActionName,