"""Tests for adaptive condition selection during eval runs."""

from __future__ import annotations

import shutil
from dataclasses import dataclass, field
from pathlib import Path

import pytest

from evals import run_eval
from evals.adaptive_budget_online_policy import (
    ACTION_INCREASE_BUDGET,
    ACTION_KEEP_BUDGET,
    AdaptiveBudgetPolicyController,
    BudgetPolicyConfig,
)
from evals.adaptive_budget_online_policy import (
    STATE_SCHEMA_VERSION as BUDGET_STATE_SCHEMA_VERSION,
)
from evals.adaptive_online_policy import (
    ACTION_COMPACT_THEN_PATCH,
    ACTION_FILE_ONLY,
    ACTION_FRAME_ONLY,
    ACTION_INVOKE_HELPER,
    ACTION_RETRY_HELPER_STRICT_JSON,
    ACTION_SEARCH_ONLY,
    ACTION_SKIP_HELPER,
    ACTION_SNAPSHOT_LITE,
    ONLINE_POLICY_ACTIONS,
    AdaptiveOnlinePolicyController,
    OnlinePolicyConfig,
)
from evals.adaptive_online_policy import (
    STATE_SCHEMA_VERSION as ONLINE_STATE_SCHEMA_VERSION,
)
from evals.patchers.base import ContextCompactionResult, ContextRequestResult, PatchResult
from evals.patchers.openai_compat_patcher import OpenAICompatPatcher
from llmdebug import _json as json


def _make_case(tmp_path: Path) -> run_eval.CaseSpec:
    case_dir = tmp_path / "case_src"
    case_dir.mkdir(parents=True, exist_ok=True)
    (case_dir / "buggy.py").write_text(
        "import helper\n\n\ndef f() -> int:\n    return helper.value()\n",
        encoding="utf-8",
    )
    (case_dir / "helper.py").write_text("def value() -> int:\n    return 1\n", encoding="utf-8")
    (case_dir / "test_buggy.py").write_text(
        "from buggy import f\n\n\ndef test_f() -> None:\n    assert f() == 2\n",
        encoding="utf-8",
    )
    return run_eval.CaseSpec(
        case_id="adaptive_case",
        path=case_dir,
        category="traceback_controls",
        difficulty="easy",
        snapshot_dependency="helpful",
        expected_exception="AssertionError",
        tags=[],
        python_args=["-m", "pytest", "-q"],
        timeout_sec=30,
    )


def _snapshot_payload() -> dict:
    return {
        "exception": {"type": "AssertionError", "message": "expected 2 got 1"},
        "frames": [
            {
                "file_rel": "buggy.py",
                "line": 4,
                "function": "f",
                "code": "return helper.value()",
                "locals": {"x": 1},
            }
        ],
        "pytest": {"nodeid": "test_buggy.py::test_f", "repro": ["python", "-m", "pytest", "-q"]},
    }


def _online_policy_state_payload(
    *,
    mode: str,
    preferred_action: str,
) -> dict[str, object]:
    return {
        "schema_version": ONLINE_STATE_SCHEMA_VERSION,
        "mode": mode,
        "algorithm": "linucb",
        "alpha": 0.0,
        "epsilon": 0.0,
        "l2": 1.0,
        "warmup_decisions": 0,
        "save_every": 1,
        "feature_names": ["bias"],
        "decisions": 0,
        "updates": 0,
        "action_counts": dict.fromkeys(ONLINE_POLICY_ACTIONS, 0),
        "A": {action: [[1.0]] for action in ONLINE_POLICY_ACTIONS},
        "b": {
            action: [2.0 if action == preferred_action else 0.0] for action in ONLINE_POLICY_ACTIONS
        },
    }


def _skip_biased_online_controller(
    tmp_path: Path, *, mode: str
) -> tuple[AdaptiveOnlinePolicyController, Path]:
    state_path = tmp_path / f"policy_state_{mode}.json"
    events_path = tmp_path / f"policy_events_{mode}.jsonl"
    state_path.write_text(
        json.dumps(
            _online_policy_state_payload(mode=mode, preferred_action=ACTION_SKIP_HELPER),
            ensure_ascii=False,
        ),
        encoding="utf-8",
    )
    config = OnlinePolicyConfig.normalize(
        mode=mode,
        algorithm="linucb",
        alpha=0.0,
        epsilon=0.0,
        l2=1.0,
        warmup_decisions=0,
        save_every=1,
        state_path=state_path,
        events_path=events_path,
        random_seed=0,
    )
    return AdaptiveOnlinePolicyController(config), events_path


def _invoke_biased_online_controller(
    tmp_path: Path, *, mode: str
) -> tuple[AdaptiveOnlinePolicyController, Path]:
    state_path = tmp_path / f"policy_state_invoke_{mode}.json"
    events_path = tmp_path / f"policy_events_invoke_{mode}.jsonl"
    state_path.write_text(
        json.dumps(
            _online_policy_state_payload(mode=mode, preferred_action=ACTION_INVOKE_HELPER),
            ensure_ascii=False,
        ),
        encoding="utf-8",
    )
    config = OnlinePolicyConfig.normalize(
        mode=mode,
        algorithm="linucb",
        alpha=0.0,
        epsilon=0.0,
        l2=1.0,
        warmup_decisions=0,
        save_every=1,
        state_path=state_path,
        events_path=events_path,
        random_seed=0,
    )
    return AdaptiveOnlinePolicyController(config), events_path


def _preferred_action_online_controller(
    tmp_path: Path, *, mode: str, preferred_action: str
) -> tuple[AdaptiveOnlinePolicyController, Path]:
    state_path = tmp_path / f"policy_state_{preferred_action}_{mode}.json"
    events_path = tmp_path / f"policy_events_{preferred_action}_{mode}.jsonl"
    state_path.write_text(
        json.dumps(
            _online_policy_state_payload(mode=mode, preferred_action=preferred_action),
            ensure_ascii=False,
        ),
        encoding="utf-8",
    )
    config = OnlinePolicyConfig.normalize(
        mode=mode,
        algorithm="linucb",
        alpha=0.0,
        epsilon=0.0,
        l2=1.0,
        warmup_decisions=0,
        save_every=1,
        state_path=state_path,
        events_path=events_path,
        random_seed=0,
    )
    return AdaptiveOnlinePolicyController(config), events_path


def _increase_biased_budget_controller(
    tmp_path: Path, *, mode: str
) -> tuple[AdaptiveBudgetPolicyController, Path]:
    state_path = tmp_path / f"budget_policy_state_{mode}.json"
    events_path = tmp_path / f"budget_policy_events_{mode}.jsonl"
    state_path.write_text(
        json.dumps(
            {
                "schema_version": BUDGET_STATE_SCHEMA_VERSION,
                "mode": mode,
                "algorithm": "linucb",
                "alpha": 0.0,
                "epsilon": 0.0,
                "l2": 1.0,
                "warmup_decisions": 0,
                "save_every": 1,
                "feature_names": ["bias"],
                "decisions": 0,
                "updates": 0,
                "action_counts": {
                    ACTION_KEEP_BUDGET: 0,
                    ACTION_INCREASE_BUDGET: 0,
                },
                "A": {
                    ACTION_KEEP_BUDGET: [[1.0]],
                    ACTION_INCREASE_BUDGET: [[1.0]],
                },
                "b": {
                    ACTION_KEEP_BUDGET: [0.0],
                    ACTION_INCREASE_BUDGET: [2.0],
                },
            },
            ensure_ascii=False,
        ),
        encoding="utf-8",
    )
    config = BudgetPolicyConfig.normalize(
        mode=mode,
        algorithm="linucb",
        alpha=0.0,
        epsilon=0.0,
        l2=1.0,
        warmup_decisions=0,
        save_every=1,
        state_path=state_path,
        events_path=events_path,
        random_seed=0,
    )
    return AdaptiveBudgetPolicyController(config), events_path


@dataclass
class _AdaptiveGatedCapturePatcher:
    name: str = "adaptive_gated_capture"
    request_calls: int = 0
    prompts: list[str] = field(default_factory=list)

    def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del prompt, context_dir, cwd
        self.request_calls += 1
        return ContextRequestResult(
            request_json={
                "need_more_context": True,
                "requests": [{"type": "file", "path": "buggy.py"}],
                "sufficiency_rationale": "need code context",
            },
            raw_response='{"need_more_context":true}',
            meta={},
        )

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del context_dir, cwd
        self.prompts.append(prompt)
        return PatchResult(diff=None, meta={"reason": "no_diff"})


@dataclass
class _AdaptiveDiscretionDeclinePatcher:
    name: str = "adaptive_discretion_decline"
    request_calls: int = 0

    def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del prompt, context_dir, cwd
        self.request_calls += 1
        return ContextRequestResult(
            request_json={
                "need_more_context": False,
                "requests": [],
                "sufficiency_rationale": "enough evidence",
            },
            raw_response='{"need_more_context":false,"requests":[]}',
            meta={},
        )

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del prompt, context_dir, cwd
        return PatchResult(diff=None, meta={"reason": "no_diff"})


@dataclass
class _AdaptiveDiscretionInvalidPatcher:
    name: str = "adaptive_discretion_invalid"
    request_calls: int = 0
    prompts: list[str] = field(default_factory=list)

    def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del prompt, context_dir, cwd
        self.request_calls += 1
        return ContextRequestResult(request_json=None, raw_response="{invalid-json", meta={})

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del context_dir, cwd
        self.prompts.append(prompt)
        return PatchResult(diff=None, meta={"reason": "no_diff"})


@dataclass
class _AdaptiveDiscretionBackendUnavailablePatcher:
    name: str = "adaptive_discretion_backend_unavailable"
    request_calls: int = 0
    prompts: list[str] = field(default_factory=list)

    def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del prompt, context_dir, cwd
        self.request_calls += 1
        return ContextRequestResult(
            request_json=None,
            raw_response=None,
            meta={
                "error": "request_failed",
                "exc": "ConnectionRefusedError: [Errno 61] Connection refused",
                "request_context_failure_kind": "backend_unavailable",
            },
        )

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del context_dir, cwd
        self.prompts.append(prompt)
        return PatchResult(diff=None, meta={"reason": "no_diff"})


@dataclass
class _AdaptiveDiscretionTransportFailurePatcher:
    name: str = "adaptive_discretion_transport_failure"
    request_calls: int = 0
    prompts: list[str] = field(default_factory=list)

    def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del prompt, context_dir, cwd
        self.request_calls += 1
        return ContextRequestResult(
            request_json=None,
            raw_response=None,
            meta={
                "error": "request_failed",
                "exc": "ReadTimeout: timed out while waiting for response",
                "request_context_retry_count": 1,
                "request_context_transport_retry_count": 1,
            },
        )

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del context_dir, cwd
        self.prompts.append(prompt)
        return PatchResult(diff=None, meta={"reason": "no_diff"})


@dataclass
class _AdaptiveDiscretionRequestBudgetPatcher:
    name: str = "adaptive_discretion_request_budget"
    request_calls: int = 0
    prompts: list[str] = field(default_factory=list)

    def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del prompt, context_dir, cwd
        self.request_calls += 1
        return ContextRequestResult(
            request_json=None,
            raw_response=None,
            meta={"error": "request_budget_exceeded"},
        )

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del context_dir, cwd
        self.prompts.append(prompt)
        return PatchResult(diff=None, meta={"reason": "no_diff"})


@dataclass
class _AdaptiveDiscretionSnapshotGenericPatcher:
    name: str = "adaptive_discretion_snapshot_generic"
    request_calls: int = 0
    prompts: list[str] = field(default_factory=list)

    def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del prompt, context_dir, cwd
        self.request_calls += 1
        return ContextRequestResult(
            request_json={
                "need_more_context": False,
                "need_snapshot": True,
                "snapshot_justification": "Need more context.",
                "requests": [],
                "sufficiency_rationale": "need state",
            },
            raw_response='{"need_more_context":false,"need_snapshot":true}',
            meta={},
        )

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del context_dir, cwd
        self.prompts.append(prompt)
        return PatchResult(diff=None, meta={"reason": "no_diff"})


@dataclass
class _AdaptiveToggleDiffPatcher:
    name: str = "adaptive_toggle_diff"
    request_calls: int = 0
    patch_calls: int = 0

    def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del prompt, context_dir, cwd
        self.request_calls += 1
        return ContextRequestResult(
            request_json={
                "need_more_context": True,
                "requests": [{"type": "file", "path": "buggy.py"}],
                "sufficiency_rationale": "need source",
            },
            raw_response='{"need_more_context":true}',
            meta={},
        )

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del prompt, context_dir, cwd
        self.patch_calls += 1
        if self.patch_calls % 2 == 1:
            diff = (
                "diff --git a/helper.py b/helper.py\n"
                "--- a/helper.py\n"
                "+++ b/helper.py\n"
                "@@ -1,2 +1,2 @@\n"
                " def value() -> int:\n"
                "-    return 1\n"
                "+    return 2\n"
            )
        else:
            diff = (
                "diff --git a/helper.py b/helper.py\n"
                "--- a/helper.py\n"
                "+++ b/helper.py\n"
                "@@ -1,2 +1,2 @@\n"
                " def value() -> int:\n"
                "-    return 2\n"
                "+    return 1\n"
            )
        return PatchResult(diff=diff, meta={"reason": "toggle_diff"})


@dataclass
class _AdaptiveTestEditPatcher:
    name: str = "adaptive_test_edit"
    request_calls: int = 0

    def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del prompt, context_dir, cwd
        self.request_calls += 1
        return ContextRequestResult(
            request_json={
                "need_more_context": True,
                "requests": [{"type": "file", "path": "buggy.py"}],
                "sufficiency_rationale": "need source",
            },
            raw_response='{"need_more_context":true}',
            meta={},
        )

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del prompt, context_dir, cwd
        diff = (
            "diff --git a/test_buggy.py b/test_buggy.py\n"
            "--- a/test_buggy.py\n"
            "+++ b/test_buggy.py\n"
            "@@ -3,4 +3,4 @@\n"
            " \n"
            " \n"
            " def test_f() -> None:\n"
            "-    assert f() == 2\n"
            "+    assert f() == 1\n"
        )
        return PatchResult(diff=diff, meta={"reason": "test_edit"})


@dataclass
class _AdaptiveNeighborEditPatcher:
    name: str = "adaptive_neighbor_edit"
    request_calls: int = 0

    def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del prompt, context_dir, cwd
        self.request_calls += 1
        return ContextRequestResult(
            request_json={
                "need_more_context": True,
                "requests": [{"type": "file", "path": "buggy.py"}],
                "sufficiency_rationale": "need source",
            },
            raw_response='{"need_more_context":true}',
            meta={},
        )

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del prompt, context_dir, cwd
        diff = (
            "diff --git a/helper.py b/helper.py\n"
            "--- a/helper.py\n"
            "+++ b/helper.py\n"
            "@@ -1,2 +1,2 @@\n"
            " def value() -> int:\n"
            "-    return 1\n"
            "+    return 2\n"
        )
        return PatchResult(diff=diff, meta={"reason": "neighbor_edit"})


@dataclass
class _SyntaxCyclingPatcher:
    name: str = "syntax_cycling"
    diffs: list[str] = field(default_factory=list)
    metas: list[dict[str, object]] = field(default_factory=list)
    patch_calls: int = 0

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del prompt, context_dir, cwd
        self.patch_calls += 1
        if not self.diffs:
            return PatchResult(diff=None, meta={"reason": "no_diff"})
        idx = min(self.patch_calls - 1, len(self.diffs) - 1)
        meta: dict[str, object] = {"reason": "cycle_diff", "llm_call_count": 1}
        if self.metas:
            meta.update(self.metas[min(idx, len(self.metas) - 1)])
        return PatchResult(diff=self.diffs[idx], meta=meta)


