Files

1 line
30 KiB
JSON

{"repo": "brumaire01/hermes-web-ui", "files": {"tests/client/app-store.test.ts": {"61926f339b6d6c2df84b5afb1a71caaefcb56f76": {"t": 1785746466.2886362, "n": 14672, "h": "b30560150fb9339f", "c": "// @vitest-environment jsdom\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\nimport { createPinia, setActivePinia } from 'pinia'\n\nconst mockSystemApi = vi.hoisted(() => ({\n checkHealth: vi.fn(),\n fetchAvailableModels: vi.fn(),\n addCustomModel: vi.fn(),\n removeCustomModel: vi.fn(),\n updateDefaultModel: vi.fn(),\n updateModelAlias: vi.fn(),\n updateModelVisibility: vi.fn(),\n triggerUpdate: vi.fn(),\n}))\n\nvi.mock('@/api/hermes/system', () => mockSystemApi)\nvi.mock('@/api/client', () => ({ hasApiKey: () => true }))\n\nimport { useAppStore } from '@/stores/hermes/app'\n\ndescribe('App Store', () => {\n beforeEach(() => {\n setActivePinia(createPinia())\n vi.clearAllMocks()\n mockSystemApi.addCustomModel.mockResolvedValue({ success: true, custom_models: {} })\n mockSystemApi.removeCustomModel.mockResolvedValue({ success: true, custom_models: {} })\n window.localStorage.clear()\n })\n\n afterEach(() => {\n vi.useRealTimers()\n })\n\n it('persists desktop sidebar collapsed state to localStorage', () => {\n const store = useAppStore()\n\n expect(store.sidebarCollapsed).toBe(false)\n\n store.toggleSidebarCollapsed()\n expect(store.sidebarCollapsed).toBe(true)\n expect(window.localStorage.getItem('hermes_sidebar_collapsed')).toBe('1')\n\n store.toggleSidebarCollapsed()\n expect(store.sidebarCollapsed).toBe(false)\n expect(window.localStorage.getItem('hermes_sidebar_collapsed')).toBe('0')\n })\n\n it('loads model visibility and falls back when the configured default is hidden', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: 'deepseek-chat',\n default_provider: 'deepseek',\n groups: [\n {\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n api_key: 'sk-test',\n models: ['deepseek-reasoner'],\n },\n ],\n allProviders: [],\n model_visibility: {\n deepseek: { mode: 'include', models: ['deepseek-reasoner'] },\n },\n })\n const store = useAppStore()\n\n await store.loadModels()\n\n expect(store.modelVisibility).toEqual({\n deepseek: { mode: 'include', models: ['deepseek-reasoner'] },\n })\n expect(store.selectedModel).toBe('deepseek-reasoner')\n expect(store.selectedProvider).toBe('deepseek')\n expect(store.customModels).toEqual({})\n expect(store.isModelVisible('deepseek', 'deepseek-reasoner')).toBe(true)\n expect(store.isModelVisible('deepseek', 'deepseek-chat')).toBe(false)\n })\n\n it('loads aliases while falling back from a hidden default without rehydrating it as custom', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: 'deepseek-chat',\n default_provider: 'deepseek',\n groups: [\n {\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n api_key: 'sk-test',\n models: ['deepseek-reasoner'],\n available_models: ['deepseek-chat', 'deepseek-reasoner'],\n },\n ],\n allProviders: [\n {\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n api_key: 'sk-test',\n models: ['deepseek-chat', 'deepseek-reasoner'],\n },\n ],\n model_aliases: {\n deepseek: { 'deepseek-reasoner': 'Reasoner Alias' },\n },\n model_visibility: {\n deepseek: { mode: 'include', models: ['deepseek-reasoner'] },\n },\n })\n const store = useAppStore()\n\n await store.loadModels()\n\n expect(store.modelAliases).toEqual({\n deepseek: { 'deepseek-reasoner': 'Reasoner Alias' },\n })\n expect(store.modelVisibility).toEqual({\n deepseek: { mode: 'include', models: ['deepseek-reasoner'] },\n })\n expect(store.selectedModel).toBe('deepseek-reasoner')\n expect(store.selectedProvider).toBe('deepseek')\n expect(store.displayModelName('deepseek-reasoner', 'deepseek')).toBe('Reasoner Alias')\n expect(store.customModels).toEqual({})\n })\n\n it('persists model visibility without changing the canonical selected model id', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: 'deepseek-reasoner',\n default_provider: 'deepseek',\n groups: [\n {\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n api_key: 'sk-test',\n models: ['deepseek-reasoner'],\n },\n ],\n allProviders: [],\n model_visibility: {\n deepseek: { mode: 'include', models: ['deepseek-reasoner'] },\n },\n })\n mockSystemApi.updateModelVisibility.mockResolvedValue({\n success: true,\n model_visibility: {\n deepseek: { mode: 'include', models: ['deepseek-reasoner'] },\n },\n })\n const store = useAppStore()\n\n await store.setModelVisibility('deepseek', { mode: 'include', models: ['deepseek-reasoner'] })\n\n expect(mockSystemApi.updateModelVisibility).toHaveBeenCalledWith({\n provider: 'deepseek',\n mode: 'include',\n models: ['deepseek-reasoner'],\n })\n expect(store.selectedModel).toBe('deepseek-reasoner')\n expect(store.selectedProvider).toBe('deepseek')\n expect(mockSystemApi.updateDefaultModel).not.toHaveBeenCalled()\n })\n\n it('marks the client stale when the served Web UI version changes', async () => {\n mockSystemApi.checkHealth.mockResolvedValue({\n status: 'ok',\n webui_version: '0.5.17',\n webui_latest: '0.5.17',\n webui_update_available: false,\n })\n const store = useAppStore()\n\n await store.checkConnection()\n\n expect(store.connected).toBe(true)\n expect(store.serverVersion).toBe('0.5.17')\n expect(store.clientOutdated).toBe(true)\n expect(store.updateAvailable).toBe(false)\n })\n\n it('does not mark the client stale when the served Web UI version matches this bundle', async () => {\n mockSystemApi.checkHealth.mockResolvedValue({\n status: 'ok',\n webui_version: 'test',\n webui_latest: 'test',\n webui_update_available: false,\n })\n const store = useAppStore()\n\n await store.checkConnection()\n\n expect(store.serverVersion).toBe('test')\n expect(store.clientOutdated).toBe(false)\n })\n\n it('clears the updating state and reports failure when self-update request fails', async () => {\n const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})\n mockSystemApi.triggerUpdate.mockRejectedValue(new Error('install failed'))\n const store = useAppStore()\n\n const ok = await store.doUpdate()\n\n expect(ok).toBe(false)\n expect(store.updating).toBe(false)\n expect(consoleError).toHaveBeenCalledWith('Failed to update Hermes Web UI:', expect.any(Error))\n consoleError.mockRestore()\n })\n\n it('loads model aliases and resolves display names without changing canonical IDs', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: 'deepseek-v4-flash',\n default_provider: 'deepseek',\n groups: [{\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n models: ['deepseek-v4-flash'],\n api_key: '',\n }],\n allProviders: [],\n model_aliases: {\n deepseek: { 'deepseek-v4-flash': 'Flash Alias' },\n },\n })\n const store = useAppStore()\n\n await store.loadModels()\n\n expect(store.selectedModel).toBe('deepseek-v4-flash')\n expect(store.getModelAlias('deepseek-v4-flash', 'deepseek')).toBe('Flash Alias')\n expect(store.displayModelName('deepseek-v4-flash', 'deepseek')).toBe('Flash Alias')\n expect(store.displayModelName('unknown', 'deepseek')).toBe('unknown')\n })\n\n it('does not refetch available models within the cache window after an empty response', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: '',\n default_provider: '',\n groups: [],\n allProviders: [],\n })\n const store = useAppStore()\n\n await store.loadModels()\n await store.loadModels()\n\n expect(mockSystemApi.fetchAvailableModels).toHaveBeenCalledTimes(1)\n })\n\n it('waits only up to the run timeout for the first available models request', async () => {\n vi.useFakeTimers()\n mockSystemApi.fetchAvailableModels.mockReturnValue(new Promise(() => {}))\n const store = useAppStore()\n let resolved = false\n\n const waitPromise = store.waitForModelsForRun(15000).then(() => {\n resolved = true\n })\n\n expect(mockSystemApi.fetchAvailableModels).toHaveBeenCalledTimes(1)\n await vi.advanceTimersByTimeAsync(14999)\n expect(resolved).toBe(false)\n await vi.advanceTimersByTimeAsync(1)\n await waitPromise\n expect(resolved).toBe(true)\n expect(store.modelGroups).toEqual([])\n })\n\n it('keeps aliases scoped to their provider when model IDs overlap', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: 'shared-model',\n default_provider: 'provider-a',\n groups: [\n {\n provider: 'provider-a',\n label: 'Provider A',\n base_url: 'https://a.example/v1',\n models: ['shared-model'],\n api_key: '',\n },\n {\n provider: 'provider-b',\n label: 'Provider B',\n base_url: 'https://b.example/v1',\n models: ['shared-model'],\n api_key: '',\n },\n ],\n allProviders: [],\n model_aliases: {\n 'provider-a': { 'shared-model': 'A Alias' },\n },\n })\n const store = useAppStore()\n\n await store.loadModels()\n\n expect(store.displayModelName('shared-model', 'provider-a')).toBe('A Alias')\n expect(store.displayModelName('shared-model', 'provider-b')).toBe('shared-model')\n expect(store.displayModelName('shared-model')).toBe('A Alias')\n })\n\n it('rehydrates an active unlisted default model as removable after loading models', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: 'manually-supported-id',\n default_provider: 'deepseek',\n groups: [{\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n models: ['deepseek-v4-flash'],\n api_key: '',\n }],\n allProviders: [],\n model_aliases: {},\n })\n const store = useAppStore()\n\n await store.loadModels()\n\n expect(store.selectedModel).toBe('manually-supported-id')\n expect(store.customModels).toEqual({ deepseek: ['manually-supported-id'] })\n })\n\n it('loads persisted custom models from the server response', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: 'gemma-4-26b-a4b-it',\n default_provider: 'google-ai-studio',\n groups: [{\n provider: 'google-ai-studio',\n label: 'Google AI Studio',\n base_url: 'https://generativelanguage.googleapis.com/v1beta',\n models: ['gemma-4-26b-a4b-it'],\n api_key: '',\n }],\n allProviders: [],\n custom_models: {\n 'google-ai-studio': ['gemma-4-26b-a4b-it'],\n },\n })\n const store = useAppStore()\n\n await store.loadModels()\n\n expect(store.selectedModel).toBe('gemma-4-26b-a4b-it')\n expect(store.customModels).toEqual({\n 'google-ai-studio': ['gemma-4-26b-a4b-it'],\n })\n })\n\n it('saves and clears model aliases via the Web UI-only alias API', async () => {\n mockSystemApi.updateModelAlias.mockResolvedValue(undefined)\n const store = useAppStore()\n\n await store.setModelAlias('deepseek-v4-flash', 'deepseek', ' Flash Alias ')\n\n expect(mockSystemApi.updateModelAlias).toHaveBeenCalledWith({\n provider: 'deepseek',\n model: 'deepseek-v4-flash',\n alias: 'Flash Alias',\n })\n expect(store.modelAliases).toEqual({ deepseek: { 'deepseek-v4-flash': 'Flash Alias' } })\n\n await store.setModelAlias('deepseek-v4-flash', 'deepseek', '')\n expect(store.modelAliases).toEqual({})\n })\n\n it('removes an unlisted custom model and falls back to a listed model when active', async () => {\n mockSystemApi.updateDefaultModel.mockResolvedValue(undefined)\n const store = useAppStore()\n store.modelGroups = [{\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n models: ['deepseek-v4-flash'],\n api_key: '',\n }]\n mockSystemApi.addCustomModel.mockResolvedValue({\n success: true,\n custom_models: { deepseek: ['test'] },\n })\n mockSystemApi.removeCustomModel.mockResolvedValue({\n success: true,\n custom_models: {},\n })\n\n await store.switchModel('test', 'deepseek')\n expect(store.selectedModel).toBe('test')\n expect(store.customModels).toEqual({ deepseek: ['test'] })\n expect(mockSystemApi.addCustomModel).toHaveBeenCalledWith({\n provider: 'deepseek',\n model: 'test',\n })\n\n await store.removeCustomModel('test', 'deepseek')\n expect(store.customModels).toEqual({})\n expect(mockSystemApi.removeCustomModel).toHaveBeenCalledWith({\n provider: 'deepseek',\n model: 'test',\n })\n expect(store.selectedModel).toBe('deepseek-v4-flash')\n expect(mockSystemApi.updateDefaultModel).toHaveBeenLastCalledWith({\n default: 'deepseek-v4-flash',\n provider: 'deepseek',\n })\n })\n\n it('removes deleted custom models from loaded model groups immediately', async () => {\n mockSystemApi.removeCustomModel.mockResolvedValue({\n success: true,\n custom_models: {},\n })\n const store = useAppStore()\n store.customModels = { deepseek: ['manual-model'] }\n store.modelGroups = [{\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n models: ['deepseek-v4-flash', 'manual-model'],\n available_models: ['deepseek-v4-flash', 'manual-model'],\n api_key: '',\n }]\n store.profileModelGroups = [{\n profile: 'default',\n default: 'deepseek-v4-flash',\n default_provider: 'deepseek',\n groups: [{\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n models: ['deepseek-v4-flash', 'manual-model'],\n available_models: ['deepseek-v4-flash', 'manual-model'],\n api_key: '',\n }],\n }]\n\n await store.removeCustomModel('manual-model', 'deepseek')\n\n expect(store.modelGroups[0].models).toEqual(['deepseek-v4-flash'])\n expect(store.modelGroups[0].available_models).toEqual(['deepseek-v4-flash'])\n expect(store.profileModelGroups[0].groups[0].models).toEqual(['deepseek-v4-flash'])\n expect(store.profileModelGroups[0].groups[0].available_models).toEqual(['deepseek-v4-flash'])\n })\n})\n"}, "af99b771fa4f6b5f912bb04317429cb0d929d322": {"t": 1785764606.5282626, "n": 14672, "h": "b30560150fb9339f", "c": "// @vitest-environment jsdom\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\nimport { createPinia, setActivePinia } from 'pinia'\n\nconst mockSystemApi = vi.hoisted(() => ({\n checkHealth: vi.fn(),\n fetchAvailableModels: vi.fn(),\n addCustomModel: vi.fn(),\n removeCustomModel: vi.fn(),\n updateDefaultModel: vi.fn(),\n updateModelAlias: vi.fn(),\n updateModelVisibility: vi.fn(),\n triggerUpdate: vi.fn(),\n}))\n\nvi.mock('@/api/hermes/system', () => mockSystemApi)\nvi.mock('@/api/client', () => ({ hasApiKey: () => true }))\n\nimport { useAppStore } from '@/stores/hermes/app'\n\ndescribe('App Store', () => {\n beforeEach(() => {\n setActivePinia(createPinia())\n vi.clearAllMocks()\n mockSystemApi.addCustomModel.mockResolvedValue({ success: true, custom_models: {} })\n mockSystemApi.removeCustomModel.mockResolvedValue({ success: true, custom_models: {} })\n window.localStorage.clear()\n })\n\n afterEach(() => {\n vi.useRealTimers()\n })\n\n it('persists desktop sidebar collapsed state to localStorage', () => {\n const store = useAppStore()\n\n expect(store.sidebarCollapsed).toBe(false)\n\n store.toggleSidebarCollapsed()\n expect(store.sidebarCollapsed).toBe(true)\n expect(window.localStorage.getItem('hermes_sidebar_collapsed')).toBe('1')\n\n store.toggleSidebarCollapsed()\n expect(store.sidebarCollapsed).toBe(false)\n expect(window.localStorage.getItem('hermes_sidebar_collapsed')).toBe('0')\n })\n\n it('loads model visibility and falls back when the configured default is hidden', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: 'deepseek-chat',\n default_provider: 'deepseek',\n groups: [\n {\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n api_key: 'sk-test',\n models: ['deepseek-reasoner'],\n },\n ],\n allProviders: [],\n model_visibility: {\n deepseek: { mode: 'include', models: ['deepseek-reasoner'] },\n },\n })\n const store = useAppStore()\n\n await store.loadModels()\n\n expect(store.modelVisibility).toEqual({\n deepseek: { mode: 'include', models: ['deepseek-reasoner'] },\n })\n expect(store.selectedModel).toBe('deepseek-reasoner')\n expect(store.selectedProvider).toBe('deepseek')\n expect(store.customModels).toEqual({})\n expect(store.isModelVisible('deepseek', 'deepseek-reasoner')).toBe(true)\n expect(store.isModelVisible('deepseek', 'deepseek-chat')).toBe(false)\n })\n\n it('loads aliases while falling back from a hidden default without rehydrating it as custom', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: 'deepseek-chat',\n default_provider: 'deepseek',\n groups: [\n {\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n api_key: 'sk-test',\n models: ['deepseek-reasoner'],\n available_models: ['deepseek-chat', 'deepseek-reasoner'],\n },\n ],\n allProviders: [\n {\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n api_key: 'sk-test',\n models: ['deepseek-chat', 'deepseek-reasoner'],\n },\n ],\n model_aliases: {\n deepseek: { 'deepseek-reasoner': 'Reasoner Alias' },\n },\n model_visibility: {\n deepseek: { mode: 'include', models: ['deepseek-reasoner'] },\n },\n })\n const store = useAppStore()\n\n await store.loadModels()\n\n expect(store.modelAliases).toEqual({\n deepseek: { 'deepseek-reasoner': 'Reasoner Alias' },\n })\n expect(store.modelVisibility).toEqual({\n deepseek: { mode: 'include', models: ['deepseek-reasoner'] },\n })\n expect(store.selectedModel).toBe('deepseek-reasoner')\n expect(store.selectedProvider).toBe('deepseek')\n expect(store.displayModelName('deepseek-reasoner', 'deepseek')).toBe('Reasoner Alias')\n expect(store.customModels).toEqual({})\n })\n\n it('persists model visibility without changing the canonical selected model id', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: 'deepseek-reasoner',\n default_provider: 'deepseek',\n groups: [\n {\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n api_key: 'sk-test',\n models: ['deepseek-reasoner'],\n },\n ],\n allProviders: [],\n model_visibility: {\n deepseek: { mode: 'include', models: ['deepseek-reasoner'] },\n },\n })\n mockSystemApi.updateModelVisibility.mockResolvedValue({\n success: true,\n model_visibility: {\n deepseek: { mode: 'include', models: ['deepseek-reasoner'] },\n },\n })\n const store = useAppStore()\n\n await store.setModelVisibility('deepseek', { mode: 'include', models: ['deepseek-reasoner'] })\n\n expect(mockSystemApi.updateModelVisibility).toHaveBeenCalledWith({\n provider: 'deepseek',\n mode: 'include',\n models: ['deepseek-reasoner'],\n })\n expect(store.selectedModel).toBe('deepseek-reasoner')\n expect(store.selectedProvider).toBe('deepseek')\n expect(mockSystemApi.updateDefaultModel).not.toHaveBeenCalled()\n })\n\n it('marks the client stale when the served Web UI version changes', async () => {\n mockSystemApi.checkHealth.mockResolvedValue({\n status: 'ok',\n webui_version: '0.5.17',\n webui_latest: '0.5.17',\n webui_update_available: false,\n })\n const store = useAppStore()\n\n await store.checkConnection()\n\n expect(store.connected).toBe(true)\n expect(store.serverVersion).toBe('0.5.17')\n expect(store.clientOutdated).toBe(true)\n expect(store.updateAvailable).toBe(false)\n })\n\n it('does not mark the client stale when the served Web UI version matches this bundle', async () => {\n mockSystemApi.checkHealth.mockResolvedValue({\n status: 'ok',\n webui_version: 'test',\n webui_latest: 'test',\n webui_update_available: false,\n })\n const store = useAppStore()\n\n await store.checkConnection()\n\n expect(store.serverVersion).toBe('test')\n expect(store.clientOutdated).toBe(false)\n })\n\n it('clears the updating state and reports failure when self-update request fails', async () => {\n const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})\n mockSystemApi.triggerUpdate.mockRejectedValue(new Error('install failed'))\n const store = useAppStore()\n\n const ok = await store.doUpdate()\n\n expect(ok).toBe(false)\n expect(store.updating).toBe(false)\n expect(consoleError).toHaveBeenCalledWith('Failed to update Hermes Web UI:', expect.any(Error))\n consoleError.mockRestore()\n })\n\n it('loads model aliases and resolves display names without changing canonical IDs', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: 'deepseek-v4-flash',\n default_provider: 'deepseek',\n groups: [{\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n models: ['deepseek-v4-flash'],\n api_key: '',\n }],\n allProviders: [],\n model_aliases: {\n deepseek: { 'deepseek-v4-flash': 'Flash Alias' },\n },\n })\n const store = useAppStore()\n\n await store.loadModels()\n\n expect(store.selectedModel).toBe('deepseek-v4-flash')\n expect(store.getModelAlias('deepseek-v4-flash', 'deepseek')).toBe('Flash Alias')\n expect(store.displayModelName('deepseek-v4-flash', 'deepseek')).toBe('Flash Alias')\n expect(store.displayModelName('unknown', 'deepseek')).toBe('unknown')\n })\n\n it('does not refetch available models within the cache window after an empty response', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: '',\n default_provider: '',\n groups: [],\n allProviders: [],\n })\n const store = useAppStore()\n\n await store.loadModels()\n await store.loadModels()\n\n expect(mockSystemApi.fetchAvailableModels).toHaveBeenCalledTimes(1)\n })\n\n it('waits only up to the run timeout for the first available models request', async () => {\n vi.useFakeTimers()\n mockSystemApi.fetchAvailableModels.mockReturnValue(new Promise(() => {}))\n const store = useAppStore()\n let resolved = false\n\n const waitPromise = store.waitForModelsForRun(15000).then(() => {\n resolved = true\n })\n\n expect(mockSystemApi.fetchAvailableModels).toHaveBeenCalledTimes(1)\n await vi.advanceTimersByTimeAsync(14999)\n expect(resolved).toBe(false)\n await vi.advanceTimersByTimeAsync(1)\n await waitPromise\n expect(resolved).toBe(true)\n expect(store.modelGroups).toEqual([])\n })\n\n it('keeps aliases scoped to their provider when model IDs overlap', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: 'shared-model',\n default_provider: 'provider-a',\n groups: [\n {\n provider: 'provider-a',\n label: 'Provider A',\n base_url: 'https://a.example/v1',\n models: ['shared-model'],\n api_key: '',\n },\n {\n provider: 'provider-b',\n label: 'Provider B',\n base_url: 'https://b.example/v1',\n models: ['shared-model'],\n api_key: '',\n },\n ],\n allProviders: [],\n model_aliases: {\n 'provider-a': { 'shared-model': 'A Alias' },\n },\n })\n const store = useAppStore()\n\n await store.loadModels()\n\n expect(store.displayModelName('shared-model', 'provider-a')).toBe('A Alias')\n expect(store.displayModelName('shared-model', 'provider-b')).toBe('shared-model')\n expect(store.displayModelName('shared-model')).toBe('A Alias')\n })\n\n it('rehydrates an active unlisted default model as removable after loading models', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: 'manually-supported-id',\n default_provider: 'deepseek',\n groups: [{\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n models: ['deepseek-v4-flash'],\n api_key: '',\n }],\n allProviders: [],\n model_aliases: {},\n })\n const store = useAppStore()\n\n await store.loadModels()\n\n expect(store.selectedModel).toBe('manually-supported-id')\n expect(store.customModels).toEqual({ deepseek: ['manually-supported-id'] })\n })\n\n it('loads persisted custom models from the server response', async () => {\n mockSystemApi.fetchAvailableModels.mockResolvedValue({\n default: 'gemma-4-26b-a4b-it',\n default_provider: 'google-ai-studio',\n groups: [{\n provider: 'google-ai-studio',\n label: 'Google AI Studio',\n base_url: 'https://generativelanguage.googleapis.com/v1beta',\n models: ['gemma-4-26b-a4b-it'],\n api_key: '',\n }],\n allProviders: [],\n custom_models: {\n 'google-ai-studio': ['gemma-4-26b-a4b-it'],\n },\n })\n const store = useAppStore()\n\n await store.loadModels()\n\n expect(store.selectedModel).toBe('gemma-4-26b-a4b-it')\n expect(store.customModels).toEqual({\n 'google-ai-studio': ['gemma-4-26b-a4b-it'],\n })\n })\n\n it('saves and clears model aliases via the Web UI-only alias API', async () => {\n mockSystemApi.updateModelAlias.mockResolvedValue(undefined)\n const store = useAppStore()\n\n await store.setModelAlias('deepseek-v4-flash', 'deepseek', ' Flash Alias ')\n\n expect(mockSystemApi.updateModelAlias).toHaveBeenCalledWith({\n provider: 'deepseek',\n model: 'deepseek-v4-flash',\n alias: 'Flash Alias',\n })\n expect(store.modelAliases).toEqual({ deepseek: { 'deepseek-v4-flash': 'Flash Alias' } })\n\n await store.setModelAlias('deepseek-v4-flash', 'deepseek', '')\n expect(store.modelAliases).toEqual({})\n })\n\n it('removes an unlisted custom model and falls back to a listed model when active', async () => {\n mockSystemApi.updateDefaultModel.mockResolvedValue(undefined)\n const store = useAppStore()\n store.modelGroups = [{\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n models: ['deepseek-v4-flash'],\n api_key: '',\n }]\n mockSystemApi.addCustomModel.mockResolvedValue({\n success: true,\n custom_models: { deepseek: ['test'] },\n })\n mockSystemApi.removeCustomModel.mockResolvedValue({\n success: true,\n custom_models: {},\n })\n\n await store.switchModel('test', 'deepseek')\n expect(store.selectedModel).toBe('test')\n expect(store.customModels).toEqual({ deepseek: ['test'] })\n expect(mockSystemApi.addCustomModel).toHaveBeenCalledWith({\n provider: 'deepseek',\n model: 'test',\n })\n\n await store.removeCustomModel('test', 'deepseek')\n expect(store.customModels).toEqual({})\n expect(mockSystemApi.removeCustomModel).toHaveBeenCalledWith({\n provider: 'deepseek',\n model: 'test',\n })\n expect(store.selectedModel).toBe('deepseek-v4-flash')\n expect(mockSystemApi.updateDefaultModel).toHaveBeenLastCalledWith({\n default: 'deepseek-v4-flash',\n provider: 'deepseek',\n })\n })\n\n it('removes deleted custom models from loaded model groups immediately', async () => {\n mockSystemApi.removeCustomModel.mockResolvedValue({\n success: true,\n custom_models: {},\n })\n const store = useAppStore()\n store.customModels = { deepseek: ['manual-model'] }\n store.modelGroups = [{\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n models: ['deepseek-v4-flash', 'manual-model'],\n available_models: ['deepseek-v4-flash', 'manual-model'],\n api_key: '',\n }]\n store.profileModelGroups = [{\n profile: 'default',\n default: 'deepseek-v4-flash',\n default_provider: 'deepseek',\n groups: [{\n provider: 'deepseek',\n label: 'DeepSeek',\n base_url: 'https://api.deepseek.com/v1',\n models: ['deepseek-v4-flash', 'manual-model'],\n available_models: ['deepseek-v4-flash', 'manual-model'],\n api_key: '',\n }],\n }]\n\n await store.removeCustomModel('manual-model', 'deepseek')\n\n expect(store.modelGroups[0].models).toEqual(['deepseek-v4-flash'])\n expect(store.modelGroups[0].available_models).toEqual(['deepseek-v4-flash'])\n expect(store.profileModelGroups[0].groups[0].models).toEqual(['deepseek-v4-flash'])\n expect(store.profileModelGroups[0].groups[0].available_models).toEqual(['deepseek-v4-flash'])\n })\n})\n"}}}}