-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat(local-ai): add editable Ollama server URL with connection test #2210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
graycyrus
merged 12 commits into
tinyhumansai:main
from
M3gA-Mind:feat/ollama-external-server-url
May 20, 2026
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
0f076cf
fix(security): always canonicalize paths before policy check
M3gA-Mind be29669
fix(security): resolve forbidden paths against workspace root; valida…
M3gA-Mind 61343cf
fix(review): unwrap double-wrapped error in image_info; add relative …
M3gA-Mind 60d7a0f
test(security): cover validate_parent_path forbidden-path block
M3gA-Mind 2011507
feat(local-ai): add editable Ollama server URL with connection test
M3gA-Mind fba0d51
fix(local-ai): address CodeRabbit review comments
M3gA-Mind 0a8bf1d
test(local-ai): add coverage for Ollama URL panel, connection test, a…
M3gA-Mind 898d7e9
fix(inference): redact credential-bearing Ollama URLs before debug lo…
M3gA-Mind 3618449
fix(inference): redact fallback Ollama URL log and fix IPv6 normaliza…
M3gA-Mind 15210c1
chore: merge upstream/main (resolve import conflicts in ModelStatusSe…
M3gA-Mind 24469d6
fix(inference): use url::Host enum for IPv6 normalization to avoid do…
M3gA-Mind 9039191
Merge remote-tracking branch 'upstream/main' into feat/ollama-externa…
M3gA-Mind File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
157 changes: 157 additions & 0 deletions
157
app/src/components/settings/panels/__tests__/LocalModelDebugPanel.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| import { fireEvent, render, screen, waitFor } from '@testing-library/react'; | ||
| import { MemoryRouter } from 'react-router-dom'; | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import LocalModelDebugPanel from '../LocalModelDebugPanel'; | ||
|
|
||
| const { mockNavigateBack } = vi.hoisted(() => ({ mockNavigateBack: vi.fn() })); | ||
|
|
||
| vi.mock('../../hooks/useSettingsNavigation', () => ({ | ||
| useSettingsNavigation: () => ({ navigateBack: mockNavigateBack, breadcrumbs: [] }), | ||
| })); | ||
|
|
||
| const mockGetConfig = vi.fn(); | ||
| vi.mock('../../../../utils/tauriCommands/config', () => ({ | ||
| openhumanGetConfig: (...args: unknown[]) => mockGetConfig(...args), | ||
| })); | ||
|
|
||
| const mockLocalAiStatus = vi.fn(); | ||
| const mockLocalAiAssetsStatus = vi.fn(); | ||
| const mockLocalAiDownloadsProgress = vi.fn(); | ||
| const mockLocalAiTestConnection = vi.fn(); | ||
| const mockUpdateLocalAiSettings = vi.fn(); | ||
| const mockLocalAiDiagnostics = vi.fn(); | ||
|
|
||
| vi.mock('../../../../utils/tauriCommands', () => ({ | ||
| openhumanLocalAiStatus: (...args: unknown[]) => mockLocalAiStatus(...args), | ||
| openhumanLocalAiAssetsStatus: (...args: unknown[]) => mockLocalAiAssetsStatus(...args), | ||
| openhumanLocalAiDownloadsProgress: (...args: unknown[]) => mockLocalAiDownloadsProgress(...args), | ||
| openhumanLocalAiTestConnection: (...args: unknown[]) => mockLocalAiTestConnection(...args), | ||
| openhumanUpdateLocalAiSettings: (...args: unknown[]) => mockUpdateLocalAiSettings(...args), | ||
| openhumanLocalAiDiagnostics: (...args: unknown[]) => mockLocalAiDiagnostics(...args), | ||
| openhumanLocalAiSummarize: vi.fn().mockResolvedValue({ result: '' }), | ||
| openhumanLocalAiPrompt: vi.fn().mockResolvedValue({ result: '' }), | ||
| openhumanLocalAiEmbed: vi.fn().mockResolvedValue({ result: [] }), | ||
| openhumanLocalAiVisionPrompt: vi.fn().mockResolvedValue({ result: '' }), | ||
| openhumanLocalAiTranscribe: vi.fn().mockResolvedValue({ result: '' }), | ||
| openhumanLocalAiTts: vi.fn().mockResolvedValue({ result: '' }), | ||
| openhumanLocalAiDownloadAsset: vi.fn().mockResolvedValue({ result: null }), | ||
| })); | ||
|
|
||
| function renderPanel() { | ||
| return render( | ||
| <MemoryRouter> | ||
| <LocalModelDebugPanel /> | ||
| </MemoryRouter> | ||
| ); | ||
| } | ||
|
|
||
| describe('LocalModelDebugPanel', () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| mockLocalAiStatus.mockResolvedValue({ result: null }); | ||
| mockLocalAiAssetsStatus.mockResolvedValue({ result: null }); | ||
| mockLocalAiDownloadsProgress.mockResolvedValue({ result: null }); | ||
| mockGetConfig.mockResolvedValue({ result: { config: {} } }); | ||
| mockLocalAiDiagnostics.mockResolvedValue({ | ||
| ok: true, | ||
| ollama_running: false, | ||
| ollama_base_url: null, | ||
| ollama_binary_path: null, | ||
| installed_models: [], | ||
| expected: { | ||
| chat_model: '', | ||
| chat_found: false, | ||
| embedding_model: '', | ||
| embedding_found: false, | ||
| vision_model: '', | ||
| vision_found: false, | ||
| }, | ||
| issues: [], | ||
| repair_actions: [], | ||
| }); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.restoreAllMocks(); | ||
| }); | ||
|
|
||
| it('renders the Ollama Server URL section with default URL', () => { | ||
| renderPanel(); | ||
| const input = screen.getByPlaceholderText('http://localhost:11434') as HTMLInputElement; | ||
| expect(input.value).toBe('http://localhost:11434'); | ||
| }); | ||
|
|
||
| it('seeds the URL input from config on mount', async () => { | ||
| mockGetConfig.mockResolvedValue({ | ||
| result: { config: { local_ai: { base_url: 'http://192.168.1.5:11434' } } }, | ||
| }); | ||
| renderPanel(); | ||
| await waitFor(() => { | ||
| const input = screen.getByPlaceholderText('http://localhost:11434') as HTMLInputElement; | ||
| expect(input.value).toBe('http://192.168.1.5:11434'); | ||
| }); | ||
| }); | ||
|
|
||
| it('keeps the default URL when config returns no base_url', async () => { | ||
| mockGetConfig.mockResolvedValue({ result: { config: { local_ai: {} } } }); | ||
| renderPanel(); | ||
| await waitFor(() => { | ||
| const input = screen.getByPlaceholderText('http://localhost:11434') as HTMLInputElement; | ||
| expect(input.value).toBe('http://localhost:11434'); | ||
| }); | ||
| expect(mockGetConfig).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('calls openhumanLocalAiTestConnection when Test Connection is clicked', async () => { | ||
| mockLocalAiTestConnection.mockResolvedValue({ reachable: true, models_count: 2 }); | ||
| renderPanel(); | ||
| const testBtn = screen.getByRole('button', { name: /Test Connection/i }); | ||
| fireEvent.click(testBtn); | ||
| await waitFor(() => { | ||
| expect(mockLocalAiTestConnection).toHaveBeenCalledWith('http://localhost:11434'); | ||
| }); | ||
| }); | ||
|
|
||
| it('shows reachable result after a successful connection test', async () => { | ||
| mockLocalAiTestConnection.mockResolvedValue({ reachable: true, models_count: 5 }); | ||
| renderPanel(); | ||
| fireEvent.click(screen.getByRole('button', { name: /Test Connection/i })); | ||
| await waitFor(() => expect(screen.getByText(/Reachable/)).toBeTruthy()); | ||
| expect(screen.getByText(/5 models/)).toBeTruthy(); | ||
| }); | ||
|
|
||
| it('shows unreachable result when connection test throws', async () => { | ||
| mockLocalAiTestConnection.mockRejectedValue(new Error('connect ECONNREFUSED')); | ||
| renderPanel(); | ||
| fireEvent.click(screen.getByRole('button', { name: /Test Connection/i })); | ||
| await waitFor(() => expect(screen.getByText(/connect ECONNREFUSED/)).toBeTruthy()); | ||
| }); | ||
|
|
||
| it('saves the URL when Save is clicked after changing the input', async () => { | ||
| mockUpdateLocalAiSettings.mockResolvedValue({ result: true }); | ||
| renderPanel(); | ||
| const urlInput = screen.getByPlaceholderText('http://localhost:11434'); | ||
| fireEvent.change(urlInput, { target: { value: 'http://192.168.1.5:11434' } }); | ||
| const saveBtn = await screen.findByRole('button', { name: 'Save' }); | ||
| expect((saveBtn as HTMLButtonElement).disabled).toBe(false); | ||
| fireEvent.click(saveBtn); | ||
| await waitFor(() => { | ||
| expect(mockUpdateLocalAiSettings).toHaveBeenCalledWith({ | ||
| base_url: 'http://192.168.1.5:11434', | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| it('resets the URL to default when Reset to default is clicked', async () => { | ||
| mockUpdateLocalAiSettings.mockResolvedValue({ result: true }); | ||
| renderPanel(); | ||
| const resetBtn = screen.getByRole('button', { name: /Reset to default/i }); | ||
| fireEvent.click(resetBtn); | ||
| await waitFor(() => { | ||
| expect(mockUpdateLocalAiSettings).toHaveBeenCalledWith({ base_url: null }); | ||
| }); | ||
| const urlInput = screen.getByPlaceholderText('http://localhost:11434') as HTMLInputElement; | ||
| expect(urlInput.value).toBe('http://localhost:11434'); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.