8ad3b97b7e
- Add bot management system with creation, suspension, and reinstatement functionality - Implement autonomous bot posting with scheduling, rate limiting, and content generation - Add content fetching system supporting RSS feeds and multiple content sources - Implement LLM-based content generation with customizable bot personalities - Add mention handling and automated response system for bot interactions - Implement API key management with encryption using AUTH_SECRET for simplified deployment - Add comprehensive bot logging system for activity tracking and error monitoring - Create bot administration pages and settings UI for managing bot configurations - Add database migrations for bot system schema including users, sources, and content items - Implement cron job system for automated bot operations and scheduled tasks - Add extensive test coverage with unit and property-based tests for core bot modules - Simplify encryption by deriving keys from AUTH_SECRET instead of separate environment variable - Implement automatic content fetching on post trigger with retry logic - Add Reddit-specific link preview handling using oEmbed API for reliable metadata extraction - Create utility scripts for bot inspection and cleanup operations - Add comprehensive bot system documentation and improvement tracking
465 lines
16 KiB
TypeScript
465 lines
16 KiB
TypeScript
/**
|
|
* Unit tests for Personality Configuration Module
|
|
*
|
|
* Tests validation, serialization, and prompt building functionality.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
validateSystemPrompt,
|
|
validateTemperature,
|
|
validateMaxTokens,
|
|
validateResponseStyle,
|
|
validatePersonalityConfig,
|
|
isValidPersonalityConfig,
|
|
serializePersonalityConfig,
|
|
deserializePersonalityConfig,
|
|
buildPromptWithPersonality,
|
|
getPersonalityPreset,
|
|
getAllPersonalityPresets,
|
|
createDefaultPersonalityConfig,
|
|
PersonalityConfig,
|
|
PersonalityError,
|
|
MIN_SYSTEM_PROMPT_LENGTH,
|
|
MAX_SYSTEM_PROMPT_LENGTH,
|
|
MIN_TEMPERATURE,
|
|
MAX_TEMPERATURE,
|
|
MIN_MAX_TOKENS,
|
|
MAX_MAX_TOKENS,
|
|
} from './personality';
|
|
|
|
describe('Personality Configuration Module', () => {
|
|
// ============================================
|
|
// SYSTEM PROMPT VALIDATION
|
|
// ============================================
|
|
|
|
describe('validateSystemPrompt', () => {
|
|
it('should accept valid system prompts', () => {
|
|
const errors = validateSystemPrompt('You are a helpful assistant bot.');
|
|
expect(errors).toHaveLength(0);
|
|
});
|
|
|
|
it('should reject undefined system prompt', () => {
|
|
const errors = validateSystemPrompt(undefined);
|
|
expect(errors).toContain('System prompt is required');
|
|
});
|
|
|
|
it('should reject null system prompt', () => {
|
|
const errors = validateSystemPrompt(null);
|
|
expect(errors).toContain('System prompt is required');
|
|
});
|
|
|
|
it('should reject non-string system prompt', () => {
|
|
const errors = validateSystemPrompt(123);
|
|
expect(errors).toContain('System prompt must be a string');
|
|
});
|
|
|
|
it('should reject system prompt that is too short', () => {
|
|
const errors = validateSystemPrompt('Hi');
|
|
expect(errors).toContain(`System prompt must be at least ${MIN_SYSTEM_PROMPT_LENGTH} characters`);
|
|
});
|
|
|
|
it('should reject system prompt that is too long', () => {
|
|
const longPrompt = 'a'.repeat(MAX_SYSTEM_PROMPT_LENGTH + 1);
|
|
const errors = validateSystemPrompt(longPrompt);
|
|
expect(errors).toContain(`System prompt must be ${MAX_SYSTEM_PROMPT_LENGTH} characters or less`);
|
|
});
|
|
|
|
it('should accept system prompt at minimum length', () => {
|
|
const minPrompt = 'a'.repeat(MIN_SYSTEM_PROMPT_LENGTH);
|
|
const errors = validateSystemPrompt(minPrompt);
|
|
expect(errors).toHaveLength(0);
|
|
});
|
|
|
|
it('should accept system prompt at maximum length', () => {
|
|
const maxPrompt = 'a'.repeat(MAX_SYSTEM_PROMPT_LENGTH);
|
|
const errors = validateSystemPrompt(maxPrompt);
|
|
expect(errors).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// TEMPERATURE VALIDATION
|
|
// ============================================
|
|
|
|
describe('validateTemperature', () => {
|
|
it('should accept valid temperature values', () => {
|
|
expect(validateTemperature(0)).toHaveLength(0);
|
|
expect(validateTemperature(0.5)).toHaveLength(0);
|
|
expect(validateTemperature(1)).toHaveLength(0);
|
|
expect(validateTemperature(1.5)).toHaveLength(0);
|
|
expect(validateTemperature(2)).toHaveLength(0);
|
|
});
|
|
|
|
it('should reject undefined temperature', () => {
|
|
const errors = validateTemperature(undefined);
|
|
expect(errors).toContain('Temperature is required');
|
|
});
|
|
|
|
it('should reject null temperature', () => {
|
|
const errors = validateTemperature(null);
|
|
expect(errors).toContain('Temperature is required');
|
|
});
|
|
|
|
it('should reject non-number temperature', () => {
|
|
const errors = validateTemperature('0.5');
|
|
expect(errors).toContain('Temperature must be a number');
|
|
});
|
|
|
|
it('should reject NaN temperature', () => {
|
|
const errors = validateTemperature(NaN);
|
|
expect(errors).toContain('Temperature must be a valid number');
|
|
});
|
|
|
|
it('should reject temperature below minimum', () => {
|
|
const errors = validateTemperature(-0.1);
|
|
expect(errors).toContain(`Temperature must be at least ${MIN_TEMPERATURE}`);
|
|
});
|
|
|
|
it('should reject temperature above maximum', () => {
|
|
const errors = validateTemperature(2.1);
|
|
expect(errors).toContain(`Temperature must be at most ${MAX_TEMPERATURE}`);
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// MAX TOKENS VALIDATION
|
|
// ============================================
|
|
|
|
describe('validateMaxTokens', () => {
|
|
it('should accept valid maxTokens values', () => {
|
|
expect(validateMaxTokens(1)).toHaveLength(0);
|
|
expect(validateMaxTokens(100)).toHaveLength(0);
|
|
expect(validateMaxTokens(1000)).toHaveLength(0);
|
|
expect(validateMaxTokens(100000)).toHaveLength(0);
|
|
});
|
|
|
|
it('should reject undefined maxTokens', () => {
|
|
const errors = validateMaxTokens(undefined);
|
|
expect(errors).toContain('Max tokens is required');
|
|
});
|
|
|
|
it('should reject null maxTokens', () => {
|
|
const errors = validateMaxTokens(null);
|
|
expect(errors).toContain('Max tokens is required');
|
|
});
|
|
|
|
it('should reject non-number maxTokens', () => {
|
|
const errors = validateMaxTokens('100');
|
|
expect(errors).toContain('Max tokens must be a number');
|
|
});
|
|
|
|
it('should reject NaN maxTokens', () => {
|
|
const errors = validateMaxTokens(NaN);
|
|
expect(errors).toContain('Max tokens must be a valid number');
|
|
});
|
|
|
|
it('should reject non-integer maxTokens', () => {
|
|
const errors = validateMaxTokens(100.5);
|
|
expect(errors).toContain('Max tokens must be an integer');
|
|
});
|
|
|
|
it('should reject maxTokens below minimum', () => {
|
|
const errors = validateMaxTokens(0);
|
|
expect(errors).toContain(`Max tokens must be at least ${MIN_MAX_TOKENS}`);
|
|
});
|
|
|
|
it('should reject maxTokens above maximum', () => {
|
|
const errors = validateMaxTokens(100001);
|
|
expect(errors).toContain(`Max tokens must be at most ${MAX_MAX_TOKENS}`);
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// RESPONSE STYLE VALIDATION
|
|
// ============================================
|
|
|
|
describe('validateResponseStyle', () => {
|
|
it('should accept undefined response style (optional)', () => {
|
|
const errors = validateResponseStyle(undefined);
|
|
expect(errors).toHaveLength(0);
|
|
});
|
|
|
|
it('should accept null response style (optional)', () => {
|
|
const errors = validateResponseStyle(null);
|
|
expect(errors).toHaveLength(0);
|
|
});
|
|
|
|
it('should accept valid response styles', () => {
|
|
expect(validateResponseStyle('formal')).toHaveLength(0);
|
|
expect(validateResponseStyle('casual')).toHaveLength(0);
|
|
expect(validateResponseStyle('custom style')).toHaveLength(0);
|
|
});
|
|
|
|
it('should reject non-string response style', () => {
|
|
const errors = validateResponseStyle(123);
|
|
expect(errors).toContain('Response style must be a string');
|
|
});
|
|
|
|
it('should reject empty response style', () => {
|
|
const errors = validateResponseStyle('');
|
|
expect(errors).toContain('Response style cannot be empty if provided');
|
|
});
|
|
|
|
it('should reject whitespace-only response style', () => {
|
|
const errors = validateResponseStyle(' ');
|
|
expect(errors).toContain('Response style cannot be empty if provided');
|
|
});
|
|
|
|
it('should reject response style that is too long', () => {
|
|
const longStyle = 'a'.repeat(101);
|
|
const errors = validateResponseStyle(longStyle);
|
|
expect(errors).toContain('Response style must be 100 characters or less');
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// COMPLETE CONFIG VALIDATION
|
|
// ============================================
|
|
|
|
describe('validatePersonalityConfig', () => {
|
|
const validConfig: PersonalityConfig = {
|
|
systemPrompt: 'You are a helpful assistant bot.',
|
|
temperature: 0.7,
|
|
maxTokens: 500,
|
|
};
|
|
|
|
it('should accept valid configuration', () => {
|
|
const result = validatePersonalityConfig(validConfig);
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
it('should accept valid configuration with response style', () => {
|
|
const result = validatePersonalityConfig({
|
|
...validConfig,
|
|
responseStyle: 'formal',
|
|
});
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
it('should reject non-object configuration', () => {
|
|
const result = validatePersonalityConfig('not an object');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Personality configuration must be an object');
|
|
});
|
|
|
|
it('should reject null configuration', () => {
|
|
const result = validatePersonalityConfig(null);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Personality configuration must be an object');
|
|
});
|
|
|
|
it('should collect all validation errors', () => {
|
|
const result = validatePersonalityConfig({
|
|
systemPrompt: 'Hi', // too short
|
|
temperature: 3, // too high
|
|
maxTokens: 0, // too low
|
|
responseStyle: '', // empty
|
|
});
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors.length).toBeGreaterThan(1);
|
|
});
|
|
});
|
|
|
|
describe('isValidPersonalityConfig', () => {
|
|
it('should return true for valid config', () => {
|
|
const config: PersonalityConfig = {
|
|
systemPrompt: 'You are a helpful assistant bot.',
|
|
temperature: 0.7,
|
|
maxTokens: 500,
|
|
};
|
|
expect(isValidPersonalityConfig(config)).toBe(true);
|
|
});
|
|
|
|
it('should return false for invalid config', () => {
|
|
expect(isValidPersonalityConfig(null)).toBe(false);
|
|
expect(isValidPersonalityConfig({ systemPrompt: 'Hi' })).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// SERIALIZATION
|
|
// ============================================
|
|
|
|
describe('serializePersonalityConfig', () => {
|
|
it('should serialize config to JSON string', () => {
|
|
const config: PersonalityConfig = {
|
|
systemPrompt: 'You are a helpful assistant bot.',
|
|
temperature: 0.7,
|
|
maxTokens: 500,
|
|
};
|
|
const serialized = serializePersonalityConfig(config);
|
|
expect(typeof serialized).toBe('string');
|
|
expect(JSON.parse(serialized)).toEqual(config);
|
|
});
|
|
|
|
it('should preserve all fields including optional ones', () => {
|
|
const config: PersonalityConfig = {
|
|
systemPrompt: 'You are a helpful assistant bot.',
|
|
temperature: 0.7,
|
|
maxTokens: 500,
|
|
responseStyle: 'formal',
|
|
};
|
|
const serialized = serializePersonalityConfig(config);
|
|
const parsed = JSON.parse(serialized);
|
|
expect(parsed.responseStyle).toBe('formal');
|
|
});
|
|
});
|
|
|
|
describe('deserializePersonalityConfig', () => {
|
|
it('should deserialize valid JSON to config', () => {
|
|
const config: PersonalityConfig = {
|
|
systemPrompt: 'You are a helpful assistant bot.',
|
|
temperature: 0.7,
|
|
maxTokens: 500,
|
|
};
|
|
const json = JSON.stringify(config);
|
|
const deserialized = deserializePersonalityConfig(json);
|
|
expect(deserialized).toEqual(config);
|
|
});
|
|
|
|
it('should throw PersonalityError for invalid JSON', () => {
|
|
expect(() => deserializePersonalityConfig('not json')).toThrow(PersonalityError);
|
|
});
|
|
|
|
it('should throw PersonalityError for invalid config', () => {
|
|
const invalidJson = JSON.stringify({ systemPrompt: 'Hi' });
|
|
expect(() => deserializePersonalityConfig(invalidJson)).toThrow(PersonalityError);
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// PROMPT BUILDING
|
|
// ============================================
|
|
|
|
describe('buildPromptWithPersonality', () => {
|
|
const personality: PersonalityConfig = {
|
|
systemPrompt: 'You are a helpful assistant bot.',
|
|
temperature: 0.7,
|
|
maxTokens: 500,
|
|
};
|
|
|
|
it('should build basic prompt with personality', () => {
|
|
const result = buildPromptWithPersonality(personality);
|
|
expect(result.systemMessage).toContain('You are a helpful assistant bot.');
|
|
expect(result.temperature).toBe(0.7);
|
|
expect(result.maxTokens).toBe(500);
|
|
expect(result.messages).toHaveLength(1);
|
|
expect(result.messages[0].role).toBe('system');
|
|
});
|
|
|
|
it('should include response style in system message', () => {
|
|
const personalityWithStyle: PersonalityConfig = {
|
|
...personality,
|
|
responseStyle: 'formal',
|
|
};
|
|
const result = buildPromptWithPersonality(personalityWithStyle);
|
|
expect(result.systemMessage).toContain('Response Style: formal');
|
|
});
|
|
|
|
it('should include context in system message', () => {
|
|
const result = buildPromptWithPersonality(personality, {
|
|
context: 'This is additional context.',
|
|
});
|
|
expect(result.systemMessage).toContain('Additional Context:');
|
|
expect(result.systemMessage).toContain('This is additional context.');
|
|
});
|
|
|
|
it('should include user message', () => {
|
|
const result = buildPromptWithPersonality(personality, {
|
|
userMessage: 'Hello, how are you?',
|
|
});
|
|
expect(result.messages).toHaveLength(2);
|
|
expect(result.messages[1].role).toBe('user');
|
|
expect(result.messages[1].content).toBe('Hello, how are you?');
|
|
});
|
|
|
|
it('should include source content', () => {
|
|
const result = buildPromptWithPersonality(personality, {
|
|
sourceContent: {
|
|
title: 'Test Article',
|
|
content: 'Article content here.',
|
|
url: 'https://example.com/article',
|
|
},
|
|
});
|
|
expect(result.messages).toHaveLength(2);
|
|
expect(result.messages[1].role).toBe('user');
|
|
expect(result.messages[1].content).toContain('Test Article');
|
|
expect(result.messages[1].content).toContain('Article content here.');
|
|
expect(result.messages[1].content).toContain('https://example.com/article');
|
|
});
|
|
|
|
it('should include conversation history', () => {
|
|
const result = buildPromptWithPersonality(personality, {
|
|
conversationHistory: [
|
|
{ role: 'user', content: 'Hello' },
|
|
{ role: 'assistant', content: 'Hi there!' },
|
|
],
|
|
userMessage: 'How are you?',
|
|
});
|
|
expect(result.messages).toHaveLength(4);
|
|
expect(result.messages[1].role).toBe('user');
|
|
expect(result.messages[1].content).toBe('Hello');
|
|
expect(result.messages[2].role).toBe('assistant');
|
|
expect(result.messages[2].content).toBe('Hi there!');
|
|
expect(result.messages[3].role).toBe('user');
|
|
expect(result.messages[3].content).toBe('How are you?');
|
|
});
|
|
});
|
|
|
|
// ============================================
|
|
// PRESETS
|
|
// ============================================
|
|
|
|
describe('getPersonalityPreset', () => {
|
|
it('should return preset by ID', () => {
|
|
const preset = getPersonalityPreset('news-curator');
|
|
expect(preset).toBeDefined();
|
|
expect(preset?.name).toBe('News Curator');
|
|
});
|
|
|
|
it('should return undefined for unknown preset', () => {
|
|
const preset = getPersonalityPreset('unknown-preset');
|
|
expect(preset).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('getAllPersonalityPresets', () => {
|
|
it('should return all presets', () => {
|
|
const presets = getAllPersonalityPresets();
|
|
expect(presets.length).toBeGreaterThan(0);
|
|
expect(presets.every(p => p.id && p.name && p.config)).toBe(true);
|
|
});
|
|
|
|
it('should return a copy of presets array', () => {
|
|
const presets1 = getAllPersonalityPresets();
|
|
const presets2 = getAllPersonalityPresets();
|
|
expect(presets1).not.toBe(presets2);
|
|
});
|
|
|
|
it('should have valid configs for all presets', () => {
|
|
const presets = getAllPersonalityPresets();
|
|
for (const preset of presets) {
|
|
expect(isValidPersonalityConfig(preset.config)).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('createDefaultPersonalityConfig', () => {
|
|
it('should create a valid default config', () => {
|
|
const config = createDefaultPersonalityConfig();
|
|
expect(isValidPersonalityConfig(config)).toBe(true);
|
|
});
|
|
|
|
it('should have reasonable default values', () => {
|
|
const config = createDefaultPersonalityConfig();
|
|
expect(config.systemPrompt.length).toBeGreaterThan(0);
|
|
expect(config.temperature).toBeGreaterThanOrEqual(0);
|
|
expect(config.temperature).toBeLessThanOrEqual(2);
|
|
expect(config.maxTokens).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|