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