Spaces:
Paused
Paused
| """Tests for visual grounding infrastructure: resolver, ingest metadata, endpoints.""" | |
| from __future__ import annotations | |
| import pytest | |
| from fastapi.testclient import TestClient | |
| def client(): | |
| from auralynq.serving.app import create_app | |
| return TestClient(create_app()) | |
| # --------------------------------------------------------------------------- | |
| # Grounding resolver unit tests | |
| # --------------------------------------------------------------------------- | |
| def test_resolver_returns_empty_for_no_citations(): | |
| from auralynq.grounding.resolver import GroundingResolver | |
| resolver = GroundingResolver() | |
| results = resolver.resolve(answer_id="test", answer="Hello", citations=[]) | |
| assert results == [] | |
| def test_resolver_page_level_grounding(): | |
| """Citation with page number but no bbox → page-level grounding.""" | |
| from auralynq.grounding.resolver import GroundingResolver | |
| resolver = GroundingResolver() | |
| citations = [ | |
| { | |
| "marker": 1, | |
| "chunk_id": "chunk_001", | |
| "doc_id": "doc_001", | |
| "source": "test.pdf", | |
| "page": 3, | |
| "score": 0.8, | |
| "visual_grounding": { | |
| "grounding_version": 1, | |
| "page": 3, | |
| "has_bbox": False, | |
| "bbox": None, | |
| "normalized_bbox": None, | |
| }, | |
| } | |
| ] | |
| results = resolver.resolve("test_id", "Some answer [1].", citations) | |
| assert len(results) == 1 | |
| r = results[0] | |
| assert r.page == 3 | |
| assert r.doc_id == "doc_001" | |
| assert r.visual_grounding_available is True | |
| assert r.grounding_stage == "page" | |
| assert len(r.highlights) == 1 | |
| ev = r.highlights[0] | |
| assert ev.support_type == "page" | |
| assert ev.normalized_bbox is None | |
| assert ev.reindex_required is False | |
| def test_resolver_span_level_grounding(): | |
| """Citation with bbox → span-level grounding.""" | |
| from auralynq.grounding.resolver import GroundingResolver | |
| resolver = GroundingResolver() | |
| citations = [ | |
| { | |
| "marker": 1, | |
| "chunk_id": "chunk_002", | |
| "doc_id": "doc_002", | |
| "source": "paper.pdf", | |
| "page": 1, | |
| "score": 0.9, | |
| "visual_grounding": { | |
| "grounding_version": 1, | |
| "page": 1, | |
| "has_bbox": True, | |
| "bbox": [72.0, 100.0, 400.0, 120.0], | |
| "normalized_bbox": [0.1, 0.13, 0.55, 0.16], | |
| "block_type": "paragraph", | |
| }, | |
| } | |
| ] | |
| results = resolver.resolve("span_test", "Answer with span [1].", citations) | |
| assert len(results) == 1 | |
| r = results[0] | |
| assert r.grounding_stage == "span" | |
| ev = r.highlights[0] | |
| assert ev.support_type == "span" | |
| assert ev.normalized_bbox == [0.1, 0.13, 0.55, 0.16] | |
| assert ev.bbox == [72.0, 100.0, 400.0, 120.0] | |
| def test_resolver_reindex_required_for_old_doc(): | |
| """Citation with grounding_version=0 → reindex_required=True.""" | |
| from auralynq.grounding.resolver import GroundingResolver | |
| resolver = GroundingResolver() | |
| citations = [ | |
| { | |
| "marker": 1, | |
| "chunk_id": "old_chunk", | |
| "doc_id": "old_doc", | |
| "source": "legacy.pdf", | |
| "page": None, | |
| "score": 0.5, | |
| "visual_grounding": { | |
| "grounding_version": 0, | |
| "page": None, | |
| "has_bbox": False, | |
| }, | |
| } | |
| ] | |
| results = resolver.resolve("old_test", "Answer.", citations) | |
| assert len(results) == 1 | |
| ev = results[0].highlights[0] | |
| assert ev.reindex_required is True | |
| assert ev.support_type == "unavailable" | |
| def test_resolver_to_api_response(): | |
| """to_api_response produces correct structure.""" | |
| from auralynq.grounding.resolver import GroundingResolver | |
| resolver = GroundingResolver() | |
| citations = [ | |
| { | |
| "marker": 1, | |
| "chunk_id": "c1", | |
| "doc_id": "d1", | |
| "source": "doc.pdf", | |
| "page": 2, | |
| "score": 0.7, | |
| "visual_grounding": { | |
| "grounding_version": 1, | |
| "page": 2, | |
| "has_bbox": True, | |
| "bbox": [0, 0, 100, 20], | |
| "normalized_bbox": [0.0, 0.0, 0.14, 0.025], | |
| }, | |
| } | |
| ] | |
| results = resolver.resolve("api_test", "The answer is X [1].", citations) | |
| api_resp = resolver.to_api_response(results) | |
| assert "highlights" in api_resp | |
| assert "claim_grounding" in api_resp | |
| assert "warnings" in api_resp | |
| assert isinstance(api_resp["visual_grounding_available"], bool) | |
| assert api_resp["visual_grounding_available"] is True | |
| assert api_resp["grounding_stage"] == "span" | |
| assert len(api_resp["highlights"]) == 1 | |
| h = api_resp["highlights"][0] | |
| assert h["page_image_url"] == "/api/documents/d1/pages/2/image" | |
| def test_resolver_page_image_url_format(): | |
| from auralynq.grounding.resolver import GroundingResolver | |
| resolver = GroundingResolver() | |
| citations = [ | |
| { | |
| "marker": 2, | |
| "chunk_id": "c2", | |
| "doc_id": "abc123", | |
| "source": "file.pdf", | |
| "page": 5, | |
| "score": 0.6, | |
| "visual_grounding": {"grounding_version": 1, "page": 5, "has_bbox": False}, | |
| } | |
| ] | |
| results = resolver.resolve("url_test", "Answer.", citations) | |
| assert results[0].page_image_url == "/api/documents/abc123/pages/5/image" | |
| def test_resolver_claim_grounding_unsupported_without_citations(): | |
| from auralynq.grounding.resolver import GroundingResolver | |
| resolver = GroundingResolver() | |
| # Long answer sentences with no citations matching | |
| citations = [ | |
| { | |
| "marker": 1, | |
| "chunk_id": "c3", | |
| "doc_id": "d3", | |
| "source": "x.pdf", | |
| "page": 1, | |
| "score": 0.5, | |
| "visual_grounding": {"grounding_version": 1, "page": 1, "has_bbox": False}, | |
| } | |
| ] | |
| results = resolver.resolve( | |
| "claim_test", "This is a long answer sentence. [1] Another unrelated claim here.", citations | |
| ) | |
| # Claim grounding should exist | |
| all_claims = [cg for r in results for cg in r.claim_grounding] | |
| assert len(all_claims) >= 1 | |
| # --------------------------------------------------------------------------- | |
| # Ingest model tests | |
| # --------------------------------------------------------------------------- | |
| def test_visual_grounding_version_constant(): | |
| from auralynq.ingest.models import VISUAL_GROUNDING_VERSION | |
| assert VISUAL_GROUNDING_VERSION == 1 | |
| def test_document_has_visual_grounding_property(): | |
| from auralynq.ingest.models import Document, SourceType | |
| doc = Document( | |
| id="d1", | |
| source="test.pdf", | |
| source_type=SourceType.pdf, | |
| visual_grounding_version=1, | |
| ) | |
| assert doc.has_visual_grounding is True | |
| doc_old = Document( | |
| id="d2", | |
| source="old.pdf", | |
| source_type=SourceType.pdf, | |
| visual_grounding_version=0, | |
| ) | |
| assert doc_old.has_visual_grounding is False | |
| def test_layout_block_model(): | |
| from auralynq.ingest.models import LayoutBlock | |
| blk = LayoutBlock( | |
| page=1, | |
| bbox=[72.0, 100.0, 400.0, 120.0], | |
| normalized_bbox=[0.1, 0.13, 0.55, 0.16], | |
| text="Sample text", | |
| block_type="paragraph", | |
| reading_order=3, | |
| page_width=720.0, | |
| page_height=960.0, | |
| ) | |
| assert blk.page == 1 | |
| assert blk.block_type == "paragraph" | |
| assert blk.normalized_bbox[0] == pytest.approx(0.1) | |
| # --------------------------------------------------------------------------- | |
| # Settings tests | |
| # --------------------------------------------------------------------------- | |
| def test_visual_grounding_settings_defaults(): | |
| from auralynq.config.settings import VisualGroundingSettings | |
| vgs = VisualGroundingSettings() | |
| assert vgs.enabled is True | |
| assert vgs.page_rendering_enabled is True | |
| assert vgs.render_dpi == 144 | |
| assert vgs.max_cached_pages == 500 | |
| assert vgs.visual_retrieval_enabled is False | |
| assert vgs.metadata_version == 1 | |
| def test_settings_has_visual(): | |
| import os | |
| os.environ["AURALYNQ_DOTENV_DISABLED"] = "1" | |
| from auralynq.config.settings import Settings | |
| s = Settings(_env_file=None) # type: ignore | |
| assert hasattr(s, "visual") | |
| assert s.visual.enabled is True | |
| # --------------------------------------------------------------------------- | |
| # API endpoint tests | |
| # --------------------------------------------------------------------------- | |
| def test_visual_grounding_settings_endpoint(client): | |
| resp = client.get("/visual-grounding/settings") | |
| assert resp.status_code == 200 | |
| data = resp.json() | |
| assert "enabled" in data | |
| assert "page_rendering_enabled" in data | |
| assert "render_dpi" in data | |
| assert "metadata_version" in data | |
| def test_document_pages_invalid_doc_id_returns_result(client): | |
| """Valid request format but unknown doc_id → empty result (not error).""" | |
| resp = client.get("/documents/abc123def456/pages") | |
| # Should return 200 with empty page list (doc not found = empty metadata) | |
| assert resp.status_code == 200 | |
| data = resp.json() | |
| assert "doc_id" in data | |
| assert "pages" in data | |
| def test_document_page_image_invalid_doc_id_rejected(client): | |
| """Path traversal attempt should be rejected.""" | |
| resp = client.get("/documents/../etc/passwd/pages/1/image") | |
| # Either 400, 404, or the path is blocked by FastAPI routing | |
| assert resp.status_code in (400, 404, 422) | |
| def test_document_page_image_bad_doc_id_rejected(client): | |
| """Doc ID with invalid chars should return 400.""" | |
| resp = client.get("/documents/../../etc/pages/1/image") | |
| assert resp.status_code in (400, 404, 422) | |
| def test_document_grounding_status_endpoint(client): | |
| resp = client.get("/documents/abc123def456/grounding-status") | |
| assert resp.status_code == 200 | |
| data = resp.json() | |
| assert "doc_id" in data | |
| assert "reindex_required" in data | |
| assert "grounding_available" in data | |
| # --------------------------------------------------------------------------- | |
| # Strategy registry: new experimental strategies | |
| # --------------------------------------------------------------------------- | |
| def test_self_rag_strategy_available(): | |
| from auralynq.rag import get_registry | |
| registry = get_registry() | |
| strategies = registry.list_all() | |
| sr = next(s for s in strategies if s["id"] == "self_rag") | |
| assert sr["available"] is True | |
| assert sr["status"] == "experimental" | |
| def test_crag_strategy_available(): | |
| from auralynq.rag import get_registry | |
| registry = get_registry() | |
| strategies = registry.list_all() | |
| crag = next(s for s in strategies if s["id"] == "crag") | |
| assert crag["available"] is True | |
| assert crag["status"] == "experimental" | |
| def test_adaptive_strategy_available(): | |
| from auralynq.rag import get_registry | |
| registry = get_registry() | |
| strategies = registry.list_all() | |
| adaptive = next(s for s in strategies if s["id"] == "adaptive") | |
| assert adaptive["available"] is True | |
| assert adaptive["status"] == "experimental" | |
| def test_lightrag_local_unavailable(): | |
| from auralynq.rag import get_registry | |
| registry = get_registry() | |
| strategies = registry.list_all() | |
| lr = next(s for s in strategies if s["id"] == "lightrag_local") | |
| assert lr["available"] is False | |
| assert lr["unavailable_reason"] is not None | |
| def test_raptor_unavailable(): | |
| from auralynq.rag import get_registry | |
| registry = get_registry() | |
| strategies = registry.list_all() | |
| raptor = next(s for s in strategies if s["id"] == "raptor") | |
| assert raptor["available"] is False | |
| assert "hierarchical" in raptor["unavailable_reason"].lower() | |
| def test_all_strategies_have_status_field(): | |
| from auralynq.rag import get_registry | |
| registry = get_registry() | |
| for s in registry.list_all(): | |
| assert s["status"] in ("available", "experimental", "planned"), f"{s['id']} has bad status" | |