@dataclass
class _AdaptiveSequentialPatchResultPatcher:
    name: str = "adaptive_sequential_patch_result"
    max_propose_sec: float = 0.0
    response_mode: str = "diff"
    patch_results: list[PatchResult] = field(default_factory=list)
    request_calls: int = 0
    patch_calls: int = 0
    prompts: list[str] = field(default_factory=list)

    def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del prompt, context_dir, cwd
        self.request_calls += 1
        return ContextRequestResult(
            request_json={
                "need_more_context": True,
                "requests": [{"type": "file", "path": "buggy.py"}],
                "sufficiency_rationale": "need source",
            },
            raw_response='{"need_more_context":true}',
            meta={},
        )

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del context_dir, cwd
        self.patch_calls += 1
        self.prompts.append(prompt)
        if not self.patch_results:
            return PatchResult(diff=None, meta={"reason": "no_diff"})
        idx = min(self.patch_calls - 1, len(self.patch_results) - 1)
        patch_result = self.patch_results[idx]
        meta = dict(patch_result.meta) if isinstance(patch_result.meta, dict) else patch_result.meta
        return PatchResult(diff=patch_result.diff, meta=meta)


@dataclass
class _AdaptiveBudgetNoDiffPatcher:
    name: str = "openai"
    max_propose_sec: float = 10.0
    max_tokens: int = 400
    response_mode: str = "edits"

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del prompt, context_dir, cwd
        return PatchResult(diff=None, meta={"reason": "no_diff"})


@dataclass
class _AdaptiveBudgetExceededPatcher:
    name: str = "openai"
    max_propose_sec: float = 10.0
    max_tokens: int = 400
    response_mode: str = "edits"

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del prompt, context_dir, cwd
        return PatchResult(diff=None, meta={"error": "request_budget_exceeded"})


class _AdaptiveBudgetNoDiffNonDataclassPatcher:
    name = "openai"
    max_propose_sec = 10.0
    response_mode = "edits"

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del prompt, context_dir, cwd
        return PatchResult(diff=None, meta={"reason": "no_diff"})


@dataclass
class _AdaptiveDelayedCreditPatcher:
    name: str = "adaptive_delayed_credit"
    patch_outcomes: tuple[str, ...] = ("no_diff", "fix")
    request_calls: int = 0
    patch_calls: int = 0

    def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del prompt, context_dir, cwd
        self.request_calls += 1
        return ContextRequestResult(
            request_json={
                "need_more_context": True,
                "requests": [{"type": "file", "path": "helper.py"}],
                "sufficiency_rationale": "need helper implementation",
            },
            raw_response='{"need_more_context":true}',
            meta={},
        )

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del prompt, context_dir, cwd
        self.patch_calls += 1
        outcome = self.patch_outcomes[min(self.patch_calls - 1, len(self.patch_outcomes) - 1)]
        if outcome == "no_diff":
            return PatchResult(diff=None, meta={"reason": "no_diff"})
        if outcome == "fix":
            return PatchResult(diff=_helper_fix_diff(), meta={"reason": "delayed_credit_fix"})
        raise AssertionError(f"unexpected patch outcome: {outcome}")


@dataclass
class _AdaptiveActionCapturePatcher:
    name: str = "adaptive_action_capture"
    request_payload: dict[str, object] | None = None
    request_meta: dict[str, object] = field(default_factory=dict)
    compacted_text: str | None = None
    compact_meta: dict[str, object] = field(default_factory=dict)
    request_calls: int = 0
    compact_calls: int = 0
    patch_calls: int = 0
    request_kwargs: list[dict[str, object]] = field(default_factory=list)
    request_prompts: list[str] = field(default_factory=list)
    patch_prompts: list[str] = field(default_factory=list)

    def request_context(
        self,
        *,
        prompt: str,
        context_dir,
        cwd,
        strict_json_first: bool = False,
    ) -> ContextRequestResult:
        del context_dir, cwd
        self.request_calls += 1
        self.request_prompts.append(prompt)
        self.request_kwargs.append({"strict_json_first": bool(strict_json_first)})
        return ContextRequestResult(
            request_json=self.request_payload,
            raw_response="{}" if self.request_payload is None else json.dumps(self.request_payload),
            meta=dict(self.request_meta),
        )

    def compact_context(
        self,
        *,
        prompt: str,
        context_text: str,
        context_dir,
        cwd,
        target_chars: int,
    ) -> ContextCompactionResult:
        del context_dir, cwd, target_chars
        self.compact_calls += 1
        self.patch_prompts.append(f"COMPACTION\n{prompt}\n{context_text}")
        return ContextCompactionResult(
            compacted_text=self.compacted_text,
            raw_response=self.compacted_text,
            meta=dict(self.compact_meta),
        )

    def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del context_dir, cwd
        self.patch_calls += 1
        self.patch_prompts.append(prompt)
        return PatchResult(diff=None, meta={"reason": "no_diff"})


def _helper_fix_diff() -> str:
    return (
        "diff --git a/helper.py b/helper.py\n"
        "--- a/helper.py\n"
        "+++ b/helper.py\n"
        "@@ -1,2 +1,2 @@\n"
        " def value() -> int:\n"
        "-    return 1\n"
        "+    return 2\n"
    )


def _test_only_expectation_diff(*, expected: int) -> str:
    return (
        "diff --git a/test_buggy.py b/test_buggy.py\n"
        "--- a/test_buggy.py\n"
        "+++ b/test_buggy.py\n"
        "@@ -3,4 +3,4 @@\n"
        " \n"
        " \n"
        " def test_f() -> None:\n"
        "-    assert f() == 2\n"
        f"+    assert f() == {expected}\n"
    )


def _helper_non_substantive_diff() -> str:
    return (
        "diff --git a/helper.py b/helper.py\n"
        "--- a/helper.py\n"
        "+++ b/helper.py\n"
        "@@ -1,2 +1,2 @@\n"
        " def value() -> int:\n"
        "     return 1\n"
    )


def _run_pytest_for_helper_fix(cwd, *args, **kwargs) -> run_eval.RunResult:
    del args, kwargs
    helper_text = (cwd / "helper.py").read_text(encoding="utf-8")
    fixed = "return 2" in helper_text
    return run_eval.RunResult(
        returncode=0 if fixed else 1,
        duration_sec=0.01,
        stdout=(
            "=== 1 passed in 0.01s ==="
            if fixed
            else "tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n"
        ),
        stderr="",
    )


def _run_adaptive_delayed_credit_case(
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
    *,
    patch_outcomes: tuple[str, ...],
    adaptive_max_helper_calls: int,
    adaptive_budget_topup_max_calls: int = 1,
    k: int,
    run_id: str,
) -> tuple[list[dict[str, object]], _AdaptiveDelayedCreditPatcher, Path]:
    case = _make_case(tmp_path)
    workdir = tmp_path / f"{run_id}_workdir"
    context_dir = tmp_path / f"{run_id}_context"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveDelayedCreditPatcher(patch_outcomes=patch_outcomes)
    controller, events_path = _invoke_biased_online_controller(tmp_path, mode="on")

    monkeypatch.setattr(run_eval, "run_pytest", _run_pytest_for_helper_fix)
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=k,
        output_format="json_compact",
        run_id=run_id,
        include_files_context=False,
        force_patch_attempt=False,
        adaptive_max_helper_calls=adaptive_max_helper_calls,
        adaptive_budget_topup_max_calls=adaptive_budget_topup_max_calls,
        adaptive_online_policy_controller=controller,
        adaptive_online_policy_mode="on",
    )
    return records, patcher, events_path


def _run_adaptive_verifier_case(
    *,
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
    patcher: object,
    run_id: str,
    apply_result: tuple[bool, str] = (False, "simulated apply failure"),
) -> tuple[list[dict[str, object]], dict[str, int], dict[str, int]]:
    case = _make_case(tmp_path)
    workdir = tmp_path / f"{run_id}_workdir"
    context_dir = tmp_path / f"{run_id}_context"
    shutil.copytree(case.path, workdir)

    run_calls = {"count": 0}
    apply_calls = {"count": 0}

    def _fake_run_pytest(*args, **kwargs) -> run_eval.RunResult:
        del args, kwargs
        run_calls["count"] += 1
        return run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        )

    def _fake_apply_diff(cwd, diff):
        del cwd, diff
        apply_calls["count"] += 1
        return apply_result

    monkeypatch.setattr(run_eval, "run_pytest", _fake_run_pytest)
    monkeypatch.setattr(run_eval, "apply_diff", _fake_apply_diff)
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id=run_id,
        include_files_context=False,
        force_patch_attempt=False,
    )
    return records, run_calls, apply_calls


def _run_adaptive_action_case(
    *,
    tmp_path: Path,
    monkeypatch: pytest.MonkeyPatch,
    patcher: object,
    preferred_action: str,
    run_id: str,
    k: int = 1,
    adaptive_helper_max_calls_per_case: int | None = None,
    snapshot_payload: dict[str, object] | None = None,
    include_snapshot: bool = True,
    extra_run_kwargs: dict[str, object] | None = None,
) -> tuple[list[dict[str, object]], Path]:
    case = _make_case(tmp_path)
    workdir = tmp_path / f"{run_id}_workdir"
    context_dir = tmp_path / f"{run_id}_context"
    shutil.copytree(case.path, workdir)
    controller, events_path = _preferred_action_online_controller(
        tmp_path, mode="on", preferred_action=preferred_action
    )

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    effective_snapshot_payload = (
        (_snapshot_payload() if snapshot_payload is None else snapshot_payload)
        if include_snapshot
        else None
    )
    monkeypatch.setattr(
        run_eval,
        "read_latest_snapshot",
        lambda *_args, **_kwargs: (effective_snapshot_payload, None),
    )

    run_kwargs: dict[str, object] = {}
    if adaptive_helper_max_calls_per_case is not None:
        run_kwargs["adaptive_max_helper_calls"] = adaptive_helper_max_calls_per_case
    if extra_run_kwargs:
        run_kwargs.update(extra_run_kwargs)

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=k,
        output_format="json_compact",
        run_id=run_id,
        include_files_context=False,
        force_patch_attempt=False,
        adaptive_online_policy_controller=controller,
        adaptive_online_policy_mode="on",
        **run_kwargs,
    )
    return records, events_path


def test_adaptive_gated_invokes_helper_after_repeated_signature(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_gated"
    context_dir = tmp_path / "context_adaptive_gated"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveGatedCapturePatcher()

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_GATED,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=2,
        output_format="json_compact",
        run_id="run-adaptive-gated",
        include_files_context=False,
        force_patch_attempt=False,
        adaptive_gate_policy="balanced",
        adaptive_stagnation_window=1,
    )

    assert len(records) == 2
    assert patcher.request_calls == 1
    first_attempt_meta = records[0]["attempt_metadata"]
    second_attempt_meta = records[1]["attempt_metadata"]
    assert first_attempt_meta["adaptive_helper_invoked"] is False
    assert second_attempt_meta["adaptive_helper_invoked"] is True
    assert first_attempt_meta["context_protocol"] == "monolithic"
    assert second_attempt_meta["context_protocol"] == "staged"
    assert records[1]["patch_meta"]["adaptive_helper_decision_reason"] in {
        "gate_balanced_repeated_signature",
        "gate_requested_helper",
    }
    assert len(patcher.prompts) == 2
    assert "Pytest output (stdout):" in patcher.prompts[1]
    assert "Stage-A evidence index:" not in patcher.prompts[1]
    assert second_attempt_meta["retry_delta_prompt"] is False


def test_adaptive_llm_discretion_decline_keeps_monolithic_attempt(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_discretion_decline"
    context_dir = tmp_path / "context_adaptive_discretion_decline"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveDiscretionDeclinePatcher()

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-adaptive-discretion-decline",
        include_files_context=False,
        force_patch_attempt=False,
    )

    assert len(records) == 1
    assert patcher.request_calls == 1
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_helper_invoked"] is True
    assert attempt_meta["adaptive_helper_materialized_context"] is False
    assert attempt_meta["adaptive_helper_outcome"] == "semantic_no_context"
    assert attempt_meta["adaptive_helper_failure_kind"] == ""
    assert attempt_meta["adaptive_helper_decision_reason"] == "llm_declined_helper"
    assert attempt_meta["adaptive_helper_calls_used_after_attempt"] == 1
    assert attempt_meta["adaptive_helper_retry_count"] == 0
    assert attempt_meta["adaptive_helper_transport_retry_count"] == 0
    assert attempt_meta["context_protocol"] == "monolithic"
    assert records[0]["patch_meta"]["stage_a_request_success"] is True


@pytest.mark.parametrize(
    ("preferred_action", "expected_type"),
    [
        (ACTION_FRAME_ONLY, "frame"),
        (ACTION_FILE_ONLY, "file"),
        (ACTION_SEARCH_ONLY, "search"),
    ],
)
def test_adaptive_llm_discretion_restricted_actions_filter_stage_a_requests(
    tmp_path,
    monkeypatch,
    preferred_action: str,
    expected_type: str,
) -> None:
    patcher = _AdaptiveActionCapturePatcher(
        request_payload={
            "need_more_context": True,
            "need_snapshot": True,
            "need_compact_context": True,
            "requests": [
                {"type": "file", "path": "buggy.py"},
                {"type": "search", "path": "buggy.py", "pattern": "helper.value"},
                {"type": "frame", "index": 0},
            ],
            "sufficiency_rationale": "need targeted evidence",
        }
    )

    records, _events_path = _run_adaptive_action_case(
        tmp_path=tmp_path,
        monkeypatch=monkeypatch,
        patcher=patcher,
        preferred_action=preferred_action,
        run_id=f"run-{preferred_action}",
    )

    attempt_meta = records[0]["attempt_metadata"]
    patch_meta = records[0]["patch_meta"]

    assert patcher.request_calls == 1
    assert attempt_meta["adaptive_helper_action_selected"] == preferred_action
    assert attempt_meta["adaptive_helper_action_executed"] == preferred_action
    assert attempt_meta["adaptive_helper_action_fallback_reason"] == ""
    assert attempt_meta["adaptive_helper_outcome"] == "materialized"
    assert patch_meta["stage_a_requests_count"] == 1
    assert patch_meta["stage_a_need_compact_context"] is False
    if expected_type == "frame":
        assert "Stage-A requested context:\n[e_" in patcher.patch_prompts[0]
        assert "frame:frame:0" in patcher.patch_prompts[0]
    elif expected_type == "file":
        assert "file:buggy.py" in patcher.patch_prompts[0]
    else:
        assert "search:buggy.py:helper.value" in patcher.patch_prompts[0]


def test_adaptive_llm_discretion_snapshot_lite_injects_snapshot_index_without_helper_budget(
    tmp_path, monkeypatch
) -> None:
    patcher = _AdaptiveActionCapturePatcher()

    records, _events_path = _run_adaptive_action_case(
        tmp_path=tmp_path,
        monkeypatch=monkeypatch,
        patcher=patcher,
        preferred_action=ACTION_SNAPSHOT_LITE,
        run_id="run-snapshot-lite",
    )

    attempt_meta = records[0]["attempt_metadata"]

    assert patcher.request_calls == 0
    assert attempt_meta["adaptive_helper_action_executed"] == ACTION_SNAPSHOT_LITE
    assert attempt_meta["adaptive_snapshot_lite_included"] is True
    assert attempt_meta["adaptive_helper_invoked"] is False
    assert attempt_meta["adaptive_helper_calls_used_after_attempt"] == 0
    assert attempt_meta["adaptive_helper_outcome"] == "materialized"
    assert "Compact snapshot index:" in patcher.patch_prompts[0]
    assert "=== llmdebug Snapshot Index ===" in patcher.patch_prompts[0]
    assert "=== llmdebug Snapshot ===" not in patcher.patch_prompts[0]


