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