gabriel / musehub public
test_musehub_proposals_touched_symbols.py python
424 lines 13.0 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 122 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 secrets
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, now_utc_iso
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 secrets.token_hex(16)
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 row = MusehubCommit(
110 commit_id=cid,
111 repo_id=repo_id,
112 branch=branch,
113 parent_ids=[],
114 message="test commit",
115 author="tester",
116 timestamp=datetime.now(timezone.utc),
117 structured_delta=_delta(*symbol_addresses) if symbol_addresses else None,
118 )
119 db.add(row)
120 await db.flush()
121 return cid
122
123
124 async def _make_branch(
125 db: AsyncSession, repo_id: str, name: str, head_commit_id: str | None = None
126 ) -> None:
127 """Seed a branch record."""
128 branch = MusehubBranch(
129 branch_id=compute_branch_id(repo_id, name),
130 repo_id=repo_id,
131 name=name,
132 head_commit_id=head_commit_id,
133 )
134 db.add(branch)
135 await db.flush()
136
137
138 # ---------------------------------------------------------------------------
139 # Unit tests for _symbols_from_delta
140 # ---------------------------------------------------------------------------
141
142
143 def test_symbols_from_delta_extracts_symbol_addresses() -> None:
144 delta = _delta(
145 "musehub/services/musehub_issues.py::create_issue",
146 "musehub/services/musehub_issues.py::get_issue",
147 )
148 result = _symbols_from_delta(delta)
149 assert "musehub/services/musehub_issues.py::create_issue" in result
150 assert "musehub/services/musehub_issues.py::get_issue" in result
151 assert len(result) == 2
152
153
154 def test_symbols_from_delta_skips_file_level_ops() -> None:
155 """File-level ops without '::' in address must not appear in result."""
156 delta = {
157 "ops": [
158 {
159 "address": "musehub/services/musehub_issues.py",
160 "child_ops": [],
161 }
162 ]
163 }
164 result = _symbols_from_delta(delta)
165 assert result == []
166
167
168 def test_symbols_from_delta_handles_non_dict() -> None:
169 assert _symbols_from_delta(None) == []
170 assert _symbols_from_delta("bad") == []
171 assert _symbols_from_delta({}) == []
172
173
174 def test_symbols_from_delta_deduplicates() -> None:
175 delta = _delta(
176 "musehub/services/foo.py::bar",
177 "musehub/services/foo.py::bar",
178 )
179 result = _symbols_from_delta(delta)
180 assert result.count("musehub/services/foo.py::bar") == 1
181
182
183 # ---------------------------------------------------------------------------
184 # Integration tests: touched_symbols populated at create / merge
185 # ---------------------------------------------------------------------------
186
187
188 async def test_touched_symbols_for_branch_extracts_from_commits(
189 db_session: AsyncSession,
190 ) -> None:
191 repo_id = await _make_repo(db_session, "ts-branch-extract")
192 await _make_commit(
193 db_session, repo_id,
194 branch="feat/s2",
195 symbol_addresses=["a/b.py::foo", "a/b.py::bar"],
196 )
197 await _make_commit(
198 db_session, repo_id,
199 branch="feat/s2",
200 symbol_addresses=["a/c.py::baz"],
201 )
202 await db_session.commit()
203
204 result = await _touched_symbols_for_branch(db_session, repo_id, "feat/s2")
205 assert "a/b.py::foo" in result
206 assert "a/b.py::bar" in result
207 assert "a/c.py::baz" in result
208 assert len(result) == 3
209
210
211 async def test_create_proposal_populates_touched_symbols(
212 db_session: AsyncSession,
213 ) -> None:
214 repo_id = await _make_repo(db_session, "ts-create")
215 head_cid = await _make_commit(
216 db_session, repo_id,
217 branch="feat/create-signal",
218 symbol_addresses=["musehub/services/x.py::MyFunc"],
219 )
220 await _make_branch(db_session, repo_id, "feat/create-signal", head_cid)
221 await _make_branch(db_session, repo_id, "main", head_cid)
222 await db_session.commit()
223
224 proposal = await create_proposal(
225 db_session,
226 repo_id=repo_id,
227 title="Test proposal",
228 from_branch="feat/create-signal",
229 to_branch="main",
230 )
231 await db_session.commit()
232
233 # Fetch the raw ORM row to verify the column was written.
234 from sqlalchemy import select as _select
235 row = (await db_session.execute(
236 _select(MusehubProposal).where(MusehubProposal.proposal_id == proposal.proposal_id)
237 )).scalar_one()
238 assert "musehub/services/x.py::MyFunc" in (row.touched_symbols or [])
239
240
241 async def test_merge_proposal_refreshes_touched_symbols(
242 db_session: AsyncSession,
243 ) -> None:
244 """touched_symbols at merge time includes any new commits added after create."""
245 repo_id = await _make_repo(db_session, "ts-merge")
246 initial_cid = await _make_commit(
247 db_session, repo_id,
248 branch="feat/refresh",
249 symbol_addresses=["svc/old.py::OldFunc"],
250 )
251 await _make_branch(db_session, repo_id, "feat/refresh", initial_cid)
252 to_cid = await _make_commit(db_session, repo_id, branch="main")
253 await _make_branch(db_session, repo_id, "main", to_cid)
254 await db_session.commit()
255
256 proposal = await create_proposal(
257 db_session,
258 repo_id=repo_id,
259 title="Refresh test",
260 from_branch="feat/refresh",
261 to_branch="main",
262 )
263 await db_session.commit()
264
265 # Push a new commit to the feature branch after proposal creation.
266 new_cid = await _make_commit(
267 db_session, repo_id,
268 branch="feat/refresh",
269 symbol_addresses=["svc/new.py::NewFunc"],
270 )
271 # Update the branch head.
272 from sqlalchemy import select as _select
273 branch_row = (await db_session.execute(
274 _select(MusehubBranch).where(
275 MusehubBranch.repo_id == repo_id, MusehubBranch.name == "feat/refresh"
276 )
277 )).scalar_one()
278 branch_row.head_commit_id = new_cid
279 await db_session.flush()
280 await db_session.commit()
281
282 await merge_proposal(db_session, repo_id, proposal.proposal_id)
283 await db_session.commit()
284
285 row = (await db_session.execute(
286 _select(MusehubProposal).where(MusehubProposal.proposal_id == proposal.proposal_id)
287 )).scalar_one()
288 touched = row.touched_symbols or []
289 assert "svc/old.py::OldFunc" in touched
290 assert "svc/new.py::NewFunc" in touched
291
292
293 # ---------------------------------------------------------------------------
294 # Integration tests: find_proposals_by_symbol_overlap
295 # ---------------------------------------------------------------------------
296
297
298 async def test_symbol_overlap_returns_match(db_session: AsyncSession) -> None:
299 repo_id = await _make_repo(db_session, "overlap-match")
300
301 # Manually seed a proposal with a known touched_symbols.
302 author_id = compute_identity_id(b"tester")
303 pid = compute_proposal_id(repo_id, author_id, "feat/fix", "main", now_utc_iso())
304 row = MusehubProposal(
305 proposal_id=pid,
306 repo_id=repo_id,
307 proposal_number=1,
308 title="Fix create_issue bug",
309 body="",
310 state="merged",
311 from_branch="feat/fix",
312 to_branch="main",
313 author="tester",
314 touched_symbols=["musehub/services/musehub_issues.py::create_issue"],
315 )
316 db_session.add(row)
317 await db_session.commit()
318
319 results = await musehub_issues.find_proposals_by_symbol_overlap(
320 db_session, repo_id,
321 ["musehub/services/musehub_issues.py::create_issue"],
322 )
323 assert len(results) == 1
324 assert results[0]["proposal_id"] == pid
325 assert results[0]["state"] == "merged"
326 assert results[0]["match_reason"] == "symbol_overlap"
327
328
329 async def test_symbol_overlap_no_match(db_session: AsyncSession) -> None:
330 repo_id = await _make_repo(db_session, "overlap-no-match")
331
332 author_id = compute_identity_id(b"tester")
333 pid = compute_proposal_id(repo_id, author_id, "feat/unrelated", "main", now_utc_iso())
334 row = MusehubProposal(
335 proposal_id=pid,
336 repo_id=repo_id,
337 proposal_number=1,
338 title="Unrelated proposal",
339 body="",
340 state="merged",
341 from_branch="feat/unrelated",
342 to_branch="main",
343 author="tester",
344 touched_symbols=["musehub/services/other.py::some_fn"],
345 )
346 db_session.add(row)
347 await db_session.commit()
348
349 results = await musehub_issues.find_proposals_by_symbol_overlap(
350 db_session, repo_id,
351 ["musehub/services/musehub_issues.py::create_issue"],
352 )
353 assert results == []
354
355
356 async def test_symbol_overlap_empty_anchors_returns_empty(
357 db_session: AsyncSession,
358 ) -> None:
359 repo_id = await _make_repo(db_session, "overlap-empty")
360 await db_session.commit()
361
362 results = await musehub_issues.find_proposals_by_symbol_overlap(
363 db_session, repo_id, []
364 )
365 assert results == []
366
367
368 async def test_symbol_overlap_cross_repo_isolation(db_session: AsyncSession) -> None:
369 repo_a = await _make_repo(db_session, "overlap-repo-a")
370 repo_b = await _make_repo(db_session, "overlap-repo-b")
371
372 author_id = compute_identity_id(b"tester")
373 pid = compute_proposal_id(repo_a, author_id, "feat/a", "main", now_utc_iso())
374 row = MusehubProposal(
375 proposal_id=pid,
376 repo_id=repo_a,
377 proposal_number=1,
378 title="Proposal in repo A",
379 body="",
380 state="merged",
381 from_branch="feat/a",
382 to_branch="main",
383 author="tester",
384 touched_symbols=["musehub/services/musehub_issues.py::create_issue"],
385 )
386 db_session.add(row)
387 await db_session.commit()
388
389 # Query against repo_b — must return nothing.
390 results = await musehub_issues.find_proposals_by_symbol_overlap(
391 db_session, repo_b,
392 ["musehub/services/musehub_issues.py::create_issue"],
393 )
394 assert results == []
395
396
397 async def test_symbol_overlap_open_proposal_matched(db_session: AsyncSession) -> None:
398 """Open proposals with matching touched_symbols are returned."""
399 repo_id = await _make_repo(db_session, "overlap-open")
400
401 author_id = compute_identity_id(b"tester")
402 pid = compute_proposal_id(repo_id, author_id, "feat/in-progress", "main", now_utc_iso())
403 row = MusehubProposal(
404 proposal_id=pid,
405 repo_id=repo_id,
406 proposal_number=1,
407 title="In-progress fix",
408 body="",
409 state="open",
410 from_branch="feat/in-progress",
411 to_branch="main",
412 author="tester",
413 touched_symbols=["musehub/api/routes/musehub/ui_issues.py::issue_detail_page"],
414 )
415 db_session.add(row)
416 await db_session.commit()
417
418 results = await musehub_issues.find_proposals_by_symbol_overlap(
419 db_session, repo_id,
420 ["musehub/api/routes/musehub/ui_issues.py::issue_detail_page"],
421 )
422 assert len(results) == 1
423 assert results[0]["proposal_id"] == pid
424 assert results[0]["state"] == "open"
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 122 days ago