"""FlakeForge Integration Tests.""" import math import os import tempfile from pathlib import Path from typing import Any, Dict import pytest from models import ( FlakeForgeAction, FlakeForgeObservation, FlakeForgeState, RunRecord, PatchRecord, RewardBreakdown, ROOT_CAUSE_TYPES, failure_mode_entropy, ) from agent.unified_agent import ( extract_think, extract_patch, extract_category_from_think, extract_confidence_from_think, infer_category_from_patch, build_unified_prompt, ) from server.patch_applier import parse_search_replace_hunks, apply_search_replace_patch from server.reward import ( compute_format_reward, compute_compile_reward, compute_stability_reward, compute_causal_proximity_reward, compute_entropy_reward, compute_anti_hack_penalty, compute_reasoning_consistency, ) class TestThinkPatchParsing: """Test extraction of and blocks from model output.""" def test_extract_both_blocks(self): response = """ Root Cause: async_wait (confidence: 0.85) Evidence: TimeoutError at test_flaky.py:12 Strategy: Increase timeout --- tests/test_flaky.py <<<<<<< SEARCH result = await asyncio.wait_for(fetch(), timeout=0.05) ======= result = await asyncio.wait_for(fetch(), timeout=0.5) >>>>>>> REPLACE """ assert "Root Cause: async_wait" in extract_think(response) assert "<<<<<<< SEARCH" in extract_patch(response) def test_extract_single_json_response(self): response = """{ "think": { "claims": [ { "claim_id": "c1", "category": "async_wait", "entity": "fetch", "location": "tests/test_flaky.py::test_fetch", "ast_node_type": "Call", "polarity": "present", "predicted_effect": "The test should stop timing out.", "reason": "The wait_for timeout is too aggressive." } ], "confidence": 0.85 }, "patch": { "hunks": [ { "hunk_id": "h1", "file": "tests/test_flaky.py", "search": " result = await asyncio.wait_for(fetch(), timeout=0.05)", "replace": " result = await asyncio.wait_for(fetch(), timeout=0.5)", "rationale": "Use a realistic timeout.", "addresses_claim": "c1" } ] } }""" assert '"claims"' in extract_think(response) patch = extract_patch(response) assert "--- tests/test_flaky.py" in patch assert "<<<<<<< SEARCH" in patch assert ">>>>>>> REPLACE" in patch def test_extract_minimal_json_response(self): """7B-model friendly: only required fields, no optional keys.""" response = """{ "think": { "claims": [ { "category": "async_wait", "entity": "fetch", "location": "tests/test_flaky.py::test_fetch", "polarity": "present", "reason": "Timeout too aggressive." } ], "confidence": 0.85 }, "patch": { "hunks": [ { "file": "tests/test_flaky.py", "search": " result = await asyncio.wait_for(fetch(), timeout=0.05)", "replace": " result = await asyncio.wait_for(fetch(), timeout=0.5)" } ] } }""" assert '"claims"' in extract_think(response) patch = extract_patch(response) assert "--- tests/test_flaky.py" in patch assert "<<<<<<< SEARCH" in patch assert ">>>>>>> REPLACE" in patch def test_extract_patch_normalizes_escaped_hunk_strings(self): response = """{ "think": { "claims": [ { "category": "concurrency", "entity": "submit", "location": "source.py::WorkerPool.submit", "polarity": "present", "reason": "Lock branch malformed" } ], "confidence": 0.85 }, "patch": { "hunks": [ { "file": "source.py", "search": " with self._lock:\\\\\\"", "replace": " with self._lock:", "rationale": "Normalize malformed line" } ] } }""" patch = extract_patch(response) assert "--- source.py" in patch assert "with self._lock:" in patch assert '\\"' not in patch def test_extract_empty_response(self): assert extract_think("") == "" assert extract_patch("") == "" def test_extract_malformed_blocks(self): response = "No XML blocks here" assert extract_think(response) == "" assert extract_patch(response) == "" def test_category_extraction(self): think = "Root Cause: async_wait (confidence: 0.85)" assert extract_category_from_think(think) == "async_wait" def test_category_normalization(self): assert extract_category_from_think("Root Cause: timeout") == "async_wait" assert extract_category_from_think("Root Cause: race") == "concurrency" assert extract_category_from_think("Root Cause: mock") == "mock_residue" assert extract_category_from_think("Root Cause: fixture") == "fixture_scope_leak" def test_confidence_extraction(self): think = "Root Cause: async_wait (confidence: 0.85)" assert extract_confidence_from_think(think) == 0.85 def test_confidence_clamping(self): assert extract_confidence_from_think("confidence: 1.5") == 1.0 assert extract_confidence_from_think("confidence: -0.5") == 0.0 class TestPatchParsing: """Test search/replace hunk parsing.""" def test_single_hunk(self): patch = """--- tests/test_flaky.py <<<<<<< SEARCH result = await asyncio.wait_for(fetch(), timeout=0.05) ======= result = await asyncio.wait_for(fetch(), timeout=0.5) >>>>>>> REPLACE""" hunks = parse_search_replace_hunks(patch) assert len(hunks) == 1 assert hunks[0].file_path == "tests/test_flaky.py" assert "timeout=0.05" in hunks[0].search_text assert "timeout=0.5" in hunks[0].replace_text def test_multiple_hunks(self): patch = """--- file1.py <<<<<<< SEARCH old1 ======= new1 >>>>>>> REPLACE --- file2.py <<<<<<< SEARCH old2 ======= new2 >>>>>>> REPLACE""" hunks = parse_search_replace_hunks(patch) assert len(hunks) == 2 assert hunks[0].file_path == "file1.py" assert hunks[1].file_path == "file2.py" def test_empty_patch(self): assert parse_search_replace_hunks("") == [] assert parse_search_replace_hunks(" ") == [] def test_apply_patch_to_file(self): with tempfile.TemporaryDirectory() as tmpdir: # Create a test file test_file = Path(tmpdir) / "test_example.py" test_file.write_text("x = 1\ny = 2\nz = 3\n") patch = """--- test_example.py <<<<<<< SEARCH y = 2 ======= y = 42 >>>>>>> REPLACE""" result = apply_search_replace_patch(Path(tmpdir), patch) assert result["success"] is True assert result["lines_changed"] >= 1 content = test_file.read_text() assert "y = 42" in content assert "y = 2" not in content class TestRewardSignals: """Test individual reward signal computation.""" def test_format_reward_perfect(self): action = FlakeForgeAction( raw_response="...", think_text="Root Cause: async_wait (confidence: 0.85)\nEvidence: ...", patch_text="<<<<<<< SEARCH\nold\n=======\nnew\n>>>>>>> REPLACE", ) reward = compute_format_reward(action) assert reward == 1.0 def test_format_reward_missing_think(self): action = FlakeForgeAction( raw_response="...", think_text="", patch_text="<<<<<<< SEARCH\nold\n=======\nnew\n>>>>>>> REPLACE", ) reward = compute_format_reward(action) assert reward == 0.5 # Only patch block present def test_format_reward_empty(self): action = FlakeForgeAction(raw_response="garbage") assert compute_format_reward(action) == 0.0 def test_compile_reward(self): assert compute_compile_reward(True) == 1.0 assert compute_compile_reward(False) == -1.0 assert compute_compile_reward(True, "SyntaxError") == -0.5 def test_stability_full_fix(self): assert compute_stability_reward(0.5, 1.0) == 2.0 def test_stability_improvement(self): reward = compute_stability_reward(0.3, 0.6) assert reward > 0 def test_stability_regression(self): reward = compute_stability_reward(0.5, 0.2) assert reward < 0 def test_stability_no_change(self): assert compute_stability_reward(0.5, 0.5) == -0.1 def test_causal_proximity_same_file(self): reward = compute_causal_proximity_reward( patch_files=["tests/test_flaky.py"], failure_frontier="tests/test_flaky.py:12:test_func", call_chain=["test_func"], boundary_crossings=[], ) assert reward == 1.0 def test_causal_proximity_distant_file(self): reward = compute_causal_proximity_reward( patch_files=["utils/helpers.py"], failure_frontier="tests/test_flaky.py:12:test_func", call_chain=["test_func"], boundary_crossings=[], ) assert reward < 0 def test_entropy_reduction(self): reward = compute_entropy_reward(pre_entropy=0.8, post_entropy=0.2) assert reward > 0 def test_entropy_increase(self): reward = compute_entropy_reward(pre_entropy=0.2, post_entropy=0.8) assert reward < 0 def test_anti_hack_sleep_injection(self): patch = """<<<<<<< SEARCH result = func() ======= import time time.sleep(1) result = func() >>>>>>> REPLACE""" penalty = compute_anti_hack_penalty(patch, ["test.py"], 3) assert penalty < 0 def test_anti_hack_skip_decorator(self): patch = """<<<<<<< SEARCH def test_flaky(): ======= @pytest.mark.skip def test_flaky(): >>>>>>> REPLACE""" penalty = compute_anti_hack_penalty(patch, ["test.py"], 2) assert penalty <= -1.0 def test_anti_hack_clean_patch(self): patch = """<<<<<<< SEARCH timeout=0.05 ======= timeout=0.5 >>>>>>> REPLACE""" penalty = compute_anti_hack_penalty(patch, ["test.py"], 1) assert penalty == 0.0 class TestReasoningConsistency: """Test reasoning-patch consistency verification.""" def test_exact_match(self): score = compute_reasoning_consistency( "async_wait", "async_wait", "root cause analysis", "timeout fix" ) assert score == 0.5 def test_related_match(self): score = compute_reasoning_consistency( "concurrency", "async_wait", "root cause analysis", "timeout fix" ) assert score == 0.25 def test_mismatch(self): score = compute_reasoning_consistency( "network", "fixture_scope_leak", "network issue", "fixture fix" ) assert score == -0.5 class TestCategoryInference: """Test inferring root cause category from what the patch modifies.""" def test_infer_async_wait(self): assert infer_category_from_patch("timeout=0.5") == "async_wait" def test_infer_concurrency(self): assert infer_category_from_patch("asyncio.Lock()") == "concurrency" def test_infer_fixture(self): assert infer_category_from_patch("yield fixture_data\n teardown()") == "fixture_scope_leak" def test_infer_mock(self): assert infer_category_from_patch("monkeypatch.setattr") == "mock_residue" def test_infer_cache(self): assert infer_category_from_patch("cache_clear()") == "module_cache_pollution" class TestFailureModeEntropy: """Test Shannon entropy calculation.""" def test_single_error_type(self): runs = [ RunRecord(passed=False, duration_ms=100, error_type="TimeoutError"), RunRecord(passed=False, duration_ms=100, error_type="TimeoutError"), ] assert failure_mode_entropy(runs) == 0.0 def test_uniform_error_distribution(self): runs = [ RunRecord(passed=False, duration_ms=100, error_type="TimeoutError"), RunRecord(passed=False, duration_ms=100, error_type="AssertionError"), ] assert failure_mode_entropy(runs) == 1.0 def test_no_failures(self): runs = [ RunRecord(passed=True, duration_ms=100), RunRecord(passed=True, duration_ms=100), ] assert failure_mode_entropy(runs) == 0.0 class TestObservationBuilding: """Test observation construction.""" def test_observation_has_deep_signals(self): obs = FlakeForgeObservation( episode_id="test-001", test_identifier="tests/test_flaky.py::test_func", step=0, steps_remaining=8, test_function_source="def test_func(): pass", source_under_test="def func(): pass", module_cache_violations=["utils.py: @lru_cache on fetch"], async_contamination_alive=True, ) assert len(obs.module_cache_violations) == 1 assert obs.async_contamination_alive is True assert obs.step == 0 def test_unified_prompt_includes_deep_signals(self): obs = FlakeForgeObservation( episode_id="test-001", test_identifier="tests/test_flaky.py::test_func", step=0, steps_remaining=8, test_function_source="def test_func(): pass", source_under_test="def func(): pass", module_cache_violations=["utils.py: @lru_cache on compute"], mock_residue_sites=["test_flaky.py:10 — patch() without context"], ) prompt = build_unified_prompt(obs) assert "Module cache violations" in prompt assert "Mock residue sites" in prompt def test_observation_run_history_limit(self): runs = [RunRecord(passed=True, duration_ms=100) for _ in range(30)] obs = FlakeForgeObservation( episode_id="test-001", test_identifier="test", step=0, steps_remaining=8, test_function_source="", source_under_test="", run_history=runs, ) assert len(obs.run_history) == 20 # Capped at 20 class TestRootCauseTypes: """Verify root cause taxonomy is complete.""" def test_all_categories_present(self): assert "async_wait" in ROOT_CAUSE_TYPES assert "module_cache_pollution" in ROOT_CAUSE_TYPES assert "fixture_scope_leak" in ROOT_CAUSE_TYPES assert "mock_residue" in ROOT_CAUSE_TYPES assert "import_side_effect" in ROOT_CAUSE_TYPES assert "unknown" in ROOT_CAUSE_TYPES def test_category_count(self): assert len(ROOT_CAUSE_TYPES) == 13