| """ |
| Test suite for FIX-33 through FIX-37. |
| Run from project root: python tests/test_recent_fixes.py |
| |
| Tests that need camel-tools (ArabicGrammarGuard) test the regex logic |
| directly without instantiating the full class. |
| """ |
| import sys |
| import os |
| import re |
|
|
| |
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) |
|
|
| PASS = 0 |
| FAIL = 0 |
|
|
| def test(name, condition, detail=""): |
| global PASS, FAIL |
| if condition: |
| PASS += 1 |
| print(f" ✅ {name}") |
| else: |
| FAIL += 1 |
| print(f" ❌ {name} — {detail}") |
|
|
|
|
| |
| |
| |
| |
| print("\n═══ FIX-33: Grammar rules — preposition + root noun protection ═══") |
|
|
| |
| from nlp.grammar import grammar_rules |
|
|
| |
| _PREP_BLOCKLIST = grammar_rules.ArabicGrammarGuard._PREP_BLOCKLIST |
|
|
| |
| def _prep_replace(m): |
| prep = m.group(1) |
| stem = m.group(2) |
| suffix = m.group(3) |
| full_word = stem + suffix |
| if full_word in _PREP_BLOCKLIST: |
| return m.group(0) |
| if stem.startswith('ال') and suffix == 'ان': |
| return m.group(0) |
| return f'{prep} {stem}ين' |
|
|
| def fix_prepositions(text): |
| return re.sub( |
| r'\b([وف]?(?:في|من|إلى|على|عن|حتى))\s+([أ-ي]{4,})(ون|ان)\b', |
| _prep_replace, text |
| ) |
|
|
| |
| root_nouns = [ |
| ("إلى الامتحان", "إلى الامتحان"), |
| ("من الإنسان", "من الإنسان"), |
| ("في الميدان", "في الميدان"), |
| ("على المكان", "على المكان"), |
| ("عن السلطان", "عن السلطان"), |
| ("إلى البرلمان", "إلى البرلمان"), |
| ("في الحيوان", "في الحيوان"), |
| ("من القرآن", "من القرآن"), |
| ("في الزمان", "في الزمان"), |
| ] |
| for input_text, expected in root_nouns: |
| result = fix_prepositions(input_text) |
| test(f"'{input_text}' → unchanged", result == expected, f"got '{result}'") |
|
|
| |
| plurals = [ |
| ("في المهندسون", "في المهندسين"), |
| ("من المعلمون", "من المعلمين"), |
| ] |
| for input_text, expected in plurals: |
| result = fix_prepositions(input_text) |
| test(f"'{input_text}' → '{expected}'", result == expected, f"got '{result}'") |
|
|
| |
| print("\n═══ FIX-33b: Attached preposition regex protection ═══") |
|
|
| def _attached_prep_replace(m): |
| prefix = m.group(1) |
| stem = m.group(2) |
| suffix = m.group(3) |
| full_word = 'ال' + stem + suffix |
| if full_word in _PREP_BLOCKLIST: |
| return m.group(0) |
| if suffix == 'ان': |
| return m.group(0) |
| return f'{prefix}ال{stem}ين' |
|
|
| def fix_attached_prep(text): |
| return re.sub(r'\b([وف]?[بلكف])ال([أ-ي]{4,})(ون|ان)\b', _attached_prep_replace, text) |
|
|
| |
| attached_root = [ |
| ("بالامتحان", "بالامتحان"), |
| ("بالإنسان", "بالإنسان"), |
| ("بالميدان", "بالميدان"), |
| ("كالسلطان", "كالسلطان"), |
| ("فبالبرلمان", "فبالبرلمان"), |
| ] |
| for input_text, expected in attached_root: |
| result = fix_attached_prep(input_text) |
| test(f"'{input_text}' → unchanged", result == expected, f"got '{result}'") |
|
|
| |
| attached_plurals = [ |
| ("بالمهندسون", "بالمهندسين"), |
| ("كالمعلمون", "كالمعلمين"), |
| ] |
| for input_text, expected in attached_plurals: |
| result = fix_attached_prep(input_text) |
| test(f"'{input_text}' → '{expected}'", result == expected, f"got '{result}'") |
|
|
| |
| def _lam_prep_replace(m): |
| prefix = m.group(1) |
| stem = m.group(2) |
| suffix = m.group(3) |
| if (stem + suffix) in _PREP_BLOCKLIST: |
| return m.group(0) |
| if suffix == 'ان': |
| return m.group(0) |
| return f'{prefix}{stem}ين' |
|
|
| def fix_lam_prep(text): |
| return re.sub(r'\b([وف]?ل)([أ-ي]{4,})(ون|ان)\b', _lam_prep_replace, text) |
|
|
| |
| lam_root = [ |
| ("لامتحان", "لامتحان"), |
| ("لإنسان", "لإنسان"), |
| ] |
| for input_text, expected in lam_root: |
| result = fix_lam_prep(input_text) |
| test(f"'{input_text}' → unchanged", result == expected, f"got '{result}'") |
|
|
| |
| test("'لمهندسون'→'لمهندسين'", |
| fix_lam_prep("لمهندسون") == "لمهندسين", |
| f"got '{fix_lam_prep('لمهندسون')}'") |
|
|
|
|
| |
| |
| |
| print("\n═══ FIX-35: Spelling — conjugation suffix protection ═══") |
|
|
| _CONJUGATION_SUFFIXES = {'ن', 'ت'} |
|
|
| def simulate_insertion_fix_check(orig_word, corr_word): |
| """Simulate the FIX-35 suffix strip check logic from app.py.""" |
| if len(orig_word) != len(corr_word) + 1: |
| return "not_applicable" |
| |
| for di in range(len(orig_word)): |
| candidate = orig_word[:di] + orig_word[di + 1:] |
| if candidate == corr_word: |
| removed_char = orig_word[di] |
| removed_pos = di |
| if (removed_char in _CONJUGATION_SUFFIXES |
| and removed_pos == len(orig_word) - 1 |
| and len(corr_word) >= 3): |
| return "blocked" |
| return "allowed" |
| return "not_applicable" |
|
|
| |
| test("'ذهبن'→'ذهب' blocked (ن suffix)", |
| simulate_insertion_fix_check("ذهبن", "ذهب") == "blocked", |
| f"got {simulate_insertion_fix_check('ذهبن', 'ذهب')}") |
|
|
| test("'كتبت'→'كتب' blocked (ت suffix)", |
| simulate_insertion_fix_check("كتبت", "كتب") == "blocked", |
| f"got {simulate_insertion_fix_check('كتبت', 'كتب')}") |
|
|
| |
| test("'درسة'→'درس' allowed (ة not in narrowed set)", |
| simulate_insertion_fix_check("درسة", "درس") == "allowed", |
| f"got {simulate_insertion_fix_check('درسة', 'درس')}") |
|
|
| test("'جلسوا'→'جلسو' allowed (ا not in narrowed set)", |
| simulate_insertion_fix_check("جلسوا", "جلسو") == "allowed", |
| f"got {simulate_insertion_fix_check('جلسوا', 'جلسو')}") |
|
|
| |
| test("'الكتتاب'→'الكتاب' allowed (mid-word extra ت)", |
| simulate_insertion_fix_check("الكتتاب", "الكتاب") == "allowed", |
| f"got {simulate_insertion_fix_check('الكتتاب', 'الكتاب')}") |
|
|
| test("'الصصف'→'الصف' allowed (mid-word extra ص)", |
| simulate_insertion_fix_check("الصصف", "الصف") == "allowed", |
| f"got {simulate_insertion_fix_check('الصصف', 'الصف')}") |
|
|
|
|
| |
| |
| |
| print("\n═══ FIX-36: Overlap resolver — grammar+punctuation merge ═══") |
|
|
| from nlp.correction_patch import CorrectionPatch, PatchSet, PRIORITY |
|
|
| |
| ps = PatchSet() |
| grammar_patch = CorrectionPatch( |
| stage='grammar', |
| start_original=18, end_original=26, |
| start_current=18, end_current=26, |
| original='المعلمون', replacement='المعلمين', |
| priority=PRIORITY['grammar'], confidence=0.9 |
| ) |
| punc_patch = CorrectionPatch( |
| stage='punctuation', |
| start_original=18, end_original=26, |
| start_current=18, end_current=26, |
| original='المعلمون', replacement='المعلمين.', |
| priority=PRIORITY['punctuation'], confidence=0.8 |
| ) |
| ps.add(grammar_patch) |
| ps.add(punc_patch) |
| resolved = ps.resolve_overlaps() |
|
|
| test("Case 1: 1 patch after merge", len(resolved) == 1, f"got {len(resolved)}") |
| if resolved: |
| test("Merged replacement = 'المعلمين.'", |
| resolved[0].replacement == 'المعلمين.', |
| f"got '{resolved[0].replacement}'") |
| test("Merged stage = grammar", |
| resolved[0].stage == 'grammar', |
| f"got '{resolved[0].stage}'") |
|
|
| |
| ps2 = PatchSet() |
| ps2.add(CorrectionPatch( |
| stage='grammar', start_original=0, end_original=5, |
| start_current=0, end_current=5, |
| original='ذهبوا', replacement='ذهبن', |
| priority=PRIORITY['grammar'], confidence=0.9 |
| )) |
| ps2.add(CorrectionPatch( |
| stage='punctuation', start_original=0, end_original=5, |
| start_current=0, end_current=5, |
| original='ذهبوا', replacement='ذهبوا.', |
| priority=PRIORITY['punctuation'], confidence=0.8 |
| )) |
| resolved2 = ps2.resolve_overlaps() |
| test("Case 2: punc merged", len(resolved2) == 1, f"got {len(resolved2)}") |
| if resolved2: |
| test("Appended punc to grammar 'ذهبن.'", |
| resolved2[0].replacement == 'ذهبن.', |
| f"got '{resolved2[0].replacement}'") |
|
|
| |
| ps3 = PatchSet() |
| ps3.add(CorrectionPatch( |
| stage='spelling', start_original=0, end_original=5, |
| start_current=0, end_current=5, |
| original='برفم', replacement='برغم', |
| priority=PRIORITY['spelling'], confidence=0.9 |
| )) |
| ps3.add(CorrectionPatch( |
| stage='punctuation', start_original=0, end_original=5, |
| start_current=0, end_current=5, |
| original='برفم', replacement='برفم.', |
| priority=PRIORITY['punctuation'], confidence=0.8 |
| )) |
| resolved3 = ps3.resolve_overlaps() |
| test("Case 3: spelling+punc merged (1 patch)", |
| len(resolved3) == 1, f"got {len(resolved3)}") |
| if resolved3: |
| test("Appended punc to spelling 'برغم.'", |
| resolved3[0].replacement == 'برغم.', |
| f"got '{resolved3[0].replacement}'") |
|
|
|
|
| |
| |
| |
| print("\n═══ FIX-34: Frontend — no auto re-analysis after Apply All ═══") |
|
|
| editor_js_path = os.path.join(os.path.dirname(__file__), '..', 'src', 'js', 'editor.js') |
| with open(editor_js_path, 'r', encoding='utf-8') as f: |
| editor_content = f.read() |
|
|
| |
| fn_match = re.search( |
| r'function applyAllSuggestions\b(.*?\n\})', |
| editor_content, re.DOTALL |
| ) |
| if fn_match: |
| fn_body = fn_match.group(1) |
| has_auto_analyze = bool(re.search(r'analyzeText\s*\(', fn_body)) |
| test("applyAllSuggestions does NOT call analyzeText()", |
| not has_auto_analyze, |
| "Found analyzeText() call inside applyAllSuggestions!") |
| |
| has_fix34_comment = 'FIX-34' in fn_body |
| test("FIX-34 comment present in function", |
| has_fix34_comment, "Missing FIX-34 comment") |
| else: |
| test("applyAllSuggestions function found", False, "Could not find function") |
|
|
|
|
| |
| |
| |
| print("\n═══ FIX-37: Terminal period fallback in app.py ═══") |
|
|
| app_py_path = os.path.join(os.path.dirname(__file__), '..', 'src', 'app.py') |
| with open(app_py_path, 'r', encoding='utf-8') as f: |
| app_content = f.read() |
|
|
| test("FIX-37 comment exists in app.py", |
| 'FIX-37' in app_content, |
| "Missing FIX-37 marker") |
|
|
| test("PUNC-FALLBACK log message exists", |
| 'PUNC-FALLBACK' in app_content, |
| "Missing PUNC-FALLBACK log") |
|
|
| test("Terminal period injection code exists", |
| "_lw_text + '.'" in app_content, |
| "Missing terminal period injection") |
|
|
| |
| for line in app_content.split('\n'): |
| if 'FIX-37' in line: |
| spaces = len(line) - len(line.lstrip()) |
| test(f"FIX-37 at correct indent (12 spaces)", |
| spaces == 12, |
| f"got {spaces} spaces") |
| break |
|
|
|
|
| |
| |
| |
| print(f"\n{'═'*60}") |
| if FAIL == 0: |
| print(f" ✅ ALL {PASS} TESTS PASSED") |
| else: |
| print(f" ❌ {FAIL} FAILED, {PASS} passed out of {PASS+FAIL} tests") |
| print(f"{'═'*60}") |
| if __name__ == "__main__": |
| sys.exit(1 if FAIL > 0 else 0) |
|
|