mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-09 08:11:09 +00:00
Discord tests: stabilize channel lane harness coverage
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user