gabriel / musehub public
test_merge_gate.py python
337 lines 12.2 KB
Raw
sha256:ef10830ce231e0a20efcb0e2586cb879471247e916616e6fdd0d51df459e2595 fix: typing audit — 0 violations, 0 untyped defs across all… Sonnet 4.6 minor ⚠ breaking 21 days ago
1 """TDD — merge gate evaluation: evaluate_merge_conditions.
2
3 Covers every active MergeConditions key:
4 require_approvals pass / fail / not set
5 max_risk_score pass / fail
6 require_signed_commits pass / fail / mixed
7 require_no_breakage pass / fail
8 require_test_coverage pass / fail
9 require_dependency_merged pass / fail
10 max_agent_commit_ratio pass / fail
11 require_domains_approved pass / fail / partial
12 require_payment_settled always unknown
13
14 Also covers:
15 - Only active conditions are returned
16 - Ordering is deterministic (canonical field order)
17 - Each entry has: key, label, met, status, actual, required
18 - Empty merge_conditions → empty list
19 - None merge_conditions → empty list
20 """
21
22 from __future__ import annotations
23
24 import pytest
25
26 from musehub.services.musehub_proposal_gate import evaluate_merge_conditions
27
28
29 # ---------------------------------------------------------------------------
30 # Helpers
31 # ---------------------------------------------------------------------------
32
33 def _commits(n: int, *, signed: bool = True, agent: bool = False) -> list[dict]:
34 return [
35 {"is_signed": signed, "is_agent": agent, "commit_id": f"sha256:{i:064x}"}
36 for i in range(n)
37 ]
38
39
40 def _gate(**kwargs: typing.Any) -> list[dict]:
41 defaults = dict(
42 approved_count=0,
43 risk_score=0.0,
44 proposal_commits=[],
45 breakage_count=0,
46 test_gap_count=0,
47 is_blocked=False,
48 domains_approved=[],
49 payment_settled=False,
50 )
51 defaults.update(kwargs)
52 return evaluate_merge_conditions(**defaults)
53
54
55 # ---------------------------------------------------------------------------
56 # None / empty → no conditions
57 # ---------------------------------------------------------------------------
58
59
60 class TestNoConditions:
61 def test_none_returns_empty(self) -> None:
62 result = evaluate_merge_conditions(
63 merge_conditions=None,
64 approved_count=0, risk_score=0.0, proposal_commits=[],
65 breakage_count=0, test_gap_count=0, is_blocked=False,
66 domains_approved=[], payment_settled=False,
67 )
68 assert result == []
69
70 def test_empty_dict_returns_empty(self) -> None:
71 result = evaluate_merge_conditions(
72 merge_conditions={},
73 approved_count=0, risk_score=0.0, proposal_commits=[],
74 breakage_count=0, test_gap_count=0, is_blocked=False,
75 domains_approved=[], payment_settled=False,
76 )
77 assert result == []
78
79
80 # ---------------------------------------------------------------------------
81 # require_approvals
82 # ---------------------------------------------------------------------------
83
84
85 class TestRequireApprovals:
86 def test_met(self) -> None:
87 result = _gate(merge_conditions={"require_approvals": 2}, approved_count=2)
88 assert len(result) == 1
89 e = result[0]
90 assert e["key"] == "require_approvals"
91 assert e["met"] is True
92 assert e["status"] == "pass"
93
94 def test_not_met(self) -> None:
95 result = _gate(merge_conditions={"require_approvals": 3}, approved_count=1)
96 e = result[0]
97 assert e["met"] is False
98 assert e["status"] == "fail"
99
100 def test_actual_shows_count(self) -> None:
101 result = _gate(merge_conditions={"require_approvals": 3}, approved_count=1)
102 assert "1" in result[0]["actual"]
103 assert "3" in result[0]["required"]
104
105 def test_not_in_conditions_not_returned(self) -> None:
106 result = _gate(merge_conditions={"require_no_breakage": True}, approved_count=5)
107 keys = [e["key"] for e in result]
108 assert "require_approvals" not in keys
109
110
111 # ---------------------------------------------------------------------------
112 # max_risk_score
113 # ---------------------------------------------------------------------------
114
115
116 class TestMaxRiskScore:
117 def test_met(self) -> None:
118 result = _gate(merge_conditions={"max_risk_score": 0.5}, risk_score=0.3)
119 assert result[0]["met"] is True
120
121 def test_not_met(self) -> None:
122 result = _gate(merge_conditions={"max_risk_score": 0.5}, risk_score=0.8)
123 assert result[0]["met"] is False
124
125 def test_exact_boundary_met(self) -> None:
126 result = _gate(merge_conditions={"max_risk_score": 0.5}, risk_score=0.5)
127 assert result[0]["met"] is True
128
129 def test_required_shows_threshold(self) -> None:
130 result = _gate(merge_conditions={"max_risk_score": 0.4}, risk_score=0.6)
131 assert "0.4" in result[0]["required"]
132
133
134 # ---------------------------------------------------------------------------
135 # require_signed_commits
136 # ---------------------------------------------------------------------------
137
138
139 class TestRequireSignedCommits:
140 def test_met_all_signed(self) -> None:
141 commits = _commits(3, signed=True)
142 result = _gate(merge_conditions={"require_signed_commits": True}, proposal_commits=commits)
143 assert result[0]["met"] is True
144
145 def test_not_met_one_unsigned(self) -> None:
146 commits = _commits(2, signed=True) + _commits(1, signed=False)
147 result = _gate(merge_conditions={"require_signed_commits": True}, proposal_commits=commits)
148 assert result[0]["met"] is False
149
150 def test_no_commits_met(self) -> None:
151 result = _gate(merge_conditions={"require_signed_commits": True}, proposal_commits=[])
152 assert result[0]["met"] is True
153
154 def test_actual_shows_signed_fraction(self) -> None:
155 commits = _commits(2, signed=True) + _commits(1, signed=False)
156 result = _gate(merge_conditions={"require_signed_commits": True}, proposal_commits=commits)
157 assert "2" in result[0]["actual"] and "3" in result[0]["actual"]
158
159
160 # ---------------------------------------------------------------------------
161 # require_no_breakage
162 # ---------------------------------------------------------------------------
163
164
165 class TestRequireNoBreakage:
166 def test_met(self) -> None:
167 result = _gate(merge_conditions={"require_no_breakage": True}, breakage_count=0)
168 assert result[0]["met"] is True
169
170 def test_not_met(self) -> None:
171 result = _gate(merge_conditions={"require_no_breakage": True}, breakage_count=3)
172 assert result[0]["met"] is False
173
174 def test_actual_shows_count(self) -> None:
175 result = _gate(merge_conditions={"require_no_breakage": True}, breakage_count=5)
176 assert "5" in result[0]["actual"]
177
178
179 # ---------------------------------------------------------------------------
180 # require_test_coverage
181 # ---------------------------------------------------------------------------
182
183
184 class TestRequireTestCoverage:
185 def test_met(self) -> None:
186 result = _gate(merge_conditions={"require_test_coverage": True}, test_gap_count=0)
187 assert result[0]["met"] is True
188
189 def test_not_met(self) -> None:
190 result = _gate(merge_conditions={"require_test_coverage": True}, test_gap_count=2)
191 assert result[0]["met"] is False
192
193
194 # ---------------------------------------------------------------------------
195 # require_dependency_merged
196 # ---------------------------------------------------------------------------
197
198
199 class TestRequireDependencyMerged:
200 def test_met_not_blocked(self) -> None:
201 result = _gate(merge_conditions={"require_dependency_merged": True}, is_blocked=False)
202 assert result[0]["met"] is True
203
204 def test_not_met_blocked(self) -> None:
205 result = _gate(merge_conditions={"require_dependency_merged": True}, is_blocked=True)
206 assert result[0]["met"] is False
207
208
209 # ---------------------------------------------------------------------------
210 # max_agent_commit_ratio
211 # ---------------------------------------------------------------------------
212
213
214 class TestMaxAgentCommitRatio:
215 def test_met(self) -> None:
216 commits = _commits(3, agent=False) + _commits(1, agent=True)
217 result = _gate(merge_conditions={"max_agent_commit_ratio": 0.5}, proposal_commits=commits)
218 assert result[0]["met"] is True # 0.25 <= 0.5
219
220 def test_not_met(self) -> None:
221 commits = _commits(1, agent=False) + _commits(3, agent=True)
222 result = _gate(merge_conditions={"max_agent_commit_ratio": 0.5}, proposal_commits=commits)
223 assert result[0]["met"] is False # 0.75 > 0.5
224
225 def test_no_commits_met(self) -> None:
226 result = _gate(merge_conditions={"max_agent_commit_ratio": 0.5}, proposal_commits=[])
227 assert result[0]["met"] is True
228
229 def test_actual_shows_percentage(self) -> None:
230 commits = _commits(1, agent=False) + _commits(1, agent=True)
231 result = _gate(merge_conditions={"max_agent_commit_ratio": 0.3}, proposal_commits=commits)
232 assert "50" in result[0]["actual"]
233
234
235 # ---------------------------------------------------------------------------
236 # require_domains_approved
237 # ---------------------------------------------------------------------------
238
239
240 class TestRequireDomainsApproved:
241 def test_met_all_domains_approved(self) -> None:
242 result = _gate(
243 merge_conditions={"require_domains_approved": ["code", "midi"]},
244 domains_approved=["code", "midi"],
245 )
246 assert result[0]["met"] is True
247
248 def test_not_met_missing_domain(self) -> None:
249 result = _gate(
250 merge_conditions={"require_domains_approved": ["code", "midi"]},
251 domains_approved=["code"],
252 )
253 assert result[0]["met"] is False
254
255 def test_actual_shows_missing(self) -> None:
256 result = _gate(
257 merge_conditions={"require_domains_approved": ["code", "midi"]},
258 domains_approved=["code"],
259 )
260 assert "midi" in result[0]["actual"]
261
262
263 # ---------------------------------------------------------------------------
264 # require_payment_settled — always unknown
265 # ---------------------------------------------------------------------------
266
267
268 class TestRequirePaymentSettled:
269 def test_unknown_when_not_settled(self) -> None:
270 result = _gate(
271 merge_conditions={"require_payment_settled": True},
272 payment_settled=False,
273 )
274 assert result[0]["status"] == "unknown"
275 assert result[0]["met"] is False
276
277 def test_pass_when_settled(self) -> None:
278 result = _gate(
279 merge_conditions={"require_payment_settled": True},
280 payment_settled=True,
281 )
282 assert result[0]["met"] is True
283 assert result[0]["status"] == "pass"
284
285
286 # ---------------------------------------------------------------------------
287 # Entry schema
288 # ---------------------------------------------------------------------------
289
290
291 class TestEntrySchema:
292 def test_every_entry_has_required_keys(self) -> None:
293 result = _gate(
294 merge_conditions={"require_approvals": 2, "max_risk_score": 0.5},
295 approved_count=3,
296 risk_score=0.3,
297 )
298 for e in result:
299 for k in ("key", "label", "met", "status", "actual", "required"):
300 assert k in e, f"missing key '{k}' in {e}"
301
302 def test_met_is_bool(self) -> None:
303 result = _gate(merge_conditions={"require_approvals": 1}, approved_count=1)
304 assert isinstance(result[0]["met"], bool)
305
306 def test_status_is_valid_string(self) -> None:
307 result = _gate(merge_conditions={"require_approvals": 1}, approved_count=1)
308 assert result[0]["status"] in ("pass", "fail", "unknown")
309
310
311 # ---------------------------------------------------------------------------
312 # Canonical ordering
313 # ---------------------------------------------------------------------------
314
315
316 class TestOrdering:
317 def test_order_is_deterministic(self) -> None:
318 conditions = {
319 "require_approvals": 2,
320 "max_risk_score": 0.5,
321 "require_signed_commits": True,
322 "require_no_breakage": True,
323 "require_test_coverage": True,
324 "require_dependency_merged": True,
325 "max_agent_commit_ratio": 0.8,
326 }
327 result = _gate(merge_conditions=conditions)
328 keys = [e["key"] for e in result]
329 assert keys == [
330 "require_approvals",
331 "max_risk_score",
332 "require_signed_commits",
333 "require_no_breakage",
334 "require_test_coverage",
335 "require_dependency_merged",
336 "max_agent_commit_ratio",
337 ]
File History 1 commit
sha256:ef10830ce231e0a20efcb0e2586cb879471247e916616e6fdd0d51df459e2595 fix: typing audit — 0 violations, 0 untyped defs across all… Sonnet 4.6 minor 21 days ago