File size: 9,073 Bytes
9cf9a2a
 
 
 
 
 
 
 
7435134
ef3c54d
9cf9a2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef3c54d
 
 
 
 
 
 
 
 
9cf9a2a
 
 
ef3c54d
9cf9a2a
 
 
 
 
 
 
 
ef3c54d
 
 
 
9cf9a2a
 
 
 
 
 
 
 
 
 
ef3c54d
 
 
9cf9a2a
 
 
 
ef3c54d
9cf9a2a
 
ef3c54d
 
 
 
 
 
 
 
 
 
 
 
 
 
9cf9a2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
"""
Integration test for article extraction in document processing pipeline.

Verifies that extract_articles utility can be imported and used
as intended in the process_law task.
"""

import pytest

from app.utils.text_chunker import extract_articles


class TestArticleExtractionIntegration:
    """Test article extraction integration scenarios."""

    def test_real_world_cameroonian_law_format(self):
        """Test extraction with realistic Cameroonian legal document format."""
        # Simulated Cameroonian legal document
        law_text = """
        LOI N° 2023-001 DU 15 JANVIER 2023
        PORTANT RÉGIME DES SOCIÉTÉS COMMERCIALES AU CAMEROUN

        CHAPITRE I - DISPOSITIONS GÉNÉRALES

        Article 1. Objet de la loi
        La présente loi fixe le régime juridique applicable aux sociétés commerciales
        au Cameroun conformément aux dispositions de l'OHADA et aux spécificités nationales.

        Article 2. Champ d'application territorial
        Les dispositions de la présente loi s'appliquent sur l'ensemble du territoire
        de la République du Cameroun et concernent toutes les sociétés commerciales.

        Article 3. Définitions et terminologie
        Au sens de la présente loi, on entend par société commerciale toute personne
        morale constituée conformément aux dispositions du présent texte et de l'OHADA.

        CHAPITRE II - CONSTITUTION DES SOCIÉTÉS

        Article 4. Conditions de constitution
        Toute société commerciale doit être constituée par un acte écrit et immatriculée
        au registre du commerce et du crédit mobilier dans les conditions fixées par la loi.

        Article 5. Capital social minimum
        Le capital social minimum des sociétés commerciales est fixé par décret
        en fonction de la forme juridique et du secteur d'activité concerné.
        """

        chunks = extract_articles(law_text)

        # Le premier chunk porte l'en-tete du texte (intitule de la loi), sous
        # le numero PREAMBULE ou LEGAL_BASIS : l'extraction ne jette aucun
        # caractere du document. Les articles proprement dits sont donc filtres.
        assert chunks[0]['number'] in ('PREAMBULE', 'LEGAL_BASIS')
        assert 'LOI N° 2023-001' in chunks[0]['content']

        articles = [c for c in chunks if c['number'].isdigit()]

        # Verify extraction
        assert len(articles) == 5
        assert [a['number'] for a in articles] == ['1', '2', '3', '4', '5']

        # Verify structure
        assert articles[0]['title'] == 'Objet de la loi'
        assert 'OHADA' in articles[0]['content']

        assert articles[3]['number'] == '4'
        assert articles[3]['title'] == 'Conditions de constitution'

        # La section est rattachee a chaque article
        assert articles[0]['section'].startswith('CHAPITRE I')
        assert articles[3]['section'].startswith('CHAPITRE II')

        # Verify all have required fields
        for article in articles:
            assert 'number' in article
            assert 'content' in article
            assert 'position' in article
            assert 'word_count' in article
            assert 'char_count' in article
            assert article['word_count'] > 0
            assert article['char_count'] > 0

    def test_short_document_is_extracted(self):
        """Un texte de deux articles est extrait, pas rejete."""
        short_text = """
        Article 1. Premier article
        Contenu du premier article avec suffisamment de texte pour validation.

        Article 2. Deuxième article seulement
        Contenu du deuxième article, court mais parfaitement valide.
        """

        # L'ancienne regle "minimum 3 articles" a ete retiree du chunker, et
        # elle ne doit pas revenir : la majorite du corpus est faite de decrets
        # de nomination a deux articles ("Article 1er : est nomme... / Article
        # 2 : le present decret sera enregistre..."). La retablir refuserait
        # l'essentiel des documents.
        articles = extract_articles(short_text)

        assert [a['number'] for a in articles] == ['1', '2']
        assert all(a['char_count'] > 0 for a in articles)

    def test_empty_document_raises(self):
        """Un document sans contenu exploitable leve toujours."""
        with pytest.raises(ValueError):
            extract_articles("   \n  \n ")

    def test_pipeline_processes_large_document(self):
        """Test that pipeline can handle large legal documents."""
        # Generate a large document (100 articles)
        articles_content = []
        for i in range(1, 101):
            articles_content.append(
                f"Article {i}. Titre de l'article {i}\n"
                f"Contenu de l'article {i} avec suffisamment de texte pour validation. "
                f"Les dispositions légales s'appliquent conformément aux normes."
            )

        law_text = "\n\n".join(articles_content)

        articles = extract_articles(law_text)

        assert len(articles) == 100
        assert articles[0]['number'] == '1'
        assert articles[99]['number'] == '100'

        # Verify positions are sequential
        positions = [a['position'] for a in articles]
        assert positions == list(range(100))

    def test_database_ready_format(self):
        """Test that extracted articles are ready for database insertion."""
        law_text = """
        Article 1. Premier article
        Contenu du premier article avec suffisamment de texte.

        Article 2. Deuxième article
        Contenu du deuxième article avec suffisamment de texte.

        Article 3. Troisième article
        Contenu du troisième article avec suffisamment de texte.
        """

        articles = extract_articles(law_text)

        # Verify database-ready format
        for article in articles:
            # All fields match database schema
            assert isinstance(article['number'], str)
            assert len(article['number']) <= 20  # VARCHAR(20) in DB

            if article['title']:
                assert isinstance(article['title'], str)
                assert len(article['title']) <= 200  # VARCHAR(200) in DB

            assert isinstance(article['content'], str)
            assert isinstance(article['position'], int)
            assert isinstance(article['word_count'], int)
            assert isinstance(article['char_count'], int)

            # parent_id is Optional[str]
            assert article['parent_id'] is None or isinstance(article['parent_id'], str)

    def test_hierarchical_articles_for_database(self):
        """Test hierarchical articles produce correct parent_id for database."""
        law_text = """
        Article 1. Article principal
        Contenu de l'article principal avec suffisamment de texte.

        Article 1.1. Premier sous-article
        Contenu du premier sous-article avec suffisamment de texte.

        Article 1.2. Deuxième sous-article
        Contenu du deuxième sous-article avec suffisamment de texte.

        Article 2. Autre article
        Contenu d'un autre article avec suffisamment de texte.
        """

        articles = extract_articles(law_text)

        # Verify parent-child relationships
        assert articles[0]['number'] == '1'
        assert articles[0]['parent_id'] is None

        assert articles[1]['number'] == '1.1'
        assert articles[1]['parent_id'] == '1'

        assert articles[2]['number'] == '1.2'
        assert articles[2]['parent_id'] == '1'

        assert articles[3]['number'] == '2'
        assert articles[3]['parent_id'] is None

    def test_non_strict_mode_for_partial_documents(self):
        """Test non-strict mode for incomplete/draft documents."""
        partial_text = """
        Article 1. Premier article incomplet
        Contenu du premier article avec suffisamment de texte pour validation.

        Article 2. Deuxième article incomplet
        Contenu du deuxième article avec suffisamment de texte également.
        """

        # Non-strict mode should accept <3 articles
        articles = extract_articles(partial_text, strict=False)

        assert len(articles) == 2
        assert articles[0]['number'] == '1'
        assert articles[1]['number'] == '2'

    def test_performance_target_met(self):
        """Test that extraction meets <500ms performance target for 1000 articles."""
        import time

        # Generate document with 100 articles (scaled test)
        articles_content = []
        for i in range(1, 101):
            articles_content.append(
                f"Article {i}. Titre {i}\n"
                f"Contenu de l'article {i} avec du texte. " * 5
            )

        law_text = "\n\n".join(articles_content)

        start = time.time()
        articles = extract_articles(law_text)
        elapsed = time.time() - start

        assert len(articles) == 100
        # 100 articles should complete in <100ms (scales to 1000 in <1s)
        assert elapsed < 0.5, f"Extraction took {elapsed:.3f}s (target: <0.5s for 100 articles)"