def test_adaptive_llm_discretion_snapshot_lite_not_credited_when_budget_drops_it(
    tmp_path, monkeypatch
) -> None:
    patcher = _AdaptiveActionCapturePatcher()

    records, _events_path = _run_adaptive_action_case(
        tmp_path=tmp_path,
        monkeypatch=monkeypatch,
        patcher=patcher,
        preferred_action=ACTION_SNAPSHOT_LITE,
        run_id="run-snapshot-lite-budget-drop",
        extra_run_kwargs={
            "prompt_budget_total_chars": 300,
            "prompt_budget_snapshot_chars": 0,
        },
    )

    attempt_meta = records[0]["attempt_metadata"]
    patch_meta = records[0]["patch_meta"]

    assert attempt_meta["adaptive_helper_action_executed"] == ACTION_SNAPSHOT_LITE
    assert attempt_meta["adaptive_snapshot_lite_included"] is False
    assert attempt_meta["adaptive_helper_outcome"] == "semantic_no_context"
    assert int(patch_meta["prompt_budget_used_chars"] or 0) <= 300
    assert "Compact snapshot index:" not in patcher.patch_prompts[0]


def test_adaptive_llm_discretion_snapshot_lite_falls_back_without_snapshot(
    tmp_path, monkeypatch
) -> None:
    patcher = _AdaptiveActionCapturePatcher()

    records, _events_path = _run_adaptive_action_case(
        tmp_path=tmp_path,
        monkeypatch=monkeypatch,
        patcher=patcher,
        preferred_action=ACTION_SNAPSHOT_LITE,
        run_id="run-snapshot-lite-fallback",
        include_snapshot=False,
    )

    attempt_meta = records[0]["attempt_metadata"]

    assert attempt_meta["adaptive_helper_action_selected"] == ACTION_SNAPSHOT_LITE
    assert attempt_meta["adaptive_helper_action_executed"] == ACTION_INVOKE_HELPER
    assert attempt_meta["adaptive_helper_action_fallback_reason"] == "snapshot_unavailable"
    assert attempt_meta["adaptive_snapshot_lite_included"] is False
    assert patcher.request_calls == 1


def test_adaptive_llm_discretion_compact_then_patch_uses_compaction_budget_only(
    tmp_path, monkeypatch
) -> None:
    patcher = _AdaptiveActionCapturePatcher(
        compacted_text="crash in buggy.py line 4; helper.value() returns 1 but test expects 2",
        compact_meta={
            "llm_call_count": 1,
            "usage": {"prompt_tokens": 11, "completion_tokens": 4, "total_tokens": 15},
            "compact_context_duration_sec": 0.3,
        },
    )

    records, _events_path = _run_adaptive_action_case(
        tmp_path=tmp_path,
        monkeypatch=monkeypatch,
        patcher=patcher,
        preferred_action=ACTION_COMPACT_THEN_PATCH,
        run_id="run-compact-then-patch",
    )

    attempt_meta = records[0]["attempt_metadata"]
    patch_meta = records[0]["patch_meta"]

    assert patcher.request_calls == 0
    assert patcher.compact_calls == 1
    assert attempt_meta["adaptive_helper_action_executed"] == ACTION_COMPACT_THEN_PATCH
    assert attempt_meta["adaptive_helper_invoked"] is False
    assert attempt_meta["adaptive_helper_calls_used_after_attempt"] == 0
    assert attempt_meta["context_compaction_invoked"] is True
    assert attempt_meta["context_compaction_trigger"] == "policy_action"
    assert attempt_meta["adaptive_helper_outcome"] == "materialized"
    assert attempt_meta["adaptive_online_local_tokens_total"] == 15
    assert patch_meta["context_compaction_success"] is True
    assert "Compacted context summary:" in patcher.patch_prompts[-1]


def test_context_compaction_policy_action_respects_mode_off() -> None:
    should_compact, reason = run_eval._should_invoke_context_compaction(
        mode="off",
        supports_context_compaction=True,
        stage_a_requested=False,
        policy_action_requested=True,
        auto_overflow_triggered=True,
        calls_used=0,
        max_calls=1,
        bundle_input_chars=128,
    )

    assert should_compact is False
    assert reason == "mode_off"


def test_adaptive_llm_discretion_compact_then_patch_falls_back_when_compaction_disabled(
    tmp_path, monkeypatch
) -> None:
    patcher = _AdaptiveActionCapturePatcher(
        request_payload={
            "need_more_context": True,
            "requests": [{"type": "file", "path": "helper.py"}],
            "sufficiency_rationale": "need helper implementation",
        }
    )

    records, _events_path = _run_adaptive_action_case(
        tmp_path=tmp_path,
        monkeypatch=monkeypatch,
        patcher=patcher,
        preferred_action=ACTION_COMPACT_THEN_PATCH,
        run_id="run-compact-then-patch-mode-off",
        extra_run_kwargs={"context_compaction_mode": "off"},
    )

    attempt_meta = records[0]["attempt_metadata"]

    assert attempt_meta["adaptive_helper_action_selected"] == ACTION_COMPACT_THEN_PATCH
    assert attempt_meta["adaptive_helper_action_executed"] == ACTION_INVOKE_HELPER
    assert attempt_meta["adaptive_helper_action_fallback_reason"] == "context_compaction_disabled"
    assert attempt_meta["context_compaction_invoked"] is False
    assert patcher.compact_calls == 0
    assert patcher.request_calls == 1


def test_adaptive_llm_discretion_compact_then_patch_falls_back_without_compaction_support(
    tmp_path, monkeypatch
) -> None:
    @dataclass
    class _NoCompactionPatcher:
        name: str = "openai"
        request_calls: int = 0

        def request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
            del prompt, context_dir, cwd
            self.request_calls += 1
            return ContextRequestResult(
                request_json={
                    "need_more_context": True,
                    "requests": [{"type": "file", "path": "helper.py"}],
                    "sufficiency_rationale": "need helper implementation",
                },
                raw_response='{"need_more_context":true}',
                meta={},
            )

        def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
            del prompt, context_dir, cwd
            return PatchResult(diff=None, meta={"reason": "no_diff"})

    patcher = _NoCompactionPatcher()

    records, _events_path = _run_adaptive_action_case(
        tmp_path=tmp_path,
        monkeypatch=monkeypatch,
        patcher=patcher,
        preferred_action=ACTION_COMPACT_THEN_PATCH,
        run_id="run-compact-then-patch-fallback",
    )

    attempt_meta = records[0]["attempt_metadata"]

    assert attempt_meta["adaptive_helper_action_selected"] == ACTION_COMPACT_THEN_PATCH
    assert attempt_meta["adaptive_helper_action_executed"] == ACTION_INVOKE_HELPER
    assert (
        attempt_meta["adaptive_helper_action_fallback_reason"] == "context_compaction_unsupported"
    )
    assert attempt_meta["context_compaction_invoked"] is False
    assert patcher.request_calls == 1


def test_adaptive_llm_discretion_retry_helper_strict_json_requires_prior_invalid_payload(
    tmp_path, monkeypatch
) -> None:
    @dataclass
    class _StrictJsonRetryPatcher:
        name: str = "openai"
        request_calls: int = 0
        request_kwargs: list[dict[str, object]] = field(default_factory=list)

        def request_context(
            self,
            *,
            prompt: str,
            context_dir,
            cwd,
            strict_json_first: bool = False,
        ) -> ContextRequestResult:
            del prompt, context_dir, cwd
            self.request_calls += 1
            self.request_kwargs.append({"strict_json_first": bool(strict_json_first)})
            if self.request_calls == 1:
                return ContextRequestResult(
                    request_json=None, raw_response="{invalid-json", meta={}
                )
            return ContextRequestResult(
                request_json={
                    "need_more_context": True,
                    "requests": [{"type": "file", "path": "helper.py"}],
                    "sufficiency_rationale": "need helper implementation",
                },
                raw_response='{"need_more_context":true}',
                meta={},
            )

        def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
            del prompt, context_dir, cwd
            return PatchResult(diff=None, meta={"reason": "no_diff"})

    patcher = _StrictJsonRetryPatcher()

    records, _events_path = _run_adaptive_action_case(
        tmp_path=tmp_path,
        monkeypatch=monkeypatch,
        patcher=patcher,
        preferred_action=ACTION_RETRY_HELPER_STRICT_JSON,
        run_id="run-retry-helper-strict-json",
        k=2,
        adaptive_helper_max_calls_per_case=2,
    )

    first_attempt = records[0]["attempt_metadata"]
    second_attempt = records[1]["attempt_metadata"]

    assert patcher.request_calls == 2
    assert first_attempt["adaptive_helper_action_selected"] == ACTION_RETRY_HELPER_STRICT_JSON
    assert first_attempt["adaptive_helper_action_executed"] == ACTION_INVOKE_HELPER
    assert (
        first_attempt["adaptive_helper_action_fallback_reason"]
        == "strict_json_requires_prior_invalid_payload"
    )
    assert patcher.request_kwargs[0]["strict_json_first"] is False
    assert second_attempt["adaptive_helper_action_executed"] == ACTION_RETRY_HELPER_STRICT_JSON
    assert patcher.request_kwargs[1]["strict_json_first"] is True
    assert second_attempt["adaptive_helper_outcome"] == "materialized"


def test_adaptive_llm_discretion_specialized_action_falls_back_when_request_context_unsupported(
    tmp_path, monkeypatch
) -> None:
    @dataclass
    class _NoRequestContextPatcher:
        name: str = "unsupported_context"

        def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
            del prompt, context_dir, cwd
            return PatchResult(diff=None, meta={"reason": "no_diff"})

    records, _events_path = _run_adaptive_action_case(
        tmp_path=tmp_path,
        monkeypatch=monkeypatch,
        patcher=_NoRequestContextPatcher(),
        preferred_action=ACTION_FILE_ONLY,
        run_id="run-file-only-fallback",
    )

    attempt_meta = records[0]["attempt_metadata"]

    assert attempt_meta["adaptive_helper_action_selected"] == ACTION_FILE_ONLY
    assert attempt_meta["adaptive_helper_action_executed"] == ACTION_INVOKE_HELPER
    assert attempt_meta["adaptive_helper_action_fallback_reason"] == "request_context_unsupported"
    assert attempt_meta["adaptive_helper_outcome"] == "skipped_unsupported"


