Skip to content

Commit 1bd29fe

Browse files
committed
add scenario creation smoketests, with push_or_update logic
1 parent 2cd01fb commit 1bd29fe

2 files changed

Lines changed: 289 additions & 18 deletions

File tree

tests/smoketests/sdk/test_async_scenario.py

Lines changed: 145 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,63 @@
44

55
import 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

913
pytestmark = [pytest.mark.smoketest]
1014

1115
TWO_MINUTE_TIMEOUT = 120
1216
FIVE_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

1566
class 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

Comments
 (0)