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>
This commit is contained in:
@@ -333,6 +333,11 @@ vi.mock('fs/promises', () => ({
|
||||
unlink: vi.fn(),
|
||||
}));
|
||||
|
||||
let mockOfflineMode = false;
|
||||
vi.mock('../../src/main/ipc/chatHandlers', () => ({
|
||||
isOfflineModeActive: vi.fn(() => mockOfflineMode),
|
||||
}));
|
||||
|
||||
// Helper to invoke a registered handler
|
||||
async function invokeHandler(channel: string, ...args: any[]): Promise<any> {
|
||||
const handler = registeredHandlers.get(channel);
|
||||
@@ -383,6 +388,7 @@ describe('IPC Handlers', () => {
|
||||
registeredHandlers.clear();
|
||||
mockGeneratedFileHashStore.clear();
|
||||
resetMockCounters();
|
||||
mockOfflineMode = false;
|
||||
|
||||
// Create a real BlogGenerationEngine with mock engines for blog handler tests
|
||||
const { BlogGenerationEngine } = await import('../../src/main/engine/BlogGenerationEngine');
|
||||
@@ -513,6 +519,64 @@ describe('IPC Handlers', () => {
|
||||
behind: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return zeroed state when offline mode is active', async () => {
|
||||
mockOfflineMode = true;
|
||||
|
||||
const result = await invokeHandler('git:remoteState', '/repo');
|
||||
|
||||
expect(mockGitEngine.getRemoteState).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ ahead: 0, behind: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('offline mode blocks network git operations', () => {
|
||||
it('should block git:fetch when offline mode is active', async () => {
|
||||
mockOfflineMode = true;
|
||||
|
||||
const result = await invokeHandler('git:fetch', '/repo');
|
||||
|
||||
expect(mockGitEngine.fetch).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ success: false, code: 'offline' });
|
||||
});
|
||||
|
||||
it('should block git:pull when offline mode is active', async () => {
|
||||
mockOfflineMode = true;
|
||||
|
||||
const result = await invokeHandler('git:pull', '/repo');
|
||||
|
||||
expect(mockGitEngine.pull).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ success: false, code: 'offline' });
|
||||
});
|
||||
|
||||
it('should block git:push when offline mode is active', async () => {
|
||||
mockOfflineMode = true;
|
||||
|
||||
const result = await invokeHandler('git:push', '/repo');
|
||||
|
||||
expect(mockGitEngine.push).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ success: false, code: 'offline' });
|
||||
});
|
||||
|
||||
it('should allow git:fetch when offline mode is inactive', async () => {
|
||||
mockOfflineMode = false;
|
||||
mockGitEngine.fetch.mockResolvedValue({ success: true });
|
||||
|
||||
const result = await invokeHandler('git:fetch', '/repo');
|
||||
|
||||
expect(mockGitEngine.fetch).toHaveBeenCalledWith('/repo');
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('should allow git:commitAll regardless of offline mode', async () => {
|
||||
mockOfflineMode = true;
|
||||
mockGitEngine.commitAll.mockResolvedValue({ success: true });
|
||||
|
||||
const result = await invokeHandler('git:commitAll', '/repo', 'test commit');
|
||||
|
||||
expect(mockGitEngine.commitAll).toHaveBeenCalledWith('/repo', 'test commit');
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('git:diffContent', () => {
|
||||
@@ -738,6 +802,21 @@ describe('IPC Handlers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ============ Publish Handlers ============
|
||||
describe('Publish Handlers', () => {
|
||||
describe('publish:uploadSite offline guard', () => {
|
||||
it('should throw when offline mode is active', async () => {
|
||||
mockOfflineMode = true;
|
||||
|
||||
await expect(invokeHandler('publish:uploadSite', {
|
||||
sshHost: 'example.com',
|
||||
sshUser: 'deploy',
|
||||
sshRemotePath: '/var/www',
|
||||
})).rejects.toThrow('Airplane mode');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============ Project Handlers ============
|
||||
describe('Project Handlers', () => {
|
||||
describe('projects:create', () => {
|
||||
|
||||
Reference in New Issue
Block a user