""" Tier 5 — State tests for enrich_repo_cards(). State tests verify that enrichment results are stable across repeated calls and that incremental data changes (new commits, new dead symbols, resolved breakage) are correctly reflected in the next enrichment — no stale caches, no phantom rows from prior calls. Test IDs -------- T500 — two successive calls with no data change return identical results T501 — adding a commit between calls increases pulse_buckets total count T502 — adding a dead symbol between calls increments dead_count by 1 T503 — removing breakage meta between calls transitions health risk → clean T504 — adding an agent commit shifts autonomy_pct from 0 to 50 T505 — replacing hottest symbol (higher churn) swaps hottest_symbol in next call """ from __future__ import annotations from datetime import datetime, timezone import pytest from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import text from musehub.db.musehub_intel_models import MusehubIntelBreakageMeta, MusehubIntelDead, MusehubSymbolIntel from musehub.services.repo_card_enrichment import enrich_repo_cards from tests.factories import create_commit, create_repo def _utc_now() -> datetime: return datetime.now(tz=timezone.utc) # --------------------------------------------------------------------------- # T500 — stability: identical result on two successive calls # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_t500_successive_calls_are_stable(db_session: AsyncSession) -> None: """T500: calling enrich_repo_cards twice with no intervening writes is idempotent.""" repo = await create_repo(db_session, visibility="public") db_session.add(MusehubSymbolIntel( repo_id=repo.repo_id, address="src/a.py::fn", churn_30d=5, blast=3 )) await db_session.commit() result_a = await enrich_repo_cards(db_session, [repo.repo_id]) result_b = await enrich_repo_cards(db_session, [repo.repo_id]) enc_a = result_a[repo.repo_id] enc_b = result_b[repo.repo_id] assert enc_a.autonomy_pct == enc_b.autonomy_pct assert enc_a.dead_count == enc_b.dead_count assert enc_a.health_status == enc_b.health_status assert enc_a.hottest_symbol.address == enc_b.hottest_symbol.address assert sum(b.count for b in enc_a.pulse_buckets) == sum(b.count for b in enc_b.pulse_buckets) # --------------------------------------------------------------------------- # T501 — new commit reflected in pulse after next call # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_t501_new_commit_increases_pulse_total(db_session: AsyncSession) -> None: """T501: inserting a commit between calls increments the pulse total count.""" repo = await create_repo(db_session, visibility="public") result_before = await enrich_repo_cards(db_session, [repo.repo_id]) total_before = sum(b.count for b in result_before[repo.repo_id].pulse_buckets) await create_commit(db_session, repo.repo_id, timestamp=_utc_now()) result_after = await enrich_repo_cards(db_session, [repo.repo_id]) total_after = sum(b.count for b in result_after[repo.repo_id].pulse_buckets) assert total_after == total_before + 1 # --------------------------------------------------------------------------- # T502 — new dead symbol increments dead_count # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_t502_new_dead_symbol_increments_count(db_session: AsyncSession) -> None: """T502: inserting a high-confidence dead symbol increments dead_count by 1.""" repo = await create_repo(db_session, visibility="public") result_before = await enrich_repo_cards(db_session, [repo.repo_id]) dead_before = result_before[repo.repo_id].dead_count db_session.add(MusehubIntelDead( repo_id=repo.repo_id, address="src/old.py::stale_fn", kind="function", confidence="high", ref="main", )) await db_session.commit() result_after = await enrich_repo_cards(db_session, [repo.repo_id]) dead_after = result_after[repo.repo_id].dead_count assert dead_after == dead_before + 1 # --------------------------------------------------------------------------- # T503 — removing breakage meta transitions health risk → clean # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_t503_removing_breakage_transitions_to_clean(db_session: AsyncSession) -> None: """T503: deleting a breakage_meta row transitions health_status from risk to clean.""" repo = await create_repo(db_session, visibility="public") db_session.add(MusehubIntelBreakageMeta( repo_id=repo.repo_id, total_issues=2, error_count=2, warning_count=0, file_count=1, ref="main", )) await db_session.commit() result_risk = await enrich_repo_cards(db_session, [repo.repo_id]) assert result_risk[repo.repo_id].health_status == "risk" await db_session.execute( text("DELETE FROM musehub_intel_breakage_meta WHERE repo_id = :rid"), {"rid": repo.repo_id}, ) await db_session.commit() result_clean = await enrich_repo_cards(db_session, [repo.repo_id]) assert result_clean[repo.repo_id].health_status == "clean" # --------------------------------------------------------------------------- # T504 — adding agent commit shifts autonomy from 0 to 50 # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_t504_agent_commit_shifts_autonomy(db_session: AsyncSession) -> None: """T504: adding one agent commit to a one-human-commit repo shifts autonomy to 50%.""" repo = await create_repo(db_session, visibility="public") human_commit = await create_commit(db_session, repo.repo_id, timestamp=_utc_now()) result_before = await enrich_repo_cards(db_session, [repo.repo_id]) assert result_before[repo.repo_id].autonomy_pct == 0 agent_commit = await create_commit(db_session, repo.repo_id, timestamp=_utc_now()) await db_session.execute( text("UPDATE musehub_commits SET agent_id = 'claude-code' WHERE commit_id = :cid"), {"cid": agent_commit.commit_id}, ) await db_session.commit() result_after = await enrich_repo_cards(db_session, [repo.repo_id]) assert result_after[repo.repo_id].autonomy_pct == 50 # --------------------------------------------------------------------------- # T505 — replacing hottest symbol swaps result in next call # --------------------------------------------------------------------------- @pytest.mark.asyncio async def test_t505_higher_churn_replaces_hottest(db_session: AsyncSession) -> None: """T505: inserting a symbol with higher churn replaces the hottest in the next call.""" repo = await create_repo(db_session, visibility="public") db_session.add(MusehubSymbolIntel( repo_id=repo.repo_id, address="src/a.py::slow_fn", churn_30d=10, blast=0 )) await db_session.commit() result_before = await enrich_repo_cards(db_session, [repo.repo_id]) assert result_before[repo.repo_id].hottest_symbol.address == "src/a.py::slow_fn" db_session.add(MusehubSymbolIntel( repo_id=repo.repo_id, address="src/b.py::hot_fn", churn_30d=999, blast=0 )) await db_session.commit() result_after = await enrich_repo_cards(db_session, [repo.repo_id]) assert result_after[repo.repo_id].hottest_symbol.address == "src/b.py::hot_fn"