gabriel / musehub public
test_no_legacy_flat_store.py python
269 lines 14.0 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago
1 """Section XX — No Legacy Flat-Store: regression guard for the per-repo migration.
2
3 Every test in this file asserts the ABSENCE of legacy flat-store code. They
4 all start RED and turn GREEN as the legacy code is deleted. Their permanent
5 role is to prevent regressions — if any of these tests starts failing, legacy
6 code was accidentally reintroduced.
7
8 What "legacy flat-store" means:
9 - LocalBackend._safe_id() — converts "sha256:abc" → "sha256_abc" for flat naming
10 - LocalBackend._root — the /data/musehub global objects directory
11 - LocalBackend._path(oid, repo_root=None) falling back to the flat store
12 - put/get/exists/delete without repo_root silently using the flat store
13 - settings.musehub_objects_dir (/data/musehub) — superseded by musehub_repos_dir
14 - musehub_sync._object_disk_path() — built old <repo_id>/<safe_id> paths
15 - musehub/api/routes/musehub/objects.py validating disk paths against
16 musehub_objects_dir instead of musehub_repos_dir
17
18 Per-repo store (current canonical layout):
19 /data/repos/<owner>/<slug>/objects/sha256/<2-hex>/<62-hex>
20
21 This file intentionally does NOT test the positive behaviour (that is already
22 covered by test_storage_backends.py and test_storage_tiers.py). It tests
23 only the negative: that the old code paths are gone.
24 """
25 from __future__ import annotations
26
27 import ast
28 import inspect
29 import textwrap
30 from pathlib import Path
31
32 import pytest
33 from muse.core.types import long_id
34
35 # ── helpers ───────────────────────────────────────────────────────────────────
36
37 _REPO_ROOT = Path(__file__).parent.parent # ~/ecosystem/musehub
38
39
40 def _source(rel: str) -> str:
41 return (_REPO_ROOT / rel).read_text()
42
43
44 def _ast_names(rel: str) -> set[str]:
45 """Return all function/class names defined at module level."""
46 tree = ast.parse(_source(rel))
47 return {n.name for n in ast.walk(tree) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))}
48
49
50 # ═══════════════════════════════════════════════════════════════════════════════
51 # 1. LocalBackend — no _safe_id, no _root, no flat-store fallback in _path
52 # ═══════════════════════════════════════════════════════════════════════════════
53
54
55 class TestLocalBackendNoLegacy:
56 def test_no_safe_id_method(self) -> None:
57 """_safe_id() must be deleted — it exists only to sanitise flat-store filenames."""
58 from musehub.storage.backends import LocalBackend
59 assert not hasattr(LocalBackend, "_safe_id"), (
60 "LocalBackend._safe_id still exists. Delete it — it was only needed "
61 "for the legacy flat-store naming scheme (sha256:abc → sha256_abc)."
62 )
63
64 def test_no_root_attribute(self, tmp_path: Path) -> None:
65 """_root was the global /data/musehub objects directory — must be gone."""
66 from musehub.storage.backends import LocalBackend
67 b = LocalBackend()
68 assert not hasattr(b, "_root"), (
69 "LocalBackend._root still exists. It pointed at the legacy global "
70 "flat-store directory. Remove it and its __init__ assignment."
71 )
72
73 def test_init_takes_no_objects_dir_arg(self, tmp_path: Path) -> None:
74 """__init__(objects_dir=...) was for the flat store — no longer needed."""
75 from musehub.storage.backends import LocalBackend
76 import inspect
77 sig = inspect.signature(LocalBackend.__init__)
78 assert "objects_dir" not in sig.parameters, (
79 "LocalBackend.__init__ still accepts 'objects_dir'. That parameter "
80 "was used to set self._root for the flat store. Remove it."
81 )
82
83 def test_path_without_repo_root_raises(self, tmp_path: Path) -> None:
84 """_path(oid, repo_root=None) must raise — no silent flat-store fallback."""
85 from musehub.storage.backends import LocalBackend
86 b = LocalBackend()
87 with pytest.raises((ValueError, TypeError, AssertionError)):
88 b._path(long_id("a" * 64), repo_root=None)
89
90 def test_path_with_repo_root_uses_algo_shard_layout(self, tmp_path: Path) -> None:
91 """Per-repo path must be <repo_root>/objects/sha256/<2hex>/<62hex>."""
92 from musehub.storage.backends import LocalBackend
93 b = LocalBackend()
94 repo_root = tmp_path / "repos" / "gabriel" / "muse"
95 hex64 = "ab" + "c" * 62
96 oid = long_id(hex64)
97 p = b._path(oid, repo_root=repo_root)
98 expected = repo_root / "objects" / "sha256" / "ab" / ("c" * 62)
99 assert p == expected, (
100 f"Expected per-repo path {expected} but got {p}. "
101 "LocalBackend._path must produce algo/shard/rest layout."
102 )
103
104 async def test_put_without_repo_root_raises(self, tmp_path: Path) -> None:
105 """put() without repo_root must raise — no silent flat-store write."""
106 from musehub.storage.backends import LocalBackend
107 b = LocalBackend()
108 with pytest.raises((ValueError, TypeError, AssertionError)):
109 await b.put(long_id("a" * 64), b"data", repo_root=None)
110
111 async def test_get_without_repo_root_raises(self, tmp_path: Path) -> None:
112 """get() without repo_root must raise — no silent flat-store read."""
113 from musehub.storage.backends import LocalBackend
114 b = LocalBackend()
115 with pytest.raises((ValueError, TypeError, AssertionError)):
116 await b.get(long_id("a" * 64), repo_root=None)
117
118 async def test_exists_without_repo_root_raises(self, tmp_path: Path) -> None:
119 """exists() without repo_root must raise — no silent flat-store check."""
120 from musehub.storage.backends import LocalBackend
121 b = LocalBackend()
122 with pytest.raises((ValueError, TypeError, AssertionError)):
123 await b.exists(long_id("a" * 64), repo_root=None)
124
125 async def test_per_repo_roundtrip(self, tmp_path: Path) -> None:
126 """Full put/get/exists/delete cycle with explicit repo_root works."""
127 from musehub.storage.backends import LocalBackend
128 b = LocalBackend()
129 repo_root = tmp_path / "repos" / "gabriel" / "muse"
130 hex64 = "ab" + "c" * 62
131 oid = long_id(hex64)
132 data = b"per-repo content"
133
134 uri = await b.put(oid, data, repo_root=repo_root)
135 assert uri.startswith("local://")
136 assert await b.exists(oid, repo_root=repo_root) is True
137 assert await b.get(oid, repo_root=repo_root) == data
138
139 await b.delete(oid, repo_root=repo_root)
140 assert await b.exists(oid, repo_root=repo_root) is False
141
142 async def test_per_repo_object_on_disk_at_correct_path(self, tmp_path: Path) -> None:
143 """Object bytes land at objects/sha256/<2hex>/<62hex>, not at a flat path."""
144 from musehub.storage.backends import LocalBackend
145 b = LocalBackend()
146 repo_root = tmp_path / "repos" / "gabriel" / "muse"
147 hex64 = "de" + "f" * 62
148 oid = long_id(hex64)
149 await b.put(oid, b"content", repo_root=repo_root)
150
151 expected = repo_root / "objects" / "sha256" / "de" / ("f" * 62)
152 assert expected.exists(), (
153 f"Object not found at expected per-repo path {expected}. "
154 "Check LocalBackend._path algo/shard layout."
155 )
156
157 def test_no_flat_path_in_source(self) -> None:
158 """backends.py must not reference the flat legacy path patterns."""
159 src = _source("musehub/storage/backends.py")
160 forbidden = [
161 "_safe_id",
162 "musehub_objects_dir",
163 "self._root",
164 ]
165 for pattern in forbidden:
166 assert pattern not in src, (
167 f"Found '{pattern}' in backends.py — legacy flat-store remnant. "
168 "Delete it."
169 )
170
171
172 # ═══════════════════════════════════════════════════════════════════════════════
173 # 2. Config — musehub_objects_dir removed, musehub_repos_dir present
174 # ═══════════════════════════════════════════════════════════════════════════════
175
176
177 class TestConfigNoLegacy:
178 def test_no_musehub_objects_dir_in_settings(self) -> None:
179 """settings.musehub_objects_dir must be removed — superseded by musehub_repos_dir."""
180 from musehub.config import Settings
181 assert "musehub_objects_dir" not in Settings.model_fields, (
182 "Settings.musehub_objects_dir still exists. Remove it — the flat-store "
183 "root (/data/musehub) is no longer used. Only musehub_repos_dir remains."
184 )
185
186 def test_musehub_repos_dir_present(self) -> None:
187 """settings.musehub_repos_dir must exist — this is the canonical store root."""
188 from musehub.config import Settings
189 assert "musehub_repos_dir" in Settings.model_fields, (
190 "Settings.musehub_repos_dir is missing. It must be present and default "
191 "to '/data/repos'."
192 )
193
194 def test_no_objects_dir_in_config_source(self) -> None:
195 """config.py must not define musehub_objects_dir."""
196 src = _source("musehub/config.py")
197 assert "musehub_objects_dir" not in src, (
198 "Found 'musehub_objects_dir' in config.py. Delete it."
199 )
200
201
202 # ═══════════════════════════════════════════════════════════════════════════════
203 # 3. musehub_sync — no _object_disk_path, no flat-store writes
204 # ═══════════════════════════════════════════════════════════════════════════════
205
206
207 class TestMusehubSyncNoLegacy:
208 def test_no_object_disk_path_function(self) -> None:
209 """_object_disk_path() built the old <repo_id>/<safe_id> flat path — must be gone."""
210 from musehub.services import musehub_sync
211 assert not hasattr(musehub_sync, "_object_disk_path"), (
212 "musehub_sync._object_disk_path still exists. Delete it — it computed "
213 "the legacy /data/musehub/<repo_id>/<object_id> path."
214 )
215
216 def test_no_musehub_objects_dir_in_sync_source(self) -> None:
217 """musehub_sync.py must not import or reference musehub_objects_dir."""
218 src = _source("musehub/services/musehub_sync.py")
219 assert "musehub_objects_dir" not in src, (
220 "Found 'musehub_objects_dir' in musehub_sync.py. Remove it — objects "
221 "are now written via LocalBackend with an explicit repo_root."
222 )
223
224 def test_no_safe_id_in_sync_source(self) -> None:
225 """No manual safe_id conversion in sync — that was for flat-store filenames."""
226 src = _source("musehub/services/musehub_sync.py")
227 # Only flag the colon-replacement pattern used for flat filenames
228 assert 'replace(":", "-")' not in src and 'replace(":", "_")' not in src, (
229 "Found legacy colon-replacement in musehub_sync.py. Delete it — "
230 "_object_disk_path was the only caller of this pattern."
231 )
232
233
234 # ═══════════════════════════════════════════════════════════════════════════════
235 # 4. objects route — validates disk path against musehub_repos_dir, not objects_dir
236 # ═══════════════════════════════════════════════════════════════════════════════
237
238
239 class TestObjectsRouteNoLegacy:
240 def test_no_musehub_objects_dir_in_objects_route(self) -> None:
241 """The objects download route must not reference musehub_objects_dir."""
242 src = _source("musehub/api/routes/musehub/objects.py")
243 assert "musehub_objects_dir" not in src, (
244 "Found 'musehub_objects_dir' in objects.py. The security check must "
245 "validate disk paths against musehub_repos_dir instead."
246 )
247
248 def test_objects_route_validates_against_repos_dir(self) -> None:
249 """The path-traversal guard in the objects route must use musehub_repos_dir."""
250 src = _source("musehub/api/routes/musehub/objects.py")
251 assert "musehub_repos_dir" in src, (
252 "objects.py does not reference musehub_repos_dir. The disk_path "
253 "security check must validate that paths stay inside musehub_repos_dir "
254 "(/data/repos), not the old musehub_objects_dir (/data/musehub)."
255 )
256
257
258 # ═══════════════════════════════════════════════════════════════════════════════
259 # 5. storage/__init__.py docstring — no flat-store description
260 # ═══════════════════════════════════════════════════════════════════════════════
261
262
263 class TestStorageInitNoLegacy:
264 def test_no_flat_store_description_in_init(self) -> None:
265 """storage/__init__.py docstring must not describe the legacy flat layout."""
266 src = _source("musehub/storage/__init__.py")
267 assert "musehub_objects_dir" not in src, (
268 "Found 'musehub_objects_dir' in storage/__init__.py. Update the docstring."
269 )
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago