Skip to main content
Glama
automation.test.ts13.9 kB
// ============================================================================= // kivv - Automation Worker Integration Tests // ============================================================================= // Tests for the checkpointed cron automation system // Note: These tests use mocks to avoid real API calls // ============================================================================= import { describe, it as test, expect, beforeEach, vi } from 'vitest'; const mock = vi.fn; // ============================================================================= // Test Configuration // ============================================================================= /** * Mock environment for testing */ interface MockEnv { DB: any; CACHE: any; PAPERS: any; CLAUDE_API_KEY: string; CRON_SECRET: string; } /** * Create mock D1 database for testing */ function createMockD1() { const data: Record<string, any[]> = { users: [ { id: 1, username: 'jeff', email: 'jeff@example.com', api_key: 'key1', is_active: 1 }, { id: 2, username: 'wife', email: 'wife@example.com', api_key: 'key2', is_active: 1 } ], topics: [ { id: 1, user_id: 1, topic_name: 'AI Safety', arxiv_query: 'cat:cs.AI AND safety', enabled: 1 }, { id: 2, user_id: 2, topic_name: 'Healthcare AI', arxiv_query: 'cat:cs.AI AND healthcare', enabled: 1 } ], papers: [], user_paper_status: [] }; return { prepare: (query: string) => ({ bind: (...params: any[]) => ({ all: async () => { // Mock database queries if (query.includes('SELECT * FROM users')) { return { results: data.users }; } if (query.includes('SELECT * FROM topics')) { const userId = params[0]; return { results: data.topics.filter(t => t.user_id === userId && t.enabled === 1) }; } if (query.includes('SELECT id FROM papers WHERE arxiv_id')) { const arxivId = params[0]; const paper = data.papers.find((p: any) => p.arxiv_id === arxivId); return { results: paper ? [paper] : [] }; } return { results: [] }; }, first: async () => { if (query.includes('SELECT id FROM papers WHERE arxiv_id')) { const arxivId = params[0]; return data.papers.find((p: any) => p.arxiv_id === arxivId) || null; } return null; }, run: async () => { // Mock insert/update operations if (query.includes('INSERT INTO papers')) { const mockPaper = { id: data.papers.length + 1, arxiv_id: params[0], title: params[1] }; data.papers.push(mockPaper); } return { success: true }; } }) }) }; } /** * Create mock KV namespace for testing */ function createMockKV() { const storage: Record<string, string> = {}; return { get: async (key: string) => storage[key] || null, put: async (key: string, value: string, options?: any) => { storage[key] = value; }, delete: async (key: string) => { delete storage[key]; } }; } /** * Create mock R2 bucket for testing */ function createMockR2() { return { put: async (key: string, value: any) => ({ success: true }), get: async (key: string) => null, delete: async (key: string) => ({ success: true }) }; } // ============================================================================= // Test Suite: Checkpoint System // ============================================================================= describe('Checkpoint System', () => { let mockEnv: MockEnv; beforeEach(() => { mockEnv = { DB: createMockD1(), CACHE: createMockKV(), PAPERS: createMockR2(), CLAUDE_API_KEY: 'test-api-key', CRON_SECRET: 'test-secret' }; }); test('should create new checkpoint on first run', async () => { const today = new Date().toISOString().split('T')[0]; const checkpointKey = `checkpoint:automation:${today}`; // Simulate saving a checkpoint const checkpoint = { date: today, users_processed: 0, papers_found: 0, papers_summarized: 0, total_cost: 0, errors: [], completed: false }; await mockEnv.CACHE.put(checkpointKey, JSON.stringify(checkpoint)); // Verify checkpoint was saved const saved = await mockEnv.CACHE.get(checkpointKey); expect(saved).toBeDefined(); const parsed = JSON.parse(saved!); expect(parsed.date).toBe(today); expect(parsed.users_processed).toBe(0); expect(parsed.completed).toBe(false); }); test('should load existing checkpoint on subsequent runs', async () => { const today = new Date().toISOString().split('T')[0]; const checkpointKey = `checkpoint:automation:${today}`; // Create existing checkpoint const existingCheckpoint = { date: today, users_processed: 1, papers_found: 10, papers_summarized: 5, total_cost: 0.05, errors: [], last_user_id: 1, completed: false }; await mockEnv.CACHE.put(checkpointKey, JSON.stringify(existingCheckpoint)); // Load checkpoint const loaded = await mockEnv.CACHE.get(checkpointKey); const parsed = JSON.parse(loaded!); expect(parsed.users_processed).toBe(1); expect(parsed.last_user_id).toBe(1); expect(parsed.papers_found).toBe(10); }); test('should resume from last_user_id after failure', async () => { const today = new Date().toISOString().split('T')[0]; const checkpointKey = `checkpoint:automation:${today}`; // Checkpoint showing user 1 was processed const checkpoint = { date: today, users_processed: 1, papers_found: 10, papers_summarized: 5, total_cost: 0.05, errors: [], last_user_id: 1, // User 1 completed completed: false }; await mockEnv.CACHE.put(checkpointKey, JSON.stringify(checkpoint)); // In a real automation run, user 1 would be skipped // and processing would start from user 2 const loaded = JSON.parse((await mockEnv.CACHE.get(checkpointKey))!); expect(loaded.last_user_id).toBe(1); // Verify that users with id <= 1 would be skipped const allUsers = [ { id: 1, username: 'jeff' }, { id: 2, username: 'wife' } ]; const usersToProcess = allUsers.filter(u => !loaded.last_user_id || u.id > loaded.last_user_id); expect(usersToProcess.length).toBe(1); expect(usersToProcess[0].username).toBe('wife'); }); }); // ============================================================================= // Test Suite: Budget Tracking // ============================================================================= describe('Budget Tracking', () => { test('should stop when budget exceeds $1', () => { const checkpoint = { date: '2025-11-30', users_processed: 5, papers_found: 100, papers_summarized: 50, total_cost: 1.05, // Exceeded budget errors: [], completed: false }; // Budget check logic const budgetExceeded = checkpoint.total_cost >= 1.0; expect(budgetExceeded).toBe(true); }); test('should continue when budget is under $1', () => { const checkpoint = { date: '2025-11-30', users_processed: 3, papers_found: 50, papers_summarized: 25, total_cost: 0.45, errors: [], completed: false }; const budgetExceeded = checkpoint.total_cost >= 1.0; expect(budgetExceeded).toBe(false); }); test('should track cost per user', () => { const userCosts = [ { user: 'jeff', cost: 0.25 }, { user: 'wife', cost: 0.30 }, { user: 'friend', cost: 0.20 } ]; const totalCost = userCosts.reduce((sum, u) => sum + u.cost, 0); expect(totalCost).toBe(0.75); expect(totalCost).toBeLessThan(1.0); }); }); // ============================================================================= // Test Suite: Error Handling // ============================================================================= describe('Error Handling', () => { test('should log errors and continue with next user', () => { const errors: string[] = []; // Simulate user processing with error const users = ['jeff', 'wife', 'friend']; for (const user of users) { try { if (user === 'wife') { throw new Error('Failed to fetch topics'); } // Process user successfully } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unknown error'; errors.push(`${user}: ${errorMsg}`); continue; // Continue with next user } } expect(errors.length).toBe(1); expect(errors[0]).toContain('wife'); expect(errors[0]).toContain('Failed to fetch topics'); }); test('should save checkpoint after each user (even on error)', async () => { const mockKV = createMockKV(); const checkpointKey = 'checkpoint:automation:2025-11-30'; // Simulate processing with error let checkpoint = { date: '2025-11-30', users_processed: 0, papers_found: 0, papers_summarized: 0, total_cost: 0, errors: [] as string[], last_user_id: undefined as number | undefined, completed: false }; // User 1: success checkpoint.users_processed = 1; checkpoint.last_user_id = 1; await mockKV.put(checkpointKey, JSON.stringify(checkpoint)); // User 2: error checkpoint.errors.push('user2: API error'); checkpoint.last_user_id = 2; // Still mark as processed await mockKV.put(checkpointKey, JSON.stringify(checkpoint)); // Verify checkpoint saved even after error const saved = JSON.parse((await mockKV.get(checkpointKey))!); expect(saved.last_user_id).toBe(2); expect(saved.errors.length).toBe(1); }); }); // ============================================================================= // Test Suite: Authentication // ============================================================================= describe('Authentication', () => { test('should reject requests without cf-cron header or auth', () => { const request = new Request('http://localhost/run', { method: 'POST', headers: {} }); const cronHeader = request.headers.get('cf-cron'); const authHeader = request.headers.get('authorization'); const isAuthenticated = cronHeader || authHeader === 'Bearer test-secret'; expect(isAuthenticated).toBe(false); }); test('should accept requests with cf-cron header', () => { const request = new Request('http://localhost/run', { method: 'POST', headers: { 'cf-cron': '1' } }); const cronHeader = request.headers.get('cf-cron'); expect(cronHeader).toBeDefined(); }); test('should accept requests with valid authorization header', () => { const request = new Request('http://localhost/run', { method: 'POST', headers: { 'authorization': 'Bearer test-secret' } }); const authHeader = request.headers.get('authorization'); const isAuthenticated = authHeader === 'Bearer test-secret'; expect(isAuthenticated).toBe(true); }); }); // ============================================================================= // Test Suite: User Processing // ============================================================================= describe('User Processing', () => { test('should fetch enabled topics for user', async () => { const mockDB = createMockD1(); // Fetch topics for user 1 const topics = await mockDB .prepare('SELECT * FROM topics WHERE user_id = ? AND enabled = 1') .bind(1) .all(); expect(topics.results).toBeDefined(); expect(topics.results.length).toBeGreaterThan(0); expect(topics.results[0].user_id).toBe(1); expect(topics.results[0].enabled).toBe(1); }); test('should skip users with no enabled topics', async () => { const mockDB = createMockD1(); // Fetch topics for non-existent user const topics = await mockDB .prepare('SELECT * FROM topics WHERE user_id = ? AND enabled = 1') .bind(999) .all(); expect(topics.results.length).toBe(0); // In real code, this would return early with 0 papers processed }); test('should combine multiple topics with OR', () => { const topics = [ { arxiv_query: 'cat:cs.AI AND safety' }, { arxiv_query: 'cat:cs.LG AND interpretability' } ]; const combinedQuery = topics .map(t => `(${t.arxiv_query})`) .join(' OR '); expect(combinedQuery).toBe('(cat:cs.AI AND safety) OR (cat:cs.LG AND interpretability)'); }); }); // ============================================================================= // Test Suite: Paper Deduplication // ============================================================================= describe('Paper Deduplication', () => { test('should skip papers that already exist in database', async () => { const mockDB = createMockD1(); // Add a paper to mock database await mockDB .prepare('INSERT INTO papers (...)') .bind('2311.12345', 'Test Paper') .run(); // Check if paper exists const existing = await mockDB .prepare('SELECT id FROM papers WHERE arxiv_id = ?') .bind('2311.12345') .first(); expect(existing).toBeDefined(); expect(existing.arxiv_id).toBe('2311.12345'); }); test('should insert new papers not in database', async () => { const mockDB = createMockD1(); // Check for non-existent paper const existing = await mockDB .prepare('SELECT id FROM papers WHERE arxiv_id = ?') .bind('2311.99999') .first(); expect(existing).toBeNull(); // In real code, this would proceed with summarization and insertion }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jeffaf/kivv'

If you have feedback or need assistance with the MCP directory API, please join our Discord server