gabriel / musehub public
test_repo_card_stress.py python
198 lines 7.3 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago
1 """
2 Tier 4 — Stress tests for enrich_repo_cards() under load.
3
4 All tests run against the test database (not mocks) to catch real query
5 behaviour: N+1 regressions, batch overflows, and degenerate data patterns
6 that would silently misbehave in production.
7
8 Test IDs
9 --------
10 T400 — enriching 50 repos issues exactly 5 SQL queries (no N+1)
11 T401 — enriching 100 repos completes in < 2 s (performance floor)
12 T402 — repos with 1000 commits each produce correct pulse buckets
13 T403 — 100 symbols per repo returns the correct hottest without full-scan
14 T404 — mixed batch: some repos with data, some without — no cross-contamination
15 T405 — passing duplicate repo_ids is idempotent (no doubled rows)
16 """
17 from __future__ import annotations
18
19 import time
20 from datetime import datetime, timedelta, timezone
21
22 import pytest
23 from sqlalchemy.ext.asyncio import AsyncSession
24
25 from musehub.services.repo_card_enrichment import (
26 _PULSE_DAYS,
27 enrich_repo_cards,
28 )
29 from tests.factories import create_commit, create_repo
30
31
32 def _utc_now() -> datetime:
33 return datetime.now(tz=timezone.utc)
34
35
36 def _days_ago(n: int) -> datetime:
37 return _utc_now() - timedelta(days=n)
38
39
40 # ---------------------------------------------------------------------------
41 # T400 — no N+1: 5 queries regardless of batch size
42 # ---------------------------------------------------------------------------
43
44 @pytest.mark.asyncio
45 async def test_t400_no_n_plus_one_queries(db_session: AsyncSession) -> None:
46 """T400: enriching 50 repos uses at most 6 queries (5 signal + 1 init)."""
47 repos = [await create_repo(db_session, visibility="public") for _ in range(50)]
48 repo_ids = [r.repo_id for r in repos]
49
50 query_count = 0
51 original_execute = db_session.execute
52
53 async def counting_execute(stmt, *args, **kwargs):
54 nonlocal query_count
55 query_count += 1
56 return await original_execute(stmt, *args, **kwargs)
57
58 db_session.execute = counting_execute # type: ignore[method-assign]
59 await enrich_repo_cards(db_session, repo_ids)
60 db_session.execute = original_execute # type: ignore[method-assign]
61
62 # 5 signal queries (pulse, autonomy, hottest, blast, dead+breakage).
63 # Some implementations may split dead/breakage — allow up to 7.
64 assert query_count <= 7, f"Expected ≤7 queries, got {query_count}"
65
66
67 # ---------------------------------------------------------------------------
68 # T401 — 100-repo batch completes in < 2 s
69 # ---------------------------------------------------------------------------
70
71 @pytest.mark.asyncio
72 async def test_t401_hundred_repos_under_two_seconds(db_session: AsyncSession) -> None:
73 """T401: enrich_repo_cards with 100 repos finishes in under 2 seconds."""
74 repos = [await create_repo(db_session, visibility="public") for _ in range(100)]
75 repo_ids = [r.repo_id for r in repos]
76
77 t0 = time.monotonic()
78 await enrich_repo_cards(db_session, repo_ids)
79 elapsed = time.monotonic() - t0
80
81 assert elapsed < 2.0, f"Enrichment took {elapsed:.2f}s — expected < 2s"
82
83
84 # ---------------------------------------------------------------------------
85 # T402 — 1000 commits produce valid 30-day pulse
86 # ---------------------------------------------------------------------------
87
88 @pytest.mark.asyncio
89 async def test_t402_high_volume_commits_correct_pulse(db_session: AsyncSession) -> None:
90 """T402: a repo with 1000 commits in the window yields valid 30-bucket pulse."""
91 repo = await create_repo(db_session, visibility="public")
92
93 # Spread 1000 commits across the 30-day window
94 for i in range(1000):
95 day_offset = i % _PULSE_DAYS
96 await create_commit(db_session, repo.repo_id, timestamp=_days_ago(day_offset))
97
98 result = await enrich_repo_cards(db_session, [repo.repo_id])
99 enc = result[repo.repo_id]
100
101 assert len(enc.pulse_buckets) == _PULSE_DAYS
102 total_counted = sum(b.count for b in enc.pulse_buckets)
103 assert total_counted == 1000
104 # Busiest bucket is normalised to h=24
105 max_h = max(b.h for b in enc.pulse_buckets)
106 assert max_h == 24
107
108
109 # ---------------------------------------------------------------------------
110 # T403 — 100 symbols: hottest is still the correct one
111 # ---------------------------------------------------------------------------
112
113 @pytest.mark.asyncio
114 async def test_t403_hundred_symbols_hottest_correct(db_session: AsyncSession) -> None:
115 """T403: with 100 symbols the hottest is reliably the one with max churn_30d."""
116 from musehub.db.musehub_models import MusehubSymbolIntel
117
118 repo = await create_repo(db_session, visibility="public")
119
120 for i in range(99):
121 db_session.add(MusehubSymbolIntel(
122 repo_id=repo.repo_id,
123 address=f"src/mod_{i}.py::fn_{i}",
124 churn_30d=i,
125 blast=0,
126 ))
127 # The winner: churn_30d = 9999
128 db_session.add(MusehubSymbolIntel(
129 repo_id=repo.repo_id,
130 address="src/winner.py::hottest_fn",
131 churn_30d=9999,
132 blast=0,
133 ))
134 await db_session.commit()
135
136 result = await enrich_repo_cards(db_session, [repo.repo_id])
137 enc = result[repo.repo_id]
138
139 assert enc.hottest_symbol is not None
140 assert enc.hottest_symbol.address == "src/winner.py::hottest_fn"
141 assert enc.hottest_symbol.churn_30d == 9999
142
143
144 # ---------------------------------------------------------------------------
145 # T404 — mixed batch: data isolation
146 # ---------------------------------------------------------------------------
147
148 @pytest.mark.asyncio
149 async def test_t404_mixed_batch_no_cross_contamination(db_session: AsyncSession) -> None:
150 """T404: 25 repos with data + 25 without — no signal leaks between repos."""
151 from musehub.db.musehub_models import MusehubSymbolIntel
152
153 repos_with = [await create_repo(db_session, visibility="public") for _ in range(25)]
154 repos_without = [await create_repo(db_session, visibility="public") for _ in range(25)]
155
156 for repo in repos_with:
157 db_session.add(MusehubSymbolIntel(
158 repo_id=repo.repo_id,
159 address="src/a.py::fn",
160 churn_30d=10,
161 blast=5,
162 ))
163 await db_session.commit()
164
165 all_ids = [r.repo_id for r in repos_with + repos_without]
166 result = await enrich_repo_cards(db_session, all_ids)
167
168 for repo in repos_with:
169 assert result[repo.repo_id].hottest_symbol is not None
170
171 for repo in repos_without:
172 enc = result[repo.repo_id]
173 assert enc.hottest_symbol is None
174 assert enc.blast_leader is None
175 assert enc.dead_count == 0
176 assert enc.autonomy_pct == 0
177
178
179 # ---------------------------------------------------------------------------
180 # T405 — duplicate repo_ids are idempotent
181 # ---------------------------------------------------------------------------
182
183 @pytest.mark.asyncio
184 async def test_t405_duplicate_repo_ids_idempotent(db_session: AsyncSession) -> None:
185 """T405: passing the same repo_id twice yields exactly one result entry."""
186 repo = await create_repo(db_session, visibility="public")
187 await create_commit(db_session, repo.repo_id, timestamp=_utc_now())
188
189 result = await enrich_repo_cards(
190 db_session, [repo.repo_id, repo.repo_id, repo.repo_id]
191 )
192
193 # Only one entry regardless of duplicates in input
194 assert len(result) == 1
195 assert repo.repo_id in result
196 # Pulse should not double-count due to deduplication
197 total_counted = sum(b.count for b in result[repo.repo_id].pulse_buckets)
198 assert total_counted == 1
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago