44
55import pytest
66
7- from runloop_api_client .sdk import AsyncRunloopSDK
7+ from runloop_api_client .sdk import AsyncRunloopSDK , AsyncScenarioBuilder
8+ from tests .smoketests .utils import unique_name
9+ from runloop_api_client .types import ScenarioView
10+ from runloop_api_client .sdk ._types import SDKScenarioUpdateParams
11+ from runloop_api_client .sdk ._helpers import filter_params
812
913pytestmark = [pytest .mark .smoketest ]
1014
1115TWO_MINUTE_TIMEOUT = 120
1216FIVE_MINUTE_TIMEOUT = 300
17+ TEN_MINUTE_TIMEOUT = 600
18+
19+ # Metadata tag for all smoketest scenarios (for easy identification/cleanup)
20+ SMOKETEST_METADATA = {"smoketest" : "true" }
21+
22+
23+ async def push_or_update_scenario (sdk_client : AsyncRunloopSDK , builder : AsyncScenarioBuilder ) -> ScenarioView :
24+ """Push a new scenario or update existing one with the same name.
25+
26+ This is a workaround until scenario delete endpoint is available.
27+ Uses fixed scenario names to avoid littering the platform with test scenarios.
28+
29+ When updating an existing scenario, this function will delete the OLD blueprint/snapshot
30+ that's no longer needed (if different from the new one). The NEW blueprint/snapshot
31+ is kept so the scenario remains runnable.
32+ """
33+ # Check if scenario already exists
34+ scenarios = await sdk_client .scenario .list (name = builder .name , limit = 1 )
35+
36+ if scenarios :
37+ # Get old scenario info to find old blueprint/snapshot IDs
38+ scenario = scenarios [0 ]
39+ old_scenario_info = await scenario .get_info ()
40+ old_env = old_scenario_info .environment
41+ old_blueprint_id = old_env .blueprint_id if old_env else None
42+ old_snapshot_id = old_env .snapshot_id if old_env else None
43+
44+ # Get new blueprint/snapshot IDs from builder
45+ new_blueprint_id = builder ._blueprint .id if builder ._blueprint else None
46+ new_snapshot_id = builder ._snapshot .id if builder ._snapshot else None
47+
48+ # Update existing scenario with builder's params
49+ params = builder ._build_params ()
50+ result = await scenario .update (** filter_params (params , SDKScenarioUpdateParams ))
51+
52+ # Delete OLD blueprint/snapshot if they're being replaced
53+ if old_blueprint_id and old_blueprint_id != new_blueprint_id :
54+ await sdk_client .blueprint .from_id (old_blueprint_id ).delete ()
55+
56+ if old_snapshot_id and old_snapshot_id != new_snapshot_id :
57+ await sdk_client .snapshot .from_id (old_snapshot_id ).delete ()
58+
59+ return result
60+ else :
61+ # Create new scenario - keep the blueprint/snapshot (scenario needs them)
62+ scenario = await builder .push ()
63+ return await scenario .get_info ()
1364
1465
1566class TestAsyncScenarioRetrieval :
@@ -52,7 +103,7 @@ class TestAsyncScenarioRun:
52103 """Test async scenario run operations."""
53104
54105 @pytest .mark .timeout (FIVE_MINUTE_TIMEOUT )
55- async def test_scenario_run_lifecycle (self , async_sdk_client : AsyncRunloopSDK ) -> None :
106+ async def test_scenario_run_async_lifecycle (self , async_sdk_client : AsyncRunloopSDK ) -> None :
56107 """Test running a scenario and accessing the devbox.
57108
58109 This test:
@@ -63,7 +114,7 @@ async def test_scenario_run_lifecycle(self, async_sdk_client: AsyncRunloopSDK) -
63114 5. Cancels the run
64115 """
65116 # Find a scenario to run
66- scenarios = await async_sdk_client .scenario .list (limit = 5 )
117+ scenarios = await async_sdk_client .scenario .list (limit = 1 )
67118 if not scenarios :
68119 pytest .skip ("No scenarios available to test run" )
69120
@@ -72,6 +123,7 @@ async def test_scenario_run_lifecycle(self, async_sdk_client: AsyncRunloopSDK) -
72123
73124 # Start a run
74125 run = await scenario .run_async (run_name = "sdk-smoketest-async-run" )
126+ devbox = None
75127
76128 try :
77129 assert run .id is not None
@@ -82,7 +134,8 @@ async def test_scenario_run_lifecycle(self, async_sdk_client: AsyncRunloopSDK) -
82134
83135 # Access devbox
84136 devbox = run .devbox
85- assert devbox .id == run .devbox_id
137+ info = await devbox .get_info ()
138+ assert info .status == "running"
86139
87140 # Get run info
88141 info = await run .get_info ()
@@ -94,20 +147,22 @@ async def test_scenario_run_lifecycle(self, async_sdk_client: AsyncRunloopSDK) -
94147 try :
95148 await run .cancel ()
96149 except Exception :
97- pass # Best effort cleanup
150+ if devbox :
151+ await devbox .shutdown ()
98152
99153 @pytest .mark .timeout (FIVE_MINUTE_TIMEOUT )
100- async def test_scenario_run_and_await_env_ready (self , async_sdk_client : AsyncRunloopSDK ) -> None :
101- """Test run_and_await_env_ready convenience method."""
154+ async def test_scenario_run (self , async_sdk_client : AsyncRunloopSDK ) -> None :
155+ """Test run convenience method."""
102156 # Find a scenario to run
103- scenarios = await async_sdk_client .scenario .list (limit = 5 )
157+ scenarios = await async_sdk_client .scenario .list (limit = 1 )
104158 if not scenarios :
105159 pytest .skip ("No scenarios available to test run" )
106160
107161 scenario = scenarios [0 ]
108162
109163 # Start a run and wait for environment in one call
110164 run = await scenario .run (run_name = "sdk-smoketest-async-await" )
165+ devbox = None
111166
112167 try :
113168 assert run .id is not None
@@ -123,4 +178,85 @@ async def test_scenario_run_and_await_env_ready(self, async_sdk_client: AsyncRun
123178 try :
124179 await run .cancel ()
125180 except Exception :
126- pass
181+ if devbox :
182+ await devbox .shutdown ()
183+
184+
185+ class TestAsyncScenarioBuilder :
186+ """Test AsyncScenarioBuilder operations."""
187+
188+ @pytest .mark .timeout (TWO_MINUTE_TIMEOUT )
189+ async def test_scenario_builder_minimal (self , async_sdk_client : AsyncRunloopSDK ) -> None :
190+ """Test creating/updating a minimal scenario with just problem statement and scorer."""
191+ builder = (
192+ async_sdk_client .scenario .builder ("sdk-smoketest-async-builder-minimal" )
193+ .with_problem_statement ("Async minimal test problem statement" )
194+ .with_metadata (SMOKETEST_METADATA )
195+ .add_command_scorer ("async-minimal-scorer" , command = "echo 1.0" )
196+ )
197+
198+ info = await push_or_update_scenario (async_sdk_client , builder )
199+
200+ assert info .name == "sdk-smoketest-async-builder-minimal"
201+ assert info .input_context .problem_statement == "Async minimal test problem statement"
202+ assert len (info .scoring_contract .scoring_function_parameters ) == 1
203+ assert info .scoring_contract .scoring_function_parameters [0 ].name == "async-minimal-scorer"
204+
205+ @pytest .mark .timeout (FIVE_MINUTE_TIMEOUT )
206+ async def test_scenario_builder_with_blueprint (self , async_sdk_client : AsyncRunloopSDK ) -> None :
207+ """Test creating/updating a scenario from a blueprint.
208+ """
209+ blueprint = await async_sdk_client .blueprint .create (
210+ name = unique_name ("sdk-smoketest-async-scenario-bp" ),
211+ dockerfile = "FROM ubuntu:20.04" ,
212+ )
213+
214+ builder = (
215+ async_sdk_client .scenario .builder ("sdk-smoketest-async-builder-blueprint" )
216+ .from_blueprint (blueprint )
217+ .with_working_directory ("/home/user" )
218+ .with_problem_statement ("Async blueprint test problem" )
219+ .with_metadata (SMOKETEST_METADATA )
220+ .add_command_scorer ("async-blueprint-scorer" , command = "echo 1.0" )
221+ )
222+
223+ info = await push_or_update_scenario (async_sdk_client , builder )
224+
225+ assert info .name == "sdk-smoketest-async-builder-blueprint"
226+ assert info .input_context .problem_statement == "Async blueprint test problem"
227+ assert info .environment is not None
228+ assert info .environment .blueprint_id == blueprint .id
229+ assert info .environment .working_directory == "/home/user"
230+
231+ @pytest .mark .timeout (TEN_MINUTE_TIMEOUT )
232+ async def test_scenario_builder_with_snapshot (self , async_sdk_client : AsyncRunloopSDK ) -> None :
233+ """Test creating/updating a scenario from a snapshot.
234+ """
235+ # Create blueprint -> devbox -> snapshot chain
236+ blueprint = await async_sdk_client .blueprint .create (
237+ name = unique_name ("sdk-smoketest-async-scenario-snap-bp" ),
238+ dockerfile = "FROM ubuntu:20.04" ,
239+ )
240+ devbox = await async_sdk_client .devbox .create (blueprint_id = blueprint .id )
241+ snapshot = await devbox .snapshot_disk (name = unique_name ("sdk-smoketest-async-scenario-snap" ))
242+
243+ # Shut down the devbox - it's not needed after creating the snapshot
244+ try :
245+ await devbox .shutdown ()
246+ except Exception :
247+ pass
248+
249+ builder = (
250+ async_sdk_client .scenario .builder ("sdk-smoketest-async-builder-snapshot" )
251+ .from_snapshot (snapshot )
252+ .with_problem_statement ("Async snapshot test problem" )
253+ .with_metadata (SMOKETEST_METADATA )
254+ .add_command_scorer ("async-snapshot-scorer" , command = "echo 1.0" )
255+ )
256+
257+ info = await push_or_update_scenario (async_sdk_client , builder )
258+
259+ assert info .name == "sdk-smoketest-async-builder-snapshot"
260+ assert info .input_context .problem_statement == "Async snapshot test problem"
261+ assert info .environment is not None
262+ assert info .environment .snapshot_id == snapshot .id
0 commit comments