openenvhack / tests /test_environment.py
aadi-joshi's picture
Initial commit
822ca51
Raw History Blame Contribute Delete
9.86 kB
"""Integration tests for DBEREnvironment -- step/reset/state API."""
import pytest
from db_er.models import DBERAction, DBERObservation, DBERState
from server.environment import DBEREnvironment
# Helpers
def make_action(action_type: str, **kwargs) -> DBERAction:
return DBERAction(action_type=action_type, **kwargs)
def sql(query: str) -> DBERAction:
return make_action("execute_sql", query=query)
def submit(notes: str = "done") -> DBERAction:
return make_action("submit_resolution", notes=notes)
# reset()
class TestReset:
def test_reset_returns_observation(self):
env = DBEREnvironment()
obs = env.reset(task_id=1)
assert isinstance(obs, DBERObservation)
assert obs.task_id == 1
assert obs.budget_remaining == 100
assert obs.step_count == 0
assert obs.done is False
assert "users" in obs.incident_ticket.lower() or "UNIQUE" in obs.incident_ticket
def test_reset_seed_picks_task(self):
env = DBEREnvironment()
obs = env.reset(seed=0)
assert obs.task_id == 1
obs = env.reset(seed=1)
assert obs.task_id == 2
obs = env.reset(seed=2)
assert obs.task_id == 3
def test_reset_overrides_task_id(self):
env = DBEREnvironment()
obs = env.reset(seed=0, task_id=3)
assert obs.task_id == 3
def test_reset_starts_with_violations(self):
env = DBEREnvironment()
obs1 = env.reset(task_id=1)
assert obs1.violation_count > 0 # 3 duplicate rows
def test_reset_gives_clean_state(self):
env = DBEREnvironment()
obs = env.reset(task_id=1)
# Take a step
env.step(sql("SELECT * FROM users"))
# Reset again - must return to clean state
obs2 = env.reset(task_id=1)
assert obs2.step_count == 0
assert obs2.budget_remaining == 100
def test_double_reset_isolated(self):
env = DBEREnvironment()
env.reset(task_id=1)
# Delete all users (should not persist after reset)
env.step(sql("DELETE FROM users WHERE id >= 9"))
obs2 = env.reset(task_id=1)
assert obs2.violation_count == 3 # duplicates back
def test_invalid_task_id_raises(self):
env = DBEREnvironment()
with pytest.raises(ValueError):
env.reset(task_id=99)
# step() - budget mechanics
class TestBudget:
def test_select_costs_1(self):
env = DBEREnvironment()
env.reset(task_id=1)
obs = env.step(sql("SELECT * FROM users"))
assert obs.budget_remaining == 99
def test_mutation_costs_5(self):
env = DBEREnvironment()
env.reset(task_id=1)
obs = env.step(sql("DELETE FROM users WHERE id = 9"))
assert obs.budget_remaining == 95
def test_pragma_costs_1(self):
env = DBEREnvironment()
env.reset(task_id=1)
obs = env.step(sql("PRAGMA table_info(users)"))
assert obs.budget_remaining == 99
def test_step_count_increments(self):
env = DBEREnvironment()
env.reset(task_id=1)
for i in range(3):
obs = env.step(sql("SELECT 1"))
assert obs.step_count == 3
def test_budget_exhaustion_ends_episode(self):
env = DBEREnvironment()
env.reset(task_id=1)
# Use up budget with mutations
for _ in range(20):
obs = env.step(sql("SELECT 1")) # 1 point each
# Keep draining
for _ in range(80):
obs = env.step(sql("SELECT 1"))
# At 0 budget, done should be True
assert obs.done is True or obs.budget_remaining <= 0
# step() - SQL execution
class TestSqlExecution:
def test_select_returns_rows(self):
env = DBEREnvironment()
env.reset(task_id=1)
obs = env.step(sql("SELECT id, email FROM users ORDER BY id"))
assert isinstance(obs.last_query_result, list)
assert len(obs.last_query_result) == 11 # 8 + 3 duplicates
def test_mutation_returns_rows_affected(self):
env = DBEREnvironment()
env.reset(task_id=1)
obs = env.step(sql("DELETE FROM users WHERE id = 9"))
assert obs.last_action_type == "MUTATION"
def test_bad_sql_returns_error(self):
env = DBEREnvironment()
env.reset(task_id=1)
obs = env.step(sql("SELECT * FROM nonexistent_table_xyz"))
assert obs.last_action_type == "ERROR"
assert "error" in obs.error_logs.lower() or "no such table" in obs.error_logs.lower()
def test_zero_row_delete_penalised(self):
env = DBEREnvironment()
env.reset(task_id=1)
# Delete a row that doesn't exist
obs = env.step(sql("DELETE FROM users WHERE id = 9999"))
assert obs.reward is not None and obs.reward < 0
def test_spam_penalty_on_repeated_select(self):
env = DBEREnvironment()
env.reset(task_id=1)
for _ in range(3):
obs = env.step(sql("SELECT COUNT(*) FROM users"))
# Third execution should incur spam penalty
assert obs.reward is not None and obs.reward <= 0
# step() - intermediate rewards
class TestIntermediateRewards:
def test_violation_reduction_gives_positive_reward(self):
env = DBEREnvironment()
env.reset(task_id=1)
# Delete 1 duplicate -> violation_count 3->2 -> +0.2 reward
obs = env.step(sql("DELETE FROM users WHERE id = 9"))
assert obs.reward is not None and obs.reward >= 0.2
def test_multiple_violation_reductions(self):
env = DBEREnvironment()
env.reset(task_id=1)
# Delete all 3 duplicates at once -> violation_count 3->0 -> +0.6
obs = env.step(sql("""
DELETE FROM users
WHERE id NOT IN (SELECT MIN(id) FROM users GROUP BY email)
"""))
assert obs.reward is not None and obs.reward >= 0.6
assert obs.violation_count == 0
# step() - safety violations
class TestSafetyViolations:
def test_drop_core_table_ends_episode(self):
env = DBEREnvironment()
env.reset(task_id=1)
obs = env.step(sql("DROP TABLE users"))
assert obs.done is True
assert obs.reward == -1.0
def test_drop_non_core_table_is_ok(self):
env = DBEREnvironment()
env.reset(task_id=2)
# Create and drop a temp table
env.step(sql("CREATE TABLE temp_work (id INTEGER PRIMARY KEY)"))
obs = env.step(sql("DROP TABLE temp_work"))
assert obs.done is False
assert obs.reward != -1.0
def test_bare_delete_ends_episode(self):
env = DBEREnvironment()
env.reset(task_id=1)
obs = env.step(sql("DELETE FROM users"))
assert obs.done is True
assert obs.reward == -1.0
def test_catastrophic_row_loss_ends_episode(self):
env = DBEREnvironment()
env.reset(task_id=1)
# Golden DB has 8 users. Safety floor = 90% of 8 = 7.2.
# Delete 9 of 11 rows -> 2 remain, which is < 7.2 - catastrophic.
obs = env.step(sql("DELETE FROM users WHERE id > 2"))
assert obs.done is True
assert obs.reward == -1.0
def test_safe_delete_with_where_does_not_end_episode(self):
env = DBEREnvironment()
env.reset(task_id=1)
# Delete the 3 duplicate rows (11->8). Golden floor = 8 rows.
# 8 >= 8*0.9 = 7.2 -> safe, no catastrophic loss.
obs = env.step(sql("DELETE FROM users WHERE id IN (9, 10, 11)"))
assert obs.done is False
# submit_resolution()
class TestSubmitResolution:
def test_submit_without_fix_gives_low_score(self):
env = DBEREnvironment()
env.reset(task_id=1)
obs = env.step(submit("Nothing fixed."))
assert obs.done is True
# Grader score for unrepaired DB should be < 1.0
# Final reward = score - 0.5 penalty (if score < 0.5)
assert obs.reward < 1.0
def test_submit_after_fix_gives_high_score(self):
env = DBEREnvironment()
env.reset(task_id=1)
env.step(sql("""
DELETE FROM users
WHERE id NOT IN (SELECT MIN(id) FROM users GROUP BY email)
"""))
obs = env.step(submit("Deleted duplicate rows."))
assert obs.done is True
assert obs.reward is not None and obs.reward >= 0.9
def test_submit_ends_episode(self):
env = DBEREnvironment()
env.reset(task_id=1)
obs = env.step(submit("done"))
assert obs.done is True
# Further steps should raise
with pytest.raises(RuntimeError):
env.step(sql("SELECT 1"))
# state property
class TestState:
def test_state_reflects_current_episode(self):
env = DBEREnvironment()
env.reset(task_id=2)
env.step(sql("SELECT * FROM vendors"))
state = env.state
assert isinstance(state, DBERState)
assert state.task_id == 2
assert state.budget_remaining == 99
assert state.step_count == 1
def test_state_tracks_violations(self):
env = DBEREnvironment()
env.reset(task_id=1)
state_before = env.state
import json
env.step(sql("DELETE FROM users WHERE id = 9"))
state_after = env.state
assert state_after.violation_count < state_before.violation_count
# close()
class TestClose:
def test_close_is_idempotent(self):
env = DBEREnvironment()
env.reset(task_id=1)
env.close()
env.close() # should not raise
def test_step_after_close_still_works(self):
# close() is a no-op (class-level shared state for HTTP compatibility),
# so step() after close() should succeed, not raise.
env = DBEREnvironment()
env.reset(task_id=1)
env.close()
obs = env.step(sql("SELECT 1"))
assert obs is not None