gabriel / musehub public
test_zlib_object_decompression.py python
295 lines 9.8 KB
Raw
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago
1 """Tests for zlib-compressed object decompression in raw file serving and README fetching.
2
3 Root cause: objects pushed via the old wire path were stored zlib-compressed in R2.
4 The raw file endpoint and _fetch_readme read bytes directly from storage without
5 decompressing, so staging serves garbled bytes while localhost (filesystem backend
6 with repair logic) serves clean text.
7
8 Fix: detect zlib magic bytes and decompress before serving — in both
9 _fetch_readme and raw_file_semantic.
10 """
11 from __future__ import annotations
12
13 import zlib
14 from unittest.mock import AsyncMock, MagicMock, patch
15
16 import pytest
17 import pytest_asyncio
18 from httpx import AsyncClient, ASGITransport
19
20 from musehub.main import app
21
22
23 # ---------------------------------------------------------------------------
24 # Helpers
25 # ---------------------------------------------------------------------------
26
27 def _zlib_compress(text: str) -> bytes:
28 return zlib.compress(text.encode())
29
30
31 def _raw_bytes(text: str) -> bytes:
32 return text.encode()
33
34
35 README_TEXT = "# muse-zsh\n\nOh My ZSH plugin for Muse.\n"
36 ZLIB_README = _zlib_compress(README_TEXT)
37 RAW_README = _raw_bytes(README_TEXT)
38
39
40 # ---------------------------------------------------------------------------
41 # Unit tests — decompress_if_needed utility
42 # ---------------------------------------------------------------------------
43
44 def test_decompress_if_needed_passes_plain_text_through() -> None:
45 from musehub.types.compression import decompress_if_needed
46 data = b"# plain text README\n"
47 assert decompress_if_needed(data) == data
48
49
50 def test_decompress_if_needed_decompresses_zlib_level_default() -> None:
51 from musehub.types.compression import decompress_if_needed
52 compressed = zlib.compress(b"hello world\n")
53 assert decompress_if_needed(compressed) == b"hello world\n"
54
55
56 def test_decompress_if_needed_decompresses_zlib_level_1() -> None:
57 from musehub.types.compression import decompress_if_needed
58 compressed = zlib.compress(b"hello\n", level=1)
59 assert decompress_if_needed(compressed) == b"hello\n"
60
61
62 def test_decompress_if_needed_decompresses_zlib_level_9() -> None:
63 from musehub.types.compression import decompress_if_needed
64 compressed = zlib.compress(b"hello\n", level=9)
65 assert decompress_if_needed(compressed) == b"hello\n"
66
67
68 def test_decompress_if_needed_passes_empty_bytes_through() -> None:
69 from musehub.types.compression import decompress_if_needed
70 assert decompress_if_needed(b"") == b""
71
72
73 def test_decompress_if_needed_passes_binary_non_zlib_through() -> None:
74 from musehub.types.compression import decompress_if_needed
75 # PNG magic bytes — not zlib
76 data = b"\x89PNG\r\n\x1a\n" + b"\x00" * 100
77 assert decompress_if_needed(data) == data
78
79
80 def test_decompress_if_needed_handles_truncated_zlib_gracefully() -> None:
81 from musehub.types.compression import decompress_if_needed
82 # zlib magic bytes but truncated — should return original bytes, not raise
83 data = b"\x78\x9c\x00" # valid zlib header, invalid body
84 result = decompress_if_needed(data)
85 # Must not raise — returns original bytes on decompression failure
86 assert isinstance(result, bytes)
87
88
89 def test_decompress_if_needed_decompresses_full_readme() -> None:
90 from musehub.types.compression import decompress_if_needed
91 assert decompress_if_needed(ZLIB_README).decode() == README_TEXT
92
93
94 # ---------------------------------------------------------------------------
95 # Unit tests — _fetch_readme decompresses stored objects
96 # ---------------------------------------------------------------------------
97
98 @pytest.mark.asyncio
99 async def test_fetch_readme_decompresses_zlib_object(
100 monkeypatch: pytest.MonkeyPatch,
101 ) -> None:
102 """_fetch_readme must decode zlib-compressed objects from storage."""
103 from musehub.api.routes.musehub import _ui_helpers
104
105 mock_storage = MagicMock()
106 mock_storage.get = AsyncMock(return_value=ZLIB_README)
107 monkeypatch.setattr(_ui_helpers, "_get_storage_backend", lambda *_: mock_storage)
108
109 from musehub.api.routes.musehub._ui_helpers import _fetch_readme
110
111 class _Entry:
112 name = "README.md"
113 object_id = "abc123"
114
115 result = await _fetch_readme(
116 db=MagicMock(),
117 repo_id="repo-1",
118 ref="main",
119 entries=[_Entry()], # type: ignore[list-item]
120 )
121 assert result == README_TEXT
122
123
124 @pytest.mark.asyncio
125 async def test_fetch_readme_plain_text_object_unchanged(
126 monkeypatch: pytest.MonkeyPatch,
127 ) -> None:
128 """_fetch_readme must pass through already-plain-text objects unchanged."""
129 from musehub.api.routes.musehub import _ui_helpers
130
131 mock_storage = MagicMock()
132 mock_storage.get = AsyncMock(return_value=RAW_README)
133 monkeypatch.setattr(_ui_helpers, "_get_storage_backend", lambda *_: mock_storage)
134
135 from musehub.api.routes.musehub._ui_helpers import _fetch_readme
136
137 class _Entry:
138 name = "README.md"
139 object_id = "abc123"
140
141 result = await _fetch_readme(
142 db=MagicMock(),
143 repo_id="repo-1",
144 ref="main",
145 entries=[_Entry()], # type: ignore[list-item]
146 )
147 assert result == README_TEXT
148
149
150 # ---------------------------------------------------------------------------
151 # Integration tests — raw_file_semantic endpoint decompresses
152 # ---------------------------------------------------------------------------
153
154 @pytest.mark.asyncio
155 async def test_raw_endpoint_decompresses_zlib_object(
156 client: AsyncClient,
157 monkeypatch: pytest.MonkeyPatch,
158 ) -> None:
159 """GET /owner/repo/raw/ref/README.md must return plain text even when the
160 stored object is zlib-compressed."""
161 from musehub.api.routes.musehub import ui_tree
162 from musehub.api.routes.musehub import repos as repos_mod
163
164 # Stub repo resolution
165 monkeypatch.setattr(
166 ui_tree,
167 "_resolve_repo",
168 AsyncMock(return_value=("repo-id-1", MagicMock(), MagicMock())),
169 )
170
171 # Stub file metadata lookup
172 monkeypatch.setattr(
173 ui_tree.musehub_repository,
174 "get_file_at_ref",
175 AsyncMock(return_value={"object_id": "deadbeef"}),
176 )
177
178 # Stub storage: exists=True, stream yields zlib-compressed bytes
179 mock_storage = MagicMock()
180 mock_storage.exists = AsyncMock(return_value=True)
181
182 async def _compressed_stream(object_id: str, chunk_size: int = 65536) -> None:
183 yield ZLIB_README
184
185 mock_storage.stream = _compressed_stream
186 monkeypatch.setattr(ui_tree, "_get_storage_backend", lambda *_: mock_storage)
187
188 resp = await client.get("/gabriel/muse-zsh/raw/main/README.md")
189 assert resp.status_code == 200
190 assert resp.text == README_TEXT
191
192
193 @pytest.mark.asyncio
194 async def test_raw_endpoint_plain_text_object_unchanged(
195 client: AsyncClient,
196 monkeypatch: pytest.MonkeyPatch,
197 ) -> None:
198 """GET /owner/repo/raw/ref/README.md must pass through plain text unchanged."""
199 from musehub.api.routes.musehub import ui_tree
200
201 monkeypatch.setattr(
202 ui_tree,
203 "_resolve_repo",
204 AsyncMock(return_value=("repo-id-1", MagicMock(), MagicMock())),
205 )
206 monkeypatch.setattr(
207 ui_tree.musehub_repository,
208 "get_file_at_ref",
209 AsyncMock(return_value={"object_id": "deadbeef"}),
210 )
211
212 mock_storage = MagicMock()
213 mock_storage.exists = AsyncMock(return_value=True)
214
215 async def _plain_stream(object_id: str, chunk_size: int = 65536) -> None:
216 yield RAW_README
217
218 mock_storage.stream = _plain_stream
219 monkeypatch.setattr(ui_tree, "_get_storage_backend", lambda *_: mock_storage)
220
221 resp = await client.get("/gabriel/muse-zsh/raw/main/README.md")
222 assert resp.status_code == 200
223 assert resp.text == README_TEXT
224
225
226 @pytest.mark.asyncio
227 async def test_raw_endpoint_zlib_toml_file_decompressed(
228 client: AsyncClient,
229 monkeypatch: pytest.MonkeyPatch,
230 ) -> None:
231 """Non-README files (e.g. .toml) are also decompressed when zlib-stored."""
232 from musehub.api.routes.musehub import ui_tree
233
234 toml_content = '[workspace]\nversion = 1\n'
235 compressed_toml = zlib.compress(toml_content.encode())
236
237 monkeypatch.setattr(
238 ui_tree,
239 "_resolve_repo",
240 AsyncMock(return_value=("repo-id-1", MagicMock(), MagicMock())),
241 )
242 monkeypatch.setattr(
243 ui_tree.musehub_repository,
244 "get_file_at_ref",
245 AsyncMock(return_value={"object_id": "deadbeef"}),
246 )
247
248 mock_storage = MagicMock()
249 mock_storage.exists = AsyncMock(return_value=True)
250
251 async def _compressed_stream(object_id: str, chunk_size: int = 65536) -> None:
252 yield compressed_toml
253
254 mock_storage.stream = _compressed_stream
255 monkeypatch.setattr(ui_tree, "_get_storage_backend", lambda *_: mock_storage)
256
257 resp = await client.get("/gabriel/muse-zsh/raw/main/.museattributes")
258 assert resp.status_code == 200
259 assert resp.text == toml_content
260
261
262 @pytest.mark.asyncio
263 async def test_raw_endpoint_binary_file_not_decompressed(
264 client: AsyncClient,
265 monkeypatch: pytest.MonkeyPatch,
266 ) -> None:
267 """Binary files (PNG etc.) must not be decompressed — their bytes are served raw."""
268 from musehub.api.routes.musehub import ui_tree
269
270 # PNG magic — starts with bytes that are NOT a zlib header
271 png_bytes = b"\x89PNG\r\n\x1a\n" + b"\x00" * 50
272
273 monkeypatch.setattr(
274 ui_tree,
275 "_resolve_repo",
276 AsyncMock(return_value=("repo-id-1", MagicMock(), MagicMock())),
277 )
278 monkeypatch.setattr(
279 ui_tree.musehub_repository,
280 "get_file_at_ref",
281 AsyncMock(return_value={"object_id": "deadbeef"}),
282 )
283
284 mock_storage = MagicMock()
285 mock_storage.exists = AsyncMock(return_value=True)
286
287 async def _png_stream(object_id: str, chunk_size: int = 65536) -> None:
288 yield png_bytes
289
290 mock_storage.stream = _png_stream
291 monkeypatch.setattr(ui_tree, "_get_storage_backend", lambda *_: mock_storage)
292
293 resp = await client.get("/gabriel/muse-zsh/raw/main/logo.png")
294 assert resp.status_code == 200
295 assert resp.content == png_bytes
File History 1 commit
sha256:a34090cc4a394a78bd72cbbe34b08cc59525141e19135b6c0ab154f10611b9ef debug(push/stream): instrument O-frame decode path with INF… Sonnet 4.6 patch 121 days ago