feat(v031): VisionDetect — isVisionCapable + refreshVisionCache (fetch 주입)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
110
tests/unit/VisionDetect.test.ts
Normal file
110
tests/unit/VisionDetect.test.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user