/** * Tests fuer die Drafting-Engine LLM-Kaskade (OVH -> Ollama) + Stream-Parser. */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' const mockFetch = vi.fn() vi.stubGlobal('fetch', mockFetch) describe('llm-cascade parser', () => { it('parseOllamaLine extrahiert message.content', async () => { const { parseOllamaLine } = await import('../llm-cascade') expect(parseOllamaLine('{"message":{"content":"X"}}')).toBe('X') expect(parseOllamaLine('')).toBeNull() expect(parseOllamaLine('kaputt')).toBeNull() }) it('parseSSELine extrahiert delta.content', async () => { const { parseSSELine } = await import('../llm-cascade') expect(parseSSELine('data: {"choices":[{"delta":{"content":"Y"}}]}')).toBe('Y') expect(parseSSELine('data: [DONE]')).toBeNull() expect(parseSSELine('event: ping')).toBeNull() }) }) describe('cascadeComplete', () => { beforeEach(() => { vi.resetModules() mockFetch.mockReset() }) afterEach(() => { vi.unstubAllEnvs() }) it('nutzt OVH zuerst wenn konfiguriert (json + response_format)', async () => { vi.stubEnv('OVH_LLM_URL', 'https://ovh.test') vi.stubEnv('OVH_LLM_MODEL', 'gpt-oss-120b') vi.stubEnv('OVH_LLM_KEY', 'k') const { cascadeComplete } = await import('../llm-cascade') mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ choices: [{ message: { content: '{"ok":1}' } }], usage: { completion_tokens: 42 } }), }) const r = await cascadeComplete([{ role: 'user', content: 'hi' }], { json: true, maxTokens: 1000 }) expect(r).toEqual({ content: '{"ok":1}', tokensUsed: 42, provider: 'ovh' }) const [url, opts] = mockFetch.mock.calls[0] expect(url).toContain('/v1/chat/completions') const body = JSON.parse(opts.body) expect(body.response_format).toEqual({ type: 'json_object' }) expect(body.stream).toBe(false) }) it('faellt auf Ollama zurueck wenn OVH nicht konfiguriert ist', async () => { const { cascadeComplete } = await import('../llm-cascade') mockFetch.mockResolvedValueOnce({ ok: true, json: async () => ({ message: { content: 'hallo' }, eval_count: 7 }), }) const r = await cascadeComplete([{ role: 'user', content: 'hi' }]) expect(r).toEqual({ content: 'hallo', tokensUsed: 7, provider: 'ollama' }) expect(mockFetch.mock.calls[0][0]).toContain('/api/chat') }) it('faellt auf Ollama zurueck wenn OVH einen Fehler liefert', async () => { vi.stubEnv('OVH_LLM_URL', 'https://ovh.test') vi.stubEnv('OVH_LLM_MODEL', 'gpt-oss-120b') const { cascadeComplete } = await import('../llm-cascade') mockFetch .mockResolvedValueOnce({ ok: false, status: 502 }) .mockResolvedValueOnce({ ok: true, json: async () => ({ message: { content: 'fallback' }, eval_count: 3 }) }) const r = await cascadeComplete([{ role: 'user', content: 'hi' }]) expect(r?.provider).toBe('ollama') expect(r?.content).toBe('fallback') }) it('liefert null wenn weder OVH noch Ollama antworten', async () => { const { cascadeComplete } = await import('../llm-cascade') mockFetch.mockResolvedValue({ ok: false, status: 500 }) expect(await cascadeComplete([{ role: 'user', content: 'hi' }])).toBeNull() }) })