Spaces:
Sleeping
Sleeping
| """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" | |
| 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() | |
| def http(): | |
| with httpx.Client(base_url=BASE_URL, timeout=180.0) as client: | |
| yield client | |
| 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 | |