Spaces:
Sleeping
Sleeping
File size: 13,591 Bytes
6daf142 04e4b5b 6daf142 04e4b5b 6daf142 04e4b5b 1607c63 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 | """
Tests for the API Contract Validator Environment.
Run from the api_contract_validator/ directory:
pytest tests/ -v
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import pytest
from server.environment import ValidatorEnvironment
from server.spec_generator import generate_scenario_for_task, AVAILABLE_TASKS
from models import ValidatorAction
@pytest.fixture
def env():
"""Fresh environment for each test."""
return ValidatorEnvironment()
# ββ Task structure βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_six_tasks_registered():
assert len(AVAILABLE_TASKS) == 6
expected = {
"find_type_mismatches",
"validate_nested_objects",
"detect_breaking_changes",
"validate_response_schema",
"validate_cross_field_constraints",
"validate_auth_request",
}
assert set(AVAILABLE_TASKS) == expected
def test_all_tasks_have_violations():
for task_name in AVAILABLE_TASKS:
scenario = generate_scenario_for_task(task_name)
assert len(scenario.violations) >= 4, (
f"{task_name} has only {len(scenario.violations)} violations"
)
assert scenario.max_steps >= len(scenario.violations), (
f"{task_name}: max_steps({scenario.max_steps}) < violations({len(scenario.violations)})"
)
# ββ Reset behaviour ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_all_tasks_reset_cleanly(env):
for task_name in AVAILABLE_TASKS:
obs = env.reset(task_name=task_name)
assert obs.task_name == task_name
assert obs.done is False
assert obs.reward == 0.0
assert obs.violations_found == []
assert obs.violations_remaining > 0
# ββ Correct violation reward βββββββββββββββββββββββββββββββββββββββββββββββ
def test_correct_violation_gives_plus_one(env):
scenario = generate_scenario_for_task("find_type_mismatches")
env.reset(task_name="find_type_mismatches")
first = scenario.violations[0]
action = ValidatorAction(
field_path=first.field_path,
violation_type=first.violation_type,
description="test",
)
result = env.step(action)
assert result.reward == 1.0
assert len(result.violations_found) == 1
# ββ False positive penalty βββββββββββββββββββββββββββββββββββββββββββββββββ
def test_false_positive_gives_negative_reward(env):
env.reset(task_name="find_type_mismatches")
action = ValidatorAction(
field_path="nonexistent_field_xyz_abc",
violation_type="type_mismatch",
description="fabricated",
)
result = env.step(action)
assert result.reward == pytest.approx(-0.3)
# ββ Duplicate penalty ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_duplicate_gives_small_penalty(env):
scenario = generate_scenario_for_task("find_type_mismatches")
env.reset(task_name="find_type_mismatches")
first = scenario.violations[0]
action = ValidatorAction(
field_path=first.field_path,
violation_type=first.violation_type,
description="test",
)
result1 = env.step(action)
assert result1.reward == 1.0
result2 = env.step(action) # duplicate
assert result2.reward == pytest.approx(-0.1)
# ββ DONE signal ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_done_signal_ends_episode(env):
env.reset(task_name="find_type_mismatches")
action = ValidatorAction(field_path="DONE", violation_type="", description="")
result = env.step(action)
assert result.done is True
assert result.reward >= 0.0
# ββ HINT mechanic ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_hint_costs_half_point(env):
env.reset(task_name="find_type_mismatches")
action = ValidatorAction(field_path="HINT", violation_type="", description="")
result = env.step(action)
assert result.reward == pytest.approx(-0.5)
assert "Hint:" in result.feedback
assert result.done is False
# ββ Proximity reward βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_proximity_reward_for_correct_path_wrong_type(env):
scenario = generate_scenario_for_task("find_type_mismatches")
env.reset(task_name="find_type_mismatches")
first = scenario.violations[0]
action = ValidatorAction(
field_path=first.field_path,
violation_type="extra_field", # wrong type on purpose
description="proximity test",
)
result = env.step(action)
assert result.reward == pytest.approx(0.3)
# ββ Seed reproducibility βββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_seed_gives_same_scenario():
for task_name in AVAILABLE_TASKS:
s1 = generate_scenario_for_task(task_name, seed=42)
s2 = generate_scenario_for_task(task_name, seed=42)
assert [v.field_path for v in s1.violations] == [
v.field_path for v in s2.violations
], f"{task_name}: seed=42 gave different results across calls"
def test_different_seeds_give_different_easy_scenarios():
"""Easy task pool should vary with different seeds."""
paths_by_seed = set()
for seed in range(8):
s = generate_scenario_for_task("find_type_mismatches", seed=seed)
key = tuple(sorted(v.field_path for v in s.violations))
paths_by_seed.add(key)
assert len(paths_by_seed) > 1, "Different seeds produced identical scenarios"
# ββ Cross-field task βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_cross_field_task_has_seven_violations():
scenario = generate_scenario_for_task("validate_cross_field_constraints")
assert len(scenario.violations) == 7
def test_cross_field_violations_use_correct_type():
scenario = generate_scenario_for_task("validate_cross_field_constraints")
for v in scenario.violations:
assert v.violation_type == "cross_field_constraint", (
f"Expected cross_field_constraint, got {v.violation_type} for {v.field_path}"
)
# ββ Auth task ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_auth_task_has_six_violations():
scenario = generate_scenario_for_task("validate_auth_request")
assert len(scenario.violations) == 6
def test_auth_task_variants_differ():
s_even = generate_scenario_for_task("validate_auth_request", seed=0)
s_odd = generate_scenario_for_task("validate_auth_request", seed=1)
paths_even = {v.field_path for v in s_even.violations}
paths_odd = {v.field_path for v in s_odd.violations}
assert paths_even != paths_odd, "Even and odd seed should give different auth scenarios"
# ββ Easy pool expansion ββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_easy_pool_has_twelve_variants():
from server.spec_generator import _EASY_POOL
assert len(_EASY_POOL) == 12, f"Expected 12 pool entries, got {len(_EASY_POOL)}"
# ββ Phase 2 β impact tracing βββββββββββββββββββββββββββββββββββββββββββββββ
def test_phase2_reset_returns_service_graph(env):
obs = env.reset(task_name="trace_downstream_blast_radius", seed=1)
assert obs.phase == "tracing"
assert obs.total_consumers >= 3
assert "consumers" in obs.service_graph
assert obs.feedback
def test_phase2_perfect_trace_scores_high(env):
env.reset(task_name="trace_downstream_blast_radius", seed=1)
action = ValidatorAction(
action_type="trace_impact",
affected_services=[
"OrdersService",
"BillingService",
"NotificationsService",
],
)
result = env.step(action)
assert result.done is True
assert result.reward > 2.0 # 3 hits @ +0.8 each
assert env.state.score > 0.9
def test_phase2_false_flag_penalty(env):
env.reset(task_name="trace_downstream_blast_radius", seed=1)
action = ValidatorAction(
action_type="trace_impact",
affected_services=["OrdersService", "AnalyticsETL"], # one false flag
)
result = env.step(action)
assert result.done is True
# 1 hit (+0.8) + 2 missed (-0.5 each) + 1 false (-0.4) = -0.6
assert result.reward < 0
def test_phase2_unknown_service_treated_as_false_flag(env):
env.reset(task_name="trace_downstream_blast_radius", seed=1)
action = ValidatorAction(
action_type="trace_impact",
affected_services=["NonexistentService"],
)
result = env.step(action)
assert result.done is True
assert result.reward < 0
# ββ Phase 3 β fix proposal βββββββββββββββββββββββββββββββββββββββββββββββββ
def test_phase3_reset_returns_violation_and_consumers(env):
obs = env.reset(task_name="propose_backward_compat_fix", seed=1)
assert obs.phase == "fix_proposal"
assert obs.detected_violation
assert obs.consumer_specs
def test_phase3_good_field_alias_passes_all_consumers(env):
env.reset(task_name="propose_backward_compat_fix", seed=1)
action = ValidatorAction(
action_type="propose_fix",
fix_strategy="field_alias",
spec_patch={"aliases": {"email": "email_address"}},
rationale="Keep old field name as alias",
)
result = env.step(action)
assert result.done is True
assert result.reward >= 2.0
assert env.state.fix_validated is True
def test_phase3_malformed_strategy_penalty(env):
env.reset(task_name="propose_backward_compat_fix", seed=1)
action = ValidatorAction(
action_type="propose_fix",
fix_strategy="not_a_real_strategy",
spec_patch={},
)
result = env.step(action)
assert result.reward < 0
assert env.state.fix_validated is False
def test_phase3_breaking_consumer_penalised(env):
env.reset(task_name="propose_backward_compat_fix", seed=1)
# dual_write but missing the new field β breaks all consumers
action = ValidatorAction(
action_type="propose_fix",
fix_strategy="dual_write",
spec_patch={"emit_fields": ["email"]},
)
result = env.step(action)
assert result.reward < 0
assert env.state.fix_validated is False
# ββ Cascade β full workflow βββββββββββββββββββββββββββββββββββββββββββββββ
def test_cascade_starts_in_tracing_phase(env):
obs = env.reset(task_name="multi_service_cascade_fix", seed=1)
assert obs.phase == "tracing"
def test_cascade_transitions_to_fix_after_correct_trace(env):
env.reset(task_name="multi_service_cascade_fix", seed=1)
trace = ValidatorAction(
action_type="trace_impact",
affected_services=[
"OrdersService",
"BillingService",
"NotificationsService",
],
)
obs = env.step(trace)
assert obs.done is False
assert obs.phase == "fix_proposal"
fix = ValidatorAction(
action_type="propose_fix",
fix_strategy="field_alias",
spec_patch={"aliases": {"email": "email_address"}},
)
obs = env.step(fix)
assert obs.done is True
assert env.state.fix_validated is True
# ββ Determinism ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_phase2_seed_determinism(env):
obs1 = env.reset(task_name="trace_downstream_blast_radius", seed=1)
obs2 = env.reset(task_name="trace_downstream_blast_radius", seed=1)
services1 = sorted(c["name"] for c in obs1.service_graph["consumers"])
services2 = sorted(c["name"] for c in obs2.service_graph["consumers"])
assert services1 == services2
def test_different_seeds_pick_different_scenarios(env):
obs_even = env.reset(task_name="trace_downstream_blast_radius", seed=0)
obs_odd = env.reset(task_name="trace_downstream_blast_radius", seed=1)
name_even = obs_even.service_graph["producer"]
name_odd = obs_odd.service_graph["producer"]
assert name_even != name_odd
|