From 8bf0860f349ce1eb481bf932f49adcd3091c5999 Mon Sep 17 00:00:00 2001 From: Siddarth Chalasani Date: Wed, 25 Mar 2026 18:20:41 -0700 Subject: [PATCH 1/2] don't hardcode `.ai` into `get_tunnel_url()` --- src/runloop_api_client/sdk/async_devbox.py | 4 +++- src/runloop_api_client/sdk/devbox.py | 4 +++- tests/sdk/async_devbox/test_core.py | 25 ++++++++++++++++++++++ tests/sdk/devbox/test_core.py | 24 +++++++++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/runloop_api_client/sdk/async_devbox.py b/src/runloop_api_client/sdk/async_devbox.py index cdedcb5db..98f150e9a 100644 --- a/src/runloop_api_client/sdk/async_devbox.py +++ b/src/runloop_api_client/sdk/async_devbox.py @@ -161,7 +161,9 @@ async def get_tunnel_url( tunnel_view = await self.get_tunnel(**options) if tunnel_view is None: return None - return f"https://{port}-{tunnel_view.tunnel_key}.tunnel.runloop.ai" + api_host = self._client.base_url.host + base_domain = api_host[4:] if api_host.startswith("api.") else api_host + return f"https://{port}-{tunnel_view.tunnel_key}.tunnel.{base_domain}" async def logs( self, diff --git a/src/runloop_api_client/sdk/devbox.py b/src/runloop_api_client/sdk/devbox.py index aec345715..3580d8167 100644 --- a/src/runloop_api_client/sdk/devbox.py +++ b/src/runloop_api_client/sdk/devbox.py @@ -160,7 +160,9 @@ def get_tunnel_url( tunnel_view = self.get_tunnel(**options) if tunnel_view is None: return None - return f"https://{port}-{tunnel_view.tunnel_key}.tunnel.runloop.ai" + api_host = self._client.base_url.host + base_domain = api_host[4:] if api_host.startswith("api.") else api_host + return f"https://{port}-{tunnel_view.tunnel_key}.tunnel.{base_domain}" def logs( self, diff --git a/tests/sdk/async_devbox/test_core.py b/tests/sdk/async_devbox/test_core.py index e22b41b7f..e87aaa387 100644 --- a/tests/sdk/async_devbox/test_core.py +++ b/tests/sdk/async_devbox/test_core.py @@ -9,6 +9,8 @@ from types import SimpleNamespace from unittest.mock import AsyncMock +import httpx + import pytest from tests.sdk.conftest import MockDevboxView @@ -377,6 +379,7 @@ async def test_get_tunnel_url_constructs_url(self, mock_async_client: AsyncMock) tunnel=tunnel_view, ) mock_async_client.devboxes.retrieve = AsyncMock(return_value=devbox_view_with_tunnel) + mock_async_client.base_url = httpx.URL("https://api.runloop.ai") devbox = AsyncDevbox(mock_async_client, "dbx_123") result = await devbox.get_tunnel_url(8080) @@ -384,6 +387,28 @@ async def test_get_tunnel_url_constructs_url(self, mock_async_client: AsyncMock) assert result == "https://8080-abc123xyz.tunnel.runloop.ai" mock_async_client.devboxes.retrieve.assert_called_once_with("dbx_123") + @pytest.mark.asyncio + async def test_get_tunnel_url_derives_domain_from_base_url(self, mock_async_client: AsyncMock) -> None: + """Test get_tunnel_url derives tunnel domain from client base_url.""" + tunnel_view = SimpleNamespace( + tunnel_key="abc123xyz", + auth_mode="open", + create_time_ms=1234567890000, + ) + devbox_view_with_tunnel = SimpleNamespace( + id="dbx_123", + status="running", + tunnel=tunnel_view, + ) + mock_async_client.devboxes.retrieve = AsyncMock(return_value=devbox_view_with_tunnel) + devbox = AsyncDevbox(mock_async_client, "dbx_123") + + mock_async_client.base_url = httpx.URL("https://api.runloop.pro") + assert await devbox.get_tunnel_url(8080) == "https://8080-abc123xyz.tunnel.runloop.pro" + + mock_async_client.base_url = httpx.URL("http://127.0.0.1:8080") + assert await devbox.get_tunnel_url(8080) == "https://8080-abc123xyz.tunnel.127.0.0.1" + @pytest.mark.asyncio async def test_get_tunnel_url_returns_none_when_no_tunnel(self, mock_async_client: AsyncMock) -> None: """Test get_tunnel_url returns None when no tunnel is enabled.""" diff --git a/tests/sdk/devbox/test_core.py b/tests/sdk/devbox/test_core.py index 74bca0e75..d385d5ff7 100644 --- a/tests/sdk/devbox/test_core.py +++ b/tests/sdk/devbox/test_core.py @@ -9,6 +9,8 @@ from types import SimpleNamespace from unittest.mock import Mock +import httpx + import pytest from tests.sdk.conftest import ( @@ -373,6 +375,7 @@ def test_get_tunnel_url_constructs_url(self, mock_client: Mock) -> None: tunnel=tunnel_view, ) mock_client.devboxes.retrieve.return_value = devbox_view_with_tunnel + mock_client.base_url = httpx.URL("https://api.runloop.ai") devbox = Devbox(mock_client, "dbx_123") result = devbox.get_tunnel_url(8080) @@ -380,6 +383,27 @@ def test_get_tunnel_url_constructs_url(self, mock_client: Mock) -> None: assert result == "https://8080-abc123xyz.tunnel.runloop.ai" mock_client.devboxes.retrieve.assert_called_once_with("dbx_123") + def test_get_tunnel_url_derives_domain_from_base_url(self, mock_client: Mock) -> None: + """Test get_tunnel_url derives tunnel domain from client base_url.""" + tunnel_view = SimpleNamespace( + tunnel_key="abc123xyz", + auth_mode="open", + create_time_ms=1234567890000, + ) + devbox_view_with_tunnel = SimpleNamespace( + id="dbx_123", + status="running", + tunnel=tunnel_view, + ) + mock_client.devboxes.retrieve.return_value = devbox_view_with_tunnel + devbox = Devbox(mock_client, "dbx_123") + + mock_client.base_url = httpx.URL("https://api.runloop.pro") + assert devbox.get_tunnel_url(8080) == "https://8080-abc123xyz.tunnel.runloop.pro" + + mock_client.base_url = httpx.URL("http://127.0.0.1:8080") + assert devbox.get_tunnel_url(8080) == "https://8080-abc123xyz.tunnel.127.0.0.1" + def test_get_tunnel_url_returns_none_when_no_tunnel(self, mock_client: Mock) -> None: """Test get_tunnel_url returns None when no tunnel is enabled.""" devbox_view_no_tunnel = SimpleNamespace( From f04845d5d7f24f74898e5dee486991d2aa192d9b Mon Sep 17 00:00:00 2001 From: Siddarth Chalasani Date: Wed, 25 Mar 2026 18:21:03 -0700 Subject: [PATCH 2/2] formatting --- tests/sdk/async_devbox/test_core.py | 1 - tests/sdk/devbox/test_core.py | 1 - uv.lock | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/sdk/async_devbox/test_core.py b/tests/sdk/async_devbox/test_core.py index e87aaa387..ec10a88b3 100644 --- a/tests/sdk/async_devbox/test_core.py +++ b/tests/sdk/async_devbox/test_core.py @@ -10,7 +10,6 @@ from unittest.mock import AsyncMock import httpx - import pytest from tests.sdk.conftest import MockDevboxView diff --git a/tests/sdk/devbox/test_core.py b/tests/sdk/devbox/test_core.py index d385d5ff7..88525219f 100644 --- a/tests/sdk/devbox/test_core.py +++ b/tests/sdk/devbox/test_core.py @@ -10,7 +10,6 @@ from unittest.mock import Mock import httpx - import pytest from tests.sdk.conftest import ( diff --git a/uv.lock b/uv.lock index 970c26cc8..bac2c9a14 100644 --- a/uv.lock +++ b/uv.lock @@ -2386,7 +2386,7 @@ wheels = [ [[package]] name = "runloop-api-client" -version = "1.13.1" +version = "1.13.2" source = { editable = "." } dependencies = [ { name = "anyio" },