66
77import pytest
88
9- from runloop_api_client .sdk .async_snapshot import AsyncSnapshot
10- from runloop_api_client .sdk .async_blueprint import AsyncBlueprint
11- from runloop_api_client .sdk .async_scenario_builder import AsyncScenarioBuilder
9+ from runloop_api_client .sdk import AsyncSnapshot , AsyncBlueprint , AsyncScenarioBuilder
1210from runloop_api_client .types .scoring_function_param import ScorerTestBasedScoringFunctionTestFile
1311
1412
@@ -34,7 +32,7 @@ def mock_snapshot(self, mock_async_client: MagicMock) -> AsyncSnapshot:
3432 return AsyncSnapshot (mock_async_client , "snap-123" )
3533
3634 @pytest .fixture
37- def builder (self , mock_async_client : MagicMock ) -> AsyncScenarioBuilder :
35+ def mock_builder (self , mock_async_client : MagicMock ) -> AsyncScenarioBuilder :
3836 """Create an AsyncScenarioBuilder instance with mock client."""
3937 return AsyncScenarioBuilder ("test-scenario" , mock_async_client )
4038
@@ -48,113 +46,117 @@ def test_instantiation(self, mock_async_client: MagicMock) -> None:
4846 assert repr (builder ) == "<AsyncScenarioBuilder name='my-scenario'>"
4947
5048 def test_from_blueprint_and_snapshot (
51- self , builder : AsyncScenarioBuilder , mock_blueprint : AsyncBlueprint , mock_snapshot : AsyncSnapshot
49+ self , mock_builder : AsyncScenarioBuilder , mock_blueprint : AsyncBlueprint , mock_snapshot : AsyncSnapshot
5250 ) -> None :
5351 """Test blueprint/snapshot setting returns self and are mutually exclusive."""
5452 # from_blueprint returns self and sets blueprint
55- result = builder .from_blueprint (mock_blueprint )
56- assert result is builder
57- assert builder ._blueprint is mock_blueprint
58- assert builder ._snapshot is None
53+ result = mock_builder .from_blueprint (mock_blueprint )
54+ assert result is mock_builder
55+ assert mock_builder ._blueprint is mock_blueprint
56+ assert mock_builder ._snapshot is None
5957
6058 # from_snapshot returns self, sets snapshot, and clears blueprint
61- result = builder .from_snapshot (mock_snapshot )
62- assert result is builder
63- assert builder ._snapshot is mock_snapshot
64- assert builder ._blueprint is None
59+ result = mock_builder .from_snapshot (mock_snapshot )
60+ assert result is mock_builder
61+ assert mock_builder ._snapshot is mock_snapshot
62+ assert mock_builder ._blueprint is None
6563
6664 # from_blueprint clears snapshot
67- builder .from_blueprint (mock_blueprint )
68- assert builder ._blueprint is mock_blueprint
69- assert builder ._snapshot is None
65+ mock_builder .from_blueprint (mock_blueprint )
66+ assert mock_builder ._blueprint is mock_blueprint
67+ assert mock_builder ._snapshot is None
7068
71- def test_scorers (self , builder : AsyncScenarioBuilder ) -> None :
69+ def test_scorers (self , mock_builder : AsyncScenarioBuilder ) -> None :
7270 """Test all scorer types, optional params, and multiple scorers."""
7371 # Test scorer with test files
7472 test_files : list [ScorerTestBasedScoringFunctionTestFile ] = [
7573 {"file_path" : "test_main.py" , "file_contents" : "def test_foo(): pass" }
7674 ]
77- result = builder .add_test_command_scorer (
75+ result = mock_builder .add_test_command_scorer (
7876 "test-scorer" , test_command = "pytest" , weight = 2.0 , test_files = test_files
7977 )
80- assert result is builder
81- assert builder ._scorers [0 ]["name" ] == "test-scorer"
82- assert builder ._scorers [0 ]["weight" ] == 2.0
83- assert builder ._scorers [0 ]["scorer" ]["type" ] == "test_based_scorer"
84- assert builder ._scorers [0 ]["scorer" ].get ("test_command" ) == "pytest"
85- assert builder ._scorers [0 ]["scorer" ].get ("test_files" ) == test_files
78+ assert result is mock_builder
79+ assert mock_builder ._scorers [0 ]["name" ] == "test-scorer"
80+ assert mock_builder ._scorers [0 ]["weight" ] == 2.0
81+ assert mock_builder ._scorers [0 ]["scorer" ]["type" ] == "test_based_scorer"
82+ assert mock_builder ._scorers [0 ]["scorer" ].get ("test_command" ) == "pytest"
83+ assert mock_builder ._scorers [0 ]["scorer" ].get ("test_files" ) == test_files
8684
8785 # Command scorer
88- builder .add_shell_command_scorer ("cmd-scorer" , command = "./check.sh" )
89- assert builder ._scorers [1 ]["scorer" ]["type" ] == "command_scorer"
90- assert builder ._scorers [1 ]["scorer" ].get ("command" ) == "./check.sh"
86+ mock_builder .add_shell_command_scorer ("cmd-scorer" , command = "./check.sh" )
87+ assert mock_builder ._scorers [1 ]["scorer" ]["type" ] == "command_scorer"
88+ assert mock_builder ._scorers [1 ]["scorer" ].get ("command" ) == "./check.sh"
9189
9290 # Bash scorer
93- builder .add_bash_script_scorer ("bash-scorer" , bash_script = "echo 'score=1.0'" )
94- assert builder ._scorers [2 ]["scorer" ]["type" ] == "bash_script_scorer"
95- assert builder ._scorers [2 ]["scorer" ].get ("bash_script" ) == "echo 'score=1.0'"
91+ mock_builder .add_bash_script_scorer ("bash-scorer" , bash_script = "echo 'score=1.0'" )
92+ assert mock_builder ._scorers [2 ]["scorer" ]["type" ] == "bash_script_scorer"
93+ assert mock_builder ._scorers [2 ]["scorer" ].get ("bash_script" ) == "echo 'score=1.0'"
9694
9795 # Python scorer with optional params
98- builder .add_python_script_scorer (
96+ mock_builder .add_python_script_scorer (
9997 "python-scorer" ,
10098 python_script = "print('1.0')" ,
10199 python_version_constraint = ">=3.10" ,
102100 requirements_contents = "numpy" ,
103101 )
104- assert builder ._scorers [3 ]["scorer" ]["type" ] == "python_script_scorer"
105- assert builder ._scorers [3 ]["scorer" ].get ("python_version_constraint" ) == ">=3.10"
106- assert builder ._scorers [3 ]["scorer" ].get ("requirements_contents" ) == "numpy"
102+ assert mock_builder ._scorers [3 ]["scorer" ]["type" ] == "python_script_scorer"
103+ assert mock_builder ._scorers [3 ]["scorer" ].get ("python_version_constraint" ) == ">=3.10"
104+ assert mock_builder ._scorers [3 ]["scorer" ].get ("requirements_contents" ) == "numpy"
107105
108106 # AST grep scorer with optional lang
109- builder .add_ast_grep_scorer ("ast-scorer" , pattern = "$A.foo()" , search_directory = "/src" , lang = "python" )
110- assert builder ._scorers [4 ]["scorer" ]["type" ] == "ast_grep_scorer"
111- assert builder ._scorers [4 ]["scorer" ].get ("pattern" ) == "$A.foo()"
112- assert builder ._scorers [4 ]["scorer" ].get ("lang" ) == "python"
107+ mock_builder .add_ast_grep_scorer ("ast-scorer" , pattern = "$A.foo()" , search_directory = "/src" , lang = "python" )
108+ assert mock_builder ._scorers [4 ]["scorer" ]["type" ] == "ast_grep_scorer"
109+ assert mock_builder ._scorers [4 ]["scorer" ].get ("pattern" ) == "$A.foo()"
110+ assert mock_builder ._scorers [4 ]["scorer" ].get ("lang" ) == "python"
113111
114112 # Custom scorer with optional params
115- builder .add_custom_scorer ("custom-scorer" , custom_scorer_type = "my_scorer" , scorer_params = {"threshold" : 0.5 })
116- assert builder ._scorers [5 ]["scorer" ]["type" ] == "custom_scorer"
117- assert builder ._scorers [5 ]["scorer" ].get ("custom_scorer_type" ) == "my_scorer"
118- assert builder ._scorers [5 ]["scorer" ].get ("scorer_params" ) == {"threshold" : 0.5 }
113+ mock_builder .add_custom_scorer (
114+ "custom-scorer" , custom_scorer_type = "my_scorer" , scorer_params = {"threshold" : 0.5 }
115+ )
116+ assert mock_builder ._scorers [5 ]["scorer" ]["type" ] == "custom_scorer"
117+ assert mock_builder ._scorers [5 ]["scorer" ].get ("custom_scorer_type" ) == "my_scorer"
118+ assert mock_builder ._scorers [5 ]["scorer" ].get ("scorer_params" ) == {"threshold" : 0.5 }
119119
120120 # Verify multiple scorers accumulated
121- assert len (builder ._scorers ) == 6
121+ assert len (mock_builder ._scorers ) == 6
122122
123- def test_add_scorer_rejects_invalid_weight (self , builder : AsyncScenarioBuilder ) -> None :
123+ def test_add_scorer_rejects_invalid_weight (self , mock_builder : AsyncScenarioBuilder ) -> None :
124124 """Test that adding a scorer with zero or negative weight raises ValueError."""
125125 with pytest .raises (ValueError , match = "Scorer weight must be positive" ):
126- builder .add_bash_script_scorer ("bad" , bash_script = "echo 1" , weight = 0.0 )
126+ mock_builder .add_bash_script_scorer ("bad" , bash_script = "echo 1" , weight = 0.0 )
127127
128128 with pytest .raises (ValueError , match = "Scorer weight must be positive" ):
129- builder .add_bash_script_scorer ("bad" , bash_script = "echo 1" , weight = - 1.0 )
129+ mock_builder .add_bash_script_scorer ("bad" , bash_script = "echo 1" , weight = - 1.0 )
130130
131- def test_build_params_validation (self , builder : AsyncScenarioBuilder ) -> None :
131+ def test_build_params_validation (self , mock_builder : AsyncScenarioBuilder ) -> None :
132132 """Test _build_params raises for missing required fields."""
133133 # Missing problem statement
134- builder .add_test_command_scorer ("test" , test_command = "pytest" )
134+ mock_builder .add_test_command_scorer ("test" , test_command = "pytest" )
135135 with pytest .raises (ValueError , match = "Problem statement is required" ):
136- builder ._build_params ()
136+ mock_builder ._build_params ()
137137
138138 # Missing scorer (new builder)
139- builder2 = AsyncScenarioBuilder ("test2" , builder ._client )
139+ builder2 = AsyncScenarioBuilder ("test2" , mock_builder ._client )
140140 builder2 .with_problem_statement ("Fix the bug" )
141141 with pytest .raises (ValueError , match = "At least one scorer is required" ):
142142 builder2 ._build_params ()
143143
144- def test_build_params_with_all_options (self , builder : AsyncScenarioBuilder , mock_blueprint : AsyncBlueprint ) -> None :
144+ def test_build_params_with_all_options (
145+ self , mock_builder : AsyncScenarioBuilder , mock_blueprint : AsyncBlueprint
146+ ) -> None :
145147 """Test _build_params with all optional fields set."""
146- builder .with_problem_statement ("Fix the bug" )
147- builder .with_additional_context ({"hint" : "line 42" })
148- builder .add_test_command_scorer ("tests" , test_command = "pytest" )
149- builder .from_blueprint (mock_blueprint )
150- builder .with_working_directory ("/app" )
151- builder .with_metadata ({"team" : "infra" })
152- builder .with_reference_output ("diff content" )
153- builder .with_required_env_vars (["API_KEY" ])
154- builder .with_required_secrets (["db_pass" ])
155- builder .with_validation_type ("FORWARD" )
156-
157- params = builder ._build_params ()
148+ mock_builder .with_problem_statement ("Fix the bug" )
149+ mock_builder .with_additional_context ({"hint" : "line 42" })
150+ mock_builder .add_test_command_scorer ("tests" , test_command = "pytest" )
151+ mock_builder .from_blueprint (mock_blueprint )
152+ mock_builder .with_working_directory ("/app" )
153+ mock_builder .with_metadata ({"team" : "infra" })
154+ mock_builder .with_reference_output ("diff content" )
155+ mock_builder .with_required_env_vars (["API_KEY" ])
156+ mock_builder .with_required_secrets (["db_pass" ])
157+ mock_builder .with_validation_type ("FORWARD" )
158+
159+ params = mock_builder ._build_params ()
158160
159161 assert params ["name" ] == "test-scenario"
160162 assert params ["input_context" ]["problem_statement" ] == "Fix the bug"
@@ -169,14 +171,14 @@ def test_build_params_with_all_options(self, builder: AsyncScenarioBuilder, mock
169171 assert params .get ("required_secret_names" ) == ["db_pass" ]
170172 assert params .get ("validation_type" ) == "FORWARD"
171173
172- def test_build_params_normalizes_weights (self , builder : AsyncScenarioBuilder ) -> None :
174+ def test_build_params_normalizes_weights (self , mock_builder : AsyncScenarioBuilder ) -> None :
173175 """Test that _build_params normalizes scorer weights to sum to 1.0."""
174- builder .with_problem_statement ("Fix the bug" )
175- builder .add_bash_script_scorer ("scorer1" , bash_script = "echo 1" , weight = 1.0 )
176- builder .add_bash_script_scorer ("scorer2" , bash_script = "echo 2" , weight = 2.0 )
177- builder .add_bash_script_scorer ("scorer3" , bash_script = "echo 3" , weight = 3.0 )
176+ mock_builder .with_problem_statement ("Fix the bug" )
177+ mock_builder .add_bash_script_scorer ("scorer1" , bash_script = "echo 1" , weight = 1.0 )
178+ mock_builder .add_bash_script_scorer ("scorer2" , bash_script = "echo 2" , weight = 2.0 )
179+ mock_builder .add_bash_script_scorer ("scorer3" , bash_script = "echo 3" , weight = 3.0 )
178180
179- params = builder ._build_params ()
181+ params = mock_builder ._build_params ()
180182 scorers = list (params ["scoring_contract" ]["scoring_function_parameters" ])
181183
182184 # Weights 1, 2, 3 should normalize to 1/6, 2/6, 3/6
@@ -191,15 +193,15 @@ def test_build_params_normalizes_weights(self, builder: AsyncScenarioBuilder) ->
191193
192194 @pytest .mark .asyncio
193195 async def test_push_calls_api_and_returns_scenario (
194- self , builder : AsyncScenarioBuilder , mock_async_client : MagicMock
196+ self , mock_builder : AsyncScenarioBuilder , mock_async_client : MagicMock
195197 ) -> None :
196198 """Test push() calls API with correct params and returns AsyncScenario."""
197199 mock_async_client .scenarios .create .return_value .id = "scn-new-123"
198200
199- builder .with_problem_statement ("Fix the bug" )
200- builder .add_test_command_scorer ("tests" , test_command = "pytest" )
201+ mock_builder .with_problem_statement ("Fix the bug" )
202+ mock_builder .add_test_command_scorer ("tests" , test_command = "pytest" )
201203
202- scenario = await builder .push ()
204+ scenario = await mock_builder .push ()
203205
204206 mock_async_client .scenarios .create .assert_called_once ()
205207 call_kwargs = mock_async_client .scenarios .create .call_args .kwargs
@@ -208,10 +210,10 @@ async def test_push_calls_api_and_returns_scenario(
208210
209211 assert scenario .id == "scn-new-123"
210212
211- def test_fluent_chaining (self , builder : AsyncScenarioBuilder , mock_blueprint : AsyncBlueprint ) -> None :
213+ def test_fluent_chaining (self , mock_builder : AsyncScenarioBuilder , mock_blueprint : AsyncBlueprint ) -> None :
212214 """Test that all builder methods can be chained fluently."""
213215 result = (
214- builder .from_blueprint (mock_blueprint )
216+ mock_builder .from_blueprint (mock_blueprint )
215217 .with_working_directory ("/app" )
216218 .with_problem_statement ("Fix the bug" )
217219 .with_additional_context ({"hint" : "check main.py" })
@@ -223,8 +225,8 @@ def test_fluent_chaining(self, builder: AsyncScenarioBuilder, mock_blueprint: As
223225 .with_validation_type ("FORWARD" )
224226 )
225227
226- assert result is builder
227- assert builder ._blueprint is mock_blueprint
228- assert builder ._working_directory == "/app"
229- assert builder ._problem_statement == "Fix the bug"
230- assert len (builder ._scorers ) == 1
228+ assert result is mock_builder
229+ assert mock_builder ._blueprint is mock_blueprint
230+ assert mock_builder ._working_directory == "/app"
231+ assert mock_builder ._problem_statement == "Fix the bug"
232+ assert len (mock_builder ._scorers ) == 1
0 commit comments