gabriel / musehub public

test_musehub_issues_branch_reachability.py file-level

at sha256:3 · View file ↗ · Intel ↗

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