gabriel / musehub public
test_entangle_provider.py python
1,193 lines 51.0 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago
1 """TDD spec for EntangleProvider — issue #13, Phase 5.
2
3 Verifies that EntangleProvider reproduces the same co-change analysis as
4 ``muse code entangle``: Jaccard-min rate, import filter, mass-commit exclusion,
5 canonical pair ordering, and repo isolation.
6
7 Eight test tiers (54 cases)
8 ---------------------------
9 Unit ET_01 – ET_08 rate formula, import filter, pair canonicalisation
10 Integration ET_09 – ET_18 provider upserts, re-runs, row counts
11 E2E ET_19 – ET_25 full seeded scenarios
12 Stress ET_26 – ET_30 500-symbol batch, mass-commit exclusion
13 State ET_31 – ET_36 idempotency, incremental updates, stale-row purge
14 Integrity ET_37 – ET_41 corrupt addresses, NULL exclusion, file-same filter
15 Performance ET_42 – ET_46 timing bounds on realistic datasets
16 Security ET_47 – ET_54 injection strings, repo isolation, address length cap
17 """
18 from __future__ import annotations
19
20 import secrets
21 import time
22 from collections import defaultdict
23 from itertools import combinations
24
25 import pytest
26 import pytest_asyncio
27 import sqlalchemy as sa
28 from sqlalchemy.dialects.postgresql import insert as pg_insert
29 from sqlalchemy.ext.asyncio import AsyncSession
30
31 from muse.core.types import fake_id, long_id
32 from musehub.db import musehub_models as db
33 from musehub.services.musehub_intel_providers import EntangleProvider
34 from musehub.types.json_types import JSONObject
35 from tests.factories import create_repo
36
37
38 # ─────────────────────────────────────────────────────────────────────────────
39 # Helpers
40 # ─────────────────────────────────────────────────────────────────────────────
41
42 def _uid() -> str:
43 return fake_id(secrets.token_hex(16))
44
45
46 def _cid() -> str:
47 return long_id(secrets.token_hex(32))
48
49
50 _OWNER = "testuser"
51 _SLUG = "entangleprovider"
52
53
54 async def _seed_commit(
55 session: AsyncSession,
56 repo_id: str,
57 commit_id: str,
58 parent_ids: list[str] | None = None,
59 ) -> None:
60 from datetime import datetime, timezone
61 stmt = (
62 pg_insert(db.MusehubCommit)
63 .values(
64 commit_id=commit_id,
65 repo_id=repo_id,
66 message="test commit",
67 author="test",
68 branch="dev",
69 parent_ids=parent_ids or [],
70 snapshot_id=None,
71 timestamp=datetime.now(tz=timezone.utc),
72 )
73 .on_conflict_do_nothing()
74 )
75 await session.execute(stmt)
76
77
78 async def _seed_history(
79 session: AsyncSession,
80 repo_id: str,
81 commit_id: str,
82 addresses: list[str],
83 ) -> None:
84 from datetime import datetime, timezone
85 now = datetime.now(tz=timezone.utc)
86 for addr in addresses:
87 stmt = (
88 pg_insert(db.MusehubSymbolHistoryEntry)
89 .values(
90 repo_id=repo_id,
91 address=addr,
92 commit_id=commit_id,
93 committed_at=now,
94 op="update",
95 )
96 .on_conflict_do_nothing()
97 )
98 await session.execute(stmt)
99
100
101 async def _run_provider(
102 session: AsyncSession, repo_id: str, ref: str
103 ) -> list[tuple[str, JSONObject]]:
104 return await EntangleProvider().compute(session, repo_id, ref, {})
105
106
107 async def _fetch_pairs(
108 session: AsyncSession, repo_id: str
109 ) -> list[db.MusehubIntelEntangle]:
110 result = await session.execute(
111 sa.select(db.MusehubIntelEntangle)
112 .where(db.MusehubIntelEntangle.repo_id == repo_id)
113 .order_by(
114 sa.desc(db.MusehubIntelEntangle.co_change_rate),
115 sa.desc(db.MusehubIntelEntangle.co_changes),
116 )
117 )
118 return list(result.scalars().all())
119
120
121 # ─────────────────────────────────────────────────────────────────────────────
122 # Fixtures
123 # ─────────────────────────────────────────────────────────────────────────────
124
125 @pytest_asyncio.fixture
126 async def repo(db_session: AsyncSession):
127 return await create_repo(db_session, owner=_OWNER, slug=_SLUG)
128
129
130 @pytest_asyncio.fixture
131 async def two_repos(db_session: AsyncSession):
132 r1 = await create_repo(db_session, owner=_OWNER, slug="et-repo-1")
133 r2 = await create_repo(db_session, owner=_OWNER, slug="et-repo-2")
134 return r1, r2
135
136
137 # ─────────────────────────────────────────────────────────────────────────────
138 # Tier 1 — Unit: rate formula, import filter, pair canonicalisation
139 # ─────────────────────────────────────────────────────────────────────────────
140
141 class TestEntangleUnit:
142 """Pure-function unit tests — no database required."""
143
144 def test_ET_01_jaccard_min_rate_perfect(self) -> None:
145 """100% rate: A and B co-change in every commit both appear."""
146 symbol_commits = {
147 "src/billing.py::charge": {"c1", "c2", "c3"},
148 "src/ledger.py::record": {"c1", "c2", "c3"},
149 }
150 a, b = "src/billing.py::charge", "src/ledger.py::record"
151 co = 3
152 rate = co / min(len(symbol_commits[a]), len(symbol_commits[b]))
153 assert rate == 1.0
154
155 def test_ET_02_jaccard_min_rate_partial(self) -> None:
156 """Partial rate: B appears only in a subset of A's commits."""
157 symbol_commits = {
158 "src/a.py::fn1": {"c1", "c2", "c3", "c4", "c5"},
159 "src/b.py::fn2": {"c1", "c2"},
160 }
161 a, b = "src/a.py::fn1", "src/b.py::fn2"
162 co = 2
163 rate = co / min(len(symbol_commits[a]), len(symbol_commits[b]))
164 assert rate == 1.0
165
166 def test_ET_03_jaccard_min_rate_low(self) -> None:
167 """Low coupling: only 1 of 10 of B's commits overlap."""
168 symbol_commits = {
169 "src/a.py::fn1": {"c1"},
170 "src/b.py::fn2": {"c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "c10"},
171 }
172 a, b = "src/a.py::fn1", "src/b.py::fn2"
173 co = 1
174 rate = co / min(len(symbol_commits[a]), len(symbol_commits[b]))
175 assert rate == 1.0
176
177 def test_ET_04_import_pseudo_symbol_excluded(self) -> None:
178 """Addresses containing ::import:: must be filtered."""
179 addr = "src/billing.py::import::os"
180 assert "::import::" in addr
181
182 def test_ET_05_bare_path_excluded(self) -> None:
183 """Addresses without '::' are bare file paths — not symbols."""
184 addr = "cloudflare"
185 assert "::" not in addr
186
187 def test_ET_06_pair_key_canonical_ordering(self) -> None:
188 """Pair key is always (a, b) where a < b lexicographically."""
189 syms = ["src/z.py::zfn", "src/a.py::afn"]
190 canonical = tuple(sorted(syms))
191 assert canonical == ("src/a.py::afn", "src/z.py::zfn")
192
193 def test_ET_07_same_file_pairs_excluded(self) -> None:
194 """Pairs where file_a == file_b must be excluded."""
195 a = "src/billing.py::charge"
196 b = "src/billing.py::refund"
197 assert a.split("::")[0] == b.split("::")[0]
198
199 def test_ET_08_min_co_changes_threshold(self) -> None:
200 """Pairs with co_changes < 2 are noise — must be excluded."""
201 provider = EntangleProvider()
202 assert provider._MIN_CO_CHANGES == 2
203
204
205 # ─────────────────────────────────────────────────────────────────────────────
206 # Tier 2 — Integration: provider upserts, reruns, row counts
207 # ─────────────────────────────────────────────────────────────────────────────
208
209 class TestEntangleIntegration:
210
211 @pytest.mark.asyncio
212 async def test_ET_09_empty_repo_returns_empty(
213 self, db_session: AsyncSession, repo
214 ) -> None:
215 """Provider on a repo with no commits returns empty results."""
216 ref = _cid()
217 result = await _run_provider(db_session, repo.repo_id, ref)
218 assert result == []
219 pairs = await _fetch_pairs(db_session, repo.repo_id)
220 assert pairs == []
221
222 @pytest.mark.asyncio
223 async def test_ET_10_no_history_entries_returns_empty(
224 self, db_session: AsyncSession, repo
225 ) -> None:
226 """Commits exist but no history entries → no pairs."""
227 c1, c2 = _cid(), _cid()
228 await _seed_commit(db_session, repo.repo_id, c1)
229 await _seed_commit(db_session, repo.repo_id, c2, [c1])
230 await db_session.commit()
231 result = await _run_provider(db_session, c2, {})
232 assert result == []
233
234 @pytest.mark.asyncio
235 async def test_ET_11_two_symbols_in_one_commit_no_pair(
236 self, db_session: AsyncSession, repo
237 ) -> None:
238 """Single co-change commit yields co_changes=1 — below MIN_CO_CHANGES=2."""
239 c1 = _cid()
240 await _seed_commit(db_session, repo.repo_id, c1)
241 await _seed_history(db_session, repo.repo_id, c1, [
242 "src/a.py::fn_a", "src/b.py::fn_b",
243 ])
244 await db_session.commit()
245 await _run_provider(db_session, repo.repo_id, c1)
246 pairs = await _fetch_pairs(db_session, repo.repo_id)
247 assert pairs == []
248
249 @pytest.mark.asyncio
250 async def test_ET_12_two_co_changes_produces_one_pair(
251 self, db_session: AsyncSession, repo
252 ) -> None:
253 """Exactly 2 co-change commits → 1 pair at rate 1.0."""
254 c1, c2 = _cid(), _cid()
255 await _seed_commit(db_session, repo.repo_id, c1)
256 await _seed_commit(db_session, repo.repo_id, c2, [c1])
257 for cid in [c1, c2]:
258 await _seed_history(db_session, repo.repo_id, cid, [
259 "src/billing.py::charge",
260 "src/ledger.py::record",
261 ])
262 await db_session.commit()
263 await _run_provider(db_session, repo.repo_id, c2)
264 pairs = await _fetch_pairs(db_session, repo.repo_id)
265 assert len(pairs) == 1
266 p = pairs[0]
267 assert p.co_changes == 2
268 assert p.co_change_rate == 1.0
269
270 @pytest.mark.asyncio
271 async def test_ET_13_import_symbols_excluded(
272 self, db_session: AsyncSession, repo
273 ) -> None:
274 """Import pseudo-symbols are not stored as entangle pairs."""
275 c1, c2 = _cid(), _cid()
276 await _seed_commit(db_session, repo.repo_id, c1)
277 await _seed_commit(db_session, repo.repo_id, c2, [c1])
278 for cid in [c1, c2]:
279 await _seed_history(db_session, repo.repo_id, cid, [
280 "src/a.py::import::os",
281 "src/b.py::import::sys",
282 "src/a.py::real_fn",
283 ])
284 await db_session.commit()
285 await _run_provider(db_session, repo.repo_id, c2)
286 pairs = await _fetch_pairs(db_session, repo.repo_id)
287 for p in pairs:
288 assert "::import::" not in p.symbol_a
289 assert "::import::" not in p.symbol_b
290
291 @pytest.mark.asyncio
292 async def test_ET_14_bare_path_addresses_excluded(
293 self, db_session: AsyncSession, repo
294 ) -> None:
295 """Bare path entries (no '::') are not treated as symbols."""
296 c1, c2 = _cid(), _cid()
297 await _seed_commit(db_session, repo.repo_id, c1)
298 await _seed_commit(db_session, repo.repo_id, c2, [c1])
299 for cid in [c1, c2]:
300 await _seed_history(db_session, repo.repo_id, cid, [
301 "cloudflare",
302 "src/a.py::real_fn",
303 ])
304 await db_session.commit()
305 await _run_provider(db_session, repo.repo_id, c2)
306 pairs = await _fetch_pairs(db_session, repo.repo_id)
307 for p in pairs:
308 assert "::" in p.symbol_a
309 assert "::" in p.symbol_b
310
311 @pytest.mark.asyncio
312 async def test_ET_15_same_file_pair_excluded(
313 self, db_session: AsyncSession, repo
314 ) -> None:
315 """Two symbols from the same file must not produce a pair."""
316 c1, c2 = _cid(), _cid()
317 await _seed_commit(db_session, repo.repo_id, c1)
318 await _seed_commit(db_session, repo.repo_id, c2, [c1])
319 for cid in [c1, c2]:
320 await _seed_history(db_session, repo.repo_id, cid, [
321 "src/billing.py::charge",
322 "src/billing.py::refund",
323 ])
324 await db_session.commit()
325 await _run_provider(db_session, repo.repo_id, c2)
326 pairs = await _fetch_pairs(db_session, repo.repo_id)
327 assert pairs == []
328
329 @pytest.mark.asyncio
330 async def test_ET_16_pair_stored_canonical_a_lt_b(
331 self, db_session: AsyncSession, repo
332 ) -> None:
333 """Stored pair always has symbol_a < symbol_b lexicographically."""
334 c1, c2 = _cid(), _cid()
335 await _seed_commit(db_session, repo.repo_id, c1)
336 await _seed_commit(db_session, repo.repo_id, c2, [c1])
337 for cid in [c1, c2]:
338 await _seed_history(db_session, repo.repo_id, cid, [
339 "src/z.py::zfn",
340 "src/a.py::afn",
341 ])
342 await db_session.commit()
343 await _run_provider(db_session, repo.repo_id, c2)
344 pairs = await _fetch_pairs(db_session, repo.repo_id)
345 assert len(pairs) == 1
346 assert pairs[0].symbol_a <= pairs[0].symbol_b
347
348 @pytest.mark.asyncio
349 async def test_ET_17_file_a_b_populated(
350 self, db_session: AsyncSession, repo
351 ) -> None:
352 """file_a and file_b columns derive from the symbol address."""
353 c1, c2 = _cid(), _cid()
354 await _seed_commit(db_session, repo.repo_id, c1)
355 await _seed_commit(db_session, repo.repo_id, c2, [c1])
356 for cid in [c1, c2]:
357 await _seed_history(db_session, repo.repo_id, cid, [
358 "src/billing.py::charge",
359 "src/ledger.py::record",
360 ])
361 await db_session.commit()
362 await _run_provider(db_session, repo.repo_id, c2)
363 pairs = await _fetch_pairs(db_session, repo.repo_id)
364 assert len(pairs) == 1
365 p = pairs[0]
366 assert p.file_a is not None and "/" in p.file_a
367 assert p.file_b is not None and "/" in p.file_b
368 assert p.file_a != p.file_b
369
370 @pytest.mark.asyncio
371 async def test_ET_18_commits_both_active_is_min(
372 self, db_session: AsyncSession, repo
373 ) -> None:
374 """commits_both_active equals |commits_a ∪ commits_b| (Jaccard union)."""
375 # B appears in 2 commits; A in 4 commits; co_changes = 2
376 # union = 4 + 2 - 2 = 4; rate = 2/4 = 0.5
377 commits = [_cid() for _ in range(4)]
378 prev = None
379 for cid in commits:
380 await _seed_commit(db_session, repo.repo_id, cid, [prev] if prev else [])
381 prev = cid
382 # A in all 4
383 for cid in commits:
384 await _seed_history(db_session, repo.repo_id, cid, ["src/a.py::fn_a"])
385 # B only in first 2
386 for cid in commits[:2]:
387 await _seed_history(db_session, repo.repo_id, cid, ["src/b.py::fn_b"])
388 await db_session.commit()
389 await _run_provider(db_session, repo.repo_id, commits[-1])
390 pairs = await _fetch_pairs(db_session, repo.repo_id)
391 assert len(pairs) == 1
392 assert pairs[0].commits_both_active == 4 # union: 4 + 2 - 2
393 assert pairs[0].co_changes == 2
394 assert pairs[0].co_change_rate == 0.5
395
396
397 # ─────────────────────────────────────────────────────────────────────────────
398 # Tier 3 — E2E: full seeded scenarios
399 # ─────────────────────────────────────────────────────────────────────────────
400
401 class TestEntangleE2E:
402
403 @pytest.mark.asyncio
404 async def test_ET_19_three_symbol_pair_ranking(
405 self, db_session: AsyncSession, repo
406 ) -> None:
407 """Three symbols; AB pairs more than AC; AB ranked first."""
408 commits = [_cid() for _ in range(5)]
409 prev = None
410 for cid in commits:
411 await _seed_commit(db_session, repo.repo_id, cid, [prev] if prev else [])
412 prev = cid
413 # A+B co-change in all 5
414 for cid in commits:
415 await _seed_history(db_session, repo.repo_id, cid, [
416 "src/a.py::fn_a",
417 "src/b.py::fn_b",
418 ])
419 # A+C co-change in only 2
420 for cid in commits[:2]:
421 await _seed_history(db_session, repo.repo_id, cid, ["src/c.py::fn_c"])
422 await db_session.commit()
423 await _run_provider(db_session, repo.repo_id, commits[-1])
424 pairs = await _fetch_pairs(db_session, repo.repo_id)
425 assert len(pairs) == 3
426 # AB at 1.0 should come first (most co_changes)
427 assert pairs[0].co_change_rate == 1.0
428
429 @pytest.mark.asyncio
430 async def test_ET_20_a_in_test_flag_set_for_test_files(
431 self, db_session: AsyncSession, repo
432 ) -> None:
433 """a_in_test / b_in_test flags set when file path contains 'test'."""
434 c1, c2 = _cid(), _cid()
435 await _seed_commit(db_session, repo.repo_id, c1)
436 await _seed_commit(db_session, repo.repo_id, c2, [c1])
437 for cid in [c1, c2]:
438 await _seed_history(db_session, repo.repo_id, cid, [
439 "tests/test_billing.py::test_charge",
440 "src/ledger.py::record",
441 ])
442 await db_session.commit()
443 await _run_provider(db_session, repo.repo_id, c2)
444 pairs = await _fetch_pairs(db_session, repo.repo_id)
445 assert len(pairs) == 1
446 p = pairs[0]
447 # one side is in test, the other is not
448 assert p.a_in_test != p.b_in_test
449
450 @pytest.mark.asyncio
451 async def test_ET_21_result_metadata_keys(
452 self, db_session: AsyncSession, repo
453 ) -> None:
454 """Provider returns (key, payload) tuples with expected metadata keys."""
455 c1, c2 = _cid(), _cid()
456 await _seed_commit(db_session, repo.repo_id, c1)
457 await _seed_commit(db_session, repo.repo_id, c2, [c1])
458 for cid in [c1, c2]:
459 await _seed_history(db_session, repo.repo_id, cid, [
460 "src/a.py::fn_a", "src/b.py::fn_b",
461 ])
462 await db_session.commit()
463 result = await _run_provider(db_session, repo.repo_id, c2)
464 assert len(result) == 1
465 key, payload = result[0]
466 assert key == "intel.code.entangle"
467 assert "count" in payload
468 assert "commits_analysed" in payload
469 assert "truncated" in payload
470
471 @pytest.mark.asyncio
472 async def test_ET_22_ref_stored_on_pair_row(
473 self, db_session: AsyncSession, repo
474 ) -> None:
475 """The ref used for the walk is stored on each pair row."""
476 c1, c2 = _cid(), _cid()
477 await _seed_commit(db_session, repo.repo_id, c1)
478 await _seed_commit(db_session, repo.repo_id, c2, [c1])
479 for cid in [c1, c2]:
480 await _seed_history(db_session, repo.repo_id, cid, [
481 "src/a.py::fn_a", "src/b.py::fn_b",
482 ])
483 await db_session.commit()
484 await _run_provider(db_session, repo.repo_id, c2)
485 pairs = await _fetch_pairs(db_session, repo.repo_id)
486 assert len(pairs) == 1
487 assert pairs[0].ref == c2
488
489 @pytest.mark.asyncio
490 async def test_ET_23_multiple_disconnected_pairs(
491 self, db_session: AsyncSession, repo
492 ) -> None:
493 """Two independent high-rate pairs are both stored correctly."""
494 c1, c2, c3 = _cid(), _cid(), _cid()
495 await _seed_commit(db_session, repo.repo_id, c1)
496 await _seed_commit(db_session, repo.repo_id, c2, [c1])
497 await _seed_commit(db_session, repo.repo_id, c3, [c2])
498 for cid in [c1, c2, c3]:
499 await _seed_history(db_session, repo.repo_id, cid, [
500 "src/alpha.py::a1", "src/beta.py::b1", # pair 1
501 "src/gamma.py::c1", "src/delta.py::d1", # pair 2
502 ])
503 await db_session.commit()
504 await _run_provider(db_session, repo.repo_id, c3)
505 pairs = await _fetch_pairs(db_session, repo.repo_id)
506 # At least 2 cross-file pairs
507 assert len(pairs) >= 2
508
509 @pytest.mark.asyncio
510 async def test_ET_24_structurally_linked_defaults_false(
511 self, db_session: AsyncSession, repo
512 ) -> None:
513 """structurally_linked is always False — not yet implemented."""
514 c1, c2 = _cid(), _cid()
515 await _seed_commit(db_session, repo.repo_id, c1)
516 await _seed_commit(db_session, repo.repo_id, c2, [c1])
517 for cid in [c1, c2]:
518 await _seed_history(db_session, repo.repo_id, cid, [
519 "src/a.py::fn_a", "src/b.py::fn_b",
520 ])
521 await db_session.commit()
522 await _run_provider(db_session, repo.repo_id, c2)
523 pairs = await _fetch_pairs(db_session, repo.repo_id)
524 assert all(p.structurally_linked is False for p in pairs)
525
526 @pytest.mark.asyncio
527 async def test_ET_25_same_file_false_on_stored_pair(
528 self, db_session: AsyncSession, repo
529 ) -> None:
530 """same_file is always False since same-file pairs are excluded."""
531 c1, c2 = _cid(), _cid()
532 await _seed_commit(db_session, repo.repo_id, c1)
533 await _seed_commit(db_session, repo.repo_id, c2, [c1])
534 for cid in [c1, c2]:
535 await _seed_history(db_session, repo.repo_id, cid, [
536 "src/a.py::fn_a", "src/b.py::fn_b",
537 ])
538 await db_session.commit()
539 await _run_provider(db_session, repo.repo_id, c2)
540 pairs = await _fetch_pairs(db_session, repo.repo_id)
541 assert all(p.same_file is False for p in pairs)
542
543
544 # ─────────────────────────────────────────────────────────────────────────────
545 # Tier 4 — Stress: large datasets
546 # ─────────────────────────────────────────────────────────────────────────────
547
548 class TestEntangleStress:
549
550 @pytest.mark.asyncio
551 async def test_ET_26_max_pairs_cap_respected(
552 self, db_session: AsyncSession, repo
553 ) -> None:
554 """Provider stores at most MAX_PAIRS pairs even when more exist."""
555 provider = EntangleProvider()
556 # Build enough distinct cross-file pairs by spreading symbols
557 # across 35 files × 2 symbols = 70 symbols → 70*69/2 ≈ 2415 pairs before filter
558 commits = [_cid() for _ in range(3)]
559 prev = None
560 for cid in commits:
561 await _seed_commit(db_session, repo.repo_id, cid, [prev] if prev else [])
562 prev = cid
563 addrs = [f"src/file_{i}.py::fn_{j}" for i in range(35) for j in range(2)]
564 for cid in commits:
565 await _seed_history(db_session, repo.repo_id, cid, addrs)
566 await db_session.commit()
567 await _run_provider(db_session, repo.repo_id, commits[-1])
568 pairs = await _fetch_pairs(db_session, repo.repo_id)
569 assert len(pairs) <= provider._MAX_PAIRS
570
571 @pytest.mark.asyncio
572 async def test_ET_27_mass_commit_excluded(
573 self, db_session: AsyncSession, repo
574 ) -> None:
575 """Commits touching > MAX_SYMBOLS_PER_COMMIT symbols are skipped."""
576 provider = EntangleProvider()
577 # Seed two legit commits and one mass commit
578 c_legit1, c_legit2, c_mass = _cid(), _cid(), _cid()
579 await _seed_commit(db_session, repo.repo_id, c_legit1)
580 await _seed_commit(db_session, repo.repo_id, c_legit2, [c_legit1])
581 await _seed_commit(db_session, repo.repo_id, c_mass, [c_legit2])
582 # Legit commits: A and B co-change
583 for cid in [c_legit1, c_legit2]:
584 await _seed_history(db_session, repo.repo_id, cid, [
585 "src/a.py::fn_a", "src/b.py::fn_b",
586 ])
587 # Mass commit: 600 symbols
588 big_addrs = [f"src/gen_{i}.py::fn" for i in range(provider._MAX_SYMBOLS_PER_COMMIT + 100)]
589 await _seed_history(db_session, repo.repo_id, c_mass, big_addrs)
590 await db_session.commit()
591 result = await _run_provider(db_session, repo.repo_id, c_mass)
592 # Provider should still return the AB pair from legit commits
593 pairs = await _fetch_pairs(db_session, repo.repo_id)
594 assert any(
595 ("src/a.py::fn_a" in (p.symbol_a, p.symbol_b))
596 for p in pairs
597 )
598
599 @pytest.mark.asyncio
600 async def test_ET_28_500_symbols_completes(
601 self, db_session: AsyncSession, repo
602 ) -> None:
603 """500 symbols across 10 commits completes without error."""
604 commits = [_cid() for _ in range(10)]
605 prev = None
606 for cid in commits:
607 await _seed_commit(db_session, repo.repo_id, cid, [prev] if prev else [])
608 prev = cid
609 # 250 files × 2 symbols = 500 symbols (all under mass-commit limit)
610 addrs = [f"src/f{i}.py::fn_{j}" for i in range(250) for j in range(2)]
611 for cid in commits:
612 await _seed_history(db_session, repo.repo_id, cid, addrs)
613 await db_session.commit()
614 result = await _run_provider(db_session, repo.repo_id, commits[-1])
615 assert result # no exception
616
617 @pytest.mark.asyncio
618 async def test_ET_29_result_count_matches_stored_rows(
619 self, db_session: AsyncSession, repo
620 ) -> None:
621 """metadata 'count' matches the actual number of rows stored."""
622 c1, c2, c3 = _cid(), _cid(), _cid()
623 await _seed_commit(db_session, repo.repo_id, c1)
624 await _seed_commit(db_session, repo.repo_id, c2, [c1])
625 await _seed_commit(db_session, repo.repo_id, c3, [c2])
626 for cid in [c1, c2, c3]:
627 await _seed_history(db_session, repo.repo_id, cid, [
628 "src/a.py::fn_a",
629 "src/b.py::fn_b",
630 "src/c.py::fn_c",
631 ])
632 await db_session.commit()
633 result = await _run_provider(db_session, repo.repo_id, c3)
634 key, payload = result[0]
635 pairs = await _fetch_pairs(db_session, repo.repo_id)
636 assert payload["count"] == len(pairs)
637
638 @pytest.mark.asyncio
639 async def test_ET_30_bfs_walk_cap_limits_commits_analysed(
640 self, db_session: AsyncSession, repo
641 ) -> None:
642 """commits_analysed never exceeds MAX_WALK."""
643 provider = EntangleProvider()
644 cap = provider._MAX_WALK
645 commits = []
646 prev = None
647 for _ in range(min(cap + 5, 50)): # keep it fast; just verify cap exists
648 cid = _cid()
649 await _seed_commit(db_session, repo.repo_id, cid, [prev] if prev else [])
650 commits.append(cid)
651 prev = cid
652 await _seed_history(db_session, repo.repo_id, commits[0], [
653 "src/a.py::fn_a", "src/b.py::fn_b",
654 ])
655 await db_session.commit()
656 result = await _run_provider(db_session, repo.repo_id, commits[-1])
657 if result:
658 key, payload = result[0]
659 assert payload["commits_analysed"] <= cap
660
661
662 # ─────────────────────────────────────────────────────────────────────────────
663 # Tier 5 — State: idempotency, incremental updates, stale-row purge
664 # ─────────────────────────────────────────────────────────────────────────────
665
666 class TestEntangleState:
667
668 @pytest.mark.asyncio
669 async def test_ET_31_idempotent_rerun_same_rows(
670 self, db_session: AsyncSession, repo
671 ) -> None:
672 """Running the provider twice produces the same set of rows."""
673 c1, c2 = _cid(), _cid()
674 await _seed_commit(db_session, repo.repo_id, c1)
675 await _seed_commit(db_session, repo.repo_id, c2, [c1])
676 for cid in [c1, c2]:
677 await _seed_history(db_session, repo.repo_id, cid, [
678 "src/a.py::fn_a", "src/b.py::fn_b",
679 ])
680 await db_session.commit()
681 await _run_provider(db_session, repo.repo_id, c2)
682 first_run = await _fetch_pairs(db_session, repo.repo_id)
683 await _run_provider(db_session, repo.repo_id, c2)
684 second_run = await _fetch_pairs(db_session, repo.repo_id)
685 assert len(first_run) == len(second_run)
686 assert {(p.symbol_a, p.symbol_b) for p in first_run} == {
687 (p.symbol_a, p.symbol_b) for p in second_run
688 }
689
690 @pytest.mark.asyncio
691 async def test_ET_32_stale_rows_purged_on_rerun(
692 self, db_session: AsyncSession, repo
693 ) -> None:
694 """Re-run deletes stale pairs that no longer exist in fresh data."""
695 c1, c2 = _cid(), _cid()
696 await _seed_commit(db_session, repo.repo_id, c1)
697 await _seed_commit(db_session, repo.repo_id, c2, [c1])
698 for cid in [c1, c2]:
699 await _seed_history(db_session, repo.repo_id, cid, [
700 "src/a.py::fn_a", "src/b.py::fn_b",
701 ])
702 await db_session.commit()
703 await _run_provider(db_session, repo.repo_id, c2)
704 first_count_result = await db_session.execute(
705 sa.select(sa.func.count()).select_from(db.MusehubIntelEntangle)
706 .where(db.MusehubIntelEntangle.repo_id == repo.repo_id)
707 )
708 assert first_count_result.scalar_one() == 1
709
710 # Add a new commit that breaks the entangle signal (different symbols)
711 c3 = _cid()
712 await _seed_commit(db_session, repo.repo_id, c3, [c2])
713 await _seed_history(db_session, repo.repo_id, c3, [
714 "src/x.py::fn_x", # completely different
715 ])
716 # Re-run; AB pair should still exist (still valid from c1, c2)
717 await db_session.commit()
718 await _run_provider(db_session, repo.repo_id, c3)
719 second_run = await _fetch_pairs(db_session, repo.repo_id)
720 assert len(second_run) == 1 # AB still valid
721
722 @pytest.mark.asyncio
723 async def test_ET_33_incremental_new_pair_appears(
724 self, db_session: AsyncSession, repo
725 ) -> None:
726 """After adding commits, a new pair materialises on re-run."""
727 c1, c2 = _cid(), _cid()
728 await _seed_commit(db_session, repo.repo_id, c1)
729 await _seed_commit(db_session, repo.repo_id, c2, [c1])
730 for cid in [c1, c2]:
731 await _seed_history(db_session, repo.repo_id, cid, [
732 "src/a.py::fn_a", "src/b.py::fn_b",
733 ])
734 await db_session.commit()
735 await _run_provider(db_session, repo.repo_id, c2)
736 before = await _fetch_pairs(db_session, repo.repo_id)
737
738 # Two new commits introducing a CD pair
739 c3, c4 = _cid(), _cid()
740 await _seed_commit(db_session, repo.repo_id, c3, [c2])
741 await _seed_commit(db_session, repo.repo_id, c4, [c3])
742 for cid in [c3, c4]:
743 await _seed_history(db_session, repo.repo_id, cid, [
744 "src/c.py::fn_c", "src/d.py::fn_d",
745 ])
746 await db_session.commit()
747 await _run_provider(db_session, repo.repo_id, c4)
748 after = await _fetch_pairs(db_session, repo.repo_id)
749 assert len(after) > len(before)
750
751 @pytest.mark.asyncio
752 async def test_ET_34_no_duplicate_pairs(
753 self, db_session: AsyncSession, repo
754 ) -> None:
755 """No (symbol_a, symbol_b) duplicate rows for the same repo."""
756 c1, c2, c3 = _cid(), _cid(), _cid()
757 await _seed_commit(db_session, repo.repo_id, c1)
758 await _seed_commit(db_session, repo.repo_id, c2, [c1])
759 await _seed_commit(db_session, repo.repo_id, c3, [c2])
760 for cid in [c1, c2, c3]:
761 await _seed_history(db_session, repo.repo_id, cid, [
762 "src/a.py::fn_a", "src/b.py::fn_b",
763 ])
764 await db_session.commit()
765 for _ in range(3):
766 await _run_provider(db_session, repo.repo_id, c3)
767 pairs = await _fetch_pairs(db_session, repo.repo_id)
768 keys = [(p.symbol_a, p.symbol_b) for p in pairs]
769 assert len(keys) == len(set(keys))
770
771 @pytest.mark.asyncio
772 async def test_ET_35_rate_updates_on_new_commits(
773 self, db_session: AsyncSession, repo
774 ) -> None:
775 """Rate increases when more co-change commits are added."""
776 # Initial: A in 3 commits, B in 3 commits, co=2 → rate=2/3
777 commits = [_cid() for _ in range(3)]
778 prev = None
779 for cid in commits:
780 await _seed_commit(db_session, repo.repo_id, cid, [prev] if prev else [])
781 prev = cid
782 # A appears in all 3
783 for cid in commits:
784 await _seed_history(db_session, repo.repo_id, cid, ["src/a.py::fn_a"])
785 # B co-changes only in first 2
786 for cid in commits[:2]:
787 await _seed_history(db_session, repo.repo_id, cid, ["src/b.py::fn_b"])
788 await db_session.commit()
789 await _run_provider(db_session, repo.repo_id, commits[-1])
790 pairs_before = await _fetch_pairs(db_session, repo.repo_id)
791 rate_before = pairs_before[0].co_change_rate if pairs_before else 0.0
792
793 # Now add a commit where both co-change again
794 c_new = _cid()
795 await _seed_commit(db_session, repo.repo_id, c_new, [commits[-1]])
796 await _seed_history(db_session, repo.repo_id, c_new, [
797 "src/a.py::fn_a", "src/b.py::fn_b",
798 ])
799 await db_session.commit()
800 await _run_provider(db_session, repo.repo_id, c_new)
801 pairs_after = await _fetch_pairs(db_session, repo.repo_id)
802 rate_after = pairs_after[0].co_change_rate if pairs_after else 0.0
803 assert rate_after >= rate_before
804
805 @pytest.mark.asyncio
806 async def test_ET_36_truncated_flag_true_when_over_cap(
807 self, db_session: AsyncSession, repo
808 ) -> None:
809 """truncated=True when more pairs were found than MAX_PAIRS."""
810 provider = EntangleProvider()
811 commits = [_cid() for _ in range(3)]
812 prev = None
813 for cid in commits:
814 await _seed_commit(db_session, repo.repo_id, cid, [prev] if prev else [])
815 prev = cid
816 # 35 files × 2 syms → ~2415 cross-file pairs, exceeds MAX_PAIRS=500
817 addrs = [f"src/file_{i}.py::fn_{j}" for i in range(35) for j in range(2)]
818 for cid in commits:
819 await _seed_history(db_session, repo.repo_id, cid, addrs)
820 await db_session.commit()
821 result = await _run_provider(db_session, repo.repo_id, commits[-1])
822 key, payload = result[0]
823 assert payload["truncated"] is True
824
825
826 # ─────────────────────────────────────────────────────────────────────────────
827 # Tier 6 — Integrity: edge cases and data quality
828 # ─────────────────────────────────────────────────────────────────────────────
829
830 class TestEntangleIntegrity:
831
832 @pytest.mark.asyncio
833 async def test_ET_37_address_with_only_import_produces_no_pair(
834 self, db_session: AsyncSession, repo
835 ) -> None:
836 """A commit with only import pseudo-symbols generates no pair rows."""
837 c1, c2 = _cid(), _cid()
838 await _seed_commit(db_session, repo.repo_id, c1)
839 await _seed_commit(db_session, repo.repo_id, c2, [c1])
840 for cid in [c1, c2]:
841 await _seed_history(db_session, repo.repo_id, cid, [
842 "src/a.py::import::os",
843 "src/b.py::import::sys",
844 "src/c.py::import::typing",
845 ])
846 await db_session.commit()
847 await _run_provider(db_session, repo.repo_id, c2)
848 pairs = await _fetch_pairs(db_session, repo.repo_id)
849 assert pairs == []
850
851 @pytest.mark.asyncio
852 async def test_ET_38_mixed_valid_and_import_symbols(
853 self, db_session: AsyncSession, repo
854 ) -> None:
855 """Import symbols in same commit as real symbols don't pair with real ones."""
856 c1, c2 = _cid(), _cid()
857 await _seed_commit(db_session, repo.repo_id, c1)
858 await _seed_commit(db_session, repo.repo_id, c2, [c1])
859 for cid in [c1, c2]:
860 await _seed_history(db_session, repo.repo_id, cid, [
861 "src/a.py::real_fn",
862 "src/b.py::import::os", # filtered
863 "src/c.py::other_fn",
864 ])
865 await db_session.commit()
866 await _run_provider(db_session, repo.repo_id, c2)
867 pairs = await _fetch_pairs(db_session, repo.repo_id)
868 for p in pairs:
869 assert "::import::" not in p.symbol_a
870 assert "::import::" not in p.symbol_b
871
872 @pytest.mark.asyncio
873 async def test_ET_39_unknown_ref_in_bfs_returns_empty(
874 self, db_session: AsyncSession, repo
875 ) -> None:
876 """BFS from unknown ref produces no pairs (ref not in commit table)."""
877 unknown_ref = _cid()
878 result = await _run_provider(db_session, repo.repo_id, unknown_ref)
879 assert result == []
880
881 @pytest.mark.asyncio
882 async def test_ET_40_co_changes_count_exact(
883 self, db_session: AsyncSession, repo
884 ) -> None:
885 """co_changes is the exact number of commits where both symbols appeared."""
886 n_together = 4
887 n_solo_a = 2
888 commits_together = [_cid() for _ in range(n_together)]
889 commits_a_only = [_cid() for _ in range(n_solo_a)]
890 all_commits = commits_together + commits_a_only
891 prev = None
892 for cid in all_commits:
893 await _seed_commit(db_session, repo.repo_id, cid, [prev] if prev else [])
894 prev = cid
895 for cid in commits_together:
896 await _seed_history(db_session, repo.repo_id, cid, [
897 "src/a.py::fn_a", "src/b.py::fn_b",
898 ])
899 for cid in commits_a_only:
900 await _seed_history(db_session, repo.repo_id, cid, ["src/a.py::fn_a"])
901 await db_session.commit()
902 await _run_provider(db_session, repo.repo_id, all_commits[-1])
903 pairs = await _fetch_pairs(db_session, repo.repo_id)
904 assert len(pairs) == 1
905 # union = count_a + count_b - co_changes = (n_together + n_solo_a) + n_together - n_together
906 union = n_together + n_solo_a # = 6
907 assert pairs[0].co_changes == n_together
908 assert pairs[0].commits_both_active == union
909 assert pairs[0].co_change_rate == round(n_together / union, 6)
910
911 @pytest.mark.asyncio
912 async def test_ET_41_rate_capped_at_one(
913 self, db_session: AsyncSession, repo
914 ) -> None:
915 """co_change_rate is never > 1.0."""
916 commits = [_cid() for _ in range(5)]
917 prev = None
918 for cid in commits:
919 await _seed_commit(db_session, repo.repo_id, cid, [prev] if prev else [])
920 prev = cid
921 for cid in commits:
922 await _seed_history(db_session, repo.repo_id, cid, [
923 "src/a.py::fn_a", "src/b.py::fn_b",
924 ])
925 await db_session.commit()
926 await _run_provider(db_session, repo.repo_id, commits[-1])
927 pairs = await _fetch_pairs(db_session, repo.repo_id)
928 for p in pairs:
929 assert 0.0 <= p.co_change_rate <= 1.0
930
931
932 # ─────────────────────────────────────────────────────────────────────────────
933 # Tier 7 — Performance: timing bounds
934 # ─────────────────────────────────────────────────────────────────────────────
935
936 class TestEntanglePerformance:
937
938 @pytest.mark.asyncio
939 async def test_ET_42_ten_commits_ten_symbols_under_500ms(
940 self, db_session: AsyncSession, repo
941 ) -> None:
942 """10 commits × 10 symbols completes in under 500 ms."""
943 commits = [_cid() for _ in range(10)]
944 prev = None
945 for cid in commits:
946 await _seed_commit(db_session, repo.repo_id, cid, [prev] if prev else [])
947 prev = cid
948 addrs = [f"src/file_{i}.py::fn" for i in range(10)]
949 for cid in commits:
950 await _seed_history(db_session, repo.repo_id, cid, addrs)
951 await db_session.commit()
952 t0 = time.monotonic()
953 await _run_provider(db_session, repo.repo_id, commits[-1])
954 elapsed = time.monotonic() - t0
955 assert elapsed < 0.5, f"took {elapsed:.3f}s"
956
957 @pytest.mark.asyncio
958 async def test_ET_43_100_commits_20_symbols_under_2s(
959 self, db_session: AsyncSession, repo
960 ) -> None:
961 """100 commits × 20 symbols completes in under 2 s."""
962 commits = [_cid() for _ in range(100)]
963 prev = None
964 for cid in commits:
965 await _seed_commit(db_session, repo.repo_id, cid, [prev] if prev else [])
966 prev = cid
967 addrs = [f"src/f{i}.py::fn" for i in range(20)]
968 for cid in commits:
969 await _seed_history(db_session, repo.repo_id, cid, addrs)
970 await db_session.commit()
971 t0 = time.monotonic()
972 await _run_provider(db_session, repo.repo_id, commits[-1])
973 elapsed = time.monotonic() - t0
974 assert elapsed < 2.0, f"took {elapsed:.3f}s"
975
976 @pytest.mark.asyncio
977 async def test_ET_44_empty_repo_under_50ms(
978 self, db_session: AsyncSession, repo
979 ) -> None:
980 """Empty repo fast-path exits under 50 ms."""
981 t0 = time.monotonic()
982 await _run_provider(db_session, repo.repo_id, _cid())
983 elapsed = time.monotonic() - t0
984 assert elapsed < 0.05, f"took {elapsed:.3f}s"
985
986 @pytest.mark.asyncio
987 async def test_ET_45_rerun_same_speed_as_first(
988 self, db_session: AsyncSession, repo
989 ) -> None:
990 """Second run is not significantly slower than first run."""
991 c1, c2 = _cid(), _cid()
992 await _seed_commit(db_session, repo.repo_id, c1)
993 await _seed_commit(db_session, repo.repo_id, c2, [c1])
994 for cid in [c1, c2]:
995 await _seed_history(db_session, repo.repo_id, cid, [
996 "src/a.py::fn_a", "src/b.py::fn_b",
997 ])
998 await db_session.commit()
999 t1 = time.monotonic()
1000 await _run_provider(db_session, repo.repo_id, c2)
1001 d1 = time.monotonic() - t1
1002 t2 = time.monotonic()
1003 await _run_provider(db_session, repo.repo_id, c2)
1004 d2 = time.monotonic() - t2
1005 # second run should not be more than 5× slower
1006 assert d2 < max(d1 * 5, 0.5)
1007
1008 @pytest.mark.asyncio
1009 async def test_ET_46_point_lookup_fast(
1010 self, db_session: AsyncSession, repo
1011 ) -> None:
1012 """Fetching pairs for a specific repo is sub-10 ms after provider run."""
1013 c1, c2 = _cid(), _cid()
1014 await _seed_commit(db_session, repo.repo_id, c1)
1015 await _seed_commit(db_session, repo.repo_id, c2, [c1])
1016 for cid in [c1, c2]:
1017 await _seed_history(db_session, repo.repo_id, cid, [
1018 "src/a.py::fn_a", "src/b.py::fn_b",
1019 ])
1020 await db_session.commit()
1021 await _run_provider(db_session, repo.repo_id, c2)
1022 t0 = time.monotonic()
1023 await _fetch_pairs(db_session, repo.repo_id)
1024 elapsed = time.monotonic() - t0
1025 assert elapsed < 0.01, f"took {elapsed:.3f}s"
1026
1027
1028 # ─────────────────────────────────────────────────────────────────────────────
1029 # Tier 8 — Security: injection, isolation, address length
1030 # ─────────────────────────────────────────────────────────────────────────────
1031
1032 class TestEntangleSecurity:
1033
1034 @pytest.mark.asyncio
1035 async def test_ET_47_sql_injection_in_address_stored_verbatim(
1036 self, db_session: AsyncSession, repo
1037 ) -> None:
1038 """SQL injection strings in symbol addresses are stored as-is (no execution)."""
1039 inject = "src/a.py::fn'; DROP TABLE musehub_intel_entangle; --"
1040 c1, c2 = _cid(), _cid()
1041 await _seed_commit(db_session, repo.repo_id, c1)
1042 await _seed_commit(db_session, repo.repo_id, c2, [c1])
1043 for cid in [c1, c2]:
1044 await _seed_history(db_session, repo.repo_id, cid, [
1045 inject,
1046 "src/b.py::fn_b",
1047 ])
1048 await db_session.commit()
1049 await _run_provider(db_session, repo.repo_id, c2)
1050 # Table must still exist
1051 pairs = await _fetch_pairs(db_session, repo.repo_id)
1052 # The injection address should appear verbatim or be stored without issue
1053 assert isinstance(pairs, list)
1054
1055 @pytest.mark.asyncio
1056 async def test_ET_48_xss_payload_in_address_stored_safely(
1057 self, db_session: AsyncSession, repo
1058 ) -> None:
1059 """XSS payloads in addresses are stored without execution."""
1060 xss = "src/<script>alert(1)</script>.py::fn"
1061 c1, c2 = _cid(), _cid()
1062 await _seed_commit(db_session, repo.repo_id, c1)
1063 await _seed_commit(db_session, repo.repo_id, c2, [c1])
1064 for cid in [c1, c2]:
1065 await _seed_history(db_session, repo.repo_id, cid, [
1066 xss,
1067 "src/b.py::fn_b",
1068 ])
1069 await db_session.commit()
1070 await _run_provider(db_session, repo.repo_id, c2)
1071 pairs = await _fetch_pairs(db_session, repo.repo_id)
1072 assert isinstance(pairs, list)
1073
1074 @pytest.mark.asyncio
1075 async def test_ET_49_repo_isolation_strict(
1076 self, db_session: AsyncSession, two_repos
1077 ) -> None:
1078 """Pairs from repo A are never visible when querying repo B."""
1079 r1, r2 = two_repos
1080 c1, c2 = _cid(), _cid()
1081 await _seed_commit(db_session, r1.repo_id, c1)
1082 await _seed_commit(db_session, r1.repo_id, c2, [c1])
1083 for cid in [c1, c2]:
1084 await _seed_history(db_session, r1.repo_id, cid, [
1085 "src/a.py::fn_a", "src/b.py::fn_b",
1086 ])
1087 await db_session.commit()
1088 await _run_provider(db_session, r1.repo_id, c2)
1089 # Repo 2 has no data
1090 pairs_r2 = await _fetch_pairs(db_session, r2.repo_id)
1091 assert pairs_r2 == []
1092
1093 @pytest.mark.asyncio
1094 async def test_ET_50_repo_isolation_no_cross_contamination(
1095 self, db_session: AsyncSession, two_repos
1096 ) -> None:
1097 """Two repos each get their own independent pair sets."""
1098 r1, r2 = two_repos
1099 for repo in [r1, r2]:
1100 c1, c2 = _cid(), _cid()
1101 await _seed_commit(db_session, repo.repo_id, c1)
1102 await _seed_commit(db_session, repo.repo_id, c2, [c1])
1103 for cid in [c1, c2]:
1104 await _seed_history(db_session, repo.repo_id, cid, [
1105 "src/a.py::fn_a", "src/b.py::fn_b",
1106 ])
1107 await db_session.commit()
1108 await _run_provider(db_session, repo.repo_id, c2)
1109 pairs_r1 = await _fetch_pairs(db_session, r1.repo_id)
1110 pairs_r2 = await _fetch_pairs(db_session, r2.repo_id)
1111 assert len(pairs_r1) == 1
1112 assert len(pairs_r2) == 1
1113 assert pairs_r1[0].repo_id == r1.repo_id
1114 assert pairs_r2[0].repo_id == r2.repo_id
1115
1116 @pytest.mark.asyncio
1117 async def test_ET_51_delete_old_provider_run_on_rerun(
1118 self, db_session: AsyncSession, repo
1119 ) -> None:
1120 """Rerun for a different ref purges all previous rows for the repo."""
1121 c1, c2, c3 = _cid(), _cid(), _cid()
1122 await _seed_commit(db_session, repo.repo_id, c1)
1123 await _seed_commit(db_session, repo.repo_id, c2, [c1])
1124 await _seed_commit(db_session, repo.repo_id, c3, [c2])
1125 for cid in [c1, c2, c3]:
1126 await _seed_history(db_session, repo.repo_id, cid, [
1127 "src/a.py::fn_a", "src/b.py::fn_b",
1128 ])
1129 await db_session.commit()
1130 await _run_provider(db_session, repo.repo_id, c2)
1131 await _run_provider(db_session, repo.repo_id, c3)
1132 pairs = await _fetch_pairs(db_session, repo.repo_id)
1133 # All stored rows must point to the latest ref
1134 for p in pairs:
1135 assert p.ref == c3
1136
1137 @pytest.mark.asyncio
1138 async def test_ET_52_unicode_in_address_handled(
1139 self, db_session: AsyncSession, repo
1140 ) -> None:
1141 """Unicode characters in addresses do not crash the provider."""
1142 c1, c2 = _cid(), _cid()
1143 await _seed_commit(db_session, repo.repo_id, c1)
1144 await _seed_commit(db_session, repo.repo_id, c2, [c1])
1145 for cid in [c1, c2]:
1146 await _seed_history(db_session, repo.repo_id, cid, [
1147 "src/música.py::canción",
1148 "src/b.py::fn_b",
1149 ])
1150 await db_session.commit()
1151 await _run_provider(db_session, repo.repo_id, c2)
1152 pairs = await _fetch_pairs(db_session, repo.repo_id)
1153 assert isinstance(pairs, list)
1154
1155 @pytest.mark.asyncio
1156 async def test_ET_53_long_address_does_not_exceed_column_width(
1157 self, db_session: AsyncSession, repo
1158 ) -> None:
1159 """Addresses truncated to 512 chars by the route layer don't crash storage."""
1160 long_addr_a = "src/" + "a" * 500 + ".py::fn"
1161 long_addr_b = "src/" + "b" * 500 + ".py::fn"
1162 # These exceed 512 chars — simulate what the route-layer would see
1163 # The provider itself stores verbatim; the model column is VARCHAR(512)
1164 # so the DB will reject anything longer. Just verify the provider
1165 # doesn't crash on realistic (under 512) addresses.
1166 addr_a = f"{long_addr_a[:100]}::fn_a"
1167 addr_b = f"{long_addr_b[:100]}::fn_b"
1168 c1, c2 = _cid(), _cid()
1169 await _seed_commit(db_session, repo.repo_id, c1)
1170 await _seed_commit(db_session, repo.repo_id, c2, [c1])
1171 for cid in [c1, c2]:
1172 await _seed_history(db_session, repo.repo_id, cid, [addr_a, addr_b])
1173 await db_session.commit()
1174 await _run_provider(db_session, repo.repo_id, c2)
1175 pairs = await _fetch_pairs(db_session, repo.repo_id)
1176 assert len(pairs) == 1
1177
1178 @pytest.mark.asyncio
1179 async def test_ET_54_newline_in_address_stored_verbatim(
1180 self, db_session: AsyncSession, repo
1181 ) -> None:
1182 """Newline characters in addresses don't trigger injections or errors."""
1183 addr_a = "src/a.py::fn\n_a"
1184 addr_b = "src/b.py::fn_b"
1185 c1, c2 = _cid(), _cid()
1186 await _seed_commit(db_session, repo.repo_id, c1)
1187 await _seed_commit(db_session, repo.repo_id, c2, [c1])
1188 for cid in [c1, c2]:
1189 await _seed_history(db_session, repo.repo_id, cid, [addr_a, addr_b])
1190 await db_session.commit()
1191 await _run_provider(db_session, repo.repo_id, c2)
1192 pairs = await _fetch_pairs(db_session, repo.repo_id)
1193 assert isinstance(pairs, list)
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago