"""Parser tests, asserted against the real government PDFs. Each test pins a property that was found by inspecting the actual files, so a regression in extraction shows up as a named failure rather than as quietly worse retrieval. """ from __future__ import annotations import re import pytest from app.rag.parse import ( ARTICLE_HEADING_RE, INSTRUMENT_TITLE_RE, LIGATURE_REPAIRS, WIDE_GLYPH_RATIO, ParsedDocument, _instrument_key, _normalise, ) pytestmark = pytest.mark.integration def test_article_counts_match_the_published_instruments(parsed: list[ParsedDocument]): """The Labour Law has 74 articles; Cabinet Resolution 1/2022 has 39; Law 26/2007 has 37.""" by_law = {d.law_id: d for d in parsed} labour = by_law["uae-labour-law"] decree = [a for a in labour.articles if "cabinet" not in a.part_id] cabinet = [a for a in labour.articles if "cabinet" in a.part_id] assert len(decree) == 74 assert len(cabinet) == 39 assert {a.article_no for a in decree} == set(range(1, 75)) assert {a.article_no for a in cabinet} == set(range(1, 40)) assert len(by_law["dubai-tenancy-law"].articles) == 37 assert len(by_law["dubai-rent-decree"].articles) == 4 def test_contents_cross_check_passes(diagnostics): """The documents' own tables of contents agree with what the body pass produced.""" assert diagnostics.contents_missing_in_body == [] assert diagnostics.contents_extra_in_body == [] assert diagnostics.contents_check_passed assert diagnostics.contents_entries_seen == 113 def test_broken_th_ligature_is_repaired(parsed: list[ParsedDocument], diagnostics): """The Labour Law's font maps the `Th` ligature to a bare `T`; every one is restored. Without the repair the corpus reads "Te employer shall" throughout, which damages both BM25 (the token is wrong) and the embedding (the word is wrong). """ broken = re.compile( r"\b(?:Te|Tis|Tese|Tose|Teir|Tere|Tey|Tem|Tus|Terefore|Tereof|Tereto|Terein)\b" ) for document in parsed: for article in document.articles: assert not broken.search(article.text), ( f"{document.law_id} art {article.article_no}: unrepaired ligature" ) assert diagnostics.ligature_repairs > 300 assert LIGATURE_REPAIRS == {"T": "Th"} assert 0.7 < WIDE_GLYPH_RATIO < 0.9 def test_the_word_the_survives(parsed: list[ParsedDocument]): """A repair that produced nothing would also pass the negative test above.""" text = " ".join(a.text for d in parsed for a in d.articles) assert text.count("The ") > 250 def test_two_instruments_are_split_into_parts(parsed: list[ParsedDocument]): """One PDF holds the Decree-Law and Cabinet Resolution; their article numbers collide.""" labour = next(d for d in parsed if d.law_id == "uae-labour-law") parts = {a.part_id for a in labour.articles} assert len(parts) == 2 keys = {a.citation_key for a in labour.articles} assert len(keys) == len(labour.articles), "citation keys must be unique" def test_amendment_duplicate_article_is_split(parsed: list[ParsedDocument]): """Law 33/2008 restates Article 2 of the law it amends AND has its own Article 2.""" amendment = next(d for d in parsed if d.law_id == "dubai-tenancy-amendment") twos = [a for a in amendment.articles if a.article_no == 2] assert len(twos) == 2 assert twos[0].part_id != twos[1].part_id def test_missing_heading_is_recovered(parsed: list[ParsedDocument], diagnostics): """Cabinet Resolution Article 2 has no printed label in the source PDF.""" labour = next(d for d in parsed if d.law_id == "uae-labour-law") article = next(a for a in labour.articles if a.article_no == 2 and "cabinet" in a.part_id) assert article.title == "Classification of Establishments" assert "classified" in article.text.lower() assert diagnostics.inferred_headings == 1 def test_no_article_body_was_eaten_by_the_title(parsed: list[ParsedDocument]): """A justified first line must not be mistaken for a centred title. Regression test for a real defect: Decree 43/2013 Article 1 lost its opening line ("When renewing Real Property Lease Contracts...") to the title field. """ decree = next(d for d in parsed if d.law_id == "dubai-rent-decree") article = next(a for a in decree.articles if a.article_no == 1) assert article.title == "Percentages of Increase" assert article.text.startswith("When renewing Real Property Lease Contracts") def test_definitions_table_reads_term_then_definition(parsed: list[ParsedDocument]): """Sub-pixel y differences must not invert a two-column definitions table.""" labour = next(d for d in parsed if d.law_id == "uae-labour-law") article = next(a for a in labour.articles if a.article_no == 1 and "cabinet" not in a.part_id) assert "State: United Arab Emirates." in article.text assert "Ministry: Ministry of Human Resources" in article.text def test_sections_carry_forward_within_a_part(parsed: list[ParsedDocument]): """Dubai Law 26/2007 prints chapter headings above the first article of each chapter.""" tenancy = next(d for d in parsed if d.law_id == "dubai-tenancy-law") by_no = {a.article_no: a for a in tenancy.articles} assert by_no[5].title == "Term of Lease Contract" assert by_no[6].title == "" assert by_no[6].section == "Term of Lease Contract" def test_pages_are_recorded_and_ordered(parsed: list[ParsedDocument]): for document in parsed: for article in document.articles: assert 1 <= article.page_start <= article.page_end <= document.page_count class TestHeadingRegex: """The anchoring is what keeps in-body cross-references out of the heading set.""" @pytest.mark.parametrize( "line", ["Article (5)", "Article 5", " Article (54) * ", "ARTICLE (12)", "Article (7):"] ) def test_accepts_real_headings(self, line: str): assert ARTICLE_HEADING_RE.match(line) @pytest.mark.parametrize( "line", [ "Article (9) of this Law.", "in accordance with Article (5) of the Decree-Law", "Articles (2), (3), (4) of the Original Law are superseded", "Article (58-64)", ], ) def test_rejects_cross_references(self, line: str): assert ARTICLE_HEADING_RE.match(line) is None class TestInstrumentTitle: @pytest.mark.parametrize( "line", [ "Law No. (26) of 2007", "Federal Decree-Law No. (33) of 2021", "Cabinet Resolution No. (1) Of 2022", "Decree No. (43) of 2013", ], ) def test_accepts_bare_titles(self, line: str): assert INSTRUMENT_TITLE_RE.match(line) @pytest.mark.parametrize( "line", [ "Law No. (16) of 2007 Establishing the Real Estate Regulatory Agency,", "Federal Law No. (5) of 1985 Issuing the Civil Code of the United Arab " "Emirates and its", "Decree No. (2) of 1993 Establishing a Special Tribunal", ], ) def test_rejects_recital_lines(self, line: str): """Every Dubai law lists other instruments in its recitals.""" assert INSTRUMENT_TITLE_RE.match(line) is None def test_instrument_key_is_canonical(self): assert _instrument_key("Law No. (26) of 2007") == ("law", 26, 2007) assert _instrument_key("Federal Decree-Law No. 33 of 2021 Regarding X") == ( "federaldecreelaw", 33, 2021, ) assert _instrument_key("no instrument here") is None class TestNormalise: def test_folds_compatibility_ligatures(self): assert _normalise("efficiency") == "efficiency" assert _normalise("define") == "define" def test_collapses_unicode_spaces_only(self): assert _normalise("a b\u200bc") == "a bc" def test_preserves_accented_letters(self): """Regression guard: a character-class RANGE here would blank Latin/Greek text.""" assert _normalise("café Ünter Ωmega") == "café Ünter Ωmega" def test_contents_page_numbers_never_reach_an_article_title(parsed: list[ParsedDocument]): """A contents row is ``Article (N) | Title | page``, and the page is not the title. ``PAGE_FURNITURE_RE`` dropped a bare page number but not a *range*, so the Labour Law's ``Article (58-64) | Penalties | 39-40`` row indexed seven articles under the title "Penalties 39-40". Nothing downstream would have rejected that: it is a plausible-looking string, and it rode into the chunk text and therefore into BM25. """ titled = [ (document.law_id, article.article_no, article.title) for document in parsed for article in document.articles if article.title ] assert titled, "no titles parsed at all" contaminated = [entry for entry in titled if any(ch.isdigit() for ch in entry[2])] assert contaminated == [], f"page numbers leaked into titles: {contaminated}" labour = next(d for d in parsed if d.law_id == "uae-labour-law") penalties = [a.title for a in labour.articles if 58 <= a.article_no <= 64] assert penalties == ["Penalties"] * len(penalties), penalties