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.
229 lines
8.2 KiB
TypeScript
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')
|
|
})
|
|
})
|