import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { resolveGatewayModel } from '../local-gateway.js'; import { prefixBoocodeLocalModels, clearProviderSnapshotCache, getProviderSnapshot } from '../provider-snapshot.js'; import { loadLlamaProviders } from '../llama-providers.js'; import { loadProviderConfig } from '../provider-config-registry.js'; vi.mock('../acp-probe.js', () => ({ probeAcpProvider: vi.fn(), })); import { probeAcpProvider } from '../acp-probe.js'; const mockProbe = vi.mocked(probeAcpProvider); /** Load a providers fixture into the in-memory registry. */ function loadProvidersFixture(providers: Array<{ id: string; label: string; baseUrl: string; kind?: string }>): void { const file = { defaultProvider: providers[0]?.id ?? 'llama-swap', providers, }; const path = join(tmpdir(), `llama-providers-w7-${Date.now()}.json`); writeFileSync(path, JSON.stringify(file), 'utf8'); loadLlamaProviders(path, 'http://localhost:8080'); } function mockSql(agents: Array<{ name: string; install_path: string | null; supports_acp: boolean; models: Array<{ id: string; label: string }> | null; label: string | null; transport: string | null; last_probed_at?: string | null; }>) { return vi.fn((strings: TemplateStringsArray) => { const query = strings.join(''); if (query.includes('FROM available_agents')) { return Promise.resolve(agents); } if (query.includes('UPDATE available_agents')) { return Promise.resolve([]); } return Promise.resolve([]); }) as unknown as import('../db.js').Sql; } // --- Gateway model-id parsing tests --- describe('resolveGatewayModel', () => { beforeEach(() => { loadProvidersFixture([ { id: 'sam-desktop', label: 'Sam Desktop', baseUrl: 'http://100.101.41.16:8401' }, { id: 'embedding', label: 'Embedding', baseUrl: 'http://100.90.172.55:8411' }, ]); }); it('resolves composite "provider/model" to the correct baseUrl', () => { const result = resolveGatewayModel('sam-desktop/qwen3.6-35b'); expect(result).toEqual({ baseUrl: 'http://100.101.41.16:8401', wireModelId: 'qwen3.6-35b', }); }); it('resolves a different provider to its own baseUrl', () => { const result = resolveGatewayModel('embedding/gemma-4-12b'); expect(result).toEqual({ baseUrl: 'http://100.90.172.55:8411', wireModelId: 'gemma-4-12b', }); }); it('returns error for unknown provider', () => { const result = resolveGatewayModel('nonexistent/model'); expect(result).toHaveProperty('error'); expect((result as { error: string }).error).toContain('unknown provider'); }); it('bare model resolves to default provider', () => { const result = resolveGatewayModel('qwen3.6-35b'); expect(result).toEqual({ baseUrl: 'http://100.101.41.16:8401', wireModelId: 'qwen3.6-35b', }); }); it('two providers serving the SAME wire model name hit different baseUrls', () => { const r1 = resolveGatewayModel('sam-desktop/qwen3.6-35b'); const r2 = resolveGatewayModel('embedding/qwen3.6-35b'); expect(r1).toHaveProperty('baseUrl', 'http://100.101.41.16:8401'); expect(r2).toHaveProperty('baseUrl', 'http://100.90.172.55:8411'); expect((r1 as { wireModelId: string }).wireModelId).toBe('qwen3.6-35b'); expect((r2 as { wireModelId: string }).wireModelId).toBe('qwen3.6-35b'); }); }); // --- prefixBoocodeLocalModels --- describe('prefixBoocodeLocalModels', () => { it('wraps composite ids with boocode-local prefix', () => { const result = prefixBoocodeLocalModels([ { id: 'sam-desktop/qwen3.6-35b', label: 'Qwen' }, { id: 'embedding/gemma-4-12b', label: 'Gemma' }, ]); expect(result.map((m) => m.id)).toEqual([ 'boocode-local/sam-desktop/qwen3.6-35b', 'boocode-local/embedding/gemma-4-12b', ]); }); it('leaves already-prefixed ids unchanged', () => { const result = prefixBoocodeLocalModels([ { id: 'boocode-local/sam-desktop/qwen3.6-35b', label: 'Qwen' }, ]); expect(result[0].id).toBe('boocode-local/sam-desktop/qwen3.6-35b'); }); it('preserves label and other fields', () => { const result = prefixBoocodeLocalModels([ { id: 'sam-desktop/qwen3.6-35b', label: 'Qwen 3.6 35B', isDefault: true }, ]); expect(result[0]).toEqual({ id: 'boocode-local/sam-desktop/qwen3.6-35b', label: 'Qwen 3.6 35B', isDefault: true, }); }); }); // --- parseModel inner-slash preservation --- describe('gateway model id parsing preserves inner slashes', () => { beforeEach(() => { loadProvidersFixture([ { id: 'sam-desktop', label: 'Sam Desktop', baseUrl: 'http://100.101.41.16:8401' }, ]); }); it('parses "sam-desktop/qwen3.6-35b-a3b-mxfp4" preserving the full wire id', () => { const result = resolveGatewayModel('sam-desktop/qwen3.6-35b-a3b-mxfp4'); expect(result).toHaveProperty('wireModelId', 'qwen3.6-35b-a3b-mxfp4'); }); it('parses model ids with dots and hyphens', () => { const result = resolveGatewayModel('sam-desktop/deepseek-r1-0528'); expect(result).toHaveProperty('wireModelId', 'deepseek-r1-0528'); }); }); // --- Snapshot advertising shape (integration) --- describe('provider snapshot opencode entry uses boocode-local prefix', () => { beforeEach(() => { clearProviderSnapshotCache(); loadProviderConfig('/nonexistent-coder-providers.json'); vi.restoreAllMocks(); vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: [{ id: 'local-model' }, { id: 'qwen3.6-35b' }], }), }), ); mockProbe.mockResolvedValue({ ok: true, models: [], modes: [], defaultModeId: null, commands: [], }); }); it('opencode snapshot entry has boocode-local prefixed model ids', async () => { loadProvidersFixture([ { id: 'sam-desktop', label: 'Sam Desktop', baseUrl: 'http://100.101.41.16:8401' }, ]); const sql = mockSql([ { name: 'opencode', install_path: '/usr/bin/opencode', supports_acp: true, models: null, label: 'OpenCode', transport: 'acp', last_probed_at: null, }, ]); const config = { LLAMA_SWAP_URL: 'http://llama-swap.test', PROVIDER_PROBE_TTL_MS: 86_400_000, DEFAULT_MODEL: 'qwen3.6-35b', } as import('../config.js').Config; const entries = await getProviderSnapshot(sql, config, '/tmp/test', true); const opencode = entries.find((e) => e.name === 'opencode'); expect(opencode).toBeDefined(); // W7: all model ids start with "boocode-local/" and never "llama-swap/". for (const m of opencode!.models) { expect(m.id).toMatch(/^boocode-local\//); expect(m.id).not.toMatch(/^llama-swap\//); } }); }); // --- Gateway HTTP proxy tests (W7 audit M3) --- describe('local gateway HTTP proxy', () => { let app: import('fastify').FastifyInstance; const fetchMock = vi.fn(); beforeEach(async () => { loadProvidersFixture([ { id: 'sam-desktop', label: 'Sam Desktop', baseUrl: 'http://machine-a.test:8401' }, { id: 'laptop', label: 'Laptop', baseUrl: 'http://machine-b.test:8401' }, ]); vi.stubGlobal('fetch', fetchMock); fetchMock.mockReset(); const { default: Fastify } = await import('fastify'); const { registerLocalGatewayRoutes } = await import('../local-gateway.js'); app = Fastify({ logger: false }); registerLocalGatewayRoutes(app); await app.ready(); }); afterEach(async () => { vi.unstubAllGlobals(); await app.close(); }); it('proxies non-streaming requests to the right provider with the bare wire id', async () => { fetchMock.mockResolvedValue( new Response(JSON.stringify({ id: 'cmpl-1', model: 'qwen3.6-35b' }), { status: 200, headers: { 'content-type': 'application/json' }, }), ); const res = await app.inject({ method: 'POST', url: '/v1/chat/completions', payload: { model: 'sam-desktop/qwen3.6-35b', messages: [] }, }); expect(res.statusCode).toBe(200); expect(res.json()).toMatchObject({ id: 'cmpl-1' }); expect(fetchMock).toHaveBeenCalledTimes(1); const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; expect(url).toBe('http://machine-a.test:8401/v1/chat/completions'); expect(JSON.parse(init.body as string).model).toBe('qwen3.6-35b'); }); it('routes duplicate wire model names to different machines by provider prefix', async () => { fetchMock.mockResolvedValue( new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'content-type': 'application/json' }, }), ); await app.inject({ method: 'POST', url: '/v1/chat/completions', payload: { model: 'sam-desktop/qwen3.6-35b', messages: [] }, }); await app.inject({ method: 'POST', url: '/v1/chat/completions', payload: { model: 'laptop/qwen3.6-35b', messages: [] }, }); const urls = fetchMock.mock.calls.map((c) => c[0] as string); expect(urls).toEqual([ 'http://machine-a.test:8401/v1/chat/completions', 'http://machine-b.test:8401/v1/chat/completions', ]); }); it('returns 400 for an unknown provider without calling upstream', async () => { const res = await app.inject({ method: 'POST', url: '/v1/chat/completions', payload: { model: 'nonexistent/some-model', messages: [] }, }); expect(res.statusCode).toBe(400); expect(res.json().error).toContain('unknown provider'); expect(fetchMock).not.toHaveBeenCalled(); }); it('returns 400 when the model field is missing', async () => { const res = await app.inject({ method: 'POST', url: '/v1/chat/completions', payload: { messages: [] }, }); expect(res.statusCode).toBe(400); expect(fetchMock).not.toHaveBeenCalled(); }); it('returns an OpenAI-shaped 502 error when upstream replies non-JSON', async () => { fetchMock.mockResolvedValue( new Response('gateway error', { status: 200, headers: { 'content-type': 'text/html' }, }), ); const res = await app.inject({ method: 'POST', url: '/v1/chat/completions', payload: { model: 'sam-desktop/qwen3.6-35b', messages: [] }, }); expect(res.statusCode).toBe(502); expect(res.json().error.message).toContain('non-JSON'); }); it('relays streaming responses chunk-for-chunk with the upstream status', async () => { const chunks = ['data: {"a":1}\n\n', 'data: {"a":2}\n\n', 'data: [DONE]\n\n']; const stream = new ReadableStream({ start(controller) { for (const c of chunks) controller.enqueue(new TextEncoder().encode(c)); controller.close(); }, }); fetchMock.mockResolvedValue( new Response(stream, { status: 200, headers: { 'content-type': 'text/event-stream' } }), ); const res = await app.inject({ method: 'POST', url: '/v1/chat/completions', payload: { model: 'laptop/qwen3.6-35b', messages: [], stream: true }, }); expect(res.statusCode).toBe(200); expect(res.headers['content-type']).toBe('text/event-stream'); expect(res.body).toBe(chunks.join('')); }); it('forwards inbound X-Boo-Source header to upstream', async () => { fetchMock.mockResolvedValue( new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'content-type': 'application/json' }, }), ); await app.inject({ method: 'POST', url: '/v1/chat/completions', payload: { model: 'sam-desktop/qwen3.6-35b', messages: [] }, headers: { 'x-boo-source': 'arena' }, }); expect(fetchMock).toHaveBeenCalledTimes(1); const callHeaders = (fetchMock.mock.calls[0] as [string, RequestInit])[1]?.headers as Record; expect(callHeaders['X-Boo-Source']).toBe('arena'); }); it('defaults X-Boo-Source to boocoder when not present', async () => { fetchMock.mockResolvedValue( new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'content-type': 'application/json' }, }), ); await app.inject({ method: 'POST', url: '/v1/chat/completions', payload: { model: 'sam-desktop/qwen3.6-35b', messages: [] }, }); expect(fetchMock).toHaveBeenCalledTimes(1); const callHeaders = (fetchMock.mock.calls[0] as [string, RequestInit])[1]?.headers as Record; expect(callHeaders['X-Boo-Source']).toBe('boocoder'); }); }); // --- opencode config sync shape (W7 audit B1) --- describe('buildBoocodeLocalProviderConfig', () => { it('emits an opencode-routable provider: npm + options.baseURL + models as object map', async () => { loadProvidersFixture([ { id: 'sam-desktop', label: 'Sam Desktop', baseUrl: 'http://machine-a.test:8401' }, ]); const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify({ data: [{ id: 'qwen3.6-35b' }] }), { status: 200, headers: { 'content-type': 'application/json' }, }), ); vi.stubGlobal('fetch', fetchMock); try { const { buildBoocodeLocalProviderConfig } = await import('../opencode-config-sync.js'); const cfg = await buildBoocodeLocalProviderConfig('http://127.0.0.1:9502'); expect(cfg.npm).toBe('@ai-sdk/openai-compatible'); expect(cfg.options?.baseURL).toBe('http://127.0.0.1:9502/v1'); expect(Array.isArray(cfg.models)).toBe(false); expect(cfg.models).toHaveProperty(['sam-desktop/qwen3.6-35b']); } finally { vi.unstubAllGlobals(); } }); });