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