gabriel / musehub public
test_phase1_stable_provider.py python
708 lines 27.5 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 123 days ago
1 """TDD spec for Phase 1 — StableProvider migration + pure SQL rewrite (issue #12).
2
3 Verifies that ``StableProvider`` derives stability records entirely from
4 ``musehub_symbol_intel`` without any subprocess calls, and that the new
5 ``last_changed_commit`` column is populated correctly.
6
7 Seven test tiers
8 ----------------
9 Unit P1_01 – P1_06 _days_stable_from_dt() helper
10 Integration P1_07 – P1_14 Provider upserts, filtering, reruns
11 E2E P1_15 – P1_18 Seed symbol_intel → run provider → verify DB
12 Stress P1_19 – P1_21 500-row batch, idempotency
13 Data Integrity P1_22 – P1_24 NULL exclusion, kind filter, uniqueness
14 Performance P1_25 – P1_26 Batch timing bounds
15 Security P1_27 – P1_28 Injection verbatim storage, repo isolation
16 """
17 from __future__ import annotations
18
19 import secrets
20 import time
21 from datetime import datetime, timedelta, timezone
22
23 import pytest
24 import pytest_asyncio
25 from sqlalchemy.dialects.postgresql import insert as pg_insert
26 from sqlalchemy.ext.asyncio import AsyncSession
27
28 from muse.core.types import fake_id, long_id
29 from musehub.db import musehub_models as db
30 from musehub.services.musehub_intel_providers import StableProvider, _days_stable_from_dt
31 from tests.factories import create_repo
32
33
34 # ---------------------------------------------------------------------------
35 # Helpers
36 # ---------------------------------------------------------------------------
37
38 def _uid() -> str:
39 return fake_id(secrets.token_hex(16))
40
41
42 _OWNER = "testuser"
43 _SLUG = "stableprovider"
44 _REF = long_id("a" * 64)
45 _REF2 = long_id("b" * 64)
46
47
48 async def _seed_symbol(
49 session: AsyncSession,
50 repo_id: str,
51 *,
52 address: str,
53 churn: int = 0,
54 churn_30d: int = 0,
55 churn_90d: int = 0,
56 last_changed: datetime | None = None,
57 last_commit_id: str | None = None,
58 symbol_kind: str = "function",
59 ) -> None:
60 """Insert or upsert a ``musehub_symbol_intel`` row for test fixtures.
61
62 Parameters
63 ----------
64 session: Active async SQLAlchemy session.
65 repo_id: Target repository ID.
66 address: Symbol address (``file.py::fn``).
67 churn: Lifetime change count (0 → since_start eligible).
68 churn_30d: Changes in last 30 days (0 → stable candidate).
69 churn_90d: Changes in last 90 days (0 → stable candidate).
70 last_changed: UTC datetime of last modification; None excludes from stable.
71 last_commit_id: Commit ID of last modification; stored as last_changed_commit.
72 symbol_kind: Symbol kind string (function / class / etc.).
73 """
74 stmt = (
75 pg_insert(db.MusehubSymbolIntel)
76 .values(
77 repo_id=repo_id,
78 address=address,
79 symbol_kind=symbol_kind,
80 churn=churn,
81 churn_30d=churn_30d,
82 churn_90d=churn_90d,
83 blast=0,
84 blast_direct=0,
85 blast_cross=0,
86 last_changed=last_changed,
87 last_commit_id=last_commit_id,
88 author_count=1,
89 gravity=0.0,
90 weekly=[0] * 12,
91 blast_top=[],
92 )
93 .on_conflict_do_update(
94 index_elements=["repo_id", "address"],
95 set_={
96 "churn": churn,
97 "churn_30d": churn_30d,
98 "churn_90d": churn_90d,
99 "last_changed": last_changed,
100 "last_commit_id": last_commit_id,
101 },
102 )
103 )
104 await session.execute(stmt)
105 await session.flush()
106
107
108 # ---------------------------------------------------------------------------
109 # Fixtures
110 # ---------------------------------------------------------------------------
111
112 @pytest_asyncio.fixture
113 async def stable_repo(db_session: AsyncSession):
114 """Bare repo — no symbol_intel rows seeded."""
115 return await create_repo(db_session, owner=_OWNER, slug=_SLUG)
116
117
118 @pytest_asyncio.fixture
119 async def stable_repo_with_symbols(db_session: AsyncSession, stable_repo):
120 """Repo seeded with a mix of stable and unstable symbols."""
121 repo_id = stable_repo.repo_id
122 now = datetime.now(timezone.utc)
123 await db_session.commit()
124
125 # stable — untouched for 180 days, churn_30d=0, churn_90d=0
126 await _seed_symbol(
127 db_session, repo_id,
128 address="pkg/core.py::parse_frame",
129 churn=3, churn_30d=0, churn_90d=0,
130 last_changed=now - timedelta(days=180),
131 last_commit_id=_REF,
132 )
133 # eternal — never modified
134 await _seed_symbol(
135 db_session, repo_id,
136 address="pkg/codec.py::pack",
137 churn=0, churn_30d=0, churn_90d=0,
138 last_changed=now - timedelta(days=365),
139 last_commit_id=None,
140 )
141 # unstable — active in last 30 days
142 await _seed_symbol(
143 db_session, repo_id,
144 address="pkg/api.py::handler",
145 churn=12, churn_30d=4, churn_90d=4,
146 last_changed=now - timedelta(days=10),
147 last_commit_id=_REF,
148 )
149 # no last_changed — should be excluded
150 await _seed_symbol(
151 db_session, repo_id,
152 address="pkg/init.py::bootstrap",
153 churn=0, churn_30d=0, churn_90d=0,
154 last_changed=None,
155 last_commit_id=None,
156 )
157 await db_session.commit()
158 return stable_repo
159
160
161 # ---------------------------------------------------------------------------
162 # Tier 1 — Unit: _days_stable_from_dt()
163 # ---------------------------------------------------------------------------
164
165 class TestDaysStableHelper:
166 """Unit tests for the ``_days_stable_from_dt`` pure helper function."""
167
168 def test_P1_01_none_returns_zero(self) -> None:
169 """None input → 0 (no last_changed means no stability data)."""
170 assert _days_stable_from_dt(None) == 0
171
172 def test_P1_02_exactly_90_days_ago(self) -> None:
173 """Datetime 90 days ago → 90."""
174 dt = datetime.now(timezone.utc) - timedelta(days=90)
175 assert _days_stable_from_dt(dt) == 90
176
177 def test_P1_03_future_timestamp_clamped_to_zero(self) -> None:
178 """Future timestamp → 0, never negative."""
179 dt = datetime.now(timezone.utc) + timedelta(days=30)
180 assert _days_stable_from_dt(dt) == 0
181
182 def test_P1_04_epoch_returns_large_positive(self) -> None:
183 """Unix epoch → thousands of days (sanity check for ancient timestamps)."""
184 epoch = datetime(1970, 1, 1, tzinfo=timezone.utc)
185 assert _days_stable_from_dt(epoch) > 10_000
186
187 def test_P1_05_naive_datetime_treated_as_utc(self) -> None:
188 """Timezone-naive datetime treated as UTC — no TypeError raised."""
189 naive = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(days=45)
190 assert _days_stable_from_dt(naive) == 45
191
192 def test_P1_06_one_day_ago_returns_one(self) -> None:
193 """One day ago → 1."""
194 dt = datetime.now(timezone.utc) - timedelta(days=1, seconds=1)
195 assert _days_stable_from_dt(dt) == 1
196
197
198 # ---------------------------------------------------------------------------
199 # Tier 2 — Integration: provider upserts and filtering
200 # ---------------------------------------------------------------------------
201
202 class TestStableProviderIntegration:
203 """Integration tests — provider run against a real async DB session."""
204
205 @pytest.mark.asyncio
206 async def test_P1_07_stable_symbol_upserted(
207 self, db_session: AsyncSession, stable_repo_with_symbols
208 ) -> None:
209 """Stable symbol with churn_30d=0, churn_90d=0 → row written to intel_stable."""
210 repo_id = stable_repo_with_symbols.repo_id
211 await StableProvider().compute(db_session, repo_id, _REF, {})
212 await db_session.flush()
213 from sqlalchemy import select
214 row = (await db_session.execute(
215 select(db.MusehubIntelStable).where(
216 db.MusehubIntelStable.repo_id == repo_id,
217 db.MusehubIntelStable.address == "pkg/core.py::parse_frame",
218 )
219 )).scalar_one_or_none()
220 assert row is not None
221
222 @pytest.mark.asyncio
223 async def test_P1_08_days_stable_value_correct(
224 self, db_session: AsyncSession, stable_repo_with_symbols
225 ) -> None:
226 """days_stable ≈ 180 for symbol last changed 180 days ago."""
227 repo_id = stable_repo_with_symbols.repo_id
228 await StableProvider().compute(db_session, repo_id, _REF, {})
229 await db_session.flush()
230 from sqlalchemy import select
231 row = (await db_session.execute(
232 select(db.MusehubIntelStable).where(
233 db.MusehubIntelStable.repo_id == repo_id,
234 db.MusehubIntelStable.address == "pkg/core.py::parse_frame",
235 )
236 )).scalar_one()
237 assert 178 <= row.days_stable <= 182
238
239 @pytest.mark.asyncio
240 async def test_P1_09_since_start_true_for_zero_lifetime_churn(
241 self, db_session: AsyncSession, stable_repo_with_symbols
242 ) -> None:
243 """Symbol with churn=0 → since_start=True."""
244 repo_id = stable_repo_with_symbols.repo_id
245 await StableProvider().compute(db_session, repo_id, _REF, {})
246 await db_session.flush()
247 from sqlalchemy import select
248 row = (await db_session.execute(
249 select(db.MusehubIntelStable).where(
250 db.MusehubIntelStable.repo_id == repo_id,
251 db.MusehubIntelStable.address == "pkg/codec.py::pack",
252 )
253 )).scalar_one()
254 assert row.since_start is True
255
256 @pytest.mark.asyncio
257 async def test_P1_10_since_start_false_for_nonzero_lifetime_churn(
258 self, db_session: AsyncSession, stable_repo_with_symbols
259 ) -> None:
260 """Symbol with churn=3 (but churn_30d=0) → since_start=False."""
261 repo_id = stable_repo_with_symbols.repo_id
262 await StableProvider().compute(db_session, repo_id, _REF, {})
263 await db_session.flush()
264 from sqlalchemy import select
265 row = (await db_session.execute(
266 select(db.MusehubIntelStable).where(
267 db.MusehubIntelStable.repo_id == repo_id,
268 db.MusehubIntelStable.address == "pkg/core.py::parse_frame",
269 )
270 )).scalar_one()
271 assert row.since_start is False
272
273 @pytest.mark.asyncio
274 async def test_P1_11_last_changed_commit_populated(
275 self, db_session: AsyncSession, stable_repo_with_symbols
276 ) -> None:
277 """last_changed_commit carries the last_commit_id from symbol_intel."""
278 repo_id = stable_repo_with_symbols.repo_id
279 await StableProvider().compute(db_session, repo_id, _REF, {})
280 await db_session.flush()
281 from sqlalchemy import select
282 row = (await db_session.execute(
283 select(db.MusehubIntelStable).where(
284 db.MusehubIntelStable.repo_id == repo_id,
285 db.MusehubIntelStable.address == "pkg/core.py::parse_frame",
286 )
287 )).scalar_one()
288 assert row.last_changed_commit == _REF
289
290 @pytest.mark.asyncio
291 async def test_P1_12_nonzero_churn_30d_excluded(
292 self, db_session: AsyncSession, stable_repo_with_symbols
293 ) -> None:
294 """Symbol with churn_30d=4 must NOT appear in intel_stable."""
295 repo_id = stable_repo_with_symbols.repo_id
296 await StableProvider().compute(db_session, repo_id, _REF, {})
297 await db_session.flush()
298 from sqlalchemy import select
299 row = (await db_session.execute(
300 select(db.MusehubIntelStable).where(
301 db.MusehubIntelStable.repo_id == repo_id,
302 db.MusehubIntelStable.address == "pkg/api.py::handler",
303 )
304 )).scalar_one_or_none()
305 assert row is None
306
307 @pytest.mark.asyncio
308 async def test_P1_13_rerun_updates_days_stable_in_place(
309 self, db_session: AsyncSession, stable_repo_with_symbols
310 ) -> None:
311 """Second provider run updates existing row — no duplicate."""
312 repo_id = stable_repo_with_symbols.repo_id
313 await StableProvider().compute(db_session, repo_id, _REF, {})
314 await db_session.flush()
315 await StableProvider().compute(db_session, repo_id, _REF2, {})
316 await db_session.flush()
317 from sqlalchemy import select, func
318 count = (await db_session.execute(
319 select(func.count()).where(
320 db.MusehubIntelStable.repo_id == repo_id,
321 db.MusehubIntelStable.address == "pkg/core.py::parse_frame",
322 )
323 )).scalar_one()
324 assert count == 1
325
326 @pytest.mark.asyncio
327 async def test_P1_14_ref_column_updated_on_rerun(
328 self, db_session: AsyncSession, stable_repo_with_symbols
329 ) -> None:
330 """Second run with a different ref → ref column reflects the new value."""
331 repo_id = stable_repo_with_symbols.repo_id
332 await StableProvider().compute(db_session, repo_id, _REF, {})
333 await db_session.flush()
334 db_session.expire_all()
335 await StableProvider().compute(db_session, repo_id, _REF2, {})
336 await db_session.flush()
337 from sqlalchemy import select
338 row = (await db_session.execute(
339 select(db.MusehubIntelStable).where(
340 db.MusehubIntelStable.repo_id == repo_id,
341 db.MusehubIntelStable.address == "pkg/core.py::parse_frame",
342 ).execution_options(populate_existing=True)
343 )).scalar_one()
344 assert row.ref == _REF2
345
346
347 # ---------------------------------------------------------------------------
348 # Tier 3 — E2E: seed → provider → verify DB shape
349 # ---------------------------------------------------------------------------
350
351 class TestStableProviderE2E:
352 """End-to-end tests — full seed-to-DB round-trip."""
353
354 @pytest.mark.asyncio
355 async def test_P1_15_row_count_positive(
356 self, db_session: AsyncSession, stable_repo_with_symbols
357 ) -> None:
358 """At least one row written after running provider on seeded data."""
359 repo_id = stable_repo_with_symbols.repo_id
360 results = await StableProvider().compute(db_session, repo_id, _REF, {})
361 await db_session.flush()
362 count = results[0][1]["count"] if results else 0
363 assert count > 0
364
365 @pytest.mark.asyncio
366 async def test_P1_16_days_stable_positive(
367 self, db_session: AsyncSession, stable_repo_with_symbols
368 ) -> None:
369 """All written rows have days_stable > 0."""
370 from sqlalchemy import select
371 repo_id = stable_repo_with_symbols.repo_id
372 await StableProvider().compute(db_session, repo_id, _REF, {})
373 await db_session.flush()
374 rows = (await db_session.execute(
375 select(db.MusehubIntelStable).where(
376 db.MusehubIntelStable.repo_id == repo_id
377 )
378 )).scalars().all()
379 assert all(r.days_stable > 0 for r in rows)
380
381 @pytest.mark.asyncio
382 async def test_P1_17_last_changed_commit_is_sha256_prefixed_or_none(
383 self, db_session: AsyncSession, stable_repo_with_symbols
384 ) -> None:
385 """last_changed_commit is either None or starts with 'sha256:'."""
386 from sqlalchemy import select
387 repo_id = stable_repo_with_symbols.repo_id
388 await StableProvider().compute(db_session, repo_id, _REF, {})
389 await db_session.flush()
390 rows = (await db_session.execute(
391 select(db.MusehubIntelStable).where(
392 db.MusehubIntelStable.repo_id == repo_id
393 )
394 )).scalars().all()
395 for row in rows:
396 assert row.last_changed_commit is None or row.last_changed_commit.startswith("sha256:")
397
398 @pytest.mark.asyncio
399 async def test_P1_18_since_start_only_when_lifetime_churn_zero(
400 self, db_session: AsyncSession, stable_repo_with_symbols
401 ) -> None:
402 """since_start=True only for symbols whose lifetime churn is 0."""
403 from sqlalchemy import select
404 repo_id = stable_repo_with_symbols.repo_id
405 await StableProvider().compute(db_session, repo_id, _REF, {})
406 await db_session.flush()
407 rows = (await db_session.execute(
408 select(db.MusehubIntelStable).where(
409 db.MusehubIntelStable.repo_id == repo_id,
410 db.MusehubIntelStable.since_start == True, # noqa: E712
411 )
412 )).scalars().all()
413 # Only pkg/codec.py::pack has churn=0
414 addresses = {r.address for r in rows}
415 assert "pkg/api.py::handler" not in addresses
416 assert "pkg/core.py::parse_frame" not in addresses
417
418
419 # ---------------------------------------------------------------------------
420 # Tier 4 — Stress: large batch, idempotency
421 # ---------------------------------------------------------------------------
422
423 class TestStableProviderStress:
424 """Stress tests — large symbol counts and repeated runs."""
425
426 @pytest.mark.asyncio
427 async def test_P1_19_500_symbols_all_upserted(
428 self, db_session: AsyncSession, stable_repo
429 ) -> None:
430 """500 qualifying symbols all land in intel_stable after one run."""
431 from sqlalchemy import select, func
432 repo_id = stable_repo.repo_id
433 now = datetime.now(timezone.utc)
434 await db_session.commit()
435 for i in range(500):
436 await _seed_symbol(
437 db_session, repo_id,
438 address=f"pkg/mod{i}.py::fn_{i}",
439 churn=0, churn_30d=0, churn_90d=0,
440 last_changed=now - timedelta(days=100 + i),
441 last_commit_id=_REF,
442 )
443 await db_session.commit()
444 await StableProvider().compute(db_session, repo_id, _REF, {})
445 await db_session.flush()
446 count = (await db_session.execute(
447 select(func.count()).where(db.MusehubIntelStable.repo_id == repo_id)
448 )).scalar_one()
449 assert count == 500
450
451 @pytest.mark.asyncio
452 async def test_P1_20_no_duplicates_after_single_run(
453 self, db_session: AsyncSession, stable_repo
454 ) -> None:
455 """No duplicate (repo_id, address) pairs after a single run."""
456 from sqlalchemy import select, func
457 repo_id = stable_repo.repo_id
458 now = datetime.now(timezone.utc)
459 await db_session.commit()
460 for i in range(50):
461 await _seed_symbol(
462 db_session, repo_id,
463 address=f"pkg/dup{i}.py::fn",
464 churn=0, churn_30d=0, churn_90d=0,
465 last_changed=now - timedelta(days=200),
466 last_commit_id=_REF,
467 )
468 await db_session.commit()
469 await StableProvider().compute(db_session, repo_id, _REF, {})
470 await db_session.flush()
471 total = (await db_session.execute(
472 select(func.count()).where(db.MusehubIntelStable.repo_id == repo_id)
473 )).scalar_one()
474 assert total == 50
475
476 @pytest.mark.asyncio
477 async def test_P1_21_upsert_is_idempotent(
478 self, db_session: AsyncSession, stable_repo
479 ) -> None:
480 """Running the provider twice produces the same row count as running once."""
481 from sqlalchemy import select, func
482 repo_id = stable_repo.repo_id
483 now = datetime.now(timezone.utc)
484 await db_session.commit()
485 for i in range(20):
486 await _seed_symbol(
487 db_session, repo_id,
488 address=f"pkg/idem{i}.py::fn",
489 churn=0, churn_30d=0, churn_90d=0,
490 last_changed=now - timedelta(days=150),
491 last_commit_id=_REF,
492 )
493 await db_session.commit()
494 await StableProvider().compute(db_session, repo_id, _REF, {})
495 await db_session.flush()
496 await StableProvider().compute(db_session, repo_id, _REF2, {})
497 await db_session.flush()
498 count = (await db_session.execute(
499 select(func.count()).where(db.MusehubIntelStable.repo_id == repo_id)
500 )).scalar_one()
501 assert count == 20
502
503
504 # ---------------------------------------------------------------------------
505 # Tier 5 — Data Integrity
506 # ---------------------------------------------------------------------------
507
508 class TestStableProviderDataIntegrity:
509 """Data integrity tests — exclusion rules and uniqueness guarantees."""
510
511 @pytest.mark.asyncio
512 async def test_P1_22_null_last_changed_excluded(
513 self, db_session: AsyncSession, stable_repo
514 ) -> None:
515 """Symbols with last_changed=NULL are not written to intel_stable."""
516 from sqlalchemy import select, func
517 repo_id = stable_repo.repo_id
518 await db_session.commit()
519 await _seed_symbol(
520 db_session, repo_id,
521 address="pkg/null.py::fn",
522 churn=0, churn_30d=0, churn_90d=0,
523 last_changed=None,
524 last_commit_id=None,
525 )
526 await db_session.commit()
527 await StableProvider().compute(db_session, repo_id, _REF, {})
528 await db_session.flush()
529 count = (await db_session.execute(
530 select(func.count()).where(db.MusehubIntelStable.repo_id == repo_id)
531 )).scalar_one()
532 assert count == 0
533
534 @pytest.mark.asyncio
535 async def test_P1_23_nonzero_churn_90d_excluded(
536 self, db_session: AsyncSession, stable_repo
537 ) -> None:
538 """Symbol with churn_90d > 0 is excluded even if churn_30d = 0."""
539 from sqlalchemy import select, func
540 repo_id = stable_repo.repo_id
541 now = datetime.now(timezone.utc)
542 await db_session.commit()
543 await _seed_symbol(
544 db_session, repo_id,
545 address="pkg/slow.py::fn",
546 churn=5, churn_30d=0, churn_90d=2,
547 last_changed=now - timedelta(days=45),
548 last_commit_id=_REF,
549 )
550 await db_session.commit()
551 await StableProvider().compute(db_session, repo_id, _REF, {})
552 await db_session.flush()
553 count = (await db_session.execute(
554 select(func.count()).where(db.MusehubIntelStable.repo_id == repo_id)
555 )).scalar_one()
556 assert count == 0
557
558 @pytest.mark.asyncio
559 async def test_P1_24_address_unique_per_repo(
560 self, db_session: AsyncSession, stable_repo
561 ) -> None:
562 """(repo_id, address) primary key — two repos can share the same address."""
563 from sqlalchemy import select, func
564 repo_id = stable_repo.repo_id
565 repo2 = await create_repo(db_session, owner=_OWNER, slug="stableprovider2")
566 repo_id2 = repo2.repo_id
567 now = datetime.now(timezone.utc)
568 await db_session.commit()
569 for rid in (repo_id, repo_id2):
570 await _seed_symbol(
571 db_session, rid,
572 address="shared/utils.py::parse",
573 churn=0, churn_30d=0, churn_90d=0,
574 last_changed=now - timedelta(days=200),
575 last_commit_id=_REF,
576 )
577 await db_session.commit()
578 await StableProvider().compute(db_session, repo_id, _REF, {})
579 await StableProvider().compute(db_session, repo_id2, _REF, {})
580 await db_session.flush()
581 count = (await db_session.execute(
582 select(func.count()).where(
583 db.MusehubIntelStable.address == "shared/utils.py::parse"
584 )
585 )).scalar_one()
586 assert count == 2
587
588
589 # ---------------------------------------------------------------------------
590 # Tier 6 — Performance
591 # ---------------------------------------------------------------------------
592
593 class TestStableProviderPerformance:
594 """Performance tests — batch timing bounds for production-scale data."""
595
596 @pytest.mark.asyncio
597 async def test_P1_25_1000_row_batch_under_5s(
598 self, db_session: AsyncSession, stable_repo
599 ) -> None:
600 """First-run upsert of 1000 symbols completes in under 5 seconds."""
601 repo_id = stable_repo.repo_id
602 now = datetime.now(timezone.utc)
603 await db_session.commit()
604 for i in range(1000):
605 await _seed_symbol(
606 db_session, repo_id,
607 address=f"pkg/perf{i}.py::fn",
608 churn=0, churn_30d=0, churn_90d=0,
609 last_changed=now - timedelta(days=100 + (i % 900)),
610 last_commit_id=_REF,
611 )
612 await db_session.commit()
613 start = time.monotonic()
614 await StableProvider().compute(db_session, repo_id, _REF, {})
615 await db_session.flush()
616 elapsed = time.monotonic() - start
617 assert elapsed < 5.0, f"First run took {elapsed:.2f}s — expected < 5s"
618
619 @pytest.mark.asyncio
620 async def test_P1_26_second_run_all_conflicts_under_5s(
621 self, db_session: AsyncSession, stable_repo
622 ) -> None:
623 """Second run (all-conflict upsert path) also completes in under 5 seconds."""
624 repo_id = stable_repo.repo_id
625 now = datetime.now(timezone.utc)
626 await db_session.commit()
627 for i in range(1000):
628 await _seed_symbol(
629 db_session, repo_id,
630 address=f"pkg/perf2_{i}.py::fn",
631 churn=0, churn_30d=0, churn_90d=0,
632 last_changed=now - timedelta(days=100 + (i % 900)),
633 last_commit_id=_REF,
634 )
635 await db_session.commit()
636 await StableProvider().compute(db_session, repo_id, _REF, {})
637 await db_session.flush()
638 start = time.monotonic()
639 await StableProvider().compute(db_session, repo_id, _REF2, {})
640 await db_session.flush()
641 elapsed = time.monotonic() - start
642 assert elapsed < 5.0, f"Second run took {elapsed:.2f}s — expected < 5s"
643
644
645 # ---------------------------------------------------------------------------
646 # Tier 7 — Security
647 # ---------------------------------------------------------------------------
648
649 class TestStableProviderSecurity:
650 """Security tests — injection safety and repo isolation."""
651
652 @pytest.mark.asyncio
653 async def test_P1_27_sql_injection_in_address_stored_verbatim(
654 self, db_session: AsyncSession, stable_repo
655 ) -> None:
656 """Malicious address string is stored as plain text — not executed."""
657 from sqlalchemy import select
658 repo_id = stable_repo.repo_id
659 injection = "'; DROP TABLE musehub_intel_stable; --"
660 now = datetime.now(timezone.utc)
661 await db_session.commit()
662 await _seed_symbol(
663 db_session, repo_id,
664 address=injection,
665 churn=0, churn_30d=0, churn_90d=0,
666 last_changed=now - timedelta(days=100),
667 last_commit_id=_REF,
668 )
669 await db_session.commit()
670 await StableProvider().compute(db_session, repo_id, _REF, {})
671 await db_session.flush()
672 # Table must still exist and contain the verbatim string
673 row = (await db_session.execute(
674 select(db.MusehubIntelStable).where(
675 db.MusehubIntelStable.repo_id == repo_id,
676 db.MusehubIntelStable.address == injection,
677 )
678 )).scalar_one_or_none()
679 assert row is not None
680 assert row.address == injection
681
682 @pytest.mark.asyncio
683 async def test_P1_28_repo_isolation(
684 self, db_session: AsyncSession, stable_repo
685 ) -> None:
686 """Running provider for repo A does not write rows for repo B."""
687 from sqlalchemy import select, func
688 repo_id_a = stable_repo.repo_id
689 repo_b = await create_repo(db_session, owner=_OWNER, slug="stableisolation")
690 repo_id_b = repo_b.repo_id
691 now = datetime.now(timezone.utc)
692 await db_session.commit()
693 # Seed only repo A
694 await _seed_symbol(
695 db_session, repo_id_a,
696 address="shared/fn.py::do_work",
697 churn=0, churn_30d=0, churn_90d=0,
698 last_changed=now - timedelta(days=120),
699 last_commit_id=_REF,
700 )
701 await db_session.commit()
702 await StableProvider().compute(db_session, repo_id_a, _REF, {})
703 await db_session.flush()
704 # repo B must have zero rows
705 count_b = (await db_session.execute(
706 select(func.count()).where(db.MusehubIntelStable.repo_id == repo_id_b)
707 )).scalar_one()
708 assert count_b == 0
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 123 days ago