import { existsSync, readFileSync } from 'fs' import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises' import { tmpdir } from 'os' import { join } from 'path' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' // Mock hermes-cli vi.mock('../../packages/server/src/services/hermes/hermes-cli', () => ({ listProfiles: vi.fn(), getProfile: vi.fn(), createProfile: vi.fn(), deleteProfile: vi.fn(), renameProfile: vi.fn(), useProfile: vi.fn(), stopGateway: vi.fn(), startGateway: vi.fn(), startGatewayBackground: vi.fn(), setupReset: vi.fn(), exportProfile: vi.fn(), importProfile: vi.fn(), })) import * as hermesCli from '../../packages/server/src/services/hermes/hermes-cli' describe('Profile Routes', () => { const originalHermesHome = process.env.HERMES_HOME const tempHomes: string[] = [] beforeEach(() => { vi.clearAllMocks() }) afterEach(async () => { if (originalHermesHome === undefined) delete process.env.HERMES_HOME else process.env.HERMES_HOME = originalHermesHome await Promise.all(tempHomes.splice(0).map(dir => rm(dir, { recursive: true, force: true }))) }) describe('hermes-cli wrapper', () => { it('listProfiles returns array', async () => { const mockProfiles = [{ name: 'default', active: true }] vi.mocked(hermesCli.listProfiles).mockResolvedValue(mockProfiles as any) const result = await hermesCli.listProfiles() expect(result).toEqual(mockProfiles) }) it('getProfile returns profile detail', async () => { const mockDetail = { name: 'default', path: '/tmp/default' } vi.mocked(hermesCli.getProfile).mockResolvedValue(mockDetail as any) const result = await hermesCli.getProfile('default') expect(result).toEqual(mockDetail) expect(hermesCli.getProfile).toHaveBeenCalledWith('default') }) it('createProfile calls CLI with name and clone flag', async () => { vi.mocked(hermesCli.createProfile).mockResolvedValue('Profile created') await hermesCli.createProfile('test', true) expect(hermesCli.createProfile).toHaveBeenCalledWith('test', true) }) it('deleteProfile calls CLI with name', async () => { vi.mocked(hermesCli.deleteProfile).mockResolvedValue(true) await hermesCli.deleteProfile('test') expect(hermesCli.deleteProfile).toHaveBeenCalledWith('test') }) it('renameProfile calls CLI with old and new name', async () => { vi.mocked(hermesCli.renameProfile).mockResolvedValue(true) await hermesCli.renameProfile('old', 'new') expect(hermesCli.renameProfile).toHaveBeenCalledWith('old', 'new') }) }) describe('profile deletion fallback', () => { it('removes a reserved profile directory when Hermes CLI refuses to delete it', async () => { const hermesHome = await mkdtemp(join(tmpdir(), 'hermes-profile-delete-')) tempHomes.push(hermesHome) process.env.HERMES_HOME = hermesHome const badProfileDir = join(hermesHome, 'profiles', 'hermes') await mkdir(badProfileDir, { recursive: true }) await writeFile(join(badProfileDir, 'config.yaml'), 'model:\n default: bad\n', 'utf-8') await writeFile(join(hermesHome, 'active_profile'), 'hermes\n', 'utf-8') vi.mocked(hermesCli.deleteProfile).mockResolvedValue(false) const { remove } = await import('../../packages/server/src/controllers/hermes/profiles') const ctx: any = { params: { name: 'hermes' }, status: 200, body: undefined } await remove(ctx) expect(ctx.status).toBe(200) expect(ctx.body).toEqual({ success: true, fallback: 'removed_reserved_profile_from_disk' }) expect(existsSync(badProfileDir)).toBe(false) expect(readFileSync(join(hermesHome, 'active_profile'), 'utf-8')).toBe('default\n') }) it('does not bypass Hermes CLI failures for normal profile names', async () => { const hermesHome = await mkdtemp(join(tmpdir(), 'hermes-profile-delete-')) tempHomes.push(hermesHome) process.env.HERMES_HOME = hermesHome const profileDir = join(hermesHome, 'profiles', 'work') await mkdir(profileDir, { recursive: true }) vi.mocked(hermesCli.deleteProfile).mockResolvedValue(false) const { remove } = await import('../../packages/server/src/controllers/hermes/profiles') const ctx: any = { params: { name: 'work' }, status: 200, body: undefined } await remove(ctx) expect(ctx.status).toBe(500) expect(ctx.body).toEqual({ error: 'Failed to delete profile' }) expect(existsSync(profileDir)).toBe(true) }) }) })