narada 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: narada
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Python client SDK for Narada
5
5
  Project-URL: Homepage, https://github.com/NaradaAI/narada-python-sdk/narada
6
6
  Project-URL: Repository, https://github.com/NaradaAI/narada-python-sdk
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "narada"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Python client SDK for Narada"
5
5
  license = "Apache-2.0"
6
6
  readme = "README.md"
@@ -970,8 +970,7 @@ class BrowserEnvironment(BaseBrowserEnvironment):
970
970
  )
971
971
 
972
972
  async def _initialize(self) -> None:
973
- self._playwright_context_manager = async_playwright()
974
- self._playwright = await self._playwright_context_manager.__aenter__()
973
+ await self._start_playwright()
975
974
  if self._attach_to_existing:
976
975
  await self._initialize_in_existing_browser_window()
977
976
  else:
@@ -997,6 +996,10 @@ class BrowserEnvironment(BaseBrowserEnvironment):
997
996
  finally:
998
997
  await self._stop_playwright()
999
998
 
999
+ async def _start_playwright(self) -> None:
1000
+ self._playwright_context_manager = async_playwright()
1001
+ self._playwright = await self._playwright_context_manager.__aenter__()
1002
+
1000
1003
  async def _stop_playwright(self) -> None:
1001
1004
  if self._playwright_context_manager is None:
1002
1005
  return
@@ -1480,8 +1483,7 @@ class CloudBrowserEnvironment(BaseBrowserEnvironment):
1480
1483
  ``#narada-browser-window-id`` (extension install retries apply). ``config`` controls
1481
1484
  interactive prompts and related behavior.