def test_adaptive_openai_helper_response_mode_override(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_openai_override"
    context_dir = tmp_path / "context_adaptive_openai_override"
    shutil.copytree(case.path, workdir)

    captured_response_modes: list[str] = []

    def _fake_request_context(self, *, prompt: str, context_dir, cwd) -> ContextRequestResult:
        del self, prompt, context_dir, cwd
        return ContextRequestResult(
            request_json={
                "need_more_context": True,
                "requests": [{"type": "file", "path": "buggy.py"}],
                "sufficiency_rationale": "need source",
            },
            raw_response='{"need_more_context":true}',
            meta={},
        )

    def _fake_propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del prompt, context_dir, cwd
        captured_response_modes.append(str(self.response_mode))
        return PatchResult(
            diff=None, meta={"reason": "no_diff", "response_mode": self.response_mode}
        )

    monkeypatch.setattr(OpenAICompatPatcher, "request_context", _fake_request_context)
    monkeypatch.setattr(OpenAICompatPatcher, "propose_patch", _fake_propose_patch)
    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    patcher = OpenAICompatPatcher(
        base_url="http://127.0.0.1:1234",
        model="dummy",
        response_mode="edits",
        force_patch_attempt=False,
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-adaptive-openai-override",
        include_files_context=False,
        force_patch_attempt=False,
        adaptive_openai_helper_response_mode="diff",
    )

    assert len(records) == 1
    assert captured_response_modes == ["diff"]
    assert records[0]["patch_meta"]["adaptive_openai_response_mode_override"] == "diff"
    assert records[0]["model_metadata"]["response_mode"] == "diff"


def test_adaptive_llm_discretion_invalid_response_counts_as_consult_without_context(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_discretion_invalid"
    context_dir = tmp_path / "context_adaptive_discretion_invalid"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveDiscretionInvalidPatcher()

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-adaptive-discretion-invalid",
        include_files_context=False,
        force_patch_attempt=False,
    )

    assert len(records) == 1
    assert patcher.request_calls == 1
    assert len(patcher.prompts) == 1
    assert "Stage-A requested context:" not in patcher.prompts[0]
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_helper_invoked"] is True
    assert attempt_meta["adaptive_helper_materialized_context"] is False
    assert attempt_meta["adaptive_helper_outcome"] == "invalid_payload"
    assert attempt_meta["adaptive_helper_failure_kind"] == "invalid_payload"
    assert attempt_meta["adaptive_helper_decision_reason"] == "llm_invalid_response_no_helper"
    assert attempt_meta["adaptive_helper_calls_used_after_attempt"] == 1
    assert attempt_meta["adaptive_helper_retry_count"] == 0
    assert attempt_meta["adaptive_helper_transport_retry_count"] == 0
    assert attempt_meta["context_protocol"] == "monolithic"
    assert records[0]["patch_meta"]["stage_a_request_success"] is False
    assert records[0]["patch_meta"]["stage_a_reason"] == "helper_invalid_payload"


def test_adaptive_llm_discretion_attempt1_snapshot_denial_syncs_helper_reason(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_discretion_snapshot_denial"
    context_dir = tmp_path / "context_adaptive_discretion_snapshot_denial"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveDiscretionSnapshotGenericPatcher()

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-adaptive-discretion-snapshot-denial",
        include_files_context=False,
        force_patch_attempt=False,
    )

    assert len(records) == 1
    assert patcher.request_calls == 1
    attempt_meta = records[0]["attempt_metadata"]
    patch_meta = records[0]["patch_meta"]
    assert attempt_meta["adaptive_snapshot_attempt1_denied"] is True
    assert attempt_meta["adaptive_snapshot_included"] is False
    assert attempt_meta["adaptive_helper_invoked"] is True
    assert attempt_meta["adaptive_helper_materialized_context"] is False
    assert attempt_meta["adaptive_helper_outcome"] == "semantic_no_context"
    assert attempt_meta["adaptive_helper_decision_reason"] == "llm_snapshot_request_denied_attempt1"
    assert patch_meta["adaptive_helper_decision_reason"] == "llm_snapshot_request_denied_attempt1"
    assert attempt_meta["context_protocol"] == "monolithic"


def test_adaptive_llm_discretion_backend_unavailable_does_not_spend_helper_budget(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_discretion_transport_failure"
    context_dir = tmp_path / "context_adaptive_discretion_transport_failure"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveDiscretionBackendUnavailablePatcher()

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-adaptive-discretion-transport-failure",
        include_files_context=False,
        force_patch_attempt=False,
    )

    assert len(records) == 1
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_helper_invoked"] is True
    assert attempt_meta["adaptive_helper_materialized_context"] is False
    assert attempt_meta["adaptive_helper_outcome"] == "unavailable_backend"
    assert attempt_meta["adaptive_helper_failure_kind"] == "backend_unavailable"
    assert attempt_meta["adaptive_helper_calls_used_after_attempt"] == 0
    assert attempt_meta["adaptive_helper_retry_count"] == 0
    assert attempt_meta["adaptive_helper_transport_retry_count"] == 0
    assert attempt_meta["context_protocol"] == "monolithic"
    assert records[0]["patch_meta"]["stage_a_reason"] == "helper_unavailable_backend"


def test_adaptive_llm_discretion_transport_failure_does_not_spend_helper_budget(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_discretion_transport_failure_retryable"
    context_dir = tmp_path / "context_adaptive_discretion_transport_failure_retryable"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveDiscretionTransportFailurePatcher()

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-adaptive-discretion-transport-failure-retryable",
        include_files_context=False,
        force_patch_attempt=False,
    )

    assert len(records) == 1
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_helper_invoked"] is True
    assert attempt_meta["adaptive_helper_materialized_context"] is False
    assert attempt_meta["adaptive_helper_outcome"] == "transport_failed"
    assert attempt_meta["adaptive_helper_failure_kind"] == "transport"
    assert attempt_meta["adaptive_helper_calls_used_after_attempt"] == 0
    assert attempt_meta["adaptive_helper_retry_count"] == 1
    assert attempt_meta["adaptive_helper_transport_retry_count"] == 1
    assert attempt_meta["context_protocol"] == "monolithic"
    assert records[0]["patch_meta"]["stage_a_reason"] == "helper_transport_failed"


def test_adaptive_llm_discretion_caps_helper_calls_and_stops_on_no_progress(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_discretion_cap"
    context_dir = tmp_path / "context_adaptive_discretion_cap"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveGatedCapturePatcher(name="adaptive_discretion_cap")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=5,
        output_format="json_compact",
        run_id="run-adaptive-discretion-cap",
        include_files_context=False,
        force_patch_attempt=False,
        adaptive_max_helper_calls=1,
        adaptive_budget_topup_max_calls=0,
    )

    assert len(records) == 3
    assert patcher.request_calls == 1
    assert len(patcher.prompts) == 3
    assert records[1]["attempt_metadata"]["adaptive_helper_budget_exhausted"] is True
    assert records[2]["attempt_metadata"]["adaptive_progress_stop_triggered"] is True
    assert records[2]["patch_meta"]["adaptive_progress_stop_reason"] == "helper_budget_exhausted"
    assert records[2]["note"] == "no_patch_proposed"


def test_adaptive_llm_discretion_grants_one_budget_topup(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_discretion_topup"
    context_dir = tmp_path / "context_adaptive_discretion_topup"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveGatedCapturePatcher(name="adaptive_discretion_topup")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=4,
        output_format="json_compact",
        run_id="run-adaptive-discretion-topup",
        include_files_context=False,
        force_patch_attempt=False,
        adaptive_max_helper_calls=1,
        adaptive_budget_topup_max_calls=1,
    )

    assert len(records) == 4
    assert patcher.request_calls == 2
    topup_attempts = [
        record
        for record in records
        if bool(record["attempt_metadata"]["adaptive_budget_topup_granted"])
    ]
    assert len(topup_attempts) == 1
    assert (
        topup_attempts[0]["attempt_metadata"]["adaptive_budget_topup_reason"]
        == "need_more_context_recovery_once"
    )
    assert records[-1]["attempt_metadata"]["adaptive_helper_budget_exhausted"] is True
    assert records[-1]["attempt_metadata"]["adaptive_progress_stop_triggered"] is True


def test_snapshot_justification_generic_detector_handles_variants() -> None:
    assert run_eval._is_generic_snapshot_justification("Need more context.")
    assert run_eval._is_generic_snapshot_justification("Need the snapshot, please!")
    assert not run_eval._is_generic_snapshot_justification(
        "Need snapshot to inspect locals in frame 0 for None-path divergence."
    )


def test_adaptive_reintroduces_stage_a_index_when_context_is_truncated(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_index_fallback"
    context_dir = tmp_path / "context_adaptive_index_fallback"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveGatedCapturePatcher(name="adaptive_index_fallback")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-adaptive-index-fallback",
        include_files_context=False,
        force_patch_attempt=False,
        prompt_budget_total_chars=900,
        prompt_budget_evidence_chars=80,
    )

    assert len(records) == 1
    assert patcher.request_calls == 1
    assert len(patcher.prompts) == 1
    assert records[0]["attempt_metadata"]["stage_a_index_included"] is True
    assert records[0]["attempt_metadata"]["stage_a_index_reason"] == "context_truncated_fallback"


def test_adaptive_postpatch_recompute_avoids_premature_stop_on_signature_change(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_postpatch_recompute"
    context_dir = tmp_path / "context_adaptive_postpatch_recompute"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveToggleDiffPatcher()

    run_outputs = [
        "sig_a",  # initial run
        "sig_a",  # after attempt 1
        "sig_a",  # after attempt 2
        "sig_b",  # after attempt 3 (changed)
        "sig_b",  # after attempt 4
    ]
    call_counter = {"idx": 0}

    def _fake_run_pytest(*args, **kwargs) -> run_eval.RunResult:
        del args, kwargs
        idx = call_counter["idx"]
        call_counter["idx"] += 1
        signature = run_outputs[min(idx, len(run_outputs) - 1)]
        return run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout=f"{signature}\n",
            stderr="",
        )

    monkeypatch.setattr(run_eval, "run_pytest", _fake_run_pytest)
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )
    monkeypatch.setattr(
        run_eval,
        "_build_failure_signature_for_attempt",
        lambda *, run, snapshot: (run.stdout or "").strip(),
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=4,
        output_format="json_compact",
        run_id="run-adaptive-postpatch-recompute",
        include_files_context=False,
        force_patch_attempt=False,
        adaptive_max_helper_calls=1,
    )

    assert len(records) == 4
    third_attempt = records[2]
    assert third_attempt["attempt"] == 3
    assert third_attempt["patch_meta"]["adaptive_progress_recomputed_after_patch"] is True
    assert third_attempt["attempt_metadata"]["adaptive_progress_stop_triggered"] is False


def test_adaptive_patch_verifier_rejects_test_only_patch_before_apply(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_verifier_reject"
    context_dir = tmp_path / "context_adaptive_verifier_reject"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveTestEditPatcher()

    run_calls = {"count": 0}
    apply_calls = {"count": 0}

    def _fake_run_pytest(*args, **kwargs) -> run_eval.RunResult:
        del args, kwargs
        run_calls["count"] += 1
        return run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        )

    def _fake_apply_diff(cwd, diff):
        del cwd, diff
        apply_calls["count"] += 1
        return True, ""

    monkeypatch.setattr(run_eval, "run_pytest", _fake_run_pytest)
    monkeypatch.setattr(run_eval, "apply_diff", _fake_apply_diff)
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-adaptive-verifier-reject",
        include_files_context=False,
        force_patch_attempt=False,
    )

    assert len(records) == 1
    assert run_calls["count"] == 1
    assert apply_calls["count"] == 0
    assert records[0]["note"] == "patch_verifier_rejected"
    assert records[0]["patch_meta"]["patch_verifier_enabled"] is True
    assert records[0]["patch_meta"]["patch_verifier_scope"] == "adaptive_only"
    assert records[0]["patch_meta"]["patch_verifier_passed"] is False
    assert records[0]["attempt_metadata"]["patch_verifier_passed"] is False


def test_adaptive_patch_verifier_allows_neighbor_patch_and_reaches_apply(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_verifier_pass"
    context_dir = tmp_path / "context_adaptive_verifier_pass"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveNeighborEditPatcher()

    run_calls = {"count": 0}
    apply_calls = {"count": 0}

    def _fake_run_pytest(*args, **kwargs) -> run_eval.RunResult:
        del args, kwargs
        run_calls["count"] += 1
        return run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        )

    def _fake_apply_diff(cwd, diff):
        del cwd, diff
        apply_calls["count"] += 1
        return False, "simulated apply failure"

    monkeypatch.setattr(run_eval, "run_pytest", _fake_run_pytest)
    monkeypatch.setattr(run_eval, "apply_diff", _fake_apply_diff)
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-adaptive-verifier-pass",
        include_files_context=False,
        force_patch_attempt=False,
    )

    assert len(records) == 1
    assert run_calls["count"] == 1
    assert apply_calls["count"] == 1
    assert records[0]["note"] == "patch_apply_failed"
    assert records[0]["patch_meta"]["patch_verifier_enabled"] is True
    assert records[0]["patch_meta"]["patch_verifier_passed"] is True


def test_adaptive_patch_verifier_repair_passes_and_reaches_apply(tmp_path, monkeypatch) -> None:
    patcher = _AdaptiveSequentialPatchResultPatcher(
        patch_results=[
            PatchResult(diff=_test_only_expectation_diff(expected=1), meta={"reason": "test_edit"}),
            PatchResult(diff=_helper_fix_diff(), meta={"reason": "verifier_repair"}),
        ]
    )

    records, run_calls, apply_calls = _run_adaptive_verifier_case(
        tmp_path=tmp_path,
        monkeypatch=monkeypatch,
        patcher=patcher,
        run_id="run-adaptive-verifier-repair-pass",
    )

    assert len(records) == 1
    assert run_calls["count"] == 1
    assert apply_calls["count"] == 1
    assert patcher.patch_calls == 2
    assert "Verifier-guided repair request" in patcher.prompts[1]
    assert "test_buggy.py" in patcher.prompts[1]
    assert records[0]["note"] == "patch_apply_failed"
    assert records[0]["patch_meta"]["patch_verifier_passed"] is True
    assert records[0]["patch_meta"]["patch_file_paths"] == ["helper.py"]
    assert records[0]["patch_meta"]["patch_verifier_recovery_eligible"] is True
    assert records[0]["patch_meta"]["patch_verifier_recovery_invoked"] is True
    assert records[0]["patch_meta"]["patch_verifier_recovery_result"] == "passed_verifier"
    assert (
        records[0]["patch_meta"]["patch_verifier_recovery_score_before"]
        < run_eval.PATCH_VERIFIER_MIN_SCORE
    )
    assert records[0]["patch_meta"]["patch_verifier_recovery_patch_files_before"] == [
        "test_buggy.py"
    ]
    assert records[0]["attempt_metadata"]["patch_verifier_recovery_terminal_note"] == (
        "patch_apply_failed"
    )


def test_adaptive_patch_verifier_repair_no_diff_keeps_rejection(tmp_path, monkeypatch) -> None:
    patcher = _AdaptiveSequentialPatchResultPatcher(
        patch_results=[
            PatchResult(diff=_test_only_expectation_diff(expected=1), meta={"reason": "test_edit"}),
            PatchResult(diff=None, meta={"reason": "no_diff", "llm_call_count": 1}),
        ]
    )

    records, run_calls, apply_calls = _run_adaptive_verifier_case(
        tmp_path=tmp_path,
        monkeypatch=monkeypatch,
        patcher=patcher,
        run_id="run-adaptive-verifier-repair-no-diff",
    )

    assert len(records) == 1
    assert run_calls["count"] == 1
    assert apply_calls["count"] == 0
    assert patcher.patch_calls == 2
    assert records[0]["note"] == "patch_verifier_rejected"
    assert records[0]["patch_meta"]["patch_verifier_passed"] is False
    assert records[0]["patch_meta"]["patch_file_paths"] == ["test_buggy.py"]
    assert records[0]["patch_meta"]["patch_verifier_recovery_result"] == "no_diff"
    assert records[0]["patch_meta"]["patcher_request_failed"] is False


def test_adaptive_patch_verifier_repair_request_failure_keeps_main_attempt_state(
    tmp_path, monkeypatch
) -> None:
    patcher = _AdaptiveSequentialPatchResultPatcher(
        patch_results=[
            PatchResult(diff=_test_only_expectation_diff(expected=1), meta={"reason": "test_edit"}),
            PatchResult(
                diff=None,
                meta={
                    "error": "request_failed",
                    "exc": "ReadTimeout: timed out while waiting for response",
                    "llm_call_count": 1,
                },
            ),
        ]
    )

    records, run_calls, apply_calls = _run_adaptive_verifier_case(
        tmp_path=tmp_path,
        monkeypatch=monkeypatch,
        patcher=patcher,
        run_id="run-adaptive-verifier-repair-request-failed",
    )

    assert len(records) == 1
    assert run_calls["count"] == 1
    assert apply_calls["count"] == 0
    assert patcher.patch_calls == 2
    assert records[0]["note"] == "patch_verifier_rejected"
    assert records[0]["patch_meta"]["patch_verifier_recovery_result"] == "request_failed"
    assert records[0]["patch_meta"]["patcher_request_failed"] is False
    assert records[0]["attempt_metadata"]["patcher_request_failed"] is False
    assert records[0]["patch_meta"]["patch_verifier_recovery_raw_meta"]["error"] == (
        "request_failed"
    )


def test_adaptive_patch_verifier_repair_still_rejected_updates_final_candidate(
    tmp_path, monkeypatch
) -> None:
    patcher = _AdaptiveSequentialPatchResultPatcher(
        patch_results=[
            PatchResult(diff=_test_only_expectation_diff(expected=1), meta={"reason": "test_edit"}),
            PatchResult(diff=_helper_non_substantive_diff(), meta={"reason": "non_substantive"}),
        ]
    )

    records, run_calls, apply_calls = _run_adaptive_verifier_case(
        tmp_path=tmp_path,
        monkeypatch=monkeypatch,
        patcher=patcher,
        run_id="run-adaptive-verifier-repair-still-rejected",
    )

    assert len(records) == 1
    assert run_calls["count"] == 1
    assert apply_calls["count"] == 0
    assert patcher.patch_calls == 2
    assert records[0]["note"] == "patch_verifier_rejected"
    assert records[0]["patch_meta"]["patch_verifier_recovery_result"] == "still_rejected"
    assert records[0]["patch_meta"]["patch_file_paths"] == ["helper.py"]
    assert "no_substantive_change" in records[0]["patch_meta"]["patch_verifier_reasons"]
    assert records[0]["patch_meta"]["patch_verifier_recovery_patch_files_before"] == [
        "test_buggy.py"
    ]
    assert (
        "touches_failing_test_file"
        in records[0]["patch_meta"]["patch_verifier_recovery_reasons_before"]
    )


def test_adaptive_patch_verifier_repair_skips_non_substantive_initial_reject(
    tmp_path, monkeypatch
) -> None:
    patcher = _AdaptiveSequentialPatchResultPatcher(
        patch_results=[
            PatchResult(diff=_helper_non_substantive_diff(), meta={"reason": "non_substantive"})
        ]
    )

    records, run_calls, apply_calls = _run_adaptive_verifier_case(
        tmp_path=tmp_path,
        monkeypatch=monkeypatch,
        patcher=patcher,
        run_id="run-adaptive-verifier-repair-not-eligible",
    )

    assert len(records) == 1
    assert run_calls["count"] == 1
    assert apply_calls["count"] == 0
    assert patcher.patch_calls == 1
    assert records[0]["note"] == "patch_verifier_rejected"
    assert records[0]["patch_meta"]["patch_verifier_recovery_eligible"] is False
    assert records[0]["patch_meta"]["patch_verifier_recovery_invoked"] is False
    assert records[0]["patch_meta"]["patch_verifier_recovery_result"] == "not_eligible"


