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