import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Projects } from '@/pages/Projects'; import { useOpencodeStore } from '@/stores/opencode'; import { createProjectConfig } from '../../shared/project-config'; const hostApiFetchMock = vi.fn(); const invokeIpcMock = vi.fn(); vi.mock('@/lib/host-api', () => ({ hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args), ensureHostApiToken: vi.fn().mockResolvedValue('test-token'), })); vi.mock('@/lib/api-client', () => ({ invokeIpc: (...args: unknown[]) => invokeIpcMock(...args), })); describe('Projects page', () => { beforeEach(() => { vi.clearAllMocks(); useOpencodeStore.setState({ status: { state: 'stopped', port: 4096 }, health: null, projects: [], activeProject: null, sessions: [], sessionsByProjectId: {}, selectedSessionId: null, sessionStatuses: {}, sessionMessages: [], sessionMessagesBySessionId: {}, streamingMessage: null, streamingMessagesBySessionId: {}, streamingTools: [], streamingToolsBySessionId: {}, sendingSessionId: null, sendingSessionIds: {}, queuedSessionPrompts: {}, runtimeAutoStartAttempted: false, loading: false, error: null, }); }); it('renders projects loaded from the opencode project store', async () => { hostApiFetchMock.mockResolvedValueOnce({ projects: [{ id: 'prj_1', path: 'D:/repo/packages/ui', name: 'ui', createdAt: '2026-05-12T00:00:00.000Z', updatedAt: '2026-05-12T00:00:00.000Z', lastOpenedAt: '2026-05-12T00:00:00.000Z', }], activeProject: null, }); render(); await waitFor(() => expect(screen.getByText('ui')).toBeInTheDocument()); expect(screen.getByText('D:/repo/packages/ui')).toBeInTheDocument(); expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects'); }); it('opens a folder through the native picker', async () => { const project = { id: 'prj_2', path: 'D:/repo/packages/api', name: 'api', createdAt: '2026-05-12T00:00:00.000Z', updatedAt: '2026-05-12T00:00:00.000Z', lastOpenedAt: '2026-05-12T00:00:00.000Z', }; hostApiFetchMock .mockResolvedValueOnce({ projects: [], activeProject: null }) .mockResolvedValueOnce({ success: true, project, projects: [project], activeProject: project, }); invokeIpcMock.mockResolvedValueOnce({ canceled: false, filePaths: ['D:/repo/packages/api'], }); render(); await screen.findByText('No projects yet'); fireEvent.click(screen.getAllByRole('button', { name: /open folder/i })[0]); await waitFor(() => { expect(invokeIpcMock).toHaveBeenCalledWith('dialog:open', { properties: ['openDirectory'], title: 'Open Project Folder', }); }); expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/open', { method: 'POST', body: JSON.stringify({ path: 'D:/repo/packages/api' }), }); }); it('does not activate a project whose configuration is missing', async () => { const project = { id: 'prj_missing_template', path: 'D:/repo/packages/api', name: 'api', createdAt: '2026-05-12T00:00:00.000Z', updatedAt: '2026-05-12T00:00:00.000Z', lastOpenedAt: '2026-05-12T00:00:00.000Z', }; hostApiFetchMock .mockResolvedValueOnce({ projects: [project], activeProject: null }) .mockResolvedValueOnce({ status: 'missing' }); render(); const useButton = await screen.findByRole('button', { name: 'Use' }); fireEvent.click(useButton); await waitFor(() => { expect(hostApiFetchMock).toHaveBeenCalledWith( `/api/opencode/projects/config?projectId=${encodeURIComponent(project.id)}`, ); }); expect(useOpencodeStore.getState().activeProject).toBeNull(); expect(await screen.findByText(/configuration is missing/i)).toBeInTheDocument(); }); it('activates a project only after its configuration validates', async () => { const project = { id: 'prj_valid_template', path: 'D:/repo/packages/ui', name: 'ui', createdAt: '2026-05-12T00:00:00.000Z', updatedAt: '2026-05-12T00:00:00.000Z', lastOpenedAt: '2026-05-12T00:00:00.000Z', }; const config = { ...createProjectConfig(), initialized: true }; hostApiFetchMock .mockResolvedValueOnce({ projects: [project], activeProject: null }) .mockResolvedValueOnce({ status: 'valid', config, knowledgeFiles: [] }) .mockResolvedValueOnce({ success: true, project, projects: [project], activeProject: project, }); render(); fireEvent.click(await screen.findByRole('button', { name: 'Use' })); await waitFor(() => { expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/active', { method: 'POST', body: JSON.stringify({ projectId: project.id }), }); }); expect(useOpencodeStore.getState().activeProject).toEqual(project); }); });