def test_adaptive_patch_verifier_repair_skips_when_attempt_budget_is_exhausted(
    tmp_path, monkeypatch
) -> None:
    patcher = _AdaptiveSequentialPatchResultPatcher(
        name="openai",
        max_propose_sec=5.0,
        patch_results=[
            PatchResult(diff=_test_only_expectation_diff(expected=1), meta={"reason": "test_edit"})
        ],
    )
    remaining_budget_values = iter([5.0, 0.0])

    def _fake_remaining_attempt_budget(_deadline) -> float:
        return next(remaining_budget_values, 0.0)

    monkeypatch.setattr(
        run_eval,
        "_remaining_attempt_propose_budget_sec",
        _fake_remaining_attempt_budget,
    )

    records, run_calls, apply_calls = _run_adaptive_verifier_case(
        tmp_path=tmp_path,
        monkeypatch=monkeypatch,
        patcher=patcher,
        run_id="run-adaptive-verifier-repair-budget-exhausted",
    )

    assert len(records) == 1
    assert run_calls["count"] == 1
    assert apply_calls["count"] == 0
    assert records[0]["note"] == "patch_verifier_rejected"
    assert records[0]["patch_meta"]["patch_file_paths"] == ["test_buggy.py"]
    assert records[0]["patch_meta"]["patch_verifier_recovery_eligible"] is True
    assert records[0]["patch_meta"]["patch_verifier_recovery_invoked"] is False
    assert records[0]["patch_meta"]["patch_verifier_recovery_result"] == (
        "skipped_deadline_exhausted"
    )


def test_adaptive_patch_verifier_infers_source_scope_from_failing_test_file(tmp_path) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_verifier_scope_infer"
    shutil.copytree(case.path, workdir)

    diff = (
        "diff --git a/buggy.py b/buggy.py\n"
        "--- a/buggy.py\n"
        "+++ b/buggy.py\n"
        "@@ -2,4 +2,4 @@\n"
        " \n"
        " \n"
        " def f() -> int:\n"
        "-    return helper.value()\n"
        "+    return helper.value() + 0\n"
    )
    snapshot = {
        "exception": {"type": "AssertionError", "message": "expected 2 got 1"},
        "frames": [
            {
                "file_rel": "tests/test_buggy.py",
                "line": 4,
                "function": "test_f",
                "code": "assert f() == 2",
                "locals": {},
            }
        ],
    }
    stdout = "test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n"

    passed_without_infer, meta_without_infer = run_eval._score_patch_relevance(
        diff=diff,
        cwd=workdir,
        snapshot=snapshot,
        stdout=stdout,
        stderr="",
        infer_test_source_scope=False,
    )
    passed_with_infer, meta_with_infer = run_eval._score_patch_relevance(
        diff=diff,
        cwd=workdir,
        snapshot=snapshot,
        stdout=stdout,
        stderr="",
        infer_test_source_scope=True,
    )

    assert passed_without_infer is False
    assert meta_without_infer["patch_verifier_source_scope_matched"] is False
    assert passed_with_infer is True
    assert meta_with_infer["patch_verifier_source_scope_matched"] is True
    assert "buggy.py" in meta_with_infer["patch_verifier_inferred_source_scope"]


def test_adaptive_patch_verifier_infers_scope_from_subprocess_target(tmp_path) -> None:
    workdir = tmp_path / "workdir_adaptive_verifier_subprocess_scope"
    (workdir / "tests").mkdir(parents=True, exist_ok=True)
    (workdir / "buggy.py").write_text("def f() -> int:\n    return 1\n", encoding="utf-8")
    (workdir / "tests" / "test_cli.py").write_text(
        (
            "import subprocess\n\n"
            "def test_cli() -> None:\n"
            "    subprocess.run(['python', 'buggy.py'])\n"
            "    assert True\n"
        ),
        encoding="utf-8",
    )

    diff = (
        "diff --git a/buggy.py b/buggy.py\n"
        "--- a/buggy.py\n"
        "+++ b/buggy.py\n"
        "@@ -1,2 +1,2 @@\n"
        " def f() -> int:\n"
        "-    return 1\n"
        "+    return 2\n"
    )
    snapshot = {
        "exception": {"type": "AssertionError", "message": "cli mismatch"},
        "frames": [
            {
                "file_rel": "tests/test_cli.py",
                "line": 4,
                "function": "test_cli",
                "code": "assert True",
                "locals": {},
            }
        ],
    }
    stdout = "tests/test_cli.py::test_cli\nE   AssertionError: cli mismatch\n"

    passed_without_infer, _meta_without_infer = run_eval._score_patch_relevance(
        diff=diff,
        cwd=workdir,
        snapshot=snapshot,
        stdout=stdout,
        stderr="",
        infer_test_source_scope=False,
    )
    passed_with_infer, meta_with_infer = run_eval._score_patch_relevance(
        diff=diff,
        cwd=workdir,
        snapshot=snapshot,
        stdout=stdout,
        stderr="",
        infer_test_source_scope=True,
    )

    assert passed_without_infer is False
    assert passed_with_infer is True
    assert meta_with_infer["patch_verifier_source_scope_matched"] is True
    assert "buggy.py" in meta_with_infer["patch_verifier_inferred_source_scope"]


def test_patch_verifier_scope_match_avoids_ambiguous_basename_false_positive() -> None:
    ambiguous_scope = {"pkg/a/utils.py", "pkg/b/utils.py"}
    assert (
        run_eval._patch_mentions_scope_files(
            patch_files=["other/utils.py"],
            scope_files=ambiguous_scope,
        )
        is False
    )
    assert (
        run_eval._patch_mentions_scope_files(
            patch_files=["utils.py"],
            scope_files=ambiguous_scope,
        )
        is False
    )

    unambiguous_scope = {"pkg/a/buggy.py"}
    assert (
        run_eval._patch_mentions_scope_files(
            patch_files=["buggy.py"],
            scope_files=unambiguous_scope,
        )
        is True
    )
    assert (
        run_eval._patch_mentions_scope_files(
            patch_files=["other/buggy.py"],
            scope_files=unambiguous_scope,
        )
        is False
    )


def test_syntax_preflight_retries_within_same_attempt(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_syntax_preflight_retry"
    context_dir = tmp_path / "context_syntax_preflight_retry"
    shutil.copytree(case.path, workdir)
    patcher = _SyntaxCyclingPatcher(
        name="syntax_preflight_retry",
        diffs=[
            (
                "diff --git a/helper.py b/helper.py\n"
                "--- a/helper.py\n"
                "+++ b/helper.py\n"
                "@@ -1,2 +1,2 @@\n"
                " def value() -> int:\n"
                "-    return 1\n"
                "+    return (\n"
            ),
            (
                "diff --git a/helper.py b/helper.py\n"
                "--- a/helper.py\n"
                "+++ b/helper.py\n"
                "@@ -1,2 +1,2 @@\n"
                " def value() -> int:\n"
                "-    return 1\n"
                "+    return 2\n"
            ),
        ],
    )

    run_calls = {"count": 0}

    def _fake_run_pytest(*args, **kwargs) -> run_eval.RunResult:
        del args, kwargs
        run_calls["count"] += 1
        if run_calls["count"] == 1:
            return run_eval.RunResult(
                returncode=1,
                duration_sec=0.01,
                stdout="test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
                stderr="",
            )
        helper_text = (workdir / "helper.py").read_text(encoding="utf-8")
        if "return 2" in helper_text and "return (" not in helper_text:
            return run_eval.RunResult(
                returncode=0,
                duration_sec=0.01,
                stdout="1 passed\n",
                stderr="",
            )
        return run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="E   SyntaxError: '(' was never closed\n",
            stderr="",
        )

    monkeypatch.setattr(run_eval, "run_pytest", _fake_run_pytest)
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.WITH_SNAPSHOT,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-syntax-preflight-retry",
        include_files_context=False,
        force_patch_attempt=False,
        syntax_preflight_enabled=True,
        syntax_preflight_max_cycles=2,
    )

    assert len(records) == 1
    assert run_calls["count"] == 2
    assert patcher.patch_calls == 2
    assert records[0]["success"] is True
    assert records[0]["note"] == "success"
    assert records[0]["attempt"] == 1
    assert records[0]["attempt_metadata"]["syntax_preflight_cycles_used"] == 2
    assert records[0]["attempt_metadata"]["syntax_preflight_failures"] == 1
    events = records[0]["patch_meta"]["syntax_preflight_events"]
    assert len(events) == 2
    assert events[0]["passed"] is False
    assert events[1]["passed"] is True


def test_syntax_preflight_exhaustion_marks_attempt_failed(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_syntax_preflight_failed"
    context_dir = tmp_path / "context_syntax_preflight_failed"
    shutil.copytree(case.path, workdir)
    patcher = _SyntaxCyclingPatcher(
        name="syntax_preflight_failed",
        diffs=[
            (
                "diff --git a/helper.py b/helper.py\n"
                "--- a/helper.py\n"
                "+++ b/helper.py\n"
                "@@ -1,2 +1,2 @@\n"
                " def value() -> int:\n"
                "-    return 1\n"
                "+    return (\n"
            )
        ],
    )

    run_calls = {"count": 0}

    def _fake_run_pytest(*args, **kwargs) -> run_eval.RunResult:
        del args, kwargs
        run_calls["count"] += 1
        return run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        )

    monkeypatch.setattr(run_eval, "run_pytest", _fake_run_pytest)
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.WITH_SNAPSHOT,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-syntax-preflight-failed",
        include_files_context=False,
        force_patch_attempt=False,
        syntax_preflight_enabled=True,
        syntax_preflight_max_cycles=2,
    )

    assert len(records) == 1
    assert run_calls["count"] == 1
    assert patcher.patch_calls == 2
    assert records[0]["success"] is False
    assert records[0]["note"] == "syntax_preflight_failed"
    assert records[0]["attempt_metadata"]["syntax_preflight_cycles_used"] == 2
    assert records[0]["attempt_metadata"]["syntax_preflight_failures"] == 2
    assert records[0]["attempt_metadata"]["syntax_preflight_last_error_type"] == "PyCompileError"
    events = records[0]["patch_meta"]["syntax_preflight_events"]
    assert len(events) == 2
    assert all(event["passed"] is False for event in events)


def test_syntax_preflight_clears_transient_patch_meta_between_cycles(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_syntax_preflight_meta_cleanup"
    context_dir = tmp_path / "context_syntax_preflight_meta_cleanup"
    shutil.copytree(case.path, workdir)
    patcher = _SyntaxCyclingPatcher(
        name="syntax_preflight_meta_cleanup",
        diffs=[
            (
                "diff --git a/helper.py b/helper.py\n"
                "--- a/helper.py\n"
                "+++ b/helper.py\n"
                "@@ -1,2 +1,2 @@\n"
                " def value() -> int:\n"
                "-    return 1\n"
                "+    return (\n"
            ),
            (
                "diff --git a/helper.py b/helper.py\n"
                "--- a/helper.py\n"
                "+++ b/helper.py\n"
                "@@ -1,2 +1,2 @@\n"
                " def value() -> int:\n"
                "-    return 1\n"
                "+    return 2\n"
            ),
        ],
        metas=[
            {"runner_forced_patch_attempt": True},
            {},
        ],
    )

    run_calls = {"count": 0}

    def _fake_run_pytest(*args, **kwargs) -> run_eval.RunResult:
        del args, kwargs
        run_calls["count"] += 1
        if run_calls["count"] == 1:
            return run_eval.RunResult(
                returncode=1,
                duration_sec=0.01,
                stdout="test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
                stderr="",
            )
        return run_eval.RunResult(
            returncode=0,
            duration_sec=0.01,
            stdout="1 passed\n",
            stderr="",
        )

    monkeypatch.setattr(run_eval, "run_pytest", _fake_run_pytest)
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.WITH_SNAPSHOT,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-syntax-preflight-meta-cleanup",
        include_files_context=False,
        force_patch_attempt=False,
        syntax_preflight_enabled=True,
        syntax_preflight_max_cycles=2,
    )

    assert len(records) == 1
    assert records[0]["success"] is True
    assert patcher.patch_calls == 2
    assert "runner_forced_patch_attempt" not in records[0]["patch_meta"]


def test_patch_sanitize_reject_then_retry_success(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_patch_sanitize_retry"
    context_dir = tmp_path / "context_patch_sanitize_retry"
    shutil.copytree(case.path, workdir)
    patcher = _SyntaxCyclingPatcher(
        name="patch_sanitize_retry",
        diffs=[
            (
                "diff --git a/helper.py b/helper.py\n"
                "--- a/helper.py\n"
                "+++ b/helper.py\n"
                "@@ -1,2 +1,3 @@\n"
                " def value() -> int:\n"
                "+    # keep \\n marker\n"
                "     return 1\n"
            ),
            (
                "diff --git a/helper.py b/helper.py\n"
                "--- a/helper.py\n"
                "+++ b/helper.py\n"
                "@@ -1,2 +1,2 @@\n"
                " def value() -> int:\n"
                "-    return 1\n"
                "+    return 2\n"
            ),
        ],
    )

    run_calls = {"count": 0}

    def _fake_run_pytest(*args, **kwargs) -> run_eval.RunResult:
        del args, kwargs
        run_calls["count"] += 1
        if run_calls["count"] == 1:
            return run_eval.RunResult(
                returncode=1,
                duration_sec=0.01,
                stdout="test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
                stderr="",
            )
        return run_eval.RunResult(
            returncode=0,
            duration_sec=0.01,
            stdout="1 passed\n",
            stderr="",
        )

    monkeypatch.setattr(run_eval, "run_pytest", _fake_run_pytest)
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.WITH_SNAPSHOT,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-patch-sanitize-retry",
        include_files_context=False,
        force_patch_attempt=False,
        syntax_preflight_enabled=True,
        syntax_preflight_max_cycles=2,
        patch_sanitize_enabled=True,
    )

    assert len(records) == 1
    assert records[0]["success"] is True
    assert run_calls["count"] == 2
    assert patcher.patch_calls == 2
    assert records[0]["attempt_metadata"]["syntax_preflight_failures"] == 1
    events = records[0]["patch_meta"]["syntax_preflight_events"]
    assert len(events) == 2
    assert events[0]["passed"] is False
    assert events[0]["syntax_preflight_error_type"] == "PatchSanitizationError"
    assert events[1]["passed"] is True


def test_patch_sanitize_exhaustion_marks_attempt_failed(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_patch_sanitize_failed"
    context_dir = tmp_path / "context_patch_sanitize_failed"
    shutil.copytree(case.path, workdir)
    patcher = _SyntaxCyclingPatcher(
        name="patch_sanitize_failed",
        diffs=[
            (
                "diff --git a/helper.py b/helper.py\n"
                "--- a/helper.py\n"
                "+++ b/helper.py\n"
                "@@ -1,2 +1,3 @@\n"
                " def value() -> int:\n"
                "+    # keep \\n marker\n"
                "     return 1\n"
            )
        ],
    )

    run_calls = {"count": 0}

    def _fake_run_pytest(*args, **kwargs) -> run_eval.RunResult:
        del args, kwargs
        run_calls["count"] += 1
        return run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        )

    monkeypatch.setattr(run_eval, "run_pytest", _fake_run_pytest)
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.WITH_SNAPSHOT,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-patch-sanitize-failed",
        include_files_context=False,
        force_patch_attempt=False,
        syntax_preflight_enabled=True,
        syntax_preflight_max_cycles=2,
        patch_sanitize_enabled=True,
    )

    assert len(records) == 1
    assert records[0]["success"] is False
    assert records[0]["note"] == "syntax_preflight_failed"
    assert run_calls["count"] == 1
    assert patcher.patch_calls == 2
    assert records[0]["attempt_metadata"]["syntax_preflight_failures"] == 2
    assert (
        records[0]["attempt_metadata"]["syntax_preflight_last_error_type"]
        == "PatchSanitizationError"
    )
    assert records[0]["patch_meta"]["patch_sanitize_rejected"] is True
    assert records[0]["patch_meta"]["patch_sanitize_reasons"]


