morphcloud 0.1.76__tar.gz → 0.1.78__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.
- {morphcloud-0.1.76 → morphcloud-0.1.78}/PKG-INFO +1 -1
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/experimental/browser.py +4 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/sandbox/__init__.py +1 -1
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/sandbox/_sandbox.py +43 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/pyproject.toml +1 -1
- morphcloud-0.1.78/tests/integration/test_sandbox.py +315 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/uv.lock +4 -4
- {morphcloud-0.1.76 → morphcloud-0.1.78}/.github/workflows/publish.yaml +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/.gitignore +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/LICENSE +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/Makefile +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/README.md +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/conf.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/examples/browser_example.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/examples/openai_agents_sdk/main.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/examples/pydantic_ai/main.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/index.rst +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/make.bat +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/__init__.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/_asyncify.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/_bash_interpreter.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/_llm.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/_scramble.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/_ssh.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/_utils.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/api.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/cli.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/computer/__init__.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/computer/_computer.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/experimental/__init__.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/morphcloud/py.typed +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/requirements.txt +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/scripts/increment_version.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/__init__.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/pytest.ini +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_as_container.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_branching.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_command_execution.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_file_operations.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_from_tag_multiple.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_http_service.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_metadata.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_simple.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_snapshot_operations.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_ssh_key_rotation.py +0 -0
- {morphcloud-0.1.76 → morphcloud-0.1.78}/tests/integration/test_ttl.py +0 -0
|
@@ -513,6 +513,7 @@ class BrowserSession:
|
|
|
513
513
|
|
|
514
514
|
# Start instance (don't use context manager to keep it running)
|
|
515
515
|
instance = snapshot.start(metadata={"name": name}, ttl_seconds=ttl_seconds)
|
|
516
|
+
instance.wait_until_ready()
|
|
516
517
|
|
|
517
518
|
# Verify Chrome installation
|
|
518
519
|
if verbose:
|
|
@@ -670,6 +671,9 @@ class BrowserSession:
|
|
|
670
671
|
return session
|
|
671
672
|
|
|
672
673
|
except Exception as e:
|
|
674
|
+
import traceback
|
|
675
|
+
|
|
676
|
+
print(traceback.format_exc(e))
|
|
673
677
|
raise RuntimeError(f"Failed to create browser session: {e}")
|
|
674
678
|
|
|
675
679
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
from ._sandbox import ExecutionResult, LanguageSupport, OutputType, Sandbox
|
|
1
|
+
from ._sandbox import ExecutionResult, LanguageSupport, OutputType, Sandbox, SandboxAPI
|
|
@@ -266,9 +266,52 @@ class Sandbox:
|
|
|
266
266
|
self._ws_connections = ws_connections
|
|
267
267
|
self._session_id = session_id
|
|
268
268
|
|
|
269
|
+
def _discover_existing_kernels(self) -> None:
|
|
270
|
+
"""
|
|
271
|
+
Discover and reconnect to existing kernels on the Jupyter server.
|
|
272
|
+
This preserves kernel state when getting an existing sandbox instance.
|
|
273
|
+
"""
|
|
274
|
+
try:
|
|
275
|
+
# Get list of existing kernels from Jupyter server
|
|
276
|
+
response = requests.get(f"{self.jupyter_url}/api/kernels", timeout=10.0)
|
|
277
|
+
response.raise_for_status()
|
|
278
|
+
existing_kernels = response.json()
|
|
279
|
+
|
|
280
|
+
# Map kernel specs to our supported languages
|
|
281
|
+
kernel_to_language = {}
|
|
282
|
+
for language in LanguageSupport.get_supported_languages():
|
|
283
|
+
kernel_name = LanguageSupport.get_kernel_name(language)
|
|
284
|
+
kernel_to_language[kernel_name] = language
|
|
285
|
+
|
|
286
|
+
# Connect to existing kernels that match our supported languages
|
|
287
|
+
for kernel_info in existing_kernels:
|
|
288
|
+
kernel_id = kernel_info.get("id")
|
|
289
|
+
kernel_spec = kernel_info.get("name")
|
|
290
|
+
|
|
291
|
+
if kernel_spec in kernel_to_language:
|
|
292
|
+
language = kernel_to_language[kernel_spec]
|
|
293
|
+
# Only connect if we don't already have a kernel for this language
|
|
294
|
+
if language not in self._kernel_ids:
|
|
295
|
+
self._kernel_ids[language] = kernel_id
|
|
296
|
+
# Connect WebSocket to existing kernel
|
|
297
|
+
try:
|
|
298
|
+
self._connect_websocket(kernel_id)
|
|
299
|
+
except ConnectionError:
|
|
300
|
+
# If we can't connect, remove from our tracking
|
|
301
|
+
del self._kernel_ids[language]
|
|
302
|
+
|
|
303
|
+
except requests.RequestException:
|
|
304
|
+
# If we can't discover existing kernels, that's okay
|
|
305
|
+
# New kernels will be created as needed
|
|
306
|
+
pass
|
|
307
|
+
except Exception:
|
|
308
|
+
# Any other error during discovery should not prevent connection
|
|
309
|
+
pass
|
|
310
|
+
|
|
269
311
|
def connect(self, timeout_seconds: int = 60) -> Sandbox:
|
|
270
312
|
"""Ensure Jupyter service is running and accessible"""
|
|
271
313
|
self.wait_for_jupyter(timeout_seconds)
|
|
314
|
+
self._discover_existing_kernels()
|
|
272
315
|
return self
|
|
273
316
|
|
|
274
317
|
def wait_for_jupyter(self, timeout: int = 60) -> bool:
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Function-scoped tests for Sandbox functionality in MorphCloud SDK.
|
|
3
|
+
"""
|
|
4
|
+
import pytest
|
|
5
|
+
import logging
|
|
6
|
+
import uuid
|
|
7
|
+
import os
|
|
8
|
+
import pytest_asyncio
|
|
9
|
+
|
|
10
|
+
from morphcloud.api import MorphCloudClient
|
|
11
|
+
from morphcloud.sandbox import Sandbox, SandboxAPI
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("morph-tests")
|
|
14
|
+
|
|
15
|
+
# Mark all tests as asyncio tests
|
|
16
|
+
pytestmark = pytest.mark.asyncio
|
|
17
|
+
|
|
18
|
+
# Configure pytest-asyncio
|
|
19
|
+
def pytest_configure(config):
|
|
20
|
+
config.option.asyncio_default_fixture_loop_scope = "function"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def api_key():
|
|
25
|
+
"""Get API key from environment variable."""
|
|
26
|
+
key = os.environ.get("MORPH_API_KEY")
|
|
27
|
+
if not key:
|
|
28
|
+
pytest.fail("MORPH_API_KEY environment variable must be set")
|
|
29
|
+
return key
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture
|
|
33
|
+
def base_url():
|
|
34
|
+
"""Get base URL from environment variable."""
|
|
35
|
+
return os.environ.get("MORPH_BASE_URL")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest_asyncio.fixture
|
|
39
|
+
async def client(api_key, base_url):
|
|
40
|
+
"""Create a MorphCloudClient."""
|
|
41
|
+
client = MorphCloudClient(api_key=api_key, base_url=base_url)
|
|
42
|
+
logger.info("Created MorphCloud client")
|
|
43
|
+
return client
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest_asyncio.fixture
|
|
47
|
+
async def sandbox_api(client):
|
|
48
|
+
"""Create a SandboxAPI instance."""
|
|
49
|
+
api = SandboxAPI(client)
|
|
50
|
+
logger.info("Created SandboxAPI")
|
|
51
|
+
return api
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def test_sandbox_creation_and_connection(client):
|
|
55
|
+
"""Test basic sandbox creation and connection."""
|
|
56
|
+
logger.info("Testing sandbox creation and connection")
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
# Create a new sandbox
|
|
60
|
+
sandbox = Sandbox.new(client=client, ttl_seconds=600)
|
|
61
|
+
logger.info(f"Created sandbox: {sandbox._instance.id}")
|
|
62
|
+
|
|
63
|
+
# Connect to the sandbox
|
|
64
|
+
sandbox.connect()
|
|
65
|
+
logger.info("Connected to sandbox successfully")
|
|
66
|
+
|
|
67
|
+
# Verify sandbox properties
|
|
68
|
+
assert sandbox._instance.id.startswith("morphvm_"), "Sandbox ID should start with 'morphvm_'"
|
|
69
|
+
assert sandbox.jupyter_url is not None, "Sandbox should have a Jupyter URL"
|
|
70
|
+
assert isinstance(sandbox._kernel_ids, dict), "Sandbox should have kernel_ids dictionary"
|
|
71
|
+
|
|
72
|
+
logger.info("Sandbox creation and connection test passed")
|
|
73
|
+
|
|
74
|
+
finally:
|
|
75
|
+
# Clean up
|
|
76
|
+
if 'sandbox' in locals():
|
|
77
|
+
try:
|
|
78
|
+
logger.info(f"Cleaning up sandbox {sandbox._instance.id}")
|
|
79
|
+
sandbox.close()
|
|
80
|
+
sandbox.shutdown()
|
|
81
|
+
logger.info("Sandbox cleaned up successfully")
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.error(f"Error cleaning up sandbox: {e}")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def test_sandbox_code_execution(client):
|
|
87
|
+
"""Test code execution in sandbox."""
|
|
88
|
+
logger.info("Testing sandbox code execution")
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
# Create and connect to sandbox
|
|
92
|
+
sandbox = Sandbox.new(client=client, ttl_seconds=600)
|
|
93
|
+
sandbox.connect()
|
|
94
|
+
logger.info(f"Created and connected to sandbox: {sandbox._instance.id}")
|
|
95
|
+
|
|
96
|
+
# Test Python code execution
|
|
97
|
+
test_value = uuid.uuid4().hex[:8]
|
|
98
|
+
result = sandbox.run_code(f"test_var = '{test_value}'", language="python")
|
|
99
|
+
assert result.success, f"Python code execution failed: {result.error}"
|
|
100
|
+
logger.info("Python variable assignment successful")
|
|
101
|
+
|
|
102
|
+
# Verify the variable was set
|
|
103
|
+
result = sandbox.run_code("print(test_var)", language="python")
|
|
104
|
+
assert result.success, f"Python variable retrieval failed: {result.error}"
|
|
105
|
+
assert test_value in result.text, f"Expected '{test_value}' in output, got: {result.text}"
|
|
106
|
+
logger.info("Python variable retrieval successful")
|
|
107
|
+
|
|
108
|
+
# Test JavaScript code execution
|
|
109
|
+
result = sandbox.run_code("console.log('hello from js');", language="javascript")
|
|
110
|
+
assert result.success, f"JavaScript code execution failed: {result.error}"
|
|
111
|
+
assert "hello from js" in result.text, f"Expected 'hello from js' in output, got: {result.text}"
|
|
112
|
+
logger.info("JavaScript code execution successful")
|
|
113
|
+
|
|
114
|
+
logger.info("Sandbox code execution test passed")
|
|
115
|
+
|
|
116
|
+
finally:
|
|
117
|
+
if 'sandbox' in locals():
|
|
118
|
+
try:
|
|
119
|
+
sandbox.close()
|
|
120
|
+
sandbox.shutdown()
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.error(f"Error cleaning up sandbox: {e}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def test_kernel_persistence_across_get_calls(client, sandbox_api):
|
|
126
|
+
"""Test that kernel state persists when using .get() to retrieve a sandbox."""
|
|
127
|
+
logger.info("Testing kernel persistence across .get() calls")
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
# Create and connect to first sandbox instance
|
|
131
|
+
sandbox1 = Sandbox.new(client=client, ttl_seconds=600)
|
|
132
|
+
sandbox1.connect()
|
|
133
|
+
logger.info(f"Created sandbox1: {sandbox1._instance.id}")
|
|
134
|
+
|
|
135
|
+
# Set a variable in Python
|
|
136
|
+
test_value = f"kernel_test_{uuid.uuid4().hex[:8]}"
|
|
137
|
+
result1 = sandbox1.run_code(f"persistent_var = '{test_value}'", language="python")
|
|
138
|
+
assert result1.success, f"Failed to set variable: {result1.error}"
|
|
139
|
+
logger.info(f"Set persistent_var to '{test_value}'")
|
|
140
|
+
|
|
141
|
+
# Get the kernel ID for verification
|
|
142
|
+
original_kernel_id = sandbox1._kernel_ids.get("python")
|
|
143
|
+
assert original_kernel_id is not None, "No Python kernel ID found"
|
|
144
|
+
logger.info(f"Original Python kernel ID: {original_kernel_id}")
|
|
145
|
+
|
|
146
|
+
# Retrieve the same sandbox using .get()
|
|
147
|
+
sandbox2 = sandbox_api.get(sandbox1._instance.id)
|
|
148
|
+
sandbox2.connect()
|
|
149
|
+
logger.info(f"Retrieved sandbox2 via .get(): {sandbox2._instance.id}")
|
|
150
|
+
|
|
151
|
+
# Verify kernel ID is preserved
|
|
152
|
+
retrieved_kernel_id = sandbox2._kernel_ids.get("python")
|
|
153
|
+
assert retrieved_kernel_id == original_kernel_id, (
|
|
154
|
+
f"Kernel IDs don't match: original={original_kernel_id}, "
|
|
155
|
+
f"retrieved={retrieved_kernel_id}"
|
|
156
|
+
)
|
|
157
|
+
logger.info("Kernel ID preservation verified")
|
|
158
|
+
|
|
159
|
+
# Verify variable state is preserved
|
|
160
|
+
result2 = sandbox2.run_code("print(persistent_var)", language="python")
|
|
161
|
+
assert result2.success, f"Failed to access persistent variable: {result2.error}"
|
|
162
|
+
assert test_value in result2.text, (
|
|
163
|
+
f"Persistent variable not found. Expected '{test_value}' in output: {result2.text}"
|
|
164
|
+
)
|
|
165
|
+
logger.info("Variable state preservation verified")
|
|
166
|
+
|
|
167
|
+
logger.info("Kernel persistence test passed")
|
|
168
|
+
|
|
169
|
+
finally:
|
|
170
|
+
# Clean up
|
|
171
|
+
for i, sandbox in enumerate([s for s in [locals().get('sandbox1'), locals().get('sandbox2')] if s]):
|
|
172
|
+
try:
|
|
173
|
+
logger.info(f"Cleaning up sandbox{i+1}: {sandbox._instance.id}")
|
|
174
|
+
sandbox.close()
|
|
175
|
+
if i == 0: # Only shutdown once
|
|
176
|
+
sandbox.shutdown()
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.error(f"Error cleaning up sandbox{i+1}: {e}")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def test_multiple_language_kernel_persistence(client, sandbox_api):
|
|
182
|
+
"""Test kernel persistence works for multiple programming languages."""
|
|
183
|
+
logger.info("Testing multiple language kernel persistence")
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
# Create and connect to sandbox
|
|
187
|
+
sandbox1 = Sandbox.new(client=client, ttl_seconds=600)
|
|
188
|
+
sandbox1.connect()
|
|
189
|
+
logger.info(f"Created sandbox1: {sandbox1._instance.id}")
|
|
190
|
+
|
|
191
|
+
# Set variables in different languages
|
|
192
|
+
python_value = f"py_{uuid.uuid4().hex[:8]}"
|
|
193
|
+
js_value = f"js_{uuid.uuid4().hex[:8]}"
|
|
194
|
+
|
|
195
|
+
python_result = sandbox1.run_code(f"py_var = '{python_value}'", language="python")
|
|
196
|
+
assert python_result.success, f"Python execution failed: {python_result.error}"
|
|
197
|
+
|
|
198
|
+
js_result = sandbox1.run_code(f"var js_var = '{js_value}';", language="javascript")
|
|
199
|
+
assert js_result.success, f"JavaScript execution failed: {js_result.error}"
|
|
200
|
+
|
|
201
|
+
logger.info(f"Set variables - Python: {python_value}, JavaScript: {js_value}")
|
|
202
|
+
|
|
203
|
+
# Store original kernel IDs
|
|
204
|
+
original_kernels = sandbox1._kernel_ids.copy()
|
|
205
|
+
logger.info(f"Original kernel IDs: {original_kernels}")
|
|
206
|
+
|
|
207
|
+
# Retrieve sandbox and verify all kernels are preserved
|
|
208
|
+
sandbox2 = sandbox_api.get(sandbox1._instance.id)
|
|
209
|
+
sandbox2.connect()
|
|
210
|
+
logger.info(f"Retrieved sandbox2: {sandbox2._instance.id}")
|
|
211
|
+
logger.info(f"Retrieved kernel IDs: {sandbox2._kernel_ids}")
|
|
212
|
+
|
|
213
|
+
# Check that kernel IDs match for all languages
|
|
214
|
+
for language, kernel_id in original_kernels.items():
|
|
215
|
+
retrieved_kernel_id = sandbox2._kernel_ids.get(language)
|
|
216
|
+
assert retrieved_kernel_id == kernel_id, (
|
|
217
|
+
f"Kernel ID mismatch for {language}: "
|
|
218
|
+
f"original={kernel_id}, retrieved={retrieved_kernel_id}"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Verify variables are accessible
|
|
222
|
+
py_check = sandbox2.run_code("print(py_var)", language="python")
|
|
223
|
+
assert py_check.success and python_value in py_check.text, (
|
|
224
|
+
f"Python variable not preserved: {py_check.text}"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
js_check = sandbox2.run_code("console.log(js_var);", language="javascript")
|
|
228
|
+
assert js_check.success and js_value in js_check.text, (
|
|
229
|
+
f"JavaScript variable not preserved: {js_check.text}"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
logger.info("Multiple language kernel persistence test passed")
|
|
233
|
+
|
|
234
|
+
finally:
|
|
235
|
+
# Clean up
|
|
236
|
+
for i, sandbox in enumerate([s for s in [locals().get('sandbox1'), locals().get('sandbox2')] if s]):
|
|
237
|
+
try:
|
|
238
|
+
sandbox.close()
|
|
239
|
+
if i == 0: # Only shutdown once
|
|
240
|
+
sandbox.shutdown()
|
|
241
|
+
except Exception as e:
|
|
242
|
+
logger.error(f"Error cleaning up sandbox{i+1}: {e}")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
async def test_kernel_discovery_with_fresh_sandbox(client, sandbox_api):
|
|
246
|
+
"""Test that kernel discovery handles fresh sandboxes correctly."""
|
|
247
|
+
logger.info("Testing kernel discovery with fresh sandbox")
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
# Create sandbox but don't connect yet
|
|
251
|
+
sandbox1 = Sandbox.new(client=client, ttl_seconds=600)
|
|
252
|
+
logger.info(f"Created fresh sandbox: {sandbox1._instance.id}")
|
|
253
|
+
|
|
254
|
+
# Get the sandbox without any prior kernel creation
|
|
255
|
+
sandbox2 = sandbox_api.get(sandbox1._instance.id)
|
|
256
|
+
sandbox2.connect()
|
|
257
|
+
logger.info("Connected to fresh sandbox via .get()")
|
|
258
|
+
|
|
259
|
+
# Should be able to run code (will create new kernel)
|
|
260
|
+
test_value = f"fresh_{uuid.uuid4().hex[:8]}"
|
|
261
|
+
result = sandbox2.run_code(f"print('{test_value}')", language="python")
|
|
262
|
+
assert result.success, f"Failed to run code on fresh sandbox: {result.error}"
|
|
263
|
+
assert test_value in result.text, f"Expected '{test_value}' in output: {result.text}"
|
|
264
|
+
|
|
265
|
+
logger.info("Fresh sandbox kernel discovery test passed")
|
|
266
|
+
|
|
267
|
+
finally:
|
|
268
|
+
# Clean up
|
|
269
|
+
for sandbox in [locals().get('sandbox1'), locals().get('sandbox2')]:
|
|
270
|
+
if sandbox:
|
|
271
|
+
try:
|
|
272
|
+
sandbox.close()
|
|
273
|
+
sandbox.shutdown()
|
|
274
|
+
break # Only shutdown once
|
|
275
|
+
except Exception as e:
|
|
276
|
+
logger.error(f"Error cleaning up sandbox: {e}")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
async def test_sandbox_error_handling(client):
|
|
280
|
+
"""Test error handling in sandbox operations."""
|
|
281
|
+
logger.info("Testing sandbox error handling")
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
# Create and connect to sandbox
|
|
285
|
+
sandbox = Sandbox.new(client=client, ttl_seconds=600)
|
|
286
|
+
sandbox.connect()
|
|
287
|
+
logger.info(f"Created sandbox: {sandbox._instance.id}")
|
|
288
|
+
|
|
289
|
+
# Test code with syntax error
|
|
290
|
+
result = sandbox.run_code("print('missing quote)", language="python")
|
|
291
|
+
assert not result.success, "Code with syntax error should fail"
|
|
292
|
+
assert result.error is not None, "Failed code should have error message"
|
|
293
|
+
logger.info("Syntax error handling verified")
|
|
294
|
+
|
|
295
|
+
# Test unsupported language
|
|
296
|
+
result = sandbox.run_code("print('test')", language="unsupported")
|
|
297
|
+
assert not result.success, "Unsupported language should fail"
|
|
298
|
+
assert "Unsupported language" in result.error, f"Expected unsupported language error: {result.error}"
|
|
299
|
+
logger.info("Unsupported language handling verified")
|
|
300
|
+
|
|
301
|
+
# Verify sandbox is still functional after errors
|
|
302
|
+
result = sandbox.run_code("print('still works')", language="python")
|
|
303
|
+
assert result.success, "Sandbox should remain functional after errors"
|
|
304
|
+
assert "still works" in result.text, f"Expected 'still works' in output: {result.text}"
|
|
305
|
+
logger.info("Sandbox resilience after errors verified")
|
|
306
|
+
|
|
307
|
+
logger.info("Sandbox error handling test passed")
|
|
308
|
+
|
|
309
|
+
finally:
|
|
310
|
+
if 'sandbox' in locals():
|
|
311
|
+
try:
|
|
312
|
+
sandbox.close()
|
|
313
|
+
sandbox.shutdown()
|
|
314
|
+
except Exception as e:
|
|
315
|
+
logger.error(f"Error cleaning up sandbox: {e}")
|
|
@@ -113,11 +113,11 @@ wheels = [
|
|
|
113
113
|
|
|
114
114
|
[[package]]
|
|
115
115
|
name = "certifi"
|
|
116
|
-
version = "2025.
|
|
116
|
+
version = "2025.7.9"
|
|
117
117
|
source = { registry = "https://pypi.org/simple" }
|
|
118
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
118
|
+
sdist = { url = "https://files.pythonhosted.org/packages/de/8a/c729b6b60c66a38f590c4e774decc4b2ec7b0576be8f1aa984a53ffa812a/certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079", size = 160386, upload-time = "2025-07-09T02:13:58.874Z" }
|
|
119
119
|
wheels = [
|
|
120
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
120
|
+
{ url = "https://files.pythonhosted.org/packages/66/f3/80a3f974c8b535d394ff960a11ac20368e06b736da395b551a49ce950cce/certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39", size = 159230, upload-time = "2025-07-09T02:13:57.007Z" },
|
|
121
121
|
]
|
|
122
122
|
|
|
123
123
|
[[package]]
|
|
@@ -576,7 +576,7 @@ wheels = [
|
|
|
576
576
|
|
|
577
577
|
[[package]]
|
|
578
578
|
name = "morphcloud"
|
|
579
|
-
version = "0.1.
|
|
579
|
+
version = "0.1.78"
|
|
580
580
|
source = { editable = "." }
|
|
581
581
|
dependencies = [
|
|
582
582
|
{ name = "anthropic" },
|
|
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
|
|
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
|
|
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
|
|
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
|