Files
hermes-web-ui/tests/client/profiles-store.test.ts
2026-05-24 10:11:03 +08:00

229 lines
8.2 KiB
TypeScript

// @vitest-environment jsdom
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
const mockProfilesApi = vi.hoisted(() => ({
fetchProfiles: vi.fn(),
fetchProfileDetail: vi.fn(),
createProfile: vi.fn(),
deleteProfile: vi.fn(),
renameProfile: vi.fn(),
switchProfile: vi.fn(),
exportProfile: vi.fn(),
importProfile: vi.fn(),
updateProfileAvatar: vi.fn(),
deleteProfileAvatar: vi.fn(),
}))
vi.mock('@/api/hermes/profiles', () => mockProfilesApi)
import { useProfilesStore } from '@/stores/hermes/profiles'
describe('Profiles Store', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
})
it('fetchProfiles loads profiles and sets active', async () => {
const profiles = [
{ name: 'default', active: true, model: 'gpt-4', alias: '' },
{ name: 'dev', active: false, model: 'gpt-4', alias: '' },
]
mockProfilesApi.fetchProfiles.mockResolvedValue(profiles)
const store = useProfilesStore()
await store.fetchProfiles()
expect(store.profiles).toEqual(profiles)
expect(store.activeProfile?.name).toBe('default')
expect(store.loading).toBe(false)
})
it('fetchProfiles sets loading state', async () => {
mockProfilesApi.fetchProfiles.mockImplementation(
() => new Promise(resolve => setTimeout(() => resolve([]), 10))
)
const store = useProfilesStore()
const fetchPromise = store.fetchProfiles()
expect(store.loading).toBe(true)
await fetchPromise
expect(store.loading).toBe(false)
})
it('createProfile calls API and refreshes list', async () => {
mockProfilesApi.createProfile.mockResolvedValue({ success: true })
mockProfilesApi.fetchProfiles.mockResolvedValue([
{ name: 'default', active: true, model: 'gpt-4', alias: '' },
{ name: 'new-profile', active: false, model: 'gpt-4', alias: '' },
])
const store = useProfilesStore()
const result = await store.createProfile('new-profile', false)
expect(result.success).toBe(true)
expect(mockProfilesApi.createProfile).toHaveBeenCalledWith('new-profile', false)
expect(store.profiles).toHaveLength(2)
})
it('deleteProfile clears detail cache', async () => {
mockProfilesApi.deleteProfile.mockResolvedValue(true)
mockProfilesApi.fetchProfiles.mockResolvedValue([
{ name: 'default', active: true, model: 'gpt-4', alias: '' },
])
const store = useProfilesStore()
store.detailMap['test'] = { name: 'test', path: '/tmp/test', model: '', provider: '', skills: 0, hasEnv: false, hasSoulMd: false }
await store.deleteProfile('test')
expect(store.detailMap['test']).toBeUndefined()
expect(mockProfilesApi.deleteProfile).toHaveBeenCalledWith('test')
})
it('fetchProfileDetail uses cache', async () => {
const detail = { name: 'cached', path: '/tmp/cached', model: 'gpt-4', provider: 'openai', skills: 5, hasEnv: true, hasSoulMd: false }
const store = useProfilesStore()
store.detailMap['cached'] = detail
const result = await store.fetchProfileDetail('cached')
expect(result).toEqual(detail)
expect(mockProfilesApi.fetchProfileDetail).not.toHaveBeenCalled()
})
it('updateAvatar updates profile, detail cache, and active profile', async () => {
const savedAvatar = { type: 'image', dataUrl: 'data:image/png;base64,YQ==' }
mockProfilesApi.updateProfileAvatar.mockResolvedValue(savedAvatar)
const store = useProfilesStore()
store.profiles = [
{ name: 'default', active: true, model: 'gpt-4', alias: '' },
{ name: 'dev', active: false, model: 'gpt-4', alias: '' },
]
store.activeProfile = store.profiles[0]
store.detailMap.default = { name: 'default', path: '/tmp/default', model: '', provider: '', skills: 0, hasEnv: false, hasSoulMd: false }
const result = await store.updateAvatar('default', { type: 'image', dataUrl: savedAvatar.dataUrl })
expect(result).toEqual(savedAvatar)
expect(mockProfilesApi.updateProfileAvatar).toHaveBeenCalledWith('default', { type: 'image', dataUrl: savedAvatar.dataUrl })
expect(store.profiles[0].avatar).toEqual(savedAvatar)
expect(store.activeProfile?.avatar).toEqual(savedAvatar)
expect(store.detailMap.default.avatar).toEqual(savedAvatar)
})
it('deleteAvatar clears avatar state', async () => {
mockProfilesApi.deleteProfileAvatar.mockResolvedValue(undefined)
const store = useProfilesStore()
store.profiles = [
{ name: 'default', active: true, model: 'gpt-4', alias: '', avatar: { type: 'generated', seed: 'old' } },
]
store.activeProfile = store.profiles[0]
store.detailMap.default = {
name: 'default',
path: '/tmp/default',
model: '',
provider: '',
skills: 0,
hasEnv: false,
hasSoulMd: false,
avatar: { type: 'generated', seed: 'old' },
}
await store.deleteAvatar('default')
expect(mockProfilesApi.deleteProfileAvatar).toHaveBeenCalledWith('default')
expect(store.profiles[0].avatar).toBeNull()
expect(store.activeProfile?.avatar).toBeNull()
expect(store.detailMap.default.avatar).toBeNull()
})
it('switchProfile sets switching state', async () => {
mockProfilesApi.switchProfile.mockResolvedValue(true)
mockProfilesApi.fetchProfiles.mockResolvedValue([])
const store = useProfilesStore()
const switchPromise = store.switchProfile('dev')
expect(store.switching).toBe(true)
await switchPromise
expect(store.switching).toBe(false)
})
it('switchProfile updates activeProfileName immediately', async () => {
mockProfilesApi.switchProfile.mockResolvedValue(true)
mockProfilesApi.fetchProfiles.mockResolvedValue([
{ name: 'default', active: false, model: 'gpt-4', alias: '' },
{ name: 'dev', active: true, model: 'gpt-4', alias: '' },
])
const store = useProfilesStore()
await store.switchProfile('dev')
// activeProfileName should be updated immediately
expect(store.activeProfileName).toBe('dev')
// localStorage should also be updated
expect(localStorage.getItem('hermes_active_profile_name')).toBe('dev')
})
it('switchProfile does not update state when API fails', async () => {
const initialName = 'default'
localStorage.setItem('hermes_active_profile_name', initialName)
mockProfilesApi.switchProfile.mockResolvedValue(false) // API failed
const store = useProfilesStore()
store.activeProfileName = initialName
const result = await store.switchProfile('dev')
// Should return false
expect(result).toBe(false)
// activeProfileName should NOT change
expect(store.activeProfileName).toBe(initialName)
// localStorage should NOT change
expect(localStorage.getItem('hermes_active_profile_name')).toBe(initialName)
})
it('switchProfile keeps activeProfileName even if fetchProfiles fails', async () => {
const initialName = 'default'
localStorage.setItem('hermes_active_profile_name', initialName)
mockProfilesApi.switchProfile.mockResolvedValue(true)
mockProfilesApi.fetchProfiles.mockRejectedValue(new Error('Network error'))
const store = useProfilesStore()
store.activeProfileName = initialName
const result = await store.switchProfile('dev')
// Should return true (API succeeded)
expect(result).toBe(true)
// activeProfileName should be updated even though fetchProfiles failed
expect(store.activeProfileName).toBe('dev')
// localStorage should be updated
expect(localStorage.getItem('hermes_active_profile_name')).toBe('dev')
})
it('switchProfile keeps the local selected profile independent of backend active flags', async () => {
const initialName = 'default'
localStorage.setItem('hermes_active_profile_name', initialName)
mockProfilesApi.switchProfile.mockResolvedValue(true)
mockProfilesApi.fetchProfiles.mockResolvedValue([
{ name: 'default', active: true, model: 'gpt-4', alias: '' },
{ name: 'dev', active: false, model: 'gpt-4', alias: '' },
])
const store = useProfilesStore()
store.activeProfileName = initialName
const result = await store.switchProfile('dev')
expect(result).toBe(true)
expect(store.activeProfileName).toBe('dev')
expect(localStorage.getItem('hermes_active_profile_name')).toBe('dev')
})
})