import { describe, it, expect, vi } from 'vitest'; import { isVisionCapable, refreshVisionCache } from '@main/services/VisionDetect.js'; import type { OllamaModel } from '@main/services/VisionDetect.js'; // --------------------------------------------------------------------------- // isVisionCapable // --------------------------------------------------------------------------- describe('isVisionCapable', () => { it('returns true when details.family is in VISION_FAMILIES', () => { const model: OllamaModel = { name: 'some-model', details: { family: 'llava' } }; expect(isVisionCapable(model)).toBe(true); }); it('returns true when details.families contains a vision family', () => { const model: OllamaModel = { name: 'some-model', details: { families: ['text', 'minicpm-v'] } }; expect(isVisionCapable(model)).toBe(true); }); it('returns true when name contains a vision hint (case-insensitive)', () => { const model: OllamaModel = { name: 'My-Vision-Model:latest' }; expect(isVisionCapable(model)).toBe(true); }); it('returns true when name contains "vl" hint', () => { const model: OllamaModel = { name: 'qwen2-vl:7b' }; expect(isVisionCapable(model)).toBe(true); }); it('returns false for a plain text model with no vision signals', () => { const model: OllamaModel = { name: 'gemma2:9b', details: { family: 'gemma', families: ['gemma'] } }; expect(isVisionCapable(model)).toBe(false); }); // v0.3.1 Cut F final fix — gemma family default 정정. gemma4 도 vision-capable hint. it('returns true for gemma4 family (future-proof)', () => { const model: OllamaModel = { name: 'gemma4-vision:e4b', details: { family: 'gemma4' } }; expect(isVisionCapable(model)).toBe(true); }); it('returns true for gemma4 in name hints (no family)', () => { const model: OllamaModel = { name: 'custom-gemma4:latest' }; expect(isVisionCapable(model)).toBe(true); }); }); // --------------------------------------------------------------------------- // refreshVisionCache // --------------------------------------------------------------------------- describe('refreshVisionCache', () => { function makeSettings(overrides: Partial<{ isAiEnabled: boolean; setCalled: { models: string[]; at: Date } | null; }> = {}) { const setCalled: { models: string[]; at: Date } | null = null; const settings = { isAiEnabled: vi.fn().mockResolvedValue(overrides.isAiEnabled ?? true), setVisionCapableCache: vi.fn().mockImplementation(async () => undefined), }; return settings; } it('returns ok:false with reason "ai_disabled" when AI is off', async () => { const settings = makeSettings({ isAiEnabled: false }); const result = await refreshVisionCache({ settings: settings as never, endpoint: 'http://localhost:11434', }); expect(result).toEqual({ ok: false, reason: 'ai_disabled' }); expect(settings.setVisionCapableCache).not.toHaveBeenCalled(); }); it('returns ok:false with http reason on non-ok response', async () => { const settings = makeSettings(); const fetchImpl = vi.fn().mockResolvedValue({ ok: false, status: 503 }); const result = await refreshVisionCache({ settings: settings as never, endpoint: 'http://localhost:11434', fetchImpl: fetchImpl as never, }); expect(result).toEqual({ ok: false, reason: 'tags http 503' }); }); it('returns ok:false with unreachable reason on fetch throw', async () => { const settings = makeSettings(); const fetchImpl = vi.fn().mockRejectedValue(new Error('ECONNREFUSED')); const result = await refreshVisionCache({ settings: settings as never, endpoint: 'http://localhost:11434', fetchImpl: fetchImpl as never, }); expect(result.ok).toBe(false); if (!result.ok) expect(result.reason).toMatch(/unreachable/); }); it('filters vision-capable models, persists cache, returns ok:true + models', async () => { const settings = makeSettings(); const fixedNow = new Date('2026-05-09T00:00:00.000Z'); const responseBody = { models: [ { name: 'llava:13b', details: { family: 'llava' } }, { name: 'gemma2:9b', details: { family: 'gemma' } }, { name: 'qwen2-vl:7b' }, ], }; const fetchImpl = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve(responseBody), }); const result = await refreshVisionCache({ settings: settings as never, endpoint: 'http://localhost:11434', fetchImpl: fetchImpl as never, now: () => fixedNow, }); expect(result).toEqual({ ok: true, models: ['llava:13b', 'qwen2-vl:7b'] }); expect(settings.setVisionCapableCache).toHaveBeenCalledWith( ['llava:13b', 'qwen2-vl:7b'], fixedNow ); }); });