test: add comprehensive unit tests for utils and security
- Add security.test.ts: 36 tests for webhook validation, rate limiting - Add circuit-breaker.test.ts: 31 tests for state transitions - Add retry.test.ts: 25 tests for exponential backoff - Add api-helper.test.ts: 25 tests for API abstraction - Add optimistic-lock.test.ts: 11 tests for concurrency control - Add summary-service.test.ts: 29 tests for profile system Total: 157 new test cases (222 passing overall) - Fix setup.ts D1 schema initialization for Miniflare - Update vitest.config.ts to exclude demo files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
433
tests/summary-service.test.ts
Normal file
433
tests/summary-service.test.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* 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
|
||||
vi.mock('../src/openai-service', () => ({
|
||||
generateProfileWithOpenAI: vi.fn(async () => '테스트 프로필'),
|
||||
generateOpenAIResponse: vi.fn(async () => '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);
|
||||
|
||||
const { generateOpenAIResponse } = await import('../src/openai-service');
|
||||
|
||||
await generateAIResponse(
|
||||
testEnv,
|
||||
testUserId,
|
||||
testChatId,
|
||||
'최근에 뭐 했어?'
|
||||
);
|
||||
|
||||
expect(generateOpenAIResponse).toHaveBeenCalled();
|
||||
const callArgs = vi.mocked(generateOpenAIResponse).mock.calls[0];
|
||||
const systemPrompt = callArgs[2] as string;
|
||||
|
||||
expect(systemPrompt).toContain('사용자 프로필');
|
||||
});
|
||||
|
||||
it('should include recent messages in context', async () => {
|
||||
await createMessageBuffer(testUserId, testChatId, 'user', '이전 메시지');
|
||||
await createMessageBuffer(testUserId, testChatId, 'bot', '이전 응답');
|
||||
|
||||
const { generateOpenAIResponse } = await import('../src/openai-service');
|
||||
|
||||
await generateAIResponse(
|
||||
testEnv,
|
||||
testUserId,
|
||||
testChatId,
|
||||
'새 메시지'
|
||||
);
|
||||
|
||||
expect(generateOpenAIResponse).toHaveBeenCalled();
|
||||
const callArgs = vi.mocked(generateOpenAIResponse).mock.calls[0];
|
||||
const recentContext = callArgs[3] as Array<{ role: string; content: string }>;
|
||||
|
||||
expect(recentContext.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
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('도구');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user