Spaces:
Runtime error
Runtime error
Add extreme final submission tests (48 tests: math, load, cross-file, adversarial, compliance)
4757a2e Download code-review-env/tests/test_extreme_final.py from DeepParmar/code-review: direct link, hf CLI and curl.
- Browser
- Download file 26.2 kB
-
https://huggingface.co/spaces/DeepParmar/code-review/resolve/main/code-review-env/tests/test_extreme_final.py
- Command line
-
hf download hf://spaces/DeepParmar/code-review/code-review-env/tests/test_extreme_final.py
-
curl -L -o test_extreme_final.py https://huggingface.co/spaces/DeepParmar/code-review/resolve/main/code-review-env/tests/test_extreme_final.py
26.2 kB
| """Extreme final submission tests β MATH, LOAD, CROSS-FILE, ADVERSARIAL, COMPLIANCE. | |
| Covers every test case from the submission spec: math scoring, load stress, cross-file | |
| matching, adversarial injection resistance, and OpenEnv compliance checks. | |
| """ | |
| from __future__ import annotations | |
| import sys | |
| import time | |
| from pathlib import Path | |
| from typing import Any, Dict, List | |
| import pytest | |
| from fastapi.testclient import TestClient | |
| # Ensure imports work | |
| _root = Path(__file__).resolve().parents[1] | |
| if str(_root) not in sys.path: | |
| sys.path.insert(0, str(_root)) | |
| from env.environment import CodeReviewEnv | |
| from env.models import CodeReviewAction, ReviewComment | |
| from server import app | |
| client = TestClient(app) | |
| # βββ HELPERS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _reset(task_id: str = "easy") -> Dict[str, Any]: | |
| r = client.post("/reset", json={"task_id": task_id}) | |
| assert r.status_code == 200, f"Reset failed: {r.text}" | |
| return r.json() | |
| def _step(action: Dict[str, Any]) -> Dict[str, Any]: | |
| r = client.post("/step", json=action) | |
| assert r.status_code in (200, 422), f"Step failed: {r.text}" | |
| return r.json() | |
| def _state() -> Dict[str, Any]: | |
| r = client.get("/state") | |
| assert r.status_code == 200 | |
| return r.json() | |
| # βββ MATH TESTS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestMath: | |
| """MATH-01 through MATH-10: scoring correctness.""" | |
| def test_math_01_partial_score_not_ceiling(self): | |
| """Partial score: find one easy bug, then done. Score NOT 0.999.""" | |
| _reset("easy") | |
| _step({"operation": "add_comment", "line_number": 18, "severity": "major", | |
| "category": "bug", "message": "Off-by-one: loop iterates full len."}) | |
| result = _step({"operation": "done"}) | |
| score = result["info"]["current_score"] | |
| assert 0.10 <= score <= 0.80, f"Expected partial score 0.10-0.80, got {score}" | |
| assert score != 0.999, "Score hit ceiling 0.999 on partial" | |
| def test_math_02_false_positives_reduce_score(self): | |
| """1 correct + 4 false positives β score < 0.40.""" | |
| _reset("medium") | |
| _step({"operation": "add_comment", "line_number": 20, "severity": "major", | |
| "category": "security", "message": "Hardcoded secret in source code."}) | |
| for ln in [1, 2, 3, 4]: | |
| _step({"operation": "add_comment", "line_number": ln, "severity": "major", | |
| "category": "bug", "message": "False positive noise."}) | |
| result = _step({"operation": "done"}) | |
| score = result["info"]["current_score"] | |
| assert score < 0.50, f"Expected score < 0.50 with 4 FPs, got {score}" | |
| def test_math_03_zero_bugs_floor(self): | |
| """Done immediately β score == 0.001.""" | |
| _reset("hard") | |
| result = _step({"operation": "done"}) | |
| score = result["info"]["current_score"] | |
| assert score == 0.001 or score <= 0.01, f"Expected floor score, got {score}" | |
| def test_math_04_perfect_hard_task(self): | |
| """Find all 6 real bugs with tier3 keywords β score >= 0.90.""" | |
| _reset("hard") | |
| _step({"operation": "add_comment", "line_number": 30, "severity": "critical", | |
| "category": "security", "message": "Unsafe YAML loading allows arbitrary code execution via untrusted input."}) | |
| _step({"operation": "add_comment", "line_number": 35, "severity": "critical", | |
| "category": "security", "message": "ECB mode reveals plaintext pattern in ciphertext, data exposure."}) | |
| _step({"operation": "add_comment", "line_number": 41, "severity": "major", | |
| "category": "bug", "message": "AsyncGenerator resource exhaustion and memory leak without aclose."}) | |
| _step({"operation": "add_comment", "line_number": 47, "severity": "critical", | |
| "category": "bug", "message": "Async data race with corrupted state via gather concurrent."}) | |
| _step({"operation": "add_comment", "line_number": 18, "severity": "critical", | |
| "category": "security", "message": "Hardcoded fallback secret key exposed in source code. Attacker can compromise."}) | |
| _step({"operation": "add_comment", "line_number": 26, "severity": "major", | |
| "category": "performance", "message": "Synchronous file write blocks event loop, concurrency degraded throughput."}) | |
| result = _step({"operation": "done"}) | |
| score = result["info"]["current_score"] | |
| assert score >= 0.90, f"Expected >= 0.90 for perfect hard, got {score}" | |
| def test_math_05_determinism(self): | |
| """5 identical runs must yield identical scores.""" | |
| scores = [] | |
| for _ in range(5): | |
| _reset("easy") | |
| _step({"operation": "add_comment", "line_number": 18, "severity": "major", | |
| "category": "bug", "message": "Off-by-one in loop bound."}) | |
| result = _step({"operation": "done"}) | |
| scores.append(result["info"]["current_score"]) | |
| assert len(set(scores)) == 1, f"Non-deterministic: {scores}" | |
| def test_math_06_more_bugs_higher_score(self): | |
| """More bugs found β higher score.""" | |
| # Run A: 1 bug | |
| _reset("easy") | |
| _step({"operation": "add_comment", "line_number": 18, "severity": "major", | |
| "category": "bug", "message": "Off-by-one in loop bound."}) | |
| result_a = _step({"operation": "done"}) | |
| score_a = result_a["info"]["current_score"] | |
| # Run B: 3 bugs | |
| _reset("easy") | |
| _step({"operation": "add_comment", "line_number": 18, "severity": "major", | |
| "category": "bug", "message": "Off-by-one in loop bound."}) | |
| _step({"operation": "add_comment", "line_number": 21, "severity": "major", | |
| "category": "bug", "message": "Missing null check: list elements may be None."}) | |
| _step({"operation": "add_comment", "line_number": 25, "severity": "minor", | |
| "category": "bug", "message": "Assignment used inside conditional instead of comparison."}) | |
| result_b = _step({"operation": "done"}) | |
| score_b = result_b["info"]["current_score"] | |
| assert score_b > score_a, f"Expected score_B({score_b}) > score_A({score_a})" | |
| def test_math_07_confidence_bonus_on_correct(self): | |
| """Correct add_comment with confidence=95 gives higher reward.""" | |
| # Without confidence | |
| _reset("easy") | |
| r1 = _step({"operation": "add_comment", "line_number": 18, "severity": "major", | |
| "category": "bug", "message": "Off-by-one in loop bound."}) | |
| reward_without = r1["reward"] | |
| # With high confidence | |
| _reset("easy") | |
| r2 = _step({"operation": "add_comment", "line_number": 18, "severity": "major", | |
| "category": "bug", "message": "Off-by-one in loop bound.", | |
| "confidence": 95}) | |
| reward_with = r2["reward"] | |
| assert reward_with >= reward_without, f"Expected confidence bonus: {reward_with} >= {reward_without}" | |
| def test_math_08_confidence_penalty_on_wrong(self): | |
| """Wrong add_comment with confidence=95 gives double penalty.""" | |
| _reset("easy") | |
| r1 = _step({"operation": "add_comment", "line_number": 1, "severity": "major", | |
| "category": "bug", "message": "False bug.", | |
| "confidence": 95}) | |
| reward = r1["reward"] | |
| # penalty is -0.10 base + -0.10 confidence = clamped to 0.01 | |
| assert reward == 0.01, f"Expected 0.01 (clamped), got {reward}" | |
| def test_math_09_red_herring_penalty(self): | |
| """Flagging red herring β reward 0.01 (clamped).""" | |
| _reset("hard") | |
| r = _step({"operation": "add_comment", "line_number": 54, "severity": "nit", | |
| "category": "style", "message": "Exception swallowed silently."}) | |
| reward = r["reward"] | |
| assert reward == 0.01, f"Expected 0.01 for red herring, got {reward}" | |
| def test_math_10_cross_file_correct_match(self): | |
| """add_comment on config_loader.py bug β positive reward.""" | |
| _reset("hard") | |
| r = _step({"operation": "add_comment", "line_number": 18, "severity": "critical", | |
| "category": "security", "message": "Hardcoded secret key exposed in source code."}) | |
| reward = r["reward"] | |
| assert reward > 0.01, f"Expected positive reward, got {reward}" | |
| # βββ LOAD TESTS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestLoad: | |
| """LOAD-01 through LOAD-04: stress and edge cases.""" | |
| def test_load_01_1000_false_positives(self): | |
| """1000 sequential false positives β no crash, score=0.001.""" | |
| _reset("easy") | |
| for i in range(1000): | |
| _step({"operation": "add_comment", "line_number": 1, "severity": "minor", | |
| "category": "style", "message": f"False positive #{i}"}) | |
| result = _step({"operation": "done"}) | |
| score = result["info"]["current_score"] | |
| assert score <= 0.01, f"Expected floor score with 1000 FPs, got {score}" | |
| def test_load_02_50_rapid_resets(self): | |
| """50 rapid resets with no state bleed.""" | |
| for i in range(50): | |
| obs = _reset("easy" if i % 3 == 0 else "medium" if i % 3 == 1 else "hard") | |
| assert obs["step_number"] == 1, f"Reset #{i}: step not 1" | |
| assert obs["existing_comments"] == [], f"Reset #{i}: comments not empty" | |
| def test_load_03_max_steps_enforcement(self): | |
| """Submit actions until max_steps hit β episode ends automatically.""" | |
| obs = _reset("easy") | |
| max_s = obs["max_steps"] | |
| done = False | |
| step_count = 0 | |
| for i in range(max_s + 5): | |
| r = _step({"operation": "add_comment", "line_number": 1, "severity": "minor", | |
| "category": "style", "message": f"Step {i}"}) | |
| step_count += 1 | |
| if r.get("done", False): | |
| done = True | |
| break | |
| assert done, f"Episode did not end after {step_count} steps" | |
| def test_load_04_concurrent_state_isolation(self): | |
| """Sequential task switches maintain clean state.""" | |
| _reset("easy") | |
| _step({"operation": "add_comment", "line_number": 18, "severity": "major", | |
| "category": "bug", "message": "Bug."}) | |
| _reset("medium") | |
| state = _state() | |
| assert state["bugs_found"] == 0, f"State bled from easy to medium" | |
| # βββ CROSS-FILE TESTS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestCrossFile: | |
| """CF-01 through CF-08: multi-file repository features.""" | |
| def test_cf_01_all_3_files_inspectable(self): | |
| """inspect_file for each of 3 files.""" | |
| obs = _reset("hard") | |
| for fname in ["crypto_service.py", "config_loader.py", "audit_logger.py"]: | |
| r = _step({"operation": "inspect_file", "filename": fname}) | |
| assert r.get("info", {}).get("error") is None, f"inspect_file({fname}) had error" | |
| def test_cf_02_inspect_lines_boundary(self): | |
| """inspect_lines within 40-line limit succeeds, over 40 fails.""" | |
| _reset("hard") | |
| # Within boundary | |
| r1 = _step({"operation": "inspect_lines", "filename": "crypto_service.py", | |
| "start_line": 1, "end_line": 40}) | |
| assert r1.get("info", {}).get("error") is None, "40-line range should succeed" | |
| _reset("hard") | |
| # Over boundary | |
| r2 = _step({"operation": "inspect_lines", "filename": "crypto_service.py", | |
| "start_line": 1, "end_line": 42}) | |
| assert r2.get("info", {}).get("error") is not None, "42-line range should error" | |
| def test_cf_03_cross_file_correct_file(self): | |
| """Bug in config_loader.py matched correctly.""" | |
| _reset("hard") | |
| r = _step({"operation": "add_comment", "line_number": 18, "severity": "critical", | |
| "category": "security", "message": "Hardcoded secret key exposed in source code."}) | |
| assert r["reward"] > 0.01, f"Expected positive reward for correct cross-file bug" | |
| def test_cf_04_cross_file_wrong_file_fallback(self): | |
| """Line 18 on crypto_service.py (no such bug) β uses fallback matching.""" | |
| _reset("hard") | |
| # Line 18 in crypto_service.py is NOT a bug line per ground truth | |
| # (ground truth bugs on crypto_service.py are lines 30, 35, 41, 47, 54) | |
| # Due to fallback matching, line 18 from config_loader.py may match | |
| r = _step({"operation": "add_comment", "line_number": 18, "severity": "critical", | |
| "category": "security", "message": "Hardcoded secret key exposed in source code."}) | |
| # This should still match via fallback | |
| assert r["reward"] > 0.01, "Fallback matching should find config_loader bug" | |
| def test_cf_05_no_filename_backward_compat(self): | |
| """add_comment without filename on hard task β searches all files.""" | |
| _reset("hard") | |
| r = _step({"operation": "add_comment", "line_number": 30, "severity": "critical", | |
| "category": "security", "message": "Unsafe YAML loading allows arbitrary code execution."}) | |
| assert r["reward"] > 0.01, "No-filename backward compat should match by line" | |
| def test_cf_06_nonexistent_file(self): | |
| """inspect_file(nonexistent.py) β error, no crash.""" | |
| _reset("hard") | |
| r = _step({"operation": "inspect_file", "filename": "nonexistent.py"}) | |
| assert r.get("info", {}).get("error") is not None, "Nonexistent file should error" | |
| def test_cf_07_available_files_present(self): | |
| """reset(hard) β observation has available_files.""" | |
| obs = _reset("hard") | |
| assert obs.get("available_files") is not None, "available_files missing" | |
| assert set(obs["available_files"]) == {"crypto_service.py", "config_loader.py", "audit_logger.py"} | |
| def test_cf_08_repository_files_present(self): | |
| """reset(hard) β observation has repository_files.""" | |
| obs = _reset("hard") | |
| assert obs.get("repository_files") is not None, "repository_files missing" | |
| assert "crypto_service.py" in obs["repository_files"] | |
| assert "config_loader.py" in obs["repository_files"] | |
| assert "audit_logger.py" in obs["repository_files"] | |
| # βββ ADVERSARIAL TESTS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestAdversarial: | |
| """ATK-01 through ATK-15: adversarial injection and edge cases.""" | |
| def test_atk_01_unicode_injection(self): | |
| """Unicode in message field.""" | |
| _reset("easy") | |
| r = _step({"operation": "add_comment", "line_number": 1, "severity": "major", | |
| "category": "bug", "message": "ECB ε ε―ζ¨‘εΌ π unsafe"}) | |
| assert "error" not in r or r.get("info", {}).get("error") is None or True # no crash | |
| def test_atk_02_sql_injection_message(self): | |
| """SQL injection in message field.""" | |
| _reset("easy") | |
| r = _step({"operation": "add_comment", "line_number": 1, "severity": "major", | |
| "category": "bug", "message": "'; DROP TABLE bugs; --"}) | |
| assert r is not None # no crash | |
| def test_atk_03_script_injection(self): | |
| """Script tag in message field.""" | |
| _reset("easy") | |
| r = _step({"operation": "add_comment", "line_number": 1, "severity": "major", | |
| "category": "bug", "message": "<script>alert('xss')</script>"}) | |
| assert r is not None # no crash | |
| def test_atk_04_extremely_long_message(self): | |
| """10000 char message.""" | |
| _reset("easy") | |
| r = _step({"operation": "add_comment", "line_number": 1, "severity": "major", | |
| "category": "bug", "message": "a" * 10000}) | |
| assert r is not None # no crash | |
| def test_atk_05_null_bytes_message(self): | |
| """Null bytes in message.""" | |
| _reset("easy") | |
| r = _step({"operation": "add_comment", "line_number": 1, "severity": "major", | |
| "category": "bug", "message": "bug\x00found"}) | |
| assert r is not None # no crash | |
| def test_atk_06_negative_line_number(self): | |
| """Negative line number β 422 validation error.""" | |
| _reset("easy") | |
| r = client.post("/step", json={"operation": "add_comment", "line_number": -1, | |
| "severity": "major", "category": "bug", "message": "Neg line"}) | |
| assert r.status_code == 422, f"Expected 422 for negative line, got {r.status_code}" | |
| def test_atk_07_huge_line_number(self): | |
| """Line number 999999 β false positive, no crash.""" | |
| _reset("easy") | |
| r = _step({"operation": "add_comment", "line_number": 999999, "severity": "major", | |
| "category": "bug", "message": "Huge line"}) | |
| assert r is not None # no crash | |
| def test_atk_08_confidence_over_100(self): | |
| """Confidence=101 β 422 validation error.""" | |
| _reset("easy") | |
| r = client.post("/step", json={"operation": "add_comment", "line_number": 1, | |
| "severity": "major", "category": "bug", | |
| "message": "Test", "confidence": 101}) | |
| assert r.status_code == 422, f"Expected 422 for confidence=101, got {r.status_code}" | |
| def test_atk_09_confidence_negative(self): | |
| """Confidence=-1 β 422 validation error.""" | |
| _reset("easy") | |
| r = client.post("/step", json={"operation": "add_comment", "line_number": 1, | |
| "severity": "major", "category": "bug", | |
| "message": "Test", "confidence": -1}) | |
| assert r.status_code == 422, f"Expected 422 for confidence=-1, got {r.status_code}" | |
| def test_atk_10_empty_message(self): | |
| """Empty message β 422 or false positive.""" | |
| _reset("easy") | |
| r = client.post("/step", json={"operation": "add_comment", "line_number": 1, | |
| "severity": "major", "category": "bug", "message": ""}) | |
| # Model requires min_length=1 on message | |
| assert r.status_code == 422, f"Expected 422 for empty message, got {r.status_code}" | |
| def test_atk_11_malformed_json(self): | |
| """Malformed JSON body β 422, not 500.""" | |
| _reset("easy") | |
| r = client.post("/step", content=b"{bad json", headers={"Content-Type": "application/json"}) | |
| assert r.status_code == 422, f"Expected 422 for malformed JSON, got {r.status_code}" | |
| def test_atk_12_missing_required_fields(self): | |
| """Missing required fields β 422, not 500.""" | |
| _reset("easy") | |
| r = client.post("/step", json={}) | |
| assert r.status_code == 422, f"Expected 422 for empty body, got {r.status_code}" | |
| def test_atk_13_step_after_done(self): | |
| """Step after episode done β graceful error or re-step.""" | |
| _reset("easy") | |
| _step({"operation": "done"}) | |
| # Second step should not crash | |
| r = client.post("/step", json={"operation": "add_comment", "line_number": 1, | |
| "severity": "major", "category": "bug", "message": "Late"}) | |
| assert r.status_code in (200, 422, 500) # must not crash | |
| def test_atk_14_reset_mid_episode(self): | |
| """Reset mid-episode β clean new state.""" | |
| _reset("easy") | |
| _step({"operation": "add_comment", "line_number": 18, "severity": "major", | |
| "category": "bug", "message": "Bug."}) | |
| obs = _reset("medium") | |
| assert obs["task_id"] == "medium" | |
| assert obs["existing_comments"] == [] | |
| state = _state() | |
| assert state["bugs_found"] == 0 | |
| def test_atk_15_red_herring_high_confidence(self): | |
| """Red herring with confidence=99 β clamped 0.01 reward.""" | |
| _reset("hard") | |
| r = _step({"operation": "add_comment", "line_number": 54, "severity": "nit", | |
| "category": "style", "message": "Exception swallowed silently.", | |
| "confidence": 99}) | |
| assert r["reward"] == 0.01, f"Expected 0.01 for red herring + high conf, got {r['reward']}" | |
| # βββ OPENENV COMPLIANCE TESTS ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestOpenEnvCompliance: | |
| """OE-01 through OE-05: OpenEnv spec compliance.""" | |
| def test_oe_02_observation_fields_easy(self): | |
| """reset(easy) returns all required observation fields.""" | |
| obs = _reset("easy") | |
| required = ["task_id", "pr_title", "pr_description", "full_file", | |
| "code_diff", "existing_comments", "step_number", "max_steps"] | |
| for field in required: | |
| assert field in obs, f"Missing field: {field}" | |
| def test_oe_02_observation_fields_hard(self): | |
| """reset(hard) returns repository_files and available_files.""" | |
| obs = _reset("hard") | |
| assert "repository_files" in obs | |
| assert "available_files" in obs | |
| def test_oe_03_score_always_valid_range(self): | |
| """Run 50 random sequences, ensure all rewards in valid range.""" | |
| import random | |
| for _ in range(50): | |
| task = random.choice(["easy", "medium", "hard"]) | |
| _reset(task) | |
| for _ in range(random.randint(0, 3)): | |
| r = _step({"operation": "add_comment", "line_number": random.randint(1, 50), | |
| "severity": random.choice(["critical", "major", "minor", "nit"]), | |
| "category": random.choice(["bug", "security", "performance", "style"]), | |
| "message": "Random test comment."}) | |
| reward = r["reward"] | |
| assert 0.01 <= reward <= 0.99, f"Reward out of range: {reward}" | |
| result = _step({"operation": "done"}) | |
| reward = result["reward"] | |
| score = result["info"]["current_score"] | |
| assert 0.01 <= reward <= 0.99, f"Final reward out of range: {reward}" | |
| assert 0.001 <= score <= 0.999, f"Score out of range: {score}" | |
| def test_oe_04_baseline_reproducible(self): | |
| """Two identical benchmark runs produce identical scores.""" | |
| scores = [] | |
| for _ in range(2): | |
| _reset("medium") | |
| _step({"operation": "add_comment", "line_number": 20, "severity": "major", | |
| "category": "security", "message": "Hardcoded secret in source code."}) | |
| _step({"operation": "add_comment", "line_number": 21, "severity": "critical", | |
| "category": "security", "message": "SQL injection via string concatenation."}) | |
| _step({"operation": "add_comment", "line_number": 23, "severity": "major", | |
| "category": "security", "message": "XSS: untrusted input in HTML."}) | |
| _step({"operation": "add_comment", "line_number": 24, "severity": "critical", | |
| "category": "security", "message": "IDOR: missing authorization check."}) | |
| result = _step({"operation": "done"}) | |
| scores.append(result["info"]["current_score"]) | |
| assert scores[0] == scores[1], f"Non-reproducible: {scores}" | |
| def test_oe_05_runtime_under_10s_per_task(self): | |
| """Benchmark mode completes quickly (not real LLM, just env).""" | |
| t0 = time.time() | |
| for task in ["easy", "medium", "hard"]: | |
| _reset(task) | |
| _step({"operation": "done"}) | |
| elapsed = time.time() - t0 | |
| assert elapsed < 10.0, f"Too slow: {elapsed:.2f}s" | |
| # βββ ENDPOINT TESTS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestEndpoints: | |
| """Health and state endpoint testing.""" | |
| def test_health_returns_200(self): | |
| r = client.get("/health") | |
| assert r.status_code == 200 | |
| assert r.json()["status"] == "ok" | |
| def test_root_returns_200(self): | |
| r = client.get("/") | |
| assert r.status_code == 200 | |
| def test_state_returns_bounded_score(self): | |
| """State endpoint returns score in valid range.""" | |
| _reset("easy") | |
| state = _state() | |
| score = state.get("running_score", 0) | |
| assert 0.001 <= score <= 0.999 or score == 0.01 or score == 0.001 | |
| # βββ REWARD CLAMPING TESTS ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestRewardClamping: | |
| """Verify rewards are always clamped: max(0.01, min(0.99, reward)).""" | |
| def test_clamp_positive_reward(self): | |
| """Correct comment reward clamped to <= 0.99.""" | |
| _reset("easy") | |
| r = _step({"operation": "add_comment", "line_number": 18, "severity": "major", | |
| "category": "bug", "message": "Off-by-one in loop bound."}) | |
| assert r["reward"] <= 0.99 | |
| def test_clamp_negative_reward(self): | |
| """False positive reward clamped to >= 0.01.""" | |
| _reset("easy") | |
| r = _step({"operation": "add_comment", "line_number": 1, "severity": "major", | |
| "category": "bug", "message": "Not a bug."}) | |
| assert r["reward"] >= 0.01 | |
| def test_clamp_done_score(self): | |
| """Done score clamped to [0.001, 0.999].""" | |
| _reset("easy") | |
| result = _step({"operation": "done"}) | |
| score = result["info"]["current_score"] | |
| assert 0.001 <= score <= 0.999 | |