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