gabriel / musehub public
test_musehub_issues_commit_graph.py python
280 lines 8.9 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 156 days ago
1 """Tests for Signal 1: commit graph containment in find_proposals_by_commit_graph.
2
3 Covers:
4 - Merged proposal matched via BFS ancestor walk (depth 1 — direct parent)
5 - Merged proposal matched via BFS ancestor walk (depth 2 — grandparent)
6 - Open proposal matched via branch membership
7 - No match when anchors do not intersect the commit graph
8 - Short-form (8-char) anchor resolution
9 - Empty commit_anchors returns empty list immediately
10 - Proposals from another repo are not returned
11 """
12 from __future__ import annotations
13
14 import uuid
15 from datetime import datetime, timezone
16
17 import pytest
18 from sqlalchemy.ext.asyncio import AsyncSession
19
20 from musehub.db.musehub_models import (
21 MusehubCommit,
22 MusehubIssue,
23 MusehubProposal,
24 MusehubRepo,
25 )
26 from musehub.services import musehub_issues
27
28
29 # ---------------------------------------------------------------------------
30 # Helpers
31 # ---------------------------------------------------------------------------
32
33
34 def _uid() -> str:
35 return str(uuid.uuid4())
36
37
38 def _commit_id() -> str:
39 """Return a random 64-char hex commit ID."""
40 return uuid.uuid4().hex + uuid.uuid4().hex
41
42
43 async def _make_repo(db: AsyncSession, slug: str = "graph-test") -> str:
44 repo = MusehubRepo(
45 name=slug,
46 owner="testuser",
47 slug=slug,
48 visibility="public",
49 owner_user_id=_uid(),
50 )
51 db.add(repo)
52 await db.commit()
53 await db.refresh(repo)
54 return str(repo.repo_id)
55
56
57 async def _make_commit(
58 db: AsyncSession,
59 repo_id: str,
60 *,
61 parent_ids: list[str] | None = None,
62 branch: str = "dev",
63 commit_id: str | None = None,
64 ) -> str:
65 """Seed a commit row and return its commit_id."""
66 cid = commit_id or _commit_id()
67 row = MusehubCommit(
68 commit_id=cid,
69 repo_id=repo_id,
70 message="test commit",
71 author="tester",
72 branch=branch,
73 parent_ids=parent_ids or [],
74 timestamp=datetime.now(timezone.utc),
75 )
76 db.add(row)
77 await db.flush()
78 return cid
79
80
81 async def _make_proposal(
82 db: AsyncSession,
83 repo_id: str,
84 *,
85 state: str = "merged",
86 from_branch: str = "feat/x",
87 to_branch: str = "main",
88 merge_commit_id: str | None = None,
89 number: int = 1,
90 ) -> str:
91 """Seed a proposal and return its proposal_id."""
92 pid = _uid()
93 row = MusehubProposal(
94 proposal_id=pid,
95 repo_id=repo_id,
96 proposal_number=number,
97 title=f"Proposal {number}",
98 body="",
99 state=state,
100 from_branch=from_branch,
101 to_branch=to_branch,
102 author="tester",
103 merge_commit_id=merge_commit_id,
104 )
105 db.add(row)
106 await db.flush()
107 return pid
108
109
110 # ---------------------------------------------------------------------------
111 # Tests
112 # ---------------------------------------------------------------------------
113
114
115 async def test_empty_anchors_returns_empty(db_session: AsyncSession) -> None:
116 """No anchors → fast-path returns [] without hitting the DB."""
117 repo_id = await _make_repo(db_session, "empty-anchors")
118 result = await musehub_issues.find_proposals_by_commit_graph(
119 db_session, repo_id, []
120 )
121 assert result == []
122
123
124 async def test_merged_proposal_matched_depth_1(db_session: AsyncSession) -> None:
125 """Merged proposal linked when anchor commit is a direct parent of merge_commit_id."""
126 repo_id = await _make_repo(db_session, "merged-depth-1")
127
128 # anchor commit (the feature branch head)
129 anchor_cid = await _make_commit(db_session, repo_id, branch="feat/security")
130
131 # merge commit whose parent list includes the anchor
132 merge_cid = await _make_commit(
133 db_session, repo_id, parent_ids=[anchor_cid], branch="main"
134 )
135
136 pid = await _make_proposal(
137 db_session, repo_id, state="merged", merge_commit_id=merge_cid, number=1
138 )
139 await db_session.commit()
140
141 results = await musehub_issues.find_proposals_by_commit_graph(
142 db_session, repo_id, [anchor_cid]
143 )
144 assert len(results) == 1
145 assert results[0]["proposal_id"] == pid
146 assert results[0]["state"] == "merged"
147 assert results[0]["match_reason"] == "commit_graph"
148
149
150 async def test_merged_proposal_matched_depth_2(db_session: AsyncSession) -> None:
151 """Merged proposal linked when anchor is a grandparent (depth 2) of merge_commit_id."""
152 repo_id = await _make_repo(db_session, "merged-depth-2")
153
154 # anchor: grandparent commit
155 grandparent_cid = await _make_commit(db_session, repo_id, branch="feat/y")
156 # parent: its child
157 parent_cid = await _make_commit(
158 db_session, repo_id, parent_ids=[grandparent_cid], branch="feat/y"
159 )
160 # merge commit
161 merge_cid = await _make_commit(
162 db_session, repo_id, parent_ids=[parent_cid], branch="main"
163 )
164
165 pid = await _make_proposal(
166 db_session, repo_id, state="merged", merge_commit_id=merge_cid, number=2
167 )
168 await db_session.commit()
169
170 results = await musehub_issues.find_proposals_by_commit_graph(
171 db_session, repo_id, [grandparent_cid]
172 )
173 assert len(results) == 1
174 assert results[0]["proposal_id"] == pid
175 assert results[0]["match_reason"] == "commit_graph"
176
177
178 async def test_open_proposal_matched_via_branch(db_session: AsyncSession) -> None:
179 """Open proposal linked when an anchor commit lives on the proposal's from_branch."""
180 repo_id = await _make_repo(db_session, "open-branch")
181
182 # A commit on the feature branch that matches the anchor
183 anchor_cid = await _make_commit(
184 db_session, repo_id, branch="feat/open-fix"
185 )
186 pid = await _make_proposal(
187 db_session, repo_id,
188 state="open",
189 from_branch="feat/open-fix",
190 to_branch="main",
191 merge_commit_id=None,
192 number=3,
193 )
194 await db_session.commit()
195
196 results = await musehub_issues.find_proposals_by_commit_graph(
197 db_session, repo_id, [anchor_cid]
198 )
199 assert len(results) == 1
200 assert results[0]["proposal_id"] == pid
201 assert results[0]["state"] == "open"
202 assert results[0]["match_reason"] == "commit_graph"
203
204
205 async def test_no_match_when_anchor_not_in_graph(db_session: AsyncSession) -> None:
206 """No results when the anchor commit is unrelated to any proposal's commit graph."""
207 repo_id = await _make_repo(db_session, "no-match")
208
209 unrelated_cid = await _make_commit(db_session, repo_id, branch="feat/unrelated")
210
211 # Proposal whose ancestors don't include unrelated_cid
212 other_cid = await _make_commit(db_session, repo_id, branch="feat/other")
213 merge_cid = await _make_commit(
214 db_session, repo_id, parent_ids=[other_cid], branch="main"
215 )
216 await _make_proposal(
217 db_session, repo_id, state="merged", merge_commit_id=merge_cid, number=4
218 )
219 await db_session.commit()
220
221 results = await musehub_issues.find_proposals_by_commit_graph(
222 db_session, repo_id, [unrelated_cid]
223 )
224 assert results == []
225
226
227 async def test_short_anchor_prefix_match(db_session: AsyncSession) -> None:
228 """Short (8-char) anchor resolves correctly via prefix match."""
229 repo_id = await _make_repo(db_session, "short-anchor")
230
231 anchor_cid = await _make_commit(db_session, repo_id, branch="feat/z")
232 merge_cid = await _make_commit(
233 db_session, repo_id, parent_ids=[anchor_cid], branch="main"
234 )
235 pid = await _make_proposal(
236 db_session, repo_id, state="merged", merge_commit_id=merge_cid, number=5
237 )
238 await db_session.commit()
239
240 # Use only the first 8 chars of the full commit ID
241 short_anchor = anchor_cid[:8]
242 results = await musehub_issues.find_proposals_by_commit_graph(
243 db_session, repo_id, [short_anchor]
244 )
245 assert len(results) == 1
246 assert results[0]["proposal_id"] == pid
247
248
249 async def test_proposals_from_other_repo_not_returned(db_session: AsyncSession) -> None:
250 """Commit graph lookup is strictly scoped to the given repo_id."""
251 repo_a = await _make_repo(db_session, "graph-repo-a")
252 repo_b = await _make_repo(db_session, "graph-repo-b")
253
254 # Anchor and proposal are in repo_a
255 anchor_cid = await _make_commit(db_session, repo_a, branch="feat/q")
256 merge_cid = await _make_commit(
257 db_session, repo_a, parent_ids=[anchor_cid], branch="main"
258 )
259 await _make_proposal(
260 db_session, repo_a, state="merged", merge_commit_id=merge_cid, number=6
261 )
262 await db_session.commit()
263
264 # Query against repo_b — should find nothing
265 results = await musehub_issues.find_proposals_by_commit_graph(
266 db_session, repo_b, [anchor_cid]
267 )
268 assert results == []
269
270
271 async def test_anchor_unresolved_returns_empty(db_session: AsyncSession) -> None:
272 """Anchor that doesn't match any stored commit yields an empty result."""
273 repo_id = await _make_repo(db_session, "unresolved-anchor")
274 await db_session.commit()
275
276 phantom_anchor = "deadbeef" # not in musehub_commits
277 results = await musehub_issues.find_proposals_by_commit_graph(
278 db_session, repo_id, [phantom_anchor]
279 )
280 assert results == []
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 156 days ago