| """Citation verifier — the last line of defence against a fabricated reference.""" |
|
|
| from __future__ import annotations |
|
|
| import pytest |
|
|
| from app.core.models import CitationStatus |
| from app.rag.verify import extract_citations, find_uncited_claims, verify_answer |
| from tests.conftest import make_chunk, make_scored |
|
|
|
|
| @pytest.fixture |
| def evidence(): |
| return [ |
| make_scored(make_chunk(article_no=51, law_label="Labour Law")), |
| make_scored(make_chunk(article_no=30, law_label="Labour Law")), |
| make_scored(make_chunk(article_no=25, law_label="Tenancy Law", law_id="dubai-tenancy-law")), |
| ] |
|
|
|
|
| class TestExtractCitations: |
| @pytest.mark.parametrize( |
| ("text", "expected"), |
| [ |
| ("x [Labour Law, Article 51] y", ("Labour Law", 51)), |
| ("x [Labour Law, Art. 51] y", ("Labour Law", 51)), |
| ("x [Labour Law, Art 51] y", ("Labour Law", 51)), |
| ("x [labour law , article (51) ] y", ("labour law", 51)), |
| ("x [Tenancy Law, Article 25] y", ("Tenancy Law", 25)), |
| ], |
| ) |
| def test_accepts_the_tolerated_forms(self, text: str, expected: tuple[str, int]): |
| """A verifier that only recognises perfect formatting silently passes bad output.""" |
| found = extract_citations(text) |
| assert len(found) == 1 |
| assert (found[0][1], found[0][2]) == expected |
|
|
| def test_ignores_prose_that_is_not_a_citation(self): |
| assert extract_citations("Article 51 says something without brackets") == [] |
| assert extract_citations("[see the annex]") == [] |
|
|
|
|
| class TestVerifyAnswer: |
| def test_citation_present_in_context_is_verified(self, evidence): |
| report = verify_answer("Gratuity is payable [Labour Law, Article 51].", evidence) |
| assert report.passed |
| assert report.verified_count == 1 |
| assert report.unsupported_count == 0 |
| assert report.citations[0].chunk_id is not None |
|
|
| def test_fabricated_article_is_flagged_not_dropped(self, evidence): |
| """Article 99 was never retrieved. The answer is flagged, and the citation kept. |
| |
| Silently deleting it would hand the reader a redacted answer with no way to know |
| something was removed. |
| """ |
| report = verify_answer("It says X [Labour Law, Article 99].", evidence) |
| assert not report.passed |
| assert report.unsupported_count == 1 |
| assert report.citations[0].status is CitationStatus.UNSUPPORTED |
| assert report.citations[0].chunk_id is None |
|
|
| def test_right_article_wrong_law_is_unsupported(self, evidence): |
| """Article 51 exists — in the Labour Law, not the Tenancy Law.""" |
| report = verify_answer("See [Tenancy Law, Article 51].", evidence) |
| assert not report.passed |
| assert report.unsupported_count == 1 |
|
|
| def test_label_matching_ignores_case_and_punctuation(self, evidence): |
| report = verify_answer("See [labour-law, Article 51].", evidence) |
| assert report.passed |
|
|
| def test_matches_at_article_level_not_chunk_level(self): |
| """A model given chunk 2 of Article 30 and citing Article 30 has cited correctly.""" |
| chunk = make_chunk(article_no=30, text="Labour Law - Article 30 (2/2)\ntail of the article") |
| report = verify_answer("See [Labour Law, Article 30].", [make_scored(chunk)]) |
| assert report.passed |
|
|
| def test_no_citations_at_all_still_passes_but_reports_uncited(self, evidence): |
| """Zero citations is not a fabrication; it is reported as unverified prose.""" |
| report = verify_answer("The law is generally quite protective of workers here.", evidence) |
| assert report.passed |
| assert report.verified_count == 0 |
| assert report.uncited_sentences |
|
|
| def test_empty_evidence_makes_every_citation_unsupported(self): |
| report = verify_answer("See [Labour Law, Article 51].", []) |
| assert not report.passed |
| assert report.unsupported_count == 1 |
|
|
|
|
| class TestUncitedClaims: |
| def test_short_fragments_are_not_claims(self): |
| assert find_uncited_claims("Yes. No. Maybe.") == [] |
|
|
| def test_cited_sentences_are_not_reported(self): |
| text = "A worker gets thirty days of annual leave each year [Labour Law, Article 29]." |
| assert find_uncited_claims(text) == [] |
|
|
| def test_uncited_assertion_is_reported(self): |
| text = "The employer must always pay double wages on public holidays without exception." |
| assert len(find_uncited_claims(text)) == 1 |
|
|