def test_patch_sanitize_disabled_allows_patch_to_apply(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_patch_sanitize_disabled"
    context_dir = tmp_path / "context_patch_sanitize_disabled"
    shutil.copytree(case.path, workdir)
    patcher = _SyntaxCyclingPatcher(
        name="patch_sanitize_disabled",
        diffs=[
            (
                "diff --git a/helper.py b/helper.py\n"
                "--- a/helper.py\n"
                "+++ b/helper.py\n"
                "@@ -1,2 +1,3 @@\n"
                " def value() -> int:\n"
                "+    # keep \\n marker\n"
                "     return 1\n"
            )
        ],
    )

    run_calls = {"count": 0}

    def _fake_run_pytest(*args, **kwargs) -> run_eval.RunResult:
        del args, kwargs
        run_calls["count"] += 1
        if run_calls["count"] == 1:
            return run_eval.RunResult(
                returncode=1,
                duration_sec=0.01,
                stdout="test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
                stderr="",
            )
        return run_eval.RunResult(
            returncode=0,
            duration_sec=0.01,
            stdout="1 passed\n",
            stderr="",
        )

    monkeypatch.setattr(run_eval, "run_pytest", _fake_run_pytest)
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.WITH_SNAPSHOT,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-patch-sanitize-disabled",
        include_files_context=False,
        force_patch_attempt=False,
        syntax_preflight_enabled=False,
        patch_sanitize_enabled=False,
    )

    assert len(records) == 1
    assert records[0]["success"] is True
    assert run_calls["count"] == 2
    assert patcher.patch_calls == 1
    assert records[0]["patch_meta"]["patch_sanitize_enabled"] is False
    assert records[0]["patch_meta"]["patch_sanitize_rejected"] is False


