/
GreyCat
/
openhuman
Обзор
Документация
Войти
/
GreyCat
/
openhuman
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
app/src/components/BootCheckGate/__tests__/BootCheckGate.test.tsx
898 строк
31 KB
Martian
fix(tauri): identify foreign process on core port + consent-gated force-quit (#3331) (#3946)
23 июн 2026, 15:00
Не верифицирован
23 июн 2026, 15:00
f1dcce4
Код
Авторство
О чём код?
/** * Component tests for BootCheckGate. * * Strategy: * - Mock runBootCheck so we control the result without real RPC/invoke. * - Use a minimal Redux store that starts with coreMode.mode = 'unset' * (picker) or set (check flow). * - Assert rendered text and dispatched actions for each meaningful state. */ import { configureStore } from '@reduxjs/toolkit'; import { isTauri } from '@tauri-apps/api/core'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { Provider } from 'react-redux'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import coreModeReducer, { type CoreModeState } from '../../../store/coreModeSlice'; import localeReducer from '../../../store/localeSlice'; import BootCheckGate from '../BootCheckGate'; // The global test setup mocks isTauri()=>false (web). The existing picker // behavior under test was written for desktop (local option visible, // pre-selected). Force desktop runtime for those describes; the new web // describe at the bottom flips it back to false. const mockedIsTauri = vi.mocked(isTauri); // --------------------------------------------------------------------------- // Mocks // --------------------------------------------------------------------------- const mockRunBootCheck = vi.fn(); vi.mock('../../../lib/bootCheck', () => ({ runBootCheck: (...args: unknown[]) => mockRunBootCheck(...args), })); const mockRecoverPortConflict = vi.fn(); const mockForceQuitPortOwner = vi.fn(); vi.mock('../../../services/bootCheckService', async importOriginal => { const actual = await importOriginal<typeof import('../../../services/bootCheckService')>(); return { ...actual, recoverPortConflict: (...args: unknown[]) => mockRecoverPortConflict(...args), forceQuitPortOwner: (...args: unknown[]) => mockForceQuitPortOwner(...args), }; }); const mockTestCoreRpcConnection = vi.fn(); vi.mock('../../../services/coreRpcClient', () => ({ callCoreRpc: vi.fn(), clearCoreRpcUrlCache: vi.fn(), clearCoreRpcTokenCache: vi.fn(), testCoreRpcConnection: (...args: unknown[]) => mockTestCoreRpcConnection(...args), })); vi.mock('../../../utils/configPersistence', async importOriginal => { const actual = await importOriginal<typeof import('../../../utils/configPersistence')>(); return { ...actual, storeRpcUrl: vi.fn(), storeCoreToken: vi.fn(), clearStoredCoreToken: vi.fn(), storeCoreMode: vi.fn(), clearStoredCoreMode: vi.fn(), }; }); // --------------------------------------------------------------------------- // Store factory // --------------------------------------------------------------------------- function makeStore(initialMode?: CoreModeState['mode']) { return configureStore({ reducer: { coreMode: coreModeReducer, locale: localeReducer }, preloadedState: { coreMode: { mode: initialMode ?? { kind: 'unset' } } satisfies CoreModeState, }, }); } function renderGate(store = makeStore()) { return render( <Provider store={store}> <BootCheckGate> <div data-testid="app-content">App Content</div> </BootCheckGate> </Provider> ); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- // All describes below assume desktop unless they explicitly opt out. beforeEach(() => { mockedIsTauri.mockReturnValue(true); }); describe('BootCheckGate — picker (unset mode)', () => { it('shows the mode picker when coreMode is unset', () => { renderGate(); expect(screen.getByText('Select a Runtime')).toBeInTheDocument(); expect(screen.getByText('Run Locally (Recommended)')).toBeInTheDocument(); expect(screen.getByText('Run on the Cloud (Complex)')).toBeInTheDocument(); }); it('does NOT render children while in picker', () => { renderGate(); expect(screen.queryByTestId('app-content')).not.toBeInTheDocument(); }); it('continues with local mode when user clicks Continue', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'match' }); renderGate(); // Local is pre-selected — just click Continue fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByTestId('app-content')).toBeInTheDocument(); }); }); it('shows URL input when user selects Cloud', () => { renderGate(); fireEvent.click(screen.getByText('Run on the Cloud (Complex)')); expect(screen.getByPlaceholderText(/https:\/\/core\.example\.com/)).toBeInTheDocument(); }); it('shows URL validation error when cloud URL is empty', () => { renderGate(); fireEvent.click(screen.getByText('Run on the Cloud (Complex)')); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); expect(screen.getByText('Please enter a runtime URL.')).toBeInTheDocument(); }); it('shows URL validation error for non-http URL', () => { renderGate(); fireEvent.click(screen.getByText('Run on the Cloud (Complex)')); const input = screen.getByPlaceholderText(/https:\/\/core\.example\.com/); fireEvent.change(input, { target: { value: 'ftp://invalid' } }); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); expect(screen.getByText(/start with http/)).toBeInTheDocument(); }); it('shows URL validation error for malformed URL string', () => { renderGate(); fireEvent.click(screen.getByText('Run on the Cloud (Complex)')); const input = screen.getByPlaceholderText(/https:\/\/core\.example\.com/); fireEvent.change(input, { target: { value: 'not a url at all' } }); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); expect(screen.getByText(/That doesn't look like a valid URL/)).toBeInTheDocument(); }); it('shows token validation error when cloud URL is valid but token is missing', () => { renderGate(); fireEvent.click(screen.getByText('Run on the Cloud (Complex)')); const urlInput = screen.getByPlaceholderText(/https:\/\/core\.example\.com/); fireEvent.change(urlInput, { target: { value: 'https://core.example.com/rpc' } }); // Token left blank. fireEvent.click(screen.getByRole('button', { name: 'Continue' })); expect(screen.getByText(/We'll need an auth token to connect/i)).toBeInTheDocument(); }); it('accepts a Tailscale HTTP core URL in cloud mode', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'match' }); renderGate(); fireEvent.click(screen.getByText('Run on the Cloud (Complex)')); fireEvent.change(screen.getByPlaceholderText(/https:\/\/core\.example\.com/), { target: { value: 'http://100.116.244.64:7788/rpc' }, }); fireEvent.change(screen.getByPlaceholderText(/Bearer token/i), { target: { value: 'tok-1234' }, }); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByTestId('app-content')).toBeInTheDocument(); }); expect(mockRunBootCheck).toHaveBeenCalledWith( expect.objectContaining({ kind: 'cloud', url: 'http://100.116.244.64:7788/rpc', token: 'tok-1234', }), expect.any(Object) ); }); it('normalizes a cloud core base URL to the /rpc endpoint before continuing', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'match' }); renderGate(); fireEvent.click(screen.getByText('Run on the Cloud (Complex)')); fireEvent.change(screen.getByPlaceholderText(/https:\/\/core\.example\.com/), { target: { value: 'https://example.trycloudflare.com/' }, }); fireEvent.change(screen.getByPlaceholderText(/Bearer token/i), { target: { value: 'tok-1234' }, }); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByTestId('app-content')).toBeInTheDocument(); }); expect(mockRunBootCheck).toHaveBeenCalledWith( expect.objectContaining({ kind: 'cloud', url: 'https://example.trycloudflare.com/rpc', token: 'tok-1234', }), expect.any(Object) ); }); it('warns about public HTTP cloud URLs but does not block them', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'match' }); renderGate(); fireEvent.click(screen.getByText('Run on the Cloud (Complex)')); const urlInput = screen.getByPlaceholderText(/https:\/\/core\.example\.com/); fireEvent.change(urlInput, { target: { value: 'http://core.example.com/rpc' } }); // Non-blocking warning shows inline as soon as the public HTTP URL is typed. expect(screen.getByText(/traffic will not be encrypted/i)).toBeInTheDocument(); fireEvent.change(screen.getByPlaceholderText(/Bearer token/i), { target: { value: 'tok-1234' }, }); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); // The boot check still proceeds with the HTTP URL. await waitFor(() => { expect(mockRunBootCheck).toHaveBeenCalledWith( expect.objectContaining({ kind: 'cloud', url: 'http://core.example.com/rpc', token: 'tok-1234', }), expect.any(Object) ); }); }); it('clears the token error as soon as the user types into the token field', () => { renderGate(); fireEvent.click(screen.getByText('Run on the Cloud (Complex)')); fireEvent.change(screen.getByPlaceholderText(/https:\/\/core\.example\.com/), { target: { value: 'https://core.example.com/rpc' }, }); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); expect(screen.getByText(/We'll need an auth token to connect/i)).toBeInTheDocument(); const tokenInput = screen.getByPlaceholderText(/Bearer token/i); fireEvent.change(tokenInput, { target: { value: 'tok' } }); expect(screen.queryByText(/We'll need an auth token to connect/i)).not.toBeInTheDocument(); }); it('advances past picker and triggers boot check when cloud URL + token are both set', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'match' }); renderGate(); fireEvent.click(screen.getByText('Run on the Cloud (Complex)')); fireEvent.change(screen.getByPlaceholderText(/https:\/\/core\.example\.com/), { target: { value: 'https://core.example.com/rpc' }, }); fireEvent.change(screen.getByPlaceholderText(/Bearer token/i), { target: { value: 'tok-1234' }, }); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByTestId('app-content')).toBeInTheDocument(); }); expect(mockRunBootCheck).toHaveBeenCalledWith( expect.objectContaining({ kind: 'cloud', url: 'https://core.example.com/rpc', token: 'tok-1234', }), expect.any(Object) ); }); }); describe('BootCheckGate — picker test connection', () => { beforeEach(() => { mockTestCoreRpcConnection.mockReset(); }); function fillCloudInputs(url = 'https://core.example.com/rpc', token = 'tok-abc') { fireEvent.click(screen.getByText('Run on the Cloud (Complex)')); fireEvent.change(screen.getByPlaceholderText(/https:\/\/core\.example\.com/), { target: { value: url }, }); fireEvent.change(screen.getByPlaceholderText(/Bearer token/i), { target: { value: token } }); } it('shows Connected on a 200 response', async () => { mockTestCoreRpcConnection.mockResolvedValue({ ok: true, status: 200, json: async () => ({ result: { ok: true } }), } as unknown as Response); renderGate(); fillCloudInputs(); fireEvent.click(screen.getByRole('button', { name: 'Test Connection' })); await waitFor(() => { expect(screen.getByTestId('test-status-ok')).toBeInTheDocument(); }); expect(mockTestCoreRpcConnection).toHaveBeenCalledWith( 'https://core.example.com/rpc', 'tok-abc' ); }); it('tests /rpc when the user enters a cloud core base URL', async () => { mockTestCoreRpcConnection.mockResolvedValue({ ok: true, status: 200, json: async () => ({ result: { ok: true } }), } as unknown as Response); renderGate(); fillCloudInputs('https://example.trycloudflare.com/'); fireEvent.click(screen.getByRole('button', { name: 'Test Connection' })); await waitFor(() => { expect(screen.getByTestId('test-status-ok')).toBeInTheDocument(); }); expect(mockTestCoreRpcConnection).toHaveBeenCalledWith( 'https://example.trycloudflare.com/rpc', 'tok-abc' ); }); it('shows Auth failed on a 401 response', async () => { mockTestCoreRpcConnection.mockResolvedValue({ ok: false, status: 401, json: async () => ({ error: 'unauthorized' }), } as unknown as Response); renderGate(); fillCloudInputs(); fireEvent.click(screen.getByRole('button', { name: 'Test Connection' })); await waitFor(() => { expect(screen.getByTestId('test-status-auth')).toBeInTheDocument(); }); }); it('shows Auth failed on a 403 response', async () => { mockTestCoreRpcConnection.mockResolvedValue({ ok: false, status: 403, json: async () => ({}), } as unknown as Response); renderGate(); fillCloudInputs(); fireEvent.click(screen.getByRole('button', { name: 'Test Connection' })); await waitFor(() => { expect(screen.getByTestId('test-status-auth')).toBeInTheDocument(); }); }); it('shows Unreachable when fetch rejects', async () => { mockTestCoreRpcConnection.mockRejectedValue(new Error('network down')); renderGate(); fillCloudInputs(); fireEvent.click(screen.getByRole('button', { name: 'Test Connection' })); await waitFor(() => { expect(screen.getByTestId('test-status-unreachable')).toBeInTheDocument(); }); expect(screen.getByTestId('test-status-unreachable').textContent).toMatch(/network down/); }); it('shows Unreachable on non-2xx non-auth response', async () => { mockTestCoreRpcConnection.mockResolvedValue({ ok: false, status: 500, json: async () => ({}), } as unknown as Response); renderGate(); fillCloudInputs(); fireEvent.click(screen.getByRole('button', { name: 'Test Connection' })); await waitFor(() => { expect(screen.getByTestId('test-status-unreachable')).toBeInTheDocument(); }); expect(screen.getByTestId('test-status-unreachable').textContent).toMatch(/HTTP 500/); }); it('does not call the test endpoint when URL is missing', () => { renderGate(); fireEvent.click(screen.getByText('Run on the Cloud (Complex)')); fireEvent.click(screen.getByRole('button', { name: 'Test Connection' })); expect(mockTestCoreRpcConnection).not.toHaveBeenCalled(); expect(screen.getByText('Please enter a runtime URL.')).toBeInTheDocument(); }); it('does not call the test endpoint when token is missing', () => { renderGate(); fireEvent.click(screen.getByText('Run on the Cloud (Complex)')); fireEvent.change(screen.getByPlaceholderText(/https:\/\/core\.example\.com/), { target: { value: 'https://core.example.com/rpc' }, }); fireEvent.click(screen.getByRole('button', { name: 'Test Connection' })); expect(mockTestCoreRpcConnection).not.toHaveBeenCalled(); expect(screen.getByText(/We'll need an auth token to connect/i)).toBeInTheDocument(); }); it('clears a stale ok status when the user edits inputs again', async () => { mockTestCoreRpcConnection.mockResolvedValue({ ok: true, status: 200, json: async () => ({}), } as unknown as Response); renderGate(); fillCloudInputs(); fireEvent.click(screen.getByRole('button', { name: 'Test Connection' })); await waitFor(() => { expect(screen.getByTestId('test-status-ok')).toBeInTheDocument(); }); fireEvent.change(screen.getByPlaceholderText(/Bearer token/i), { target: { value: 'tok-def' }, }); expect(screen.queryByTestId('test-status-ok')).not.toBeInTheDocument(); }); }); describe('BootCheckGate — checking state', () => { it('shows checking spinner while boot check is in flight', async () => { // Never resolves during this test mockRunBootCheck.mockImplementation(() => new Promise(() => {})); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByText('Waking up your runtime…')).toBeInTheDocument(); }); }); }); describe('BootCheckGate — match result', () => { it('renders children once boot check returns match', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'match' }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByTestId('app-content')).toBeInTheDocument(); }); }); }); describe('BootCheckGate — daemonDetected', () => { it('shows daemon detection screen', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'daemonDetected' }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByText('Legacy Background Runtime Detected')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Remove and Continue' })).toBeInTheDocument(); }); }); }); describe('BootCheckGate — outdatedLocal', () => { it('shows outdated local screen', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'outdatedLocal' }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByText('Local Runtime Needs a Restart')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Restart Runtime' })).toBeInTheDocument(); }); }); }); describe('BootCheckGate — outdatedCloud', () => { it('shows outdated cloud screen', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'outdatedCloud' }); const store = makeStore({ kind: 'cloud', url: 'https://core.example.com/rpc' }); // Trigger the check by rendering with an already-set mode mockRunBootCheck.mockResolvedValue({ kind: 'outdatedCloud' }); render( <Provider store={store}> <BootCheckGate> <div data-testid="app-content">App Content</div> </BootCheckGate> </Provider> ); await waitFor(() => { expect(screen.getByText('Cloud Runtime Needs an Update')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Update Cloud Runtime' })).toBeInTheDocument(); }); }); }); describe('BootCheckGate — noVersionMethod', () => { it('shows no version method screen', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'noVersionMethod' }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByText('Runtime Version Check Failed')).toBeInTheDocument(); }); }); }); describe('BootCheckGate — unreachable', () => { it('shows unreachable screen with quit and switch mode buttons', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'unreachable', reason: 'Connection refused' }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByText("Can't Reach the Runtime")).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Quit' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Pick a Different Runtime' })).toBeInTheDocument(); }); }); it("returns to picker when 'Pick a Different Runtime' is clicked", async () => { mockRunBootCheck.mockResolvedValue({ kind: 'unreachable', reason: 'Connection refused' }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByRole('button', { name: 'Pick a Different Runtime' })).toBeInTheDocument(); }); fireEvent.click(screen.getByRole('button', { name: 'Pick a Different Runtime' })); await waitFor(() => { expect(screen.getByText('Select a Runtime')).toBeInTheDocument(); }); }); }); describe('BootCheckGate — pre-set mode (subsequent launches)', () => { beforeEach(() => { vi.clearAllMocks(); }); it('skips picker and goes directly to checking when mode is already set', async () => { mockRunBootCheck.mockImplementation(() => new Promise(() => {})); const store = makeStore({ kind: 'local' }); render( <Provider store={store}> <BootCheckGate> <div data-testid="app-content">App Content</div> </BootCheckGate> </Provider> ); await waitFor(() => { expect(screen.getByText('Waking up your runtime…')).toBeInTheDocument(); }); expect(screen.queryByText('Select a Runtime')).not.toBeInTheDocument(); }); }); describe('BootCheckGate — port conflict recovery', () => { beforeEach(() => { mockRecoverPortConflict.mockReset(); mockRunBootCheck.mockReset(); }); it('shows "Fix Automatically" button when portConflict=true', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'unreachable', reason: 'port conflict', portConflict: true, }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByTestId('fix-automatically-btn')).toBeInTheDocument(); }); expect(screen.getByTestId('fix-automatically-btn').textContent).toBe('Fix Automatically'); }); it('does not show "Fix Automatically" button when portConflict is not set', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'unreachable', reason: 'some other error' }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByText("Can't Reach the Runtime")).toBeInTheDocument(); }); expect(screen.queryByTestId('fix-automatically-btn')).not.toBeInTheDocument(); }); it('calls recoverPortConflict when "Fix Automatically" is clicked', async () => { mockRunBootCheck .mockResolvedValueOnce({ kind: 'unreachable', reason: 'port conflict', portConflict: true }) .mockResolvedValue({ kind: 'match' }); mockRecoverPortConflict.mockResolvedValue({ success: true, message: 'ok', new_port: 7789 }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByTestId('fix-automatically-btn')).toBeInTheDocument(); }); fireEvent.click(screen.getByTestId('fix-automatically-btn')); await waitFor(() => { expect(mockRecoverPortConflict).toHaveBeenCalled(); }); }); it('re-runs boot check after successful recovery', async () => { mockRunBootCheck .mockResolvedValueOnce({ kind: 'unreachable', reason: 'port conflict', portConflict: true }) .mockResolvedValue({ kind: 'match' }); mockRecoverPortConflict.mockResolvedValue({ success: true, message: 'ok', new_port: 7789 }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByTestId('fix-automatically-btn')).toBeInTheDocument(); }); fireEvent.click(screen.getByTestId('fix-automatically-btn')); await waitFor(() => { expect(screen.getByTestId('app-content')).toBeInTheDocument(); }); expect(mockRunBootCheck).toHaveBeenCalledTimes(2); }); it('shows portConflictFixFailed message when recovery fails', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'unreachable', reason: 'port conflict', portConflict: true, }); mockRecoverPortConflict.mockResolvedValue({ success: false, message: 'still busy', new_port: undefined, }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByTestId('fix-automatically-btn')).toBeInTheDocument(); }); fireEvent.click(screen.getByTestId('fix-automatically-btn')); await waitFor(() => { expect( screen.getByText("Automatic fix didn't work. Please restart your computer and try again.") ).toBeInTheDocument(); }); }); it('"Pick a Different Runtime" still renders as secondary for port conflict', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'unreachable', reason: 'port conflict', portConflict: true, }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByRole('button', { name: 'Pick a Different Runtime' })).toBeInTheDocument(); }); }); }); describe('BootCheckGate — foreign port owner', () => { beforeEach(() => { mockRecoverPortConflict.mockReset(); mockForceQuitPortOwner.mockReset(); mockRunBootCheck.mockReset(); }); const foreignResult = { kind: 'unreachable' as const, reason: 'port conflict', portConflict: true, foreignOwner: { pid: 4242, name: 'Skype.exe' }, }; it('names the foreign owner and offers force-quit instead of Fix Automatically', async () => { mockRunBootCheck.mockResolvedValue(foreignResult); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByTestId('force-quit-owner-btn')).toBeInTheDocument(); }); expect(screen.getByText(/Skype\.exe \(PID 4242\)/)).toBeInTheDocument(); expect(screen.getByTestId('force-quit-owner-btn').textContent).toContain('Skype.exe'); expect(screen.queryByTestId('fix-automatically-btn')).not.toBeInTheDocument(); }); it('force-quits the owner and re-runs the boot check on success', async () => { mockRunBootCheck.mockResolvedValueOnce(foreignResult).mockResolvedValue({ kind: 'match' }); mockForceQuitPortOwner.mockResolvedValue({ success: true, message: 'ok', new_port: 7789 }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByTestId('force-quit-owner-btn')).toBeInTheDocument(); }); fireEvent.click(screen.getByTestId('force-quit-owner-btn')); await waitFor(() => { expect(screen.getByTestId('app-content')).toBeInTheDocument(); }); expect(mockForceQuitPortOwner).toHaveBeenCalledWith(4242); expect(mockRunBootCheck).toHaveBeenCalledTimes(2); }); it('shows the force-quit failed message when termination fails', async () => { mockRunBootCheck.mockResolvedValue(foreignResult); mockForceQuitPortOwner.mockResolvedValue({ success: false, message: 'access denied' }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByTestId('force-quit-owner-btn')).toBeInTheDocument(); }); fireEvent.click(screen.getByTestId('force-quit-owner-btn')); await waitFor(() => { expect(screen.getByText(/You may need to close it manually/)).toBeInTheDocument(); }); }); it('upgrades to a force-quit button when Fix Automatically surfaces an owner', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'unreachable', reason: 'port conflict', portConflict: true, }); mockRecoverPortConflict.mockResolvedValue({ success: false, message: 'still busy', foreign_owner: { pid: 99, name: 'node.exe' }, }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByTestId('fix-automatically-btn')).toBeInTheDocument(); }); fireEvent.click(screen.getByTestId('fix-automatically-btn')); await waitFor(() => { expect(screen.getByTestId('force-quit-owner-btn')).toBeInTheDocument(); }); expect(screen.getByText(/node\.exe \(PID 99\)/)).toBeInTheDocument(); }); it('renders cleanly when the owner name is unknown (empty)', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'unreachable', reason: 'port conflict', portConflict: true, foreignOwner: { pid: 4242, name: '' }, }); renderGate(); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByTestId('force-quit-owner-btn')).toBeInTheDocument(); }); // No leftover "{name}" / no duplicated PID; the pid still shows. expect(screen.getByText(/\(PID 4242\) is using/)).toBeInTheDocument(); expect(screen.getByTestId('force-quit-owner-btn').textContent?.trim()).toBe('Force-quit'); }); }); describe('BootCheckGate — picker (web build, !isTauri)', () => { beforeEach(() => { mockedIsTauri.mockReturnValue(false); }); it('uses the web-friendly title and hides the Local option', () => { renderGate(); expect(screen.getByText('Connect to Your Runtime')).toBeInTheDocument(); expect(screen.queryByText('Select a Runtime')).not.toBeInTheDocument(); expect(screen.queryByText('Run Locally (Recommended)')).not.toBeInTheDocument(); // The selectable Cloud tile is also gone — cloud is implicit and the // URL/token form is rendered directly. expect( screen.queryByRole('button', { name: 'Run on the Cloud (Complex)' }) ).not.toBeInTheDocument(); }); it('renders the cloud form fields immediately (cloud is the only option)', () => { renderGate(); expect(screen.getByPlaceholderText(/https:\/\/core\.example\.com/)).toBeInTheDocument(); expect(screen.getByPlaceholderText(/Bearer token/i)).toBeInTheDocument(); }); it('shows a Download desktop app CTA linking to the release page', () => { renderGate(); const cta = screen.getByTestId('web-download-cta'); expect(cta).toBeInTheDocument(); const link = cta.querySelector('a'); expect(link).not.toBeNull(); expect(link?.getAttribute('href')).toMatch( /github\.com\/tinyhumansai\/openhuman\/releases\/latest/ ); expect(link?.getAttribute('target')).toBe('_blank'); expect(link?.getAttribute('rel')).toMatch(/noopener/); }); it('continues into a cloud boot check when URL + token are provided', async () => { mockRunBootCheck.mockResolvedValue({ kind: 'match' }); renderGate(); fireEvent.change(screen.getByPlaceholderText(/https:\/\/core\.example\.com/), { target: { value: 'https://core.example.com/rpc' }, }); fireEvent.change(screen.getByPlaceholderText(/Bearer token/i), { target: { value: 'tok-web' }, }); fireEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => { expect(screen.getByTestId('app-content')).toBeInTheDocument(); }); expect(mockRunBootCheck).toHaveBeenCalledWith( expect.objectContaining({ kind: 'cloud', url: 'https://core.example.com/rpc', token: 'tok-web', }), expect.any(Object) ); }); });