/** * Unit Tests for summary-service.ts * * 테스트 범위: * 1. addToBuffer - 메시지 버퍼 추가 * 2. getBufferedMessages - 버퍼 메시지 조회 * 3. getLatestSummary - 최신 요약 조회 * 4. getAllSummaries - 모든 요약 조회 (최대 3개) * 5. getConversationContext - 전체 컨텍스트 조회 * 6. processAndSummarize - 요약 실행 및 저장 * 7. generateAIResponse - AI 응답 생성 */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { addToBuffer, getBufferedMessages, getLatestSummary, getAllSummaries, getConversationContext, processAndSummarize, generateAIResponse, } from '../src/summary-service'; import { createTestUser, createSummary, createMessageBuffer, getTestDB, } from './setup'; import { Env } from '../src/types'; // Mock OpenAI service // Store captured arguments for inspection in tests let capturedSystemPrompt: string | undefined; let capturedRecentContext: Array<{ role: string; content: string }> | undefined; vi.mock('../src/openai-service', () => ({ generateProfileWithOpenAI: vi.fn(async () => '테스트 프로필'), generateOpenAIResponse: vi.fn(async (env, userMessage, systemPrompt, recentContext) => { // Capture arguments for test inspection capturedSystemPrompt = systemPrompt as string; capturedRecentContext = recentContext as Array<{ role: string; content: string }>; return 'AI 응답 테스트'; }), })); describe('summary-service', () => { let testUserId: number; const testChatId = 'test_chat_123'; let testEnv: Env; beforeEach(async () => { // 각 테스트마다 새로운 사용자 생성 testUserId = await createTestUser('123456789', 'testuser'); // Mock Env 객체 생성 testEnv = { DB: getTestDB(), OPENAI_API_KEY: 'test-key', SUMMARY_THRESHOLD: '20', MAX_SUMMARIES_PER_USER: '3', AI: { run: vi.fn(async () => ({ response: 'Workers AI 응답' })), }, } as unknown as Env; }); describe('addToBuffer', () => { it('should add user message to buffer and return count', async () => { const count = await addToBuffer( testEnv.DB, testUserId, testChatId, 'user', '안녕하세요' ); expect(count).toBe(1); }); it('should add multiple messages and return correct count', async () => { await addToBuffer(testEnv.DB, testUserId, testChatId, 'user', '메시지 1'); await addToBuffer(testEnv.DB, testUserId, testChatId, 'bot', '응답 1'); const count = await addToBuffer(testEnv.DB, testUserId, testChatId, 'user', '메시지 2'); expect(count).toBe(3); }); it('should distinguish between user and bot roles', async () => { await addToBuffer(testEnv.DB, testUserId, testChatId, 'user', '사용자 메시지'); await addToBuffer(testEnv.DB, testUserId, testChatId, 'bot', '봇 메시지'); const messages = await getBufferedMessages(testEnv.DB, testUserId, testChatId); expect(messages).toHaveLength(2); expect(messages[0].role).toBe('user'); expect(messages[1].role).toBe('bot'); }); }); describe('getBufferedMessages', () => { it('should return empty array for new user', async () => { const messages = await getBufferedMessages(testEnv.DB, testUserId, testChatId); expect(messages).toEqual([]); }); it('should return messages in chronological order', async () => { await createMessageBuffer(testUserId, testChatId, 'user', '첫 번째'); await createMessageBuffer(testUserId, testChatId, 'bot', '두 번째'); await createMessageBuffer(testUserId, testChatId, 'user', '세 번째'); const messages = await getBufferedMessages(testEnv.DB, testUserId, testChatId); expect(messages).toHaveLength(3); expect(messages[0].message).toBe('첫 번째'); expect(messages[1].message).toBe('두 번째'); expect(messages[2].message).toBe('세 번째'); }); it('should include all message fields', async () => { await createMessageBuffer(testUserId, testChatId, 'user', '테스트 메시지'); const messages = await getBufferedMessages(testEnv.DB, testUserId, testChatId); expect(messages[0]).toHaveProperty('id'); expect(messages[0]).toHaveProperty('role'); expect(messages[0]).toHaveProperty('message'); expect(messages[0]).toHaveProperty('created_at'); }); }); describe('getLatestSummary', () => { it('should return null for user without summaries', async () => { const summary = await getLatestSummary(testEnv.DB, testUserId, testChatId); expect(summary).toBeNull(); }); it('should return the most recent summary', async () => { await createSummary(testUserId, testChatId, 1, '첫 번째 요약', 20); await createSummary(testUserId, testChatId, 2, '두 번째 요약', 40); await createSummary(testUserId, testChatId, 3, '최신 요약', 60); const summary = await getLatestSummary(testEnv.DB, testUserId, testChatId); expect(summary).not.toBeNull(); expect(summary?.generation).toBe(3); expect(summary?.summary).toBe('최신 요약'); expect(summary?.message_count).toBe(60); }); it('should include all summary fields', async () => { await createSummary(testUserId, testChatId, 1, '테스트 요약', 20); const summary = await getLatestSummary(testEnv.DB, testUserId, testChatId); expect(summary).toHaveProperty('id'); expect(summary).toHaveProperty('generation'); expect(summary).toHaveProperty('summary'); expect(summary).toHaveProperty('message_count'); expect(summary).toHaveProperty('created_at'); }); }); describe('getAllSummaries', () => { it('should return empty array for user without summaries', async () => { const summaries = await getAllSummaries(testEnv.DB, testUserId, testChatId); expect(summaries).toEqual([]); }); it('should return summaries in descending order (most recent first)', async () => { await createSummary(testUserId, testChatId, 1, '오래된 요약', 20); await createSummary(testUserId, testChatId, 2, '중간 요약', 40); await createSummary(testUserId, testChatId, 3, '최신 요약', 60); const summaries = await getAllSummaries(testEnv.DB, testUserId, testChatId); expect(summaries).toHaveLength(3); expect(summaries[0].generation).toBe(3); expect(summaries[1].generation).toBe(2); expect(summaries[2].generation).toBe(1); }); it('should limit to maximum 3 summaries', async () => { await createSummary(testUserId, testChatId, 1, '요약 1', 20); await createSummary(testUserId, testChatId, 2, '요약 2', 40); await createSummary(testUserId, testChatId, 3, '요약 3', 60); await createSummary(testUserId, testChatId, 4, '요약 4', 80); const summaries = await getAllSummaries(testEnv.DB, testUserId, testChatId); expect(summaries).toHaveLength(3); expect(summaries[0].generation).toBe(4); expect(summaries[1].generation).toBe(3); expect(summaries[2].generation).toBe(2); }); }); describe('getConversationContext', () => { it('should return context for new user without summaries', async () => { const context = await getConversationContext(testEnv.DB, testUserId, testChatId); expect(context.previousSummary).toBeNull(); expect(context.summaries).toEqual([]); expect(context.recentMessages).toEqual([]); expect(context.totalMessages).toBe(0); }); it('should return context with summary and messages', async () => { await createSummary(testUserId, testChatId, 1, '사용자 프로필', 20); await createMessageBuffer(testUserId, testChatId, 'user', '메시지 1'); await createMessageBuffer(testUserId, testChatId, 'bot', '응답 1'); const context = await getConversationContext(testEnv.DB, testUserId, testChatId); expect(context.previousSummary).not.toBeNull(); expect(context.previousSummary?.generation).toBe(1); expect(context.summaries).toHaveLength(1); expect(context.recentMessages).toHaveLength(2); expect(context.totalMessages).toBe(22); // 20 (from summary) + 2 (buffer) }); it('should return previousSummary as most recent from summaries array', async () => { await createSummary(testUserId, testChatId, 1, '오래된 요약', 20); await createSummary(testUserId, testChatId, 2, '최신 요약', 40); const context = await getConversationContext(testEnv.DB, testUserId, testChatId); expect(context.previousSummary).not.toBeNull(); expect(context.previousSummary?.generation).toBe(2); expect(context.summaries[0].generation).toBe(2); }); it('should include all required fields in context', async () => { const context = await getConversationContext(testEnv.DB, testUserId, testChatId); expect(context).toHaveProperty('previousSummary'); expect(context).toHaveProperty('summaries'); expect(context).toHaveProperty('recentMessages'); expect(context).toHaveProperty('totalMessages'); }); }); describe('processAndSummarize', () => { it('should not summarize when below threshold', async () => { // Add 10 messages (below threshold of 20) for (let i = 0; i < 10; i++) { await createMessageBuffer(testUserId, testChatId, 'user', `메시지 ${i}`); } const result = await processAndSummarize(testEnv, testUserId, testChatId); expect(result.summarized).toBe(false); expect(result.summary).toBeUndefined(); }); it('should summarize when threshold is reached', async () => { // Add 20 messages (threshold) for (let i = 0; i < 20; i++) { await createMessageBuffer(testUserId, testChatId, 'user', `메시지 ${i}`); } const result = await processAndSummarize(testEnv, testUserId, testChatId); expect(result.summarized).toBe(true); expect(result.summary).toBeDefined(); }); it('should clear buffer after summarization', async () => { // Add 20 messages for (let i = 0; i < 20; i++) { await createMessageBuffer(testUserId, testChatId, 'user', `메시지 ${i}`); } await processAndSummarize(testEnv, testUserId, testChatId); const messages = await getBufferedMessages(testEnv.DB, testUserId, testChatId); expect(messages).toHaveLength(0); }); it('should create new summary with correct generation', async () => { await createSummary(testUserId, testChatId, 1, '첫 번째 요약', 20); // Add 20 more messages for (let i = 0; i < 20; i++) { await createMessageBuffer(testUserId, testChatId, 'user', `메시지 ${i}`); } await processAndSummarize(testEnv, testUserId, testChatId); const summary = await getLatestSummary(testEnv.DB, testUserId, testChatId); expect(summary?.generation).toBe(2); expect(summary?.message_count).toBe(40); // 20 + 20 }); it('should maintain maximum 3 summaries', async () => { // Create 3 existing summaries await createSummary(testUserId, testChatId, 1, '요약 1', 20); await createSummary(testUserId, testChatId, 2, '요약 2', 40); await createSummary(testUserId, testChatId, 3, '요약 3', 60); // Add 20 messages to trigger 4th summary for (let i = 0; i < 20; i++) { await createMessageBuffer(testUserId, testChatId, 'user', `메시지 ${i}`); } await processAndSummarize(testEnv, testUserId, testChatId); const summaries = await getAllSummaries(testEnv.DB, testUserId, testChatId); expect(summaries).toHaveLength(3); expect(summaries[0].generation).toBe(4); // Most recent expect(summaries[2].generation).toBe(2); // Oldest kept (generation 1 deleted) }); it('should use OpenAI when API key is available', async () => { const { generateProfileWithOpenAI } = await import('../src/openai-service'); for (let i = 0; i < 20; i++) { await createMessageBuffer(testUserId, testChatId, 'user', `메시지 ${i}`); } await processAndSummarize(testEnv, testUserId, testChatId); expect(generateProfileWithOpenAI).toHaveBeenCalled(); }); it('should fall back to Workers AI when OpenAI key is not available', async () => { const envNoOpenAI = { ...testEnv, OPENAI_API_KEY: undefined, } as unknown as Env; for (let i = 0; i < 20; i++) { await createMessageBuffer(testUserId, testChatId, 'user', `메시지 ${i}`); } await processAndSummarize(envNoOpenAI, testUserId, testChatId); expect(envNoOpenAI.AI.run).toHaveBeenCalled(); }); }); describe('generateAIResponse', () => { it('should generate response for user without profile', async () => { const response = await generateAIResponse( testEnv, testUserId, testChatId, '안녕하세요' ); expect(response).toBeDefined(); expect(typeof response).toBe('string'); }); it('should include profile in system prompt when available', async () => { await createSummary(testUserId, testChatId, 1, '사용자는 개발자입니다', 20); // Verify summary was created correctly const summaries = await getAllSummaries(testEnv.DB, testUserId, testChatId); expect(summaries).toHaveLength(1); const { generateOpenAIResponse } = await import('../src/openai-service'); // Reset captured values capturedSystemPrompt = undefined; await generateAIResponse( testEnv, testUserId, testChatId, '최근에 뭐 했어?' ); expect(generateOpenAIResponse).toHaveBeenCalled(); // Use captured system prompt from mock expect(capturedSystemPrompt).toBeDefined(); // When summaries exist, system prompt should include profile content // Format: "## 사용자 프로필 (N개 버전 통합)" followed by versioned profile if (summaries.length > 0 && capturedSystemPrompt) { expect(capturedSystemPrompt).toContain('사용자는 개발자입니다'); } }); it('should include recent messages in context', async () => { // Note: We need to use the same userId and chatId as testUserId and testChatId // because generateAIResponse uses those for context lookup await createMessageBuffer(testUserId, testChatId, 'user', '이전 메시지'); await createMessageBuffer(testUserId, testChatId, 'bot', '이전 응답'); const { generateOpenAIResponse } = await import('../src/openai-service'); // Reset captured values capturedRecentContext = undefined; await generateAIResponse( testEnv, testUserId, testChatId, '새 메시지' ); expect(generateOpenAIResponse).toHaveBeenCalled(); // Use captured recent context from mock expect(capturedRecentContext).toBeDefined(); // recentContext uses getSmartContext() when telegramUserId is provided. // Without telegramUserId parameter, it returns empty [], then falls back // to getConversationContext().recentMessages // Since we created buffer messages above, we should have at least 2 messages if (capturedRecentContext) { expect(capturedRecentContext.length).toBeGreaterThanOrEqual(2); expect(capturedRecentContext[0].role).toBe('user'); expect(capturedRecentContext[0].content).toBe('이전 메시지'); } }); it('should use OpenAI when API key is available', async () => { const { generateOpenAIResponse } = await import('../src/openai-service'); await generateAIResponse( testEnv, testUserId, testChatId, '테스트 메시지' ); expect(generateOpenAIResponse).toHaveBeenCalled(); }); it('should fall back to Workers AI when OpenAI key is not available', async () => { const envNoOpenAI = { ...testEnv, OPENAI_API_KEY: undefined, } as unknown as Env; await generateAIResponse( envNoOpenAI, testUserId, testChatId, '테스트 메시지' ); expect(envNoOpenAI.AI.run).toHaveBeenCalled(); }); it('should include tool usage instructions in system prompt', async () => { const { generateOpenAIResponse } = await import('../src/openai-service'); await generateAIResponse( testEnv, testUserId, testChatId, '날씨 알려줘' ); const callArgs = vi.mocked(generateOpenAIResponse).mock.calls[0]; const systemPrompt = callArgs[2] as string; expect(systemPrompt).toContain('날씨'); expect(systemPrompt).toContain('도구'); }); }); });