def test_openai_response_mode_autoswitch_on_patch_sanitize_retry(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_openai_autoswitch_sanitize"
    context_dir = tmp_path / "context_openai_autoswitch_sanitize"
    shutil.copytree(case.path, workdir)

    captured_response_modes: list[str] = []
    diffs = [
        (
            "diff --git a/helper.py b/helper.py\n"
            "--- a/helper.py\n"
            "+++ b/helper.py\n"
            "@@ -1,2 +1,3 @@\n"
            " def value() -> int:\n"
            "+    # keep \\n marker\n"
            "     return 1\n"
        ),
        (
            "diff --git a/helper.py b/helper.py\n"
            "--- a/helper.py\n"
            "+++ b/helper.py\n"
            "@@ -1,2 +1,2 @@\n"
            " def value() -> int:\n"
            "-    return 1\n"
            "+    return 2\n"
        ),
    ]

    def _fake_propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del prompt, context_dir, cwd
        captured_response_modes.append(str(self.response_mode))
        idx = min(len(captured_response_modes) - 1, len(diffs) - 1)
        return PatchResult(diff=diffs[idx], meta={"response_mode": self.response_mode})

    run_calls = {"count": 0}

    def _fake_run_pytest(*args, **kwargs) -> run_eval.RunResult:
        del args, kwargs
        run_calls["count"] += 1
        if run_calls["count"] == 1:
            return run_eval.RunResult(
                returncode=1,
                duration_sec=0.01,
                stdout="test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
                stderr="",
            )
        return run_eval.RunResult(
            returncode=0,
            duration_sec=0.01,
            stdout="1 passed\n",
            stderr="",
        )

    monkeypatch.setattr(OpenAICompatPatcher, "propose_patch", _fake_propose_patch)
    monkeypatch.setattr(run_eval, "run_pytest", _fake_run_pytest)
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    patcher = OpenAICompatPatcher(
        base_url="http://127.0.0.1:1234",
        model="dummy",
        response_mode="edits",
        force_patch_attempt=False,
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.WITH_SNAPSHOT,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-openai-autoswitch-sanitize",
        include_files_context=False,
        force_patch_attempt=False,
        syntax_preflight_enabled=True,
        syntax_preflight_max_cycles=2,
        patch_sanitize_enabled=True,
        openai_response_mode_autoswitch="retry_to_diff",
    )

    assert len(records) == 1
    assert records[0]["success"] is True
    assert run_calls["count"] == 2
    assert captured_response_modes == ["edits", "diff"]
    assert records[0]["patch_meta"]["openai_response_mode_autoswitched"] is True
    assert records[0]["patch_meta"]["openai_response_mode_autoswitch_reason"] == (
        "patch_sanitize_rejected"
    )
    assert records[0]["model_metadata"]["response_mode"] == "diff"


def test_openai_response_mode_autoswitch_on_syntax_preflight_retry(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_openai_autoswitch_syntax"
    context_dir = tmp_path / "context_openai_autoswitch_syntax"
    shutil.copytree(case.path, workdir)

    captured_response_modes: list[str] = []
    diffs = [
        (
            "diff --git a/helper.py b/helper.py\n"
            "--- a/helper.py\n"
            "+++ b/helper.py\n"
            "@@ -1,2 +1,2 @@\n"
            " def value() -> int:\n"
            "-    return 1\n"
            "+    return (\n"
        ),
        (
            "diff --git a/helper.py b/helper.py\n"
            "--- a/helper.py\n"
            "+++ b/helper.py\n"
            "@@ -1,2 +1,2 @@\n"
            " def value() -> int:\n"
            "-    return 1\n"
            "+    return 2\n"
        ),
    ]

    def _fake_propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
        del prompt, context_dir, cwd
        captured_response_modes.append(str(self.response_mode))
        idx = min(len(captured_response_modes) - 1, len(diffs) - 1)
        return PatchResult(diff=diffs[idx], meta={"response_mode": self.response_mode})

    run_calls = {"count": 0}

    def _fake_run_pytest(*args, **kwargs) -> run_eval.RunResult:
        del args, kwargs
        run_calls["count"] += 1
        if run_calls["count"] == 1:
            return run_eval.RunResult(
                returncode=1,
                duration_sec=0.01,
                stdout="test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
                stderr="",
            )
        helper_text = (workdir / "helper.py").read_text(encoding="utf-8")
        if "return 2" in helper_text and "return (" not in helper_text:
            return run_eval.RunResult(
                returncode=0,
                duration_sec=0.01,
                stdout="1 passed\n",
                stderr="",
            )
        return run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="E   SyntaxError: '(' was never closed\n",
            stderr="",
        )

    monkeypatch.setattr(OpenAICompatPatcher, "propose_patch", _fake_propose_patch)
    monkeypatch.setattr(run_eval, "run_pytest", _fake_run_pytest)
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    patcher = OpenAICompatPatcher(
        base_url="http://127.0.0.1:1234",
        model="dummy",
        response_mode="edits",
        force_patch_attempt=False,
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.WITH_SNAPSHOT,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-openai-autoswitch-syntax",
        include_files_context=False,
        force_patch_attempt=False,
        syntax_preflight_enabled=True,
        syntax_preflight_max_cycles=2,
        patch_sanitize_enabled=False,
        openai_response_mode_autoswitch="retry_to_diff",
    )

    assert len(records) == 1
    assert records[0]["success"] is True
    assert run_calls["count"] == 2
    assert captured_response_modes == ["edits", "diff"]
    assert records[0]["patch_meta"]["openai_response_mode_autoswitched"] is True
    assert records[0]["patch_meta"]["openai_response_mode_autoswitch_reason"] == (
        "syntax_preflight_failed"
    )
    assert records[0]["model_metadata"]["response_mode"] == "diff"


def test_adaptive_online_policy_shadow_keeps_baseline_action(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_online_shadow"
    context_dir = tmp_path / "context_adaptive_online_shadow"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveGatedCapturePatcher(name="adaptive_online_shadow")
    controller, events_path = _skip_biased_online_controller(tmp_path, mode="shadow")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=1,
            output_format="json_compact",
            run_id="run-adaptive-online-shadow",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_online_policy_controller=controller,
        )
    finally:
        controller.close()

    assert len(records) == 1
    assert patcher.request_calls == 1
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_online_policy_mode"] == "shadow"
    assert attempt_meta["adaptive_online_action_selected"] == ACTION_SKIP_HELPER
    assert attempt_meta["adaptive_online_action_applied"] == ACTION_INVOKE_HELPER
    assert attempt_meta["adaptive_online_override_reason"] == "shadow_mode_baseline"
    assert attempt_meta["adaptive_online_behavior_action"] == ACTION_INVOKE_HELPER
    assert attempt_meta["adaptive_online_behavior_propensity"] == 1.0
    assert attempt_meta["adaptive_online_feature_scaling_profile"] == "v1"
    assert isinstance(attempt_meta["adaptive_online_feature_vector_raw"], dict)
    assert attempt_meta["adaptive_online_updated"] is False
    assert attempt_meta["adaptive_online_update_reason"] == "shadow_no_update"
    assert attempt_meta["adaptive_online_event_logged"] is True
    events = [line for line in events_path.read_text(encoding="utf-8").splitlines() if line.strip()]
    assert len(events) == 1


def test_adaptive_online_policy_on_can_skip_helper(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_online_on"
    context_dir = tmp_path / "context_adaptive_online_on"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveGatedCapturePatcher(name="adaptive_online_on")
    controller, events_path = _skip_biased_online_controller(tmp_path, mode="on")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=1,
            output_format="json_compact",
            run_id="run-adaptive-online-on",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_online_policy_controller=controller,
        )
    finally:
        controller.close()

    assert len(records) == 1
    assert patcher.request_calls == 0
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_helper_decision_reason"] == "online_policy_skip_helper"
    assert attempt_meta["adaptive_online_policy_mode"] == "on"
    assert attempt_meta["adaptive_online_action_selected"] == ACTION_SKIP_HELPER
    assert attempt_meta["adaptive_online_action_applied"] == ACTION_SKIP_HELPER
    assert attempt_meta["adaptive_online_behavior_action"] == ACTION_SKIP_HELPER
    assert attempt_meta["adaptive_online_behavior_propensity"] > 0.0
    assert attempt_meta["adaptive_online_feature_scaling_profile"] == "v1"
    assert isinstance(attempt_meta["adaptive_online_feature_vector_raw"], dict)
    assert isinstance(attempt_meta["adaptive_online_feature_vector"], dict)
    assert attempt_meta["adaptive_online_updated"] is True
    assert attempt_meta["adaptive_online_update_reason"] == "on_policy_update"
    assert attempt_meta["adaptive_online_event_logged"] is True
    events = [line for line in events_path.read_text(encoding="utf-8").splitlines() if line.strip()]
    assert len(events) == 1


def test_adaptive_online_policy_suppresses_update_for_invalid_helper_payload(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_online_invalid_payload"
    context_dir = tmp_path / "context_adaptive_online_invalid_payload"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveDiscretionInvalidPatcher()
    controller, events_path = _invoke_biased_online_controller(tmp_path, mode="on")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=1,
            output_format="json_compact",
            run_id="run-adaptive-online-invalid-payload",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_online_policy_controller=controller,
        )
    finally:
        controller.close()

    assert len(records) == 1
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_helper_outcome"] == "invalid_payload"
    assert attempt_meta["adaptive_online_updated"] is False
    assert attempt_meta["adaptive_online_update_reason"] == "helper_invalid_payload_no_update"
    assert attempt_meta["adaptive_online_event_logged"] is True
    events = [line for line in events_path.read_text(encoding="utf-8").splitlines() if line.strip()]
    assert len(events) == 1


def test_adaptive_online_policy_suppresses_update_for_helper_backend_unavailable(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_online_transport_failure"
    context_dir = tmp_path / "context_adaptive_online_transport_failure"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveDiscretionBackendUnavailablePatcher()
    controller, events_path = _invoke_biased_online_controller(tmp_path, mode="on")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=1,
            output_format="json_compact",
            run_id="run-adaptive-online-transport-failure",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_online_policy_controller=controller,
        )
    finally:
        controller.close()

    assert len(records) == 1
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_helper_outcome"] == "unavailable_backend"
    assert attempt_meta["adaptive_online_updated"] is False
    assert attempt_meta["adaptive_online_update_reason"] == "helper_backend_unavailable_no_update"
    assert attempt_meta["adaptive_online_event_logged"] is True
    events = [line for line in events_path.read_text(encoding="utf-8").splitlines() if line.strip()]
    assert len(events) == 1


def test_adaptive_online_policy_suppresses_update_for_helper_transport_failure(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_online_transport_failure_retryable"
    context_dir = tmp_path / "context_adaptive_online_transport_failure_retryable"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveDiscretionTransportFailurePatcher()
    controller, events_path = _invoke_biased_online_controller(tmp_path, mode="on")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=1,
            output_format="json_compact",
            run_id="run-adaptive-online-transport-failure-retryable",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_online_policy_controller=controller,
        )
    finally:
        controller.close()

    assert len(records) == 1
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_helper_outcome"] == "transport_failed"
    assert attempt_meta["adaptive_online_updated"] is False
    assert attempt_meta["adaptive_online_update_reason"] == "helper_transport_no_update"
    assert attempt_meta["adaptive_online_event_logged"] is True
    events = [line for line in events_path.read_text(encoding="utf-8").splitlines() if line.strip()]
    assert len(events) == 1


def test_adaptive_online_policy_updates_for_helper_request_budget_exceeded(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_online_request_budget_exceeded"
    context_dir = tmp_path / "context_adaptive_online_request_budget_exceeded"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveDiscretionRequestBudgetPatcher()
    controller, events_path = _invoke_biased_online_controller(tmp_path, mode="on")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=1,
            output_format="json_compact",
            run_id="run-adaptive-online-request-budget-exceeded",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_online_policy_controller=controller,
        )
    finally:
        controller.close()

    assert len(records) == 1
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_helper_outcome"] == "request_budget_exceeded"
    assert attempt_meta["adaptive_helper_failure_kind"] == "request_budget_exceeded"
    assert attempt_meta["adaptive_helper_calls_used_after_attempt"] == 1
    assert attempt_meta["adaptive_online_updated"] is True
    assert attempt_meta["adaptive_online_update_reason"] == "on_policy_update"
    assert attempt_meta["adaptive_online_event_logged"] is True
    assert records[0]["patch_meta"]["stage_a_reason"] == "helper_request_budget_exceeded"
    events = [line for line in events_path.read_text(encoding="utf-8").splitlines() if line.strip()]
    assert len(events) == 1


def test_adaptive_stagnation_override_forces_helper_after_policy_skip(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_stagnation_override"
    context_dir = tmp_path / "context_adaptive_stagnation_override"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveGatedCapturePatcher(name="adaptive_stagnation_override")
    controller, _events_path = _skip_biased_online_controller(tmp_path, mode="on")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=2,
            output_format="json_compact",
            run_id="run-adaptive-stagnation-override",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_online_policy_controller=controller,
        )
    finally:
        controller.close()

    assert len(records) == 2
    assert patcher.request_calls == 1
    assert records[0]["attempt_metadata"]["adaptive_stagnation_override_applied"] is False
    attempt_meta = records[1]["attempt_metadata"]
    assert attempt_meta["adaptive_stagnation_override_applied"] is True
    assert (
        attempt_meta["adaptive_stagnation_override_reason"]
        == "repeated_no_progress_after_stagnation_note"
    )
    assert attempt_meta["adaptive_helper_invoked"] is True
    assert attempt_meta["adaptive_online_action_applied"] == ACTION_SKIP_HELPER
    assert attempt_meta["adaptive_online_behavior_action"] == ACTION_INVOKE_HELPER
    assert attempt_meta["adaptive_online_behavior_propensity"] == 1.0

    events = [
        json.loads(line) for line in _events_path.read_text(encoding="utf-8").splitlines() if line
    ]
    assert len(events) == 2
    assert events[1]["applied_action"] == ACTION_SKIP_HELPER
    assert events[1]["behavior_action"] == ACTION_INVOKE_HELPER
    assert events[1]["behavior_propensity"] == 1.0


def test_adaptive_online_feature_vector_includes_extended_and_lagged_signals(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_online_features"
    context_dir = tmp_path / "context_adaptive_online_features"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveDiscretionInvalidPatcher(name="adaptive_online_feature_keys")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    records = run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=2,
        output_format="json_compact",
        run_id="run-adaptive-online-feature-keys",
        include_files_context=False,
        force_patch_attempt=False,
        adaptive_informativeness_pregate=True,
    )

    assert len(records) == 2
    feature_vector_1 = records[0]["attempt_metadata"]["adaptive_online_feature_vector"]
    feature_vector_2 = records[1]["attempt_metadata"]["adaptive_online_feature_vector"]
    expected_keys = {
        "attempts_remaining_frac",
        "helper_budget_remaining_frac",
        "helper_topup_used_frac",
        "helper_topup_remaining_frac",
        "context_compaction_enabled",
        "supports_context_compaction",
        "context_compaction_calls_used_frac",
        "context_compaction_calls_remaining_frac",
        "last_context_compaction_requested",
        "last_context_compaction_invoked",
        "last_context_compaction_success",
        "last_context_compaction_ratio",
        "last_helper_need_more_context",
        "last_helper_invalid_response",
        "last_helper_declined",
        "case_difficulty_ord",
        "case_snapshot_dependency_ord",
        "case_bug_source_imported",
        "pregate_exception_is_assertion",
        "pregate_exception_is_timeout",
        "pregate_exception_is_syntax",
    }
    assert expected_keys.issubset(feature_vector_1.keys())
    assert expected_keys.issubset(feature_vector_2.keys())
    raw_feature_vector_1 = records[0]["attempt_metadata"]["adaptive_online_feature_vector_raw"]
    assert expected_keys.issubset(raw_feature_vector_1.keys())
    assert feature_vector_1["attempts_remaining_frac"] == 0.5
    assert feature_vector_2["attempts_remaining_frac"] == 0.0
    assert feature_vector_1["case_difficulty_ord"] == 0.0
    assert feature_vector_1["case_snapshot_dependency_ord"] == 0.5
    assert feature_vector_1["case_bug_source_imported"] == 0.0
    assert feature_vector_1["pregate_exception_is_assertion"] == 1.0
    assert feature_vector_1["last_helper_invalid_response"] == 0.0
    assert feature_vector_2["last_helper_invalid_response"] == 1.0


def test_adaptive_online_policy_logging_failure_does_not_abort_attempt(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_online_logging_failure"
    context_dir = tmp_path / "context_adaptive_online_logging_failure"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveGatedCapturePatcher(name="adaptive_online_logging_failure")
    controller, _events_path = _skip_biased_online_controller(tmp_path, mode="on")

    def _raise_record_outcome(**kwargs):
        del kwargs
        raise OSError("simulated filesystem failure")

    monkeypatch.setattr(controller, "record_outcome", _raise_record_outcome)
    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=1,
            output_format="json_compact",
            run_id="run-adaptive-online-logging-failure",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_online_policy_controller=controller,
        )
    finally:
        controller.close()

    assert len(records) == 1
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_online_updated"] is False
    assert attempt_meta["adaptive_online_event_logged"] is False
    assert attempt_meta["adaptive_online_update_reason"] == "event_logging_error:OSError"


def test_adaptive_budget_policy_shadow_keeps_baseline_budget(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_budget_shadow"
    context_dir = tmp_path / "context_adaptive_budget_shadow"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveBudgetNoDiffPatcher()
    controller, events_path = _increase_biased_budget_controller(tmp_path, mode="shadow")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=1,
            output_format="json_compact",
            run_id="run-adaptive-budget-shadow",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_budget_policy_controller=controller,
        )
    finally:
        controller.close()

    assert len(records) == 1
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_budget_policy_mode"] == "shadow"
    assert attempt_meta["adaptive_budget_action_selected"] == ACTION_INCREASE_BUDGET
    assert attempt_meta["adaptive_budget_action_applied"] == ACTION_KEEP_BUDGET
    assert attempt_meta["adaptive_budget_behavior_action"] == ACTION_KEEP_BUDGET
    assert attempt_meta["adaptive_budget_behavior_propensity"] == 1.0
    assert attempt_meta["adaptive_budget_feature_scaling_profile"] == "v1"
    assert isinstance(attempt_meta["adaptive_budget_feature_vector_raw"], dict)
    assert attempt_meta["adaptive_budget_updated"] is False
    assert attempt_meta["adaptive_budget_update_reason"] == "shadow_no_update"
    assert attempt_meta["adaptive_budget_event_logged"] is True
    assert attempt_meta["adaptive_budget_default_control_values"]["helper_max_calls"] == 1
    assert attempt_meta["adaptive_budget_selected_control_values"]["helper_max_calls"] == 2
    assert attempt_meta["adaptive_budget_applied_control_values"]["helper_max_calls"] == 1
    assert attempt_meta["adaptive_budget_selected_controls_changed"]["stage_a_requests"] is True
    assert attempt_meta["adaptive_budget_applied_controls_changed"]["stage_a_requests"] is False
    assert attempt_meta["attempt_shared_propose_budget_base_sec"] == 10.0
    assert attempt_meta["attempt_shared_propose_budget_multiplier"] == 1.0
    assert attempt_meta["attempt_shared_propose_budget_sec"] == 10.0
    events = [line for line in events_path.read_text(encoding="utf-8").splitlines() if line.strip()]
    assert len(events) == 1


def test_adaptive_budget_policy_on_applies_multiplier(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_budget_on"
    context_dir = tmp_path / "context_adaptive_budget_on"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveBudgetNoDiffPatcher()
    controller, events_path = _increase_biased_budget_controller(tmp_path, mode="on")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=1,
            output_format="json_compact",
            run_id="run-adaptive-budget-on",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_budget_policy_controller=controller,
        )
    finally:
        controller.close()

    assert len(records) == 1
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_budget_policy_mode"] == "on"
    assert attempt_meta["adaptive_budget_action_selected"] == ACTION_INCREASE_BUDGET
    assert attempt_meta["adaptive_budget_action_applied"] == ACTION_INCREASE_BUDGET
    assert attempt_meta["adaptive_budget_behavior_action"] == ACTION_INCREASE_BUDGET
    assert attempt_meta["adaptive_budget_behavior_propensity"] > 0.0
    assert attempt_meta["adaptive_budget_feature_scaling_profile"] == "v1"
    assert isinstance(attempt_meta["adaptive_budget_feature_vector_raw"], dict)
    assert isinstance(attempt_meta["adaptive_budget_feature_vector"], dict)
    assert attempt_meta["adaptive_budget_updated"] is True
    assert attempt_meta["adaptive_budget_update_reason"] == "on_policy_update"
    assert attempt_meta["adaptive_budget_event_logged"] is True
    assert attempt_meta["adaptive_budget_tier_index_before"] == 0
    assert attempt_meta["adaptive_budget_tier_index_after"] == 1
    assert attempt_meta["adaptive_budget_ups_used"] == 1
    assert attempt_meta["adaptive_budget_default_control_values"]["openai_max_tokens"] == 400
    assert attempt_meta["adaptive_budget_selected_control_values"]["openai_max_tokens"] == 600
    assert attempt_meta["adaptive_budget_applied_control_values"]["stage_a_max_requests"] == 7
    assert attempt_meta["adaptive_budget_applied_controls_changed"]["retry_history_budget"] is True
    assert attempt_meta["attempt_shared_propose_budget_base_sec"] == 10.0
    assert attempt_meta["attempt_shared_propose_budget_multiplier"] == 1.5
    assert attempt_meta["attempt_shared_propose_budget_sec"] == 15.0
    assert attempt_meta["adaptive_budget_partial_override"] is False
    assert attempt_meta["adaptive_budget_effective_openai_max_tokens"] == 600
    events = [line for line in events_path.read_text(encoding="utf-8").splitlines() if line.strip()]
    assert len(events) == 1


def test_adaptive_budget_policy_falls_back_when_budget_override_replace_fails(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_budget_replace_fail"
    context_dir = tmp_path / "context_adaptive_budget_replace_fail"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveBudgetNoDiffNonDataclassPatcher()
    controller, _events_path = _increase_biased_budget_controller(tmp_path, mode="on")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=1,
            output_format="json_compact",
            run_id="run-adaptive-budget-replace-fail",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_budget_policy_controller=controller,
        )
    finally:
        controller.close()

    assert len(records) == 1
    attempt_meta = records[0]["attempt_metadata"]
    patch_meta = records[0]["patch_meta"]
    assert attempt_meta["adaptive_budget_action_selected"] == ACTION_INCREASE_BUDGET
    assert attempt_meta["adaptive_budget_action_applied"] == ACTION_INCREASE_BUDGET
    assert attempt_meta["adaptive_budget_tier_index_before"] == 0
    assert attempt_meta["adaptive_budget_tier_index_after"] == 1
    assert attempt_meta["adaptive_budget_ups_used"] == 1
    assert attempt_meta["attempt_shared_propose_budget_base_sec"] == 10.0
    assert attempt_meta["attempt_shared_propose_budget_multiplier"] == 1.5
    assert attempt_meta["attempt_shared_propose_budget_sec"] == 10.0
    assert attempt_meta["adaptive_budget_partial_override"] is True
    assert attempt_meta["adaptive_budget_effective_stage_a_max_requests"] == 7
    assert attempt_meta["adaptive_budget_effective_openai_max_tokens"] == 0
    assert attempt_meta["adaptive_budget_partial_override_errors"]["max_propose_sec"].startswith(
        "TypeError:"
    )
    assert "TypeError" in str(patch_meta.get("adaptive_budget_propose_budget_override_error"))


def test_adaptive_budget_policy_penalizes_request_budget_exceeded(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_budget_penalty"
    context_dir = tmp_path / "context_adaptive_budget_penalty"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveBudgetExceededPatcher()
    controller, _events_path = _increase_biased_budget_controller(tmp_path, mode="on")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=1,
            output_format="json_compact",
            run_id="run-adaptive-budget-penalty",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_budget_policy_controller=controller,
            adaptive_reward_lambda_tokens=0.0,
            adaptive_reward_mu_latency=0.0,
            adaptive_reward_eta_progress=0.0,
            adaptive_reward_kappa_stagnation=0.0,
            adaptive_budget_reward_rho_budget_exceeded=0.5,
        )
    finally:
        controller.close()

    assert len(records) == 1
    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_budget_request_budget_exceeded"] is True
    assert attempt_meta["adaptive_budget_reward_budget_exceeded_penalty"] == 0.5
    assert attempt_meta["adaptive_budget_reward"] == -0.5
    assert attempt_meta["adaptive_budget_reward_progress_hint"] == 0.0
    assert attempt_meta["adaptive_budget_reward_stagnation_penalty"] == 0.0


def test_adaptive_budget_feature_vector_excludes_case_metadata(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_budget_feature_keys"
    context_dir = tmp_path / "context_adaptive_budget_feature_keys"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveBudgetNoDiffPatcher(name="openai", max_propose_sec=9.0)
    controller, _events_path = _increase_biased_budget_controller(tmp_path, mode="on")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=2,
            output_format="json_compact",
            run_id="run-adaptive-budget-feature-keys",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_budget_policy_controller=controller,
            adaptive_budget_policy_max_ups_per_case=2,
        )
    finally:
        controller.close()

    assert len(records) == 2
    feature_vector = records[0]["attempt_metadata"]["adaptive_budget_feature_vector"]
    expected_keys = {
        "attempts_remaining_frac",
        "helper_low_yield_ratio",
        "adaptive_no_progress_token_sum_k",
        "context_compaction_enabled",
        "current_budget_tier_frac",
        "can_increase_budget_tier",
        "budget_ups_used_frac",
        "budget_ups_remaining_frac",
        "base_propose_budget_sec",
        "budget_can_raise_propose_budget",
        "budget_can_raise_max_tokens",
        "budget_can_raise_helper_allowance",
        "budget_can_raise_stage_a_requests",
        "budget_can_raise_stage_a_chars",
        "budget_can_raise_retry_history_budget",
        "budget_can_raise_compaction_allowance",
        "budget_can_switch_response_mode",
        "budget_next_tier_multiplier_delta",
        "pregate_exception_is_assertion",
    }
    assert expected_keys.issubset(feature_vector.keys())
    assert "case_difficulty_ord" not in feature_vector
    assert "case_snapshot_dependency_ord" not in feature_vector
    assert "case_bug_source_imported" not in feature_vector


def test_adaptive_budget_policy_on_non_openai_patcher_still_applies_runner_controls(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_budget_non_openai"
    context_dir = tmp_path / "context_adaptive_budget_non_openai"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveBudgetNoDiffPatcher(name="command")
    controller, _events_path = _increase_biased_budget_controller(tmp_path, mode="on")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=1,
            output_format="json_compact",
            run_id="run-adaptive-budget-non-openai",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_budget_policy_controller=controller,
        )
    finally:
        controller.close()

    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_budget_action_applied"] == ACTION_INCREASE_BUDGET
    assert attempt_meta["adaptive_budget_tier_index_after"] == 1
    assert attempt_meta["attempt_shared_propose_budget_base_sec"] == 0.0
    assert attempt_meta["attempt_shared_propose_budget_sec"] == 0.0
    assert attempt_meta["adaptive_budget_effective_stage_a_max_requests"] == 7
    assert attempt_meta["adaptive_budget_effective_openai_max_tokens"] == 0
    assert attempt_meta["adaptive_budget_partial_override"] is False


def test_adaptive_budget_policy_keeps_unlimited_helper_budget_unlimited(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_budget_unlimited_helper"
    context_dir = tmp_path / "context_adaptive_budget_unlimited_helper"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveBudgetNoDiffPatcher()
    controller, _events_path = _increase_biased_budget_controller(tmp_path, mode="on")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=1,
            output_format="json_compact",
            run_id="run-adaptive-budget-unlimited-helper",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_budget_policy_controller=controller,
            adaptive_max_helper_calls=0,
        )
    finally:
        controller.close()

    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_budget_action_applied"] == ACTION_INCREASE_BUDGET
    assert attempt_meta["adaptive_helper_budget_unlimited"] is True
    assert attempt_meta["adaptive_budget_effective_helper_max_calls"] == 0
    assert attempt_meta["adaptive_budget_applied_control_values"]["helper_max_calls"] == 0


def test_adaptive_budget_policy_highest_tier_switches_response_mode(tmp_path, monkeypatch) -> None:
    case = _make_case(tmp_path)
    workdir = tmp_path / "workdir_adaptive_budget_diff_mode"
    context_dir = tmp_path / "context_adaptive_budget_diff_mode"
    shutil.copytree(case.path, workdir)
    patcher = _AdaptiveBudgetNoDiffPatcher()
    controller, _events_path = _increase_biased_budget_controller(tmp_path, mode="on")

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )
    monkeypatch.setattr(
        run_eval, "read_latest_snapshot", lambda *_args, **_kwargs: (_snapshot_payload(), None)
    )

    try:
        records = run_eval.run_one_condition(
            case=case,
            condition=run_eval.Condition.ADAPTIVE_LLM_DISCRETION,
            workdir=workdir,
            context_dir_base=context_dir,
            patcher=patcher,
            k=1,
            output_format="json_compact",
            run_id="run-adaptive-budget-diff-mode",
            include_files_context=False,
            force_patch_attempt=False,
            adaptive_budget_policy_controller=controller,
            adaptive_budget_policy_tiers="1.0,2.0",
        )
    finally:
        controller.close()

    attempt_meta = records[0]["attempt_metadata"]
    assert attempt_meta["adaptive_budget_action_applied"] == ACTION_INCREASE_BUDGET
    assert attempt_meta["adaptive_budget_effective_response_mode"] == "diff"
    assert attempt_meta["adaptive_budget_applied_controls_changed"]["response_mode"] is True


