gabriel / musehub public
test_repo_card_state.py python
189 lines 7.5 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago
1 """
2 Tier 5 — State tests for enrich_repo_cards().
3
4 State tests verify that enrichment results are stable across repeated calls and
5 that incremental data changes (new commits, new dead symbols, resolved breakage)
6 are correctly reflected in the next enrichment — no stale caches, no phantom
7 rows from prior calls.
8
9 Test IDs
10 --------
11 T500 — two successive calls with no data change return identical results
12 T501 — adding a commit between calls increases pulse_buckets total count
13 T502 — adding a dead symbol between calls increments dead_count by 1
14 T503 — removing breakage meta between calls transitions health risk → clean
15 T504 — adding an agent commit shifts autonomy_pct from 0 to 50
16 T505 — replacing hottest symbol (higher churn) swaps hottest_symbol in next call
17 """
18 from __future__ import annotations
19
20 from datetime import datetime, timezone
21
22 import pytest
23 from sqlalchemy.ext.asyncio import AsyncSession
24 from sqlalchemy import text
25
26 from musehub.db.musehub_models import (
27 MusehubIntelBreakageMeta,
28 MusehubIntelDead,
29 MusehubSymbolIntel,
30 )
31 from musehub.services.repo_card_enrichment import enrich_repo_cards
32 from tests.factories import create_commit, create_repo
33
34
35 def _utc_now() -> datetime:
36 return datetime.now(tz=timezone.utc)
37
38
39 # ---------------------------------------------------------------------------
40 # T500 — stability: identical result on two successive calls
41 # ---------------------------------------------------------------------------
42
43 @pytest.mark.asyncio
44 async def test_t500_successive_calls_are_stable(db_session: AsyncSession) -> None:
45 """T500: calling enrich_repo_cards twice with no intervening writes is idempotent."""
46 repo = await create_repo(db_session, visibility="public")
47 db_session.add(MusehubSymbolIntel(
48 repo_id=repo.repo_id, address="src/a.py::fn", churn_30d=5, blast=3
49 ))
50 await db_session.commit()
51
52 result_a = await enrich_repo_cards(db_session, [repo.repo_id])
53 result_b = await enrich_repo_cards(db_session, [repo.repo_id])
54
55 enc_a = result_a[repo.repo_id]
56 enc_b = result_b[repo.repo_id]
57
58 assert enc_a.autonomy_pct == enc_b.autonomy_pct
59 assert enc_a.dead_count == enc_b.dead_count
60 assert enc_a.health_status == enc_b.health_status
61 assert enc_a.hottest_symbol.address == enc_b.hottest_symbol.address
62 assert sum(b.count for b in enc_a.pulse_buckets) == sum(b.count for b in enc_b.pulse_buckets)
63
64
65 # ---------------------------------------------------------------------------
66 # T501 — new commit reflected in pulse after next call
67 # ---------------------------------------------------------------------------
68
69 @pytest.mark.asyncio
70 async def test_t501_new_commit_increases_pulse_total(db_session: AsyncSession) -> None:
71 """T501: inserting a commit between calls increments the pulse total count."""
72 repo = await create_repo(db_session, visibility="public")
73
74 result_before = await enrich_repo_cards(db_session, [repo.repo_id])
75 total_before = sum(b.count for b in result_before[repo.repo_id].pulse_buckets)
76
77 await create_commit(db_session, repo.repo_id, timestamp=_utc_now())
78
79 result_after = await enrich_repo_cards(db_session, [repo.repo_id])
80 total_after = sum(b.count for b in result_after[repo.repo_id].pulse_buckets)
81
82 assert total_after == total_before + 1
83
84
85 # ---------------------------------------------------------------------------
86 # T502 — new dead symbol increments dead_count
87 # ---------------------------------------------------------------------------
88
89 @pytest.mark.asyncio
90 async def test_t502_new_dead_symbol_increments_count(db_session: AsyncSession) -> None:
91 """T502: inserting a high-confidence dead symbol increments dead_count by 1."""
92 repo = await create_repo(db_session, visibility="public")
93
94 result_before = await enrich_repo_cards(db_session, [repo.repo_id])
95 dead_before = result_before[repo.repo_id].dead_count
96
97 db_session.add(MusehubIntelDead(
98 repo_id=repo.repo_id,
99 address="src/old.py::stale_fn",
100 kind="function",
101 confidence="high",
102 ref="main",
103 ))
104 await db_session.commit()
105
106 result_after = await enrich_repo_cards(db_session, [repo.repo_id])
107 dead_after = result_after[repo.repo_id].dead_count
108
109 assert dead_after == dead_before + 1
110
111
112 # ---------------------------------------------------------------------------
113 # T503 — removing breakage meta transitions health risk → clean
114 # ---------------------------------------------------------------------------
115
116 @pytest.mark.asyncio
117 async def test_t503_removing_breakage_transitions_to_clean(db_session: AsyncSession) -> None:
118 """T503: deleting a breakage_meta row transitions health_status from risk to clean."""
119 repo = await create_repo(db_session, visibility="public")
120 db_session.add(MusehubIntelBreakageMeta(
121 repo_id=repo.repo_id,
122 total_issues=2,
123 error_count=2,
124 warning_count=0,
125 file_count=1,
126 ref="main",
127 ))
128 await db_session.commit()
129
130 result_risk = await enrich_repo_cards(db_session, [repo.repo_id])
131 assert result_risk[repo.repo_id].health_status == "risk"
132
133 await db_session.execute(
134 text("DELETE FROM musehub_intel_breakage_meta WHERE repo_id = :rid"),
135 {"rid": repo.repo_id},
136 )
137 await db_session.commit()
138
139 result_clean = await enrich_repo_cards(db_session, [repo.repo_id])
140 assert result_clean[repo.repo_id].health_status == "clean"
141
142
143 # ---------------------------------------------------------------------------
144 # T504 — adding agent commit shifts autonomy from 0 to 50
145 # ---------------------------------------------------------------------------
146
147 @pytest.mark.asyncio
148 async def test_t504_agent_commit_shifts_autonomy(db_session: AsyncSession) -> None:
149 """T504: adding one agent commit to a one-human-commit repo shifts autonomy to 50%."""
150 repo = await create_repo(db_session, visibility="public")
151 human_commit = await create_commit(db_session, repo.repo_id, timestamp=_utc_now())
152
153 result_before = await enrich_repo_cards(db_session, [repo.repo_id])
154 assert result_before[repo.repo_id].autonomy_pct == 0
155
156 agent_commit = await create_commit(db_session, repo.repo_id, timestamp=_utc_now())
157 await db_session.execute(
158 text("UPDATE musehub_commits SET agent_id = 'claude-code' WHERE commit_id = :cid"),
159 {"cid": agent_commit.commit_id},
160 )
161 await db_session.commit()
162
163 result_after = await enrich_repo_cards(db_session, [repo.repo_id])
164 assert result_after[repo.repo_id].autonomy_pct == 50
165
166
167 # ---------------------------------------------------------------------------
168 # T505 — replacing hottest symbol swaps result in next call
169 # ---------------------------------------------------------------------------
170
171 @pytest.mark.asyncio
172 async def test_t505_higher_churn_replaces_hottest(db_session: AsyncSession) -> None:
173 """T505: inserting a symbol with higher churn replaces the hottest in the next call."""
174 repo = await create_repo(db_session, visibility="public")
175 db_session.add(MusehubSymbolIntel(
176 repo_id=repo.repo_id, address="src/a.py::slow_fn", churn_30d=10, blast=0
177 ))
178 await db_session.commit()
179
180 result_before = await enrich_repo_cards(db_session, [repo.repo_id])
181 assert result_before[repo.repo_id].hottest_symbol.address == "src/a.py::slow_fn"
182
183 db_session.add(MusehubSymbolIntel(
184 repo_id=repo.repo_id, address="src/b.py::hot_fn", churn_30d=999, blast=0
185 ))
186 await db_session.commit()
187
188 result_after = await enrich_repo_cards(db_session, [repo.repo_id])
189 assert result_after[repo.repo_id].hottest_symbol.address == "src/b.py::hot_fn"
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago