import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { ChatMessage } from './llm.service.js'; import { LlmService } from './llm.service.js'; const BASE = 'http://192.168.1.70:1111/v1'; function json(body: unknown): Response { return { ok: true, status: 200, json: async () => body } as Response; } /** Fresh service with a clean env (explicit base/model/timeout). */ function makeService(): LlmService { process.env.LLM_API_BASE = BASE; process.env.LLM_MODEL = 'qwen3.8-27b'; process.env.LLM_TIMEOUT_MS = '60000'; return new LlmService(); } describe('LlmService', () => { let fetchMock: ReturnType; beforeEach(() => { fetchMock = vi.fn(); vi.stubGlobal('fetch', fetchMock); }); afterEach(() => { vi.unstubAllGlobals(); for (const k of ['LLM_API_BASE', 'LLM_MODEL', 'LLM_TIMEOUT_MS']) delete process.env[k]; }); it('listModels returns the data[].id strings from GET /models', async () => { const svc = makeService(); fetchMock.mockResolvedValueOnce( json({ data: [ { id: 'qwen3.8-27b' }, { id: 'text-embedding-nomic-embed-text-v1.5' }, ], }), ); await expect(svc.listModels()).resolves.toEqual([ 'qwen3.8-27b', 'text-embedding-nomic-embed-text-v1.5', ]); expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock.mock.calls[0][0]).toBe(`${BASE}/models`); }); it('chat posts model+messages and returns choices[0].message.content', async () => { const svc = makeService(); fetchMock.mockResolvedValueOnce( json({ choices: [{ message: { content: 'hello back' } }] }), ); const messages: ChatMessage[] = [ { role: 'system', content: 'be brief' }, { role: 'user', content: 'hi' }, ]; await expect( svc.chat(messages, { maxTokens: 50, temperature: 0.2 }), ).resolves.toBe('hello back'); const [url, init] = fetchMock.mock.calls[0]; expect(url).toBe(`${BASE}/chat/completions`); expect(init.method).toBe('POST'); expect(JSON.parse(init.body)).toEqual({ model: 'qwen3.8-27b', messages, maxTokens: 50, temperature: 0.2, }); }); it('chat throws on non-2xx with the HTTP status in the message', async () => { const svc = makeService(); fetchMock.mockResolvedValueOnce({ ok: false, status: 503, text: async () => 'upstream down', } as Response); await expect(svc.chat([{ role: 'user', content: 'hi' }])).rejects.toThrow( /HTTP 503.*upstream down/, ); }); it('summarize calls chat with the system prompt plus the user text', async () => { const svc = makeService(); fetchMock.mockResolvedValueOnce( json({ choices: [{ message: { content: 'the summary' } }] }), ); await expect(svc.summarize('a long text')).resolves.toBe('the summary'); const [, init] = fetchMock.mock.calls[0]; expect(JSON.parse(init.body).messages).toEqual([ { role: 'system', content: 'Summarize the following text concisely.' }, { role: 'user', content: 'a long text' }, ]); }); });