"""Phase 5 projection and GraphRAG boundary tests.""" from __future__ import annotations import asyncio from datetime import datetime import pytest from ont_platform.core.graph.cypher_guard import ReadOnlyCypherGuard, UnsafeCypherError from ont_platform.core.graph.search import CandidateGraphSearchService from ont_platform.core.projection.rdf_to_neo4j import RDFToNeo4jProjector from ont_platform.storage.candidate_repository import CandidateRepository from ont_platform.storage.models import Base, SourceDocument from sqlalchemy import create_engine from sqlalchemy.orm import Session, sessionmaker def _session() -> Session: engine = create_engine("sqlite:///:memory:") Base.metadata.create_all(engine) return sessionmaker(bind=engine)() def test_rdf_projection_keeps_neo4j_as_projection_store() -> None: async def run(): projector = RDFToNeo4jProjector(project_id="proj_graph") return await projector.preview_projection( [ ("http://example.test/Alice", "http://example.test/knows", "http://example.test/Bob"), ("http://example.test/Alice", "http://www.w3.org/2000/01/rdf-schema#label", "Alice"), ], provenance={"source_url": "https://example.test/source", "evidence_ids": ["EV_graph"]}, ) result = asyncio.run(run()) payload = result.to_dict() assert payload["contract"]["canonical_store"] == "rdf_fuseki" assert payload["contract"]["projection_store"] == "neo4j" assert payload["node_count"] >= 2 assert payload["relationships"][0]["provenance"]["evidence_ids"] == ["EV_graph"] assert payload["source_graph_hash"] def test_read_only_cypher_guard_blocks_writes_and_enforces_limit() -> None: guard = ReadOnlyCypherGuard(max_limit=25) sanitized = guard.sanitize("MATCH (n:Entity) RETURN n", limit=100) assert "LIMIT 25" in sanitized.query with pytest.raises(UnsafeCypherError): guard.sanitize("MATCH (n) DETACH DELETE n") def test_candidate_graph_search_returns_source_provenance() -> None: db = _session() document = SourceDocument( id="doc_graph", project_id="proj_graph", source_url="https://example.test/source", document_type="html", title="Graph Source", text="Alice works at Acme.", content_hash="hash_graph", fingerprint="fp_graph", retrieved_at=datetime.utcnow(), extracted_by="trafilatura", ) db.add(document) repository = CandidateRepository(db) repository.save_lightweight_result( project_id="proj_graph", document_id="doc_graph", result={ "entities": [ { "id": "E_alice_graph", "label": "Alice", "type": "person", "confidence": 0.9, "evidence_ids": ["EV_graph"], } ], "relations": [], "evidence_spans": [ { "id": "EV_graph", "text": "Alice works at Acme.", "start_offset": 0, "end_offset": 20, } ], }, ) results = CandidateGraphSearchService(db).search( project_id="proj_graph", query="alice", limit=10, ) assert len(results) == 1 payload = results[0].to_dict() assert payload["provenance"]["source_url"] == "https://example.test/source" assert payload["provenance"]["evidence_spans"][0]["id"] == "EV_graph"