diff --git a/apps/desktop/src/lib/gateway-rpc.test.ts b/apps/desktop/src/lib/gateway-rpc.test.ts new file mode 100644 index 00000000000..6da30b87b76 --- /dev/null +++ b/apps/desktop/src/lib/gateway-rpc.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest' + +import { isMissingRpcMethod } from './gateway-rpc' + +describe('isMissingRpcMethod', () => { + it('detects JSON-RPC method-not-found errors', () => { + expect(isMissingRpcMethod(new Error('unknown method: projects.create'))).toBe(true) + expect(isMissingRpcMethod(new Error('Method not found'))).toBe(true) + expect(isMissingRpcMethod(new Error('RPC failed: -32601'))).toBe(true) + }) + + it('ignores unrelated failures', () => { + expect(isMissingRpcMethod(new Error('Hermes gateway is not connected'))).toBe(false) + expect(isMissingRpcMethod(new Error('no such project'))).toBe(false) + }) +}) diff --git a/apps/desktop/src/store/projects.test.ts b/apps/desktop/src/store/projects.test.ts index 9e80fe5b889..24bc2a6f854 100644 --- a/apps/desktop/src/store/projects.test.ts +++ b/apps/desktop/src/store/projects.test.ts @@ -5,15 +5,26 @@ import { $sidebarAgentsGrouped } from '@/store/layout' import { $activeProjectId, $projectScope, + $projectsRpcAvailable, $worktreeRefreshToken, ALL_PROJECTS, createProject, enterProject, exitProjectScope, + openProjectCreate, pickProjectFolder, + refreshProjects, refreshWorktrees } from './projects' +vi.mock('@/i18n', () => ({ + translateNow: (key: string) => key +})) + +vi.mock('@/store/notifications', () => ({ + notify: vi.fn() +})) + vi.mock('@/lib/desktop-fs', () => ({ desktopDefaultCwd: vi.fn(), isDesktopFsRemoteMode: vi.fn(), @@ -33,6 +44,8 @@ const selectDesktopPaths = vi.mocked(fs.selectDesktopPaths) const gw = await import('@/store/gateway') const activeGateway = vi.mocked(gw.activeGateway) +const notifications = await import('@/store/notifications') +const notify = vi.mocked(notifications.notify) describe('project scope', () => { beforeEach(() => { @@ -115,6 +128,7 @@ describe('createProject', () => { vi.clearAllMocks() $sidebarAgentsGrouped.set(false) $activeProjectId.set(null) + $projectsRpcAvailable.set(null) }) it('creates the project and flips into the grouped view so a blank slate shows it', async () => { @@ -139,4 +153,44 @@ describe('createProject', () => { expect($sidebarAgentsGrouped.get()).toBe(true) expect($activeProjectId.get()).toBe('p_new') }) + + it('marks the backend stale and surfaces a friendly error when projects.create is missing', async () => { + activeGateway.mockReturnValue({ + connectionState: 'open', + request: vi.fn().mockRejectedValue(new Error('unknown method: projects.create')) + } as never) + + await expect(createProject({ folders: ['/srv/demo'], name: 'Demo' })).rejects.toThrow( + 'sidebar.projects.staleBackend' + ) + expect($projectsRpcAvailable.get()).toBe(false) + }) +}) + +describe('projects RPC capability', () => { + beforeEach(() => { + vi.clearAllMocks() + $projectsRpcAvailable.set(null) + }) + + it('marks the backend stale when projects.list is missing', async () => { + activeGateway.mockReturnValue({ + connectionState: 'open', + request: vi.fn().mockRejectedValue(new Error('unknown method: projects.list')) + } as never) + + await refreshProjects() + + expect($projectsRpcAvailable.get()).toBe(false) + }) + + it('blocks opening the create dialog once the backend is known stale', () => { + $projectsRpcAvailable.set(false) + + openProjectCreate() + + expect(notify).toHaveBeenCalledWith( + expect.objectContaining({ kind: 'warning', message: 'sidebar.projects.staleBackend' }) + ) + }) })