Update ConnectAI codebase

This commit is contained in:
g1nation
2026-05-18 08:15:01 +09:00
parent 88664c7c6e
commit 86cacaeb03
38 changed files with 1043 additions and 99 deletions
+61
View File
@@ -0,0 +1,61 @@
import { buildDevilSystemPrompt, buildDevilUserPrompt } from '../src/features/devilAgent/devilPrompt';
describe('Devil Agent prompt builder', () => {
test('system prompt enforces the 6 core rules', () => {
const sys = buildDevilSystemPrompt();
// 핵심 규칙들이 모두 들어있는지 검증 — 정확한 문자열은 prompt 본문 변경에 깨지지만,
// 가치 보존되는 핵심 키워드 기준으로 체크.
expect(sys).toContain('도현');
expect(sys).toContain('한 가지 약점'); // rule 1
expect(sys).toContain('만약'); // rule 2 framing
expect(sys).toContain('칭찬'); // rule 3 — 금지
expect(sys).toContain('통계'); // rule 4 — 환각 가드
expect(sys).toContain('우려'); // rule 5 출력 형식
expect(sys).toContain('검증'); // rule 5 출력 형식
expect(sys).toContain('근거:'); // rule 6 출처 태그
});
test('user prompt includes both user prompt and assistant answer', () => {
const u = buildDevilUserPrompt({
userPrompt: '회원가입 흐름 설계 도와줘',
assistantAnswer: '이메일+비번 기본 flow 를 권장합니다.',
});
expect(u).toContain('회원가입 흐름 설계 도와줘');
expect(u).toContain('이메일+비번 기본 flow 를 권장합니다');
expect(u).toContain('Astra 의 직전 답변');
expect(u).toContain('가장 본질적인 약점 하나');
});
test('user prompt with brainContext adds reference section', () => {
const u = buildDevilUserPrompt({
userPrompt: 'Q',
assistantAnswer: 'A',
brainContext: '## brain/sample.md\n샘플 자료입니다.',
});
expect(u).toContain('참고 가능한 Second Brain 자료');
expect(u).toContain('샘플 자료입니다');
});
test('user prompt with userRebuttal switches mode to deeper round', () => {
const u = buildDevilUserPrompt({
userPrompt: 'Q',
assistantAnswer: 'A',
userRebuttal: '그건 사용자 입장에선 오히려 편리한데?',
});
expect(u).toContain('사용자의 재반박');
expect(u).toContain('그건 사용자 입장에선 오히려 편리한데');
expect(u).toContain('한 단계 더 깊은 약점');
// 첫 라운드 표시는 보이지 말아야 — userRebuttal 있으면 다른 분기.
expect(u).not.toContain('가장 본질적인 약점 하나를 골라');
});
test('user prompt handles empty fields gracefully', () => {
const u = buildDevilUserPrompt({ userPrompt: '', assistantAnswer: '' });
expect(u).toContain('(빈 질문)');
expect(u).toContain('(빈 답변)');
});
});
// generateDevilRebuttal 은 vscode 의존 — settings 토글 / module dynamic import 흐름 때문에
// 본 unit suite 에서는 직접 호출 X. 대신 prompt 빌더에 집중.
// settings 통합 흐름은 통합 테스트(향후)로 분리.
+97
View File
@@ -0,0 +1,97 @@
import { parseModelPrefix, makeModelId, providerLabel } from '../src/features/providers/types';
import { _internals } from '../src/features/providers/streamHelpers';
describe('parseModelPrefix', () => {
test('matches openrouter / anthropic / gemini prefixes', () => {
expect(parseModelPrefix('openrouter:anthropic/claude-3.5-sonnet')).toEqual({
provider: 'openrouter', model: 'anthropic/claude-3.5-sonnet',
});
expect(parseModelPrefix('anthropic:claude-3-5-sonnet-20241022')).toEqual({
provider: 'anthropic', model: 'claude-3-5-sonnet-20241022',
});
expect(parseModelPrefix('gemini:gemini-2.0-flash-exp')).toEqual({
provider: 'gemini', model: 'gemini-2.0-flash-exp',
});
});
test('returns null for local engine model ids', () => {
expect(parseModelPrefix('gemma4:e2b')).toBeNull();
expect(parseModelPrefix('llama-3.2:8b')).toBeNull();
expect(parseModelPrefix('google/gemma-4-e4b')).toBeNull();
});
test('returns null for empty / undefined input', () => {
expect(parseModelPrefix('')).toBeNull();
expect(parseModelPrefix(undefined as any)).toBeNull();
});
test('makeModelId round-trips with parseModelPrefix', () => {
const id = makeModelId('openrouter', 'meta/llama-3.3-70b');
expect(id).toBe('openrouter:meta/llama-3.3-70b');
expect(parseModelPrefix(id)).toEqual({ provider: 'openrouter', model: 'meta/llama-3.3-70b' });
});
test('providerLabel returns Korean-friendly labels', () => {
expect(providerLabel('openrouter')).toBe('OpenRouter');
expect(providerLabel('anthropic')).toBe('Anthropic');
expect(providerLabel('gemini')).toBe('Gemini');
});
});
describe('Anthropic event → OpenAI SSE conversion', () => {
const conv = _internals.anthropicEventToOpenAI;
test('content_block_delta with text emits OpenAI chunk', () => {
const out = conv('content_block_delta', '{"delta":{"type":"text_delta","text":"안녕"}}');
expect(out).toBeTruthy();
expect(out).toContain('"content":"안녕"');
expect(out).toMatch(/^data: /);
expect(out!.endsWith('\n\n')).toBe(true);
});
test('non-delta events (message_start, content_block_start, etc.) are ignored', () => {
expect(conv('message_start', '{}')).toBeNull();
expect(conv('content_block_start', '{}')).toBeNull();
expect(conv('message_stop', '{}')).toBeNull();
});
test('malformed JSON returns null', () => {
expect(conv('content_block_delta', 'not json')).toBeNull();
expect(conv('content_block_delta', '{')).toBeNull();
});
test('empty text field returns null (no zero-length chunks)', () => {
expect(conv('content_block_delta', '{"delta":{"type":"text_delta","text":""}}')).toBeNull();
expect(conv('content_block_delta', '{"delta":{}}')).toBeNull();
});
});
describe('Gemini event → OpenAI SSE conversion', () => {
const conv = _internals.geminiEventToOpenAI;
test('extracts text from candidates[0].content.parts', () => {
const out = conv(null, '{"candidates":[{"content":{"parts":[{"text":"안녕"}],"role":"model"}}]}');
expect(out).toBeTruthy();
expect(out).toContain('"content":"안녕"');
expect(out).toMatch(/^data: /);
});
test('joins multiple parts into one chunk', () => {
const out = conv(null, '{"candidates":[{"content":{"parts":[{"text":"안"},{"text":"녕"}]}}]}');
expect(out).toContain('"content":"안녕"');
});
test('returns null when candidates missing / empty', () => {
expect(conv(null, '{}')).toBeNull();
expect(conv(null, '{"candidates":[]}')).toBeNull();
expect(conv(null, '{"candidates":[{"content":{}}]}')).toBeNull();
});
test('malformed JSON returns null', () => {
expect(conv(null, 'not json')).toBeNull();
});
test('empty text returns null', () => {
expect(conv(null, '{"candidates":[{"content":{"parts":[{"text":""}]}}]}')).toBeNull();
});
});