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

test: adding environment and knowledge tests #862

Merged
merged 1 commit into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
182 changes: 182 additions & 0 deletions packages/core/src/tests/environment.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
161 changes: 161 additions & 0 deletions packages/core/src/tests/knowledge.test.ts
Original file line number Diff line number Diff line change
@@ -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 <div>HTML content</div>';
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);
});
});
});
});
Loading