---
description: testing guidelines
globs:
alwaysApply: false
---
# Testing Guidelines for MCP TypeScript Project
This guide covers testing patterns, best practices, and requirements for the Cursor Conversations MCP project using Vitest.
## **Test File Organization**
- **File Naming Convention**
- Use `.test.ts` suffix for test files (e.g., `cache.test.ts`)
- Place test files alongside their corresponding source files
- Mirror the source file structure in test organization
```typescript
// ✅ DO: Proper test file structure
src/
utils/
cache.ts
cache.test.ts // Test file next to source
validation.ts
validation.test.ts
database/
reader.ts
reader.test.ts // Future test file
```
- **Test Suite Structure**
- Use descriptive `describe` blocks for logical grouping
- Group related functionality together
- Use nested `describe` blocks for complex modules
```typescript
// ✅ DO: Well-organized test structure
describe('CursorDatabaseReader', () => {
describe('Connection Management', () => {
it('should connect to database successfully', () => {});
it('should handle connection errors', () => {});
});
describe('Conversation Retrieval', () => {
it('should get conversation by ID', () => {});
it('should return null for non-existent conversation', () => {});
});
});
```
## **Vitest Configuration Patterns**
- **Test Setup and Teardown**
- Use `beforeEach` for test isolation
- Use `afterEach` for cleanup
- Use `beforeAll`/`afterAll` for expensive setup
```typescript
// ✅ DO: Proper setup and teardown
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
describe('Cache', () => {
let cache: Cache;
beforeEach(() => {
cache = new Cache();
});
afterEach(() => {
cache.destroy();
vi.clearAllMocks();
});
});
```
- **Mock Management**
- Import `vi` from 'vitest' for mocking
- Use `vi.useFakeTimers()` for time-based tests
- Clear mocks in `afterEach` to prevent test pollution
```typescript
// ✅ DO: Proper timer mocking
describe('TTL (Time-To-Live)', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should expire entries after TTL', () => {
cache = new Cache({ defaultTTL: 1000 });
cache.set('key1', 'value1');
vi.advanceTimersByTime(1001);
expect(cache.get('key1')).toBeUndefined();
});
});
```
## **Error Testing Patterns**
- **Error Class Testing**
- Test error message construction
- Test error inheritance hierarchy
- Test custom error properties
- Test error stack traces
```typescript
// ✅ DO: Comprehensive error testing
describe('DatabaseConnectionError', () => {
it('should create database connection error', () => {
const dbPath = '/path/to/database.db';
const error = new DatabaseConnectionError(dbPath);
expect(error.message).toBe(`Database error: Failed to connect to database at path: ${dbPath}`);
expect(error.code).toBe('DATABASE_CONNECTION_ERROR');
expect(error.statusCode).toBe(500);
expect(error).toBeInstanceOf(DatabaseError);
expect(error).toBeInstanceOf(MCPError);
});
it('should create error with original cause', () => {
const dbPath = '/path/to/database.db';
const originalError = new Error('Permission denied');
const error = new DatabaseConnectionError(dbPath, originalError);
expect(error.message).toContain('Permission denied');
expect(error.stack).toContain('Caused by:');
});
});
```
- **Error Handling in Functions**
- Test both success and failure paths
- Test error propagation
- Test error transformation
```typescript
// ✅ DO: Test error scenarios
describe('parseConversationJSON', () => {
it('should parse valid conversation JSON', () => {
const validJson = JSON.stringify({ composerId: 'test-123' });
const result = parser.parseConversationJSON(validJson);
expect(result.composerId).toBe('test-123');
});
it('should throw error for invalid JSON', () => {
const invalidJson = '{ invalid json }';
expect(() => parser.parseConversationJSON(invalidJson))
.toThrow('Failed to parse conversation JSON');
});
it('should throw error for missing required fields', () => {
const incompleteJson = JSON.stringify({ conversation: [] });
expect(() => parser.parseConversationJSON(incompleteJson))
.toThrow('Invalid conversation format');
});
});
```
## **Database Testing Patterns**
- **Database Reader Testing**
- Mock database connections for unit tests
- Test both legacy and modern conversation formats
- Test error handling for database operations
- Test caching behavior
```typescript
// ✅ DO: Database testing with mocks
describe('CursorDatabaseReader', () => {
let reader: CursorDatabaseReader;
let mockDb: any;
beforeEach(() => {
mockDb = {
prepare: vi.fn(),
close: vi.fn()
};
reader = new CursorDatabaseReader();
// Mock the database connection
});
it('should handle database connection errors', async () => {
const invalidPath = '/nonexistent/path.db';
reader = new CursorDatabaseReader({ dbPath: invalidPath });
await expect(reader.connect()).rejects.toThrow(DatabaseConnectionError);
});
});
```
- **Parser Testing**
- Test parsing of different conversation formats
- Test extraction of specific data (messages, code blocks, files)
- Test malformed data handling
```typescript
// ✅ DO: Parser testing with real data structures
describe('ConversationParser', () => {
it('should extract messages from legacy conversation', () => {
const legacyConversation = {
composerId: 'legacy-123',
conversation: [
{
type: 1,
bubbleId: 'bubble-1',
text: 'First message',
relevantFiles: [],
suggestedCodeBlocks: []
}
]
};
const messages = parser.extractMessages(legacyConversation);
expect(messages).toHaveLength(1);
expect(messages[0].text).toBe('First message');
});
});
```
## **Utility Function Testing**
- **Validation Testing**
- Test all validation rules
- Test edge cases and boundary conditions
- Test sanitization functions
```typescript
// ✅ DO: Comprehensive validation testing
describe('sanitizeLimit', () => {
it('should return provided limit when valid', () => {
expect(sanitizeLimit(50, 100)).toBe(50);
});
it('should return default when limit is undefined', () => {
expect(sanitizeLimit(undefined, 100)).toBe(100);
});
it('should clamp to maximum when limit exceeds max', () => {
expect(sanitizeLimit(150, 100)).toBe(100);
});
it('should handle negative values', () => {
expect(sanitizeLimit(-10, 100)).toBe(1);
});
});
```
- **Cache Testing**
- Test basic operations (get, set, delete, clear)
- Test TTL functionality with fake timers
- Test eviction policies (LRU, FIFO)
- Test size limits and cleanup
```typescript
// ✅ DO: Cache testing with different scenarios
describe('Cache Eviction', () => {
it('should evict entries when max size is reached (LRU)', () => {
cache = new Cache({ maxSize: 2, evictionPolicy: 'lru' });
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.get('key1'); // Make key1 more recently used
cache.set('key3', 'value3'); // Should evict key2
expect(cache.has('key1')).toBe(true);
expect(cache.has('key2')).toBe(false);
expect(cache.has('key3')).toBe(true);
});
});
```
## **MCP Tool Testing**
- **Tool Function Testing**
- Test parameter validation using Zod schemas
- Test successful operations
- Test error responses
- Mock external dependencies
```typescript
// ✅ DO: MCP tool testing (future pattern)
describe('ConversationTools', () => {
describe('listConversations', () => {
it('should return conversation summaries', async () => {
const mockReader = {
getConversationIds: vi.fn().mockResolvedValue(['conv1', 'conv2']),
getConversationSummary: vi.fn().mockResolvedValue({
composerId: 'conv1',
format: 'legacy',
messageCount: 5
})
};
const result = await listConversations({
limit: 10,
format: 'both'
}, mockReader);
expect(result.content).toHaveLength(1);
expect(result.content[0].type).toBe('text');
});
it('should handle database errors gracefully', async () => {
const mockReader = {
getConversationIds: vi.fn().mockRejectedValue(new DatabaseError('Connection failed'))
};
const result = await listConversations({}, mockReader);
expect(result.content[0].text).toContain('Error');
});
});
});
```
## **Test Data Management**
- **Test Data Creation**
- Create realistic test data that matches actual formats
- Use factory functions for complex objects
- Keep test data minimal but representative
```typescript
// ✅ DO: Test data factories
function createLegacyConversation(overrides = {}) {
return {
composerId: 'test-123',
hasLoaded: true,
text: '',
richText: '',
conversation: [
{
type: 1,
bubbleId: 'bubble-1',
text: 'Test message',
relevantFiles: [],
suggestedCodeBlocks: [],
attachedFoldersNew: []
}
],
...overrides
};
}
function createModernConversation(overrides = {}) {
return {
composerId: 'test-456',
_v: 2,
hasLoaded: true,
text: '',
richText: '',
fullConversationHeadersOnly: [
{
type: 1,
bubbleId: 'bubble-1'
}
],
...overrides
};
}
```
## **Assertion Patterns**
- **Specific Assertions**
- Use specific matchers for better error messages
- Test object properties individually when needed
- Use `toMatchObject` for partial matching
```typescript
// ✅ DO: Specific and meaningful assertions
expect(error).toBeInstanceOf(ValidationError);
expect(error.code).toBe('VALIDATION_ERROR');
expect(error.statusCode).toBe(400);
expect(error.field).toBe('email');
// ✅ DO: Partial object matching
expect(result).toMatchObject({
composerId: 'test-123',
format: 'legacy',
messageCount: expect.any(Number)
});
// ✅ DO: Array content testing
expect(files).toContain('src/utils/cache.ts');
expect(files).toHaveLength(3);
```
## **Performance Testing**
- **Large Data Sets**
- Test with realistic data sizes
- Test memory usage patterns
- Test timeout scenarios
```typescript
// ✅ DO: Performance-aware testing
it('should handle large conversation lists efficiently', async () => {
const largeConversationList = Array.from({ length: 1000 }, (_, i) => `conv-${i}`);
const startTime = Date.now();
const result = await reader.getConversationIds({ limit: 1000 });
const duration = Date.now() - startTime;
expect(result).toHaveLength(1000);
expect(duration).toBeLessThan(5000); // Should complete within 5 seconds
});
```
## **Integration Testing Considerations**
- **Database Integration**
- Use test databases or in-memory SQLite for integration tests
- Test actual file system operations when needed
- Clean up test artifacts
```typescript
// ✅ DO: Integration test setup (future pattern)
describe('Database Integration', () => {
let testDbPath: string;
let reader: CursorDatabaseReader;
beforeAll(async () => {
testDbPath = path.join(__dirname, 'test.db');
// Set up test database
});
afterAll(async () => {
// Clean up test database
if (fs.existsSync(testDbPath)) {
fs.unlinkSync(testDbPath);
}
});
});
```
## **Test Coverage Requirements**
- **Minimum Coverage Targets**
- Aim for 90%+ line coverage on utility functions
- Aim for 80%+ line coverage on database operations
- Ensure all error paths are tested
- Test all public API methods
- **Critical Areas for Testing**
- Error handling and custom error classes
- Data parsing and validation
- Cache operations and eviction
- Database connection and query operations
- MCP tool parameter validation and responses
## **Running Tests**
- **Test Commands**
- `yarn test` - Run all tests
- `yarn test:ui` - Run tests with UI interface
- `yarn test --watch` - Run tests in watch mode
- `yarn test --coverage` - Run tests with coverage report
- **Test Configuration**
- Tests run in Node.js environment
- Global test utilities available (describe, it, expect)
- Test files: `src/**/*.{test,spec}.{ts,js}`
## **File References**
- Test configuration: [vitest.config.ts](mdc:vitest.config.ts)
- Example test files:
- [src/utils/cache.test.ts](mdc:src/utils/cache.test.ts)
- [src/utils/errors.test.ts](mdc:src/utils/errors.test.ts)
- [src/database/parser.test.ts](mdc:src/database/parser.test.ts)
- Source files needing tests:
- [src/database/reader.ts](mdc:src/database/reader.ts)
- [src/tools/conversation-tools.ts](mdc:src/tools/conversation-tools.ts)
- [src/server.ts](mdc:src/server.ts)