diff --git a/tests/gateway/test_api_server_web_console.py b/tests/gateway/test_api_server_web_console.py new file mode 100644 index 000000000..2e04727e3 --- /dev/null +++ b/tests/gateway/test_api_server_web_console.py @@ -0,0 +1,68 @@ +import pytest +from aiohttp import web +from aiohttp.test_utils import TestClient, TestServer + +from gateway.config import PlatformConfig +from gateway.platforms.api_server import APIServerAdapter, cors_middleware, security_headers_middleware + + +def _make_adapter(api_key: str = '') -> APIServerAdapter: + extra = {'key': api_key} if api_key else {} + return APIServerAdapter(PlatformConfig(enabled=True, extra=extra)) + + +def _create_app(adapter: APIServerAdapter) -> web.Application: + mws = [mw for mw in (cors_middleware, security_headers_middleware) if mw is not None] + app = web.Application(middlewares=mws) + app['api_server_adapter'] = adapter + adapter._register_routes(app) + return app + + +class TestWebConsoleRoutes: + @pytest.mark.asyncio + async def test_root_serves_web_console_html(self): + adapter = _make_adapter() + app = _create_app(adapter) + async with TestClient(TestServer(app)) as cli: + resp = await cli.get('/') + assert resp.status == 200 + text = await resp.text() + assert 'Hermes Web Console' in text + assert '/api/gui/browser/status' in text + assert '/api/gui/browser/heal' in text + + @pytest.mark.asyncio + async def test_browser_status_returns_json(self): + adapter = _make_adapter() + app = _create_app(adapter) + async with TestClient(TestServer(app)) as cli: + from unittest.mock import patch + with patch('gateway.platforms.api_server_ui.browser_runtime_status', return_value={'mode': 'local', 'session_count': 0, 'available': True}): + resp = await cli.get('/api/gui/browser/status') + assert resp.status == 200 + data = await resp.json() + assert data['mode'] == 'local' + assert data['session_count'] == 0 + + @pytest.mark.asyncio + async def test_browser_status_requires_auth_when_key_set(self): + adapter = _make_adapter(api_key='sk-secret') + app = _create_app(adapter) + async with TestClient(TestServer(app)) as cli: + resp = await cli.get('/api/gui/browser/status') + assert resp.status == 401 + + @pytest.mark.asyncio + async def test_browser_heal_invokes_runtime_heal(self): + adapter = _make_adapter() + app = _create_app(adapter) + async with TestClient(TestServer(app)) as cli: + from unittest.mock import patch + with patch('gateway.platforms.api_server_ui.browser_runtime_heal', return_value={'success': True, 'before': {'session_count': 1}, 'after': {'session_count': 0}}) as mock_heal: + resp = await cli.post('/api/gui/browser/heal') + assert resp.status == 200 + data = await resp.json() + assert data['success'] is True + assert data['after']['session_count'] == 0 + mock_heal.assert_called_once_with() diff --git a/tests/tools/test_browser_runtime_cockpit.py b/tests/tools/test_browser_runtime_cockpit.py new file mode 100644 index 000000000..cf1aca6f8 --- /dev/null +++ b/tests/tools/test_browser_runtime_cockpit.py @@ -0,0 +1,61 @@ +from unittest.mock import Mock, patch + + +class TestBrowserRuntimeCockpit: + def setup_method(self): + import tools.browser_tool as bt + self.bt = bt + self.orig_active = bt._active_sessions.copy() + self.orig_last = bt._session_last_activity.copy() + + def teardown_method(self): + self.bt._active_sessions.clear() + self.bt._active_sessions.update(self.orig_active) + self.bt._session_last_activity.clear() + self.bt._session_last_activity.update(self.orig_last) + + def test_runtime_status_reports_mode_and_sessions(self): + import tools.browser_tool as bt + + bt._active_sessions['task-a'] = { + 'session_name': 'sess-a', + 'bb_session_id': 'bb_123', + 'cdp_url': 'ws://browser/devtools/browser/abc', + } + bt._session_last_activity['task-a'] = 111.0 + + provider = Mock() + provider.provider_name.return_value = 'browserbase' + + with patch('tools.browser_tool._get_cdp_override', return_value='ws://browser/devtools/browser/override'), \ + patch('tools.browser_tool._get_cloud_provider', return_value=provider), \ + patch('tools.browser_tool.check_browser_requirements', return_value=True), \ + patch('tools.browser_tool._find_agent_browser', return_value='/usr/local/bin/agent-browser'): + status = bt.browser_runtime_status() + + assert status['mode'] == 'cdp' + assert status['available'] is True + assert status['cloud_provider'] == 'browserbase' + assert status['session_count'] == 1 + assert status['active_sessions'][0]['task_id'] == 'task-a' + assert status['self_healing']['orphan_reaper'] is True + + def test_runtime_heal_cleans_sessions(self): + import tools.browser_tool as bt + + bt._active_sessions['task-a'] = {'session_name': 'sess-a'} + bt._active_sessions['task-b'] = {'session_name': 'sess-b'} + + with patch('tools.browser_tool.cleanup_all_browsers') as mock_cleanup, \ + patch('tools.browser_tool._reap_orphaned_browser_sessions') as mock_reap, \ + patch('tools.browser_tool.browser_runtime_status', side_effect=[ + {'session_count': 2, 'mode': 'local', 'available': True}, + {'session_count': 0, 'mode': 'local', 'available': True}, + ]): + result = bt.browser_runtime_heal() + + mock_cleanup.assert_called_once_with() + mock_reap.assert_called_once_with() + assert result['success'] is True + assert result['before']['session_count'] == 2 + assert result['after']['session_count'] == 0