Files
hermes-ui/tests/client/profiles-store.test.ts
T
toor 1e9ea89d86
Build / build (push) Canceled after 0s
NPM Lockfile Check / npm ci --ignore-scripts (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
chore: bootstrap hermes-ui skeleton from 0.6.29 snapshot
Import the client, server, skills, tests, and build scripts as the starting
point for the rebuild, then trim the surfaces this project does not ship:
Electron desktop packaging, the marketing website, and the ESP32 firmware
tree. CI keeps the web build, e2e, lockfile, release, and Docker workflows.

Rename the package and CLI entry points to hermes-ui, drop the prepare hook so
npm ci no longer triggers a build, and drop the removed tsconfig and nodemon
references. Rewrite AGENTS.md, ARCHITECTURE.md, DEVELOPMENT.md, and the READMEs
so the documented package map and hard rules match this repository.

reference_code/ is ignored: it is a read-only snapshot, not an input to the build.
2026-10-16 10:34:35 +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')
})
})