1482
1485
  """
1483
- self._playwright_context_manager = async_playwright()
1484
- self._playwright = await self._playwright_context_manager.__aenter__()
1486
+ await self._start_playwright()
1485
1487
 
1486
1488
  request_body = {
1487
1489
  "require_extension": True,
@@ -1557,6 +1559,10 @@ class CloudBrowserEnvironment(BaseBrowserEnvironment):
1557
1559
  # Re-raise the original connection error
1558
1560
  raise
1559
1561
 
1562
+ async def _start_playwright(self) -> None:
1563
+ self._playwright_context_manager = async_playwright()
1564
+ self._playwright = await self._playwright_context_manager.__aenter__()
1565
+
1560
1566
  async def _stop_playwright(self) -> None:
1561
1567
  if self._playwright_context_manager is None:
1562
1568
  return
@@ -1597,6 +1603,7 @@ class CloudBrowserEnvironment(BaseBrowserEnvironment):
1597
1603
  session_id: str,
1598
1604
  login_url: str,
1599
1605
  cdp_auth_headers: dict[str, str],
1606
+ expected_browser_window_id: str | None = None,
1600
1607
  ) -> None:
1601
1608
  assert self._playwright is not None
1602
1609
 
@@ -1608,6 +1615,23 @@ class CloudBrowserEnvironment(BaseBrowserEnvironment):
1608
1615
  # Navigate to login URL (provided by backend with custom token)
1609
1616
  context = browser.contexts[0]
1610
1617
  initialization_page = context.pages[0]
1618
+ if expected_browser_window_id is not None:
1619
+ # Put the backend-owned browser ID into sessionStorage before hydration
1620
+ # so AgentCore sessions use the right Firestore route when needed.
1621
+ expected_browser_window_id_json = json.dumps(expected_browser_window_id)
1622
+ await context.add_init_script(
1623
+ script=f"""
1624
+ (() => {{
1625
+ const expectedBrowserWindowId = {expected_browser_window_id_json};
1626
+ try {{
1627
+ sessionStorage.setItem(
1628
+ "naradaBrowserWindowId",
1629
+ expectedBrowserWindowId
1630
+ );
1631
+ }} catch (_error) {{}}
1632
+ }})();
1633
+ """
1634
+ )
1611
1635
  await initialization_page.goto(
1612
1636
  login_url, timeout=15_000, wait_until="domcontentloaded"
1613
1637
  )
@@ -1628,7 +1652,7 @@ class CloudBrowserEnvironment(BaseBrowserEnvironment):
1628
1652
  raise
1629
1653
  logging.info("Waiting for Narada extension to be installed...")
1630
1654
  await asyncio.sleep(1)
1631
- except NaradaTimeoutError:
1655
+ except (NaradaTimeoutError, NaradaExtensionUnauthenticatedError):
1632
1656
  if attempt == max_attempts - 1:
1633
1657
  raise
1634
1658
  # If browser window ID is not found, reload the page and try again
@@ -1637,6 +1661,15 @@ class CloudBrowserEnvironment(BaseBrowserEnvironment):
1637
1661
  login_url, timeout=15_000, wait_until="domcontentloaded"
1638
1662
  )
1639
1663
 
1664
+ if (
1665
+ expected_browser_window_id is not None
1666
+ and browser_window_id != expected_browser_window_id
1667
+ ):
1668
+ raise RuntimeError(
1669
+ "Initialized cloud session reported browserWindowId "
1670
+ f"{browser_window_id!r}, expected {expected_browser_window_id!r}."
1671
+ )
1672
+
1640
1673
  self._browser_window_id = browser_window_id
1641
1674
  self._session_id = session_id
1642
1675
  self._context = context
@@ -80,7 +80,9 @@ def _build_cloud_environment_with_page(page: AsyncMock) -> CloudBrowserEnvironme
80
80
  auth_headers={"x-api-key": "test-key"},
81
81
  config=BrowserConfig(interactive=False),
82
82
  )
83
- browser = SimpleNamespace(contexts=[SimpleNamespace(pages=[page])])
83
+ browser = SimpleNamespace(
84
+ contexts=[SimpleNamespace(pages=[page], add_init_script=AsyncMock())]
85
+ )
84
86
  env._playwright = SimpleNamespace(
85
87
  chromium=SimpleNamespace(connect_over_cdp=AsyncMock(return_value=browser))
86
88
  )
@@ -236,6 +238,35 @@ async def test_extension_action_request_includes_action_execution_id(
236
238
  assert action_execution_id.startswith("action_")
237
239
 
238
240
 
241
+ @pytest.mark.asyncio
242
+ async def test_remote_browser_environment_with_cloud_session_stops_session_by_default(
243
+ monkeypatch: pytest.MonkeyPatch,
244
+ ) -> None:
245
+ import narada.environment as environment_module
246
+
247
+ stop_cloud_browser_session = AsyncMock()
248
+ monkeypatch.setattr(
249
+ environment_module,
250
+ "_stop_cloud_browser_session",
251
+ stop_cloud_browser_session,
252
+ )
253
+
254
+ env = RemoteBrowserEnvironment(
255
+ browser_window_id="browser-window-123",
256
+ cloud_browser_session_id="session-123",
257
+ auth_headers={"x-api-key": "test-key"},
258
+ )
259
+
260
+ await env.close()
261
+
262
+ stop_cloud_browser_session.assert_awaited_once_with(
263
+ base_url=env._base_url,
264
+ auth_headers={"x-api-key": "test-key"},
265
+ session_id="session-123",
266
+ timeout=None,
267
+ )
268
+
269
+
239
270
  @pytest.mark.asyncio
240
271
  async def test_lambda_environment_uses_backend_initialization(
241
272
  monkeypatch: pytest.MonkeyPatch,
@@ -312,6 +343,7 @@ async def test_cloud_browser_environment_uses_domcontentloaded_for_login_navigat
312
343
  ) -> None:
313
344
  page = AsyncMock()
314
345
  env = _build_cloud_environment_with_page(page)
346
+ context = env._playwright.chromium.connect_over_cdp.return_value.contexts[0]
315
347
 
316
348
  wait_for_browser_window_id = AsyncMock(return_value="browser-window-123")
317
349
  monkeypatch.setattr(
@@ -335,10 +367,69 @@ async def test_cloud_browser_environment_uses_domcontentloaded_for_login_navigat
335
367
  BrowserConfig(interactive=False),
336
368
  timeout=30_000,
337
369
  )
370
+ context.add_init_script.assert_not_awaited()
338
371
  assert env.browser_window_id == "browser-window-123"
339
372
  assert env.cloud_browser_session_id == "session-123"
340
373
 
341
374
 
375
+ @pytest.mark.asyncio
376
+ async def test_cloud_browser_environment_seeds_expected_browser_window_id_before_navigation(
377
+ monkeypatch: pytest.MonkeyPatch,
378
+ ) -> None:
379
+ page = AsyncMock()
380
+ env = _build_cloud_environment_with_page(page)
381
+ context = env._playwright.chromium.connect_over_cdp.return_value.contexts[0]
382
+ events: list[str] = []
383
+
384
+ async def add_init_script(*args, **kwargs) -> None:
385
+ events.append("seed")
386
+
387
+ async def goto(*args, **kwargs) -> None:
388
+ events.append("goto")
389
+
390
+ context.add_init_script.side_effect = add_init_script
391
+ page.goto.side_effect = goto
392
+ wait_for_browser_window_id = AsyncMock(return_value="backend-window-123")
393
+ monkeypatch.setattr(
394
+ env, "_wait_for_cloud_browser_window_id", wait_for_browser_window_id
395
+ )
396
+
397
+ await env._initialize_cloud_browser_window(
398
+ cdp_websocket_url="wss://agentcore.example.test/session-123",
399
+ session_id="session-123",
400
+ login_url="https://app.narada.ai/chat?customToken=test-token",
401
+ cdp_auth_headers={"Authorization": "signed-cdp"},
402
+ expected_browser_window_id="backend-window-123",
403
+ )
404
+
405
+ assert events[:2] == ["seed", "goto"]
406
+ script = context.add_init_script.await_args.kwargs["script"]
407
+ assert "naradaBrowserWindowId" in script
408
+ assert "backend-window-123" in script
409
+ assert env.browser_window_id == "backend-window-123"
410
+
411
+
412
+ @pytest.mark.asyncio
413
+ async def test_cloud_browser_environment_rejects_unexpected_seeded_browser_window_id(
414
+ monkeypatch: pytest.MonkeyPatch,
415
+ ) -> None:
416
+ page = AsyncMock()
417
+ env = _build_cloud_environment_with_page(page)
418
+ wait_for_browser_window_id = AsyncMock(return_value="frontend-window-123")
419
+ monkeypatch.setattr(
420
+ env, "_wait_for_cloud_browser_window_id", wait_for_browser_window_id
421
+ )
422
+
423
+ with pytest.raises(RuntimeError, match="expected 'backend-window-123'"):
424
+ await env._initialize_cloud_browser_window(
425
+ cdp_websocket_url="wss://agentcore.example.test/session-123",
426
+ session_id="session-123",
427
+ login_url="https://app.narada.ai/chat?customToken=test-token",
428
+ cdp_auth_headers={"Authorization": "signed-cdp"},
429
+ expected_browser_window_id="backend-window-123",
430
+ )
431
+
432
+
342
433
  @pytest.mark.asyncio
343
434
  async def test_cloud_browser_environment_uses_domcontentloaded_for_retry_navigation(
344
435
  monkeypatch: pytest.MonkeyPatch,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes