Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: discord test #3478

Merged
merged 25 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions packages/plugin-discord/__tests__/discord-client.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Events } from 'discord.js';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { DiscordClient } from '../src';
import { DiscordConfig } from '../src/environment';

// Mock @elizaos/core
vi.mock('@elizaos/core', () => ({
Expand Down Expand Up @@ -56,15 +57,13 @@ vi.mock('discord.js', () => {
});

describe('DiscordClient', () => {
let mockConfig: DiscordConfig;
let mockRuntime: any;
let discordClient: DiscordClient;

beforeEach(() => {
mockRuntime = {
getSetting: vi.fn((key: string) => {
if (key === 'DISCORD_API_TOKEN') return 'mock-token';
return undefined;
}),
getSetting: vi.fn(),
getState: vi.fn(),
setState: vi.fn(),
getMemory: vi.fn(),
Expand All @@ -81,13 +80,16 @@ describe('DiscordClient', () => {
}
};

discordClient = new DiscordClient(mockRuntime);
mockConfig = {
DISCORD_API_TOKEN: "mock-token",
}

discordClient = new DiscordClient(mockRuntime, mockConfig);
});

it('should initialize with correct configuration', () => {
expect(discordClient.apiToken).toBe('mock-token');
expect(discordClient.client).toBeDefined();
expect(mockRuntime.getSetting).toHaveBeenCalledWith('DISCORD_API_TOKEN');
});

it('should login to Discord on initialization', () => {
Expand Down
38 changes: 38 additions & 0 deletions packages/plugin-discord/__tests__/environment.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, it, expect } from 'vitest';
import { validateDiscordConfig } from '../src/environment';
import type { IAgentRuntime } from '@elizaos/core';

// Mock runtime environment
const mockRuntime: IAgentRuntime = {
env: {
DISCORD_API_TOKEN: 'mocked-discord-token',
},
getEnv: function (key: string) {
return this.env[key] || null;
},
getSetting: function (key: string) {
return this.env[key] || null;
}
} as unknown as IAgentRuntime;

describe('Discord Environment Configuration', () => {
it('should validate correct configuration', async () => {
const config = await validateDiscordConfig(mockRuntime);
expect(config).toBeDefined();
expect(config.DISCORD_API_TOKEN).toBe('mocked-discord-token');
});

it('should throw an error when DISCORD_API_TOKEN is missing', async () => {
const invalidRuntime = {
...mockRuntime,
env: {
...mockRuntime.env,
DISCORD_API_TOKEN: undefined,
},
} as IAgentRuntime;

await expect(validateDiscordConfig(invalidRuntime)).rejects.toThrowError(
'Discord configuration validation failed:\nDISCORD_API_TOKEN: Discord API token is required'
);
});
});
208 changes: 208 additions & 0 deletions packages/plugin-discord/__tests__/message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { MessageManager } from "../src/messages.ts";
import { ChannelType, Client, Collection, AttachmentBuilder } from "discord.js";
import { type IAgentRuntime, ServiceType } from "@elizaos/core";
import type { VoiceManager } from "../src/voice";

vi.mock("@elizaos/core", () => ({
logger: {
info: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
stringToUuid: (str: string) => str,
messageCompletionFooter:
"# INSTRUCTIONS: Choose the best response for the agent.",
shouldRespondFooter: "# INSTRUCTIONS: Choose if the agent should respond.",
generateMessageResponse: vi.fn(),
generateShouldRespond: vi.fn().mockResolvedValue("IGNORE"), // Prevent API calls by always returning "IGNORE"
composeContext: vi.fn(),
ModelClass: {
TEXT_SMALL: "TEXT_SMALL",
},
ServiceType: {
VIDEO: "VIDEO",
BROWSER: "BROWSER",
},
}));

describe("Discord MessageManager", () => {
let mockRuntime: IAgentRuntime;
let mockClient: Client;
let mockDiscordClient: { client: Client; runtime: IAgentRuntime };
let mockVoiceManager: VoiceManager;
let mockMessage: any;
let messageManager: MessageManager;

beforeEach(() => {
vi.clearAllMocks();
mockRuntime = {
character: {
name: "TestBot",
templates: {},
clientConfig: {
discord: {
allowedChannelIds: ["mock-channal-id"],
shouldIgnoreBotMessages: true,
shouldIgnoreDirectMessages: true,
},
},
},
evaluate: vi.fn(),
composeState: vi.fn(),
ensureConnection: vi.fn(),
ensureUserExists: vi.fn(),
messageManager: {
createMemory: vi.fn(),
addEmbeddingToMemory: vi.fn(),
},
databaseAdapter: {
getParticipantUserState: vi.fn().mockResolvedValue("ACTIVE"),
log: vi.fn(),
},
processActions: vi.fn(),
} as unknown as IAgentRuntime;

mockClient = new Client({ intents: [] });
mockClient.user = {
id: "mock-bot-id",
username: "MockBot",
tag: "MockBot#0001",
displayName: "MockBotDisplay",
} as any;

mockDiscordClient = {
client: mockClient,
runtime: mockRuntime,
};

mockVoiceManager = {
playAudioStream: vi.fn(),
} as unknown as VoiceManager;

messageManager = new MessageManager(mockDiscordClient, mockVoiceManager);

const guild = {
members: {
cache: {
get: vi.fn().mockReturnValue({
nickname: "MockBotNickname",
permissions: {
has: vi.fn().mockReturnValue(true), // Bot has permissions
},
}),
},
},
};
mockMessage = {
content: "Hello, MockBot!",
author: {
id: "mock-user-id",
username: "MockUser",
bot: false,
},
guild,
channel: {
id: "mock-channal-id",
type: ChannelType.GuildText,
send: vi.fn(),
guild,
client: {
user: mockClient.user,
},
permissionsFor: vi.fn().mockReturnValue({
has: vi.fn().mockReturnValue(true),
}),
},
id: "mock-message-id",
createdTimestamp: Date.now(),
mentions: {
has: vi.fn().mockReturnValue(false),
},
reference: null,
attachments: [],
};
});

it("should initialize MessageManager", () => {
expect(messageManager).toBeDefined();
});

it("should process user messages", async () => {
// Prevent further message processing after response check
vi.spyOn(
Object.getPrototypeOf(messageManager),
"_shouldRespond"
).mockReturnValueOnce(false);

await messageManager.handleMessage(mockMessage);
expect(mockRuntime.ensureConnection).toHaveBeenCalled();
expect(mockRuntime.messageManager.createMemory).toHaveBeenCalled();
});

it("should ignore bot messages", async () => {
mockMessage.author.bot = true;
await messageManager.handleMessage(mockMessage);
expect(mockRuntime.ensureConnection).not.toHaveBeenCalled();
});

it("should ignore messages from restricted channels", async () => {
mockMessage.channel.id = "undefined-channel-id";
await messageManager.handleMessage(mockMessage);
expect(mockRuntime.ensureConnection).not.toHaveBeenCalled();
});

it.each([
["Hey MockBot, are you there?", "username"],
["MockBot#0001, respond please.", "tag"],
["MockBotNickname, can you help?", "nickname"],
["MoCkBoT, can you help?", "mixed case mention"],
])(
"should respond if the bot name is included in the message",
async (content) => {
mockMessage.content = content;

const result = await messageManager["_shouldRespond"](mockMessage, {});
expect(result).toBe(true);
}
);

it("should process audio attachments", async () => {
vi.spyOn(
Object.getPrototypeOf(messageManager),
"_shouldRespond"
).mockReturnValueOnce(false);
vi.spyOn(messageManager, "processMessageMedia").mockReturnValueOnce(
Promise.resolve({ processedContent: "", attachments: [] })
);

const myVariable = new Collection<string, any>([
[
"mock-attachment-id",
{
attachment: "https://www.example.mp3",
name: "mock-attachment.mp3",
contentType: "audio/mpeg",
},
],
]);

mockMessage.attachments = myVariable;

const processAttachmentsMock = vi.fn().mockResolvedValue([]);

const mockAttachmentManager = {
processAttachments: processAttachmentsMock,
} as unknown as (typeof messageManager)["attachmentManager"]; // ✅ Correct typing

// Override the private property with a mock
Object.defineProperty(messageManager, "attachmentManager", {
value: mockAttachmentManager,
writable: true,
});

await messageManager.handleMessage(mockMessage);

expect(processAttachmentsMock).toHaveBeenCalled();
});
});
7 changes: 1 addition & 6 deletions packages/plugin-discord/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ import type { IAgentRuntime } from "@elizaos/core";
import { z } from "zod";

export const discordEnvSchema = z.object({
DISCORD_APPLICATION_ID: z
.string()
.min(1, "Discord application ID is required"),
DISCORD_API_TOKEN: z.string().min(1, "Discord API token is required"),
});

Expand All @@ -15,10 +12,8 @@ export async function validateDiscordConfig(
): Promise<DiscordConfig> {
try {
const config = {
DISCORD_APPLICATION_ID:
runtime.getSetting("DISCORD_APPLICATION_ID"),
DISCORD_API_TOKEN:
runtime.getSetting("DISCORD_API_TOKEN"),
runtime.getSetting("DISCORD_API_TOKEN") || "",
tcm390 marked this conversation as resolved.
Show resolved Hide resolved
};

return discordEnvSchema.parse(config);
Expand Down
Loading
Loading