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