gabriel / musehub public
test_per_repo_object_store.py python
572 lines 26.5 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 123 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 secrets
23 import time
24 from pathlib import Path
25 from unittest.mock import patch
26
27 import pytest
28 from muse.core.types import long_id
29
30 # ---------------------------------------------------------------------------
31 # Helpers
32 # ---------------------------------------------------------------------------
33
34
35 def _uid() -> str:
36 return secrets.token_hex(16)
37
38
39 def _oid() -> str:
40 """A valid sha256-prefixed object ID."""
41 return long_id(secrets.token_hex(32))
42
43
44 def _backend(tmp_path: Path, objects_dir: str | None = None) -> "LocalBackend":
45 from musehub.storage.backends import LocalBackend
46 return LocalBackend()
47
48
49 def _repo_root(tmp_path: Path, owner: str = "alice", slug: str = "myrepo") -> Path:
50 root = tmp_path / "repos" / owner / slug
51 root.mkdir(parents=True, exist_ok=True)
52 return root
53
54
55 # ═══════════════════════════════════════════════════════════════════════════════
56 # Tier 1 — Unit (pure logic, no I/O)
57 # ═══════════════════════════════════════════════════════════════════════════════
58
59
60 class TestUnit:
61 """Pure path logic — no filesystem writes."""
62
63 def test_path_with_repo_root_uses_algo_sharded_layout(self, tmp_path: Path) -> None:
64 """_path(oid, repo_root=...) → repo_root/objects/sha256/<2-hex>/<62-hex>"""
65 from musehub.storage.backends import LocalBackend
66 b = LocalBackend()
67 rr = _repo_root(tmp_path)
68 oid = _oid()
69 path = b._path(oid, repo_root=rr)
70 hex_part = oid.removeprefix("sha256:")
71 # algo/shard/rest layout
72 assert path.name == hex_part[2:] # 62-char remainder
73 assert path.parent.name == hex_part[:2] # 2-char shard
74 assert path.parent.parent.name == "sha256"
75 assert path.parent.parent.parent == (rr / "objects").resolve()
76
77 def test_path_without_repo_root_raises(self, tmp_path: Path) -> None:
78 """_path(oid) without repo_root raises ValueError — repo_root is required."""
79 from musehub.storage.backends import LocalBackend
80 b = LocalBackend()
81 oid = _oid()
82 with pytest.raises(ValueError):
83 b._path(oid)
84
85 def test_path_with_repo_root_no_colon_on_disk(self, tmp_path: Path) -> None:
86 """The algo/shard/rest layout never puts a colon on disk."""
87 from musehub.storage.backends import LocalBackend
88 b = LocalBackend()
89 rr = _repo_root(tmp_path)
90 oid = _oid()
91 path = b._path(oid, repo_root=rr)
92 assert ":" not in str(path)
93
94 def test_path_two_repos_different_directories(self, tmp_path: Path) -> None:
95 """Objects in different repos resolve to different directories."""
96 from musehub.storage.backends import LocalBackend
97 b = LocalBackend()
98 rr_alice = _repo_root(tmp_path, owner="alice", slug="repo1")
99 rr_bob = _repo_root(tmp_path, owner="bob", slug="repo2")
100 oid = _oid()
101 p_alice = b._path(oid, repo_root=rr_alice)
102 p_bob = b._path(oid, repo_root=rr_bob)
103 assert p_alice != p_bob
104 assert str(p_alice).startswith(str(rr_alice))
105 assert str(p_bob).startswith(str(rr_bob))
106
107 def test_uri_for_with_repo_root_contains_algo_and_objects(self, tmp_path: Path) -> None:
108 """uri_for with repo_root encodes the full algo-namespaced path."""
109 from musehub.storage.backends import LocalBackend
110 b = LocalBackend()
111 rr = _repo_root(tmp_path)
112 oid = _oid()
113 uri = b.uri_for(oid, repo_root=rr)
114 assert uri.startswith("local://")
115 assert "/objects/sha256/" in uri
116
117 def test_uri_for_without_repo_root_raises(self, tmp_path: Path) -> None:
118 """uri_for without repo_root raises ValueError — repo_root is required."""
119 from musehub.storage.backends import LocalBackend
120 b = LocalBackend()
121 oid = _oid()
122 with pytest.raises(ValueError):
123 b.uri_for(oid)
124
125 def test_path_traversal_in_object_id_raises_with_repo_root(self, tmp_path: Path) -> None:
126 """object_path validates object IDs strictly — traversal raises ValueError."""
127 from musehub.storage.backends import LocalBackend
128 b = LocalBackend()
129 rr = _repo_root(tmp_path)
130 with pytest.raises(ValueError):
131 b._path("../../etc/passwd", repo_root=rr)
132
133 def test_settings_has_musehub_repos_dir(self) -> None:
134 """settings.musehub_repos_dir must exist and be a non-empty string."""
135 from musehub.config import settings
136 assert hasattr(settings, "musehub_repos_dir")
137 assert isinstance(settings.musehub_repos_dir, str)
138 assert settings.musehub_repos_dir != ""
139
140 def test_repo_root_for_returns_correct_path(self, tmp_path: Path) -> None:
141 """repo_root_for(owner, slug) → repos_dir/owner/slug"""
142 from musehub.storage.backends import repo_root_for
143 root = repo_root_for("alice", "muse", repos_dir=str(tmp_path / "repos"))
144 assert root == tmp_path / "repos" / "alice" / "muse"
145
146 def test_repo_root_for_uses_settings_default(self) -> None:
147 """repo_root_for without repos_dir falls back to settings.musehub_repos_dir."""
148 from musehub.storage.backends import repo_root_for
149 from musehub.config import settings
150 root = repo_root_for("alice", "muse")
151 assert root == Path(settings.musehub_repos_dir) / "alice" / "muse"
152
153 def test_path_with_repo_root_same_oid_same_path(self, tmp_path: Path) -> None:
154 """Same object_id + same repo_root always resolves to the same path."""
155 from musehub.storage.backends import LocalBackend
156 b = LocalBackend()
157 rr = _repo_root(tmp_path)
158 oid = _oid()
159 p1 = b._path(oid, repo_root=rr)
160 p2 = b._path(oid, repo_root=rr)
161 assert p1 == p2
162
163
164 # ═══════════════════════════════════════════════════════════════════════════════
165 # Tier 2 — Integration (real filesystem I/O)
166 # ═══════════════════════════════════════════════════════════════════════════════
167
168
169 class TestIntegration:
170 """Real filesystem — put, get, exists, delete with repo_root."""
171
172 async def test_put_with_repo_root_creates_file_in_repo_dir(self, tmp_path: Path) -> None:
173 """put(oid, data, repo_root=...) writes file under repo_root/objects/."""
174 from musehub.storage.backends import LocalBackend
175 b = LocalBackend()
176 rr = _repo_root(tmp_path)
177 oid = _oid()
178 data = b"per-repo data"
179 uri = await b.put(oid, data, repo_root=rr)
180 assert uri.startswith("local://")
181 disk = Path(uri.removeprefix("local://"))
182 assert disk.exists()
183 assert disk.read_bytes() == data
184 assert str(rr) in str(disk)
185
186 async def test_get_with_repo_root_reads_from_repo_dir(self, tmp_path: Path) -> None:
187 """get(oid, repo_root=...) retrieves data written by put with same repo_root."""
188 from musehub.storage.backends import LocalBackend
189 b = LocalBackend()
190 rr = _repo_root(tmp_path)
191 oid = _oid()
192 data = b"read me back"
193 await b.put(oid, data, repo_root=rr)
194 result = await b.get(oid, repo_root=rr)
195 assert result == data
196
197 async def test_exists_with_repo_root_true_after_put(self, tmp_path: Path) -> None:
198 from musehub.storage.backends import LocalBackend
199 b = LocalBackend()
200 rr = _repo_root(tmp_path)
201 oid = _oid()
202 assert await b.exists(oid, repo_root=rr) is False
203 await b.put(oid, b"data", repo_root=rr)
204 assert await b.exists(oid, repo_root=rr) is True
205
206 async def test_delete_with_repo_root_removes_file(self, tmp_path: Path) -> None:
207 from musehub.storage.backends import LocalBackend
208 b = LocalBackend()
209 rr = _repo_root(tmp_path)
210 oid = _oid()
211 await b.put(oid, b"bye", repo_root=rr)
212 assert await b.exists(oid, repo_root=rr) is True
213 await b.delete(oid, repo_root=rr)
214 assert await b.exists(oid, repo_root=rr) is False
215
216 async def test_put_without_repo_root_raises(self, tmp_path: Path) -> None:
217 """put without repo_root raises ValueError — repo_root is required."""
218 from musehub.storage.backends import LocalBackend
219 b = LocalBackend()
220 oid = _oid()
221 with pytest.raises(ValueError):
222 await b.put(oid, b"no root 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()
228 rr_alice = _repo_root(tmp_path, "alice", "repo1")
229 rr_bob = _repo_root(tmp_path, "bob", "repo2")
230 oid = long_id("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()
242 rr_a = _repo_root(tmp_path, "alice", "repo1")
243 rr_b = _repo_root(tmp_path, "bob", "repo2")
244 oid = long_id("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()
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()
265 rr = _repo_root(tmp_path)
266 result = await b.get(long_id("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()
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()
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()
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()
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()
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()
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()
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()
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_two_repo_paths_do_not_collide(self, tmp_path: Path) -> None:
404 """Same oid written to two different repo roots are stored independently."""
405 from musehub.storage.backends import LocalBackend
406 b = LocalBackend()
407 rr_a = _repo_root(tmp_path, owner="alice", slug="repo")
408 rr_b = _repo_root(tmp_path, owner="bob", slug="repo")
409 oid = _oid()
410
411 await b.put(oid, b"alice version", repo_root=rr_a)
412 await b.put(oid, b"bob version", repo_root=rr_b)
413
414 assert await b.get(oid, repo_root=rr_a) == b"alice version"
415 assert await b.get(oid, repo_root=rr_b) == b"bob version"
416
417 async def test_delete_from_one_repo_does_not_affect_other(self, tmp_path: Path) -> None:
418 """Deleting with repo_root only removes that repo's copy; another repo's copy survives."""
419 from musehub.storage.backends import LocalBackend
420 b = LocalBackend()
421 rr_a = _repo_root(tmp_path, owner="alice", slug="repo")
422 rr_b = _repo_root(tmp_path, owner="bob", slug="repo")
423 oid = _oid()
424
425 await b.put(oid, b"alice copy", repo_root=rr_a)
426 await b.put(oid, b"bob copy", repo_root=rr_b)
427 await b.delete(oid, repo_root=rr_a)
428
429 assert await b.get(oid, repo_root=rr_a) is None
430 assert await b.get(oid, repo_root=rr_b) == b"bob copy"
431
432 async def test_file_inside_repo_algo_shard_structure(self, tmp_path: Path) -> None:
433 """After put, the file exists at repo_root/objects/sha256/<shard>/<rest>."""
434 from musehub.storage.backends import LocalBackend
435 b = LocalBackend()
436 rr = _repo_root(tmp_path)
437 oid = long_id("ab" * 32)
438 await b.put(oid, b"exact path check", repo_root=rr)
439 hex_part = "ab" * 32
440 expected = rr / "objects" / "sha256" / hex_part[:2] / hex_part[2:]
441 assert expected.exists()
442 assert expected.read_bytes() == b"exact path check"
443
444 async def test_repo_root_for_path_structure(self, tmp_path: Path) -> None:
445 """repo_root_for produces the exact expected path structure."""
446 from musehub.storage.backends import repo_root_for
447 rr = repo_root_for("gabriel", "musehub", repos_dir=str(tmp_path))
448 assert rr == tmp_path / "gabriel" / "musehub"
449
450
451 # ═══════════════════════════════════════════════════════════════════════════════
452 # Tier 6 — Security
453 # ═══════════════════════════════════════════════════════════════════════════════
454
455
456 class TestSecurity:
457 def test_path_traversal_in_object_id_raises_with_repo_root(self, tmp_path: Path) -> None:
458 """object_path validates object IDs strictly — traversal raises ValueError."""
459 from musehub.storage.backends import LocalBackend
460 b = LocalBackend()
461 rr = _repo_root(tmp_path)
462 with pytest.raises(ValueError):
463 b._path("../../etc/passwd", repo_root=rr)
464
465 def test_invalid_object_id_raises_with_repo_root(self, tmp_path: Path) -> None:
466 """Any non-sha256:<64-hex> object_id raises ValueError in per-repo mode."""
467 from musehub.storage.backends import LocalBackend
468 b = LocalBackend()
469 rr = _repo_root(tmp_path)
470 with pytest.raises(ValueError):
471 b._path("../../../../../root/.ssh/authorized_keys", repo_root=rr)
472
473 async def test_put_with_invalid_object_id_raises(self, tmp_path: Path) -> None:
474 """put with a non-valid object_id raises — no file written outside repo."""
475 from musehub.storage.backends import LocalBackend
476 b = LocalBackend()
477 rr = _repo_root(tmp_path)
478 with pytest.raises((ValueError, Exception)):
479 await b.put("../escaped", b"data", repo_root=rr)
480
481 async def test_get_with_invalid_object_id_raises(self, tmp_path: Path) -> None:
482 """get with invalid object_id raises ValueError — no filesystem access."""
483 from musehub.storage.backends import LocalBackend
484 b = LocalBackend()
485 rr = _repo_root(tmp_path)
486 with pytest.raises(ValueError):
487 await b.get("../../etc/passwd", repo_root=rr)
488
489 async def test_exists_with_invalid_object_id_raises(self, tmp_path: Path) -> None:
490 """exists with invalid object_id raises ValueError."""
491 from musehub.storage.backends import LocalBackend
492 b = LocalBackend()
493 rr = _repo_root(tmp_path)
494 with pytest.raises(ValueError):
495 await b.exists("../outside", repo_root=rr)
496
497 def test_repo_root_for_does_not_allow_traversal_in_owner(self, tmp_path: Path) -> None:
498 """repo_root_for must not allow path traversal via owner or slug."""
499 from musehub.storage.backends import repo_root_for
500 import pytest
501 with pytest.raises((ValueError, Exception)):
502 repo_root_for("../../etc", "passwd", repos_dir=str(tmp_path))
503
504 def test_repo_root_for_does_not_allow_traversal_in_slug(self, tmp_path: Path) -> None:
505 from musehub.storage.backends import repo_root_for
506 import pytest
507 with pytest.raises((ValueError, Exception)):
508 repo_root_for("alice", "../../shadow", repos_dir=str(tmp_path))
509
510 async def test_file_immutable_after_put_with_repo_root(self, tmp_path: Path) -> None:
511 """After put with repo_root, the file must be 0o444 (immutable)."""
512 import stat as _stat
513 from musehub.storage.backends import LocalBackend
514 b = LocalBackend()
515 rr = _repo_root(tmp_path)
516 oid = _oid()
517 await b.put(oid, b"immutable", repo_root=rr)
518 path = b._path(oid, repo_root=rr)
519 mode = _stat.S_IMODE(path.stat().st_mode)
520 assert mode == 0o444
521
522
523 # ═══════════════════════════════════════════════════════════════════════════════
524 # Tier 7 — Performance
525 # ═══════════════════════════════════════════════════════════════════════════════
526
527
528 class TestPerformance:
529 async def test_put_with_repo_root_latency(self, tmp_path: Path) -> None:
530 """Single put with repo_root must complete in under 0.5s."""
531 from musehub.storage.backends import LocalBackend
532 b = LocalBackend()
533 rr = _repo_root(tmp_path)
534 data = b"perf payload" * 100
535 start = time.perf_counter()
536 await b.put(_oid(), data, repo_root=rr)
537 elapsed = time.perf_counter() - start
538 assert elapsed < 0.5
539
540 async def test_50_sequential_puts_with_repo_root_under_budget(self, tmp_path: Path) -> None:
541 """50 sequential puts with repo_root must complete in under 2s."""
542 from musehub.storage.backends import LocalBackend
543 b = LocalBackend()
544 rr = _repo_root(tmp_path)
545 data = b"payload" * 100
546 start = time.perf_counter()
547 for i in range(50):
548 await b.put(_oid(), data, repo_root=rr)
549 elapsed = time.perf_counter() - start
550 assert elapsed < 2.0
551
552 async def test_exists_50_calls_with_repo_root_under_budget(self, tmp_path: Path) -> None:
553 """50 exists() calls with repo_root must complete in under 1s."""
554 from musehub.storage.backends import LocalBackend
555 b = LocalBackend()
556 rr = _repo_root(tmp_path)
557 oid = _oid()
558 await b.put(oid, b"data", repo_root=rr)
559 start = time.perf_counter()
560 for _ in range(50):
561 await b.exists(oid, repo_root=rr)
562 elapsed = time.perf_counter() - start
563 assert elapsed < 1.0
564
565 async def test_repo_root_for_1000_calls_under_1_second(self, tmp_path: Path) -> None:
566 """repo_root_for is pure path math — 1000 calls must be under 1s."""
567 from musehub.storage.backends import repo_root_for
568 start = time.perf_counter()
569 for i in range(1000):
570 repo_root_for(f"user{i}", "repo", repos_dir=str(tmp_path))
571 elapsed = time.perf_counter() - start
572 assert elapsed < 1.0
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 123 days ago