본인 dogfood 환경 = gemma4:e4b (텍스트). vision 변종은 현재 gemma3 (vision-capable) 또는 향후 gemma4 출시 시. 양 family 모두 hint 에 포함 — capability detection 이 future-proof. - VisionDetect.VISION_FAMILIES + VISION_NAME_HINTS 에 'gemma4' 추가 - isVisionCapable test 2건 추가 (gemma4 family / gemma4 name hint detection) - spec §1 + §2 의 'gemma3 family default' → 'gemma family — gemma3 / gemma4' 영향: 기존 detection 정확도 무영향 (set 추가만), 사용자가 gemma4 vision 변종을 설치하면 자동 인식.
122 lines
4.7 KiB
TypeScript
122 lines
4.7 KiB
TypeScript
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
|
|
);
|
|
});
|
|
});
|