gabriel / musehub public
test_musehub_issues_branch_reachability.py python
309 lines 10.3 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 155 days ago
1 """Tests for Signal 3: branch-to-issue commit reachability.
2
3 find_proposals_by_branch_reachability returns open proposals where a commit
4 anchor is reachable from from_branch but NOT yet reachable from to_branch.
5
6 Covers:
7 - Anchor reachable exclusively from from_branch → proposal returned
8 - Anchor on to_branch (already integrated) → NOT returned
9 - Anchor reachable at depth 2 from from_branch → returned
10 - Anchor reachable from both branches (common ancestor) → NOT returned
11 - Empty commit_anchors → empty list
12 - Anchor not in any branch commit graph → empty list
13 - Short (8-char) anchor prefix resolution
14 - Merged proposals are excluded (from_branch deleted at merge)
15 - Cross-repo isolation
16 """
17 from __future__ import annotations
18
19 import uuid
20 from datetime import datetime, timezone
21
22 import pytest
23 from sqlalchemy.ext.asyncio import AsyncSession
24
25 from musehub.db.musehub_models import (
26 MusehubBranch,
27 MusehubCommit,
28 MusehubProposal,
29 MusehubRepo,
30 )
31 from musehub.services import musehub_issues
32
33
34 # ---------------------------------------------------------------------------
35 # Helpers
36 # ---------------------------------------------------------------------------
37
38
39 def _uid() -> str:
40 return str(uuid.uuid4())
41
42
43 def _commit_id() -> str:
44 return uuid.uuid4().hex + uuid.uuid4().hex
45
46
47 async def _make_repo(db: AsyncSession, slug: str) -> str:
48 repo = MusehubRepo(
49 name=slug, owner="testuser", slug=slug,
50 visibility="public", owner_user_id=_uid(),
51 )
52 db.add(repo)
53 await db.commit()
54 await db.refresh(repo)
55 return str(repo.repo_id)
56
57
58 async def _make_commit(
59 db: AsyncSession,
60 repo_id: str,
61 *,
62 branch: str,
63 parent_ids: list[str] | None = None,
64 commit_id: str | None = None,
65 ) -> str:
66 cid = commit_id or _commit_id()
67 db.add(MusehubCommit(
68 commit_id=cid,
69 repo_id=repo_id,
70 branch=branch,
71 parent_ids=parent_ids or [],
72 message="test",
73 author="tester",
74 timestamp=datetime.now(timezone.utc),
75 ))
76 await db.flush()
77 return cid
78
79
80 async def _make_branch(
81 db: AsyncSession, repo_id: str, name: str, head: str | None = None
82 ) -> None:
83 db.add(MusehubBranch(repo_id=repo_id, name=name, head_commit_id=head))
84 await db.flush()
85
86
87 async def _make_proposal(
88 db: AsyncSession,
89 repo_id: str,
90 *,
91 from_branch: str,
92 to_branch: str,
93 state: str = "open",
94 number: int = 1,
95 ) -> str:
96 pid = _uid()
97 db.add(MusehubProposal(
98 proposal_id=pid,
99 repo_id=repo_id,
100 proposal_number=number,
101 title=f"Proposal {number}",
102 body="",
103 state=state,
104 from_branch=from_branch,
105 to_branch=to_branch,
106 author="tester",
107 ))
108 await db.flush()
109 return pid
110
111
112 # ---------------------------------------------------------------------------
113 # Tests
114 # ---------------------------------------------------------------------------
115
116
117 async def test_empty_anchors_returns_empty(db_session: AsyncSession) -> None:
118 repo_id = await _make_repo(db_session, "br-empty")
119 result = await musehub_issues.find_proposals_by_branch_reachability(
120 db_session, repo_id, []
121 )
122 assert result == []
123
124
125 async def test_anchor_exclusively_on_from_branch_matches(db_session: AsyncSession) -> None:
126 """Anchor commit is reachable from from_branch but not from to_branch → match."""
127 repo_id = await _make_repo(db_session, "br-exclusive")
128
129 # to_branch: one commit (A)
130 a = await _make_commit(db_session, repo_id, branch="main")
131 await _make_branch(db_session, repo_id, "main", a)
132
133 # from_branch: builds on A, adds B (the anchor)
134 b = await _make_commit(db_session, repo_id, branch="feat/fix", parent_ids=[a])
135 await _make_branch(db_session, repo_id, "feat/fix", b)
136
137 pid = await _make_proposal(
138 db_session, repo_id,
139 from_branch="feat/fix", to_branch="main", number=1,
140 )
141 await db_session.commit()
142
143 results = await musehub_issues.find_proposals_by_branch_reachability(
144 db_session, repo_id, [b]
145 )
146 assert len(results) == 1
147 assert results[0]["proposal_id"] == pid
148 assert results[0]["state"] == "open"
149 assert results[0]["match_reason"] == "branch_reachability"
150
151
152 async def test_anchor_already_in_to_branch_not_matched(db_session: AsyncSession) -> None:
153 """Anchor already reachable from to_branch → NOT a match (already integrated)."""
154 repo_id = await _make_repo(db_session, "br-already-integrated")
155
156 # Both branches share commit A (which is the anchor).
157 a = await _make_commit(db_session, repo_id, branch="main")
158 b = await _make_commit(db_session, repo_id, branch="main", parent_ids=[a])
159 await _make_branch(db_session, repo_id, "main", b)
160
161 # from_branch also descends from A — but A is also in to_branch (main).
162 c = await _make_commit(db_session, repo_id, branch="feat/already", parent_ids=[a])
163 await _make_branch(db_session, repo_id, "feat/already", c)
164
165 await _make_proposal(
166 db_session, repo_id,
167 from_branch="feat/already", to_branch="main", number=1,
168 )
169 await db_session.commit()
170
171 # A is a common ancestor → excluded by the NOT EXISTS anti-join.
172 results = await musehub_issues.find_proposals_by_branch_reachability(
173 db_session, repo_id, [a]
174 )
175 assert results == []
176
177
178 async def test_anchor_at_depth_2_from_from_branch(db_session: AsyncSession) -> None:
179 """Anchor is a grandparent of from_branch HEAD (depth 2) → match."""
180 repo_id = await _make_repo(db_session, "br-depth-2")
181
182 # to_branch: just A
183 a = await _make_commit(db_session, repo_id, branch="main")
184 await _make_branch(db_session, repo_id, "main", a)
185
186 # feat: A → B (anchor) → C (HEAD)
187 b = await _make_commit(db_session, repo_id, branch="feat/deep", parent_ids=[a])
188 c = await _make_commit(db_session, repo_id, branch="feat/deep", parent_ids=[b])
189 await _make_branch(db_session, repo_id, "feat/deep", c)
190
191 pid = await _make_proposal(
192 db_session, repo_id,
193 from_branch="feat/deep", to_branch="main", number=1,
194 )
195 await db_session.commit()
196
197 # B is at depth 2 from HEAD of feat/deep; not in main.
198 results = await musehub_issues.find_proposals_by_branch_reachability(
199 db_session, repo_id, [b]
200 )
201 assert len(results) == 1
202 assert results[0]["proposal_id"] == pid
203
204
205 async def test_short_anchor_prefix_resolved(db_session: AsyncSession) -> None:
206 """8-char short anchor resolves via prefix match."""
207 repo_id = await _make_repo(db_session, "br-short-anchor")
208
209 a = await _make_commit(db_session, repo_id, branch="main")
210 await _make_branch(db_session, repo_id, "main", a)
211
212 b = await _make_commit(db_session, repo_id, branch="feat/prefix", parent_ids=[a])
213 await _make_branch(db_session, repo_id, "feat/prefix", b)
214
215 pid = await _make_proposal(
216 db_session, repo_id,
217 from_branch="feat/prefix", to_branch="main", number=1,
218 )
219 await db_session.commit()
220
221 results = await musehub_issues.find_proposals_by_branch_reachability(
222 db_session, repo_id, [b[:8]] # short-form anchor
223 )
224 assert len(results) == 1
225 assert results[0]["proposal_id"] == pid
226
227
228 async def test_merged_proposal_not_returned(db_session: AsyncSession) -> None:
229 """Merged proposals have from_branch deleted → branch HEAD gone → not returned."""
230 repo_id = await _make_repo(db_session, "br-merged-excluded")
231
232 a = await _make_commit(db_session, repo_id, branch="main")
233 # Deliberately do NOT create a "feat/done" branch row — simulates post-merge deletion.
234 await _make_branch(db_session, repo_id, "main", a)
235
236 pid = await _make_proposal(
237 db_session, repo_id,
238 from_branch="feat/done", to_branch="main",
239 state="merged", number=1,
240 )
241 await db_session.commit()
242
243 results = await musehub_issues.find_proposals_by_branch_reachability(
244 db_session, repo_id, [a]
245 )
246 # Merged proposals are filtered by state='open'; also from_branch branch row gone.
247 assert results == []
248
249
250 async def test_unresolved_anchor_returns_empty(db_session: AsyncSession) -> None:
251 """Anchor that doesn't match any stored commit → nothing to walk → empty."""
252 repo_id = await _make_repo(db_session, "br-unresolved")
253 await db_session.commit()
254
255 results = await musehub_issues.find_proposals_by_branch_reachability(
256 db_session, repo_id, ["cafebabe"]
257 )
258 assert results == []
259
260
261 async def test_cross_repo_isolation(db_session: AsyncSession) -> None:
262 """Query is strictly scoped to repo_id — no leakage between repos."""
263 repo_a = await _make_repo(db_session, "br-repo-a")
264 repo_b = await _make_repo(db_session, "br-repo-b")
265
266 a = await _make_commit(db_session, repo_a, branch="main")
267 await _make_branch(db_session, repo_a, "main", a)
268 b = await _make_commit(db_session, repo_a, branch="feat/x", parent_ids=[a])
269 await _make_branch(db_session, repo_a, "feat/x", b)
270 await _make_proposal(db_session, repo_a, from_branch="feat/x", to_branch="main")
271 await db_session.commit()
272
273 # Query against repo_b — must return nothing.
274 results = await musehub_issues.find_proposals_by_branch_reachability(
275 db_session, repo_b, [b]
276 )
277 assert results == []
278
279
280 async def test_multiple_open_proposals_only_matching_returned(
281 db_session: AsyncSession,
282 ) -> None:
283 """With two open proposals, only the one containing the anchor is returned."""
284 repo_id = await _make_repo(db_session, "br-multi")
285
286 a = await _make_commit(db_session, repo_id, branch="main")
287 await _make_branch(db_session, repo_id, "main", a)
288
289 # Proposal 1: feat/one — contains anchor B
290 b = await _make_commit(db_session, repo_id, branch="feat/one", parent_ids=[a])
291 await _make_branch(db_session, repo_id, "feat/one", b)
292 pid1 = await _make_proposal(
293 db_session, repo_id, from_branch="feat/one", to_branch="main", number=1
294 )
295
296 # Proposal 2: feat/two — contains commit C (different, not the anchor)
297 c = await _make_commit(db_session, repo_id, branch="feat/two", parent_ids=[a])
298 await _make_branch(db_session, repo_id, "feat/two", c)
299 await _make_proposal(
300 db_session, repo_id, from_branch="feat/two", to_branch="main", number=2
301 )
302
303 await db_session.commit()
304
305 results = await musehub_issues.find_proposals_by_branch_reachability(
306 db_session, repo_id, [b]
307 )
308 assert len(results) == 1
309 assert results[0]["proposal_id"] == pid1
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 155 days ago