From 6a9938c01ac9e3a9b2159e623e444053ac9a9f27 Mon Sep 17 00:00:00 2001 From: ai16z Date: Thu, 5 Dec 2024 19:15:12 +0100 Subject: [PATCH] tests: adding environment and knowledge tests --- packages/core/src/tests/environment.test.ts | 182 ++++++++++++++++++++ packages/core/src/tests/knowledge.test.ts | 161 +++++++++++++++++ 2 files changed, 343 insertions(+) create mode 100644 packages/core/src/tests/environment.test.ts create mode 100644 packages/core/src/tests/knowledge.test.ts diff --git a/packages/core/src/tests/environment.test.ts b/packages/core/src/tests/environment.test.ts new file mode 100644 index 00000000000..f38b683919f --- /dev/null +++ b/packages/core/src/tests/environment.test.ts @@ -0,0 +1,182 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { validateEnv, validateCharacterConfig } from '../environment'; +import { Clients, ModelProviderName } from '../types'; + +describe('Environment Configuration', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { + ...originalEnv, + OPENAI_API_KEY: 'sk-test123', + REDPILL_API_KEY: 'test-key', + GROK_API_KEY: 'test-key', + GROQ_API_KEY: 'gsk_test123', + OPENROUTER_API_KEY: 'test-key', + GOOGLE_GENERATIVE_AI_API_KEY: 'test-key', + ELEVENLABS_XI_API_KEY: 'test-key', + }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('should validate correct environment variables', () => { + expect(() => validateEnv()).not.toThrow(); + }); + + it('should throw error for invalid OpenAI API key format', () => { + process.env.OPENAI_API_KEY = 'invalid-key'; + expect(() => validateEnv()).toThrow("OpenAI API key must start with 'sk-'"); + }); + + it('should throw error for invalid GROQ API key format', () => { + process.env.GROQ_API_KEY = 'invalid-key'; + expect(() => validateEnv()).toThrow("GROQ API key must start with 'gsk_'"); + }); + + it('should throw error for missing required keys', () => { + delete process.env.REDPILL_API_KEY; + expect(() => validateEnv()).toThrow('REDPILL_API_KEY: Required'); + }); + + it('should throw error for multiple missing required keys', () => { + delete process.env.REDPILL_API_KEY; + delete process.env.GROK_API_KEY; + delete process.env.OPENROUTER_API_KEY; + expect(() => validateEnv()).toThrow( + 'Environment validation failed:\n' + + 'REDPILL_API_KEY: Required\n' + + 'GROK_API_KEY: Required\n' + + 'OPENROUTER_API_KEY: Required' + ); + }); +}); + +describe('Character Configuration', () => { + const validCharacterConfig = { + name: 'Test Character', + modelProvider: ModelProviderName.OPENAI, + bio: 'Test bio', + lore: ['Test lore'], + messageExamples: [[ + { + user: 'user1', + content: { + text: 'Hello', + } + } + ]], + postExamples: ['Test post'], + topics: ['topic1'], + adjectives: ['friendly'], + clients: [Clients.DISCORD], + plugins: ['test-plugin'], + style: { + all: ['style1'], + chat: ['chat-style'], + post: ['post-style'] + } + }; + + it('should validate correct character configuration', () => { + expect(() => validateCharacterConfig(validCharacterConfig)).not.toThrow(); + }); + + it('should validate configuration with optional fields', () => { + const configWithOptionals = { + ...validCharacterConfig, + id: '123e4567-e89b-12d3-a456-426614174000', + system: 'Test system', + templates: { + greeting: 'Hello!' + }, + knowledge: ['fact1'], + settings: { + secrets: { + key: 'value' + }, + voice: { + model: 'test-model', + url: 'http://example.com' + } + } + }; + expect(() => validateCharacterConfig(configWithOptionals)).not.toThrow(); + }); + + it('should throw error for missing required fields', () => { + const invalidConfig = { ...validCharacterConfig }; + delete (invalidConfig as any).name; + expect(() => validateCharacterConfig(invalidConfig)).toThrow(); + }); + + it('should validate plugin objects in plugins array', () => { + const configWithPluginObjects = { + ...validCharacterConfig, + plugins: [{ + name: 'test-plugin', + description: 'Test description' + }] + }; + expect(() => validateCharacterConfig(configWithPluginObjects)).not.toThrow(); + }); + + it('should validate client-specific configurations', () => { + const configWithClientConfig = { + ...validCharacterConfig, + clientConfig: { + discord: { + shouldIgnoreBotMessages: true, + shouldIgnoreDirectMessages: false + }, + telegram: { + shouldIgnoreBotMessages: true, + shouldIgnoreDirectMessages: true + } + } + }; + expect(() => validateCharacterConfig(configWithClientConfig)).not.toThrow(); + }); + + it('should validate twitter profile configuration', () => { + const configWithTwitter = { + ...validCharacterConfig, + twitterProfile: { + username: 'testuser', + screenName: 'Test User', + bio: 'Test bio', + nicknames: ['test'] + } + }; + expect(() => validateCharacterConfig(configWithTwitter)).not.toThrow(); + }); + + it('should validate model endpoint override', () => { + const configWithEndpoint = { + ...validCharacterConfig, + modelEndpointOverride: 'custom-endpoint' + }; + expect(() => validateCharacterConfig(configWithEndpoint)).not.toThrow(); + }); + + it('should validate message examples with additional properties', () => { + const configWithComplexMessage = { + ...validCharacterConfig, + messageExamples: [[{ + user: 'user1', + content: { + text: 'Hello', + action: 'wave', + source: 'chat', + url: 'http://example.com', + inReplyTo: '123e4567-e89b-12d3-a456-426614174000', + attachments: ['file1'], + customField: 'value' + } + }]] + }; + expect(() => validateCharacterConfig(configWithComplexMessage)).not.toThrow(); + }); +}); diff --git a/packages/core/src/tests/knowledge.test.ts b/packages/core/src/tests/knowledge.test.ts new file mode 100644 index 00000000000..436954f975b --- /dev/null +++ b/packages/core/src/tests/knowledge.test.ts @@ -0,0 +1,161 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import knowledge from '../knowledge'; +import { AgentRuntime } from '../runtime'; +import { KnowledgeItem, Memory } from '../types'; +import { getEmbeddingZeroVector } from '../embedding'; + +// Mock dependencies +vi.mock('../embedding', () => ({ + embed: vi.fn().mockResolvedValue(new Float32Array(1536).fill(0)), + getEmbeddingZeroVector: vi.fn().mockReturnValue(new Float32Array(1536).fill(0)) +})); + +vi.mock('../generation', () => ({ + splitChunks: vi.fn().mockImplementation(async (text) => [text]) +})); + +vi.mock('../uuid', () => ({ + stringToUuid: vi.fn().mockImplementation((str) => str) +})); + +describe('Knowledge Module', () => { + describe('preprocess', () => { + it('should handle invalid inputs', () => { + expect(knowledge.preprocess(null)).toBe(''); + expect(knowledge.preprocess(undefined)).toBe(''); + expect(knowledge.preprocess('')).toBe(''); + }); + + it('should remove code blocks and inline code', () => { + const input = 'Here is some code: ```const x = 1;``` and `inline code`'; + expect(knowledge.preprocess(input)).toBe('here is some code: and'); + }); + + it('should handle markdown formatting', () => { + const input = '# Header\n## Subheader\n[Link](http://example.com)\n![Image](image.jpg)'; + expect(knowledge.preprocess(input)).toBe('header subheader link image'); + }); + + it('should simplify URLs', () => { + const input = 'Visit https://www.example.com/path?param=value'; + expect(knowledge.preprocess(input)).toBe('visit example.com/path?param=value'); + }); + + it('should remove Discord mentions and HTML tags', () => { + const input = 'Hello <@123456789> and
HTML content
'; + expect(knowledge.preprocess(input)).toBe('hello and html content'); + }); + + it('should normalize whitespace and newlines', () => { + const input = 'Multiple spaces\n\n\nand\nnewlines'; + expect(knowledge.preprocess(input)).toBe('multiple spaces and newlines'); + }); + + it('should remove comments', () => { + const input = '/* Block comment */ Normal text // Line comment'; + expect(knowledge.preprocess(input)).toBe('normal text'); + }); + }); + + describe('get and set', () => { + let mockRuntime: AgentRuntime; + + beforeEach(() => { + mockRuntime = { + agentId: 'test-agent', + knowledgeManager: { + searchMemoriesByEmbedding: vi.fn().mockResolvedValue([ + { + content: { text: 'test fragment', source: 'source1' }, + similarity: 0.9 + } + ]), + createMemory: vi.fn().mockResolvedValue(undefined) + }, + documentsManager: { + getMemoryById: vi.fn().mockResolvedValue({ + id: 'source1', + content: { text: 'test document' } + }), + createMemory: vi.fn().mockResolvedValue(undefined) + } + } as unknown as AgentRuntime; + }); + + describe('get', () => { + it('should handle invalid messages', async () => { + const invalidMessage = {} as Memory; + const result = await knowledge.get(mockRuntime, invalidMessage); + expect(result).toEqual([]); + }); + + it('should retrieve knowledge items based on message content', async () => { + const message: Memory = { + agentId: 'test-agent', + content: { text: 'test query' } + } as unknown as Memory; + + const result = await knowledge.get(mockRuntime, message); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + id: 'source1', + content: { text: 'test document' } + }); + }); + + it('should handle empty processed text', async () => { + const message: Memory = { + agentId: 'test-agent', + content: { text: '```code only```' } + } as unknown as Memory; + + const result = await knowledge.get(mockRuntime, message); + expect(result).toEqual([]); + }); + }); + + describe('set', () => { + it('should store knowledge item and its fragments', async () => { + const item: KnowledgeItem = { + id: 'test-id-1234-5678-9101-112131415161', + content: { text: 'test content' } + }; + + await knowledge.set(mockRuntime, item); + + // Check if document was created + expect(mockRuntime.documentsManager.createMemory).toHaveBeenCalledWith( + expect.objectContaining({ + id: item.id, + content: item.content, + embedding: getEmbeddingZeroVector() + }) + ); + + // Check if fragment was created + expect(mockRuntime.knowledgeManager.createMemory).toHaveBeenCalledWith( + expect.objectContaining({ + content: { + source: item.id, + text: expect.any(String) + }, + embedding: expect.any(Float32Array) + }) + ); + }); + + it('should use default chunk size and bleed', async () => { + const item: KnowledgeItem = { + id: 'test-id-1234-5678-9101-112131415161', + content: { text: 'test content' } + }; + + await knowledge.set(mockRuntime, item); + + // Verify default parameters were used + expect(mockRuntime.knowledgeManager.createMemory).toHaveBeenCalledTimes(1); + }); + }); + }); +});