mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-05 12:42:30 +00:00
Route FS/git REST through the active profile, mount the remote folder picker at app root, keep the project dialog open while picking, show a first-run blank state, flip into grouped view on create, and constrain the picker scroll area so Select stays reachable.
142 lines
4.5 KiB
TypeScript
142 lines
4.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { $sidebarAgentsGrouped } from '@/store/layout'
|
|
|
|
import {
|
|
$activeProjectId,
|
|
$projectScope,
|
|
$worktreeRefreshToken,
|
|
ALL_PROJECTS,
|
|
createProject,
|
|
enterProject,
|
|
exitProjectScope,
|
|
pickProjectFolder,
|
|
refreshWorktrees
|
|
} from './projects'
|
|
|
|
vi.mock('@/lib/desktop-fs', () => ({
|
|
desktopDefaultCwd: vi.fn(),
|
|
isDesktopFsRemoteMode: vi.fn(),
|
|
selectDesktopPaths: vi.fn(),
|
|
writeDesktopFileText: vi.fn()
|
|
}))
|
|
|
|
vi.mock('@/store/gateway', () => ({
|
|
activeGateway: vi.fn(),
|
|
ensureActiveGatewayOpen: vi.fn()
|
|
}))
|
|
|
|
const fs = await import('@/lib/desktop-fs')
|
|
const desktopDefaultCwd = vi.mocked(fs.desktopDefaultCwd)
|
|
const isDesktopFsRemoteMode = vi.mocked(fs.isDesktopFsRemoteMode)
|
|
const selectDesktopPaths = vi.mocked(fs.selectDesktopPaths)
|
|
|
|
const gw = await import('@/store/gateway')
|
|
const activeGateway = vi.mocked(gw.activeGateway)
|
|
|
|
describe('project scope', () => {
|
|
beforeEach(() => {
|
|
window.localStorage.clear()
|
|
$projectScope.set(ALL_PROJECTS)
|
|
})
|
|
|
|
it('defaults to ALL_PROJECTS', () => {
|
|
expect($projectScope.get()).toBe(ALL_PROJECTS)
|
|
})
|
|
|
|
it('enterProject scopes the sidebar to the project id', () => {
|
|
// setActiveProject fires best-effort (no gateway in test → it rejects and is
|
|
// swallowed); the synchronous scope change is what matters here.
|
|
enterProject('p_123')
|
|
expect($projectScope.get()).toBe('p_123')
|
|
})
|
|
|
|
it('exitProjectScope returns to the overview', () => {
|
|
enterProject('p_123')
|
|
exitProjectScope()
|
|
expect($projectScope.get()).toBe(ALL_PROJECTS)
|
|
})
|
|
|
|
it('entering the synthetic No-project bucket still scopes (no active pin)', () => {
|
|
enterProject('__no_project__')
|
|
expect($projectScope.get()).toBe('__no_project__')
|
|
})
|
|
|
|
it('persists the scope to localStorage', () => {
|
|
enterProject('p_abc')
|
|
expect(window.localStorage.getItem('hermes.desktop.projectScope')).toBe('p_abc')
|
|
})
|
|
})
|
|
|
|
describe('worktree refresh', () => {
|
|
it('refreshWorktrees bumps the probe token so useRepoWorktreeMap refetches', () => {
|
|
const before = $worktreeRefreshToken.get()
|
|
refreshWorktrees()
|
|
expect($worktreeRefreshToken.get()).toBe(before + 1)
|
|
})
|
|
})
|
|
|
|
describe('pickProjectFolder', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('uses the remote-aware directory picker locally', async () => {
|
|
isDesktopFsRemoteMode.mockReturnValue(false)
|
|
selectDesktopPaths.mockResolvedValue(['/local/repo'])
|
|
|
|
await expect(pickProjectFolder()).resolves.toBe('/local/repo')
|
|
expect(selectDesktopPaths).toHaveBeenCalledWith({ defaultPath: undefined, directories: true, multiple: false })
|
|
})
|
|
|
|
it('seeds the picker with the backend cwd on a remote gateway', async () => {
|
|
isDesktopFsRemoteMode.mockReturnValue(true)
|
|
desktopDefaultCwd.mockResolvedValue({ branch: 'main', cwd: '/backend/work' })
|
|
selectDesktopPaths.mockResolvedValue(['/backend/work/repo'])
|
|
|
|
await expect(pickProjectFolder()).resolves.toBe('/backend/work/repo')
|
|
expect(selectDesktopPaths).toHaveBeenCalledWith({
|
|
defaultPath: '/backend/work',
|
|
directories: true,
|
|
multiple: false
|
|
})
|
|
})
|
|
|
|
it('returns null when the picker is cancelled (empty selection)', async () => {
|
|
isDesktopFsRemoteMode.mockReturnValue(false)
|
|
selectDesktopPaths.mockResolvedValue([])
|
|
|
|
await expect(pickProjectFolder()).resolves.toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('createProject', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
$sidebarAgentsGrouped.set(false)
|
|
$activeProjectId.set(null)
|
|
})
|
|
|
|
it('creates the project and flips into the grouped view so a blank slate shows it', async () => {
|
|
const created = { folders: [], id: 'p_new', name: 'Demo', primary_path: '/srv/demo' }
|
|
|
|
const request = vi.fn(async (method: string) => {
|
|
if (method === 'projects.create') {
|
|
return { project: created }
|
|
}
|
|
|
|
// Reconcile (fire-and-forget) re-reads list + tree; echo the project back
|
|
// so the optimistic state survives instead of being wiped to empty.
|
|
return { active_id: 'p_new', projects: [created], scoped_session_ids: [] }
|
|
})
|
|
|
|
activeGateway.mockReturnValue({ connectionState: 'open', request } as never)
|
|
|
|
const result = await createProject({ folders: ['/srv/demo'], name: 'Demo', use: true })
|
|
|
|
expect(result).toEqual(created)
|
|
expect(request).toHaveBeenCalledWith('projects.create', expect.objectContaining({ name: 'Demo' }))
|
|
expect($sidebarAgentsGrouped.get()).toBe(true)
|
|
expect($activeProjectId.get()).toBe('p_new')
|
|
})
|
|
})
|