diff --git a/src/runloop_api_client/sdk/async_.py b/src/runloop_api_client/sdk/async_.py index 98519f36f..ce8658c4f 100644 --- a/src/runloop_api_client/sdk/async_.py +++ b/src/runloop_api_client/sdk/async_.py @@ -82,7 +82,7 @@ async def create( devbox_view = await self._client.devboxes.create_and_await_running( **params, ) - return AsyncDevbox(self._client, devbox_view.id) + return AsyncDevbox(self._client, devbox_view) async def create_from_blueprint_id( self, @@ -101,7 +101,7 @@ async def create_from_blueprint_id( blueprint_id=blueprint_id, **params, ) - return AsyncDevbox(self._client, devbox_view.id) + return AsyncDevbox(self._client, devbox_view) async def create_from_blueprint_name( self, @@ -120,7 +120,7 @@ async def create_from_blueprint_name( blueprint_name=blueprint_name, **params, ) - return AsyncDevbox(self._client, devbox_view.id) + return AsyncDevbox(self._client, devbox_view) async def create_from_snapshot( self, @@ -139,9 +139,9 @@ async def create_from_snapshot( snapshot_id=snapshot_id, **params, ) - return AsyncDevbox(self._client, devbox_view.id) + return AsyncDevbox(self._client, devbox_view) - def from_id(self, devbox_id: str) -> AsyncDevbox: + async def from_id(self, devbox_id: str) -> AsyncDevbox: """Attach to an existing devbox by ID. Returns immediately without waiting for the devbox to reach ``running`` @@ -153,7 +153,8 @@ def from_id(self, devbox_id: str) -> AsyncDevbox: :return: Wrapper bound to the requested devbox :rtype: AsyncDevbox """ - return AsyncDevbox(self._client, devbox_id) + devbox_view = await self._client.devboxes.retrieve(devbox_id) + return AsyncDevbox(self._client, devbox_view) async def list( self, @@ -168,7 +169,7 @@ async def list( page = await self._client.devboxes.list( **params, ) - return [AsyncDevbox(self._client, item.id) for item in page.devboxes] + return [AsyncDevbox(self._client, item) for item in page.devboxes] class AsyncSnapshotOps: diff --git a/src/runloop_api_client/sdk/async_blueprint.py b/src/runloop_api_client/sdk/async_blueprint.py index 4e805b6e3..99d4717e9 100644 --- a/src/runloop_api_client/sdk/async_blueprint.py +++ b/src/runloop_api_client/sdk/async_blueprint.py @@ -101,4 +101,4 @@ async def create_devbox( blueprint_id=self._id, **params, ) - return AsyncDevbox(self._client, devbox_view.id) + return AsyncDevbox(self._client, devbox_view) diff --git a/src/runloop_api_client/sdk/async_devbox.py b/src/runloop_api_client/sdk/async_devbox.py index 2ac12ebba..6b8ab908d 100644 --- a/src/runloop_api_client/sdk/async_devbox.py +++ b/src/runloop_api_client/sdk/async_devbox.py @@ -64,16 +64,21 @@ class AsyncDevbox: # Devbox is automatically shut down on exit """ - def __init__(self, client: AsyncRunloop, devbox_id: str) -> None: + def __init__( + self, + client: AsyncRunloop, + devbox_view: DevboxView, + ) -> None: """Initialize the wrapper. :param client: Generated async Runloop client :type client: AsyncRunloop - :param devbox_id: Devbox identifier returned by the API - :type devbox_id: str + :param devbox_view: DevboxView from the API + :type devbox_view: DevboxView """ self._client = client - self._id = devbox_id + self._id = devbox_view.id + self._tunnel: Optional[TunnelView] = devbox_view.tunnel self._logger = logging.getLogger(__name__) @override @@ -104,6 +109,18 @@ def id(self) -> str: """ return self._id + @property + def tunnel(self) -> Optional[TunnelView]: + """Return the cached tunnel info, if available. + + This returns the tunnel info cached at creation time. For the latest + tunnel state, use :meth:`get_info` or :meth:`net.view_tunnel`. + + :return: Cached tunnel info, or None if no tunnel was enabled at creation + :rtype: TunnelView | None + """ + return self._tunnel + async def get_info( self, **options: Unpack[BaseRequestOptions], @@ -776,6 +793,24 @@ async def enable_tunnel( **params, ) + async def view_tunnel( + self, + **options: Unpack[BaseRequestOptions], + ) -> Optional[TunnelView]: + """Retrieve the current tunnel info for this devbox, if one exists. + + :param options: Optional request configuration + :return: Current tunnel info, or None if no tunnel is enabled + :rtype: TunnelView | None + + Example: + >>> tunnel = await devbox.net.view_tunnel() + >>> if tunnel: + ... print(f"Tunnel key: {tunnel.tunnel_key}") + """ + info = await self._devbox.get_info(**options) + return info.tunnel + async def remove_tunnel( self, **params: Unpack[SDKDevboxRemoveTunnelParams], diff --git a/src/runloop_api_client/sdk/async_scenario_run.py b/src/runloop_api_client/sdk/async_scenario_run.py index 314de676f..d9ad4ba4f 100644 --- a/src/runloop_api_client/sdk/async_scenario_run.py +++ b/src/runloop_api_client/sdk/async_scenario_run.py @@ -4,7 +4,6 @@ import os from typing import Union, Optional -from functools import cached_property from typing_extensions import Unpack, override from ..types import ScenarioRunView @@ -68,16 +67,16 @@ def devbox_id(self) -> str: """ return self._devbox_id - @cached_property - def devbox(self) -> AsyncDevbox: - """The devbox instance for this scenario run. + async def get_devbox(self) -> AsyncDevbox: + """Get the devbox instance for this scenario run. Use this to interact with the devbox environment during the scenario run. :return: AsyncDevbox instance :rtype: AsyncDevbox """ - return AsyncDevbox(self._client, self._devbox_id) + devbox_view = await self._client.devboxes.retrieve(self._devbox_id) + return AsyncDevbox(self._client, devbox_view) async def get_info( self, diff --git a/src/runloop_api_client/sdk/async_snapshot.py b/src/runloop_api_client/sdk/async_snapshot.py index 5f9c09772..954d3a184 100644 --- a/src/runloop_api_client/sdk/async_snapshot.py +++ b/src/runloop_api_client/sdk/async_snapshot.py @@ -122,4 +122,4 @@ async def create_devbox( snapshot_id=self._id, **params, ) - return AsyncDevbox(self._client, devbox_view.id) + return AsyncDevbox(self._client, devbox_view) diff --git a/src/runloop_api_client/sdk/blueprint.py b/src/runloop_api_client/sdk/blueprint.py index 8e9daac3e..65fc3aef3 100644 --- a/src/runloop_api_client/sdk/blueprint.py +++ b/src/runloop_api_client/sdk/blueprint.py @@ -101,4 +101,4 @@ def create_devbox( blueprint_id=self._id, **params, ) - return Devbox(self._client, devbox_view.id) + return Devbox(self._client, devbox_view) diff --git a/src/runloop_api_client/sdk/devbox.py b/src/runloop_api_client/sdk/devbox.py index bdf8d7389..e624cc47e 100644 --- a/src/runloop_api_client/sdk/devbox.py +++ b/src/runloop_api_client/sdk/devbox.py @@ -63,16 +63,21 @@ class Devbox: # Devbox is automatically shutdown on exit """ - def __init__(self, client: Runloop, devbox_id: str) -> None: + def __init__( + self, + client: Runloop, + devbox_view: DevboxView, + ) -> None: """Initialize the wrapper. :param client: Generated Runloop client :type client: Runloop - :param devbox_id: Devbox identifier returned by the API - :type devbox_id: str + :param devbox_view: DevboxView from the API + :type devbox_view: DevboxView """ self._client = client - self._id = devbox_id + self._id = devbox_view.id + self._tunnel: Optional[TunnelView] = devbox_view.tunnel self._logger = logging.getLogger(__name__) @override @@ -103,6 +108,18 @@ def id(self) -> str: """ return self._id + @property + def tunnel(self) -> Optional[TunnelView]: + """Return the cached tunnel info, if available. + + This returns the tunnel info cached at creation time. For the latest + tunnel state, use :meth:`get_info` or :meth:`net.view_tunnel`. + + :return: Cached tunnel info, or None if no tunnel was enabled at creation + :rtype: TunnelView | None + """ + return self._tunnel + def get_info( self, **options: Unpack[BaseRequestOptions], @@ -779,6 +796,24 @@ def enable_tunnel( **params, ) + def view_tunnel( + self, + **options: Unpack[BaseRequestOptions], + ) -> Optional[TunnelView]: + """Retrieve the current tunnel info for this devbox, if one exists. + + :param options: Optional request configuration + :return: Current tunnel info, or None if no tunnel is enabled + :rtype: TunnelView | None + + Example: + >>> tunnel = devbox.net.view_tunnel() + >>> if tunnel: + ... print(f"Tunnel key: {tunnel.tunnel_key}") + """ + info = self._devbox.get_info(**options) + return info.tunnel + def remove_tunnel( self, **params: Unpack[SDKDevboxRemoveTunnelParams], diff --git a/src/runloop_api_client/sdk/scenario_run.py b/src/runloop_api_client/sdk/scenario_run.py index ede44b105..2586e4403 100644 --- a/src/runloop_api_client/sdk/scenario_run.py +++ b/src/runloop_api_client/sdk/scenario_run.py @@ -77,7 +77,8 @@ def devbox(self) -> Devbox: :return: Devbox instance :rtype: Devbox """ - return Devbox(self._client, self._devbox_id) + devbox_view = self._client.devboxes.retrieve(self._devbox_id) + return Devbox(self._client, devbox_view) def get_info( self, diff --git a/src/runloop_api_client/sdk/snapshot.py b/src/runloop_api_client/sdk/snapshot.py index 087a74e78..282273a19 100644 --- a/src/runloop_api_client/sdk/snapshot.py +++ b/src/runloop_api_client/sdk/snapshot.py @@ -122,4 +122,4 @@ def create_devbox( snapshot_id=self._id, **params, ) - return Devbox(self._client, devbox_view.id) + return Devbox(self._client, devbox_view) diff --git a/src/runloop_api_client/sdk/sync.py b/src/runloop_api_client/sdk/sync.py index aa5fa27fc..ac68041a2 100644 --- a/src/runloop_api_client/sdk/sync.py +++ b/src/runloop_api_client/sdk/sync.py @@ -81,7 +81,7 @@ def create( devbox_view = self._client.devboxes.create_and_await_running( **params, ) - return Devbox(self._client, devbox_view.id) + return Devbox(self._client, devbox_view) def create_from_blueprint_id( self, @@ -100,7 +100,7 @@ def create_from_blueprint_id( blueprint_id=blueprint_id, **params, ) - return Devbox(self._client, devbox_view.id) + return Devbox(self._client, devbox_view) def create_from_blueprint_name( self, @@ -119,7 +119,7 @@ def create_from_blueprint_name( blueprint_name=blueprint_name, **params, ) - return Devbox(self._client, devbox_view.id) + return Devbox(self._client, devbox_view) def create_from_snapshot( self, @@ -138,7 +138,7 @@ def create_from_snapshot( snapshot_id=snapshot_id, **params, ) - return Devbox(self._client, devbox_view.id) + return Devbox(self._client, devbox_view) def from_id(self, devbox_id: str) -> Devbox: """Attach to an existing devbox by ID. @@ -152,7 +152,8 @@ def from_id(self, devbox_id: str) -> Devbox: :rtype: Devbox """ self._client.devboxes.await_running(devbox_id) - return Devbox(self._client, devbox_id) + devbox_view = self._client.devboxes.retrieve(devbox_id) + return Devbox(self._client, devbox_view) def list( self, @@ -167,7 +168,7 @@ def list( page = self._client.devboxes.list( **params, ) - return [Devbox(self._client, item.id) for item in page.devboxes] + return [Devbox(self._client, item) for item in page.devboxes] class SnapshotOps: diff --git a/tests/sdk/async_devbox/test_core.py b/tests/sdk/async_devbox/test_core.py index 263e2bd3c..c643daa19 100644 --- a/tests/sdk/async_devbox/test_core.py +++ b/tests/sdk/async_devbox/test_core.py @@ -11,7 +11,7 @@ import pytest -from tests.sdk.conftest import MockDevboxView +from tests.sdk.conftest import MockDevboxView, mock_devbox_view from runloop_api_client.sdk import AsyncDevbox from runloop_api_client.lib.polling import PollingConfig from runloop_api_client.sdk.async_devbox import ( @@ -26,12 +26,12 @@ class TestAsyncDevbox: def test_init(self, mock_async_client: AsyncMock) -> None: """Test AsyncDevbox initialization.""" - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) assert devbox.id == "dbx_123" def test_repr(self, mock_async_client: AsyncMock) -> None: """Test AsyncDevbox string representation.""" - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) assert repr(devbox) == "" @pytest.mark.asyncio @@ -39,7 +39,7 @@ async def test_context_manager_enter_exit(self, mock_async_client: AsyncMock, de """Test context manager behavior with successful shutdown.""" mock_async_client.devboxes.shutdown = AsyncMock(return_value=devbox_view) - async with AsyncDevbox(mock_async_client, "dbx_123") as devbox: + async with AsyncDevbox(mock_async_client, mock_devbox_view()) as devbox: assert devbox.id == "dbx_123" call_kwargs = mock_async_client.devboxes.shutdown.call_args[1] @@ -51,7 +51,7 @@ async def test_context_manager_exception_handling(self, mock_async_client: Async mock_async_client.devboxes.shutdown = AsyncMock(side_effect=RuntimeError("Shutdown failed")) with pytest.raises(ValueError, match="Test error"): - async with AsyncDevbox(mock_async_client, "dbx_123"): + async with AsyncDevbox(mock_async_client, mock_devbox_view()): raise ValueError("Test error") # Shutdown should be called even when body raises exception @@ -62,7 +62,7 @@ async def test_get_info(self, mock_async_client: AsyncMock, devbox_view: MockDev """Test get_info method.""" mock_async_client.devboxes.retrieve = AsyncMock(return_value=devbox_view) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.get_info( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, @@ -85,7 +85,7 @@ async def test_await_running(self, mock_async_client: AsyncMock, devbox_view: Mo mock_async_client.devboxes.await_running = AsyncMock(return_value=devbox_view) polling_config = PollingConfig(timeout_seconds=60.0) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.await_running(polling_config=polling_config) assert result == devbox_view @@ -100,7 +100,7 @@ async def test_await_suspended(self, mock_async_client: AsyncMock, devbox_view: mock_async_client.devboxes.await_suspended = AsyncMock(return_value=devbox_view) polling_config = PollingConfig(timeout_seconds=60.0) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.await_suspended(polling_config=polling_config) assert result == devbox_view @@ -114,7 +114,7 @@ async def test_shutdown(self, mock_async_client: AsyncMock, devbox_view: MockDev """Test shutdown method.""" mock_async_client.devboxes.shutdown = AsyncMock(return_value=devbox_view) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.shutdown( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, @@ -138,7 +138,7 @@ async def test_suspend(self, mock_async_client: AsyncMock, devbox_view: MockDevb """Test suspend method.""" mock_async_client.devboxes.suspend = AsyncMock(return_value=devbox_view) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.suspend( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, @@ -164,7 +164,7 @@ async def test_resume(self, mock_async_client: AsyncMock, devbox_view: MockDevbo mock_async_client.devboxes.await_running = AsyncMock(return_value=devbox_view) polling_config = PollingConfig(timeout_seconds=60.0) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.resume( polling_config=polling_config, extra_headers={"X-Custom": "value"}, @@ -193,7 +193,7 @@ async def test_resume_async(self, mock_async_client: AsyncMock, devbox_view: Moc """Test resume_async method.""" mock_async_client.devboxes.resume = AsyncMock(return_value=devbox_view) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.resume_async( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, @@ -217,7 +217,7 @@ async def test_keep_alive(self, mock_async_client: AsyncMock) -> None: """Test keep_alive method.""" mock_async_client.devboxes.keep_alive = AsyncMock(return_value=object()) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.keep_alive( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, @@ -245,7 +245,7 @@ async def test_snapshot_disk(self, mock_async_client: AsyncMock) -> None: mock_async_client.devboxes.snapshot_disk_async = AsyncMock(return_value=snapshot_data) mock_async_client.devboxes.disk_snapshots.await_completed = AsyncMock(return_value=snapshot_status) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) polling_config = PollingConfig(timeout_seconds=60.0) snapshot = await devbox.snapshot_disk( name="test-snapshot", @@ -274,7 +274,7 @@ async def test_snapshot_disk_async(self, mock_async_client: AsyncMock) -> None: snapshot_data = SimpleNamespace(id="snp_123") mock_async_client.devboxes.snapshot_disk_async = AsyncMock(return_value=snapshot_data) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) snapshot = await devbox.snapshot_disk_async( name="test-snapshot", metadata={"key": "value"}, @@ -296,7 +296,7 @@ async def test_close(self, mock_async_client: AsyncMock, devbox_view: MockDevbox """Test close method calls shutdown.""" mock_async_client.devboxes.shutdown = AsyncMock(return_value=devbox_view) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) await devbox.close() mock_async_client.devboxes.shutdown.assert_called_once() @@ -305,21 +305,21 @@ async def test_close(self, mock_async_client: AsyncMock, devbox_view: MockDevbox def test_cmd_property(self, mock_async_client: AsyncMock) -> None: """Test cmd property returns AsyncCommandInterface.""" - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) cmd = devbox.cmd assert isinstance(cmd, AsyncCommandInterface) assert cmd._devbox is devbox def test_file_property(self, mock_async_client: AsyncMock) -> None: """Test file property returns AsyncFileInterface.""" - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) file_interface = devbox.file assert isinstance(file_interface, AsyncFileInterface) assert file_interface._devbox is devbox def test_net_property(self, mock_async_client: AsyncMock) -> None: """Test net property returns AsyncNetworkInterface.""" - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) net = devbox.net assert isinstance(net, AsyncNetworkInterface) assert net._devbox is devbox diff --git a/tests/sdk/async_devbox/test_edge_cases.py b/tests/sdk/async_devbox/test_edge_cases.py index 94d9e661b..3ebbe199e 100644 --- a/tests/sdk/async_devbox/test_edge_cases.py +++ b/tests/sdk/async_devbox/test_edge_cases.py @@ -10,6 +10,7 @@ import httpx import pytest +from tests.sdk.conftest import mock_devbox_view from runloop_api_client.sdk import AsyncDevbox @@ -21,6 +22,6 @@ async def test_async_network_error(self, mock_async_client: AsyncMock) -> None: """Test handling of network errors in async.""" mock_async_client.devboxes.retrieve = AsyncMock(side_effect=httpx.NetworkError("Connection failed")) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) with pytest.raises(httpx.NetworkError): await devbox.get_info() diff --git a/tests/sdk/async_devbox/test_interfaces.py b/tests/sdk/async_devbox/test_interfaces.py index 52c439c22..4341a0732 100644 --- a/tests/sdk/async_devbox/test_interfaces.py +++ b/tests/sdk/async_devbox/test_interfaces.py @@ -12,7 +12,7 @@ import httpx import pytest -from tests.sdk.conftest import MockExecutionView +from tests.sdk.conftest import MockExecutionView, mock_devbox_view from runloop_api_client.sdk import AsyncDevbox @@ -27,7 +27,7 @@ async def test_exec_without_callbacks( mock_async_client.devboxes.execute_async = AsyncMock(return_value=execution_view) mock_async_client.devboxes.executions.await_completed = AsyncMock(return_value=execution_view) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.cmd.exec("echo hello") assert result.exit_code == 0 @@ -61,7 +61,7 @@ async def test_exec_with_stdout_callback(self, mock_async_client: AsyncMock, moc stdout_calls: list[str] = [] - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.cmd.exec("echo hello", stdout=stdout_calls.append) assert result.exit_code == 0 @@ -81,7 +81,7 @@ async def test_exec_async_returns_execution( mock_async_client.devboxes.execute_async = AsyncMock(return_value=execution_async) mock_async_client.devboxes.executions.stream_stdout_updates = AsyncMock(return_value=mock_async_stream) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) execution = await devbox.cmd.exec_async("long-running command") assert execution.execution_id == "exn_123" @@ -97,7 +97,7 @@ async def test_read(self, mock_async_client: AsyncMock) -> None: """Test file read.""" mock_async_client.devboxes.read_file_contents = AsyncMock(return_value="file content") - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.file.read(file_path="/path/to/file") assert result == "file content" @@ -109,7 +109,7 @@ async def test_write_string(self, mock_async_client: AsyncMock) -> None: execution_detail = SimpleNamespace() mock_async_client.devboxes.write_file_contents = AsyncMock(return_value=execution_detail) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.file.write(file_path="/path/to/file", contents="content") assert result == execution_detail @@ -121,7 +121,7 @@ async def test_write_bytes(self, mock_async_client: AsyncMock) -> None: execution_detail = SimpleNamespace() mock_async_client.devboxes.write_file_contents = AsyncMock(return_value=execution_detail) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.file.write(file_path="/path/to/file", contents="content") assert result == execution_detail @@ -134,7 +134,7 @@ async def test_download(self, mock_async_client: AsyncMock) -> None: mock_response.read = AsyncMock(return_value=b"file content") mock_async_client.devboxes.download_file = AsyncMock(return_value=mock_response) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.file.download(path="/path/to/file") assert result == b"file content" @@ -146,7 +146,7 @@ async def test_upload(self, mock_async_client: AsyncMock, tmp_path: Path) -> Non execution_detail = SimpleNamespace() mock_async_client.devboxes.upload_file = AsyncMock(return_value=execution_detail) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) # Create a temporary file for upload temp_file = tmp_path / "test_file.txt" temp_file.write_text("test content") @@ -166,7 +166,7 @@ async def test_create_ssh_key(self, mock_async_client: AsyncMock) -> None: ssh_key_response = SimpleNamespace(public_key="ssh-rsa ...") mock_async_client.devboxes.create_ssh_key = AsyncMock(return_value=ssh_key_response) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.net.create_ssh_key( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, @@ -184,7 +184,7 @@ async def test_create_tunnel(self, mock_async_client: AsyncMock) -> None: tunnel_view = SimpleNamespace(tunnel_id="tunnel_123") mock_async_client.devboxes.create_tunnel = AsyncMock(return_value=tunnel_view) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.net.create_tunnel( port=8080, extra_headers={"X-Custom": "value"}, @@ -202,7 +202,7 @@ async def test_remove_tunnel(self, mock_async_client: AsyncMock) -> None: """Test remove tunnel.""" mock_async_client.devboxes.remove_tunnel = AsyncMock(return_value=object()) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = await devbox.net.remove_tunnel( port=8080, extra_headers={"X-Custom": "value"}, diff --git a/tests/sdk/async_devbox/test_streaming.py b/tests/sdk/async_devbox/test_streaming.py index 3bb3e1a7b..a5f0703a6 100644 --- a/tests/sdk/async_devbox/test_streaming.py +++ b/tests/sdk/async_devbox/test_streaming.py @@ -13,7 +13,7 @@ import pytest -from tests.sdk.conftest import TASK_COMPLETION_SHORT +from tests.sdk.conftest import TASK_COMPLETION_SHORT, mock_devbox_view from runloop_api_client.sdk import AsyncDevbox from runloop_api_client._streaming import AsyncStream from runloop_api_client.types.devboxes import ExecutionUpdateChunk @@ -25,7 +25,7 @@ class TestAsyncDevboxStreaming: def test_start_streaming_no_callbacks(self, mock_async_client: AsyncMock) -> None: """Test _start_streaming returns None when no callbacks.""" - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) result = devbox._start_streaming("exn_123", stdout=None, stderr=None, output=None) assert result is None @@ -46,7 +46,7 @@ async def async_iter(): mock_async_client.devboxes.executions.stream_stdout_updates = AsyncMock(return_value=mock_async_stream) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) stdout_calls: list[str] = [] result = devbox._start_streaming("exn_123", stdout=stdout_calls.append, stderr=None, output=None) @@ -76,7 +76,7 @@ async def async_iter(): mock_async_client.devboxes.executions.stream_stderr_updates = AsyncMock(return_value=mock_async_stream) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) stderr_calls: list[str] = [] result = devbox._start_streaming("exn_123", stdout=None, stderr=stderr_calls.append, output=None) @@ -107,7 +107,7 @@ async def async_iter(): mock_async_client.devboxes.executions.stream_stdout_updates = AsyncMock(return_value=mock_async_stream) mock_async_client.devboxes.executions.stream_stderr_updates = AsyncMock(return_value=mock_async_stream) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) output_calls: list[str] = [] result = devbox._start_streaming("exn_123", stdout=None, stderr=None, output=output_calls.append) @@ -136,7 +136,7 @@ async def async_iter() -> AsyncIterator[SimpleNamespace]: mock_async_stream.__aenter__ = AsyncMock(return_value=mock_async_stream) mock_async_stream.__aexit__ = AsyncMock(return_value=None) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) calls: list[str] = [] async def stream_factory() -> AsyncStream[ExecutionUpdateChunk]: @@ -166,7 +166,7 @@ async def async_iter() -> AsyncIterator[SimpleNamespace]: mock_async_stream.__aenter__ = AsyncMock(return_value=mock_async_stream) mock_async_stream.__aexit__ = AsyncMock(return_value=None) - devbox = AsyncDevbox(mock_async_client, "dbx_123") + devbox = AsyncDevbox(mock_async_client, mock_devbox_view()) calls: list[str] = [] async def stream_factory() -> AsyncStream[ExecutionUpdateChunk]: diff --git a/tests/sdk/conftest.py b/tests/sdk/conftest.py index 29085ed45..28c6cb49d 100644 --- a/tests/sdk/conftest.py +++ b/tests/sdk/conftest.py @@ -51,6 +51,20 @@ class MockDevboxView: id: str = TEST_IDS["devbox"] status: str = "running" name: str = "test-devbox" + tunnel: Any = None + + +def mock_devbox_view( + id: str = TEST_IDS["devbox"], + status: str = "running", + name: str = "test-devbox", + tunnel: Any = None, +) -> Any: + """Create a mock DevboxView for testing. + + Returns Any to satisfy type checkers when passed to Devbox/AsyncDevbox constructors. + """ + return MockDevboxView(id=id, status=status, name=name, tunnel=tunnel) @dataclass diff --git a/tests/sdk/devbox/test_core.py b/tests/sdk/devbox/test_core.py index c131ca489..c18b38e96 100644 --- a/tests/sdk/devbox/test_core.py +++ b/tests/sdk/devbox/test_core.py @@ -11,9 +11,7 @@ import pytest -from tests.sdk.conftest import ( - MockDevboxView, -) +from tests.sdk.conftest import MockDevboxView, mock_devbox_view from runloop_api_client.sdk import Devbox from runloop_api_client._types import omit from runloop_api_client.sdk.devbox import ( @@ -29,19 +27,19 @@ class TestDevbox: def test_init(self, mock_client: Mock) -> None: """Test Devbox initialization.""" - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) assert devbox.id == "dbx_123" def test_repr(self, mock_client: Mock) -> None: """Test Devbox string representation.""" - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) assert repr(devbox) == "" def test_context_manager_enter_exit(self, mock_client: Mock, devbox_view: MockDevboxView) -> None: """Test context manager behavior with successful shutdown.""" mock_client.devboxes.shutdown.return_value = devbox_view - with Devbox(mock_client, "dbx_123") as devbox: + with Devbox(mock_client, mock_devbox_view()) as devbox: assert devbox.id == "dbx_123" call_kwargs = mock_client.devboxes.shutdown.call_args[1] @@ -52,7 +50,7 @@ def test_context_manager_exception_handling(self, mock_client: Mock) -> None: mock_client.devboxes.shutdown.side_effect = RuntimeError("Shutdown failed") with pytest.raises(ValueError, match="Test error"): - with Devbox(mock_client, "dbx_123"): + with Devbox(mock_client, mock_devbox_view()): raise ValueError("Test error") # Shutdown should be called even when body raises exception @@ -62,7 +60,7 @@ def test_get_info(self, mock_client: Mock, devbox_view: MockDevboxView) -> None: """Test get_info method.""" mock_client.devboxes.retrieve.return_value = devbox_view - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.get_info( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, @@ -84,7 +82,7 @@ def test_await_running(self, mock_client: Mock, devbox_view: MockDevboxView) -> mock_client.devboxes.await_running.return_value = devbox_view polling_config = PollingConfig(timeout_seconds=60.0) - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.await_running(polling_config=polling_config) assert result == devbox_view @@ -98,7 +96,7 @@ def test_await_suspended(self, mock_client: Mock, devbox_view: MockDevboxView) - mock_client.devboxes.await_suspended.return_value = devbox_view polling_config = PollingConfig(timeout_seconds=60.0) - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.await_suspended(polling_config=polling_config) assert result == devbox_view @@ -111,7 +109,7 @@ def test_shutdown(self, mock_client: Mock, devbox_view: MockDevboxView) -> None: """Test shutdown method.""" mock_client.devboxes.shutdown.return_value = devbox_view - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.shutdown( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, @@ -136,7 +134,7 @@ def test_suspend(self, mock_client: Mock, devbox_view: MockDevboxView) -> None: mock_client.devboxes.await_suspended.return_value = devbox_view polling_config = PollingConfig(timeout_seconds=60.0) - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.suspend( polling_config=polling_config, extra_headers={"X-Custom": "value"}, @@ -166,7 +164,7 @@ def test_resume(self, mock_client: Mock, devbox_view: MockDevboxView) -> None: mock_client.devboxes.await_running.return_value = devbox_view polling_config = PollingConfig(timeout_seconds=60.0) - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.resume( polling_config=polling_config, extra_headers={"X-Custom": "value"}, @@ -195,7 +193,7 @@ def test_resume_async(self, mock_client: Mock, devbox_view: MockDevboxView) -> N mock_client.devboxes.resume.return_value = devbox_view mock_client.devboxes.await_running = Mock() - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.resume_async( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, @@ -220,7 +218,7 @@ def test_keep_alive(self, mock_client: Mock) -> None: """Test keep_alive method.""" mock_client.devboxes.keep_alive.return_value = object() - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.keep_alive( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, @@ -247,7 +245,7 @@ def test_snapshot_disk(self, mock_client: Mock) -> None: mock_client.devboxes.snapshot_disk_async.return_value = snapshot_data mock_client.devboxes.disk_snapshots.await_completed.return_value = snapshot_status - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) polling_config = PollingConfig(timeout_seconds=60.0) snapshot = devbox.snapshot_disk( name="test-snapshot", @@ -273,7 +271,7 @@ def test_snapshot_disk_async(self, mock_client: Mock) -> None: snapshot_data = SimpleNamespace(id="snp_123") mock_client.devboxes.snapshot_disk_async.return_value = snapshot_data - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) snapshot = devbox.snapshot_disk_async( name="test-snapshot", metadata={"key": "value"}, @@ -296,7 +294,7 @@ def test_close(self, mock_client: Mock, devbox_view: MockDevboxView) -> None: """Test close method calls shutdown.""" mock_client.devboxes.shutdown.return_value = devbox_view - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) devbox.close() call_kwargs = mock_client.devboxes.shutdown.call_args[1] @@ -304,21 +302,21 @@ def test_close(self, mock_client: Mock, devbox_view: MockDevboxView) -> None: def test_cmd_property(self, mock_client: Mock) -> None: """Test cmd property returns CommandInterface.""" - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) cmd = devbox.cmd assert isinstance(cmd, CommandInterface) assert cmd._devbox is devbox def test_file_property(self, mock_client: Mock) -> None: """Test file property returns FileInterface.""" - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) file_interface = devbox.file assert isinstance(file_interface, FileInterface) assert file_interface._devbox is devbox def test_net_property(self, mock_client: Mock) -> None: """Test net property returns NetworkInterface.""" - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) net = devbox.net assert isinstance(net, NetworkInterface) assert net._devbox is devbox diff --git a/tests/sdk/devbox/test_edge_cases.py b/tests/sdk/devbox/test_edge_cases.py index 23341f0c0..03a32c351 100644 --- a/tests/sdk/devbox/test_edge_cases.py +++ b/tests/sdk/devbox/test_edge_cases.py @@ -17,6 +17,7 @@ from tests.sdk.conftest import ( NUM_CONCURRENT_THREADS, MockDevboxView, + mock_devbox_view, create_mock_httpx_response, ) from runloop_api_client.sdk import Devbox, StorageObject @@ -31,7 +32,7 @@ def test_network_error(self, mock_client: Mock) -> None: """Test handling of network errors.""" mock_client.devboxes.retrieve.side_effect = httpx.NetworkError("Connection failed") - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) with pytest.raises(httpx.NetworkError): devbox.get_info() @@ -50,7 +51,7 @@ def test_api_error(self, mock_client: Mock, status_code: int, message: str) -> N mock_client.devboxes.retrieve.side_effect = error - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) with pytest.raises(APIStatusError): devbox.get_info() @@ -58,7 +59,7 @@ def test_timeout_error(self, mock_client: Mock) -> None: """Test handling of timeout errors.""" mock_client.devboxes.retrieve.side_effect = httpx.TimeoutException("Request timed out") - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) with pytest.raises(httpx.TimeoutException): devbox.get_info(timeout=1.0) @@ -71,7 +72,7 @@ def test_empty_responses(self, mock_client: Mock) -> None: empty_view = SimpleNamespace(id="dbx_123", status="", name="") mock_client.devboxes.retrieve.return_value = empty_view - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.get_info() assert result == empty_view @@ -80,7 +81,7 @@ def test_none_values(self, mock_client: Mock) -> None: view_with_none = SimpleNamespace(id="dbx_123", status=None, name=None) mock_client.devboxes.retrieve.return_value = view_with_none - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.get_info() assert result.status is None assert result.name is None @@ -91,7 +92,7 @@ def test_concurrent_operations( """Test concurrent operations.""" mock_client.devboxes.retrieve.return_value = SimpleNamespace(id="dbx_123", status="running") - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) results: list[DevboxView] = [] def get_info() -> None: @@ -118,13 +119,13 @@ def test_context_manager_vs_manual_cleanup(self, mock_client: Mock, devbox_view: mock_client.devboxes.shutdown.return_value = devbox_view # Context manager approach (Pythonic) - with Devbox(mock_client, "dbx_123"): + with Devbox(mock_client, mock_devbox_view()): pass mock_client.devboxes.shutdown.assert_called_once() # Manual cleanup (TypeScript-like) - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) devbox.shutdown() assert mock_client.devboxes.shutdown.call_count == 2 diff --git a/tests/sdk/devbox/test_interfaces.py b/tests/sdk/devbox/test_interfaces.py index 66ef8fa7b..aeab4b729 100644 --- a/tests/sdk/devbox/test_interfaces.py +++ b/tests/sdk/devbox/test_interfaces.py @@ -12,7 +12,7 @@ import httpx -from tests.sdk.conftest import MockExecutionView +from tests.sdk.conftest import MockExecutionView, mock_devbox_view from runloop_api_client.sdk import Devbox @@ -24,7 +24,7 @@ def test_exec_without_callbacks(self, mock_client: Mock, execution_view: MockExe mock_client.devboxes.execute_async.return_value = execution_view mock_client.devboxes.executions.await_completed.return_value = execution_view - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.cmd.exec("echo hello") assert result.exit_code == 0 @@ -57,7 +57,7 @@ def test_exec_with_stdout_callback(self, mock_client: Mock, mock_stream: Mock) - stdout_calls: list[str] = [] - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.cmd.exec("echo hello", stdout=stdout_calls.append) assert result.exit_code == 0 @@ -86,7 +86,7 @@ def test_exec_with_stderr_callback(self, mock_client: Mock, mock_stream: Mock) - stderr_calls: list[str] = [] - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.cmd.exec("echo hello", stderr=stderr_calls.append) assert result.exit_code == 0 @@ -115,7 +115,7 @@ def test_exec_with_output_callback(self, mock_client: Mock, mock_stream: Mock) - output_calls: list[str] = [] - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.cmd.exec("echo hello", output=output_calls.append) assert result.exit_code == 0 @@ -146,7 +146,7 @@ def test_exec_with_all_callbacks(self, mock_client: Mock, mock_stream: Mock) -> stderr_calls: list[str] = [] output_calls: list[str] = [] - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.cmd.exec( "echo hello", stdout=stdout_calls.append, @@ -168,7 +168,7 @@ def test_exec_async_returns_execution(self, mock_client: Mock, mock_stream: Mock mock_client.devboxes.execute_async.return_value = execution_async mock_client.devboxes.executions.stream_stdout_updates.return_value = mock_stream - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) execution = devbox.cmd.exec_async("long-running command") assert execution.execution_id == "exn_123" @@ -183,7 +183,7 @@ def test_read(self, mock_client: Mock) -> None: """Test file read.""" mock_client.devboxes.read_file_contents.return_value = "file content" - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.file.read(file_path="/path/to/file") assert result == "file content" @@ -196,7 +196,7 @@ def test_write_string(self, mock_client: Mock) -> None: execution_detail = SimpleNamespace() mock_client.devboxes.write_file_contents.return_value = execution_detail - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.file.write(file_path="/path/to/file", contents="content") assert result == execution_detail @@ -210,7 +210,7 @@ def test_write_bytes(self, mock_client: Mock) -> None: execution_detail = SimpleNamespace() mock_client.devboxes.write_file_contents.return_value = execution_detail - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.file.write(file_path="/path/to/file", contents="content") assert result == execution_detail @@ -225,7 +225,7 @@ def test_download(self, mock_client: Mock) -> None: mock_response.read.return_value = b"file content" mock_client.devboxes.download_file.return_value = mock_response - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.file.download(path="/path/to/file") assert result == b"file content" @@ -238,7 +238,7 @@ def test_upload(self, mock_client: Mock, tmp_path: Path) -> None: execution_detail = SimpleNamespace() mock_client.devboxes.upload_file.return_value = execution_detail - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) # Create a temporary file for upload temp_file = tmp_path / "test_file.txt" temp_file.write_text("test content") @@ -260,7 +260,7 @@ def test_create_ssh_key(self, mock_client: Mock) -> None: ssh_key_response = SimpleNamespace(public_key="ssh-rsa ...") mock_client.devboxes.create_ssh_key.return_value = ssh_key_response - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.net.create_ssh_key( extra_headers={"X-Custom": "value"}, extra_query={"param": "value"}, @@ -284,7 +284,7 @@ def test_create_tunnel(self, mock_client: Mock) -> None: tunnel_view = SimpleNamespace(port=8080) mock_client.devboxes.create_tunnel.return_value = tunnel_view - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.net.create_tunnel( port=8080, extra_headers={"X-Custom": "value"}, @@ -309,7 +309,7 @@ def test_remove_tunnel(self, mock_client: Mock) -> None: """Test remove tunnel.""" mock_client.devboxes.remove_tunnel.return_value = object() - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox.net.remove_tunnel( port=8080, extra_headers={"X-Custom": "value"}, diff --git a/tests/sdk/devbox/test_streaming.py b/tests/sdk/devbox/test_streaming.py index 6d44a4e5e..b65e14d9f 100644 --- a/tests/sdk/devbox/test_streaming.py +++ b/tests/sdk/devbox/test_streaming.py @@ -11,7 +11,7 @@ from types import SimpleNamespace from unittest.mock import Mock -from tests.sdk.conftest import THREAD_STARTUP_DELAY +from tests.sdk.conftest import THREAD_STARTUP_DELAY, mock_devbox_view from runloop_api_client.sdk import Devbox from runloop_api_client._streaming import Stream from runloop_api_client.sdk.execution import _StreamingGroup @@ -26,7 +26,7 @@ class TestDevboxStreaming: def test_start_streaming_no_callbacks(self, mock_client: Mock) -> None: """Test _start_streaming returns None when no callbacks.""" - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) result = devbox._start_streaming("exn_123", stdout=None, stderr=None, output=None) assert result is None @@ -34,7 +34,7 @@ def test_start_streaming_stdout_only(self, mock_client: Mock, mock_stream: Mock) """Test _start_streaming with stdout callback only.""" mock_client.devboxes.executions.stream_stdout_updates.return_value = mock_stream - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) stdout_calls: list[str] = [] result = devbox._start_streaming("exn_123", stdout=stdout_calls.append, stderr=None, output=None) @@ -47,7 +47,7 @@ def test_start_streaming_stderr_only(self, mock_client: Mock, mock_stream: Mock) """Test _start_streaming with stderr callback only.""" mock_client.devboxes.executions.stream_stderr_updates.return_value = mock_stream - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) stderr_calls: list[str] = [] result = devbox._start_streaming("exn_123", stdout=None, stderr=stderr_calls.append, output=None) @@ -61,7 +61,7 @@ def test_start_streaming_output_only(self, mock_client: Mock, mock_stream: Mock) mock_client.devboxes.executions.stream_stdout_updates.return_value = mock_stream mock_client.devboxes.executions.stream_stderr_updates.return_value = mock_stream - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) output_calls: list[str] = [] result = devbox._start_streaming("exn_123", stdout=None, stderr=None, output=output_calls.append) @@ -74,7 +74,7 @@ def test_start_streaming_all_callbacks(self, mock_client: Mock, mock_stream: Moc mock_client.devboxes.executions.stream_stdout_updates.return_value = mock_stream mock_client.devboxes.executions.stream_stderr_updates.return_value = mock_stream - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) stdout_calls: list[str] = [] stderr_calls: list[str] = [] output_calls: list[str] = [] @@ -104,7 +104,7 @@ def test_spawn_stream_thread( mock_stream.__enter__ = Mock(return_value=mock_stream) mock_stream.__exit__ = Mock(return_value=None) - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) stop_event = threading.Event() calls: list[str] = [] @@ -147,7 +147,7 @@ def test_spawn_stream_thread_stop_event( mock_stream.__enter__ = Mock(return_value=mock_stream) mock_stream.__exit__ = Mock(return_value=None) - devbox = Devbox(mock_client, "dbx_123") + devbox = Devbox(mock_client, mock_devbox_view()) stop_event = threading.Event() calls: list[str] = [] diff --git a/tests/sdk/test_async_ops.py b/tests/sdk/test_async_ops.py index 432cb706d..ee88b4306 100644 --- a/tests/sdk/test_async_ops.py +++ b/tests/sdk/test_async_ops.py @@ -110,16 +110,17 @@ async def test_create_from_snapshot(self, mock_async_client: AsyncMock, devbox_v call_kwargs = mock_async_client.devboxes.create_and_await_running.call_args[1] assert call_kwargs["snapshot_id"] == "snp_123" - def test_from_id(self, mock_async_client: AsyncMock) -> None: + @pytest.mark.asyncio + async def test_from_id(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None: """Test from_id method.""" + mock_async_client.devboxes.retrieve = AsyncMock(return_value=devbox_view) + ops = AsyncDevboxOps(mock_async_client) - devbox = ops.from_id("dbx_123") + devbox = await ops.from_id("dbx_123") assert isinstance(devbox, AsyncDevbox) assert devbox.id == "dbx_123" - # Verify from_id does not wait for running status - if hasattr(mock_async_client.devboxes, "await_running"): - assert not mock_async_client.devboxes.await_running.called + mock_async_client.devboxes.retrieve.assert_awaited_once_with("dbx_123") @pytest.mark.asyncio async def test_list_empty(self, mock_async_client: AsyncMock) -> None: diff --git a/tests/sdk/test_async_scenario_run.py b/tests/sdk/test_async_scenario_run.py index c034524a0..45159650f 100644 --- a/tests/sdk/test_async_scenario_run.py +++ b/tests/sdk/test_async_scenario_run.py @@ -24,12 +24,14 @@ def test_repr(self, mock_async_client: AsyncMock) -> None: run = AsyncScenarioRun(mock_async_client, "scr_123", "dbx_123") assert repr(run) == "" - def test_devbox_property(self, mock_async_client: AsyncMock) -> None: - """Test devbox property returns AsyncDevbox wrapper.""" + async def test_get_devbox(self, mock_async_client: AsyncMock, devbox_view: MockDevboxView) -> None: + """Test get_devbox method returns AsyncDevbox wrapper.""" + mock_async_client.devboxes.retrieve = AsyncMock(return_value=devbox_view) run = AsyncScenarioRun(mock_async_client, "scr_123", "dbx_123") - devbox = run.devbox + devbox = await run.get_devbox() assert devbox.id == "dbx_123" + mock_async_client.devboxes.retrieve.assert_awaited_once_with("dbx_123") async def test_get_info(self, mock_async_client: AsyncMock, scenario_run_view: MockScenarioRunView) -> None: """Test get_info method.""" diff --git a/tests/sdk/test_ops.py b/tests/sdk/test_ops.py index ff4075e77..b3ce485aa 100644 --- a/tests/sdk/test_ops.py +++ b/tests/sdk/test_ops.py @@ -111,6 +111,7 @@ def test_create_from_snapshot(self, mock_client: Mock, devbox_view: MockDevboxVi def test_from_id(self, mock_client: Mock, devbox_view: MockDevboxView) -> None: """Test from_id method waits for running.""" mock_client.devboxes.await_running.return_value = devbox_view + mock_client.devboxes.retrieve.return_value = devbox_view ops = DevboxOps(mock_client) devbox = ops.from_id("dbx_123") @@ -118,6 +119,7 @@ def test_from_id(self, mock_client: Mock, devbox_view: MockDevboxView) -> None: assert isinstance(devbox, Devbox) assert devbox.id == "dbx_123" mock_client.devboxes.await_running.assert_called_once_with("dbx_123") + mock_client.devboxes.retrieve.assert_called_once_with("dbx_123") def test_list_empty(self, mock_client: Mock) -> None: """Test list method with empty results.""" diff --git a/tests/sdk/test_scenario_run.py b/tests/sdk/test_scenario_run.py index 339e365f8..ba118f805 100644 --- a/tests/sdk/test_scenario_run.py +++ b/tests/sdk/test_scenario_run.py @@ -24,12 +24,15 @@ def test_repr(self, mock_client: Mock) -> None: run = ScenarioRun(mock_client, "scr_123", "dbx_123") assert repr(run) == "" - def test_devbox_property(self, mock_client: Mock) -> None: + def test_devbox_property(self, mock_client: Mock, devbox_view: MockDevboxView) -> None: """Test devbox property returns Devbox wrapper.""" + mock_client.devboxes.retrieve.return_value = devbox_view + run = ScenarioRun(mock_client, "scr_123", "dbx_123") devbox = run.devbox assert devbox.id == "dbx_123" + mock_client.devboxes.retrieve.assert_called_once_with("dbx_123") def test_get_info(self, mock_client: Mock, scenario_run_view: MockScenarioRunView) -> None: """Test get_info method.""" diff --git a/tests/smoketests/sdk/test_async_devbox.py b/tests/smoketests/sdk/test_async_devbox.py index dec172301..acc6171d3 100644 --- a/tests/smoketests/sdk/test_async_devbox.py +++ b/tests/smoketests/sdk/test_async_devbox.py @@ -630,7 +630,7 @@ async def test_get_devbox_by_id(self, async_sdk_client: AsyncRunloopSDK) -> None try: # Retrieve it by ID - retrieved = async_sdk_client.devbox.from_id(created.id) + retrieved = await async_sdk_client.devbox.from_id(created.id) assert retrieved.id == created.id # Verify it's the same devbox diff --git a/tests/smoketests/sdk/test_async_scenario.py b/tests/smoketests/sdk/test_async_scenario.py index 4faeae958..53708f890 100644 --- a/tests/smoketests/sdk/test_async_scenario.py +++ b/tests/smoketests/sdk/test_async_scenario.py @@ -133,7 +133,7 @@ async def test_scenario_run_async_lifecycle(self, async_sdk_client: AsyncRunloop await run.await_env_ready() # Access devbox - devbox = run.devbox + devbox = await run.get_devbox() info = await devbox.get_info() assert info.status == "running" @@ -169,7 +169,7 @@ async def test_scenario_run(self, async_sdk_client: AsyncRunloopSDK) -> None: assert run.devbox_id is not None # Devbox should be ready - devbox = run.devbox + devbox = await run.get_devbox() info = await devbox.get_info() assert info.status == "running"