"""Witnessed local graph paths, fixed Cypher boundaries and production routing.""" import copy import os import uuid import pytest from tools.answers.question_plan import build_question_plan from tools.knowledge.sources import profile_units from tools.knowledge.evidence import STRUCTURAL_KINDS, relation_has_local_support from tools.knowledge import local_graph as local, store from tools.knowledge.graph_answers import render_similarity, graph_details from tools.knowledge.concepts import substantive_concept @pytest.mark.parametrize('concept',['články','poznámky','prehľad literatúry','reading papers','zdrojové kódy','Python']) def test_generic_study_activities_cannot_be_a_research_similarity(concept): assert not substantive_concept(concept) @pytest.mark.parametrize('concept',['znalostný graf','RAG','NER','poznámky o RAG','PEFT','anotácia pomenovaných entít']) def test_substantive_topics_are_not_removed_with_generic_words(concept): assert substantive_concept(concept) def graph_index(): rows=[] entries=[('Alena Nová','alena_nova','Systém RAG používa PEFT.',['RAG','PEFT'],'RAG'), ('Peter Starý','peter_stary','Systém chatbot používa PEFT.',['chatbot','PEFT'],'chatbot'), ('Dana Mladá','dana_mlada','Systém RAG funguje.',['RAG'],None)] for name,slug,text,concepts,subject in entries: units=profile_units({'title':name,'path':f'pages/students/2022/{slug}/README.md', 'content':'# Diplomová práca 2026\nStretnutie 5.5.2026\nStav:\n- '+text},include_records=True) for row in units: row.setdefault('concepts',[]);row.setdefault('relations',[]) row.setdefault('annotation_status','structural') if row['kind'] not in STRUCTURAL_KINDS: row.update(annotation_status='knowledge',concepts=concepts,relations=[]) if subject: relation={'subject':subject,'predicate':'uses','object':'PEFT', 'quote':subject+' používa PEFT.','verification':'supported'} assert relation_has_local_support(row,relation) row['relations']=[relation] rows+=units return {'version':'content-test','index_version':{'version':'source-test'}, 'extraction_schema':2,'academic_schema':1,'units':rows,'profiles':3} def bound(index,question='Kto pracoval na podobnej téme ako Alena Nová?'): catalog=list({r['source_path']:{'kind':'Person','name':r['person'], 'source_document':r['source_path']} for r in index['units']}.values()) return build_question_plan(question,catalog,index_version=index['index_version']) def witnessed_record(row): return {'evidence':row,'person':{'name':row['person'],'source_path':row['source_path']}, 'work':{'id':row['work_id'],'source_path':row['source_path'],'label':row['work_label'], 'type':row['work_type'],'year':row['work_year']}, 'activity':row['id'],'concepts':[local.concept_key(c) for c in row['concepts']]} @pytest.mark.parametrize('field,value', [('person','Other Person'),('work_id','wrong-work'), ('text','Invented result'),('phase','planned'),('source_path','other.md')]) def test_graph_evidence_cannot_change_checkpoint_identity_or_fact(field,value): index=graph_index();row=next(r for r in index['units'] if r.get('concepts')) record=witnessed_record(copy.deepcopy(row));record['evidence'][field]=value with pytest.raises(ValueError):local.validated_row(record,{row['id']:row}) def test_graph_edges_must_bind_the_actual_work_and_concept(): row=next(r for r in graph_index()['units'] if r.get('concepts')) record=witnessed_record(row) record['work']['id']='another-work' with pytest.raises(ValueError):local.validated_row(record,{row['id']:row}) record=witnessed_record(row);record['activity']=None with pytest.raises(ValueError):local.validated_row(record,{row['id']:row}) record=witnessed_record(row);record['concepts'].append('invented topic') with pytest.raises(ValueError):local.validated_row(record,{row['id']:row}) def test_large_witnesses_require_narrowing_instead_of_false_no_answer(): index=graph_index();a,b=[next(r for r in index['units'] if r['person']==name and r.get('concepts')) for name in ['Alena Nová','Peter Starý']] a['text']='Doložený veľmi dlhý výňatok. '*1000 result=local.LocalGraph(units=[a,b],seeds=[a],paths=[{'kind':'shared_topic','concept':'peft', 'anchor_id':a['id'],'peer_id':b['id']}]) response=render_similarity(bound(index).spec.question,index,bound(index),result) assert response['status']=='ambiguous' and response['reason']=='graph_answer_budget' def test_unverified_concept_cooccurrence_cannot_create_a_semantic_predicate(): index=graph_index();row=next(r for r in index['units'] if r.get('relations')) record={'relation':{'predicate':'uses','quote':'RAG používa PEFT.', **{k:row.get(k) for k in ('actor','work_id','phase','source_path','source_url','line')}}, 'evidence':row['id'],'subject':'rag','object':'peft'} assert local.validated_relation(record,{row['id']:row}) row['relations']=[] with pytest.raises(ValueError):local.validated_relation(record,{row['id']:row}) def test_projection_upgrade_cannot_mix_a_different_sqlite_snapshot(tmp_path,monkeypatch): from tools.knowledge import graph_projection as projection from contextlib import nullcontext index=graph_index();index['_checkpoint_sha256']='checkpoint' monkeypatch.setattr(projection,'acquire_reindex_lock',lambda *a:nullcontext()) monkeypatch.setattr(projection,'publication_lock',lambda *a,**kw:nullcontext()) monkeypatch.setattr(projection,'read_version',lambda *a:index['index_version']) monkeypatch.setattr(store,'load_index',lambda *a:index) monkeypatch.setattr(projection,'sqlite_version',lambda *a:{'version':'different'}) monkeypatch.setattr(projection,'create_neo4j_driver',lambda *a:pytest.fail('No writes on snapshot mismatch')) with pytest.raises(RuntimeError,match='different snapshots'): projection.refresh_projection(tmp_path,tmp_path/'db.sqlite') def test_similarity_shows_both_witnesses_and_only_two_verified_predicates_license_shared_use(): index=graph_index();a,b=[next(r for r in index['units'] if r['person']==name and r.get('concepts')) for name in ['Alena Nová','Peter Starý']] path={'kind':'shared_topic','concept':'peft','anchor_id':a['id'],'peer_id':b['id'], 'anchor_work_id':a['work_id'],'peer_work_id':b['work_id']} result=local.LocalGraph(units=[a,b],seeds=[a],paths=[path]) response=render_similarity(bound(index).spec.question,index,bound(index),result) assert response['accepted_for_delivery'] and len(response['source_urls'])==2 assert a['text'] in response['answer'] and b['text'] in response['answer'] assert 'Spoločný overený vzťah' not in response['answer'] result.relations=[{'evidence_id':r['id'],**r['relations'][0]} for r in [a,b]] response=render_similarity(bound(index).spec.question,index,bound(index),result) assert 'Spoločný overený vzťah: používa → PEFT' in response['answer'] assert len(response['graph_paths'])==1 and len(response['graph_relations'])==2 result.paths.append(dict(path)) response=render_similarity(bound(index).spec.question,index,bound(index),result) assert response['answer'].count(a['text'])==1 and response['answer'].count(b['text'])==1 def test_planned_use_is_not_rendered_as_actual_shared_use(): index=graph_index();rows=[r for r in index['units'] if r['person'] in {'Alena Nová','Peter Starý'} and r.get('concepts')] a,b=rows;b['phase']='planned' result=local.LocalGraph(units=rows,seeds=[a],paths=[{'kind':'shared_topic','concept':'peft', 'anchor_id':a['id'],'peer_id':b['id']}],relations=[{'evidence_id':r['id'],**r['relations'][0]} for r in rows]) response=render_similarity(bound(index).spec.question,index,bound(index),result) assert 'Spoločný overený vzťah' not in response['answer'] and '(plán' in response['answer'] def test_truncation_is_explicit_and_no_path_does_not_mean_no_similar_thesis_exists(): index=graph_index();result=local.LocalGraph(truncation_reasons=['graph_concept_limit']) response=render_similarity(bound(index).spec.question,index,bound(index),result) assert response['status']=='no_answer' and response['truncated'] and response['complete'] is False assert 'neexistuje' in response['answer'] and 'Zúž' in response['answer'] def test_production_graph_delivery_uses_local_traversal_not_the_whole_graph(tmp_path,monkeypatch): from tools.knowledge import query from contextlib import nullcontext index=graph_index();plan=bound(index) monkeypatch.setattr(query,'index_snapshot',lambda *a,**kw:nullcontext()) monkeypatch.setattr(query,'read_version',lambda *a:index['index_version']) monkeypatch.setattr(query,'load_index',lambda *a:index) monkeypatch.setattr(query,'read_graph',lambda *a:pytest.fail('Full corpus graph scan')) called=[] monkeypatch.setattr(local,'retrieve_local_graph',lambda *a,**kw:called.append(kw) or local.LocalGraph()) response=query.deliver_content(plan.spec.question,engine='graphrag',limit=2,db_file=tmp_path/'db.sqlite',plan=plan) assert called==[{'limit':2}] and response['reason']=='no_local_graph_connection' @pytest.fixture def live_graph(monkeypatch): if os.getenv('RUN_NEO4J_TESTS')!='1':pytest.skip('Requires local Neo4j') scope='local-graph-test-'+uuid.uuid4().hex monkeypatch.setattr(store,'SCOPE',scope) index=graph_index() store.write_graph(index) try:yield index finally: settings=store.get_neo4j_settings() with store.create_neo4j_driver(settings) as driver,driver.session(database=settings.database) as session: session.run('MATCH (n {graph_scope:$scope}) DETACH DELETE n',scope=scope).consume() session.run('MATCH (s:KnowledgeState {scope:$scope}) DELETE s',scope=scope).consume() def test_live_bounded_paths_join_people_works_activities_concepts_and_checked_relations(live_graph): index=live_graph result=local.retrieve_local_graph(bound(index).spec.question,index,bound(index)) assert result.paths and {r['person'] for r in result.units}=={'Alena Nová','Peter Starý','Dana Mladá'} assert any(r['_graph_activity'] for r in result.units if r.get('concepts')) assert {r['evidence_id'] for r in result.relations}=={r['id'] for r in index['units'] if r.get('relations')} response=render_similarity(bound(index).spec.question,index,bound(index),result) assert response['status']=='ok' and 'Spoločný overený vzťah' in response['answer'] def test_live_wrong_year_has_no_neighborhood_fallback(live_graph): plan=bound(live_graph,'Aké problémy riešila Alena Nová pri diplomovej práci v roku 1999?') result=local.retrieve_local_graph(plan.spec.question,live_graph,plan) assert result.units==[] and result.paths==[] def test_live_shared_articles_do_not_introduce_an_unrelated_student(live_graph): index=live_graph for row in index['units']: if row['person']=='Alena Nová' and row.get('concepts'): row['text']+='\nPrečítané články.';row['end_line']+=1;row['concepts'].append('články') extra=profile_units({'title':'Cyril Nový','path':'pages/students/2022/cyril_novy/README.md', 'content':'# Diplomová práca 2026\nStav:\n- Prečítané články o nesúvisiacej téme.'},include_records=True) for row in extra: row.update(concepts=[] if row['kind'] in STRUCTURAL_KINDS else ['články'],relations=[], annotation_status='structural' if row['kind'] in STRUCTURAL_KINDS else 'knowledge') index['units']+=extra store.write_graph(index) result=local.retrieve_local_graph(bound(index).spec.question,index,bound(index)) assert result.paths and not any(r['person']=='Cyril Nový' for r in result.units) assert not any(p['concept']=='clanky' for p in result.paths) def test_live_projection_version_and_tampered_evidence_fail_closed(live_graph): with pytest.raises(RuntimeError):local.retrieve_local_graph(bound(live_graph).spec.question, {**live_graph,'version':'different'},bound(live_graph)) settings=store.get_neo4j_settings() with store.create_neo4j_driver(settings) as driver,driver.session(database=settings.database) as session: session.run("MATCH (e:KnowledgeEvidence {graph_scope:$scope,person:'Alena Nová'}) SET e.text='Invented result'",scope=store.SCOPE).consume() with pytest.raises(ValueError):local.retrieve_local_graph(bound(live_graph).spec.question,live_graph,bound(live_graph)) def test_live_evidence_budget_reports_actual_clipping(live_graph,monkeypatch): monkeypatch.setattr(local,'EVIDENCE_LIMIT',1) plan=bound(live_graph,'Aké metódy použila Alena Nová?') result=local.retrieve_local_graph(plan.spec.question,live_graph,plan) assert len(result.units)==1 and 'graph_evidence_limit' in result.truncation_reasons