2026-04-18 00:00:24 +08:00
|
|
|
// @vitest-environment jsdom
|
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
|
import { createPinia, setActivePinia } from 'pinia'
|
|
|
|
|
|
|
|
|
|
const mockSystemApi = vi.hoisted(() => ({
|
|
|
|
|
checkHealth: vi.fn(),
|
|
|
|
|
fetchAvailableModels: vi.fn(),
|
|
|
|
|
updateDefaultModel: vi.fn(),
|
2026-05-11 15:24:45 +02:00
|
|
|
updateModelVisibility: vi.fn(),
|
2026-04-18 00:00:24 +08:00
|
|
|
triggerUpdate: vi.fn(),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
vi.mock('@/api/hermes/system', () => mockSystemApi)
|
|
|
|
|
|
|
|
|
|
import { useAppStore } from '@/stores/hermes/app'
|
|
|
|
|
|
|
|
|
|
describe('App Store', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
setActivePinia(createPinia())
|
|
|
|
|
vi.clearAllMocks()
|
|
|
|
|
window.localStorage.clear()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('persists desktop sidebar collapsed state to localStorage', () => {
|
|
|
|
|
const store = useAppStore()
|
|
|
|
|
|
|
|
|
|
expect(store.sidebarCollapsed).toBe(false)
|
|
|
|
|
|
|
|
|
|
store.toggleSidebarCollapsed()
|
|
|
|
|
expect(store.sidebarCollapsed).toBe(true)
|
|
|
|
|
expect(window.localStorage.getItem('hermes_sidebar_collapsed')).toBe('1')
|
|
|
|
|
|
|
|
|
|
store.toggleSidebarCollapsed()
|
|
|
|
|
expect(store.sidebarCollapsed).toBe(false)
|
|
|
|
|
expect(window.localStorage.getItem('hermes_sidebar_collapsed')).toBe('0')
|
|
|
|
|
})
|
2026-05-10 14:18:52 +02:00
|
|
|
|
2026-05-11 15:24:45 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
it('loads model visibility and falls back when the configured default is hidden', async () => {
|
|
|
|
|
mockSystemApi.fetchAvailableModels.mockResolvedValue({
|
|
|
|
|
default: 'deepseek-chat',
|
|
|
|
|
default_provider: 'deepseek',
|
|
|
|
|
groups: [
|
|
|
|
|
{
|
|
|
|
|
provider: 'deepseek',
|
|
|
|
|
label: 'DeepSeek',
|
|
|
|
|
base_url: 'https://api.deepseek.com/v1',
|
|
|
|
|
api_key: 'sk-test',
|
|
|
|
|
models: ['deepseek-reasoner'],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
allProviders: [],
|
|
|
|
|
model_visibility: {
|
|
|
|
|
deepseek: { mode: 'include', models: ['deepseek-reasoner'] },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
const store = useAppStore()
|
|
|
|
|
|
|
|
|
|
await store.loadModels()
|
|
|
|
|
|
|
|
|
|
expect(store.modelVisibility).toEqual({
|
|
|
|
|
deepseek: { mode: 'include', models: ['deepseek-reasoner'] },
|
|
|
|
|
})
|
|
|
|
|
expect(store.selectedModel).toBe('deepseek-reasoner')
|
|
|
|
|
expect(store.selectedProvider).toBe('deepseek')
|
|
|
|
|
expect(store.isModelVisible('deepseek', 'deepseek-reasoner')).toBe(true)
|
|
|
|
|
expect(store.isModelVisible('deepseek', 'deepseek-chat')).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('persists model visibility without changing the canonical selected model id', async () => {
|
|
|
|
|
mockSystemApi.fetchAvailableModels.mockResolvedValue({
|
|
|
|
|
default: 'deepseek-reasoner',
|
|
|
|
|
default_provider: 'deepseek',
|
|
|
|
|
groups: [
|
|
|
|
|
{
|
|
|
|
|
provider: 'deepseek',
|
|
|
|
|
label: 'DeepSeek',
|
|
|
|
|
base_url: 'https://api.deepseek.com/v1',
|
|
|
|
|
api_key: 'sk-test',
|
|
|
|
|
models: ['deepseek-reasoner'],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
allProviders: [],
|
|
|
|
|
model_visibility: {
|
|
|
|
|
deepseek: { mode: 'include', models: ['deepseek-reasoner'] },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
mockSystemApi.updateModelVisibility.mockResolvedValue({
|
|
|
|
|
success: true,
|
|
|
|
|
model_visibility: {
|
|
|
|
|
deepseek: { mode: 'include', models: ['deepseek-reasoner'] },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
const store = useAppStore()
|
|
|
|
|
|
|
|
|
|
await store.setModelVisibility('deepseek', { mode: 'include', models: ['deepseek-reasoner'] })
|
|
|
|
|
|
|
|
|
|
expect(mockSystemApi.updateModelVisibility).toHaveBeenCalledWith({
|
|
|
|
|
provider: 'deepseek',
|
|
|
|
|
mode: 'include',
|
|
|
|
|
models: ['deepseek-reasoner'],
|
|
|
|
|
})
|
|
|
|
|
expect(store.selectedModel).toBe('deepseek-reasoner')
|
|
|
|
|
expect(store.selectedProvider).toBe('deepseek')
|
|
|
|
|
expect(mockSystemApi.updateDefaultModel).not.toHaveBeenCalled()
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-10 14:18:52 +02:00
|
|
|
it('clears the updating state and reports failure when self-update request fails', async () => {
|
|
|
|
|
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
|
mockSystemApi.triggerUpdate.mockRejectedValue(new Error('install failed'))
|
|
|
|
|
const store = useAppStore()
|
|
|
|
|
|
|
|
|
|
const ok = await store.doUpdate()
|
|
|
|
|
|
|
|
|
|
expect(ok).toBe(false)
|
|
|
|
|
expect(store.updating).toBe(false)
|
|
|
|
|
expect(consoleError).toHaveBeenCalledWith('Failed to update Hermes Web UI:', expect.any(Error))
|
|
|
|
|
consoleError.mockRestore()
|
|
|
|
|
})
|
2026-04-18 00:00:24 +08:00
|
|
|
})
|