import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import Settings from '@/pages/Settings'; import { createProjectConfig } from '../../shared/project-config'; import { useOpencodeStore } from '@/stores/opencode'; import { useSettingsStore } from '@/stores/settings'; const hostApiFetchMock = vi.fn(); const invokeIpcMock = vi.fn(); vi.mock('@/lib/host-api', () => ({ hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args), })); vi.mock('@/lib/api-client', () => ({ invokeIpc: (...args: unknown[]) => invokeIpcMock(...args), toUserMessage: (error: unknown) => (error instanceof Error ? error.message : String(error)), })); vi.mock('@/components/settings/UpdateSettings', () => ({ UpdateSettings: () => null, })); vi.mock('react-i18next', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useTranslation: () => ({ t: (key: string) => key, }), }; }); vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn(), }, })); function renderSettings() { return render( } /> } /> ); } function mockValidatedWorkspaceHostApi(activeProject: { id: string; path: string; name: string; createdAt: string; updatedAt: string; lastOpenedAt: string; }) { hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => { if (path === '/api/opencode/status') { return { state: 'stopped', port: 4096 }; } if (path === '/api/opencode/config-summary') { return { model: 'openai/gpt-5.4', smallModel: 'openai/gpt-5.4-mini', providerIds: ['openai', 'openrouter'], providerCount: 2, }; } if (path === '/api/opencode/projects') { return { projects: [activeProject], activeProject }; } if (path === '/api/opencode/start' && init?.method === 'POST') { return { success: true, status: { state: 'running', port: 4097, url: 'http://127.0.0.1:4097' }, }; } if (path === '/api/opencode/sessions') { return { sessions: [] }; } if (path === '/api/opencode/sessions/status') { return { statuses: {} }; } if (path === '/api/opencode/health') { return { ok: true, status: { state: 'running', port: 4097, url: 'http://127.0.0.1:4097' }, }; } throw new Error(`Unexpected path ${path}`); }); } describe('Settings runtime workspace', () => { beforeEach(() => { vi.clearAllMocks(); invokeIpcMock.mockResolvedValue(undefined); useSettingsStore.setState({ theme: 'system', language: 'en', launchAtStartup: false, telemetryEnabled: false, proxyEnabled: false, proxyServer: '', proxyHttpServer: '', proxyHttpsServer: '', proxyAllServer: '', proxyBypassRules: '', sidebarCollapsed: false, devModeUnlocked: false, }); useOpencodeStore.setState({ status: { state: 'stopped', port: 4096 }, health: null, healthCheckedAt: null, runtimeConfigSummary: null, projects: [], activeProject: null, sessions: [], sessionsByProjectId: {}, selectedSessionId: null, sessionStatuses: {}, sessionMessages: [], sessionMessagesBySessionId: {}, streamingMessage: null, streamingMessagesBySessionId: {}, streamingTools: [], streamingToolsBySessionId: {}, sendingSessionId: null, sendingSessionIds: {}, queuedSessionPrompts: {}, sessionDiffs: [], sessionDiffLoading: false, sessionDiffError: null, fileStatuses: [], fileSearchResults: [], contentSearchResults: [], selectedContextFile: null, fileContextLoading: false, fileContextError: null, runtimeAutoStartAttempted: false, loading: false, error: null, }); }); it('hides non-general settings until the general header reveal button is clicked', async () => { const activeProject = { 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', }; mockValidatedWorkspaceHostApi(activeProject); renderSettings(); expect(await screen.findByRole('heading', { name: 'appearance.title' })).toBeInTheDocument(); expect(screen.queryByTestId('settings-opencode-workspace')).not.toBeInTheDocument(); expect(screen.queryByText('Network')).not.toBeInTheDocument(); expect(screen.queryByText('updates.title')).not.toBeInTheDocument(); expect(screen.queryByText('advanced.title')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'appearance.showMoreSettings' })); expect(await screen.findByTestId('settings-opencode-workspace')).toBeInTheDocument(); expect(screen.getByText('Network')).toBeInTheDocument(); expect(screen.getByText('updates.title')).toBeInTheDocument(); expect(screen.getByText('advanced.title')).toBeInTheDocument(); }); it('renders runtime, model, and workspace controls in settings without exposing the runtime brand', async () => { const activeProject = { 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', }; mockValidatedWorkspaceHostApi(activeProject); renderSettings(); fireEvent.click(await screen.findByRole('button', { name: 'appearance.showMoreSettings' })); const workspace = await screen.findByTestId('settings-opencode-workspace'); expect(within(workspace).getByText('Workspace')).toBeInTheDocument(); expect(screen.getByTestId('settings-opencode-runtime')).toBeInTheDocument(); expect(screen.getByTestId('settings-opencode-models')).toBeInTheDocument(); expect(screen.getByText('openai/gpt-5.4')).toBeInTheDocument(); expect(screen.getByText('openai/gpt-5.4-mini')).toBeInTheDocument(); expect(screen.getByText('2 providers')).toBeInTheDocument(); expect(screen.getAllByText('D:/repo/packages/ui').length).toBeGreaterThan(0); await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/start', { method: 'POST', }) ); expect(screen.getByRole('button', { name: 'Restart runtime' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Stop runtime' })).toBeInTheDocument(); expect(document.body.textContent).not.toMatch(/opencode/i); expect(screen.queryByLabelText(/opencode/i)).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Check health' })); await waitFor(() => expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/health')); }); it('opens the model configuration page from the workspace models card', async () => { const activeProject = { 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', }; mockValidatedWorkspaceHostApi(activeProject); renderSettings(); fireEvent.click(await screen.findByRole('button', { name: 'appearance.showMoreSettings' })); const modelsCard = await screen.findByTestId('settings-opencode-models'); fireEvent.click(within(modelsCard).getByRole('button', { name: 'Configure models' })); expect(screen.getByTestId('models-config-page')).toBeInTheDocument(); }); it('does not activate a workspace project when its configuration is missing', async () => { const activeProject = { 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', }; hostApiFetchMock.mockImplementation(async (path: string, _init?: RequestInit) => { if (path === '/api/opencode/status') return { state: 'stopped', port: 4096 }; if (path === '/api/opencode/config-summary') { return { model: 'openai/gpt-5.4', smallModel: 'openai/gpt-5.4-mini', providerIds: ['openai'], providerCount: 1, }; } if (path === '/api/opencode/projects') return { projects: [activeProject], activeProject: null }; if (path === `/api/opencode/projects/config?projectId=${encodeURIComponent(activeProject.id)}`) { return { status: 'missing' }; } throw new Error(`Unexpected path ${path}`); }); renderSettings(); fireEvent.click(await screen.findByRole('button', { name: 'appearance.showMoreSettings' })); fireEvent.click(await screen.findByRole('button', { name: `Use project ${activeProject.name}` })); expect(await screen.findByText(/configuration is missing/i)).toBeInTheDocument(); expect(useOpencodeStore.getState().activeProject).toBeNull(); }); it('activates a workspace project only after its configuration validates', async () => { const activeProject = { 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', }; const config = { ...createProjectConfig(), initialized: true }; hostApiFetchMock.mockImplementation(async (path: string, init?: RequestInit) => { if (path === '/api/opencode/status') return { state: 'stopped', port: 4096 }; if (path === '/api/opencode/config-summary') { return { model: 'openai/gpt-5.4', smallModel: 'openai/gpt-5.4-mini', providerIds: ['openai'], providerCount: 1, }; } if (path === '/api/opencode/projects') return { projects: [activeProject], activeProject: null }; if (path === `/api/opencode/projects/config?projectId=${encodeURIComponent(activeProject.id)}`) { return { status: 'valid', config, knowledgeFiles: [] }; } if (path === '/api/opencode/projects/active' && init?.method === 'POST') { return { success: true, project: activeProject, projects: [activeProject], activeProject, }; } throw new Error(`Unexpected path ${path}`); }); renderSettings(); fireEvent.click(await screen.findByRole('button', { name: 'appearance.showMoreSettings' })); fireEvent.click(await screen.findByRole('button', { name: `Use project ${activeProject.name}` })); await waitFor(() => { expect(hostApiFetchMock).toHaveBeenCalledWith('/api/opencode/projects/active', { method: 'POST', body: JSON.stringify({ projectId: activeProject.id }), }); }); expect(useOpencodeStore.getState().activeProject).toEqual(activeProject); }); });