import copy import json import pytest from tools.answer_evaluation.context_policy import answer_context_issues, citation_check, coverage_issues, prose_issues, text_contains_value from tools.answer_evaluation.checked_generation import generate_checked_answer from tools.answer_evaluation.render_answer import render_graph_answer from tools.graphrag.graph_search import extract_query_constraints from tools.graphrag.graphrag import extract_start_year @pytest.fixture def context(): return {'status': 'ok', 'complete': True, 'truncated': False, 'aggregation': None, 'sources': [{'source_url': 'https://example.test/students/2018/eva'}], 'facts': [{'person': 'Eva Nová', 'year': 2026, 'title': 'Práca o 3 modeloch', 'source_url': 'https://example.test/students/2018/eva'}], 'answer_candidates': []} @pytest.fixture def answer(context): return {'answer': '', 'status': 'ok', 'claims': [{'person': 'Eva Nová', 'year': 2026, 'title': 'Práca o 3 modeloch'}], 'source_urls': [context['sources'][0]['source_url']], 'count': None} @pytest.mark.parametrize('year', [1899, 1999, 2099, 2100, 9999]) def test_year_constraint_is_never_silently_dropped(year): assert extract_query_constraints(f'Aká práca je v roku {year}?')['years'] == [year] @pytest.mark.parametrize('text', ['Toto je úplný zoznam. Výpis je neúplný.', 'Uvádzam všetkých študentov. Výpis je neúplný.', 'Úplný zoznam, ale dôkazy sú orezané.']) def test_caveat_does_not_license_contradictory_exhaustive_claim(text): assert 'unsupported_exhaustive_statement' in coverage_issues({'answer': text, 'status': 'ok'}, {'complete': False}) @pytest.mark.parametrize('text', ['Výpis je neúplný.', 'Toto nie je úplný zoznam.', 'Nie je to úplný zoznam.']) def test_truthful_scope_notice_is_not_rejected(text): assert coverage_issues({'answer': text, 'status': 'ok'}, {'complete': False}) == [] @pytest.mark.parametrize('text', ['[zdroj][s]\n\n[s]: /vymysleny', 'zdroj', '', '[s]: javascript:alert(1)', 'x']) def test_all_citation_syntaxes_are_checked(text): assert not citation_check({'answer': text, 'source_urls': []}, {'https://example.test/a'})['citations_ok'] def test_legitimate_balanced_parentheses_and_reference_html_links(): url = 'https://example.test/a_(b)' text = f'[x]({url}) a {url}.\n[s]: <{url}>\nx' assert citation_check({'answer': text, 'source_urls': [url]}, {url})['citations_ok'] def test_count_requires_explicit_complete_aggregation(context, answer): answer['count'] = 98765 assert 'unsupported_total_count' in answer_context_issues(answer, context, check_text=False) @pytest.mark.parametrize('field,issue', [('claims', 'missing_required_claims'), ('source_urls', 'missing_required_citations')]) def test_positive_answer_needs_facts_and_citations(context, answer, field, issue): answer[field] = [] assert issue in answer_context_issues(answer, context, check_text=False) def test_every_claim_field_needs_its_source(context, answer): context['facts'].append({'person': 'Eva Nová', 'keyword': 'python', 'source_url': 'https://example.test/child'}) context['sources'].append({'source_url': 'https://example.test/child'}) answer['claims'][0]['keyword'] = 'python' assert 'missing_claim_source' in answer_context_issues(answer, context, check_text=False) answer['source_urls'].append('https://example.test/child') assert answer_context_issues(answer, context, check_text=False) == [] def test_wrong_extra_prose_year_is_rejected_but_citation_path_year_is_not(context, answer): answer['answer'] = 'Eva Nová má prácu z roku 2026. ' + answer['source_urls'][0] assert prose_issues(answer) == [] answer['answer'] += ' Obhajoba bola v roku 2099.' assert prose_issues(answer) == ['unsupported_prose_number'] def test_renderer_uses_only_validated_fields_and_preserves_model_reply(context, answer): original = copy.deepcopy(answer) def complete(_): return {'choices': [{'message': {'content': json.dumps(answer)}}]} result = generate_checked_answer('Názov a rok?', context, 'Pokyny', complete) parsed_original = {key: value for key, value in original.items() if value is not None} assert result['accepted_for_delivery'] and result['model_answer'] == parsed_original assert result['attempts'][0]['answer'] == parsed_original assert result['attempts'][0]['response']['choices'][0]['message']['content'] == json.dumps(original) assert '2026' in result['answer']['answer'] and 'Práca o 3 modeloch' in result['answer']['answer'] assert answer == original def test_zero_count_is_valid_without_claims_or_sources(): context = {'status': 'ok', 'facts': [], 'sources': [], 'complete': True, 'aggregation': {'count': 0, 'complete': True}} answer = {'answer': '', 'status': 'ok', 'claims': [], 'source_urls': [], 'count': 0} rendered = render_graph_answer(answer, context) assert answer_context_issues(rendered, context) == [] def test_directory_cohort_does_not_become_study_start(): assert extract_start_year('pages/students/2016/eva/README.md', [{'text': 'Diplomová práca 2026'}]) is None assert extract_start_year('pages/students/2016/eva/README.md', [{'text': 'Rok začiatku štúdia: 2017'}]) == 2017 assert extract_start_year('pages/students/2016/eva/README.md', [{'text': 'Beginning of the study: 2018'}]) == 2018 def test_person_inflection_keeps_distinct_names_distinct(): assert text_contains_value('Adriána Remiáša a Martina NaN1', 'Martin NaN1', person=True) assert not text_contains_value('Martin Jančura', 'Martin NaN1', person=True) assert not text_contains_value('Martin Malinovský', 'Martin Malý', person=True)