gabriel / musehub public
test_storage_backends.py python
628 lines 25.7 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 156 days ago
1 """Section 33 — Storage Backends: 7-layer test suite.
2
3 Covers:
4 musehub/storage/backends.py — StorageBackend protocol, LocalBackend, S3Backend, get_backend, decode_b64
5
6 Key behaviour under test:
7 LocalBackend:
8 - put/get/exists/delete round-trip
9 - _path sanitises repo_id and object_id (strips colon/slash from object_id)
10 - path traversal in repo_id raises ValueError
11 - put is idempotent (second write to same path is a no-op)
12 - uri_for returns "local://" prefix
13 - missing object → get returns None, exists returns False, delete is a no-op
14
15 S3Backend:
16 - _key replaces colons with underscores
17 - uri_for returns "s3://<bucket>/..." prefix
18 - put/get/exists/delete delegated to boto3 client (tested via mock)
19 - credential/boto3 errors on get/exists → return None/False (no raise)
20 - _s3_put always calls put_object (idempotent — HeadObject check removed; deduplication is upstream)
21
22 get_backend:
23 - returns LocalBackend when aws_s3_asset_bucket is unset
24 - returns S3Backend when aws_s3_asset_bucket is set
25
26 decode_b64:
27 - decodes standard base64
28 - handles missing padding (1, 2, 3 missing "=" chars)
29 """
30 from __future__ import annotations
31
32 import asyncio
33 import time
34 import uuid
35 from pathlib import Path
36 from types import SimpleNamespace
37 from typing import TypedDict
38 from unittest.mock import MagicMock, patch
39
40 import pytest
41
42 from musehub.storage.backends import LocalBackend, S3Backend, decode_b64, get_backend
43
44
45 # ── helpers ───────────────────────────────────────────────────────────────────
46
47
48 class _S3GetResponse(TypedDict):
49 Body: MagicMock
50
51
52 def _uid() -> str:
53 return str(uuid.uuid4())
54
55
56 def _backend(tmp_path: Path) -> LocalBackend:
57 return LocalBackend(objects_dir=str(tmp_path / "objects"))
58
59
60 # ═══════════════════════════════════════════════════════════════════════════════
61 # Layer 1 — Unit (pure logic, no filesystem I/O)
62 # ═══════════════════════════════════════════════════════════════════════════════
63
64
65 class TestUnitLocalBackend:
66 def test_path_basic(self, tmp_path: Path) -> None:
67 b = _backend(tmp_path)
68 p = b._path("abc123")
69 assert p.name == "abc123"
70 assert str(p).startswith(str(tmp_path))
71
72 def test_path_sanitises_colon_in_object_id(self, tmp_path: Path) -> None:
73 b = _backend(tmp_path)
74 p = b._path("sha256:deadbeef")
75 assert ":" not in p.name
76 assert p.name == "sha256_deadbeef"
77
78 def test_path_sanitises_slash_in_object_id(self, tmp_path: Path) -> None:
79 b = _backend(tmp_path)
80 p = b._path("some/nested/id")
81 assert "/" not in p.name
82
83 def test_path_sanitises_dotdot_in_object_id(self, tmp_path: Path) -> None:
84 """Path separators are sanitised so object_id cannot escape storage root."""
85 b = _backend(tmp_path)
86 p = b._path("../../etc/passwd")
87 assert str(p).startswith(str(tmp_path))
88
89 def test_path_dotdot_deep_stays_in_root(self, tmp_path: Path) -> None:
90 b = _backend(tmp_path)
91 p = b._path("../../../etc/shadow")
92 assert str(p).startswith(str(tmp_path))
93
94 def test_uri_for_has_local_prefix(self, tmp_path: Path) -> None:
95 b = _backend(tmp_path)
96 uri = b.uri_for("obj1")
97 assert uri.startswith("local://")
98
99 def test_uri_for_contains_obj(self, tmp_path: Path) -> None:
100 b = _backend(tmp_path)
101 uri = b.uri_for("myobj")
102 assert "myobj" in uri
103
104
105 class TestUnitS3Backend:
106 def test_key_basic(self) -> None:
107 b = S3Backend(bucket="test-bucket", region="us-east-1")
108 key = b._key("abc123")
109 assert "abc123" in key
110
111 def test_key_replaces_colon(self) -> None:
112 b = S3Backend(bucket="b", region="us-east-1")
113 key = b._key("sha256:deadbeef")
114 assert ":" not in key
115 assert "sha256_deadbeef" in key
116
117 def test_uri_for_s3_prefix(self) -> None:
118 b = S3Backend(bucket="my-bucket", region="us-east-1")
119 uri = b.uri_for("obj1")
120 assert uri.startswith("s3://my-bucket/")
121
122 def test_uri_for_no_colon_in_key(self) -> None:
123 b = S3Backend(bucket="b", region="us-east-1")
124 uri = b.uri_for("sha256:abc")
125 assert "sha256:abc" not in uri
126 assert "sha256_abc" in uri
127
128
129 class TestUnitDecodeb64:
130 def test_standard_base64(self) -> None:
131 import base64
132 data = b"hello world"
133 encoded = base64.b64encode(data).decode()
134 assert decode_b64(encoded) == data
135
136 def test_missing_one_padding(self) -> None:
137 import base64
138 data = b"hi"
139 encoded = base64.b64encode(data).decode().rstrip("=")
140 assert decode_b64(encoded) == data
141
142 def test_missing_two_padding(self) -> None:
143 import base64
144 data = b"h"
145 encoded = base64.b64encode(data).decode().rstrip("=")
146 assert decode_b64(encoded) == data
147
148 def test_already_padded(self) -> None:
149 import base64
150 data = b"test"
151 encoded = base64.b64encode(data).decode()
152 assert decode_b64(encoded) == data
153
154 def test_empty_string(self) -> None:
155 assert decode_b64("") == b""
156
157
158 class TestUnitGetBackend:
159 def test_get_backend_returns_local_when_no_s3_bucket(self) -> None:
160 with patch("musehub.storage.backends.settings") as mock_settings:
161 mock_settings.r2_bucket = None
162 mock_settings.aws_s3_asset_bucket = None
163 mock_settings.musehub_objects_dir = "/tmp/test-objects"
164 result = get_backend()
165 assert isinstance(result, LocalBackend)
166
167 def test_get_backend_returns_s3_when_bucket_set(self) -> None:
168 with patch("musehub.storage.backends.settings") as mock_settings:
169 mock_settings.aws_s3_asset_bucket = "my-bucket"
170 mock_settings.aws_region = "us-east-1"
171 result = get_backend()
172 assert isinstance(result, S3Backend)
173
174
175 # ═══════════════════════════════════════════════════════════════════════════════
176 # Layer 2 — Integration (real filesystem I/O via tmp_path)
177 # ═══════════════════════════════════════════════════════════════════════════════
178
179
180 class TestIntegrationLocalBackend:
181 async def test_put_and_get_round_trip(self, tmp_path: Path) -> None:
182 b = _backend(tmp_path)
183 data = b"hello storage"
184 uri = await b.put("obj1", data)
185 assert uri.startswith("local://")
186 result = await b.get("obj1")
187 assert result == data
188
189 async def test_get_missing_returns_none(self, tmp_path: Path) -> None:
190 b = _backend(tmp_path)
191 result = await b.get("nonexistent")
192 assert result is None
193
194 async def test_exists_after_put(self, tmp_path: Path) -> None:
195 b = _backend(tmp_path)
196 assert await b.exists("obj1") is False
197 await b.put("obj1", b"data")
198 assert await b.exists("obj1") is True
199
200 async def test_exists_missing_returns_false(self, tmp_path: Path) -> None:
201 b = _backend(tmp_path)
202 assert await b.exists("ghost") is False
203
204 async def test_delete_removes_object(self, tmp_path: Path) -> None:
205 b = _backend(tmp_path)
206 await b.put("obj1", b"bye")
207 assert await b.exists("obj1") is True
208 await b.delete("obj1")
209 assert await b.exists("obj1") is False
210
211 async def test_delete_nonexistent_is_noop(self, tmp_path: Path) -> None:
212 b = _backend(tmp_path)
213 # Must not raise
214 await b.delete("ghost")
215
216 async def test_put_idempotent_same_bytes(self, tmp_path: Path) -> None:
217 """Putting the same bytes twice is a no-op — content-addressed dedup."""
218 b = _backend(tmp_path)
219 await b.put("obj1", b"content")
220 await b.put("obj1", b"content") # identical — _write skips
221 result = await b.get("obj1")
222 assert result == b"content"
223
224 async def test_put_creates_parent_dirs(self, tmp_path: Path) -> None:
225 b = _backend(tmp_path)
226 await b.put("obj", b"data")
227 assert await b.exists("obj") is True
228
229 async def test_put_colon_in_object_id(self, tmp_path: Path) -> None:
230 b = _backend(tmp_path)
231 data = b"sha content"
232 await b.put("sha256:abc", data)
233 result = await b.get("sha256:abc")
234 assert result == data
235
236 async def test_global_storage_same_id_shares_object(self, tmp_path: Path) -> None:
237 """With global content-addressed storage, same object_id is shared across repos."""
238 b = _backend(tmp_path)
239 await b.put("obj", b"shared data")
240 # Same object_id accessible from any repo_id — content-addressed globally
241 assert await b.get("obj") == b"shared data"
242 assert await b.get("obj") == b"shared data"
243
244
245 class TestIntegrationS3BackendMocked:
246 """S3Backend with a mock boto3 client — no real AWS calls."""
247
248 def _mock_s3_backend(self, *, head_raises: bool = False) -> S3Backend:
249 b = S3Backend(bucket="test-bucket", region="us-east-1")
250 mock_client = MagicMock()
251 if head_raises:
252 mock_client.head_object.side_effect = Exception("NoSuchKey")
253 b._client = mock_client
254 return b
255
256 async def test_put_calls_put_object_directly(self) -> None:
257 """put() is idempotent — no HeadObject check, just put_object unconditionally."""
258 b = self._mock_s3_backend()
259 await b.put("obj1", b"data")
260 b._client.put_object.assert_called_once()
261 b._client.head_object.assert_not_called()
262
263 async def test_put_is_idempotent(self) -> None:
264 """Uploading the same object twice both succeed — put_object called twice."""
265 b = self._mock_s3_backend()
266 await b.put("obj1", b"data")
267 await b.put("obj1", b"data")
268 assert b._client.put_object.call_count == 2
269
270 async def test_get_returns_body_bytes(self) -> None:
271 b = self._mock_s3_backend()
272 mock_body = MagicMock()
273 mock_body.read.return_value = b"s3 content"
274 b._client.get_object.return_value = {"Body": mock_body}
275 result = await b.get("obj1")
276 assert result == b"s3 content"
277
278 async def test_get_returns_none_on_error(self) -> None:
279 b = self._mock_s3_backend()
280 b._client.get_object.side_effect = Exception("NoSuchKey")
281 result = await b.get("obj1")
282 assert result is None
283
284 async def test_exists_returns_true_when_head_succeeds(self) -> None:
285 b = self._mock_s3_backend()
286 b._client.head_object.return_value = {}
287 assert await b.exists("obj1") is True
288
289 async def test_exists_returns_false_on_error(self) -> None:
290 b = self._mock_s3_backend(head_raises=True)
291 assert await b.exists("obj1") is False
292
293 async def test_delete_calls_delete_object(self) -> None:
294 b = self._mock_s3_backend()
295 await b.delete("obj1")
296 b._client.delete_object.assert_called_once()
297
298 async def test_get_batch_returns_all_found_objects(self) -> None:
299 """get_batch maps every object_id to its bytes when S3 finds all of them."""
300 b = self._mock_s3_backend()
301
302 def _get_object(Bucket: str, Key: str) -> _S3GetResponse:
303 # Return the key bytes as the body so we can assert on content
304 mock_body = MagicMock()
305 mock_body.read.return_value = Key.encode()
306 return {"Body": mock_body}
307
308 b._client.get_object.side_effect = _get_object
309 result = await b.get_batch(["oid-a", "oid-b", "oid-c"])
310 assert set(result.keys()) == {"oid-a", "oid-b", "oid-c"}
311
312 async def test_get_batch_omits_missing_objects(self) -> None:
313 """get_batch omits object_ids where S3 raises (NoSuchKey etc.)."""
314 b = self._mock_s3_backend()
315 b._client.get_object.side_effect = Exception("NoSuchKey")
316 result = await b.get_batch(["oid-1", "oid-2"])
317 assert result == {}
318
319 async def test_get_batch_is_parallel_not_sequential(self) -> None:
320 """N parallel S3 gets must complete in ~1× per-object latency, not N×.
321
322 The base-class fallback is sequential; S3Backend must override get_batch
323 with asyncio.gather so that cloning large repos cannot time out at
324 Cloudflare's 100-second origin-response limit.
325 """
326 import asyncio
327
328 DELAY = 0.04 # 40 ms per object
329 N = 6
330
331 b = S3Backend(bucket="test-bucket", region="us-east-1")
332
333 # Patch get() with an async stub that sleeps to simulate network latency.
334 async def _slow_get(oid: str) -> bytes | None:
335 await asyncio.sleep(DELAY)
336 return oid.encode()
337
338 setattr(b, "get", _slow_get)
339
340 obj_ids = [f"oid-{i}" for i in range(N)]
341 start = time.perf_counter()
342 result = await b.get_batch(obj_ids)
343 elapsed = time.perf_counter() - start
344
345 assert len(result) == N
346 # Parallel: completes in roughly 1× DELAY.
347 # Sequential (base-class fallback): would take N× DELAY ≈ 240 ms.
348 assert elapsed < DELAY * 2.5, (
349 f"get_batch took {elapsed * 1000:.0f} ms; expected ≈{DELAY * 1000:.0f} ms "
350 f"(parallel). Sequential would take ≥{DELAY * N * 1000:.0f} ms. "
351 "S3Backend.get_batch must use asyncio.gather, not the base-class loop."
352 )
353
354
355 # ═══════════════════════════════════════════════════════════════════════════════
356 # Layer 3 — End-to-End (LocalBackend as the full stack backend)
357 # ═══════════════════════════════════════════════════════════════════════════════
358
359
360 class TestE2ELocalBackend:
361 async def test_full_lifecycle(self, tmp_path: Path) -> None:
362 """put → exists(True) → get → delete → exists(False) → get(None)."""
363 b = _backend(tmp_path)
364 repo_id = _uid()
365 obj_id = _uid()
366 data = b"end-to-end content"
367
368 uri = await b.put(obj_id, data)
369 assert uri.startswith("local://")
370
371 assert await b.exists(obj_id) is True
372 assert await b.get(obj_id) == data
373
374 await b.delete(obj_id)
375 assert await b.exists(obj_id) is False
376 assert await b.get(obj_id) is None
377
378 async def test_binary_data_preserved(self, tmp_path: Path) -> None:
379 b = _backend(tmp_path)
380 data = bytes(range(256)) # all byte values
381 await b.put("bin-obj", data)
382 result = await b.get("bin-obj")
383 assert result == data
384
385 async def test_large_object(self, tmp_path: Path) -> None:
386 b = _backend(tmp_path)
387 data = b"x" * (4 * 1024 * 1024) # 4 MiB
388 await b.put("large-obj", data)
389 result = await b.get("large-obj")
390 assert result == data
391
392 async def test_uri_resolves_to_disk_path(self, tmp_path: Path) -> None:
393 b = _backend(tmp_path)
394 data = b"uri content"
395 uri = await b.put("uri-obj", data)
396 disk_path = Path(uri.replace("local://", ""))
397 assert disk_path.exists()
398 assert disk_path.read_bytes() == data
399
400
401 # ═══════════════════════════════════════════════════════════════════════════════
402 # Layer 4 — Stress
403 # ═══════════════════════════════════════════════════════════════════════════════
404
405
406 class TestStressLocalBackend:
407 async def test_write_many_objects(self, tmp_path: Path) -> None:
408 b = _backend(tmp_path)
409 n = 100
410 obj_ids = [f"obj-{i}" for i in range(n)]
411 for obj_id in obj_ids:
412 await b.put(obj_id, obj_id.encode())
413
414 for obj_id in obj_ids:
415 assert await b.exists(obj_id) is True
416
417 async def test_concurrent_writes_different_objects(self, tmp_path: Path) -> None:
418 b = _backend(tmp_path)
419 obj_ids = [f"concurrent-{i}" for i in range(20)]
420 await asyncio.gather(*[b.put(oid, oid.encode()) for oid in obj_ids])
421 for oid in obj_ids:
422 assert await b.get(oid) == oid.encode()
423
424 async def test_concurrent_reads(self, tmp_path: Path) -> None:
425 b = _backend(tmp_path)
426 await b.put("shared", b"shared content")
427 results = await asyncio.gather(*[b.get("shared") for _ in range(20)])
428 assert all(r == b"shared content" for r in results)
429
430 async def test_write_many_objects_different_ids(self, tmp_path: Path) -> None:
431 """Write many distinct object_ids (global storage, repo_id is ignored for path)."""
432 b = _backend(tmp_path)
433 obj_ids = [f"obj-stress-{i}" for i in range(30)]
434 for obj_id in obj_ids:
435 await b.put(obj_id, obj_id.encode())
436 for obj_id in obj_ids:
437 assert await b.get(obj_id) == obj_id.encode()
438
439
440 # ═══════════════════════════════════════════════════════════════════════════════
441 # Layer 5 — Data Integrity
442 # ═══════════════════════════════════════════════════════════════════════════════
443
444
445 class TestDataIntegrityLocalBackend:
446 async def test_put_returns_deterministic_uri(self, tmp_path: Path) -> None:
447 b = _backend(tmp_path)
448 uri1 = await b.put("obj1", b"data")
449 uri2 = b.uri_for("obj1")
450 assert uri1 == uri2
451
452 async def test_data_not_corrupted(self, tmp_path: Path) -> None:
453 b = _backend(tmp_path)
454 payload = b"\x00\x01\x02\x03" * 1000
455 await b.put("integrity", payload)
456 result = await b.get("integrity")
457 assert result == payload
458
459 async def test_put_is_idempotent_for_identical_bytes(self, tmp_path: Path) -> None:
460 """put() with the same bytes twice is a no-op — content stays unchanged."""
461 b = _backend(tmp_path)
462 await b.put("obj", b"original")
463 await b.put("obj", b"original")
464 result = await b.get("obj")
465 assert result == b"original"
466
467 async def test_put_repairs_corrupted_file(self, tmp_path: Path) -> None:
468 """put() must overwrite an existing file whose bytes differ from the new data.
469
470 The legacy wire_push_objects endpoint stored zlib-compressed wire bytes
471 under the SHA-256 of the raw (decompressed) content — a hash/content
472 mismatch. When the pack endpoint later pushes the correct raw bytes for
473 the same object_id, put() must overwrite the corrupted file so that
474 subsequent get() calls return the real content, not the old zlib garbage.
475 """
476 import zlib
477 b = _backend(tmp_path)
478 raw = b"# real raw content"
479 corrupt = zlib.compress(raw) # what the legacy endpoint mistakenly stored
480 object_id = "obj-sha256-of-raw"
481
482 # Simulate legacy corruption: store compressed bytes directly on disk.
483 path = b._path(object_id)
484 path.parent.mkdir(parents=True, exist_ok=True)
485 path.write_bytes(corrupt)
486 import stat
487 path.chmod(stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) # immutable, as _write does
488
489 # Pack endpoint now puts the correct raw bytes.
490 await b.put(object_id, raw)
491
492 # get() must return the raw bytes, not the old compressed garbage.
493 result = await b.get(object_id)
494 assert result == raw, (
495 f"expected raw content but got {result[:20]!r} — "
496 "LocalBackend.put() failed to repair the corrupted file"
497 )
498
499 async def test_delete_only_removes_target(self, tmp_path: Path) -> None:
500 b = _backend(tmp_path)
501 await b.put("keep", b"keep")
502 await b.put("remove", b"remove")
503 await b.delete("remove")
504 assert await b.get("keep") == b"keep"
505 assert await b.get("remove") is None
506
507 def test_path_traversal_variations_sanitised(self, tmp_path: Path) -> None:
508 """object_id path separators are sanitised — all paths stay inside root."""
509 b = _backend(tmp_path)
510 traversal_attempts = [
511 "../../../etc",
512 "repo/../../../etc",
513 "repo/../../secret",
514 ]
515 for obj_id in traversal_attempts:
516 p = b._path(obj_id)
517 assert str(p).startswith(str(tmp_path))
518
519 async def test_empty_bytes_stored_and_retrieved(self, tmp_path: Path) -> None:
520 b = _backend(tmp_path)
521 await b.put("empty-obj", b"")
522 result = await b.get("empty-obj")
523 assert result == b""
524
525
526 # ═══════════════════════════════════════════════════════════════════════════════
527 # Layer 6 — Security
528 # ═══════════════════════════════════════════════════════════════════════════════
529
530
531 class TestSecurityLocalBackend:
532 def test_dotdot_in_object_id_sanitised(self, tmp_path: Path) -> None:
533 """Path separators in object_id are sanitised — no traversal possible."""
534 b = _backend(tmp_path)
535 p = b._path("../secrets")
536 assert str(p).startswith(str(tmp_path))
537
538 def test_dotdot_deep_in_object_id_sanitised(self, tmp_path: Path) -> None:
539 """Multiple levels of '..' in object_id are sanitised."""
540 b = _backend(tmp_path)
541 p = b._path("../../../../../../etc/passwd")
542 assert str(p).startswith(str(tmp_path))
543
544 def test_object_id_colon_sanitised_prevents_ambiguity(self, tmp_path: Path) -> None:
545 b = _backend(tmp_path)
546 p1 = b._path("sha256:abc")
547 p2 = b._path("sha256_abc")
548 # Both sanitise to the same safe filename
549 assert p1 == p2
550
551 async def test_put_with_dotdot_object_id_sanitised(self, tmp_path: Path) -> None:
552 """put() sanitises object_id path separators — stays inside root."""
553 b = _backend(tmp_path)
554 uri = await b.put("../outside", b"data")
555 assert uri.startswith("local://")
556 disk = uri.replace("local://", "")
557 assert str(tmp_path) in disk
558
559 async def test_get_with_dotdot_object_id_sanitised(self, tmp_path: Path) -> None:
560 """get() sanitises object_id path separators."""
561 b = _backend(tmp_path)
562 result = await b.get("../../etc/passwd")
563 assert result is None # file doesn't exist, but no error raised
564
565 async def test_exists_with_dotdot_object_id_sanitised(self, tmp_path: Path) -> None:
566 """exists() sanitises object_id path separators."""
567 b = _backend(tmp_path)
568 result = await b.exists("../outside")
569 assert result is False # sanitised path doesn't exist
570
571 async def test_delete_with_dotdot_object_id_is_noop(self, tmp_path: Path) -> None:
572 """delete() sanitises object_id — no error when file doesn't exist."""
573 b = _backend(tmp_path)
574 await b.delete("../outside") # must not raise
575
576 def test_s3_key_no_colon_injection(self) -> None:
577 b = S3Backend(bucket="b", region="us-east-1")
578 key = b._key("sha256:evil:colons")
579 assert ":" not in key
580
581
582 # ═══════════════════════════════════════════════════════════════════════════════
583 # Layer 7 — Performance
584 # ═══════════════════════════════════════════════════════════════════════════════
585
586
587 class TestPerformanceLocalBackend:
588 async def test_put_latency(self, tmp_path: Path) -> None:
589 b = _backend(tmp_path)
590 data = b"perf-test-payload" * 100
591 start = time.perf_counter()
592 await b.put("perf-obj", data)
593 elapsed = time.perf_counter() - start
594 assert elapsed < 0.5
595
596 async def test_get_latency(self, tmp_path: Path) -> None:
597 b = _backend(tmp_path)
598 data = b"x" * (512 * 1024) # 512 KiB
599 await b.put("big-obj", data)
600
601 start = time.perf_counter()
602 result = await b.get("big-obj")
603 elapsed = time.perf_counter() - start
604
605 assert result == data
606 assert elapsed < 0.5
607
608 async def test_exists_latency(self, tmp_path: Path) -> None:
609 b = _backend(tmp_path)
610 await b.put("perf-exists", b"data")
611
612 start = time.perf_counter()
613 for _ in range(50):
614 await b.exists("perf-exists")
615 elapsed = time.perf_counter() - start
616
617 assert elapsed < 1.0
618
619 async def test_50_sequential_puts_under_budget(self, tmp_path: Path) -> None:
620 b = _backend(tmp_path)
621 data = b"payload" * 100
622
623 start = time.perf_counter()
624 for i in range(50):
625 await b.put(f"seq-{i}", data)
626 elapsed = time.perf_counter() - start
627
628 assert elapsed < 2.0
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 156 days ago