Skip to content

Commit 78e94a5

Browse files
committed
further consolidate scenario builder unit tests, make sure async coverage matches sync
1 parent 46b93e3 commit 78e94a5

2 files changed

Lines changed: 153 additions & 267 deletions

File tree

tests/sdk/test_async_scenario_builder.py

Lines changed: 100 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from runloop_api_client.sdk.async_snapshot import AsyncSnapshot
1010
from runloop_api_client.sdk.async_blueprint import AsyncBlueprint
1111
from runloop_api_client.sdk.async_scenario_builder import AsyncScenarioBuilder
12+
from runloop_api_client.types.scoring_function_param import ScorerTestBasedScoringFunctionTestFile
1213

1314

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

Comments
 (0)