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