gabriel / musehub public
test_musehub_proposals_touched_symbols.py python
414 lines 12.2 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 2: symbol anchor overlap via touched_symbols on proposals.
2
3 Covers:
4 - _symbols_from_delta extracts correct symbol addresses
5 - touched_symbols populated at create_proposal time from existing branch commits
6 - touched_symbols refreshed at merge_proposal time
7 - find_proposals_by_symbol_overlap returns match when anchors intersect
8 - find_proposals_by_symbol_overlap returns empty when no intersection
9 - empty symbol_anchors returns empty list immediately
10 - cross-repo isolation
11 """
12 from __future__ import annotations
13
14 import uuid
15 from datetime import datetime, timezone
16 from typing import TypedDict
17
18 import pytest
19 from sqlalchemy.ext.asyncio import AsyncSession
20
21 from musehub.db.musehub_models import (
22 MusehubBranch,
23 MusehubCommit,
24 MusehubProposal,
25 MusehubRepo,
26 )
27 from musehub.services import musehub_issues
28 from musehub.services.musehub_proposals import (
29 _symbols_from_delta,
30 _touched_symbols_for_branch,
31 create_proposal,
32 merge_proposal,
33 )
34
35
36 # ---------------------------------------------------------------------------
37 # Helpers
38 # ---------------------------------------------------------------------------
39
40
41 class _ChildOp(TypedDict):
42 op: str
43 address: str
44 content_summary: str
45
46
47 class _DeltaOp(TypedDict):
48 address: str
49 child_ops: list[_ChildOp]
50
51
52 class _Delta(TypedDict):
53 ops: list[_DeltaOp]
54
55
56 def _uid() -> str:
57 return str(uuid.uuid4())
58
59
60 def _commit_id() -> str:
61 return uuid.uuid4().hex + uuid.uuid4().hex
62
63
64 def _delta(*symbol_addresses: str) -> _Delta:
65 """Build a minimal structured_delta containing the given symbol addresses."""
66 return _Delta(
67 ops=[
68 _DeltaOp(
69 address=addr.split("::")[0],
70 child_ops=[_ChildOp(op="update", address=addr, content_summary="function")],
71 )
72 for addr in symbol_addresses
73 ]
74 )
75
76
77 async def _make_repo(db: AsyncSession, slug: str = "sym-test") -> str:
78 repo = MusehubRepo(
79 name=slug,
80 owner="testuser",
81 slug=slug,
82 visibility="public",
83 owner_user_id=_uid(),
84 )
85 db.add(repo)
86 await db.commit()
87 await db.refresh(repo)
88 return str(repo.repo_id)
89
90
91 async def _make_commit(
92 db: AsyncSession,
93 repo_id: str,
94 *,
95 branch: str,
96 symbol_addresses: list[str] | None = None,
97 commit_id: str | None = None,
98 ) -> str:
99 """Seed a commit with an optional structured_delta and return its commit_id."""
100 cid = commit_id or _commit_id()
101 meta = {}
102 if symbol_addresses:
103 meta["structured_delta"] = _delta(*symbol_addresses)
104 row = MusehubCommit(
105 commit_id=cid,
106 repo_id=repo_id,
107 branch=branch,
108 parent_ids=[],
109 message="test commit",
110 author="tester",
111 timestamp=datetime.now(timezone.utc),
112 commit_meta=meta,
113 )
114 db.add(row)
115 await db.flush()
116 return cid
117
118
119 async def _make_branch(
120 db: AsyncSession, repo_id: str, name: str, head_commit_id: str | None = None
121 ) -> None:
122 """Seed a branch record."""
123 branch = MusehubBranch(
124 repo_id=repo_id,
125 name=name,
126 head_commit_id=head_commit_id,
127 )
128 db.add(branch)
129 await db.flush()
130
131
132 # ---------------------------------------------------------------------------
133 # Unit tests for _symbols_from_delta
134 # ---------------------------------------------------------------------------
135
136
137 def test_symbols_from_delta_extracts_symbol_addresses() -> None:
138 delta = _delta(
139 "musehub/services/musehub_issues.py::create_issue",
140 "musehub/services/musehub_issues.py::get_issue",
141 )
142 result = _symbols_from_delta(delta)
143 assert "musehub/services/musehub_issues.py::create_issue" in result
144 assert "musehub/services/musehub_issues.py::get_issue" in result
145 assert len(result) == 2
146
147
148 def test_symbols_from_delta_skips_file_level_ops() -> None:
149 """File-level ops without '::' in address must not appear in result."""
150 delta = {
151 "ops": [
152 {
153 "address": "musehub/services/musehub_issues.py",
154 "child_ops": [],
155 }
156 ]
157 }
158 result = _symbols_from_delta(delta)
159 assert result == []
160
161
162 def test_symbols_from_delta_handles_non_dict() -> None:
163 assert _symbols_from_delta(None) == []
164 assert _symbols_from_delta("bad") == []
165 assert _symbols_from_delta({}) == []
166
167
168 def test_symbols_from_delta_deduplicates() -> None:
169 delta = _delta(
170 "musehub/services/foo.py::bar",
171 "musehub/services/foo.py::bar",
172 )
173 result = _symbols_from_delta(delta)
174 assert result.count("musehub/services/foo.py::bar") == 1
175
176
177 # ---------------------------------------------------------------------------
178 # Integration tests: touched_symbols populated at create / merge
179 # ---------------------------------------------------------------------------
180
181
182 async def test_touched_symbols_for_branch_extracts_from_commits(
183 db_session: AsyncSession,
184 ) -> None:
185 repo_id = await _make_repo(db_session, "ts-branch-extract")
186 await _make_commit(
187 db_session, repo_id,
188 branch="feat/s2",
189 symbol_addresses=["a/b.py::foo", "a/b.py::bar"],
190 )
191 await _make_commit(
192 db_session, repo_id,
193 branch="feat/s2",
194 symbol_addresses=["a/c.py::baz"],
195 )
196 await db_session.commit()
197
198 result = await _touched_symbols_for_branch(db_session, repo_id, "feat/s2")
199 assert "a/b.py::foo" in result
200 assert "a/b.py::bar" in result
201 assert "a/c.py::baz" in result
202 assert len(result) == 3
203
204
205 async def test_create_proposal_populates_touched_symbols(
206 db_session: AsyncSession,
207 ) -> None:
208 repo_id = await _make_repo(db_session, "ts-create")
209 head_cid = await _make_commit(
210 db_session, repo_id,
211 branch="feat/create-signal",
212 symbol_addresses=["musehub/services/x.py::MyFunc"],
213 )
214 await _make_branch(db_session, repo_id, "feat/create-signal", head_cid)
215 await _make_branch(db_session, repo_id, "main", head_cid)
216 await db_session.commit()
217
218 proposal = await create_proposal(
219 db_session,
220 repo_id=repo_id,
221 title="Test proposal",
222 from_branch="feat/create-signal",
223 to_branch="main",
224 )
225 await db_session.commit()
226
227 # Fetch the raw ORM row to verify the column was written.
228 from sqlalchemy import select as _select
229 row = (await db_session.execute(
230 _select(MusehubProposal).where(MusehubProposal.proposal_id == proposal.proposal_id)
231 )).scalar_one()
232 assert "musehub/services/x.py::MyFunc" in (row.touched_symbols or [])
233
234
235 async def test_merge_proposal_refreshes_touched_symbols(
236 db_session: AsyncSession,
237 ) -> None:
238 """touched_symbols at merge time includes any new commits added after create."""
239 repo_id = await _make_repo(db_session, "ts-merge")
240 initial_cid = await _make_commit(
241 db_session, repo_id,
242 branch="feat/refresh",
243 symbol_addresses=["svc/old.py::OldFunc"],
244 )
245 await _make_branch(db_session, repo_id, "feat/refresh", initial_cid)
246 to_cid = await _make_commit(db_session, repo_id, branch="main")
247 await _make_branch(db_session, repo_id, "main", to_cid)
248 await db_session.commit()
249
250 proposal = await create_proposal(
251 db_session,
252 repo_id=repo_id,
253 title="Refresh test",
254 from_branch="feat/refresh",
255 to_branch="main",
256 )
257 await db_session.commit()
258
259 # Push a new commit to the feature branch after proposal creation.
260 new_cid = await _make_commit(
261 db_session, repo_id,
262 branch="feat/refresh",
263 symbol_addresses=["svc/new.py::NewFunc"],
264 )
265 # Update the branch head.
266 from sqlalchemy import select as _select
267 branch_row = (await db_session.execute(
268 _select(MusehubBranch).where(
269 MusehubBranch.repo_id == repo_id, MusehubBranch.name == "feat/refresh"
270 )
271 )).scalar_one()
272 branch_row.head_commit_id = new_cid
273 await db_session.flush()
274 await db_session.commit()
275
276 await merge_proposal(db_session, repo_id, proposal.proposal_id)
277 await db_session.commit()
278
279 row = (await db_session.execute(
280 _select(MusehubProposal).where(MusehubProposal.proposal_id == proposal.proposal_id)
281 )).scalar_one()
282 touched = row.touched_symbols or []
283 assert "svc/old.py::OldFunc" in touched
284 assert "svc/new.py::NewFunc" in touched
285
286
287 # ---------------------------------------------------------------------------
288 # Integration tests: find_proposals_by_symbol_overlap
289 # ---------------------------------------------------------------------------
290
291
292 async def test_symbol_overlap_returns_match(db_session: AsyncSession) -> None:
293 repo_id = await _make_repo(db_session, "overlap-match")
294
295 # Manually seed a proposal with a known touched_symbols.
296 pid = _uid()
297 row = MusehubProposal(
298 proposal_id=pid,
299 repo_id=repo_id,
300 proposal_number=1,
301 title="Fix create_issue bug",
302 body="",
303 state="merged",
304 from_branch="feat/fix",
305 to_branch="main",
306 author="tester",
307 touched_symbols=["musehub/services/musehub_issues.py::create_issue"],
308 )
309 db_session.add(row)
310 await db_session.commit()
311
312 results = await musehub_issues.find_proposals_by_symbol_overlap(
313 db_session, repo_id,
314 ["musehub/services/musehub_issues.py::create_issue"],
315 )
316 assert len(results) == 1
317 assert results[0]["proposal_id"] == pid
318 assert results[0]["state"] == "merged"
319 assert results[0]["match_reason"] == "symbol_overlap"
320
321
322 async def test_symbol_overlap_no_match(db_session: AsyncSession) -> None:
323 repo_id = await _make_repo(db_session, "overlap-no-match")
324
325 pid = _uid()
326 row = MusehubProposal(
327 proposal_id=pid,
328 repo_id=repo_id,
329 proposal_number=1,
330 title="Unrelated proposal",
331 body="",
332 state="merged",
333 from_branch="feat/unrelated",
334 to_branch="main",
335 author="tester",
336 touched_symbols=["musehub/services/other.py::some_fn"],
337 )
338 db_session.add(row)
339 await db_session.commit()
340
341 results = await musehub_issues.find_proposals_by_symbol_overlap(
342 db_session, repo_id,
343 ["musehub/services/musehub_issues.py::create_issue"],
344 )
345 assert results == []
346
347
348 async def test_symbol_overlap_empty_anchors_returns_empty(
349 db_session: AsyncSession,
350 ) -> None:
351 repo_id = await _make_repo(db_session, "overlap-empty")
352 await db_session.commit()
353
354 results = await musehub_issues.find_proposals_by_symbol_overlap(
355 db_session, repo_id, []
356 )
357 assert results == []
358
359
360 async def test_symbol_overlap_cross_repo_isolation(db_session: AsyncSession) -> None:
361 repo_a = await _make_repo(db_session, "overlap-repo-a")
362 repo_b = await _make_repo(db_session, "overlap-repo-b")
363
364 pid = _uid()
365 row = MusehubProposal(
366 proposal_id=pid,
367 repo_id=repo_a,
368 proposal_number=1,
369 title="Proposal in repo A",
370 body="",
371 state="merged",
372 from_branch="feat/a",
373 to_branch="main",
374 author="tester",
375 touched_symbols=["musehub/services/musehub_issues.py::create_issue"],
376 )
377 db_session.add(row)
378 await db_session.commit()
379
380 # Query against repo_b — must return nothing.
381 results = await musehub_issues.find_proposals_by_symbol_overlap(
382 db_session, repo_b,
383 ["musehub/services/musehub_issues.py::create_issue"],
384 )
385 assert results == []
386
387
388 async def test_symbol_overlap_open_proposal_matched(db_session: AsyncSession) -> None:
389 """Open proposals with matching touched_symbols are returned."""
390 repo_id = await _make_repo(db_session, "overlap-open")
391
392 pid = _uid()
393 row = MusehubProposal(
394 proposal_id=pid,
395 repo_id=repo_id,
396 proposal_number=1,
397 title="In-progress fix",
398 body="",
399 state="open",
400 from_branch="feat/in-progress",
401 to_branch="main",
402 author="tester",
403 touched_symbols=["musehub/api/routes/musehub/ui_issues.py::issue_detail_page"],
404 )
405 db_session.add(row)
406 await db_session.commit()
407
408 results = await musehub_issues.find_proposals_by_symbol_overlap(
409 db_session, repo_id,
410 ["musehub/api/routes/musehub/ui_issues.py::issue_detail_page"],
411 )
412 assert len(results) == 1
413 assert results[0]["proposal_id"] == pid
414 assert results[0]["state"] == "open"
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 156 days ago