Files
bDS/tests/engine/offline-mode.test.ts
Georg Bauer 5747925503 Feature/lmstudio provider (#30)
* chore: just a plan update

* Add LM Studio as local AI provider (OpenAI-compatible, like Ollama)

* Convert WebP thumbnails to JPEG before image analysis for LM Studio compatibility

* Strengthen language enforcement in image analysis prompt for local models

* Use i18n localized prompts for image analysis instead of English instructions

* Add airplane mode (Flugmodus) with status bar toggle and offline model preferences

* Fix flightmode: persist model IDs, skip network when offline, airplane icon

* Auto-fallback to offline models in airplane mode for chat, title, and image analysis

* Auto-select first local model as offline fallback when no explicit offline model configured

* Block git fetch/pull/push and site upload in airplane mode

* fix: thumbnails optimized for AI

* fix: error handling in airplane mode

---------

Co-authored-by: hugo <hugoms@me.com>
2026-03-02 13:35:42 +01:00

192 lines
6.6 KiB
TypeScript

/**
* Offline / airplane mode tests.
*
* Verifies ProviderRegistry offline mode behavior:
* - toggles on/off
* - restricts model resolution to local providers only
* - restricts available models to local only
* - isReady reflects local provider state
* - getProviderStatus includes offline state
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ProviderRegistry } from '../../src/main/engine/ai/providers';
describe('ProviderRegistry offline mode', () => {
let registry: ProviderRegistry;
beforeEach(() => {
registry = new ProviderRegistry();
});
// ---------- toggle ----------
it('starts with offline mode disabled', () => {
expect(registry.isOfflineMode()).toBe(false);
});
it('can enable and disable offline mode', () => {
registry.setOfflineMode(true);
expect(registry.isOfflineMode()).toBe(true);
registry.setOfflineMode(false);
expect(registry.isOfflineMode()).toBe(false);
});
// ---------- isReady in offline mode ----------
it('isReady returns false in offline mode when no local provider enabled', () => {
registry.setOpencodeKey('test-key');
registry.setOfflineMode(true);
expect(registry.isReady()).toBe(false);
});
it('isReady returns true in offline mode when Ollama enabled', () => {
registry.setOfflineMode(true);
registry.setOllamaEnabled(true);
expect(registry.isReady()).toBe(true);
});
it('isReady returns true in offline mode when LM Studio enabled', () => {
registry.setOfflineMode(true);
registry.setLmstudioEnabled(true);
expect(registry.isReady()).toBe(true);
});
// ---------- resolveModel in offline mode ----------
it('resolveModel throws for cloud models when offline', () => {
registry.setOpencodeKey('test-key');
registry.setOfflineMode(true);
expect(() => registry.resolveModel('claude-sonnet-4')).toThrow('offline');
});
it('resolveModel throws for mistral models when offline', () => {
registry.setMistralKey('test-key');
registry.setOfflineMode(true);
expect(() => registry.resolveModel('mistral-large-latest')).toThrow('offline');
});
it('resolveModel succeeds for Ollama model when offline and Ollama enabled', () => {
registry.setOllamaEnabled(true);
registry.registerOllamaModel('llama3');
registry.setOfflineMode(true);
const model = registry.resolveModel('llama3');
expect(model).toBeDefined();
});
it('resolveModel succeeds for LM Studio model when offline and LM Studio enabled', () => {
registry.setLmstudioEnabled(true);
registry.registerLmstudioModel('gemma-3-12b-it');
registry.setOfflineMode(true);
const model = registry.resolveModel('gemma-3-12b-it');
expect(model).toBeDefined();
});
// ---------- getAvailableModels filtering ----------
it('getAvailableModels returns only local models when offline', async () => {
registry.setOpencodeKey('test-key');
registry.setOllamaEnabled(true);
registry.registerOllamaModel('llama3:latest');
registry.setOfflineMode(true);
// No fetch should happen at all in offline mode
const originalFetch = globalThis.fetch;
globalThis.fetch = vi.fn().mockImplementation(async (url: string) => {
throw new Error(`Unexpected fetch to ${url} in offline mode`);
});
try {
const models = await registry.getAvailableModels();
// Only local model should appear
expect(models.length).toBe(1);
expect(models.every(m => m.provider === 'ollama' || m.provider === 'lmstudio')).toBe(true);
// fetch should NOT have been called
expect(globalThis.fetch).not.toHaveBeenCalled();
} finally {
globalThis.fetch = originalFetch;
}
});
// ---------- getKnownLocalModels ----------
it('getKnownLocalModels returns registered Ollama and LM Studio models', () => {
registry.setOllamaEnabled(true);
registry.setLmstudioEnabled(true);
registry.registerOllamaModel('llama3:latest');
registry.registerLmstudioModel('gemma-3-12b-it');
const models = registry.getKnownLocalModels();
expect(models.length).toBe(2);
expect(models.find(m => m.id === 'llama3:latest')?.provider).toBe('ollama');
expect(models.find(m => m.id === 'gemma-3-12b-it')?.provider).toBe('lmstudio');
});
it('getKnownLocalModels returns empty when no models registered', () => {
const models = registry.getKnownLocalModels();
expect(models.length).toBe(0);
});
// ---------- getFirstKnownLocalModelId ----------
it('getFirstKnownLocalModelId returns first registered model', () => {
registry.setOllamaEnabled(true);
registry.registerOllamaModel('llama3:latest');
registry.registerOllamaModel('mistral:latest');
expect(registry.getFirstKnownLocalModelId()).toBe('llama3:latest');
});
it('getFirstKnownLocalModelId returns null when no models registered', () => {
expect(registry.getFirstKnownLocalModelId()).toBeNull();
});
it('getFirstKnownLocalModelId falls back to LM Studio when no Ollama models', () => {
registry.setLmstudioEnabled(true);
registry.registerLmstudioModel('gemma-3-12b-it');
expect(registry.getFirstKnownLocalModelId()).toBe('gemma-3-12b-it');
});
// ---------- getProviderStatus includes offline ----------
it('getProviderStatus includes offlineMode field', () => {
registry.setOfflineMode(true);
const status = registry.getProviderStatus();
expect(status.offlineMode).toBe(true);
registry.setOfflineMode(false);
expect(registry.getProviderStatus().offlineMode).toBe(false);
});
// ---------- isProviderKeySet in offline mode ----------
it('isProviderKeySet returns false for cloud providers when offline', () => {
registry.setOpencodeKey('test-key');
registry.setMistralKey('test-key');
registry.setOfflineMode(true);
expect(registry.isProviderKeySet('anthropic')).toBe(false);
expect(registry.isProviderKeySet('openai')).toBe(false);
expect(registry.isProviderKeySet('mistral')).toBe(false);
});
it('isProviderKeySet returns true for local providers when offline and enabled', () => {
registry.setOllamaEnabled(true);
registry.setLmstudioEnabled(true);
registry.setOfflineMode(true);
expect(registry.isProviderKeySet('ollama')).toBe(true);
expect(registry.isProviderKeySet('lmstudio')).toBe(true);
});
// ---------- model cache invalidation on toggle ----------
it('invalidates model cache on offline mode toggle', () => {
// Pre-populate cache
registry.setOpencodeKey('test-key');
registry.setOfflineMode(true);
// Toggling should have invalidated
expect(registry.isOfflineMode()).toBe(true);
// Toggle back
registry.setOfflineMode(false);
expect(registry.isOfflineMode()).toBe(false);
});
});