"""ORCH-110 TC-08: re-test budget validation + cross-invariants (D5). Covers FR-3 / AC-5 / NFR-6: * ``_resolve_retest_timeout`` validates the config (malformed / non-positive -> safe default 900 + WARNING; never reaches subprocess); * the budget was bumped 600 -> 900; * the cross-invariant ``reaper_max_running_s > Σ(deploy-staging gate-work) + grace`` (ORCH-065/109) still holds with the new 900s re-test budget — WITHOUT raising ``reaper_max_running_s``. """ import os import tempfile os.environ.setdefault("ORCH_DB_PATH", os.path.join(tempfile.gettempdir(), "test_orch110_budget.db")) os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token") os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token") import pytest # noqa: E402 from src import merge_gate # noqa: E402 from src.config import Settings, settings # noqa: E402 # --------------------------------------------------------------------------- # _resolve_retest_timeout — validation (never-break). # --------------------------------------------------------------------------- def test_tc08_resolve_uses_positive_config(monkeypatch): monkeypatch.setattr(merge_gate.settings, "merge_retest_timeout_s", 1234, raising=False) assert merge_gate._resolve_retest_timeout() == 1234 @pytest.mark.parametrize("bad", [0, -5, "abc", None, 3.0]) def test_tc08_resolve_bad_config_falls_back_to_default(monkeypatch, bad): monkeypatch.setattr(merge_gate.settings, "merge_retest_timeout_s", bad, raising=False) # 3.0 is a valid positive int(3) -> stays 3; everything else -> 900 default. out = merge_gate._resolve_retest_timeout() if bad == 3.0: assert out == 3 else: assert out == 900 def test_tc08_default_budget_bumped_to_900(): """D5: the shipped default budget is 900 (raised from 600).""" assert Settings().merge_retest_timeout_s == 900 # --------------------------------------------------------------------------- # Cross-invariant: reaper backstop covers the worst-case deploy-staging gate-work. # --------------------------------------------------------------------------- def test_tc08_reaper_covers_deploy_staging_worstcase(): """ORCH-065/109 invariant with the new 900s re-test budget (ADR D5 table).""" s = Settings() # Worst-case sum of work charged to a deploy-staging-deployer job (ADR D5). security = 120 rebase = 120 image = 600 worst = ( s.agent_timeout_seconds # deployer agent (1800) + security + rebase + s.merge_retest_timeout_s # re-test (900, new) + s.coverage_run_timeout_s # coverage (900) + image + s.agent_kill_grace_seconds # grace (20) ) assert worst <= 4460 # matches the ADR D5 table assert s.reaper_max_running_s > worst, ( f"reaper_max_running_s={s.reaper_max_running_s} must exceed " f"deploy-staging worst-case {worst}" ) def test_tc08_reaper_still_covers_max_agent_timeout(): """ORCH-065/109: reaper_max_running_s > max(agent timeout) + grace (unchanged).""" s = Settings() assert s.reaper_max_running_s > s.agent_timeout_developer_s + s.agent_kill_grace_seconds def test_tc08_reaper_max_running_s_unchanged(): """D5 must NOT change reaper_max_running_s (stays 5400 from ORCH-109).""" assert settings.reaper_max_running_s == 5400