diff --git a/src/kb/storage/objects.py b/src/kb/storage/objects.py index a11d1e5..aae93be 100644 --- a/src/kb/storage/objects.py +++ b/src/kb/storage/objects.py @@ -2,7 +2,7 @@ Both raw files and cached parse artifacts (Unstructured element JSON) live here. Keying convention: - raw/// + raw// parse//elements.json """ @@ -125,7 +125,7 @@ def _get_backend() -> _Backend: async def put_raw_file(*, domain: str, filename: str, blob: bytes) -> tuple[str, str]: """Store a raw upload; returns (object_key, content_hash). Idempotent by content_hash.""" h = _content_hash(blob) - key = f"raw/{domain}/{h}/{filename}" + key = f"raw/{domain}/{h}" backend = _get_backend() if not await backend.exists(key): await backend.put(key, blob) diff --git a/tests/test_objects.py b/tests/test_objects.py new file mode 100644 index 0000000..d7837f0 --- /dev/null +++ b/tests/test_objects.py @@ -0,0 +1,33 @@ +"""Raw object store keying and idempotency.""" + +from __future__ import annotations + +import asyncio +import hashlib + +from kb.storage import objects + + +class FakeBackend: + def __init__(self) -> None: + self.keys: list[str] = [] + self._exists: set[str] = set() + + async def exists(self, key: str) -> bool: + return key in self._exists + + async def put(self, key: str, blob: bytes, mime: str | None = None) -> None: + self.keys.append(key) + self._exists.add(key) + + +def test_put_raw_file_is_idempotent_by_content_hash(monkeypatch) -> None: + backend = FakeBackend() + monkeypatch.setattr(objects, "_backend", backend) + + first = asyncio.run(objects.put_raw_file(domain="sec", filename="a.pdf", blob=b"same")) + second = asyncio.run(objects.put_raw_file(domain="sec", filename="b.pdf", blob=b"same")) + + expected_key = f"raw/sec/{hashlib.sha256(b'same').hexdigest()}" + assert first == second + assert backend.keys == [expected_key]