mmap-worker / tests /integration /test_image_flow.py
jugalgajjar's picture
deploy: hybrid retrieval + reranker
7499087
Raw
History Blame Contribute Delete
4.52 kB
"""Integration: image upload → OCR + Vision → chunks + embeddings.
Generates a synthetic test PNG with PIL (available in the API container's
deps via sentence-transformers). The vision call hits the real OpenRouter
endpoint — the test tolerates upstream rate-limits so it isn't flaky against
free-tier quotas.
"""
import io
import time
import uuid
import httpx
import pytest
from tests.integration.conftest import STRONG_PASSWORD, mark_user_verified
pytestmark = pytest.mark.integration
BASE_URL = "http://127.0.0.1:8000/api/v1"
def unique_email() -> str:
return f"img-{uuid.uuid4().hex[:12]}@example.com"
@pytest.fixture(scope="module")
def sample_png_bytes() -> bytes:
"""Build a tiny diagram-style PNG with PIL."""
try:
from PIL import Image, ImageDraw # type: ignore[import-not-found]
except ImportError:
pytest.skip("Pillow not available in dev venv")
img = Image.new("RGB", (400, 200), "white")
draw = ImageDraw.Draw(img)
draw.rectangle([(0, 0), (400, 40)], fill="#1e40af")
draw.text((10, 12), "MMAP Test Diagram", fill="white")
draw.rectangle([(20, 60), (180, 140)], outline="black", width=2)
draw.text((40, 90), "Block A", fill="black")
draw.rectangle([(220, 60), (380, 140)], outline="black", width=2)
draw.text((250, 90), "Block B", fill="black")
draw.line([(180, 100), (220, 100)], fill="black", width=2)
draw.text((20, 160), "Caption text below", fill="black")
buf = io.BytesIO()
img.save(buf, format="PNG")
return buf.getvalue()
@pytest.fixture
def http():
with httpx.Client(base_url=BASE_URL, timeout=180.0) as client:
yield client
@pytest.fixture
def auth(http):
email = unique_email()
http.post(
"/auth/register",
json={
"email": email,
"password": STRONG_PASSWORD,
"first_name": "Test",
"last_name": "User",
},
)
mark_user_verified(email)
tok = http.post(
"/auth/login",
json={
"email": email,
"password": STRONG_PASSWORD,
"first_name": "Test",
"last_name": "User",
},
).json()["access_token"]
return {"Authorization": f"Bearer {tok}"}
def wait_for_terminal(http, headers, doc_id, *, timeout=180.0) -> str:
deadline = time.time() + timeout
last = None
while time.time() < deadline:
last = http.get(f"/documents/{doc_id}", headers=headers).json()["status"]
if last in ("processed", "failed"):
return last
time.sleep(0.5)
return last or "timeout"
def test_image_upload_round_trip(http, auth, sample_png_bytes):
"""Image processes successfully — Vision may be rate-limited, but the
pipeline must always at least return OCR text (degraded mode)."""
upload = http.post(
"/documents",
headers=auth,
files={"file": ("diagram.png", io.BytesIO(sample_png_bytes), "image/png")},
).json()
assert upload["content_type"] == "image/png"
status = wait_for_terminal(http, auth, upload["id"])
assert status == "processed", f"expected processed, got {status}"
text_resp = http.get(f"/documents/{upload['id']}/text", headers=auth).json()
text = text_resp["extracted_text"] or ""
# OCR section is always there (Tesseract is reliable).
assert "--- OCR text ---" in text, (
f"expected OCR section even with vision rate-limited; got: {text[:200]!r}"
)
# Some OCR token must have come through (block labels or caption).
assert any(token in text.lower() for token in ("block", "diagram", "caption", "mmap"))
def test_image_chunks_land_in_qdrant(http, auth, sample_png_bytes):
upload = http.post(
"/documents",
headers=auth,
files={"file": ("diagram-q.png", io.BytesIO(sample_png_bytes), "image/png")},
).json()
assert wait_for_terminal(http, auth, upload["id"]) == "processed"
me_id = http.get("/auth/me", headers=auth).json()["id"]
r = httpx.post(
"http://127.0.0.1:6333/collections/mmap_chunks_v2/points/scroll",
json={
"limit": 100,
"with_payload": True,
"with_vector": False,
"filter": {"must": [{"key": "document_id", "match": {"value": upload["id"]}}]},
},
)
points = r.json()["result"]["points"]
assert len(points) >= 1, "image should chunk and index even with OCR-only output"
for p in points:
assert p["payload"]["user_id"] == me_id