test_musehub_issues_commit_graph.py
python
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