99from runloop_api_client .sdk .async_snapshot import AsyncSnapshot
1010from runloop_api_client .sdk .async_blueprint import AsyncBlueprint
1111from runloop_api_client .sdk .async_scenario_builder import AsyncScenarioBuilder
12+ from runloop_api_client .types .scoring_function_param import ScorerTestBasedScoringFunctionTestFile
1213
1314
1415class TestAsyncScenarioBuilder :
@@ -37,17 +38,14 @@ def builder(self, mock_async_client: MagicMock) -> AsyncScenarioBuilder:
3738 """Create an AsyncScenarioBuilder instance with mock client."""
3839 return AsyncScenarioBuilder (mock_async_client , "test-scenario" )
3940
40- def test_init (self , mock_async_client : MagicMock ) -> None :
41- """Test builder initialization."""
41+ def test_instantiation (self , mock_async_client : MagicMock ) -> None :
42+ """Test builder initialization and repr ."""
4243 builder = AsyncScenarioBuilder (mock_async_client , "my-scenario" )
4344
4445 assert builder ._client is mock_async_client
4546 assert builder ._name == "my-scenario"
4647 assert builder .name == "my-scenario"
47-
48- def test_repr (self , builder : AsyncScenarioBuilder ) -> None :
49- """Test builder __repr__."""
50- assert repr (builder ) == "<AsyncScenarioBuilder name='test-scenario'>"
48+ assert repr (builder ) == "<AsyncScenarioBuilder name='my-scenario'>"
5149
5250 def test_from_blueprint_and_snapshot (
5351 self , builder : AsyncScenarioBuilder , mock_blueprint : AsyncBlueprint , mock_snapshot : AsyncSnapshot
@@ -70,89 +68,122 @@ def test_from_blueprint_and_snapshot(
7068 assert builder ._blueprint is mock_blueprint
7169 assert builder ._snapshot is None
7270
73- def test_with_working_directory_returns_self (self , builder : AsyncScenarioBuilder ) -> None :
74- """Test with_working_directory returns self for chaining."""
75- result = builder .with_working_directory ("/app" )
76-
77- assert result is builder
78- assert builder ._working_directory == "/app"
79-
80- def test_with_problem_statement_returns_self (self , builder : AsyncScenarioBuilder ) -> None :
81- """Test with_problem_statement returns self for chaining."""
82- result = builder .with_problem_statement ("Fix the bug" )
83-
84- assert result is builder
85- assert builder ._problem_statement == "Fix the bug"
86-
87- def test_add_test_scorer (self , builder : AsyncScenarioBuilder ) -> None :
88- """Test add_test_scorer method."""
89- result = builder .add_test_scorer (
90- "my-tests" ,
91- test_command = "pytest" ,
92- weight = 2.0 ,
93- )
94-
71+ def test_scorers (self , builder : AsyncScenarioBuilder ) -> None :
72+ """Test all scorer types, optional params, and multiple scorers."""
73+ # Test scorer with test files
74+ test_files : list [ScorerTestBasedScoringFunctionTestFile ] = [
75+ {"file_path" : "test_main.py" , "file_contents" : "def test_foo(): pass" }
76+ ]
77+ result = builder .add_test_scorer ("test-scorer" , test_command = "pytest" , weight = 2.0 , test_files = test_files )
9578 assert result is builder
96- assert len ( builder ._scorers ) == 1
97- assert builder ._scorers [0 ]["name " ] == "my-tests"
79+ assert builder ._scorers [ 0 ][ "name" ] == "test-scorer"
80+ assert builder ._scorers [0 ]["weight " ] == 2.0
9881 assert builder ._scorers [0 ]["scorer" ]["type" ] == "test_based_scorer"
99-
100- def test_add_command_scorer (self , builder : AsyncScenarioBuilder ) -> None :
101- """Test add_command_scorer method."""
102- result = builder .add_command_scorer (
103- "cmd-scorer" ,
104- command = "./check.sh" ,
82+ assert builder ._scorers [0 ]["scorer" ].get ("test_command" ) == "pytest"
83+ assert builder ._scorers [0 ]["scorer" ].get ("test_files" ) == test_files
84+
85+ # Command scorer
86+ builder .add_command_scorer ("cmd-scorer" , command = "./check.sh" )
87+ assert builder ._scorers [1 ]["scorer" ]["type" ] == "command_scorer"
88+ assert builder ._scorers [1 ]["scorer" ].get ("command" ) == "./check.sh"
89+
90+ # Bash scorer
91+ builder .add_bash_scorer ("bash-scorer" , bash_script = "echo 'score=1.0'" )
92+ assert builder ._scorers [2 ]["scorer" ]["type" ] == "bash_script_scorer"
93+ assert builder ._scorers [2 ]["scorer" ].get ("bash_script" ) == "echo 'score=1.0'"
94+
95+ # Python scorer with optional params
96+ builder .add_python_scorer (
97+ "python-scorer" ,
98+ python_script = "print('1.0')" ,
99+ python_version_constraint = ">=3.10" ,
100+ requirements_contents = "numpy" ,
105101 )
106-
107- assert result is builder
108- assert builder ._scorers [0 ]["scorer" ]["type" ] == "command_scorer"
109-
110- def test_add_bash_scorer (self , builder : AsyncScenarioBuilder ) -> None :
111- """Test add_bash_scorer method."""
112- result = builder .add_bash_scorer (
113- "bash-scorer" ,
114- bash_script = "echo 'score=1.0'" ,
115- )
116-
117- assert result is builder
118- assert builder ._scorers [0 ]["scorer" ]["type" ] == "bash_script_scorer"
119-
120- def test_build_params_missing_problem_statement (self , builder : AsyncScenarioBuilder ) -> None :
121- """Test _build_params raises if problem statement is missing."""
102+ assert builder ._scorers [3 ]["scorer" ]["type" ] == "python_script_scorer"
103+ assert builder ._scorers [3 ]["scorer" ].get ("python_version_constraint" ) == ">=3.10"
104+ assert builder ._scorers [3 ]["scorer" ].get ("requirements_contents" ) == "numpy"
105+
106+ # AST grep scorer with optional lang
107+ builder .add_ast_grep_scorer ("ast-scorer" , pattern = "$A.foo()" , search_directory = "/src" , lang = "python" )
108+ assert builder ._scorers [4 ]["scorer" ]["type" ] == "ast_grep_scorer"
109+ assert builder ._scorers [4 ]["scorer" ].get ("pattern" ) == "$A.foo()"
110+ assert builder ._scorers [4 ]["scorer" ].get ("lang" ) == "python"
111+
112+ # Custom scorer with optional params
113+ builder .add_custom_scorer ("custom-scorer" , custom_scorer_type = "my_scorer" , scorer_params = {"threshold" : 0.5 })
114+ assert builder ._scorers [5 ]["scorer" ]["type" ] == "custom_scorer"
115+ assert builder ._scorers [5 ]["scorer" ].get ("custom_scorer_type" ) == "my_scorer"
116+ assert builder ._scorers [5 ]["scorer" ].get ("scorer_params" ) == {"threshold" : 0.5 }
117+
118+ # Verify multiple scorers accumulated
119+ assert len (builder ._scorers ) == 6
120+
121+ def test_add_scorer_rejects_invalid_weight (self , builder : AsyncScenarioBuilder ) -> None :
122+ """Test that adding a scorer with zero or negative weight raises ValueError."""
123+ with pytest .raises (ValueError , match = "Scorer weight must be positive" ):
124+ builder .add_bash_scorer ("bad" , bash_script = "echo 1" , weight = 0.0 )
125+
126+ with pytest .raises (ValueError , match = "Scorer weight must be positive" ):
127+ builder .add_bash_scorer ("bad" , bash_script = "echo 1" , weight = - 1.0 )
128+
129+ def test_build_params_validation (self , builder : AsyncScenarioBuilder ) -> None :
130+ """Test _build_params raises for missing required fields."""
131+ # Missing problem statement
122132 builder .add_test_scorer ("test" , test_command = "pytest" )
123-
124133 with pytest .raises (ValueError , match = "Problem statement is required" ):
125134 builder ._build_params ()
126135
127- def test_build_params_missing_scorer (self , builder : AsyncScenarioBuilder ) -> None :
128- """Test _build_params raises if no scorers are added."""
129- builder .with_problem_statement ("Fix the bug" )
130-
136+ # Missing scorer (new builder)
137+ builder2 = AsyncScenarioBuilder (builder ._client , "test2" )
138+ builder2 .with_problem_statement ("Fix the bug" )
131139 with pytest .raises (ValueError , match = "At least one scorer is required" ):
132- builder ._build_params ()
140+ builder2 ._build_params ()
133141
134- def test_build_params_minimal (self , builder : AsyncScenarioBuilder ) -> None :
135- """Test _build_params with minimal configuration ."""
142+ def test_build_params_with_all_options (self , builder : AsyncScenarioBuilder , mock_blueprint : AsyncBlueprint ) -> None :
143+ """Test _build_params with all optional fields set ."""
136144 builder .with_problem_statement ("Fix the bug" )
145+ builder .with_additional_context ({"hint" : "line 42" })
137146 builder .add_test_scorer ("tests" , test_command = "pytest" )
147+ builder .from_blueprint (mock_blueprint )
148+ builder .with_working_directory ("/app" )
149+ builder .with_metadata ({"team" : "infra" })
150+ builder .with_reference_output ("diff content" )
151+ builder .with_required_env_vars (["API_KEY" ])
152+ builder .with_required_secrets (["db_pass" ])
153+ builder .with_validation_type ("FORWARD" )
138154
139155 params = builder ._build_params ()
140156
141157 assert params ["name" ] == "test-scenario"
142158 assert params ["input_context" ]["problem_statement" ] == "Fix the bug"
143- assert len (params ["scoring_contract" ]["scoring_function_parameters" ]) == 1
144-
145- def test_build_params_with_environment (self , builder : AsyncScenarioBuilder , mock_blueprint : AsyncBlueprint ) -> None :
146- """Test _build_params includes environment parameters."""
159+ assert params ["input_context" ]["additional_context" ] == {"hint" : "line 42" }
160+ assert params ["environment_parameters" ]["blueprint_id" ] == "bp-123"
161+ assert params ["environment_parameters" ]["working_directory" ] == "/app"
162+ assert params ["metadata" ] == {"team" : "infra" }
163+ assert params ["reference_output" ] == "diff content"
164+ assert params ["required_environment_variables" ] == ["API_KEY" ]
165+ assert params ["required_secret_names" ] == ["db_pass" ]
166+ assert params ["validation_type" ] == "FORWARD"
167+
168+ def test_build_params_normalizes_weights (self , builder : AsyncScenarioBuilder ) -> None :
169+ """Test that _build_params normalizes scorer weights to sum to 1.0."""
147170 builder .with_problem_statement ("Fix the bug" )
148- builder .add_test_scorer ( "tests " , test_command = "pytest" )
149- builder .from_blueprint ( mock_blueprint )
150- builder .with_working_directory ( "/app" )
171+ builder .add_bash_scorer ( "scorer1 " , bash_script = "echo 1" , weight = 1.0 )
172+ builder .add_bash_scorer ( "scorer2" , bash_script = "echo 2" , weight = 2.0 )
173+ builder .add_bash_scorer ( "scorer3" , bash_script = "echo 3" , weight = 3.0 )
151174
152175 params = builder ._build_params ()
176+ scorers = params ["scoring_contract" ]["scoring_function_parameters" ]
153177
154- assert params ["environment_parameters" ]["blueprint_id" ] == "bp-123"
155- assert params ["environment_parameters" ]["working_directory" ] == "/app"
178+ # Weights 1, 2, 3 should normalize to 1/6, 2/6, 3/6
179+ assert len (scorers ) == 3
180+ assert abs (scorers [0 ]["weight" ] - 1 / 6 ) < 0.0001
181+ assert abs (scorers [1 ]["weight" ] - 2 / 6 ) < 0.0001
182+ assert abs (scorers [2 ]["weight" ] - 3 / 6 ) < 0.0001
183+
184+ # Total should be 1.0
185+ total = sum (s ["weight" ] for s in scorers )
186+ assert abs (total - 1.0 ) < 0.0001
156187
157188 @pytest .mark .asyncio
158189 async def test_push_calls_api_and_returns_scenario (
0 commit comments