|
| 1 | +"""UYAP Live Browser Pilot 1 — Live Interoperability Fix 6.1 testleri (OFFLINE; ağ/canlı YOK). |
| 2 | +
|
| 3 | +Ek GERÇEK operatör gözlemi: satır-yerel eye/view eylemi bir UYAP UDF görüntüleyici sekmesi açtı, ama |
| 4 | +belge RENDER OLMADI; görüntüleyici görünürde şunu yazdı: |
| 5 | +"Evrak Görüntülenemedi, Evrağı indirerek Görüntüleyebilirsiniz." Dolayısıyla görüntüleyiciye ulaşmak |
| 6 | +belge içeriğinin erişilebilir olduğunu GARANTİ ETMEZ. |
| 7 | +
|
| 8 | +Fix 6.1: görüntüleyici-SONUÇ sınıflandırması (content_available / download_required / ...) + AYNI-SATIR |
| 9 | +download geri-dönüşü (Fix-6 POZİTİF download semantiğini yeniden kullanır; global/Nth/başka-satır YOK; |
| 10 | +ham UDF çıkarımı DESTEKLENMİYORSA dürüstçe raporlanır; UYDURMA/OCR/known-truth enjeksiyonu YOK). |
| 11 | +Bu testler yalnız saf/çevrimdışı davranışı doğrular; CANLI PASS DEĞİL; post-Fix-6.1 operatör rerun gerekir. |
| 12 | +""" |
| 13 | + |
| 14 | +from __future__ import annotations |
| 15 | + |
| 16 | +import json |
| 17 | +import shutil |
| 18 | + |
| 19 | +from bs4 import BeautifulSoup |
| 20 | + |
| 21 | +from sold.ingestion.uyap import ( |
| 22 | + audit_candidate, |
| 23 | + classify_viewer_outcome, |
| 24 | + extract_evidence, |
| 25 | + extraction_supported_for, |
| 26 | + genuine_fingerprint, |
| 27 | + reconcile, |
| 28 | + resolve_row_view_action, |
| 29 | + run_pilot, |
| 30 | + viewer_download_instruction_detected, |
| 31 | +) |
| 32 | +from sold.ingestion.uyap.collect import _row_action_specs |
| 33 | + |
| 34 | +TARGET = "2026/263 Esas" |
| 35 | +REAL_MSG = "Evrak Görüntülenemedi, Evrağı indirerek Görüntüleyebilirsiniz." |
| 36 | + |
| 37 | + |
| 38 | +def _acts(inner_html: str): |
| 39 | + return _row_action_specs(BeautifulSoup(f"<div class=row>{inner_html}</div>", "html.parser")) |
| 40 | + |
| 41 | + |
| 42 | +_ROW_DL_EYE = ( |
| 43 | + '<a href="#"><svg><use href="#icon-download"></use></svg></a>' |
| 44 | + '<a href="#"><svg><use href="#icon-eye"></use></svg></a>' |
| 45 | +) |
| 46 | + |
| 47 | + |
| 48 | +# --- A. Görüntüleyici sonucu: içerik erişilebilir --------------------------------------- # |
| 49 | +def test_accessible_viewer_content_is_content_available(): |
| 50 | + txt = "ARTIRMA SONUÇ TUTANAĞI İhale Bedeli: 5.715.000,00 ALACAĞA MAHSUBEN" |
| 51 | + assert classify_viewer_outcome(txt) == "content_available" |
| 52 | + assert classify_viewer_outcome("herhangi", "dom_text") == "content_available" |
| 53 | + |
| 54 | + |
| 55 | +# --- B. download_required: gerçek Türkçe / fold / mojibake ------------------------------ # |
| 56 | +def test_real_turkish_download_instruction_is_download_required(): |
| 57 | + assert viewer_download_instruction_detected(REAL_MSG) is True |
| 58 | + assert classify_viewer_outcome(REAL_MSG) == "download_required" |
| 59 | + |
| 60 | + |
| 61 | +def test_folded_turkish_download_instruction(): |
| 62 | + folded = "evrak goruntulenemedi, evragi indirerek goruntuleyebilirsiniz." |
| 63 | + assert classify_viewer_outcome(folded) == "download_required" |
| 64 | + |
| 65 | + |
| 66 | +def test_mojibake_download_instruction(): |
| 67 | + assert classify_viewer_outcome(REAL_MSG.encode("utf-8").decode("latin-1")) == "download_required" |
| 68 | + assert classify_viewer_outcome(REAL_MSG.encode("utf-8").decode("cp1252")) == "download_required" |
| 69 | + |
| 70 | + |
| 71 | +# --- C. Katı: yalın indir / program indirme / eksik bileşen download_required DEĞİL ------ # |
| 72 | +def test_generic_indir_alone_is_not_download_required(): |
| 73 | + assert classify_viewer_outcome("Dosyayı indir") != "download_required" |
| 74 | + assert viewer_download_instruction_detected("Dosyayı indir") is False |
| 75 | + |
| 76 | + |
| 77 | +def test_generic_program_download_instruction_is_not_download_required(): |
| 78 | + assert classify_viewer_outcome("Programı indirmek için tıklayınız") != "download_required" |
| 79 | + |
| 80 | + |
| 81 | +def test_viewer_failure_without_instruction_does_not_trigger_fallback(): |
| 82 | + # 'Evrak Görüntülenemedi' TEK BAŞINA download_required değil (fallback tetiklemez) → viewer_error |
| 83 | + assert classify_viewer_outcome("Evrak Görüntülenemedi") == "viewer_error" |
| 84 | + assert viewer_download_instruction_detected("Evrak Görüntülenemedi") is False |
| 85 | + |
| 86 | + |
| 87 | +def test_download_instruction_without_failure_does_not_trigger_fallback(): |
| 88 | + assert viewer_download_instruction_detected("Evrağı indirerek Görüntüleyebilirsiniz") is False |
| 89 | + |
| 90 | + |
| 91 | +# --- D. Fix-6 POZİTİF download eylemi AYNI SATIRDAN çözülür ------------------------------ # |
| 92 | +def test_same_row_download_action_is_positively_resolved(): |
| 93 | + acts = _acts(_ROW_DL_EYE) |
| 94 | + res = resolve_row_view_action(acts) |
| 95 | + assert res["resolved"] is True and res["view_action"]["semantic"] == "view" |
| 96 | + assert res["download_action_resolved"] is True |
| 97 | + assert res["download_action"]["semantic"] == "download" |
| 98 | + # AYNI satırın eylemlerinden biri (global/Nth/başka-satır DEĞİL) |
| 99 | + assert res["download_action"] in acts and res["view_action"] in acts |
| 100 | + |
| 101 | + |
| 102 | +def test_download_only_row_has_no_resolved_view_so_no_viewer_opened(): |
| 103 | + # view çözülmediğinde görüntüleyici HİÇ açılmaz → outcome/fallback devreye girmez |
| 104 | + res = resolve_row_view_action(_acts('<a class="fa-download"></a>')) |
| 105 | + assert res["resolved"] is False and res["view_action"] is None |
| 106 | + |
| 107 | + |
| 108 | +def test_two_downloads_download_action_unresolved(): |
| 109 | + res = resolve_row_view_action(_acts('<a class="fa-download"></a><a class="fa-download"></a>')) |
| 110 | + assert res["download_action_resolved"] is False and res["download_action"] is None |
| 111 | + |
| 112 | + |
| 113 | +def test_view_only_row_download_action_unresolved(): |
| 114 | + res = resolve_row_view_action(_acts('<a title="Goruntule"></a>')) |
| 115 | + assert res["resolved"] is True |
| 116 | + assert res["download_action_resolved"] is False and res["download_action"] is None |
| 117 | + |
| 118 | + |
| 119 | +def test_non_view_unknown_not_auto_download(): |
| 120 | + # [view, unknown] → download çıkarılmaz (unknown download DEĞİL) |
| 121 | + res = resolve_row_view_action(_acts('<a title="Goruntule"></a><a class="icon-foo"></a>')) |
| 122 | + assert res["resolved"] is True |
| 123 | + assert res["download_action_resolved"] is False and res["download_action_detected"] is False |
| 124 | + |
| 125 | + |
| 126 | +def test_second_or_nth_action_not_auto_download(): |
| 127 | + res = resolve_row_view_action(_acts('<a class="x"></a><a class="y"></a>')) |
| 128 | + assert res["download_action_resolved"] is False # konum/Nth ile download SEÇİLMEZ |
| 129 | + |
| 130 | + |
| 131 | +# --- E. Çıkarım-desteği dürüstlüğü (ham UDF/PDF DESTEKLENMEZ) ---------------------------- # |
| 132 | +def test_extraction_supported_only_for_text_html(): |
| 133 | + assert extraction_supported_for(".txt") is True |
| 134 | + assert extraction_supported_for(".html") is True and extraction_supported_for(".htm") is True |
| 135 | + assert extraction_supported_for("txt") is True # nokta olmadan da |
| 136 | + assert extraction_supported_for(".udf") is False |
| 137 | + assert extraction_supported_for(".pdf") is False |
| 138 | + assert extraction_supported_for(".zip") is False |
| 139 | + assert extraction_supported_for(None) is False |
| 140 | + |
| 141 | + |
| 142 | +def test_mimetype_udf_hint_alone_does_not_imply_support(): |
| 143 | + # viewer URL mimeType=Udf tek başına destek ANLAMINA GELMEZ |
| 144 | + assert extraction_supported_for(".udf", "udf") is False |
| 145 | + assert extraction_supported_for(None, "udf") is False |
| 146 | + |
| 147 | + |
| 148 | +# --- F. OUTCOME C dürüstlüğü: indirilen ham UDF çıkarılamaz, known-truth ENJEKTE EDİLMEZ -- # |
| 149 | +def test_downloaded_unsupported_udf_yields_no_injected_price(): |
| 150 | + # İndirilmiş ama çıkarımı desteklenmeyen resmî auction_result (metin YOK) → İhale Bedeli None |
| 151 | + downloaded = {"artifact_type": "auction_result", "source_ref": "download:.udf", "extraction_supported": False} |
| 152 | + ev = extract_evidence([downloaded], institution="Ankara", file_id=TARGET) |
| 153 | + assert ev.ihale_bedeli is None # 5715000 ENJEKTE EDİLMEZ |
| 154 | + au = audit_candidate(ev, reconcile([downloaded], "Ankara", TARGET)) |
| 155 | + assert au.auction_price is None and au.decision != "ADMISSIBLE_COMPLETED_SALE" |
| 156 | + |
| 157 | + |
| 158 | +def test_result_card_satis_tutari_not_substituted(): |
| 159 | + card = {"artifact_type": "status_card", |
| 160 | + "text": "Satış Durumu Satıldı Satış Tutarı 5.715.000,00 TL 50984 Ada 1 Parsel"} |
| 161 | + ev = extract_evidence([card], institution="Ankara", file_id=TARGET) |
| 162 | + au = audit_candidate(ev, reconcile([card], "Ankara", TARGET)) |
| 163 | + assert ev.ihale_bedeli is None and au.decision != "ADMISSIBLE_COMPLETED_SALE" |
| 164 | + |
| 165 | + |
| 166 | +# --- G. Non-mutation: indirilen ham artifact uyap.json'ı DEĞİŞTİRMEZ / ADMİT ETMEZ ------- # |
| 167 | +def test_run_pilot_download_required_unsupported_non_mutating(tmp_path): |
| 168 | + from sold.structural.datasets import GENUINE_DIR |
| 169 | + |
| 170 | + gdir = tmp_path / "genuine" |
| 171 | + gdir.mkdir() |
| 172 | + for f in ("uyap.json", "kap.json", "toki.json"): |
| 173 | + shutil.copyfile(GENUINE_DIR / f, gdir / f) |
| 174 | + gp = gdir / "uyap.json" |
| 175 | + before = genuine_fingerprint(gp) |
| 176 | + # OUTCOME C: görüntüleyici download_required → aynı-satır indirme → ham UDF çıkarımı DESTEKLENMEZ |
| 177 | + artifacts = [ |
| 178 | + {"artifact_type": "status_card", |
| 179 | + "text": "Muhammen Bedel 6.800.000,00 TL KDV Oranı : %20 Satış Durumu Satıldı 50984 Ada 1 Parsel", |
| 180 | + "source_ref": "live://index"}, |
| 181 | + {"artifact_type": "auction_result", "source_ref": "download:.udf", "extraction_supported": False}, |
| 182 | + ] |
| 183 | + r = run_pilot(offline_artifacts=artifacts, genuine_path=gp, store_dir=tmp_path, report_path=tmp_path / "r.json") |
| 184 | + assert r["pilot_outcome"] in ("NOT_RUN", "FAIL", "PARTIAL") # ham UDF çıkarılamadı → PASS DEĞİL |
| 185 | + mg = r["mutation_guard"] |
| 186 | + assert mg["uyap_json_unchanged"] and mg["genuine_uyap_count_unchanged"] and mg["smm_moments_unchanged"] |
| 187 | + assert mg["uyap_sale_prob_absent"] is True |
| 188 | + after = genuine_fingerprint(gp) |
| 189 | + assert after["genuine_uyap_count"] == 7 and after["sha256"] == before["sha256"] |
| 190 | + recs = json.loads(gp.read_text(encoding="utf-8")) |
| 191 | + assert sum(1 for x in recs if str(x.get("public_record_id")) == TARGET) == 1 # 8. gözlem YOK |
| 192 | + |
| 193 | + |
| 194 | +def test_run_pilot_fix6_1_offline_supported_path_non_mutating(tmp_path): |
| 195 | + from sold.structural.datasets import GENUINE_DIR |
| 196 | + |
| 197 | + gdir = tmp_path / "genuine" |
| 198 | + gdir.mkdir() |
| 199 | + for f in ("uyap.json", "kap.json", "toki.json"): |
| 200 | + shutil.copyfile(GENUINE_DIR / f, gdir / f) |
| 201 | + gp = gdir / "uyap.json" |
| 202 | + before = genuine_fingerprint(gp) |
| 203 | + artifacts = [ |
| 204 | + {"artifact_type": "status_card", |
| 205 | + "text": "Muhammen Bedel 6.800.000,00 TL KDV Oranı : %20 Satış Durumu Satıldı 50984 Ada 1 Parsel 60 Nolu B.B.", |
| 206 | + "source_ref": "live://index"}, |
| 207 | + {"artifact_type": "auction_result", |
| 208 | + "text": ("ARTIRMA SONUÇ TUTANAĞI 50984 Ada 1 Parsel İhale Bedeli: 5.715.000,00 " |
| 209 | + "Ödenmesi Gereken Bedel: ALACAĞA MAHSUBEN Satıldı Satış İşlemleri Tamamlandı"), |
| 210 | + "source_ref": "download:.html"}, # desteklenen indirilmiş format |
| 211 | + {"artifact_type": "appraisal_report", |
| 212 | + "text": "BİLİRKİŞİ RAPORU 6.800.000,00 TL 50984 Ada 1 Parsel 60 Nolu Bağımsız Bölüm", |
| 213 | + "source_ref": "viewer:udf_viewer"}, |
| 214 | + ] |
| 215 | + r = run_pilot(offline_artifacts=artifacts, genuine_path=gp, store_dir=tmp_path, report_path=tmp_path / "r.json") |
| 216 | + assert r["verification_layer_result"] == "PASS" |
| 217 | + assert r["pilot_outcome"] == "NOT_RUN" # offline → CANLI PASS DEĞİL |
| 218 | + mg = r["mutation_guard"] |
| 219 | + assert mg["uyap_json_unchanged"] and mg["genuine_uyap_count_unchanged"] |
| 220 | + after = genuine_fingerprint(gp) |
| 221 | + assert after["genuine_uyap_count"] == 7 and after["sha256"] == before["sha256"] |
| 222 | + |
| 223 | + |
| 224 | +# --- H. Yapısal freeze -------------------------------------------------------------------- # |
| 225 | +def test_structural_freeze_four_moments_no_sale_prob(): |
| 226 | + from sold.structural import build_observed_moments, load_genuine_datasets |
| 227 | + |
| 228 | + g = load_genuine_datasets() |
| 229 | + built = build_observed_moments(g["uyap"], g["kap"], g["toki_result"]) |
| 230 | + smm = {k for k in built["moments"] if k.startswith(("uyap_win", "kap_log"))} |
| 231 | + assert smm == { |
| 232 | + "uyap_win_over_appraisal_mean", "uyap_win_over_appraisal_sd", |
| 233 | + "kap_log_ratio_mean", "kap_log_ratio_sd", |
| 234 | + } |
| 235 | + assert "uyap_sale_prob" not in built["moments"] |
| 236 | + |
| 237 | + |
| 238 | +def test_toki_external_zero_moments_and_conditional_on_trade(): |
| 239 | + from sold.structural import ( |
| 240 | + DEFAULT_FREE, |
| 241 | + PRICE_ESTIMATE_CONDITION, |
| 242 | + StructuralParams, |
| 243 | + build_observed_moments, |
| 244 | + context_from_datasets, |
| 245 | + load_genuine_datasets, |
| 246 | + source_jacobian_ranks, |
| 247 | + ) |
| 248 | + |
| 249 | + assert PRICE_ESTIMATE_CONDITION == "conditional_on_trade" |
| 250 | + g = load_genuine_datasets() |
| 251 | + built = build_observed_moments(g["uyap"], g["kap"], g["toki_result"]) |
| 252 | + ctx = context_from_datasets(g["uyap"], g["kap"]) |
| 253 | + sj = source_jacobian_ranks(StructuralParams(), ctx, DEFAULT_FREE, built["moments"], built["provenance"]) |
| 254 | + assert sj["J_TOKI"]["rank"] == 0 and sj["J_TOKI"]["n_moments"] == 0 |
0 commit comments