gabriel / musehub public
test_file_last_commits.py python
362 lines 15.0 KB
Raw
sha256:763eb2cb8675073b84c19345b27586d2ed939a9aee97c5479b69f502f1a70eff fix(tests): update test suite to match current implementation Sonnet 4.6 patch 119 days ago
1 """
2 Tests for materialized per-file last-commit data.
3
4 FLC1 — get_file_last_commits returns empty dict when table has no rows for repo
5 FLC2 — compute_and_store_file_last_commits populates table from commit history
6 FLC3 — get_file_last_commits reads from table (single query, no blob decode)
7 FLC4 — file changed in newer commit → attributed to newer commit
8 FLC5 — file unchanged since first commit → attributed to oldest commit
9 FLC6 — directory path returns the commit of its most-recently-changed file
10 FLC7 — compute is idempotent: running twice gives same result, no duplicates
11 FLC8 — only paths requested are returned (no extra rows leaked)
12 FLC9 — unknown paths return no entry (no crash)
13 FLC10 — second push updates attribution for files that changed
14 """
15 from __future__ import annotations
16
17 import hashlib
18 from collections.abc import Mapping
19 from datetime import datetime, timezone, timedelta
20
21 import pytest
22 from sqlalchemy.ext.asyncio import AsyncSession
23 from sqlalchemy import select
24
25 from musehub.db import musehub_models as db
26 from tests.factories import create_repo, create_branch
27
28
29 # ---------------------------------------------------------------------------
30 # Helpers
31 # ---------------------------------------------------------------------------
32
33 def _utc(offset_days: int = 0) -> datetime:
34 return datetime.now(tz=timezone.utc) + timedelta(days=offset_days)
35
36
37 def _snap_id(name: str) -> str:
38 return "sha256:" + hashlib.sha256(name.encode()).hexdigest()
39
40
41 def _commit_id(name: str) -> str:
42 return "sha256:" + hashlib.sha256(f"commit:{name}".encode()).hexdigest()
43
44
45 def _obj_id(name: str) -> str:
46 return "sha256:" + hashlib.sha256(f"obj:{name}".encode()).hexdigest()
47
48
49 async def _add_snapshot(session: AsyncSession, repo_id: str, snap_name: str, manifest: Mapping[str, str]) -> str:
50 """Store a snapshot with manifest blob."""
51 import msgpack
52 snap_id = _snap_id(snap_name)
53 existing = await session.get(db.MusehubSnapshot, snap_id)
54 if existing is None:
55 session.add(db.MusehubSnapshot(
56 snapshot_id=snap_id,
57 repo_id=repo_id,
58 directories=[],
59 manifest_blob=msgpack.packb(manifest, use_bin_type=True),
60 entry_count=len(manifest),
61 created_at=_utc(),
62 ))
63 await session.flush()
64 return snap_id
65
66
67 async def _add_commit(
68 session: AsyncSession,
69 repo_id: str,
70 name: str,
71 snap_name: str,
72 manifest: dict[str, str],
73 branch: str = "main",
74 ts_offset: int = 0,
75 agent_id: str = "",
76 message: str = "",
77 ) -> db.MusehubCommit:
78 snap_id = await _add_snapshot(session, repo_id, snap_name, manifest)
79 commit = db.MusehubCommit(
80 commit_id=_commit_id(name),
81 repo_id=repo_id,
82 branch=branch,
83 parent_ids=[],
84 message=message or f"commit {name}",
85 author="gabriel",
86 timestamp=_utc(ts_offset),
87 snapshot_id=snap_id,
88 agent_id=agent_id,
89 )
90 session.add(commit)
91 await session.flush()
92 return commit
93
94
95 # ---------------------------------------------------------------------------
96 # FLC1 — empty table → empty result
97 # ---------------------------------------------------------------------------
98
99 @pytest.mark.asyncio
100 async def test_flc1_empty_table_returns_empty(db_session: AsyncSession) -> None:
101 """FLC1: no rows in table → empty dict, no crash."""
102 from musehub.services.musehub_repository import get_file_last_commits
103
104 repo = await create_repo(db_session)
105 result = await get_file_last_commits(db_session, repo.repo_id, ["README.md"], ref="main")
106 assert result == {}
107
108
109 # ---------------------------------------------------------------------------
110 # FLC2 — compute populates table
111 # ---------------------------------------------------------------------------
112
113 @pytest.mark.asyncio
114 async def test_flc2_compute_populates_table(db_session: AsyncSession) -> None:
115 """FLC2: compute_and_store_file_last_commits writes rows to musehub_file_last_commits."""
116 from musehub.services.file_last_commits import compute_and_store_file_last_commits
117
118 repo = await create_repo(db_session)
119 branch = await create_branch(db_session, repo.repo_id, name="main")
120
121 manifest = {"README.md": _obj_id("readme"), "src/app.py": _obj_id("app")}
122 commit = await _add_commit(db_session, repo.repo_id, "c1", "s1", manifest)
123 branch.head_commit_id = commit.commit_id
124 await db_session.flush()
125
126 await compute_and_store_file_last_commits(db_session, repo.repo_id, "main", commit.commit_id)
127 await db_session.flush()
128
129 rows = (await db_session.execute(
130 select(db.MusehubFileLastCommit).where(
131 db.MusehubFileLastCommit.repo_id == repo.repo_id,
132 db.MusehubFileLastCommit.branch == "main",
133 )
134 )).scalars().all()
135
136 paths = {r.path for r in rows}
137 assert "README.md" in paths
138 assert "src/app.py" in paths
139
140
141 # ---------------------------------------------------------------------------
142 # FLC3 — get_file_last_commits reads from table
143 # ---------------------------------------------------------------------------
144
145 @pytest.mark.asyncio
146 async def test_flc3_reads_from_table(db_session: AsyncSession) -> None:
147 """FLC3: after compute, get_file_last_commits returns data without blob decode."""
148 from musehub.services.file_last_commits import compute_and_store_file_last_commits
149 from musehub.services.musehub_repository import get_file_last_commits
150
151 repo = await create_repo(db_session)
152 branch = await create_branch(db_session, repo.repo_id, name="main")
153
154 manifest = {"README.md": _obj_id("readme-v1")}
155 commit = await _add_commit(db_session, repo.repo_id, "c1", "s1", manifest, message="feat: init")
156 branch.head_commit_id = commit.commit_id
157 await db_session.flush()
158
159 await compute_and_store_file_last_commits(db_session, repo.repo_id, "main", commit.commit_id)
160 await db_session.flush()
161
162 result = await get_file_last_commits(db_session, repo.repo_id, ["README.md"], ref="main")
163
164 assert "README.md" in result
165 assert result["README.md"]["sha"] == commit.commit_id
166 assert result["README.md"]["message"] == "feat: init"
167
168
169 # ---------------------------------------------------------------------------
170 # FLC4 — changed file attributed to newer commit
171 # ---------------------------------------------------------------------------
172
173 @pytest.mark.asyncio
174 async def test_flc4_changed_file_attributed_to_newer_commit(db_session: AsyncSession) -> None:
175 """FLC4: file that changed in commit 2 is attributed to commit 2, not commit 1."""
176 from musehub.services.file_last_commits import compute_and_store_file_last_commits
177 from musehub.services.musehub_repository import get_file_last_commits
178
179 repo = await create_repo(db_session)
180 branch = await create_branch(db_session, repo.repo_id, name="main")
181
182 manifest_v1 = {"README.md": _obj_id("readme-v1"), "src/app.py": _obj_id("app-v1")}
183 manifest_v2 = {"README.md": _obj_id("readme-v2"), "src/app.py": _obj_id("app-v1")}
184
185 c1 = await _add_commit(db_session, repo.repo_id, "c1", "s1", manifest_v1, ts_offset=-1)
186 c2 = await _add_commit(db_session, repo.repo_id, "c2", "s2", manifest_v2, ts_offset=0)
187 branch.head_commit_id = c2.commit_id
188 await db_session.flush()
189
190 await compute_and_store_file_last_commits(db_session, repo.repo_id, "main", c2.commit_id)
191 await db_session.flush()
192
193 result = await get_file_last_commits(db_session, repo.repo_id, ["README.md", "src/app.py"], ref="main")
194
195 assert result["README.md"]["sha"] == c2.commit_id
196 assert result["src/app.py"]["sha"] == c1.commit_id
197
198
199 # ---------------------------------------------------------------------------
200 # FLC5 — unchanged file attributed to first commit
201 # ---------------------------------------------------------------------------
202
203 @pytest.mark.asyncio
204 async def test_flc5_unchanged_file_attributed_to_oldest_commit(db_session: AsyncSession) -> None:
205 """FLC5: file never changed is attributed to the oldest commit in the walk."""
206 from musehub.services.file_last_commits import compute_and_store_file_last_commits
207 from musehub.services.musehub_repository import get_file_last_commits
208
209 repo = await create_repo(db_session)
210 branch = await create_branch(db_session, repo.repo_id, name="main")
211
212 oid = _obj_id("stable")
213 c1 = await _add_commit(db_session, repo.repo_id, "c1", "s1", {"stable.py": oid}, ts_offset=-2)
214 c2 = await _add_commit(db_session, repo.repo_id, "c2", "s2", {"stable.py": oid}, ts_offset=-1)
215 c3 = await _add_commit(db_session, repo.repo_id, "c3", "s3", {"stable.py": oid}, ts_offset=0)
216 branch.head_commit_id = c3.commit_id
217 await db_session.flush()
218
219 await compute_and_store_file_last_commits(db_session, repo.repo_id, "main", c3.commit_id)
220 await db_session.flush()
221
222 result = await get_file_last_commits(db_session, repo.repo_id, ["stable.py"], ref="main")
223 assert result["stable.py"]["sha"] == c1.commit_id
224
225
226 # ---------------------------------------------------------------------------
227 # FLC6 — directory path → most-recently-changed file in dir
228 # ---------------------------------------------------------------------------
229
230 @pytest.mark.asyncio
231 async def test_flc6_directory_attributed_to_most_recent_child_commit(db_session: AsyncSession) -> None:
232 """FLC6: directory path resolves to the commit that last touched any file inside it."""
233 from musehub.services.file_last_commits import compute_and_store_file_last_commits
234 from musehub.services.musehub_repository import get_file_last_commits
235
236 repo = await create_repo(db_session)
237 branch = await create_branch(db_session, repo.repo_id, name="main")
238
239 m1 = {"src/a.py": _obj_id("a-v1"), "src/b.py": _obj_id("b-v1")}
240 m2 = {"src/a.py": _obj_id("a-v1"), "src/b.py": _obj_id("b-v2")}
241
242 c1 = await _add_commit(db_session, repo.repo_id, "c1", "s1", m1, ts_offset=-1)
243 c2 = await _add_commit(db_session, repo.repo_id, "c2", "s2", m2, ts_offset=0)
244 branch.head_commit_id = c2.commit_id
245 await db_session.flush()
246
247 await compute_and_store_file_last_commits(db_session, repo.repo_id, "main", c2.commit_id)
248 await db_session.flush()
249
250 result = await get_file_last_commits(db_session, repo.repo_id, ["src"], ref="main")
251 assert result["src"]["sha"] == c2.commit_id
252
253
254 # ---------------------------------------------------------------------------
255 # FLC7 — idempotent
256 # ---------------------------------------------------------------------------
257
258 @pytest.mark.asyncio
259 async def test_flc7_compute_is_idempotent(db_session: AsyncSession) -> None:
260 """FLC7: running compute twice yields same result, no duplicate rows."""
261 from musehub.services.file_last_commits import compute_and_store_file_last_commits
262
263 repo = await create_repo(db_session)
264 branch = await create_branch(db_session, repo.repo_id, name="main")
265
266 commit = await _add_commit(db_session, repo.repo_id, "c1", "s1", {"a.py": _obj_id("a")})
267 branch.head_commit_id = commit.commit_id
268 await db_session.flush()
269
270 await compute_and_store_file_last_commits(db_session, repo.repo_id, "main", commit.commit_id)
271 await compute_and_store_file_last_commits(db_session, repo.repo_id, "main", commit.commit_id)
272 await db_session.flush()
273
274 rows = (await db_session.execute(
275 select(db.MusehubFileLastCommit).where(
276 db.MusehubFileLastCommit.repo_id == repo.repo_id,
277 db.MusehubFileLastCommit.branch == "main",
278 db.MusehubFileLastCommit.path == "a.py",
279 )
280 )).scalars().all()
281 assert len(rows) == 1
282
283
284 # ---------------------------------------------------------------------------
285 # FLC8 — only requested paths returned
286 # ---------------------------------------------------------------------------
287
288 @pytest.mark.asyncio
289 async def test_flc8_only_requested_paths_returned(db_session: AsyncSession) -> None:
290 """FLC8: get_file_last_commits returns only the paths asked for."""
291 from musehub.services.file_last_commits import compute_and_store_file_last_commits
292 from musehub.services.musehub_repository import get_file_last_commits
293
294 repo = await create_repo(db_session)
295 branch = await create_branch(db_session, repo.repo_id, name="main")
296
297 manifest = {"a.py": _obj_id("a"), "b.py": _obj_id("b"), "c.py": _obj_id("c")}
298 commit = await _add_commit(db_session, repo.repo_id, "c1", "s1", manifest)
299 branch.head_commit_id = commit.commit_id
300 await db_session.flush()
301
302 await compute_and_store_file_last_commits(db_session, repo.repo_id, "main", commit.commit_id)
303 await db_session.flush()
304
305 result = await get_file_last_commits(db_session, repo.repo_id, ["a.py"], ref="main")
306 assert set(result.keys()) == {"a.py"}
307
308
309 # ---------------------------------------------------------------------------
310 # FLC9 — unknown paths return no entry
311 # ---------------------------------------------------------------------------
312
313 @pytest.mark.asyncio
314 async def test_flc9_unknown_paths_not_in_result(db_session: AsyncSession) -> None:
315 """FLC9: paths not in any snapshot are silently absent from result."""
316 from musehub.services.file_last_commits import compute_and_store_file_last_commits
317 from musehub.services.musehub_repository import get_file_last_commits
318
319 repo = await create_repo(db_session)
320 branch = await create_branch(db_session, repo.repo_id, name="main")
321
322 commit = await _add_commit(db_session, repo.repo_id, "c1", "s1", {"real.py": _obj_id("r")})
323 branch.head_commit_id = commit.commit_id
324 await db_session.flush()
325
326 await compute_and_store_file_last_commits(db_session, repo.repo_id, "main", commit.commit_id)
327 await db_session.flush()
328
329 result = await get_file_last_commits(db_session, repo.repo_id, ["ghost.py"], ref="main")
330 assert "ghost.py" not in result
331
332
333 # ---------------------------------------------------------------------------
334 # FLC10 — second push updates changed files
335 # ---------------------------------------------------------------------------
336
337 @pytest.mark.asyncio
338 async def test_flc10_second_push_updates_changed_files(db_session: AsyncSession) -> None:
339 """FLC10: after a second push, files that changed point to the new commit."""
340 from musehub.services.file_last_commits import compute_and_store_file_last_commits
341 from musehub.services.musehub_repository import get_file_last_commits
342
343 repo = await create_repo(db_session)
344 branch = await create_branch(db_session, repo.repo_id, name="main")
345
346 m1 = {"README.md": _obj_id("readme-v1")}
347 m2 = {"README.md": _obj_id("readme-v2")}
348
349 c1 = await _add_commit(db_session, repo.repo_id, "c1", "s1", m1, ts_offset=-1)
350 branch.head_commit_id = c1.commit_id
351 await db_session.flush()
352 await compute_and_store_file_last_commits(db_session, repo.repo_id, "main", c1.commit_id)
353 await db_session.flush()
354
355 c2 = await _add_commit(db_session, repo.repo_id, "c2", "s2", m2, ts_offset=0)
356 branch.head_commit_id = c2.commit_id
357 await db_session.flush()
358 await compute_and_store_file_last_commits(db_session, repo.repo_id, "main", c2.commit_id)
359 await db_session.flush()
360
361 result = await get_file_last_commits(db_session, repo.repo_id, ["README.md"], ref="main")
362 assert result["README.md"]["sha"] == c2.commit_id
File History 1 commit
sha256:763eb2cb8675073b84c19345b27586d2ed939a9aee97c5479b69f502f1a70eff fix(tests): update test suite to match current implementation Sonnet 4.6 patch 119 days ago