gabriel / musehub public
test_mist_phase5_profile_canvas.py python
272 lines 10.1 KB
Raw
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor ⚠ breaking 142 days ago
1 """Phase 5 TDD: Profile activity canvas — Mist domain grid.
2
3 domain_id in musehub_repos is a plain string label ("mist", "code", …) or NULL.
4 No musehub_domains join or sha256-hash domain IDs are involved.
5
6 These tests require a running PostgreSQL test DB (port 5434) — same as phases 1–4.
7 """
8 from __future__ import annotations
9
10 import uuid
11 from datetime import datetime, timedelta, timezone
12
13 import pytest
14 from sqlalchemy.ext.asyncio import AsyncSession
15
16 from musehub.db import musehub_models as db
17 from musehub.core.genesis import compute_identity_id, compute_repo_id
18
19
20 # ---------------------------------------------------------------------------
21 # Seed helpers
22 # ---------------------------------------------------------------------------
23
24 def _handle() -> str:
25 return f"mist_canvas_{uuid.uuid4().hex[:8]}"
26
27
28 def _make_repo(handle: str, slug: str, domain_id: str, ts: datetime) -> db.MusehubRepo:
29 owner_id = compute_identity_id(handle.encode())
30 repo_id = compute_repo_id(owner_id, slug, domain_id, ts.isoformat())
31 return db.MusehubRepo(
32 repo_id=repo_id,
33 name=slug,
34 owner=handle,
35 slug=slug,
36 visibility="public",
37 owner_user_id=owner_id,
38 domain_id=domain_id,
39 description="",
40 tags=[],
41 created_at=ts,
42 )
43
44
45 async def _seed_mist_repo_with_commits(
46 session: AsyncSession,
47 handle: str,
48 n_commits: int = 2,
49 days_ago: int = 3,
50 ) -> tuple[db.MusehubRepo, list[db.MusehubCommit]]:
51 """Create a mist-domain repo with n_commits."""
52 ts = datetime.now(tz=timezone.utc) - timedelta(days=days_ago)
53 repo = _make_repo(handle, f"mist-proj-{uuid.uuid4().hex[:8]}", "mist", ts)
54 session.add(repo)
55
56 commits = []
57 for i in range(n_commits):
58 c = db.MusehubCommit(
59 commit_id=f"sha256:{uuid.uuid4().hex:0<64}",
60 repo_id=repo.repo_id,
61 branch="main",
62 parent_ids=[],
63 author=handle,
64 message=f"commit {i}",
65 timestamp=ts - timedelta(hours=i),
66 )
67 session.add(c)
68 commits.append(c)
69
70 await session.flush()
71 return repo, commits
72
73
74 async def _seed_mist_repo_no_commits(
75 session: AsyncSession,
76 handle: str,
77 ) -> db.MusehubRepo:
78 """Create a mist-domain repo with no commits."""
79 ts = datetime.now(tz=timezone.utc) - timedelta(days=10)
80 repo = _make_repo(handle, f"empty-mist-{uuid.uuid4().hex[:8]}", "mist", ts)
81 session.add(repo)
82 await session.flush()
83 return repo
84
85
86 # ---------------------------------------------------------------------------
87 # 1. build_activity_canvas includes "mist" domain
88 # ---------------------------------------------------------------------------
89
90 class TestMistCanvasInclusion:
91 @pytest.mark.asyncio
92 async def test_build_activity_canvas_includes_mist_domain(
93 self, db_session: AsyncSession
94 ) -> None:
95 """build_activity_canvas must return an entry with domain='mist'."""
96 from musehub.services.musehub_profile import build_activity_canvas
97
98 handle = _handle()
99 await _seed_mist_repo_with_commits(db_session, handle, n_commits=2)
100
101 domains = await build_activity_canvas(db_session, handle)
102 domain_names = [d.domain for d in domains]
103 assert "mist" in domain_names, (
104 f"Expected 'mist' in activity canvas domains; got {domain_names}"
105 )
106
107 @pytest.mark.asyncio
108 async def test_mist_domain_grid_has_correct_length(
109 self, db_session: AsyncSession
110 ) -> None:
111 """The mist domain grid must be 364 integers (52 weeks × 7 days)."""
112 from musehub.services.musehub_profile import build_activity_canvas, _GRID_DAYS
113
114 handle = _handle()
115 await _seed_mist_repo_with_commits(db_session, handle, n_commits=1)
116
117 domains = await build_activity_canvas(db_session, handle)
118 mist = next((d for d in domains if d.domain == "mist"), None)
119 assert mist is not None
120 assert len(mist.grid) == _GRID_DAYS, (
121 f"Expected grid of {_GRID_DAYS} integers, got {len(mist.grid)}"
122 )
123
124 @pytest.mark.asyncio
125 async def test_mist_domain_total_reflects_commits(
126 self, db_session: AsyncSession
127 ) -> None:
128 """total on the mist domain entry must be >= number of commits seeded."""
129 from musehub.services.musehub_profile import build_activity_canvas
130
131 handle = _handle()
132 await _seed_mist_repo_with_commits(db_session, handle, n_commits=3)
133
134 domains = await build_activity_canvas(db_session, handle)
135 mist = next((d for d in domains if d.domain == "mist"), None)
136 assert mist is not None
137 assert mist.total >= 3, (
138 f"Expected mist.total >= 3 for 3 commits; got {mist.total}"
139 )
140
141
142 # ---------------------------------------------------------------------------
143 # 2. Empty mist repo → zero grid, no crash
144 # ---------------------------------------------------------------------------
145
146 class TestMistCanvasEmptyRepo:
147 @pytest.mark.asyncio
148 async def test_empty_mist_repo_not_in_canvas(
149 self, db_session: AsyncSession
150 ) -> None:
151 """A mist repo with no commits is excluded — canvas only shows active domains."""
152 from musehub.services.musehub_profile import build_activity_canvas
153
154 handle = _handle()
155 await _seed_mist_repo_no_commits(db_session, handle)
156
157 domains = await build_activity_canvas(db_session, handle)
158 mist = next((d for d in domains if d.domain == "mist"), None)
159 assert mist is None, "mist domain must be absent when there are no commits"
160
161 @pytest.mark.asyncio
162 async def test_no_mist_repos_not_in_canvas(
163 self, db_session: AsyncSession
164 ) -> None:
165 """A handle with no mist repos at all must not get a mist entry."""
166 from musehub.services.musehub_profile import build_activity_canvas
167
168 handle = _handle() # no repos seeded at all
169
170 domains = await build_activity_canvas(db_session, handle)
171 mist = next((d for d in domains if d.domain == "mist"), None)
172 assert mist is None, "mist domain must not appear when the user has no mist repos"
173
174
175 # ---------------------------------------------------------------------------
176 # 3. _build_domain_commit_grid isolation
177 # ---------------------------------------------------------------------------
178
179 class TestBuildMistVcsGrid:
180 @pytest.mark.asyncio
181 async def test_domain_commit_grid_is_importable(self) -> None:
182 """_build_domain_commit_grid must be defined in musehub_profile."""
183 import musehub.services.musehub_profile as _mod
184 assert hasattr(_mod, "_build_domain_commit_grid"), (
185 "_build_domain_commit_grid must be defined in musehub_profile"
186 )
187
188 @pytest.mark.asyncio
189 async def test_domain_commit_grid_counts_only_target_domain(
190 self, db_session: AsyncSession
191 ) -> None:
192 """_build_domain_commit_grid must NOT count commits from other domains."""
193 from musehub.services.musehub_profile import _build_domain_commit_grid, _utc_today
194
195 handle = _handle()
196 ts = datetime.now(tz=timezone.utc)
197 cutoff = ts - timedelta(weeks=52)
198 today = _utc_today()
199
200 # Seed a code-domain repo with 5 commits
201 owner_id = compute_identity_id(handle.encode())
202 code_repo = _make_repo(handle, f"code-proj-{uuid.uuid4().hex[:8]}", "code", ts)
203 db_session.add(code_repo)
204 for i in range(5):
205 db_session.add(db.MusehubCommit(
206 commit_id=f"sha256:{uuid.uuid4().hex:0<64}",
207 repo_id=code_repo.repo_id,
208 branch="main",
209 parent_ids=[],
210 author=handle,
211 message=f"code {i}",
212 timestamp=ts - timedelta(hours=i),
213 ))
214
215 # Seed a mist-domain repo with 2 commits
216 await _seed_mist_repo_with_commits(db_session, handle, n_commits=2)
217 await db_session.flush()
218
219 grid = await _build_domain_commit_grid(db_session, handle, today, cutoff, "mist")
220 total = sum(grid)
221 assert total == 2, (
222 f"_build_domain_commit_grid('mist') must count only mist-domain commits; "
223 f"got total={total} (expected 2)"
224 )
225
226
227 # ---------------------------------------------------------------------------
228 # 4. Regression — existing domains still present
229 # ---------------------------------------------------------------------------
230
231 class TestMistCanvasRegression:
232 @pytest.mark.asyncio
233 async def test_mist_and_code_both_shown_when_active(
234 self, db_session: AsyncSession
235 ) -> None:
236 """When a user has active mist and code repos, both domains appear."""
237 from musehub.services.musehub_profile import build_activity_canvas
238
239 handle = _handle()
240 ts = datetime.now(tz=timezone.utc) - timedelta(days=2)
241
242 # Seed a code repo with commits
243 code_repo = _make_repo(handle, f"code-{uuid.uuid4().hex[:8]}", "code", ts)
244 db_session.add(code_repo)
245 db_session.add(db.MusehubCommit(
246 commit_id=f"sha256:{uuid.uuid4().hex:0<64}",
247 repo_id=code_repo.repo_id, branch="main", parent_ids=[],
248 author=handle, message="init", timestamp=ts,
249 ))
250
251 # Seed a mist repo with commits
252 await _seed_mist_repo_with_commits(db_session, handle, n_commits=1)
253 await db_session.commit()
254
255 domains = await build_activity_canvas(db_session, handle)
256 domain_names = {d.domain for d in domains}
257 assert "mist" in domain_names, f"mist missing from {domain_names}"
258 assert "code" in domain_names, f"code missing from {domain_names}"
259
260 @pytest.mark.asyncio
261 async def test_canvas_only_includes_active_domains(
262 self, db_session: AsyncSession
263 ) -> None:
264 """Canvas returns only domains with real activity — no phantom zero rows."""
265 from musehub.services.musehub_profile import build_activity_canvas
266
267 handle = _handle()
268 # No repos seeded — canvas must be empty (no phantom domains)
269 domains = await build_activity_canvas(db_session, handle)
270 assert domains == [], (
271 f"Expected empty canvas for user with no activity; got {[d.domain for d in domains]}"
272 )
File History 1 commit
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor 142 days ago