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