def test_parse_adaptive_budget_policy_tiers_validation() -> None:
    assert run_eval._parse_adaptive_budget_policy_tiers("1.0,1.5,2.0") == [1.0, 1.5, 2.0]
    invalid_values = (
        "",
        "1.5,2.0",
        "1.0,1.0,2.0",
        "2.0,1.0",
        "-1.0,1.0",
    )
    for raw in invalid_values:
        failed = False
        try:
            run_eval._parse_adaptive_budget_policy_tiers(raw)
        except ValueError:
            failed = True
        assert failed is True


def test_adaptive_online_local_reward_components_include_progress_and_stagnation_terms() -> None:
    (
        reward,
        success_hint,
        progress_hint,
        token_penalty,
        latency_penalty,
        stagnation_penalty,
        helper_materialization_bonus,
    ) = run_eval._adaptive_online_local_reward_components(
        success=True,
        helper_materialized_context=True,
        local_tokens_total=1000,
        local_wall_sec=10.0,
        adaptive_progress_score=1.0,
        adaptive_no_progress_streak=0,
        lambda_tokens=0.1,
        mu_latency=0.2,
        token_scale=1000.0,
        latency_scale=10.0,
        eta_progress=0.15,
        kappa_stagnation=0.1,
    )
    assert success_hint == 0.25
    assert progress_hint == 0.075
    assert token_penalty == 0.1
    assert latency_penalty == 0.2
    assert helper_materialization_bonus == 0.0375
    assert stagnation_penalty == 0.0
    assert reward == pytest.approx(0.0625)


def test_adaptive_budget_local_reward_components_subtract_budget_penalty() -> None:
    (
        reward,
        success_hint,
        progress_hint,
        token_penalty,
        latency_penalty,
        stagnation_penalty,
        budget_exceeded_penalty,
    ) = run_eval._adaptive_budget_local_reward_components(
        success=False,
        local_tokens_total=0,
        local_wall_sec=0.0,
        adaptive_progress_score=0.0,
        adaptive_no_progress_streak=6,
        lambda_tokens=0.0,
        mu_latency=0.0,
        token_scale=1000.0,
        latency_scale=10.0,
        eta_progress=0.15,
        kappa_stagnation=0.1,
        rho_budget_exceeded=0.5,
        request_budget_exceeded=True,
    )
    assert success_hint == 0.0
    assert progress_hint == 0.0
    assert token_penalty == 0.0
    assert latency_penalty == 0.0
    assert stagnation_penalty == 0.05
    assert budget_exceeded_penalty == 0.5
    assert reward == -0.55


def test_pytest_failed_total_from_text_counts_failed_and_errors() -> None:
    summary = "=== 2 failed, 1 error, 5 passed in 0.12s ==="
    assert run_eval._pytest_failed_total_from_text(summary) == 3


def test_adaptive_progress_signal_uses_weighted_components_and_reason_priority() -> None:
    before = run_eval.RunResult(
        returncode=1,
        duration_sec=0.1,
        stdout="=== 4 failed, 2 passed in 0.10s ===",
        stderr="",
    )
    after = run_eval.RunResult(
        returncode=1,
        duration_sec=0.1,
        stdout="=== 2 failed, 4 passed in 0.10s ===",
        stderr="",
    )
    progress = run_eval._adaptive_progress_signal(
        note="tests_still_failing",
        success=False,
        signature_repeated=False,
        new_evidence_count=1,
        before_run=before,
        after_run=after,
        previous_failure_context={"note": "patch_verifier_rejected", "patch_verifier_score": 1},
        patch_meta={
            "patch_verifier_score": 3,
            "patch_verifier_source_scope_matched": True,
        },
    )

    assert progress.made is True
    assert progress.reason == "failure_signature_changed"
    assert progress.pytest_before_failed_total == 4
    assert progress.pytest_after_failed_total == 2
    assert progress.stage_before == "verifier_rejected"
    assert progress.stage_after == "tests_still_failing"
    assert progress.score == pytest.approx(0.55)


def test_adaptive_online_delayed_rescue_bonus_uses_horizon_and_context_multiplier() -> None:
    assert run_eval._adaptive_online_delayed_rescue_bonus(
        origin_attempt=1,
        resolved_attempt=2,
        failure_signature_repeat=False,
        case_difficulty="easy",
        case_snapshot_dependency="required",
    ) == pytest.approx(0.625)
    assert run_eval._adaptive_online_delayed_rescue_bonus(
        origin_attempt=1,
        resolved_attempt=3,
        failure_signature_repeat=False,
        case_difficulty="easy",
        case_snapshot_dependency="none",
    ) == pytest.approx(0.25)


def test_adaptive_llm_discretion_resolves_delayed_helper_credit_on_next_success(
    tmp_path, monkeypatch
) -> None:
    records, patcher, events_path = _run_adaptive_delayed_credit_case(
        tmp_path,
        monkeypatch,
        patch_outcomes=("no_diff", "fix"),
        adaptive_max_helper_calls=1,
        k=2,
        run_id="run-adaptive-delayed-credit",
    )

    assert [bool(record["success"]) for record in records] == [False, True]
    assert patcher.request_calls == 1
    first_meta = records[0]["attempt_metadata"]
    assert first_meta["adaptive_online_reward_scope"] == "helper_credit_v2"
    assert first_meta["adaptive_online_reward_delayed_rescue_bonus"] == pytest.approx(0.625)
    assert first_meta["adaptive_online_reward_credit_horizon_attempts"] == 1
    assert first_meta["adaptive_online_reward_credit_resolution_reason"] == "future_success_attempt"

    event_lines = [line for line in events_path.read_text(encoding="utf-8").splitlines() if line]
    assert len(event_lines) == 1


def test_adaptive_llm_discretion_resolves_delayed_helper_credit_on_horizon_two_success(
    tmp_path, monkeypatch
) -> None:
    records, patcher, events_path = _run_adaptive_delayed_credit_case(
        tmp_path,
        monkeypatch,
        patch_outcomes=("no_diff", "no_diff", "fix"),
        adaptive_max_helper_calls=1,
        adaptive_budget_topup_max_calls=0,
        k=3,
        run_id="run-adaptive-delayed-credit-h2",
    )

    assert [bool(record["success"]) for record in records] == [False, False, True]
    assert patcher.request_calls == 1
    first_meta = records[0]["attempt_metadata"]
    assert first_meta["adaptive_online_reward_delayed_rescue_bonus"] == pytest.approx(0.3125)
    assert first_meta["adaptive_online_reward_credit_horizon_attempts"] == 2
    assert first_meta["adaptive_online_reward_credit_resolution_reason"] == "future_success_attempt"

    event_lines = [line for line in events_path.read_text(encoding="utf-8").splitlines() if line]
    assert len(event_lines) == 1


def test_adaptive_llm_discretion_credits_only_newest_helper_before_success(
    tmp_path, monkeypatch
) -> None:
    records, patcher, events_path = _run_adaptive_delayed_credit_case(
        tmp_path,
        monkeypatch,
        patch_outcomes=("no_diff", "no_diff", "fix"),
        adaptive_max_helper_calls=2,
        k=3,
        run_id="run-adaptive-delayed-credit-supersede",
    )

    assert [bool(record["success"]) for record in records] == [False, False, True]
    assert patcher.request_calls == 3
    first_meta = records[0]["attempt_metadata"]
    second_meta = records[1]["attempt_metadata"]
    assert first_meta["adaptive_online_reward_delayed_rescue_bonus"] == 0.0
    assert (
        first_meta["adaptive_online_reward_credit_resolution_reason"]
        == "local_only_superseded_by_newer_helper"
    )
    assert second_meta["adaptive_online_reward_delayed_rescue_bonus"] == pytest.approx(0.625)
    assert second_meta["adaptive_online_reward_credit_horizon_attempts"] == 1
    assert (
        second_meta["adaptive_online_reward_credit_resolution_reason"] == "future_success_attempt"
    )

    event_lines = [line for line in events_path.read_text(encoding="utf-8").splitlines() if line]
    assert len(event_lines) == 3


def test_run_one_condition_strips_eval_only_artifacts_from_workdir_and_prompt(
    tmp_path, monkeypatch
) -> None:
    case = _make_case(tmp_path)
    (case.path / "case.json").write_text(
        json.dumps({"expected_exception": "AssertionError"}, ensure_ascii=False),
        encoding="utf-8",
    )
    (case.path / "meta.json").write_text("{}", encoding="utf-8")
    (case.path / "oracle.patch").write_text("diff --git a/x b/x\n", encoding="utf-8")
    workdir = tmp_path / "workdir_eval_artifacts"
    context_dir = tmp_path / "context_eval_artifacts"
    shutil.copytree(case.path, workdir)

    @dataclass
    class _CapturePromptPatcher:
        name: str = "capture_prompt"
        prompts: list[str] = field(default_factory=list)
        saw_meta_json: bool = False
        saw_oracle_patch: bool = False

        def propose_patch(self, *, prompt: str, context_dir, cwd) -> PatchResult:
            del context_dir
            self.prompts.append(prompt)
            self.saw_meta_json = (cwd / "case.json").exists() or (cwd / "meta.json").exists()
            self.saw_oracle_patch = (cwd / "oracle.patch").exists()
            return PatchResult(diff=None, meta={"reason": "no_diff"})

    patcher = _CapturePromptPatcher()

    monkeypatch.setattr(
        run_eval,
        "run_pytest",
        lambda *args, **kwargs: run_eval.RunResult(
            returncode=1,
            duration_sec=0.01,
            stdout="tests/test_buggy.py::test_f\nE   AssertionError: expected 2 got 1\n",
            stderr="",
        ),
    )

    run_eval.run_one_condition(
        case=case,
        condition=run_eval.Condition.TRACEBACK_ONLY,
        workdir=workdir,
        context_dir_base=context_dir,
        patcher=patcher,
        k=1,
        output_format="json_compact",
        run_id="run-eval-artifact-strip",
        include_files_context=True,
        force_patch_attempt=False,
    )

    assert patcher.prompts
    prompt = patcher.prompts[0]
    assert patcher.saw_meta_json is False
    assert patcher.saw_oracle_patch is False
    assert "meta.json" not in prompt
    assert "oracle.patch" not in prompt
    assert "expected_exception" not in prompt
    assert not (workdir / "case.json").exists()
    assert not (workdir / "meta.json").exists()
    assert not (workdir / "oracle.patch").exists()


def test_finalize_adaptive_policy_uses_recomputed_progress_metadata() -> None:
    attempt_metadata: dict[str, object] = {
        "tokens_total": 0,
        "adaptive_progress_made": False,
        "adaptive_progress_score": 0.0,
        "adaptive_no_progress_streak": 3,
    }
    patch_meta: dict[str, object] = {}
    online_finalize_kwargs: dict[str, object] = {
        "is_adaptive_llm_discretion": True,
        "online_policy_decision": None,
        "adaptive_online_policy_controller": None,
        "run_id": "run-reward-sync",
        "case_id": "case-reward-sync",
        "condition_value": "adaptive_llm_discretion",
        "attempt": 1,
        "attempt_metadata": attempt_metadata,
        "patch_meta": patch_meta,
        "online_policy_mode_for_attempt": "on",
        "online_policy_algo_for_attempt": "linucb",
        "online_policy_eligible": False,
        "online_policy_override_reason": "",
        "online_policy_feature_scaling_profile": "v1",
        "online_policy_features_raw": {},
        "online_policy_features_scaled": {},
        "helper_decision_reason": "llm_discretion_pending",
        "normalized_adaptive_online_policy_mode": "on",
        "case_difficulty": "easy",
        "case_snapshot_dependency": "helpful",
        # Deliberately stale values: should be overwritten from attempt_metadata.
        "adaptive_progress_made": True,
        "adaptive_progress_score": 1.0,
        "adaptive_no_progress_streak_current": 0,
        "reward_lambda_tokens": 0.0,
        "reward_mu_latency": 0.0,
        "reward_token_scale": 1000.0,
        "reward_latency_scale": 10.0,
        "reward_eta_progress": 0.15,
        "reward_kappa_stagnation": 0.2,
    }
    budget_finalize_kwargs: dict[str, object] = {
        "is_adaptive_condition": False,
        "budget_policy_decision": None,
        "adaptive_budget_policy_controller": None,
        "run_id": "run-reward-sync",
        "case_id": "case-reward-sync",
        "condition_value": "adaptive_llm_discretion",
        "attempt": 1,
        "attempt_metadata": attempt_metadata,
        "patch_meta": patch_meta,
        "budget_policy_mode_for_attempt": "on",
        "budget_policy_algo_for_attempt": "linucb",
        "budget_policy_eligible": False,
        "budget_policy_override_reason": "",
        "budget_policy_feature_scaling_profile": "v1",
        "budget_policy_features_raw": {},
        "budget_policy_features_scaled": {},
        "normalized_adaptive_budget_policy_mode": "on",
        # Deliberately stale values: should be overwritten from attempt_metadata.
        "adaptive_progress_made": True,
        "adaptive_progress_score": 1.0,
        "adaptive_no_progress_streak_current": 0,
        "reward_lambda_tokens": 0.0,
        "reward_mu_latency": 0.0,
        "reward_token_scale": 1000.0,
        "reward_latency_scale": 10.0,
        "reward_eta_progress": 0.15,
        "reward_kappa_stagnation": 0.2,
        "reward_rho_budget_exceeded": 0.5,
        "tiers": [1.0, 1.5],
        "tier_index_before": 0,
        "tier_index_after": 0,
        "selected_multiplier": 1.0,
        "applied_multiplier": 1.0,
        "default_multiplier": 1.0,
        "max_ups_per_case": 1,
        "ups_used": 0,
    }

    run_eval._finalize_adaptive_policy_attempt(
        note="tests_still_failing",
        success=False,
        attempt_wall_sec=0.0,
        online_finalize_kwargs=online_finalize_kwargs,
        budget_finalize_kwargs=budget_finalize_kwargs,
    )

    assert attempt_metadata["adaptive_online_reward_progress_hint"] == 0.0
    assert attempt_metadata["adaptive_online_reward_stagnation_penalty"] == 0.1
    assert attempt_metadata["adaptive_budget_reward_progress_hint"] == 0.0
    assert attempt_metadata["adaptive_budget_reward_stagnation_penalty"] == 0.1
