From bca6b7f6dd5f5b5a415484cdebc599f6098a761b Mon Sep 17 00:00:00 2001 From: Volodymyr Kasaraba Date: Wed, 13 May 2026 18:51:37 -0400 Subject: [PATCH] speed up extensionless cloud browser (v2) --- packages/narada/src/narada/client.py | 17 +++++-- packages/narada/tests/test_cloud_browser.py | 53 +++++++++++++++++++++ 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/packages/narada/src/narada/client.py b/packages/narada/src/narada/client.py index e9e53db..fb9a519 100644 --- a/packages/narada/src/narada/client.py +++ b/packages/narada/src/narada/client.py @@ -177,11 +177,18 @@ async def open_and_initialize_cloud_browser_window( session_timeout: int | None = None, require_extension: bool = True, ) -> CloudBrowserWindow: - """Creates a cloud browser by calling the backend. - - The backend creates a cloud browser session and returns - a CDP WebSocket URL. This method connects to it, initializes the extension, - and returns a CloudBrowserWindow instance. + """Create a cloud browser session and return a ``CloudBrowserWindow``. + + With ``require_extension=True`` (default), calls + ``POST /cloud-browser/create-cloud-browser-session``, then connects local Playwright + over CDP, opens ``login_url``, and waits for ``#narada-browser-window-id`` (extension + install retries apply). ``config`` controls interactive prompts and related behavior. + + With ``require_extension=False``, calls + ``POST /cloud-browser/create-and-initialize-cloud-browser-session`` instead: the API + provisions the browser and runs the same CDP initialization on the server, returning + ``session_id`` and ``browser_window_id`` in the JSON body. Local Playwright is not used + for that path, and ``config`` is ignored. """ config = config or BrowserConfig() base_url = os.getenv("NARADA_API_BASE_URL", "https://api.narada.ai/fast/v2") diff --git a/packages/narada/tests/test_cloud_browser.py b/packages/narada/tests/test_cloud_browser.py index 1949721..5810b98 100644 --- a/packages/narada/tests/test_cloud_browser.py +++ b/packages/narada/tests/test_cloud_browser.py @@ -85,3 +85,56 @@ async def fail_if_client_initializes(*args: Any, **kwargs: Any) -> None: "session_name": "fast-session", "session_timeout": 300, } + + +class _ForbiddenResponse: + ok = False + status = 403 + + async def __aenter__(self) -> "_ForbiddenResponse": + return self + + async def __aexit__(self, *args: Any) -> None: + pass + + async def json(self) -> dict[str, Any]: + return {} + + async def text(self) -> str: + return '{"detail": {"reason": "forbidden"}}' + + +class _ForbiddenClientSession: + def __init__(self) -> None: + self.posts: list[str] = [] + + async def __aenter__(self) -> "_ForbiddenClientSession": + return self + + async def __aexit__(self, *args: Any) -> None: + pass + + def post(self, url: str, **kwargs: Any) -> _ForbiddenResponse: + self.posts.append(url) + return _ForbiddenResponse() + + +@pytest.mark.asyncio +async def test_extensionless_cloud_browser_forbidden_sets_error_attributes( + monkeypatch: pytest.MonkeyPatch, +) -> None: + fake_session = _ForbiddenClientSession() + monkeypatch.setattr(client_module.aiohttp, "ClientSession", lambda: fake_session) + + narada = Narada(auth_headers={"x-api-key": "test-key"}) + + with pytest.raises(RuntimeError) as excinfo: + await narada.open_and_initialize_cloud_browser_window(require_extension=False) + + err = excinfo.value + assert getattr(err, "status_code", None) == 403 + assert getattr(err, "detail", None) == {"reason": "forbidden"} + assert len(fake_session.posts) == 1 + assert fake_session.posts[0].endswith( + "/cloud-browser/create-and-initialize-cloud-browser-session" + )