gabriel / musehub public
test_velocity_provider.py python
936 lines 40.5 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 122 days ago
1 """TDD spec for VelocityProvider — issue #16, Phase 5.
2
3 Verifies that VelocityProvider reproduces module growth velocity from the
4 symbol history store without subprocess calls: module derivation from symbol
5 addresses, op categorisation (add/delete/modify), two-window BFS analysis
6 (current vs prior), acceleration, stagnant-commit detection, extended columns
7 (prior_modified, prior_active_commits, window_size, commits_analysed), TOP cap,
8 and strict repo isolation.
9
10 Seven test tiers (50 cases)
11 ----------------------------
12 Unit VL_01 – VL_08 module derivation, accel helpers, constants
13 Integration VL_09 – VL_18 provider upserts, new columns, op categorisation
14 E2E VL_19 – VL_25 full seeded scenarios, window semantics
15 Performance VL_26 – VL_32 timing bounds
16 State VL_33 – VL_38 idempotency, stale-row purge, incremental updates
17 Security VL_39 – VL_44 injection strings, repo isolation, unicode
18 Stress VL_45 – VL_50 TOP cap, BFS cap, extended-column completeness
19 """
20 from __future__ import annotations
21
22 import secrets
23 import time
24 from datetime import datetime, timezone
25
26 import pytest
27 import pytest_asyncio
28 import sqlalchemy as sa
29 from sqlalchemy.dialects.postgresql import insert as pg_insert
30 from sqlalchemy.ext.asyncio import AsyncSession
31
32 from muse.core.types import long_id
33 from musehub.db import musehub_models as db
34 from musehub.services.musehub_intel_providers import VelocityProvider
35 from musehub.types.json_types import JSONObject
36 from musehub.api.routes.musehub.ui_intel import _vel_accel_class, _vel_accel_fmt
37 from tests.factories import create_repo
38
39
40 # ─────────────────────────────────────────────────────────────────────────────
41 # Helpers
42 # ─────────────────────────────────────────────────────────────────────────────
43
44 def _cid() -> str:
45 return long_id(secrets.token_hex(32))
46
47
48 async def _seed_commit(
49 session: AsyncSession,
50 repo_id: str,
51 commit_id: str,
52 parent_ids: list[str] | None = None,
53 ) -> None:
54 """Insert a commit row; silently skip on conflict."""
55 await session.execute(
56 pg_insert(db.MusehubCommit)
57 .values(
58 commit_id=commit_id,
59 repo_id=repo_id,
60 message="test commit",
61 author="test",
62 branch="dev",
63 parent_ids=parent_ids or [],
64 snapshot_id=None,
65 timestamp=datetime.now(timezone.utc),
66 )
67 .on_conflict_do_nothing()
68 )
69
70
71 async def _seed_history(
72 session: AsyncSession,
73 repo_id: str,
74 commit_id: str,
75 addresses: list[str],
76 op: str = "modify",
77 ) -> None:
78 """Insert symbol history entries with a given op code."""
79 for addr in addresses:
80 await session.execute(
81 pg_insert(db.MusehubSymbolHistoryEntry)
82 .values(
83 repo_id=repo_id,
84 address=addr,
85 commit_id=commit_id,
86 committed_at=datetime.now(timezone.utc),
87 op=op,
88 )
89 .on_conflict_do_nothing()
90 )
91
92
93 async def _run(session: AsyncSession, repo_id: str, ref: str) -> list[tuple[str, JSONObject]]:
94 return await VelocityProvider().compute(session, repo_id, ref, {})
95
96
97 async def _fetch(session: AsyncSession, repo_id: str) -> list[db.MusehubIntelVelocity]:
98 result = await session.execute(
99 sa.select(db.MusehubIntelVelocity)
100 .where(db.MusehubIntelVelocity.repo_id == repo_id)
101 .order_by(sa.desc(db.MusehubIntelVelocity.active_commits))
102 )
103 return list(result.scalars().all())
104
105
106 def _module(addr: str) -> str:
107 """Replicate VelocityProvider._module() for unit tests."""
108 file = addr.split("::")[0] if "::" in addr else addr
109 if "/" in file:
110 return f"{file.rsplit('/', 1)[0]}/"
111 return f"{file}/"
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="testuser", slug="velocityprovider")
121
122
123 @pytest_asyncio.fixture
124 async def two_repos(db_session: AsyncSession):
125 r1 = await create_repo(db_session, owner="testuser", slug="vel-repo-1")
126 r2 = await create_repo(db_session, owner="testuser", slug="vel-repo-2")
127 return r1, r2
128
129
130 # ─────────────────────────────────────────────────────────────────────────────
131 # Tier 1 — Unit: module derivation, accel helpers, constants
132 # ─────────────────────────────────────────────────────────────────────────────
133
134 class TestVelocityUnit:
135 """Pure-function tests — no database required."""
136
137 def test_VL_01_module_from_deep_symbol_address(self) -> None:
138 """Module extracted as directory of file component of a deep address."""
139 assert _module("musehub/services/musehub_wire.py::MyClass") == "musehub/services/"
140
141 def test_VL_02_module_from_shallow_symbol_address(self) -> None:
142 """Shallow one-directory file extracts its directory."""
143 assert _module("src/billing.py::charge") == "src/"
144
145 def test_VL_03_module_from_bare_file_no_slash(self) -> None:
146 """Root-level file (no '/') maps to '<filename>/'."""
147 assert _module("billing.py") == "billing.py/"
148
149 def test_VL_04_module_from_bare_path_with_slash(self) -> None:
150 """Bare path with slash (no '::') derives module correctly."""
151 assert _module("musehub/services/foo.py") == "musehub/services/"
152
153 def test_VL_05_accel_class_positive(self) -> None:
154 """Positive acceleration → 'up' class."""
155 assert _vel_accel_class(5.0) == "up"
156 assert _vel_accel_class(0.1) == "up"
157
158 def test_VL_06_accel_class_negative(self) -> None:
159 """Negative acceleration → 'down' class."""
160 assert _vel_accel_class(-3.0) == "down"
161 assert _vel_accel_class(-0.1) == "down"
162
163 def test_VL_07_accel_class_zero(self) -> None:
164 """Zero acceleration → 'flat' class."""
165 assert _vel_accel_class(0.0) == "flat"
166
167 def test_VL_08_accel_fmt_positive_negative_zero(self) -> None:
168 """accel_fmt prefixes '+' for positive, keeps '-' for negative, '0' for zero."""
169 assert _vel_accel_fmt(4.0) == "+4"
170 assert _vel_accel_fmt(-3.0) == "-3"
171 assert _vel_accel_fmt(0.0) == "0"
172
173
174 # ─────────────────────────────────────────────────────────────────────────────
175 # Tier 2 — Integration: provider upserts, op categorisation, new columns
176 # ─────────────────────────────────────────────────────────────────────────────
177
178 class TestVelocityIntegration:
179
180 @pytest.mark.asyncio
181 async def test_VL_09_empty_repo_returns_empty(
182 self, db_session: AsyncSession, repo
183 ) -> None:
184 """Provider on a repo with no commits returns [] and stores no rows."""
185 result = await _run(db_session, repo.repo_id, _cid())
186 assert result == []
187 assert await _fetch(db_session, repo.repo_id) == []
188
189 @pytest.mark.asyncio
190 async def test_VL_10_no_history_entries_returns_empty(
191 self, db_session: AsyncSession, repo
192 ) -> None:
193 """Commits exist but no history entries → no rows stored."""
194 c1 = _cid()
195 await _seed_commit(db_session, repo.repo_id, c1)
196 await db_session.commit()
197 result = await _run(db_session, repo.repo_id, c1)
198 assert result == []
199
200 @pytest.mark.asyncio
201 async def test_VL_11_add_op_counted_as_added(
202 self, db_session: AsyncSession, repo
203 ) -> None:
204 """History entries with op='add' increment the added counter."""
205 c1 = _cid()
206 await _seed_commit(db_session, repo.repo_id, c1)
207 await _seed_history(db_session, repo.repo_id, c1,
208 ["src/billing.py::charge"], op="add")
209 await db_session.commit()
210 await _run(db_session, repo.repo_id, c1)
211 rows = await _fetch(db_session, repo.repo_id)
212 assert len(rows) == 1
213 assert rows[0].added == 1
214 assert rows[0].removed == 0
215 assert rows[0].modified == 0
216
217 @pytest.mark.asyncio
218 async def test_VL_12_delete_op_counted_as_removed(
219 self, db_session: AsyncSession, repo
220 ) -> None:
221 """History entries with op='delete' increment the removed counter."""
222 c1 = _cid()
223 await _seed_commit(db_session, repo.repo_id, c1)
224 await _seed_history(db_session, repo.repo_id, c1,
225 ["src/billing.py::charge"], op="delete")
226 await db_session.commit()
227 await _run(db_session, repo.repo_id, c1)
228 rows = await _fetch(db_session, repo.repo_id)
229 assert rows[0].removed == 1
230 assert rows[0].added == 0
231
232 @pytest.mark.asyncio
233 async def test_VL_13_modify_op_counted_as_modified(
234 self, db_session: AsyncSession, repo
235 ) -> None:
236 """History entries with op='modify' increment the modified counter."""
237 c1 = _cid()
238 await _seed_commit(db_session, repo.repo_id, c1)
239 await _seed_history(db_session, repo.repo_id, c1,
240 ["src/billing.py::charge"], op="modify")
241 await db_session.commit()
242 await _run(db_session, repo.repo_id, c1)
243 rows = await _fetch(db_session, repo.repo_id)
244 assert rows[0].modified == 1
245 assert rows[0].added == 0
246
247 @pytest.mark.asyncio
248 async def test_VL_14_net_equals_added_minus_removed(
249 self, db_session: AsyncSession, repo
250 ) -> None:
251 """net = added - removed for the current window."""
252 c1 = _cid()
253 await _seed_commit(db_session, repo.repo_id, c1)
254 await _seed_history(db_session, repo.repo_id, c1,
255 ["src/billing.py::a", "src/billing.py::b"], op="add")
256 await _seed_history(db_session, repo.repo_id, c1,
257 ["src/billing.py::c"], op="delete")
258 await db_session.commit()
259 await _run(db_session, repo.repo_id, c1)
260 rows = await _fetch(db_session, repo.repo_id)
261 assert rows[0].net == rows[0].added - rows[0].removed
262
263 @pytest.mark.asyncio
264 async def test_VL_15_active_commits_counts_distinct_commits(
265 self, db_session: AsyncSession, repo
266 ) -> None:
267 """active_commits equals the number of distinct commits that touched the module."""
268 commits = [_cid() for _ in range(3)]
269 prev = None
270 for cid in commits:
271 await _seed_commit(db_session, repo.repo_id, cid,
272 [prev] if prev else [])
273 prev = cid
274 for cid in commits:
275 await _seed_history(db_session, repo.repo_id, cid,
276 ["src/billing.py::fn"])
277 await db_session.commit()
278 await _run(db_session, repo.repo_id, commits[-1])
279 rows = await _fetch(db_session, repo.repo_id)
280 assert rows[0].active_commits == 3
281
282 @pytest.mark.asyncio
283 async def test_VL_16_window_size_column_populated(
284 self, db_session: AsyncSession, repo
285 ) -> None:
286 """window_size column reflects VelocityProvider._WINDOW."""
287 c1 = _cid()
288 await _seed_commit(db_session, repo.repo_id, c1)
289 await _seed_history(db_session, repo.repo_id, c1,
290 ["src/billing.py::fn"])
291 await db_session.commit()
292 await _run(db_session, repo.repo_id, c1)
293 rows = await _fetch(db_session, repo.repo_id)
294 assert rows[0].window_size == VelocityProvider._WINDOW
295
296 @pytest.mark.asyncio
297 async def test_VL_17_commits_analysed_column_populated(
298 self, db_session: AsyncSession, repo
299 ) -> None:
300 """commits_analysed column reflects the BFS walk length."""
301 commits = [_cid() for _ in range(5)]
302 prev = None
303 for cid in commits:
304 await _seed_commit(db_session, repo.repo_id, cid,
305 [prev] if prev else [])
306 prev = cid
307 await _seed_history(db_session, repo.repo_id, commits[0],
308 ["src/billing.py::fn"])
309 await db_session.commit()
310 await _run(db_session, repo.repo_id, commits[-1])
311 rows = await _fetch(db_session, repo.repo_id)
312 assert rows[0].commits_analysed == 5
313
314 @pytest.mark.asyncio
315 async def test_VL_18_result_key_correct(
316 self, db_session: AsyncSession, repo
317 ) -> None:
318 """Provider returns result tuple with key 'intel.code.velocity'."""
319 c1 = _cid()
320 await _seed_commit(db_session, repo.repo_id, c1)
321 await _seed_history(db_session, repo.repo_id, c1,
322 ["src/billing.py::fn"])
323 await db_session.commit()
324 result = await _run(db_session, repo.repo_id, c1)
325 assert len(result) == 1
326 key, payload = result[0]
327 assert key == "intel.code.velocity"
328 assert "count" in payload
329 assert "commits_analysed" in payload
330 assert "truncated" in payload
331
332
333 # ─────────────────────────────────────────────────────────────────────────────
334 # Tier 3 — E2E: full seeded scenarios, window semantics
335 # ─────────────────────────────────────────────────────────────────────────────
336
337 class TestVelocityE2E:
338
339 @pytest.mark.asyncio
340 async def test_VL_19_hottest_module_ranked_first(
341 self, db_session: AsyncSession, repo
342 ) -> None:
343 """Module with more active commits is ranked first by active_commits."""
344 commits = [_cid() for _ in range(5)]
345 prev = None
346 for cid in commits:
347 await _seed_commit(db_session, repo.repo_id, cid,
348 [prev] if prev else [])
349 prev = cid
350 # services/ in all 5; tests/ in only 2
351 for cid in commits:
352 await _seed_history(db_session, repo.repo_id, cid,
353 ["musehub/services/foo.py::fn"])
354 for cid in commits[:2]:
355 await _seed_history(db_session, repo.repo_id, cid,
356 ["tests/test_foo.py::test_fn"])
357 await db_session.commit()
358 await _run(db_session, repo.repo_id, commits[-1])
359 rows = await _fetch(db_session, repo.repo_id)
360 assert rows[0].module == "musehub/services/"
361
362 @pytest.mark.asyncio
363 async def test_VL_20_two_modules_produce_two_rows(
364 self, db_session: AsyncSession, repo
365 ) -> None:
366 """Symbols from two distinct modules produce two velocity rows."""
367 c1 = _cid()
368 await _seed_commit(db_session, repo.repo_id, c1)
369 await _seed_history(db_session, repo.repo_id, c1,
370 ["src/a.py::fn", "tests/test_a.py::test_fn"])
371 await db_session.commit()
372 await _run(db_session, repo.repo_id, c1)
373 rows = await _fetch(db_session, repo.repo_id)
374 modules = {r.module for r in rows}
375 assert "src/" in modules
376 assert "tests/" in modules
377
378 @pytest.mark.asyncio
379 async def test_VL_21_stagnant_commit_detected(
380 self, db_session: AsyncSession, repo
381 ) -> None:
382 """A commit where added==removed for a module increments stagnant_commits."""
383 c1 = _cid()
384 await _seed_commit(db_session, repo.repo_id, c1)
385 # One add + one delete in same module + same commit → net=0 → stagnant
386 await _seed_history(db_session, repo.repo_id, c1,
387 ["src/billing.py::new_fn"], op="add")
388 await _seed_history(db_session, repo.repo_id, c1,
389 ["src/billing.py::old_fn"], op="delete")
390 await db_session.commit()
391 await _run(db_session, repo.repo_id, c1)
392 rows = await _fetch(db_session, repo.repo_id)
393 assert rows[0].stagnant_commits == 1
394
395 @pytest.mark.asyncio
396 async def test_VL_22_non_stagnant_commit_not_counted(
397 self, db_session: AsyncSession, repo
398 ) -> None:
399 """A commit with net != 0 does not increment stagnant_commits."""
400 c1 = _cid()
401 await _seed_commit(db_session, repo.repo_id, c1)
402 await _seed_history(db_session, repo.repo_id, c1,
403 ["src/billing.py::fn"], op="add")
404 await db_session.commit()
405 await _run(db_session, repo.repo_id, c1)
406 rows = await _fetch(db_session, repo.repo_id)
407 assert rows[0].stagnant_commits == 0
408
409 @pytest.mark.asyncio
410 async def test_VL_23_prior_window_populates_prior_fields(
411 self, db_session: AsyncSession, repo
412 ) -> None:
413 """Commits beyond _WINDOW land in the prior window and set prior_* fields."""
414 provider = VelocityProvider()
415 n = provider._WINDOW + 3
416 commits = [_cid() for _ in range(n)]
417 prev = None
418 for cid in commits:
419 await _seed_commit(db_session, repo.repo_id, cid,
420 [prev] if prev else [])
421 prev = cid
422 # touch src/ in all commits → first _WINDOW go to current, rest to prior
423 for cid in commits:
424 await _seed_history(db_session, repo.repo_id, cid,
425 ["src/billing.py::fn"], op="add")
426 await db_session.commit()
427 await _run(db_session, repo.repo_id, commits[-1])
428 rows = await _fetch(db_session, repo.repo_id)
429 row = rows[0]
430 assert row.prior_active_commits > 0
431
432 @pytest.mark.asyncio
433 async def test_VL_24_positive_acceleration_when_current_more_active(
434 self, db_session: AsyncSession, repo
435 ) -> None:
436 """acceleration > 0 when current window has higher net than prior."""
437 provider = VelocityProvider()
438 # prior window: 1 add per commit; current window: 3 adds per commit
439 prior_commits = [_cid() for _ in range(provider._WINDOW)]
440 current_commits = [_cid() for _ in range(provider._WINDOW)]
441 all_commits = prior_commits + current_commits
442 prev = None
443 for cid in all_commits:
444 await _seed_commit(db_session, repo.repo_id, cid,
445 [prev] if prev else [])
446 prev = cid
447 for cid in prior_commits:
448 await _seed_history(db_session, repo.repo_id, cid,
449 ["src/billing.py::fn1"], op="add")
450 for cid in current_commits:
451 for sym in ["src/billing.py::fn1", "src/billing.py::fn2",
452 "src/billing.py::fn3"]:
453 await _seed_history(db_session, repo.repo_id, cid,
454 [sym], op="add")
455 await db_session.commit()
456 await _run(db_session, repo.repo_id, all_commits[-1])
457 rows = await _fetch(db_session, repo.repo_id)
458 src_row = next(r for r in rows if r.module == "src/")
459 assert src_row.acceleration > 0
460
461 @pytest.mark.asyncio
462 async def test_VL_25_module_only_in_current_has_zero_prior(
463 self, db_session: AsyncSession, repo
464 ) -> None:
465 """A module only touched in the current window has prior_active_commits=0."""
466 c1 = _cid()
467 await _seed_commit(db_session, repo.repo_id, c1)
468 await _seed_history(db_session, repo.repo_id, c1,
469 ["src/billing.py::fn"])
470 await db_session.commit()
471 await _run(db_session, repo.repo_id, c1)
472 rows = await _fetch(db_session, repo.repo_id)
473 assert rows[0].prior_active_commits == 0
474 assert rows[0].prior_net == 0
475
476
477 # ─────────────────────────────────────────────────────────────────────────────
478 # Tier 4 — Performance: timing bounds
479 # ─────────────────────────────────────────────────────────────────────────────
480
481 class TestVelocityPerformance:
482
483 @pytest.mark.asyncio
484 async def test_VL_26_ten_commits_five_modules_under_500ms(
485 self, db_session: AsyncSession, repo
486 ) -> None:
487 """10 commits × 5 modules completes in under 500 ms."""
488 commits = [_cid() for _ in range(10)]
489 prev = None
490 for cid in commits:
491 await _seed_commit(db_session, repo.repo_id, cid,
492 [prev] if prev else [])
493 prev = cid
494 for cid in commits:
495 for i in range(5):
496 await _seed_history(db_session, repo.repo_id, cid,
497 [f"mod{i}/file.py::fn"])
498 await db_session.commit()
499 t0 = time.monotonic()
500 await _run(db_session, repo.repo_id, commits[-1])
501 assert time.monotonic() - t0 < 0.5
502
503 @pytest.mark.asyncio
504 async def test_VL_27_forty_commits_ten_modules_under_2s(
505 self, db_session: AsyncSession, repo
506 ) -> None:
507 """40 commits × 10 modules completes in under 2 s."""
508 commits = [_cid() for _ in range(40)]
509 prev = None
510 for cid in commits:
511 await _seed_commit(db_session, repo.repo_id, cid,
512 [prev] if prev else [])
513 prev = cid
514 for cid in commits:
515 for i in range(10):
516 await _seed_history(db_session, repo.repo_id, cid,
517 [f"mod{i}/file.py::fn"])
518 await db_session.commit()
519 t0 = time.monotonic()
520 await _run(db_session, repo.repo_id, commits[-1])
521 assert time.monotonic() - t0 < 2.0
522
523 @pytest.mark.asyncio
524 async def test_VL_28_empty_repo_fast_path_under_50ms(
525 self, db_session: AsyncSession, repo
526 ) -> None:
527 """Empty repo fast-path exits under 50 ms."""
528 t0 = time.monotonic()
529 await _run(db_session, repo.repo_id, _cid())
530 assert time.monotonic() - t0 < 0.05
531
532 @pytest.mark.asyncio
533 async def test_VL_29_rerun_not_5x_slower(
534 self, db_session: AsyncSession, repo
535 ) -> None:
536 """Second run is not more than 5× slower than the first."""
537 c1 = _cid()
538 await _seed_commit(db_session, repo.repo_id, c1)
539 await _seed_history(db_session, repo.repo_id, c1, ["src/a.py::fn"])
540 await db_session.commit()
541 t1 = time.monotonic(); await _run(db_session, repo.repo_id, c1); d1 = time.monotonic() - t1
542 t2 = time.monotonic(); await _run(db_session, repo.repo_id, c1); d2 = time.monotonic() - t2
543 assert d2 < max(d1 * 5, 0.5)
544
545 @pytest.mark.asyncio
546 async def test_VL_30_point_lookup_under_10ms(
547 self, db_session: AsyncSession, repo
548 ) -> None:
549 """Fetching velocity rows for a repo is sub-10 ms after provider run."""
550 c1 = _cid()
551 await _seed_commit(db_session, repo.repo_id, c1)
552 await _seed_history(db_session, repo.repo_id, c1, ["src/a.py::fn"])
553 await db_session.commit()
554 await _run(db_session, repo.repo_id, c1)
555 t0 = time.monotonic()
556 await _fetch(db_session, repo.repo_id)
557 assert time.monotonic() - t0 < 0.01
558
559 @pytest.mark.asyncio
560 async def test_VL_31_top20_leaderboard_query_fast(
561 self, db_session: AsyncSession, repo
562 ) -> None:
563 """Fetching top-20 leaderboard from the table is sub-50 ms."""
564 commits = [_cid() for _ in range(5)]
565 prev = None
566 for cid in commits:
567 await _seed_commit(db_session, repo.repo_id, cid,
568 [prev] if prev else [])
569 prev = cid
570 for cid in commits:
571 for i in range(20):
572 await _seed_history(db_session, repo.repo_id, cid,
573 [f"mod{i}/file.py::fn"])
574 await db_session.commit()
575 await _run(db_session, repo.repo_id, commits[-1])
576 t0 = time.monotonic()
577 await db_session.execute(
578 sa.select(db.MusehubIntelVelocity)
579 .where(db.MusehubIntelVelocity.repo_id == repo.repo_id)
580 .order_by(sa.desc(db.MusehubIntelVelocity.active_commits))
581 .limit(20)
582 )
583 assert time.monotonic() - t0 < 0.05
584
585 @pytest.mark.asyncio
586 async def test_VL_32_dashboard_preview_query_fast(
587 self, db_session: AsyncSession, repo
588 ) -> None:
589 """Dashboard preview (top 5, LIMIT query) completes under 20 ms."""
590 c1 = _cid()
591 await _seed_commit(db_session, repo.repo_id, c1)
592 for i in range(5):
593 await _seed_history(db_session, repo.repo_id, c1,
594 [f"mod{i}/file.py::fn"])
595 await db_session.commit()
596 await _run(db_session, repo.repo_id, c1)
597 t0 = time.monotonic()
598 await db_session.execute(
599 sa.select(db.MusehubIntelVelocity)
600 .where(db.MusehubIntelVelocity.repo_id == repo.repo_id)
601 .order_by(sa.desc(db.MusehubIntelVelocity.active_commits))
602 .limit(5)
603 )
604 assert time.monotonic() - t0 < 0.02
605
606
607 # ─────────────────────────────────────────────────────────────────────────────
608 # Tier 5 — State: idempotency, stale-row purge, incremental updates
609 # ─────────────────────────────────────────────────────────────────────────────
610
611 class TestVelocityState:
612
613 @pytest.mark.asyncio
614 async def test_VL_33_idempotent_two_runs(
615 self, db_session: AsyncSession, repo
616 ) -> None:
617 """Running the provider twice produces identical rows."""
618 c1 = _cid()
619 await _seed_commit(db_session, repo.repo_id, c1)
620 await _seed_history(db_session, repo.repo_id, c1, ["src/a.py::fn"])
621 await db_session.commit()
622 await _run(db_session, repo.repo_id, c1)
623 first = {(r.module, r.active_commits, r.net)
624 for r in await _fetch(db_session, repo.repo_id)}
625 await _run(db_session, repo.repo_id, c1)
626 second = {(r.module, r.active_commits, r.net)
627 for r in await _fetch(db_session, repo.repo_id)}
628 assert first == second
629
630 @pytest.mark.asyncio
631 async def test_VL_34_stale_rows_purged_on_rerun(
632 self, db_session: AsyncSession, repo
633 ) -> None:
634 """Re-run deletes all old rows before inserting fresh set."""
635 c1 = _cid()
636 await _seed_commit(db_session, repo.repo_id, c1)
637 await _seed_history(db_session, repo.repo_id, c1, ["src/a.py::fn"])
638 await db_session.commit()
639 await _run(db_session, repo.repo_id, c1)
640 count_first = (await db_session.execute(
641 sa.select(sa.func.count()).select_from(db.MusehubIntelVelocity)
642 .where(db.MusehubIntelVelocity.repo_id == repo.repo_id)
643 )).scalar_one()
644 await _run(db_session, repo.repo_id, c1)
645 count_second = (await db_session.execute(
646 sa.select(sa.func.count()).select_from(db.MusehubIntelVelocity)
647 .where(db.MusehubIntelVelocity.repo_id == repo.repo_id)
648 )).scalar_one()
649 assert count_first == count_second
650
651 @pytest.mark.asyncio
652 async def test_VL_35_incremental_new_module_appears(
653 self, db_session: AsyncSession, repo
654 ) -> None:
655 """After adding commits to a new module, it materialises on re-run."""
656 c1 = _cid()
657 await _seed_commit(db_session, repo.repo_id, c1)
658 await _seed_history(db_session, repo.repo_id, c1, ["src/a.py::fn"])
659 await db_session.commit()
660 await _run(db_session, repo.repo_id, c1)
661 modules_before = {r.module for r in await _fetch(db_session, repo.repo_id)}
662
663 c2 = _cid()
664 await _seed_commit(db_session, repo.repo_id, c2, [c1])
665 await _seed_history(db_session, repo.repo_id, c2, ["tests/test_a.py::test_fn"])
666 await db_session.commit()
667 await _run(db_session, repo.repo_id, c2)
668 modules_after = {r.module for r in await _fetch(db_session, repo.repo_id)}
669 assert len(modules_after) > len(modules_before)
670
671 @pytest.mark.asyncio
672 async def test_VL_36_no_duplicate_modules_after_three_runs(
673 self, db_session: AsyncSession, repo
674 ) -> None:
675 """No duplicate module rows after 3 consecutive runs."""
676 c1 = _cid()
677 await _seed_commit(db_session, repo.repo_id, c1)
678 await _seed_history(db_session, repo.repo_id, c1, ["src/a.py::fn"])
679 await db_session.commit()
680 for _ in range(3):
681 await _run(db_session, repo.repo_id, c1)
682 rows = await _fetch(db_session, repo.repo_id)
683 modules = [r.module for r in rows]
684 assert len(modules) == len(set(modules))
685
686 @pytest.mark.asyncio
687 async def test_VL_37_active_commits_increases_with_new_commits(
688 self, db_session: AsyncSession, repo
689 ) -> None:
690 """active_commits increases when more commits touch the module."""
691 c1, c2 = _cid(), _cid()
692 await _seed_commit(db_session, repo.repo_id, c1)
693 await _seed_history(db_session, repo.repo_id, c1, ["src/a.py::fn"])
694 await db_session.commit()
695 await _run(db_session, repo.repo_id, c1)
696 before = (await _fetch(db_session, repo.repo_id))[0].active_commits
697
698 await _seed_commit(db_session, repo.repo_id, c2, [c1])
699 await _seed_history(db_session, repo.repo_id, c2, ["src/a.py::fn"])
700 await db_session.commit()
701 await _run(db_session, repo.repo_id, c2)
702 after = (await _fetch(db_session, repo.repo_id))[0].active_commits
703 assert after > before
704
705 @pytest.mark.asyncio
706 async def test_VL_38_truncated_false_when_under_cap(
707 self, db_session: AsyncSession, repo
708 ) -> None:
709 """truncated=False when module count is within _TOP."""
710 c1 = _cid()
711 await _seed_commit(db_session, repo.repo_id, c1)
712 await _seed_history(db_session, repo.repo_id, c1, ["src/a.py::fn"])
713 await db_session.commit()
714 result = await _run(db_session, repo.repo_id, c1)
715 key, payload = result[0]
716 assert payload["truncated"] is False
717
718
719 # ─────────────────────────────────────────────────────────────────────────────
720 # Tier 6 — Security: injection, isolation, unicode
721 # ─────────────────────────────────────────────────────────────────────────────
722
723 class TestVelocitySecurity:
724
725 @pytest.mark.asyncio
726 async def test_VL_39_sql_injection_stored_verbatim(
727 self, db_session: AsyncSession, repo
728 ) -> None:
729 """SQL injection in symbol address stored as-is; table survives."""
730 inject = "src/a.py::fn'; DROP TABLE musehub_intel_velocity; --"
731 c1 = _cid()
732 await _seed_commit(db_session, repo.repo_id, c1)
733 await _seed_history(db_session, repo.repo_id, c1, [inject])
734 await db_session.commit()
735 await _run(db_session, repo.repo_id, c1)
736 assert isinstance(await _fetch(db_session, repo.repo_id), list)
737
738 @pytest.mark.asyncio
739 async def test_VL_40_xss_payload_stored_safely(
740 self, db_session: AsyncSession, repo
741 ) -> None:
742 """XSS payload in symbol address stored without execution."""
743 xss = "src/<script>alert(1)</script>.py::fn"
744 c1 = _cid()
745 await _seed_commit(db_session, repo.repo_id, c1)
746 await _seed_history(db_session, repo.repo_id, c1, [xss])
747 await db_session.commit()
748 await _run(db_session, repo.repo_id, c1)
749 assert isinstance(await _fetch(db_session, repo.repo_id), list)
750
751 @pytest.mark.asyncio
752 async def test_VL_41_repo_isolation_strict(
753 self, db_session: AsyncSession, two_repos
754 ) -> None:
755 """Velocity rows from repo A are never visible when querying repo B."""
756 r1, r2 = two_repos
757 c1 = _cid()
758 await _seed_commit(db_session, r1.repo_id, c1)
759 await _seed_history(db_session, r1.repo_id, c1, ["src/a.py::fn"])
760 await db_session.commit()
761 await _run(db_session, r1.repo_id, c1)
762 assert await _fetch(db_session, r2.repo_id) == []
763
764 @pytest.mark.asyncio
765 async def test_VL_42_two_repos_independent_rows(
766 self, db_session: AsyncSession, two_repos
767 ) -> None:
768 """Two repos each produce their own independent velocity rows."""
769 r1, r2 = two_repos
770 for repo in [r1, r2]:
771 c1 = _cid()
772 await _seed_commit(db_session, repo.repo_id, c1)
773 await _seed_history(db_session, repo.repo_id, c1,
774 ["src/a.py::fn"])
775 await db_session.commit()
776 await _run(db_session, repo.repo_id, c1)
777 rows1 = await _fetch(db_session, r1.repo_id)
778 rows2 = await _fetch(db_session, r2.repo_id)
779 assert all(r.repo_id == r1.repo_id for r in rows1)
780 assert all(r.repo_id == r2.repo_id for r in rows2)
781
782 @pytest.mark.asyncio
783 async def test_VL_43_rerun_updates_ref_column(
784 self, db_session: AsyncSession, repo
785 ) -> None:
786 """Re-run for a new ref updates the ref column on all rows."""
787 c1, c2 = _cid(), _cid()
788 await _seed_commit(db_session, repo.repo_id, c1)
789 await _seed_commit(db_session, repo.repo_id, c2, [c1])
790 for cid in [c1, c2]:
791 await _seed_history(db_session, repo.repo_id, cid,
792 ["src/a.py::fn"])
793 await db_session.commit()
794 await _run(db_session, repo.repo_id, c1)
795 await _run(db_session, repo.repo_id, c2)
796 rows = await _fetch(db_session, repo.repo_id)
797 assert all(r.ref == c2 for r in rows)
798
799 @pytest.mark.asyncio
800 async def test_VL_44_unicode_in_path_handled(
801 self, db_session: AsyncSession, repo
802 ) -> None:
803 """Unicode characters in symbol paths do not crash the provider."""
804 c1 = _cid()
805 await _seed_commit(db_session, repo.repo_id, c1)
806 await _seed_history(db_session, repo.repo_id, c1,
807 ["src/música.py::canción"])
808 await db_session.commit()
809 await _run(db_session, repo.repo_id, c1)
810 assert isinstance(await _fetch(db_session, repo.repo_id), list)
811
812
813 # ─────────────────────────────────────────────────────────────────────────────
814 # Tier 7 — Stress: TOP cap, BFS cap, extended-column completeness
815 # ─────────────────────────────────────────────────────────────────────────────
816
817 class TestVelocityStress:
818
819 @pytest.mark.asyncio
820 async def test_VL_45_top_cap_respected(
821 self, db_session: AsyncSession, repo
822 ) -> None:
823 """Stored module count never exceeds _TOP."""
824 provider = VelocityProvider()
825 c1 = _cid()
826 await _seed_commit(db_session, repo.repo_id, c1)
827 # _TOP + 5 distinct modules
828 for i in range(provider._TOP + 5):
829 await _seed_history(db_session, repo.repo_id, c1,
830 [f"mod{i:03d}/file.py::fn"])
831 await db_session.commit()
832 await _run(db_session, repo.repo_id, c1)
833 rows = await _fetch(db_session, repo.repo_id)
834 assert len(rows) <= provider._TOP
835
836 @pytest.mark.asyncio
837 async def test_VL_46_truncated_true_over_top_cap(
838 self, db_session: AsyncSession, repo
839 ) -> None:
840 """truncated=True when distinct module count exceeds _TOP."""
841 provider = VelocityProvider()
842 c1 = _cid()
843 await _seed_commit(db_session, repo.repo_id, c1)
844 for i in range(provider._TOP + 1):
845 await _seed_history(db_session, repo.repo_id, c1,
846 [f"mod{i:03d}/file.py::fn"])
847 await db_session.commit()
848 result = await _run(db_session, repo.repo_id, c1)
849 key, payload = result[0]
850 assert payload["truncated"] is True
851
852 @pytest.mark.asyncio
853 async def test_VL_47_500_commits_completes_without_error(
854 self, db_session: AsyncSession, repo
855 ) -> None:
856 """500 commits × 3 modules completes without error."""
857 commits = [_cid() for _ in range(500)]
858 prev = None
859 for cid in commits:
860 await _seed_commit(db_session, repo.repo_id, cid,
861 [prev] if prev else [])
862 prev = cid
863 for cid in commits:
864 for i in range(3):
865 await _seed_history(db_session, repo.repo_id, cid,
866 [f"mod{i}/file.py::fn"])
867 await db_session.commit()
868 result = await _run(db_session, repo.repo_id, commits[-1])
869 assert result
870
871 @pytest.mark.asyncio
872 async def test_VL_48_result_count_matches_stored_rows(
873 self, db_session: AsyncSession, repo
874 ) -> None:
875 """metadata 'count' always equals len(stored rows)."""
876 commits = [_cid() for _ in range(4)]
877 prev = None
878 for cid in commits:
879 await _seed_commit(db_session, repo.repo_id, cid,
880 [prev] if prev else [])
881 prev = cid
882 for cid in commits:
883 for i in range(3):
884 await _seed_history(db_session, repo.repo_id, cid,
885 [f"mod{i}/file.py::fn"])
886 await db_session.commit()
887 result = await _run(db_session, repo.repo_id, commits[-1])
888 key, payload = result[0]
889 rows = await _fetch(db_session, repo.repo_id)
890 assert payload["count"] == len(rows)
891
892 @pytest.mark.asyncio
893 async def test_VL_49_bfs_walk_cap_never_exceeded(
894 self, db_session: AsyncSession, repo
895 ) -> None:
896 """commits_analysed never exceeds _MAX_WALK."""
897 provider = VelocityProvider()
898 commits = [_cid() for _ in range(50)]
899 prev = None
900 for cid in commits:
901 await _seed_commit(db_session, repo.repo_id, cid,
902 [prev] if prev else [])
903 prev = cid
904 await _seed_history(db_session, repo.repo_id, commits[0],
905 ["src/a.py::fn"])
906 await db_session.commit()
907 result = await _run(db_session, repo.repo_id, commits[-1])
908 if result:
909 key, payload = result[0]
910 assert payload["commits_analysed"] <= provider._MAX_WALK
911
912 @pytest.mark.asyncio
913 async def test_VL_50_all_extended_columns_non_null(
914 self, db_session: AsyncSession, repo
915 ) -> None:
916 """Every stored row has non-null values for all four extended columns."""
917 provider = VelocityProvider()
918 n = provider._WINDOW + 3
919 commits = [_cid() for _ in range(n)]
920 prev = None
921 for cid in commits:
922 await _seed_commit(db_session, repo.repo_id, cid,
923 [prev] if prev else [])
924 prev = cid
925 for cid in commits:
926 await _seed_history(db_session, repo.repo_id, cid,
927 ["src/a.py::fn"])
928 await db_session.commit()
929 await _run(db_session, repo.repo_id, commits[-1])
930 rows = await _fetch(db_session, repo.repo_id)
931 assert rows, "expected at least one velocity row"
932 for r in rows:
933 assert r.prior_modified is not None
934 assert r.prior_active_commits is not None
935 assert r.window_size is not None
936 assert r.commits_analysed is not None
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 122 days ago