feat(v031): vision IPC + preload (get-vision-models / set / refresh)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
125
tests/unit/vision-ipc.test.ts
Normal file
125
tests/unit/vision-ipc.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
vi.mock('electron', () => ({ default: { ipcMain: { handle: vi.fn() }, dialog: {}, shell: {} } }));
|
||||
vi.mock('../../src/main/services/VisionDetect.js', () => ({
|
||||
refreshVisionCache: vi.fn(async () => ({ ok: true as const, models: ['gemma3:12b-vision'] }))
|
||||
}));
|
||||
vi.mock('../../src/main/services/GitClient.js');
|
||||
|
||||
import electron from 'electron';
|
||||
import { refreshVisionCache } from '../../src/main/services/VisionDetect.js';
|
||||
import { registerSettingsApi } from '../../src/main/ipc/settingsApi.js';
|
||||
import type { SettingsIpcDeps } from '../../src/main/ipc/settingsApi.js';
|
||||
|
||||
function getHandler(channel: string): (...args: unknown[]) => unknown {
|
||||
const handle = (electron.ipcMain as unknown as { handle: ReturnType<typeof vi.fn> }).handle;
|
||||
const call = handle.mock.calls.find((c) => c[0] === channel);
|
||||
if (!call) throw new Error(`channel ${channel} not registered`);
|
||||
return call[1] as (...args: unknown[]) => unknown;
|
||||
}
|
||||
|
||||
function makeDeps() {
|
||||
const settings = {
|
||||
getVisionModel: vi.fn(async () => 'gemma3:12b-vision'),
|
||||
setVisionModel: vi.fn(async () => {}),
|
||||
getVisionCapableCache: vi.fn(async () => ({
|
||||
models: ['gemma3:12b-vision', 'llava:13b'],
|
||||
at: '2026-05-10T05:00:00Z'
|
||||
})),
|
||||
setVisionCapableCache: vi.fn(async () => {}),
|
||||
// existing methods used by other handlers
|
||||
getAll: vi.fn(async () => ({
|
||||
ollama: { endpoint: 'http://localhost:11434', model: 'gemma2:2b' }
|
||||
})),
|
||||
setAiEnabled: vi.fn(async () => {}),
|
||||
setOnboardingCompleted: vi.fn(async () => {}),
|
||||
isAiEnabled: vi.fn(async () => true),
|
||||
getSyncRepoUrl: vi.fn(async () => null),
|
||||
setSyncRepoUrl: vi.fn(async () => {}),
|
||||
isAutoSyncEnabled: vi.fn(async () => false),
|
||||
getSyncIntervalMin: vi.fn(async () => 30),
|
||||
setSyncIntervalMin: vi.fn(async () => {}),
|
||||
setAutoSyncEnabled: vi.fn(async () => {})
|
||||
};
|
||||
|
||||
const syncSvc = {
|
||||
getSyncDir: vi.fn(() => '/tmp/sync'),
|
||||
listConflicts: vi.fn(() => []),
|
||||
resolveConflict: vi.fn(async () => ({ ok: true as const })),
|
||||
getLastStatus: vi.fn(() => ({ lastAt: null as string | null, lastResult: null as { ok: boolean } | null }))
|
||||
};
|
||||
|
||||
const deps: Partial<SettingsIpcDeps> = {
|
||||
backup: { runDaily: vi.fn(async () => ({ snapshotted: false })) } as never,
|
||||
exportSvc: {} as never,
|
||||
importSvc: {} as never,
|
||||
syncSvc: syncSvc as never,
|
||||
telemetry: { exportTo: vi.fn(async () => ({ eventCount: 0 })) } as never,
|
||||
settings: settings as never,
|
||||
getInboxWindow: () => null
|
||||
};
|
||||
|
||||
return { settings, syncSvc, deps };
|
||||
}
|
||||
|
||||
describe('vision IPC channels', () => {
|
||||
beforeEach(() => {
|
||||
(electron.ipcMain as unknown as { handle: ReturnType<typeof vi.fn> }).handle.mockClear();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('3 vision channels registered', () => {
|
||||
const { deps } = makeDeps();
|
||||
registerSettingsApi(deps as SettingsIpcDeps);
|
||||
|
||||
const handle = (electron.ipcMain as unknown as { handle: ReturnType<typeof vi.fn> }).handle;
|
||||
const channels = handle.mock.calls.map((c) => c[0]);
|
||||
expect(channels).toContain('settings:get-vision-models');
|
||||
expect(channels).toContain('settings:set-vision-model');
|
||||
expect(channels).toContain('settings:refresh-vision-cache');
|
||||
});
|
||||
|
||||
it('settings:get-vision-models returns { models, at, selected } from settings', async () => {
|
||||
const { deps, settings } = makeDeps();
|
||||
registerSettingsApi(deps as SettingsIpcDeps);
|
||||
const h = getHandler('settings:get-vision-models');
|
||||
const r = await h({});
|
||||
expect(settings.getVisionCapableCache).toHaveBeenCalled();
|
||||
expect(settings.getVisionModel).toHaveBeenCalled();
|
||||
expect(r).toEqual({
|
||||
models: ['gemma3:12b-vision', 'llava:13b'],
|
||||
at: '2026-05-10T05:00:00Z',
|
||||
selected: 'gemma3:12b-vision'
|
||||
});
|
||||
});
|
||||
|
||||
it('settings:set-vision-model calls settings.setVisionModel(value) + returns { ok: true }', async () => {
|
||||
const { deps, settings } = makeDeps();
|
||||
registerSettingsApi(deps as SettingsIpcDeps);
|
||||
const h = getHandler('settings:set-vision-model');
|
||||
const r = await h({}, 'llava:13b');
|
||||
expect(settings.setVisionModel).toHaveBeenCalledWith('llava:13b');
|
||||
expect(r).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('settings:refresh-vision-cache calls refreshVisionCache and returns result', async () => {
|
||||
const { deps } = makeDeps();
|
||||
registerSettingsApi(deps as SettingsIpcDeps);
|
||||
const h = getHandler('settings:refresh-vision-cache');
|
||||
const r = await h({});
|
||||
expect(refreshVisionCache).toHaveBeenCalledWith({
|
||||
settings: deps.settings,
|
||||
endpoint: 'http://localhost:11434'
|
||||
});
|
||||
expect(r).toEqual({ ok: true, models: ['gemma3:12b-vision'] });
|
||||
});
|
||||
|
||||
it('settings:set-vision-model with null clears the value', async () => {
|
||||
const { deps, settings } = makeDeps();
|
||||
registerSettingsApi(deps as SettingsIpcDeps);
|
||||
const h = getHandler('settings:set-vision-model');
|
||||
const r = await h({}, null);
|
||||
expect(settings.setVisionModel).toHaveBeenCalledWith(null);
|
||||
expect(r).toEqual({ ok: true });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user