33from __future__ import annotations
44
55from types import SimpleNamespace
6- from unittest .mock import AsyncMock
6+ from unittest .mock import Mock , AsyncMock
77
88import pytest
99
@@ -45,6 +45,8 @@ def test_exit_code_none(self, mock_async_client: AsyncMock) -> None:
4545 exit_status = None ,
4646 stdout = "" ,
4747 stderr = "" ,
48+ stdout_truncated = False ,
49+ stderr_truncated = False ,
4850 )
4951 result = AsyncExecutionResult (mock_async_client , "dev_123" , execution ) # type: ignore[arg-type]
5052 assert result .exit_code is None
@@ -63,6 +65,8 @@ def test_success_false(self, mock_async_client: AsyncMock) -> None:
6365 exit_status = 1 ,
6466 stdout = "" ,
6567 stderr = "error" ,
68+ stdout_truncated = False ,
69+ stderr_truncated = False ,
6670 )
6771 result = AsyncExecutionResult (mock_async_client , "dev_123" , execution ) # type: ignore[arg-type]
6872 assert result .success is False
@@ -81,6 +85,8 @@ def test_failed_true(self, mock_async_client: AsyncMock) -> None:
8185 exit_status = 1 ,
8286 stdout = "" ,
8387 stderr = "error" ,
88+ stdout_truncated = False ,
89+ stderr_truncated = False ,
8490 )
8591 result = AsyncExecutionResult (mock_async_client , "dev_123" , execution ) # type: ignore[arg-type]
8692 assert result .failed is True
@@ -94,6 +100,8 @@ def test_failed_none(self, mock_async_client: AsyncMock) -> None:
94100 exit_status = None ,
95101 stdout = "" ,
96102 stderr = "" ,
103+ stdout_truncated = False ,
104+ stderr_truncated = False ,
97105 )
98106 result = AsyncExecutionResult (mock_async_client , "dev_123" , execution ) # type: ignore[arg-type]
99107 assert result .failed is False
@@ -115,6 +123,8 @@ async def test_stdout_empty(self, mock_async_client: AsyncMock) -> None:
115123 exit_status = 0 ,
116124 stdout = None ,
117125 stderr = "" ,
126+ stdout_truncated = False ,
127+ stderr_truncated = False ,
118128 )
119129 result = AsyncExecutionResult (mock_async_client , "dev_123" , execution ) # type: ignore[arg-type]
120130 assert await result .stdout () == ""
@@ -129,6 +139,8 @@ async def test_stderr(self, mock_async_client: AsyncMock) -> None:
129139 exit_status = 1 ,
130140 stdout = "" ,
131141 stderr = "error message" ,
142+ stdout_truncated = False ,
143+ stderr_truncated = False ,
132144 )
133145 result = AsyncExecutionResult (mock_async_client , "dev_123" , execution ) # type: ignore[arg-type]
134146 assert await result .stderr () == "error message"
@@ -144,3 +156,171 @@ def test_raw_property(self, mock_async_client: AsyncMock, execution_view: MockEx
144156 """Test raw property."""
145157 result = AsyncExecutionResult (mock_async_client , "dev_123" , execution_view ) # type: ignore[arg-type]
146158 assert result .raw == execution_view
159+
160+ @pytest .mark .asyncio
161+ async def test_stdout_with_truncation_and_streaming (
162+ self , mock_async_client : AsyncMock , mock_async_stream : AsyncMock
163+ ) -> None :
164+ """Test stdout streams full output when truncated."""
165+ from types import SimpleNamespace as SN
166+
167+ # Mock chunk data
168+ async def mock_iter ():
169+ yield SN (output = "line1\n " )
170+ yield SN (output = "line2\n " )
171+ yield SN (output = "line3\n " )
172+
173+ mock_async_stream .__aiter__ = Mock (return_value = mock_iter ())
174+
175+ # Setup client mock to return our stream
176+ mock_async_client .devboxes .executions .stream_stdout_updates = AsyncMock (return_value = mock_async_stream )
177+
178+ execution = SimpleNamespace (
179+ execution_id = "exec_123" ,
180+ devbox_id = "dev_123" ,
181+ status = "completed" ,
182+ exit_status = 0 ,
183+ stdout = "partial" ,
184+ stderr = "" ,
185+ stdout_truncated = True ,
186+ stderr_truncated = False ,
187+ )
188+ result = AsyncExecutionResult (mock_async_client , "dev_123" , execution ) # type: ignore[arg-type]
189+
190+ # Should stream full output
191+ output = await result .stdout ()
192+ assert output == "line1\n line2\n line3\n "
193+ mock_async_client .devboxes .executions .stream_stdout_updates .assert_called_once_with (
194+ "exec_123" , devbox_id = "dev_123"
195+ )
196+
197+ @pytest .mark .asyncio
198+ async def test_stderr_with_truncation_and_streaming (
199+ self , mock_async_client : AsyncMock , mock_async_stream : AsyncMock
200+ ) -> None :
201+ """Test stderr streams full output when truncated."""
202+ from types import SimpleNamespace as SN
203+
204+ # Mock chunk data
205+ async def mock_iter ():
206+ yield SN (output = "error1\n " )
207+ yield SN (output = "error2\n " )
208+
209+ mock_async_stream .__aiter__ = Mock (return_value = mock_iter ())
210+
211+ # Setup client mock to return our stream
212+ mock_async_client .devboxes .executions .stream_stderr_updates = AsyncMock (return_value = mock_async_stream )
213+
214+ execution = SimpleNamespace (
215+ execution_id = "exec_123" ,
216+ devbox_id = "dev_123" ,
217+ status = "completed" ,
218+ exit_status = 0 ,
219+ stdout = "" ,
220+ stderr = "partial error" ,
221+ stdout_truncated = False ,
222+ stderr_truncated = True ,
223+ )
224+ result = AsyncExecutionResult (mock_async_client , "dev_123" , execution ) # type: ignore[arg-type]
225+
226+ # Should stream full output
227+ output = await result .stderr ()
228+ assert output == "error1\n error2\n "
229+ mock_async_client .devboxes .executions .stream_stderr_updates .assert_called_once_with (
230+ "exec_123" , devbox_id = "dev_123"
231+ )
232+
233+ @pytest .mark .asyncio
234+ async def test_stdout_with_num_lines_when_truncated (
235+ self , mock_async_client : AsyncMock , mock_async_stream : AsyncMock
236+ ) -> None :
237+ """Test stdout with num_lines parameter when truncated."""
238+ from types import SimpleNamespace as SN
239+
240+ # Mock chunk data with many lines
241+ async def mock_iter ():
242+ yield SN (output = "line1\n line2\n line3\n " )
243+ yield SN (output = "line4\n line5\n " )
244+
245+ mock_async_stream .__aiter__ = Mock (return_value = mock_iter ())
246+
247+ # Setup client mock to return our stream
248+ mock_async_client .devboxes .executions .stream_stdout_updates = AsyncMock (return_value = mock_async_stream )
249+
250+ execution = SimpleNamespace (
251+ execution_id = "exec_123" ,
252+ devbox_id = "dev_123" ,
253+ status = "completed" ,
254+ exit_status = 0 ,
255+ stdout = "line1\n " ,
256+ stderr = "" ,
257+ stdout_truncated = True ,
258+ stderr_truncated = False ,
259+ )
260+ result = AsyncExecutionResult (mock_async_client , "dev_123" , execution ) # type: ignore[arg-type]
261+
262+ # Should stream and return last 2 lines
263+ output = await result .stdout (num_lines = 2 )
264+ assert output == "line4\n line5"
265+
266+ @pytest .mark .asyncio
267+ async def test_stdout_no_streaming_when_not_truncated (self , mock_async_client : AsyncMock ) -> None :
268+ """Test stdout doesn't stream when not truncated."""
269+ execution = SimpleNamespace (
270+ execution_id = "exec_123" ,
271+ devbox_id = "dev_123" ,
272+ status = "completed" ,
273+ exit_status = 0 ,
274+ stdout = "complete output" ,
275+ stderr = "" ,
276+ stdout_truncated = False ,
277+ stderr_truncated = False ,
278+ )
279+ result = AsyncExecutionResult (mock_async_client , "dev_123" , execution ) # type: ignore[arg-type]
280+
281+ # Should return existing output without streaming
282+ output = await result .stdout ()
283+ assert output == "complete output"
284+
285+ @pytest .mark .asyncio
286+ async def test_stdout_with_num_lines_no_truncation (self , mock_async_client : AsyncMock ) -> None :
287+ """Test stdout with num_lines when not truncated."""
288+ execution = SimpleNamespace (
289+ execution_id = "exec_123" ,
290+ devbox_id = "dev_123" ,
291+ status = "completed" ,
292+ exit_status = 0 ,
293+ stdout = "line1\n line2\n line3\n line4\n line5" ,
294+ stderr = "" ,
295+ stdout_truncated = False ,
296+ stderr_truncated = False ,
297+ )
298+ result = AsyncExecutionResult (mock_async_client , "dev_123" , execution ) # type: ignore[arg-type]
299+
300+ # Should return last 2 lines without streaming
301+ output = await result .stdout (num_lines = 2 )
302+ assert output == "line4\n line5"
303+
304+ def test_count_non_empty_lines (self , mock_async_client : AsyncMock , execution_view : MockExecutionView ) -> None :
305+ """Test the _count_non_empty_lines helper method."""
306+ result = AsyncExecutionResult (mock_async_client , "dev_123" , execution_view ) # type: ignore[arg-type]
307+
308+ # Test various input strings
309+ assert result ._count_non_empty_lines ("" ) == 0
310+ assert result ._count_non_empty_lines ("single" ) == 1
311+ assert result ._count_non_empty_lines ("line1\n line2" ) == 2
312+ assert result ._count_non_empty_lines ("line1\n line2\n " ) == 2
313+ assert result ._count_non_empty_lines ("line1\n \n line3" ) == 2 # Empty line in middle
314+ assert result ._count_non_empty_lines ("line1\n line2\n line3\n \n " ) == 3 # Trailing newlines
315+
316+ def test_get_last_n_lines (self , mock_async_client : AsyncMock , execution_view : MockExecutionView ) -> None :
317+ """Test the _get_last_n_lines helper method."""
318+ result = AsyncExecutionResult (mock_async_client , "dev_123" , execution_view ) # type: ignore[arg-type]
319+
320+ # Test various scenarios
321+ assert result ._get_last_n_lines ("" , 5 ) == ""
322+ assert result ._get_last_n_lines ("single" , 1 ) == "single"
323+ assert result ._get_last_n_lines ("line1\n line2\n line3" , 2 ) == "line2\n line3"
324+ assert result ._get_last_n_lines ("line1\n line2\n line3\n " , 2 ) == "line2\n line3"
325+ assert result ._get_last_n_lines ("line1\n line2" , 10 ) == "line1\n line2" # Request more than available
326+ assert result ._get_last_n_lines ("line1\n line2" , 0 ) == "" # Zero lines
0 commit comments