"""TDD — merge gate evaluation: evaluate_merge_conditions. Covers every active MergeConditions key: require_approvals pass / fail / not set max_risk_score pass / fail require_signed_commits pass / fail / mixed require_no_breakage pass / fail require_test_coverage pass / fail require_dependency_merged pass / fail max_agent_commit_ratio pass / fail require_domains_approved pass / fail / partial require_payment_settled always unknown Also covers: - Only active conditions are returned - Ordering is deterministic (canonical field order) - Each entry has: key, label, met, status, actual, required - Empty merge_conditions → empty list - None merge_conditions → empty list """ from __future__ import annotations import pytest from musehub.services.musehub_proposal_gate import evaluate_merge_conditions # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _commits(n: int, *, signed: bool = True, agent: bool = False) -> list[dict]: return [ {"is_signed": signed, "is_agent": agent, "commit_id": f"sha256:{i:064x}"} for i in range(n) ] def _gate(**kwargs: typing.Any) -> list[dict]: defaults = dict( approved_count=0, risk_score=0.0, proposal_commits=[], breakage_count=0, test_gap_count=0, is_blocked=False, domains_approved=[], payment_settled=False, ) defaults.update(kwargs) return evaluate_merge_conditions(**defaults) # --------------------------------------------------------------------------- # None / empty → no conditions # --------------------------------------------------------------------------- class TestNoConditions: def test_none_returns_empty(self) -> None: result = evaluate_merge_conditions( merge_conditions=None, approved_count=0, risk_score=0.0, proposal_commits=[], breakage_count=0, test_gap_count=0, is_blocked=False, domains_approved=[], payment_settled=False, ) assert result == [] def test_empty_dict_returns_empty(self) -> None: result = evaluate_merge_conditions( merge_conditions={}, approved_count=0, risk_score=0.0, proposal_commits=[], breakage_count=0, test_gap_count=0, is_blocked=False, domains_approved=[], payment_settled=False, ) assert result == [] # --------------------------------------------------------------------------- # require_approvals # --------------------------------------------------------------------------- class TestRequireApprovals: def test_met(self) -> None: result = _gate(merge_conditions={"require_approvals": 2}, approved_count=2) assert len(result) == 1 e = result[0] assert e["key"] == "require_approvals" assert e["met"] is True assert e["status"] == "pass" def test_not_met(self) -> None: result = _gate(merge_conditions={"require_approvals": 3}, approved_count=1) e = result[0] assert e["met"] is False assert e["status"] == "fail" def test_actual_shows_count(self) -> None: result = _gate(merge_conditions={"require_approvals": 3}, approved_count=1) assert "1" in result[0]["actual"] assert "3" in result[0]["required"] def test_not_in_conditions_not_returned(self) -> None: result = _gate(merge_conditions={"require_no_breakage": True}, approved_count=5) keys = [e["key"] for e in result] assert "require_approvals" not in keys # --------------------------------------------------------------------------- # max_risk_score # --------------------------------------------------------------------------- class TestMaxRiskScore: def test_met(self) -> None: result = _gate(merge_conditions={"max_risk_score": 0.5}, risk_score=0.3) assert result[0]["met"] is True def test_not_met(self) -> None: result = _gate(merge_conditions={"max_risk_score": 0.5}, risk_score=0.8) assert result[0]["met"] is False def test_exact_boundary_met(self) -> None: result = _gate(merge_conditions={"max_risk_score": 0.5}, risk_score=0.5) assert result[0]["met"] is True def test_required_shows_threshold(self) -> None: result = _gate(merge_conditions={"max_risk_score": 0.4}, risk_score=0.6) assert "0.4" in result[0]["required"] # --------------------------------------------------------------------------- # require_signed_commits # --------------------------------------------------------------------------- class TestRequireSignedCommits: def test_met_all_signed(self) -> None: commits = _commits(3, signed=True) result = _gate(merge_conditions={"require_signed_commits": True}, proposal_commits=commits) assert result[0]["met"] is True def test_not_met_one_unsigned(self) -> None: commits = _commits(2, signed=True) + _commits(1, signed=False) result = _gate(merge_conditions={"require_signed_commits": True}, proposal_commits=commits) assert result[0]["met"] is False def test_no_commits_met(self) -> None: result = _gate(merge_conditions={"require_signed_commits": True}, proposal_commits=[]) assert result[0]["met"] is True def test_actual_shows_signed_fraction(self) -> None: commits = _commits(2, signed=True) + _commits(1, signed=False) result = _gate(merge_conditions={"require_signed_commits": True}, proposal_commits=commits) assert "2" in result[0]["actual"] and "3" in result[0]["actual"] # --------------------------------------------------------------------------- # require_no_breakage # --------------------------------------------------------------------------- class TestRequireNoBreakage: def test_met(self) -> None: result = _gate(merge_conditions={"require_no_breakage": True}, breakage_count=0) assert result[0]["met"] is True def test_not_met(self) -> None: result = _gate(merge_conditions={"require_no_breakage": True}, breakage_count=3) assert result[0]["met"] is False def test_actual_shows_count(self) -> None: result = _gate(merge_conditions={"require_no_breakage": True}, breakage_count=5) assert "5" in result[0]["actual"] # --------------------------------------------------------------------------- # require_test_coverage # --------------------------------------------------------------------------- class TestRequireTestCoverage: def test_met(self) -> None: result = _gate(merge_conditions={"require_test_coverage": True}, test_gap_count=0) assert result[0]["met"] is True def test_not_met(self) -> None: result = _gate(merge_conditions={"require_test_coverage": True}, test_gap_count=2) assert result[0]["met"] is False # --------------------------------------------------------------------------- # require_dependency_merged # --------------------------------------------------------------------------- class TestRequireDependencyMerged: def test_met_not_blocked(self) -> None: result = _gate(merge_conditions={"require_dependency_merged": True}, is_blocked=False) assert result[0]["met"] is True def test_not_met_blocked(self) -> None: result = _gate(merge_conditions={"require_dependency_merged": True}, is_blocked=True) assert result[0]["met"] is False # --------------------------------------------------------------------------- # max_agent_commit_ratio # --------------------------------------------------------------------------- class TestMaxAgentCommitRatio: def test_met(self) -> None: commits = _commits(3, agent=False) + _commits(1, agent=True) result = _gate(merge_conditions={"max_agent_commit_ratio": 0.5}, proposal_commits=commits) assert result[0]["met"] is True # 0.25 <= 0.5 def test_not_met(self) -> None: commits = _commits(1, agent=False) + _commits(3, agent=True) result = _gate(merge_conditions={"max_agent_commit_ratio": 0.5}, proposal_commits=commits) assert result[0]["met"] is False # 0.75 > 0.5 def test_no_commits_met(self) -> None: result = _gate(merge_conditions={"max_agent_commit_ratio": 0.5}, proposal_commits=[]) assert result[0]["met"] is True def test_actual_shows_percentage(self) -> None: commits = _commits(1, agent=False) + _commits(1, agent=True) result = _gate(merge_conditions={"max_agent_commit_ratio": 0.3}, proposal_commits=commits) assert "50" in result[0]["actual"] # --------------------------------------------------------------------------- # require_domains_approved # --------------------------------------------------------------------------- class TestRequireDomainsApproved: def test_met_all_domains_approved(self) -> None: result = _gate( merge_conditions={"require_domains_approved": ["code", "midi"]}, domains_approved=["code", "midi"], ) assert result[0]["met"] is True def test_not_met_missing_domain(self) -> None: result = _gate( merge_conditions={"require_domains_approved": ["code", "midi"]}, domains_approved=["code"], ) assert result[0]["met"] is False def test_actual_shows_missing(self) -> None: result = _gate( merge_conditions={"require_domains_approved": ["code", "midi"]}, domains_approved=["code"], ) assert "midi" in result[0]["actual"] # --------------------------------------------------------------------------- # require_payment_settled — always unknown # --------------------------------------------------------------------------- class TestRequirePaymentSettled: def test_unknown_when_not_settled(self) -> None: result = _gate( merge_conditions={"require_payment_settled": True}, payment_settled=False, ) assert result[0]["status"] == "unknown" assert result[0]["met"] is False def test_pass_when_settled(self) -> None: result = _gate( merge_conditions={"require_payment_settled": True}, payment_settled=True, ) assert result[0]["met"] is True assert result[0]["status"] == "pass" # --------------------------------------------------------------------------- # Entry schema # --------------------------------------------------------------------------- class TestEntrySchema: def test_every_entry_has_required_keys(self) -> None: result = _gate( merge_conditions={"require_approvals": 2, "max_risk_score": 0.5}, approved_count=3, risk_score=0.3, ) for e in result: for k in ("key", "label", "met", "status", "actual", "required"): assert k in e, f"missing key '{k}' in {e}" def test_met_is_bool(self) -> None: result = _gate(merge_conditions={"require_approvals": 1}, approved_count=1) assert isinstance(result[0]["met"], bool) def test_status_is_valid_string(self) -> None: result = _gate(merge_conditions={"require_approvals": 1}, approved_count=1) assert result[0]["status"] in ("pass", "fail", "unknown") # --------------------------------------------------------------------------- # Canonical ordering # --------------------------------------------------------------------------- class TestOrdering: def test_order_is_deterministic(self) -> None: conditions = { "require_approvals": 2, "max_risk_score": 0.5, "require_signed_commits": True, "require_no_breakage": True, "require_test_coverage": True, "require_dependency_merged": True, "max_agent_commit_ratio": 0.8, } result = _gate(merge_conditions=conditions) keys = [e["key"] for e in result] assert keys == [ "require_approvals", "max_risk_score", "require_signed_commits", "require_no_breakage", "require_test_coverage", "require_dependency_merged", "max_agent_commit_ratio", ]