gabriel / musehub public
test_per_repo_object_store.py python
570 lines 27.7 KB
Raw
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor ⚠ breaking 143 days ago
1 """Phase 1 — Per-repo directory isolation: 7-tier TDD spec.
2
3 Each repo gets its own object directory under ``musehub_repos_dir``.
4 Objects are no longer globally namespaced across all repos.
5
6 Target layout (Phase 1 — still flat within the repo, sharding comes in Phase 2):
7 /data/repos/<owner>/<slug>/objects/<safe_id>
8
9 API contract:
10 - ``LocalBackend._path(object_id, repo_root=None)``
11 → with repo_root: ``repo_root / "objects" / safe_id``
12 → without repo_root: ``self._root / safe_id`` (backward compat)
13 - ``LocalBackend.put/get/exists/delete/uri_for`` all accept ``repo_root``
14 - ``settings.musehub_repos_dir`` → ``/data/repos``
15 - ``repo_root_for(owner, slug, repos_dir=None)`` → ``Path(<repos_dir>/<owner>/<slug>)``
16
17 All tests here are RED until Phase 1 is implemented.
18 """
19 from __future__ import annotations
20
21 import asyncio
22 import time
23 import uuid
24 from pathlib import Path
25 from unittest.mock import patch
26
27 import pytest
28
29 # ---------------------------------------------------------------------------
30 # Helpers
31 # ---------------------------------------------------------------------------
32
33
34 def _uid() -> str:
35 return str(uuid.uuid4())
36
37
38 def _oid() -> str:
39 """A valid sha256-prefixed object ID."""
40 return f"sha256:{uuid.uuid4().hex * 2}" # 64 hex chars
41
42
43 def _backend(tmp_path: Path, objects_dir: str | None = None) -> "LocalBackend":
44 from musehub.storage.backends import LocalBackend
45 return LocalBackend(objects_dir=str(tmp_path / "global-objects"))
46
47
48 def _repo_root(tmp_path: Path, owner: str = "alice", slug: str = "myrepo") -> Path:
49 root = tmp_path / "repos" / owner / slug
50 root.mkdir(parents=True, exist_ok=True)
51 return root
52
53
54 # ═══════════════════════════════════════════════════════════════════════════════
55 # Tier 1 — Unit (pure logic, no I/O)
56 # ═══════════════════════════════════════════════════════════════════════════════
57
58
59 class TestUnit:
60 """Pure path logic — no filesystem writes."""
61
62 def test_path_with_repo_root_uses_algo_sharded_layout(self, tmp_path: Path) -> None:
63 """_path(oid, repo_root=...) → repo_root/objects/sha256/<2-hex>/<62-hex>"""
64 from musehub.storage.backends import LocalBackend
65 b = LocalBackend(objects_dir=str(tmp_path / "global"))
66 rr = _repo_root(tmp_path)
67 oid = _oid()
68 path = b._path(oid, repo_root=rr)
69 hex_part = oid.removeprefix("sha256:")
70 # algo/shard/rest layout
71 assert path.name == hex_part[2:] # 62-char remainder
72 assert path.parent.name == hex_part[:2] # 2-char shard
73 assert path.parent.parent.name == "sha256"
74 assert path.parent.parent.parent == (rr / "objects").resolve()
75
76 def test_path_without_repo_root_uses_global_root(self, tmp_path: Path) -> None:
77 """_path(oid) without repo_root still works — backward compat."""
78 from musehub.storage.backends import LocalBackend
79 b = LocalBackend(objects_dir=str(tmp_path / "global"))
80 oid = _oid()
81 path = b._path(oid)
82 assert path.parent == b._root.resolve()
83
84 def test_path_with_repo_root_no_colon_on_disk(self, tmp_path: Path) -> None:
85 """The algo/shard/rest layout never puts a colon on disk."""
86 from musehub.storage.backends import LocalBackend
87 b = LocalBackend(objects_dir=str(tmp_path / "global"))
88 rr = _repo_root(tmp_path)
89 oid = _oid()
90 path = b._path(oid, repo_root=rr)
91 assert ":" not in str(path)
92
93 def test_path_two_repos_different_directories(self, tmp_path: Path) -> None:
94 """Objects in different repos resolve to different directories."""
95 from musehub.storage.backends import LocalBackend
96 b = LocalBackend(objects_dir=str(tmp_path / "global"))
97 rr_alice = _repo_root(tmp_path, owner="alice", slug="repo1")
98 rr_bob = _repo_root(tmp_path, owner="bob", slug="repo2")
99 oid = _oid()
100 p_alice = b._path(oid, repo_root=rr_alice)
101 p_bob = b._path(oid, repo_root=rr_bob)
102 assert p_alice != p_bob
103 assert str(p_alice).startswith(str(rr_alice))
104 assert str(p_bob).startswith(str(rr_bob))
105
106 def test_uri_for_with_repo_root_contains_algo_and_objects(self, tmp_path: Path) -> None:
107 """uri_for with repo_root encodes the full algo-namespaced path."""
108 from musehub.storage.backends import LocalBackend
109 b = LocalBackend(objects_dir=str(tmp_path / "global"))
110 rr = _repo_root(tmp_path)
111 oid = _oid()
112 uri = b.uri_for(oid, repo_root=rr)
113 assert uri.startswith("local://")
114 assert "/objects/sha256/" in uri
115
116 def test_uri_for_without_repo_root_uses_global(self, tmp_path: Path) -> None:
117 """uri_for without repo_root still returns global path — backward compat."""
118 from musehub.storage.backends import LocalBackend
119 b = LocalBackend(objects_dir=str(tmp_path / "global"))
120 oid = _oid()
121 uri = b.uri_for(oid)
122 assert uri.startswith("local://")
123
124 def test_path_traversal_in_object_id_raises_with_repo_root(self, tmp_path: Path) -> None:
125 """object_path validates object IDs strictly — traversal raises ValueError."""
126 from musehub.storage.backends import LocalBackend
127 b = LocalBackend(objects_dir=str(tmp_path / "global"))
128 rr = _repo_root(tmp_path)
129 with pytest.raises(ValueError):
130 b._path("../../etc/passwd", repo_root=rr)
131
132 def test_settings_has_musehub_repos_dir(self) -> None:
133 """settings.musehub_repos_dir must exist with default /data/repos."""
134 from musehub.config import settings
135 assert hasattr(settings, "musehub_repos_dir")
136 assert settings.musehub_repos_dir == "/data/repos"
137
138 def test_repo_root_for_returns_correct_path(self, tmp_path: Path) -> None:
139 """repo_root_for(owner, slug) → repos_dir/owner/slug"""
140 from musehub.storage.backends import repo_root_for
141 root = repo_root_for("alice", "muse", repos_dir=str(tmp_path / "repos"))
142 assert root == tmp_path / "repos" / "alice" / "muse"
143
144 def test_repo_root_for_uses_settings_default(self) -> None:
145 """repo_root_for without repos_dir falls back to settings.musehub_repos_dir."""
146 from musehub.storage.backends import repo_root_for
147 from musehub.config import settings
148 root = repo_root_for("alice", "muse")
149 assert root == Path(settings.musehub_repos_dir) / "alice" / "muse"
150
151 def test_path_with_repo_root_same_oid_same_path(self, tmp_path: Path) -> None:
152 """Same object_id + same repo_root always resolves to the same path."""
153 from musehub.storage.backends import LocalBackend
154 b = LocalBackend(objects_dir=str(tmp_path / "global"))
155 rr = _repo_root(tmp_path)
156 oid = _oid()
157 p1 = b._path(oid, repo_root=rr)
158 p2 = b._path(oid, repo_root=rr)
159 assert p1 == p2
160
161
162 # ═══════════════════════════════════════════════════════════════════════════════
163 # Tier 2 — Integration (real filesystem I/O)
164 # ═══════════════════════════════════════════════════════════════════════════════
165
166
167 class TestIntegration:
168 """Real filesystem — put, get, exists, delete with repo_root."""
169
170 async def test_put_with_repo_root_creates_file_in_repo_dir(self, tmp_path: Path) -> None:
171 """put(oid, data, repo_root=...) writes file under repo_root/objects/."""
172 from musehub.storage.backends import LocalBackend
173 b = LocalBackend(objects_dir=str(tmp_path / "global"))
174 rr = _repo_root(tmp_path)
175 oid = _oid()
176 data = b"per-repo data"
177 uri = await b.put(oid, data, repo_root=rr)
178 assert uri.startswith("local://")
179 disk = Path(uri.removeprefix("local://"))
180 assert disk.exists()
181 assert disk.read_bytes() == data
182 assert str(rr) in str(disk)
183
184 async def test_get_with_repo_root_reads_from_repo_dir(self, tmp_path: Path) -> None:
185 """get(oid, repo_root=...) retrieves data written by put with same repo_root."""
186 from musehub.storage.backends import LocalBackend
187 b = LocalBackend(objects_dir=str(tmp_path / "global"))
188 rr = _repo_root(tmp_path)
189 oid = _oid()
190 data = b"read me back"
191 await b.put(oid, data, repo_root=rr)
192 result = await b.get(oid, repo_root=rr)
193 assert result == data
194
195 async def test_exists_with_repo_root_true_after_put(self, tmp_path: Path) -> None:
196 from musehub.storage.backends import LocalBackend
197 b = LocalBackend(objects_dir=str(tmp_path / "global"))
198 rr = _repo_root(tmp_path)
199 oid = _oid()
200 assert await b.exists(oid, repo_root=rr) is False
201 await b.put(oid, b"data", repo_root=rr)
202 assert await b.exists(oid, repo_root=rr) is True
203
204 async def test_delete_with_repo_root_removes_file(self, tmp_path: Path) -> None:
205 from musehub.storage.backends import LocalBackend
206 b = LocalBackend(objects_dir=str(tmp_path / "global"))
207 rr = _repo_root(tmp_path)
208 oid = _oid()
209 await b.put(oid, b"bye", repo_root=rr)
210 assert await b.exists(oid, repo_root=rr) is True
211 await b.delete(oid, repo_root=rr)
212 assert await b.exists(oid, repo_root=rr) is False
213
214 async def test_put_without_repo_root_still_works(self, tmp_path: Path) -> None:
215 """Backward compat: put without repo_root uses global root."""
216 from musehub.storage.backends import LocalBackend
217 b = LocalBackend(objects_dir=str(tmp_path / "global"))
218 oid = _oid()
219 uri = await b.put(oid, b"global data")
220 assert uri.startswith("local://")
221 result = await b.get(oid)
222 assert result == b"global data"
223
224 async def test_two_repos_same_oid_isolated(self, tmp_path: Path) -> None:
225 """Same object_id in two repos is stored independently — no cross-contamination."""
226 from musehub.storage.backends import LocalBackend
227 b = LocalBackend(objects_dir=str(tmp_path / "global"))
228 rr_alice = _repo_root(tmp_path, "alice", "repo1")
229 rr_bob = _repo_root(tmp_path, "bob", "repo2")
230 oid = "sha256:" + "ab" * 32
231
232 await b.put(oid, b"alice content", repo_root=rr_alice)
233 # Bob's repo must not see alice's object
234 assert await b.exists(oid, repo_root=rr_bob) is False
235 # Alice's repo object is still intact
236 assert await b.get(oid, repo_root=rr_alice) == b"alice content"
237
238 async def test_delete_from_one_repo_leaves_other_intact(self, tmp_path: Path) -> None:
239 """Deleting an object from repo A does not affect the same oid in repo B."""
240 from musehub.storage.backends import LocalBackend
241 b = LocalBackend(objects_dir=str(tmp_path / "global"))
242 rr_a = _repo_root(tmp_path, "alice", "repo1")
243 rr_b = _repo_root(tmp_path, "bob", "repo2")
244 oid = "sha256:" + "cd" * 32
245 await b.put(oid, b"shared content", repo_root=rr_a)
246 await b.put(oid, b"shared content", repo_root=rr_b)
247 await b.delete(oid, repo_root=rr_a)
248 assert await b.exists(oid, repo_root=rr_a) is False
249 assert await b.exists(oid, repo_root=rr_b) is True
250
251 async def test_put_creates_algo_shard_dirs_automatically(self, tmp_path: Path) -> None:
252 """put() must create repo_root/objects/sha256/<shard>/ if it does not exist yet."""
253 from musehub.storage.backends import LocalBackend
254 b = LocalBackend(objects_dir=str(tmp_path / "global"))
255 rr = tmp_path / "repos" / "new-user" / "new-repo"
256 # Do NOT mkdir — put() must create the full directory tree
257 oid = _oid()
258 await b.put(oid, b"auto-create dirs", repo_root=rr)
259 hex_part = oid.removeprefix("sha256:")
260 assert (rr / "objects" / "sha256" / hex_part[:2]).is_dir()
261
262 async def test_get_missing_with_repo_root_returns_none(self, tmp_path: Path) -> None:
263 from musehub.storage.backends import LocalBackend
264 b = LocalBackend(objects_dir=str(tmp_path / "global"))
265 rr = _repo_root(tmp_path)
266 result = await b.get("sha256:" + "ff" * 32, repo_root=rr)
267 assert result is None
268
269
270 # ═══════════════════════════════════════════════════════════════════════════════
271 # Tier 3 — End-to-End (full lifecycle via repo_root_for)
272 # ═══════════════════════════════════════════════════════════════════════════════
273
274
275 class TestE2E:
276 """Full lifecycle — put → exists → get → delete using repo_root_for()."""
277
278 async def test_full_lifecycle_via_repo_root_for(self, tmp_path: Path) -> None:
279 from musehub.storage.backends import LocalBackend, repo_root_for
280 b = LocalBackend(objects_dir=str(tmp_path / "global"))
281 rr = repo_root_for("alice", "muse", repos_dir=str(tmp_path / "repos"))
282 oid = _oid()
283 data = b"e2e content"
284
285 uri = await b.put(oid, data, repo_root=rr)
286 assert uri.startswith("local://")
287 assert await b.exists(oid, repo_root=rr) is True
288 assert await b.get(oid, repo_root=rr) == data
289
290 await b.delete(oid, repo_root=rr)
291 assert await b.exists(oid, repo_root=rr) is False
292 assert await b.get(oid, repo_root=rr) is None
293
294 async def test_disk_path_resolves_to_algo_shard_structure(self, tmp_path: Path) -> None:
295 """URI from put() maps to <repo_root>/objects/sha256/<shard>/<rest>."""
296 from musehub.storage.backends import LocalBackend, repo_root_for
297 b = LocalBackend(objects_dir=str(tmp_path / "global"))
298 rr = repo_root_for("gabriel", "musehub", repos_dir=str(tmp_path / "repos"))
299 oid = _oid()
300 uri = await b.put(oid, b"disk resolve", repo_root=rr)
301 disk = Path(uri.removeprefix("local://"))
302 assert disk.is_relative_to(rr)
303 # objects/sha256/<2-char-shard>/<62-char-rest>
304 assert disk.parent.parent.parent == (rr / "objects").resolve()
305 assert disk.parent.parent.name == "sha256"
306
307 async def test_binary_content_preserved(self, tmp_path: Path) -> None:
308 from musehub.storage.backends import LocalBackend, repo_root_for
309 b = LocalBackend(objects_dir=str(tmp_path / "global"))
310 rr = repo_root_for("alice", "muse", repos_dir=str(tmp_path / "repos"))
311 data = bytes(range(256))
312 oid = _oid()
313 await b.put(oid, data, repo_root=rr)
314 assert await b.get(oid, repo_root=rr) == data
315
316
317 # ═══════════════════════════════════════════════════════════════════════════════
318 # Tier 4 — Stress
319 # ═══════════════════════════════════════════════════════════════════════════════
320
321
322 class TestStress:
323 async def test_50_objects_per_repo_all_isolated(self, tmp_path: Path) -> None:
324 """50 objects written to each of 3 repos — no cross-repo leakage."""
325 from musehub.storage.backends import LocalBackend, repo_root_for
326 b = LocalBackend(objects_dir=str(tmp_path / "global"))
327 repos = [
328 repo_root_for("alice", "repo1", repos_dir=str(tmp_path / "repos")),
329 repo_root_for("bob", "repo2", repos_dir=str(tmp_path / "repos")),
330 repo_root_for("carol", "repo3", repos_dir=str(tmp_path / "repos")),
331 ]
332 oids = [_oid() for _ in range(50)]
333
334 for rr in repos:
335 for oid in oids:
336 await b.put(oid, oid.encode(), repo_root=rr)
337
338 for rr in repos:
339 for oid in oids:
340 assert await b.exists(oid, repo_root=rr) is True
341 assert await b.get(oid, repo_root=rr) == oid.encode()
342
343 async def test_concurrent_puts_to_different_repos(self, tmp_path: Path) -> None:
344 """Concurrent puts to N repos do not interfere with each other."""
345 from musehub.storage.backends import LocalBackend, repo_root_for
346 b = LocalBackend(objects_dir=str(tmp_path / "global"))
347 N = 20
348 repos = [
349 repo_root_for(f"user{i}", "repo", repos_dir=str(tmp_path / "repos"))
350 for i in range(N)
351 ]
352 oid = _oid()
353 data = b"concurrent content"
354
355 await asyncio.gather(*[b.put(oid, data, repo_root=rr) for rr in repos])
356
357 for rr in repos:
358 assert await b.get(oid, repo_root=rr) == data
359
360 async def test_20_repos_under_5_seconds(self, tmp_path: Path) -> None:
361 """Writing 10 objects to each of 20 repos must complete in under 5 seconds."""
362 from musehub.storage.backends import LocalBackend, repo_root_for
363 b = LocalBackend(objects_dir=str(tmp_path / "global"))
364 repos = [
365 repo_root_for(f"user{i}", "r", repos_dir=str(tmp_path / "repos"))
366 for i in range(20)
367 ]
368 oids = [_oid() for _ in range(10)]
369 start = time.perf_counter()
370 for rr in repos:
371 for oid in oids:
372 await b.put(oid, b"x" * 100, repo_root=rr)
373 elapsed = time.perf_counter() - start
374 assert elapsed < 5.0, f"20-repo × 10-object write took {elapsed:.2f}s"
375
376
377 # ═══════════════════════════════════════════════════════════════════════════════
378 # Tier 5 — Data Integrity
379 # ═══════════════════════════════════════════════════════════════════════════════
380
381
382 class TestDataIntegrity:
383 async def test_put_idempotent_with_repo_root(self, tmp_path: Path) -> None:
384 """Putting the same bytes twice with repo_root is idempotent."""
385 from musehub.storage.backends import LocalBackend
386 b = LocalBackend(objects_dir=str(tmp_path / "global"))
387 rr = _repo_root(tmp_path)
388 oid = _oid()
389 await b.put(oid, b"content", repo_root=rr)
390 await b.put(oid, b"content", repo_root=rr)
391 assert await b.get(oid, repo_root=rr) == b"content"
392
393 async def test_uri_for_with_repo_root_matches_put_uri(self, tmp_path: Path) -> None:
394 """uri_for(oid, repo_root=...) must return the same URI that put returns."""
395 from musehub.storage.backends import LocalBackend
396 b = LocalBackend(objects_dir=str(tmp_path / "global"))
397 rr = _repo_root(tmp_path)
398 oid = _oid()
399 put_uri = await b.put(oid, b"check", repo_root=rr)
400 computed_uri = b.uri_for(oid, repo_root=rr)
401 assert put_uri == computed_uri
402
403 async def test_global_and_per_repo_paths_do_not_collide(self, tmp_path: Path) -> None:
404 """An object in the global store and the same oid in a per-repo store are separate."""
405 from musehub.storage.backends import LocalBackend
406 b = LocalBackend(objects_dir=str(tmp_path / "global"))
407 rr = _repo_root(tmp_path)
408 oid = _oid()
409
410 await b.put(oid, b"global version", repo_root=None)
411 await b.put(oid, b"per-repo version", repo_root=rr)
412
413 assert await b.get(oid) == b"global version"
414 assert await b.get(oid, repo_root=rr) == b"per-repo version"
415
416 async def test_delete_with_repo_root_only_targets_that_repo(self, tmp_path: Path) -> None:
417 """Deleting with repo_root does not affect the same oid in global store."""
418 from musehub.storage.backends import LocalBackend
419 b = LocalBackend(objects_dir=str(tmp_path / "global"))
420 rr = _repo_root(tmp_path)
421 oid = _oid()
422
423 await b.put(oid, b"global", repo_root=None)
424 await b.put(oid, b"per-repo", repo_root=rr)
425 await b.delete(oid, repo_root=rr)
426
427 assert await b.get(oid) == b"global"
428 assert await b.get(oid, repo_root=rr) is None
429
430 async def test_file_inside_repo_algo_shard_structure(self, tmp_path: Path) -> None:
431 """After put, the file exists at repo_root/objects/sha256/<shard>/<rest>."""
432 from musehub.storage.backends import LocalBackend
433 b = LocalBackend(objects_dir=str(tmp_path / "global"))
434 rr = _repo_root(tmp_path)
435 oid = "sha256:" + "ab" * 32
436 await b.put(oid, b"exact path check", repo_root=rr)
437 hex_part = "ab" * 32
438 expected = rr / "objects" / "sha256" / hex_part[:2] / hex_part[2:]
439 assert expected.exists()
440 assert expected.read_bytes() == b"exact path check"
441
442 async def test_repo_root_for_path_structure(self, tmp_path: Path) -> None:
443 """repo_root_for produces the exact expected path structure."""
444 from musehub.storage.backends import repo_root_for
445 rr = repo_root_for("gabriel", "musehub", repos_dir=str(tmp_path))
446 assert rr == tmp_path / "gabriel" / "musehub"
447
448
449 # ═══════════════════════════════════════════════════════════════════════════════
450 # Tier 6 — Security
451 # ═══════════════════════════════════════════════════════════════════════════════
452
453
454 class TestSecurity:
455 def test_path_traversal_in_object_id_raises_with_repo_root(self, tmp_path: Path) -> None:
456 """object_path validates object IDs strictly — traversal raises ValueError."""
457 from musehub.storage.backends import LocalBackend
458 b = LocalBackend(objects_dir=str(tmp_path / "global"))
459 rr = _repo_root(tmp_path)
460 with pytest.raises(ValueError):
461 b._path("../../etc/passwd", repo_root=rr)
462
463 def test_invalid_object_id_raises_with_repo_root(self, tmp_path: Path) -> None:
464 """Any non-sha256:<64-hex> object_id raises ValueError in per-repo mode."""
465 from musehub.storage.backends import LocalBackend
466 b = LocalBackend(objects_dir=str(tmp_path / "global"))
467 rr = _repo_root(tmp_path)
468 with pytest.raises(ValueError):
469 b._path("../../../../../root/.ssh/authorized_keys", repo_root=rr)
470
471 async def test_put_with_invalid_object_id_raises(self, tmp_path: Path) -> None:
472 """put with a non-valid object_id raises — no file written outside repo."""
473 from musehub.storage.backends import LocalBackend
474 b = LocalBackend(objects_dir=str(tmp_path / "global"))
475 rr = _repo_root(tmp_path)
476 with pytest.raises((ValueError, Exception)):
477 await b.put("../escaped", b"data", repo_root=rr)
478
479 async def test_get_with_invalid_object_id_raises(self, tmp_path: Path) -> None:
480 """get with invalid object_id raises ValueError — no filesystem access."""
481 from musehub.storage.backends import LocalBackend
482 b = LocalBackend(objects_dir=str(tmp_path / "global"))
483 rr = _repo_root(tmp_path)
484 with pytest.raises(ValueError):
485 await b.get("../../etc/passwd", repo_root=rr)
486
487 async def test_exists_with_invalid_object_id_raises(self, tmp_path: Path) -> None:
488 """exists with invalid object_id raises ValueError."""
489 from musehub.storage.backends import LocalBackend
490 b = LocalBackend(objects_dir=str(tmp_path / "global"))
491 rr = _repo_root(tmp_path)
492 with pytest.raises(ValueError):
493 await b.exists("../outside", repo_root=rr)
494
495 def test_repo_root_for_does_not_allow_traversal_in_owner(self, tmp_path: Path) -> None:
496 """repo_root_for must not allow path traversal via owner or slug."""
497 from musehub.storage.backends import repo_root_for
498 import pytest
499 with pytest.raises((ValueError, Exception)):
500 repo_root_for("../../etc", "passwd", repos_dir=str(tmp_path))
501
502 def test_repo_root_for_does_not_allow_traversal_in_slug(self, tmp_path: Path) -> None:
503 from musehub.storage.backends import repo_root_for
504 import pytest
505 with pytest.raises((ValueError, Exception)):
506 repo_root_for("alice", "../../shadow", repos_dir=str(tmp_path))
507
508 async def test_file_immutable_after_put_with_repo_root(self, tmp_path: Path) -> None:
509 """After put with repo_root, the file must be 0o444 (immutable)."""
510 import stat as _stat
511 from musehub.storage.backends import LocalBackend
512 b = LocalBackend(objects_dir=str(tmp_path / "global"))
513 rr = _repo_root(tmp_path)
514 oid = _oid()
515 await b.put(oid, b"immutable", repo_root=rr)
516 path = b._path(oid, repo_root=rr)
517 mode = _stat.S_IMODE(path.stat().st_mode)
518 assert mode == 0o444
519
520
521 # ═══════════════════════════════════════════════════════════════════════════════
522 # Tier 7 — Performance
523 # ═══════════════════════════════════════════════════════════════════════════════
524
525
526 class TestPerformance:
527 async def test_put_with_repo_root_latency(self, tmp_path: Path) -> None:
528 """Single put with repo_root must complete in under 0.5s."""
529 from musehub.storage.backends import LocalBackend
530 b = LocalBackend(objects_dir=str(tmp_path / "global"))
531 rr = _repo_root(tmp_path)
532 data = b"perf payload" * 100
533 start = time.perf_counter()
534 await b.put(_oid(), data, repo_root=rr)
535 elapsed = time.perf_counter() - start
536 assert elapsed < 0.5
537
538 async def test_50_sequential_puts_with_repo_root_under_budget(self, tmp_path: Path) -> None:
539 """50 sequential puts with repo_root must complete in under 2s."""
540 from musehub.storage.backends import LocalBackend
541 b = LocalBackend(objects_dir=str(tmp_path / "global"))
542 rr = _repo_root(tmp_path)
543 data = b"payload" * 100
544 start = time.perf_counter()
545 for i in range(50):
546 await b.put(_oid(), data, repo_root=rr)
547 elapsed = time.perf_counter() - start
548 assert elapsed < 2.0
549
550 async def test_exists_50_calls_with_repo_root_under_budget(self, tmp_path: Path) -> None:
551 """50 exists() calls with repo_root must complete in under 1s."""
552 from musehub.storage.backends import LocalBackend
553 b = LocalBackend(objects_dir=str(tmp_path / "global"))
554 rr = _repo_root(tmp_path)
555 oid = _oid()
556 await b.put(oid, b"data", repo_root=rr)
557 start = time.perf_counter()
558 for _ in range(50):
559 await b.exists(oid, repo_root=rr)
560 elapsed = time.perf_counter() - start
561 assert elapsed < 1.0
562
563 async def test_repo_root_for_1000_calls_under_1_second(self, tmp_path: Path) -> None:
564 """repo_root_for is pure path math — 1000 calls must be under 1s."""
565 from musehub.storage.backends import repo_root_for
566 start = time.perf_counter()
567 for i in range(1000):
568 repo_root_for(f"user{i}", "repo", repos_dir=str(tmp_path))
569 elapsed = time.perf_counter() - start
570 assert elapsed < 1.0
File History 1 commit
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor 143 days ago