import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { prisma } from '@/lib/db'; import { LoxiTranslationProvider } from '@/lib/translation/loxi'; import { translateToEnglish } from '@/lib/translation/service'; import { hasDatabase, resetDatabase } from './helpers'; const BASE = 'https://api.loxi.test'; const MODELS = [ { id: 5, provider: 'claude', model_name: 'haiku', is_active: true }, { id: 2, provider: 'claude', model_name: 'opus', is_active: true }, ]; let generateCalls = 0; let modelCalls = 0; const server = setupServer(); function provider(overrides: Partial[0]> = {}) { return new LoxiTranslationProvider({ baseUrl: BASE, apiKey: 'llk_test', modelName: 'haiku', timeoutMs: 2000, ...overrides, }); } function happyPath(stdout = 'Exceptionally closed on Thursday afternoon') { server.use( http.get(`${BASE}/api/models`, ({ request }) => { modelCalls += 1; // The adapter must ask for active Claude models only. const url = new URL(request.url); expect(url.searchParams.get('provider')).toBe('claude'); expect(request.headers.get('authorization')).toBe('Bearer llk_test'); return HttpResponse.json(MODELS); }), http.post(`${BASE}/api/generate`, async ({ request }) => { generateCalls += 1; const body = (await request.json()) as { model_id: number; prompt: string }; // Resolved by name, not hard-coded. expect(body.model_id).toBe(5); expect(body.prompt).toContain('Fermeture exceptionnelle jeudi après-midi'); return HttpResponse.json({ stdout, stderr: '', exit_code: 0, duration_ms: 2774 }); }), ); } beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' })); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); describe.skipIf(!hasDatabase)('translation', () => { beforeEach(async () => { generateCalls = 0; modelCalls = 0; await resetDatabase(); await prisma.translationCache.deleteMany(); }); it('translates and stores the result', async () => { happyPath(); const result = await translateToEnglish('Fermeture exceptionnelle jeudi après-midi', provider()); expect(result).toMatchObject({ ok: true, text: 'Exceptionally closed on Thursday afternoon', cached: false, }); expect(await prisma.translationCache.count()).toBe(1); }); it('cleans the quotes a model adds', async () => { happyPath('"Exceptionally closed on Thursday afternoon"'); const result = await translateToEnglish('Fermeture exceptionnelle jeudi après-midi', provider()); expect(result).toMatchObject({ ok: true, text: 'Exceptionally closed on Thursday afternoon' }); }); it('asks the service once for the same text', async () => { happyPath(); const shared = provider(); await translateToEnglish('Fermeture exceptionnelle jeudi après-midi', shared); const second = await translateToEnglish('Fermeture exceptionnelle jeudi après-midi', shared); expect(second).toMatchObject({ ok: true, cached: true }); // Translation runs a CLI on the other side; twice is measured in seconds. expect(generateCalls).toBe(1); }); it('ignores surrounding whitespace when matching the cache', async () => { happyPath(); const shared = provider(); await translateToEnglish('Fermeture exceptionnelle jeudi après-midi', shared); const second = await translateToEnglish(' Fermeture exceptionnelle jeudi après-midi ', shared); expect(second).toMatchObject({ cached: true }); expect(generateCalls).toBe(1); }); it('resolves the model id only once per provider', async () => { happyPath(); const shared = provider(); await translateToEnglish('Premier message', shared); await translateToEnglish('Second message', shared); expect(modelCalls).toBe(1); }); it('treats a non-zero exit code as a failure despite the 200', async () => { server.use( http.get(`${BASE}/api/models`, () => HttpResponse.json(MODELS)), http.post(`${BASE}/api/generate`, () => // This is the trap: the CLI failed, the HTTP call did not. HttpResponse.json({ stdout: '', stderr: 'quota exceeded', exit_code: 1 }), ), ); const result = await translateToEnglish('Bonjour', provider()); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toContain('quota exceeded'); } expect(await prisma.translationCache.count()).toBe(0); }); it('treats an empty answer as a failure', async () => { server.use( http.get(`${BASE}/api/models`, () => HttpResponse.json(MODELS)), http.post(`${BASE}/api/generate`, () => HttpResponse.json({ stdout: ' ', exit_code: 0 })), ); expect((await translateToEnglish('Bonjour', provider())).ok).toBe(false); expect(await prisma.translationCache.count()).toBe(0); }); it('reports a server error', async () => { server.use( http.get(`${BASE}/api/models`, () => HttpResponse.json(MODELS)), http.post(`${BASE}/api/generate`, () => new HttpResponse(null, { status: 500 })), ); const result = await translateToEnglish('Bonjour', provider()); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toContain('500'); } }); it('reports an unreachable service', async () => { server.use(http.get(`${BASE}/api/models`, () => HttpResponse.error())); expect((await translateToEnglish('Bonjour', provider())).ok).toBe(false); }); it('reports a timeout without throwing', async () => { server.use( http.get(`${BASE}/api/models`, () => HttpResponse.json(MODELS)), http.post(`${BASE}/api/generate`, async () => { await new Promise((resolve) => setTimeout(resolve, 200)); return HttpResponse.json({ stdout: 'late', exit_code: 0 }); }), ); const result = await translateToEnglish('Bonjour', provider({ timeoutMs: 20 })); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toMatch(/temps|injoignable/); } }); it('says so when the configured model does not exist', async () => { server.use(http.get(`${BASE}/api/models`, () => HttpResponse.json(MODELS))); const result = await translateToEnglish('Bonjour', provider({ modelName: 'sonnet-42' })); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toContain('sonnet-42'); } expect(generateCalls).toBe(0); }); it('does nothing for empty text', async () => { const result = await translateToEnglish(' ', provider()); expect(result).toMatchObject({ ok: true, text: '' }); expect(modelCalls).toBe(0); }); it('reports a service that is not configured', async () => { const result = await translateToEnglish('Bonjour', null); expect(result).toMatchObject({ ok: false }); }); });