"""Optional real Cypher regressions; all fixtures are rolled back.""" from __future__ import annotations import os import uuid import pytest from tools.graphrag import graph_search as gs from tools.graphrag.graph_db import create_neo4j_driver, get_neo4j_settings pytestmark = pytest.mark.skipif(os.getenv('RUN_NEO4J_TESTS') != '1', reason='Set RUN_NEO4J_TESTS=1 for isolated Neo4j regressions.') @pytest.fixture def graph(monkeypatch): scope = 'regression-' + uuid.uuid4().hex monkeypatch.setattr(gs, 'GRAPH_SCOPE', scope) category = 'regcat-' + uuid.uuid4().hex settings = get_neo4j_settings() with create_neo4j_driver(settings) as driver, driver.session(database=settings.database) as session: tx = session.begin_transaction() try: for suffix, name, year, tag in [('a', 'Alena Nová', 2024, 'alpha'), ('b', 'Boris Iný', 2024, 'beta'), ('c', 'Cyril Malý', 2025, 'alpha')]: tx.run('CREATE (p:Person {id:$id, name:$name, source_document:$path, graph_scope:$scope}) ' 'CREATE (d:Document {path:$path, source_url:$url, graph_scope:$scope}) ' 'CREATE (t:Thesis {id:$thesis, title:$title, year:$year, thesis_type:"bachelor_thesis", graph_scope:$scope}) ' 'MERGE (k:Keyword {id:$keyword, name:$tag, graph_scope:$scope}) ' 'CREATE (p)-[:HAS_DOCUMENT]->(d)-[:HAS_KEYWORD]->(k) ' 'CREATE (p)-[:AUTHORED]->(t)-[:EVIDENCED_BY]->(d)', id=scope+suffix, name=name, path=scope+'/'+suffix+'.md', url='https://example.test/'+suffix, scope=scope, thesis=scope+'th'+suffix, title='Výskum '+suffix, year=year, keyword=scope+tag, tag=tag).consume() tx.run('MATCH (p:Person {id:$person})-[:HAS_DOCUMENT]->(d) ' 'CREATE (c:Category {name:$category, graph_scope:$scope}) ' 'CREATE (a:DocumentAuthor {id:$author, name:"Autor Správny", graph_scope:$scope}) ' 'CREATE (d)-[:IN_CATEGORY]->(c) CREATE (a)-[:AUTHORED_DOCUMENT]->(d) ' 'CREATE (child:Document {path:$child, graph_scope:$scope}) ' 'CREATE (wrong:DocumentAuthor {id:$wrong, name:"Autor Iný", graph_scope:$scope}) ' 'CREATE (p)-[:HAS_DOCUMENT]->(child)<-[:AUTHORED_DOCUMENT]-(wrong)', person=scope+'a', category=category, scope=scope, author=scope+'author', child=scope+'/child.md', wrong=scope+'wrong').consume() yield tx, category finally: tx.rollback() def test_real_keyword_year_conjunction(graph): tx, _ = graph result = gs.search_graph_with_session(tx, 'Ktorá z osôb Alena Nová a Boris Iný má kľúčové slovo alpha a jej práca je v roku 2024?') assert {r['person'] for r in result['facts'] if r['fact_type'] == 'person_thesis'} == {'Alena Nová'} assert {r['person'] for r in result['facts'] if r['fact_type'] == 'person_keyword'} == {'Alena Nová'} def test_real_count_and_rejected_candidate_provenance(graph): tx, _ = graph result = gs.search_graph_with_session(tx, 'Z kandidátov Alena Nová, Boris Iný, Cyril Malý vyber osoby s prácou v roku 2024. Koľko ich je?') assert result['aggregation'] == {'count': 2, 'people': ['Alena Nová', 'Boris Iný'], 'candidate_count': 3, 'complete': True} assert 'https://example.test/c' in {r['source_url'] for r in result['documents']} def test_real_author_and_category_must_share_document(graph): tx, category = graph result = gs.search_graph_with_session(tx, f'Ktorý autor dokumentu je pri osobe Alena Nová spojený s kategóriou {category}?') authors = [r['document_author'] for r in result['facts'] if r['fact_type'] == 'person_document_author'] assert authors == ['Autor Správny'] def test_real_shared_year_does_not_invent_intersection(graph): tx, _ = graph result = gs.search_graph_with_session(tx, 'V ktorom rovnakom roku je práca pri osobách Alena Nová a Cyril Malý?') assert not any(r['fact_type'] in {'person_thesis', 'person_work'} for r in result['facts']) assert next(r for r in result['facts'] if r['fact_type'] == 'academic_year_comparison')['shared_years'] == [] def test_real_shared_keyword_uses_requested_keyword(graph): tx, _ = graph result = gs.search_graph_with_session(tx, 'Sú osoby Alena Nová a Cyril Malý prepojené cez kľúčové slovo alpha? Uveď spoločné kľúčové slovo.') assert [r['keyword'] for r in result['facts'] if r['fact_type'] == 'shared_keyword'] == ['alpha'] def test_real_unknown_keyword_is_not_a_global_listing(graph): tx, _ = graph result = gs.search_graph_with_session(tx, 'Aký názov práce má Alena Nová, ak má kľúčové slovo missingtag?') assert result['facts'] == [] @pytest.mark.parametrize('question', [ 'V ktorom rovnakom roku je práca pri osobách Alena Nová a Boris Iný?', 'Aký rok práce spája osoby Alena Nová a Boris Iný?', ]) def test_real_shared_year_paraphrases_have_same_answer(graph, question): tx, _ = graph result = gs.search_graph_with_session(tx, question) assert next(r for r in result['facts'] if r['fact_type'] == 'academic_year_comparison')['shared_years'] == [2024] assert {r['year'] for r in result['facts'] if r['fact_type'] == 'person_thesis'} == {2024} def test_real_title_and_keyword_are_both_returned(graph): tx, _ = graph result = gs.search_graph_with_session(tx, 'Aký je názov práce osoby Alena Nová, ak jej dokument obsahuje kľúčové slovo alpha?') assert [(r['person'], r['title']) for r in result['facts'] if r['fact_type'] == 'person_thesis'] == [('Alena Nová', 'Výskum a')] assert [r['keyword'] for r in result['facts'] if r['fact_type'] == 'person_keyword'] == ['alpha'] def test_real_count_is_not_cut_off_by_presentation_limit(graph): tx, _ = graph result = gs.search_graph_with_session(tx, 'Z kandidátov Alena Nová, Boris Iný, Cyril Malý vyber osoby s prácou v roku 2024. Koľko ich je?', limit=1) assert result['aggregation']['count'] == 2 assert len(result['facts']) == 1 assert result['truncated']