diff --git a/client/src/components/settings/LocalLlmLibraryView.test.jsx b/client/src/components/settings/LocalLlmLibraryView.test.jsx index 5e965e193f..4d59f90a1c 100644 --- a/client/src/components/settings/LocalLlmLibraryView.test.jsx +++ b/client/src/components/settings/LocalLlmLibraryView.test.jsx @@ -154,6 +154,36 @@ describe('LocalLlmLibraryView information architecture', () => { }); }); +describe('LocalLlmLibraryView model-list diagnostics', () => { + it('shows an Ollama list failure and clears it after a successful empty refresh', async () => { + const emptyOllama = { + installed: true, + available: true, + modelCount: 0, + models: [], + }; + getLocalLlmStatus.mockResolvedValueOnce({ + backend: 'ollama', + ollama: { ...emptyOllama, modelsError: 'Ollama model list returned no data' }, + lmstudio: { installed: false, available: false, modelCount: 0, models: [] }, + }); + + await renderLibrary(); + + expect(screen.getByText(/Couldn't list Ollama models/)).toHaveTextContent('Ollama model list returned no data'); + + getLocalLlmStatus.mockResolvedValueOnce({ + backend: 'ollama', + ollama: { ...emptyOllama, modelsError: null }, + lmstudio: { installed: false, available: false, modelCount: 0, models: [] }, + }); + const progressHandler = socket.on.mock.calls.find(([event]) => event === 'localLlm:progress')[1]; + act(() => progressHandler({ event: 'complete' })); + + await waitFor(() => expect(screen.queryByText(/Couldn't list Ollama models/)).toBeNull()); + }); +}); + // The Ollama auto-upgrade banner lives beside the install that triggers it. Its // only entry point is a model install, which is a Model Library action — before // the view split the banner rendered inside the Runtimes panel, where the flow diff --git a/server/services/localLlm.js b/server/services/localLlm.js index 28095402e4..2a464f7e08 100644 --- a/server/services/localLlm.js +++ b/server/services/localLlm.js @@ -715,6 +715,8 @@ export async function getStatus() { baseUrl: ollamaStatus.baseUrl, modelCount: ollamaStatus.modelCount, models: ollamaModels, + contextLength: ollamaStatus.contextLength, + modelsError: ollamaManager.getLastInstalledModelsError(), // Best installed model for editorial review/editing, surfaced so the // manuscript editor can suggest it (and warn against the embedding model). recommendations: { editorial: recommendEditorialModel(ollamaModels, { measured: measuredOllama }) }, diff --git a/server/services/localLlm.test.js b/server/services/localLlm.test.js index a327b8b64d..448c86865d 100644 --- a/server/services/localLlm.test.js +++ b/server/services/localLlm.test.js @@ -938,6 +938,46 @@ describe('localLlm', () => { }); }); + describe('getStatus Ollama diagnostics', () => { + it('preserves the manager context-window diagnostics', async () => { + const contextLength = { runtime: 32768, applied: 65536, agentMinimum: 65536 }; + mocks.ollama.getStatus.mockResolvedValueOnce({ + available: true, + baseUrl: 'x', + version: '0.5.7', + modelCount: 0, + models: [], + contextLength, + }); + + const status = await svc.getStatus(); + + expect(status.ollama.contextLength).toEqual(contextLength); + }); + + it('keeps a null runtime and separates a failed model read from an empty list', async () => { + mocks.ollama.getStatus.mockResolvedValueOnce({ + available: true, + baseUrl: 'x', + version: '0.5.7', + modelCount: 0, + models: [], + contextLength: { runtime: null, applied: 65536, agentMinimum: 65536 }, + }); + mocks.ollama.getLastInstalledModelsError.mockReturnValueOnce('Ollama model list failed'); + + const failedStatus = await svc.getStatus(); + + expect(failedStatus.ollama.contextLength.runtime).toBeNull(); + expect(failedStatus.ollama.models).toEqual([]); + expect(failedStatus.ollama.modelsError).toBe('Ollama model list failed'); + + const emptyStatus = await svc.getStatus(); + expect(emptyStatus.ollama.models).toEqual([]); + expect(emptyStatus.ollama.modelsError).toBeNull(); + }); + }); + describe('getStatus editorial recommendation with measured evidence', () => { const installed = [ { id: 'qwen3.6:35b', name: 'qwen3.6:35b', params: '35B' },