|
| 1 | +"""UYAP Live Browser Pilot 1 — Live Interoperability Fix 7 testleri (OFFLINE; ağ/canlı YOK). |
| 2 | +
|
| 3 | +Yedinci gerçek-canlı FAIL: Fix 5 (modal/satır algısı) + Fix 6 (fa-arrow-down → download) CANLI çalıştı |
| 4 | +(download_action_resolved=true), ama HER satır için view_action_resolved=false (no_view_action) ve |
| 5 | +viewer_pages_opened=0. Kök neden: MANTIKSAL satır sınırı ÇOK DAR; introspektör bare ``<i>`` ikon |
| 6 | +descendant'larını (belge ikonu + fa-arrow-down) ActionSpec sayıyor; AYRI eye/view kontrolü (kardeş) |
| 7 | +seçili semantik-etiket-atasının DIŞINDA kalıyor. |
| 8 | +
|
| 9 | +Fix 7: MANTIKSAL satır atası = GERÇEK tıklanabilir kontrol içeren, tek-belge-etiketli en küçük ata |
| 10 | +(kardeş-actionable genişlemesiyle ayrı eye kontrolünü kapsar); ikon descendant'lar sahip actionable |
| 11 | +kontrolün metadata'sıdır. Ek: pre-opened/stale liste durumunda kart-kontrolüne yeniden tıklamadan |
| 12 | +geçerli görünür listeyi yeniden kullan. Bu testler yalnız çevrimdışı davranışı doğrular; CANLI PASS DEĞİL. |
| 13 | +""" |
| 14 | + |
| 15 | +from __future__ import annotations |
| 16 | + |
| 17 | +import json |
| 18 | +import shutil |
| 19 | + |
| 20 | +from bs4 import BeautifulSoup |
| 21 | + |
| 22 | +from sold.ingestion.uyap import ( |
| 23 | + audit_candidate, |
| 24 | + classify_document_label, |
| 25 | + detect_document_list, |
| 26 | + extract_document_rows_semantic, |
| 27 | + extract_evidence, |
| 28 | + genuine_fingerprint, |
| 29 | + preopened_document_list_reusable, |
| 30 | + reconcile, |
| 31 | + resolve_row_view_action, |
| 32 | + run_pilot, |
| 33 | +) |
| 34 | +from sold.ingestion.uyap.collect import _row_action_specs, _semantic_row_for_label, _semantic_label_elements, _bs_soup |
| 35 | + |
| 36 | +TARGET = "2026/263 Esas" |
| 37 | + |
| 38 | + |
| 39 | +# --- Run-7 gerçek-stil satır: etiket + belge ikonu + indirme oku BİR butonda; AYRI eye butonu kardeş -- # |
| 40 | +def _row(label, primary_dl="fa fa-arrow-down", view_icon="fa fa-eye"): |
| 41 | + return ( |
| 42 | + '<div class="logical-document-row">' |
| 43 | + '<button class="document-primary-control">' |
| 44 | + f'<i class="icon-docs icon-special-pink"></i><span>{label}</span>' |
| 45 | + f'<i class="{primary_dl}"></i></button>' |
| 46 | + f'<button class="document-view-control"><i class="{view_icon}"></i></button>' |
| 47 | + '</div>' |
| 48 | + ) |
| 49 | + |
| 50 | + |
| 51 | +def _modal(*labels, title="Ihale Evrak Listesi"): |
| 52 | + body = "".join(_row(l) for l in labels) |
| 53 | + return f'<div class="modal-overlay"><div class=hd>{title}</div>{body}</div>' |
| 54 | + |
| 55 | + |
| 56 | +_FOUR = ("Satis Ilani", "2- Satis Sartnamesi Ve Tutanagi", |
| 57 | + "3- BILIRKISI RAPORU 2026 263 ESAS.udf", "1- Artirma Sonuc / Uzatma Tutanagi") |
| 58 | + |
| 59 | + |
| 60 | +def _rows(html): |
| 61 | + return extract_document_rows_semantic(html) |
| 62 | + |
| 63 | + |
| 64 | +def _by_type(html): |
| 65 | + return {classify_document_label(r["label"]): r for r in _rows(html)} |
| 66 | + |
| 67 | + |
| 68 | +# --- A. Run-7 reprodüksiyonu: mantıksal satır iki BUTONU kapsar (bare <i> DEĞİL) ---------- # |
| 69 | +def test_run7_row_captures_primary_and_sibling_view_button(): |
| 70 | + rows = _rows(_modal(*_FOUR)) |
| 71 | + assert len(rows) == 4 |
| 72 | + for r in rows: |
| 73 | + assert len(r["actions"]) == 2 # iki BUTON (bare <i> değil) |
| 74 | + assert r["row_boundary"]["actionable_control_tags"] == ["button", "button"] |
| 75 | + res = resolve_row_view_action(r["actions"]) |
| 76 | + assert res["resolved"] is True # ayrı eye butonu artık dahil |
| 77 | + assert res["download_action_resolved"] is True # primary buton = download |
| 78 | + |
| 79 | + |
| 80 | +def test_auction_result_resolves_view_and_download(): |
| 81 | + r = _by_type(_modal(*_FOUR))["auction_result"] |
| 82 | + res = resolve_row_view_action(r["actions"]) |
| 83 | + assert res["view_action"]["semantic"] == "view" and res["download_action"]["semantic"] == "download" |
| 84 | + |
| 85 | + |
| 86 | +# --- B. İkon descendant ≠ ActionSpec ------------------------------------------------------ # |
| 87 | +def test_two_icons_in_one_button_is_one_actionable_control(): |
| 88 | + specs = _row_action_specs(BeautifulSoup( |
| 89 | + '<div><button><i class="icon-docs"></i><span>x</span><i class="fa fa-arrow-down"></i></button></div>', |
| 90 | + "html.parser")) |
| 91 | + assert len(specs) == 1 # tek buton (iki <i> ayrı eylem DEĞİL) |
| 92 | + assert specs[0]["tag"] == "button" and specs[0]["semantic"] == "download" |
| 93 | + assert "fa-arrow-down" in specs[0]["icon_tokens"] # ikon token sahip butonda |
| 94 | + |
| 95 | + |
| 96 | +def test_one_download_button_plus_one_view_button_is_two_controls(): |
| 97 | + specs = _row_action_specs(BeautifulSoup( |
| 98 | + '<div><button><i class="fa fa-arrow-down"></i></button><button><i class="fa fa-eye"></i></button></div>', |
| 99 | + "html.parser")) |
| 100 | + assert len(specs) == 2 and [s["tag"] for s in specs] == ["button", "button"] |
| 101 | + assert {s["semantic"] for s in specs} == {"download", "view"} |
| 102 | + |
| 103 | + |
| 104 | +def test_nested_download_arrow_icon_is_not_its_own_actionspec(): |
| 105 | + r = _by_type(_modal("1- Artirma Sonuc / Uzatma Tutanagi"))["auction_result"] |
| 106 | + # eylemler butonlar; fa-arrow-down descendant token olarak primary butonda |
| 107 | + assert all(a["tag"] == "button" for a in r["actions"]) |
| 108 | + prim = [a for a in r["actions"] if a["semantic"] == "download"][0] |
| 109 | + assert "fa-arrow-down" in prim["icon_tokens"] |
| 110 | + |
| 111 | + |
| 112 | +def test_nested_fa_eye_gives_view_to_owner_button(): |
| 113 | + r = _by_type(_modal("1- Artirma Sonuc / Uzatma Tutanagi"))["auction_result"] |
| 114 | + view = [a for a in r["actions"] if a["semantic"] == "view"][0] |
| 115 | + assert view["tag"] == "button" and "fa-eye" in view["icon_tokens"] |
| 116 | + |
| 117 | + |
| 118 | +# --- C. Actionable-sibling genişlemesi (label-butonun iç tıklanabilirinden kaçış) --------- # |
| 119 | +def test_actionable_sibling_expansion_captures_view(): |
| 120 | + # primary buton İÇİNDE bir <a> download var (iç tıklanabilir); yine de kardeş eye kapsanmalı |
| 121 | + html = ( |
| 122 | + '<div class="row">' |
| 123 | + '<button class="primary"><span>1- Artirma Sonuc / Uzatma Tutanagi</span>' |
| 124 | + '<a class="fa fa-arrow-down"></a></button>' |
| 125 | + '<button class="eye"><i class="fa fa-eye"></i></button></div>' |
| 126 | + ) |
| 127 | + label_els = _semantic_label_elements(_bs_soup(html)) |
| 128 | + _el, actions, meta = _semantic_row_for_label(label_els[0][0]) |
| 129 | + assert meta["row_boundary_strategy"] == "actionable_sibling_expansion" |
| 130 | + assert meta["logical_row_actionable_control_count"] == 2 |
| 131 | + res = resolve_row_view_action(actions) |
| 132 | + assert res["resolved"] is True and res["download_action_resolved"] is True |
| 133 | + |
| 134 | + |
| 135 | +# --- D. Satır sınırı guard'ları: tek belge kimliği; modal/body reddedilir ----------------- # |
| 136 | +def test_logical_row_contains_exactly_one_recognized_type(): |
| 137 | + for r in _rows(_modal(*_FOUR)): |
| 138 | + assert r["row_boundary"]["logical_row_recognized_type_count"] == 1 |
| 139 | + |
| 140 | + |
| 141 | +def test_row_ancestor_is_not_the_whole_modal(): |
| 142 | + rows = _rows(_modal(*_FOUR)) |
| 143 | + # satır atası modal-overlay DEĞİL (logical-document-row div); 4 tür değil 1 tür |
| 144 | + assert all(r["row_boundary"]["logical_row_ancestor_kind"] == "div" for r in rows) |
| 145 | + assert all(r["row_boundary"]["logical_row_recognized_type_count"] == 1 for r in rows) |
| 146 | + |
| 147 | + |
| 148 | +def test_multiple_recognized_types_ancestor_rejected_as_row(): |
| 149 | + # tek satır iki belge etiketi içerse (bozuk), satır o kadar geniş olmaz — tür sayısı 1 kalır |
| 150 | + rows = _rows(_modal(*_FOUR)) |
| 151 | + assert max(r["row_boundary"]["logical_row_recognized_type_count"] for r in rows) == 1 |
| 152 | + |
| 153 | + |
| 154 | +# --- E. Satır-yerellik: her belgenin kendi eylemi (global/Nth/index eşleme YOK) ----------- # |
| 155 | +def test_row_locality_each_document_has_own_view_and_download(): |
| 156 | + bt = _by_type(_modal(*_FOUR)) |
| 157 | + for atype in ("auction_result", "appraisal_report", "sale_notice", "sale_spec"): |
| 158 | + res = resolve_row_view_action(bt[atype]["actions"]) |
| 159 | + assert res["resolved"] is True and res["download_action_resolved"] is True |
| 160 | + # her satırda TAM 2 kontrol (kendi butonları); global eye listesi/Nth eşleme yok |
| 161 | + assert len(bt[atype]["actions"]) == 2 |
| 162 | + |
| 163 | + |
| 164 | +def test_bare_icon_rows_still_work_via_icon_fallback(): |
| 165 | + # Buton yoksa (Fix-5/6 bare-icon satırı) ikon-yalnız fallback korunur |
| 166 | + html = ('<div class="modal-overlay"><div class=hd>Ihale Evrak Listesi</div>' |
| 167 | + '<div class=item><span>Satis Ilani</span><i title=Indir></i><i title=Goruntule></i></div>' |
| 168 | + '<div class=item><span>1- Artirma Sonuc / Uzatma Tutanagi</span><i title=Indir></i><i title=Goruntule></i></div>' |
| 169 | + '</div>') |
| 170 | + rows = _rows(html) |
| 171 | + assert len(rows) == 2 |
| 172 | + assert all(r["row_boundary"]["row_boundary_strategy"] == "icon_only_ancestor" for r in rows) |
| 173 | + for r in rows: |
| 174 | + assert resolve_row_view_action(r["actions"])["resolved"] is True |
| 175 | + |
| 176 | + |
| 177 | +# --- F. Belirsiz/pozisyon: kardeş konumu view ANLAMINA GELMEZ ----------------------------- # |
| 178 | +def test_unknown_sibling_control_remains_unresolved(): |
| 179 | + html = ('<div class="row"><button class="primary"><span>Satis Ilani</span>' |
| 180 | + '<i class="fa fa-arrow-down"></i></button>' |
| 181 | + '<button class="mystery"><i class="icon-foo"></i></button></div>') |
| 182 | + _el, actions, _m = _semantic_row_for_label(_semantic_label_elements(_bs_soup(html))[0][0]) |
| 183 | + res = resolve_row_view_action(actions) |
| 184 | + assert res["resolved"] is False # kardeş var ama view semantiği YOK |
| 185 | + assert res["download_action_resolved"] is True # primary yine download |
| 186 | + |
| 187 | + |
| 188 | +def test_sibling_position_alone_does_not_imply_view(): |
| 189 | + html = ('<div class="row"><button class="a"><span>Satis Ilani</span><i class="icon-x"></i></button>' |
| 190 | + '<button class="b"><i class="icon-y"></i></button></div>') |
| 191 | + _el, actions, _m = _semantic_row_for_label(_semantic_label_elements(_bs_soup(html))[0][0]) |
| 192 | + assert resolve_row_view_action(actions)["resolved"] is False # ikinci/sağdaki view DEĞİL |
| 193 | + |
| 194 | + |
| 195 | +# --- G. Fix-6.1 aynı-satır fallback düzeltilmiş satırın KENDİ download eylemini alır ------ # |
| 196 | +def test_same_row_fallback_uses_corrected_rows_own_download(): |
| 197 | + r = _by_type(_modal(*_FOUR))["auction_result"] |
| 198 | + res = resolve_row_view_action(r["actions"]) |
| 199 | + # aynı DocumentRow hem view hem download çözer; fallback bu satırın download'ını kullanır |
| 200 | + assert res["download_action"]["semantic"] == "download" |
| 201 | + assert res["download_action"] in r["actions"] and res["view_action"] in r["actions"] |
| 202 | + |
| 203 | + |
| 204 | +# --- H. Pre-opened / stale belge listesi yeniden kullanımı -------------------------------- # |
| 205 | +def _listing_with_preopened(card_file="2026/263 Icra", card_inst="Ankara Gayrimenkul Satis Icra Dairesi"): |
| 206 | + return ( |
| 207 | + f'<div class=ilan-card>{card_inst} {card_file} ' |
| 208 | + '<button>Incele</button><button>Ihale Evrak Listesi</button></div>' |
| 209 | + + _modal(*_FOUR) |
| 210 | + ) |
| 211 | + |
| 212 | + |
| 213 | +LISTING_URL = "https://esatis.uyap.gov.tr/pp/index.jsp" |
| 214 | + |
| 215 | + |
| 216 | +def test_valid_preopened_list_is_reusable(): |
| 217 | + assert preopened_document_list_reusable(_listing_with_preopened(), LISTING_URL, TARGET) is True |
| 218 | + |
| 219 | + |
| 220 | +def test_preopened_reuse_flows_to_row_collection_with_fix7_boundary(): |
| 221 | + rows = _rows(_listing_with_preopened()) |
| 222 | + assert len(rows) == 4 |
| 223 | + for r in rows: |
| 224 | + assert r["row_boundary"]["actionable_control_tags"] == ["button", "button"] |
| 225 | + assert resolve_row_view_action(r["actions"])["resolved"] is True |
| 226 | + |
| 227 | + |
| 228 | +def test_hidden_preloaded_labels_do_not_trigger_preopened_reuse(): |
| 229 | + hidden_modal = _modal(*_FOUR).replace('class="modal-overlay"', 'class="modal-overlay" style="display:none"') |
| 230 | + html = ('<div class=ilan-card>Ankara 2026/263 Icra <button>Incele</button>' |
| 231 | + '<button>Ihale Evrak Listesi</button></div>' + hidden_modal) |
| 232 | + assert preopened_document_list_reusable(html, LISTING_URL, TARGET) is False |
| 233 | + |
| 234 | + |
| 235 | +def test_raw_html_only_labels_without_container_not_reused(): |
| 236 | + # etiketler var ama ortak-ata konteyner yok (ayrı üst-düzey div'ler) → detect False |
| 237 | + html = ('<div class=ilan-card>Ankara 2026/263 Icra <button>Incele</button>' |
| 238 | + '<button>Ihale Evrak Listesi</button></div>' |
| 239 | + '<div>Satis Ilani</div><div>1- Artirma Sonuc / Uzatma Tutanagi</div>') |
| 240 | + assert preopened_document_list_reusable(html, LISTING_URL, TARGET) is False |
| 241 | + |
| 242 | + |
| 243 | +def test_stale_list_from_unrelated_candidate_not_reused(): |
| 244 | + html = _listing_with_preopened(card_file="2024/99 Icra", card_inst="Izmir 6. Icra Dairesi") |
| 245 | + assert preopened_document_list_reusable(html, LISTING_URL, TARGET) is False # hedef kimlik yok |
| 246 | + |
| 247 | + |
| 248 | +def test_preopened_requires_supported_page_state(): |
| 249 | + # viewer sayfası pre-opened liste SAYILMAZ |
| 250 | + assert preopened_document_list_reusable(_modal(*_FOUR), |
| 251 | + "https://esatis.uyap.gov.tr/pp/viewer.jsp?mimeType=Udf", TARGET) is False |
| 252 | + |
| 253 | + |
| 254 | +# --- I. Fiyat/appraisal semantiği ve known-truth enjeksiyon YOK --------------------------- # |
| 255 | +def test_result_card_satis_tutari_not_substituted(): |
| 256 | + card = {"artifact_type": "status_card", |
| 257 | + "text": "Satış Durumu Satıldı Satış Tutarı 5.715.000,00 TL 50984 Ada 1 Parsel"} |
| 258 | + ev = extract_evidence([card], institution="Ankara", file_id=TARGET) |
| 259 | + au = audit_candidate(ev, reconcile([card], "Ankara", TARGET)) |
| 260 | + assert ev.ihale_bedeli is None and au.decision != "ADMISSIBLE_COMPLETED_SALE" |
| 261 | + |
| 262 | + |
| 263 | +def test_no_known_truth_injected_for_unextracted_auction(): |
| 264 | + downloaded = {"artifact_type": "auction_result", "source_ref": "download:.udf", "extraction_supported": False} |
| 265 | + ev = extract_evidence([downloaded], institution="Ankara", file_id=TARGET) |
| 266 | + assert ev.ihale_bedeli is None # 5715000 enjekte EDİLMEZ |
| 267 | + |
| 268 | + |
| 269 | +# --- J. Non-mutation + freeze ------------------------------------------------------------- # |
| 270 | +def test_run_pilot_fix7_offline_non_mutating(tmp_path): |
| 271 | + from sold.structural.datasets import GENUINE_DIR |
| 272 | + |
| 273 | + gdir = tmp_path / "genuine" |
| 274 | + gdir.mkdir() |
| 275 | + for f in ("uyap.json", "kap.json", "toki.json"): |
| 276 | + shutil.copyfile(GENUINE_DIR / f, gdir / f) |
| 277 | + gp = gdir / "uyap.json" |
| 278 | + before = genuine_fingerprint(gp) |
| 279 | + artifacts = [ |
| 280 | + {"artifact_type": "status_card", |
| 281 | + "text": "Muhammen Bedel 6.800.000,00 TL KDV Oranı : %20 Satış Durumu Satıldı 50984 Ada 1 Parsel 60 Nolu B.B.", |
| 282 | + "source_ref": "live://index"}, |
| 283 | + {"artifact_type": "auction_result", |
| 284 | + "text": ("ARTIRMA SONUÇ TUTANAĞI 50984 Ada 1 Parsel İhale Bedeli: 5.715.000,00 " |
| 285 | + "Ödenmesi Gereken Bedel: ALACAĞA MAHSUBEN Satıldı Satış İşlemleri Tamamlandı"), |
| 286 | + "source_ref": "viewer:udf_viewer"}, |
| 287 | + {"artifact_type": "appraisal_report", |
| 288 | + "text": "BİLİRKİŞİ RAPORU 6.800.000,00 TL 50984 Ada 1 Parsel 60 Nolu Bağımsız Bölüm", |
| 289 | + "source_ref": "viewer:udf_viewer"}, |
| 290 | + ] |
| 291 | + r = run_pilot(offline_artifacts=artifacts, genuine_path=gp, store_dir=tmp_path, report_path=tmp_path / "r.json") |
| 292 | + assert r["verification_layer_result"] == "PASS" |
| 293 | + assert r["pilot_outcome"] == "NOT_RUN" |
| 294 | + mg = r["mutation_guard"] |
| 295 | + assert mg["uyap_json_unchanged"] and mg["genuine_uyap_count_unchanged"] and mg["smm_moments_unchanged"] |
| 296 | + assert mg["uyap_sale_prob_absent"] is True |
| 297 | + after = genuine_fingerprint(gp) |
| 298 | + assert after["genuine_uyap_count"] == 7 and after["sha256"] == before["sha256"] |
| 299 | + recs = json.loads(gp.read_text(encoding="utf-8")) |
| 300 | + assert sum(1 for x in recs if str(x.get("public_record_id")) == TARGET) == 1 |
| 301 | + |
| 302 | + |
| 303 | +def test_structural_freeze_four_moments_no_sale_prob(): |
| 304 | + from sold.structural import build_observed_moments, load_genuine_datasets |
| 305 | + |
| 306 | + g = load_genuine_datasets() |
| 307 | + built = build_observed_moments(g["uyap"], g["kap"], g["toki_result"]) |
| 308 | + smm = {k for k in built["moments"] if k.startswith(("uyap_win", "kap_log"))} |
| 309 | + assert smm == { |
| 310 | + "uyap_win_over_appraisal_mean", "uyap_win_over_appraisal_sd", |
| 311 | + "kap_log_ratio_mean", "kap_log_ratio_sd", |
| 312 | + } |
| 313 | + assert "uyap_sale_prob" not in built["moments"] |
| 314 | + |
| 315 | + |
| 316 | +def test_toki_external_zero_moments_and_conditional_on_trade(): |
| 317 | + from sold.structural import ( |
| 318 | + DEFAULT_FREE, |
| 319 | + PRICE_ESTIMATE_CONDITION, |
| 320 | + StructuralParams, |
| 321 | + build_observed_moments, |
| 322 | + context_from_datasets, |
| 323 | + load_genuine_datasets, |
| 324 | + source_jacobian_ranks, |
| 325 | + ) |
| 326 | + |
| 327 | + assert PRICE_ESTIMATE_CONDITION == "conditional_on_trade" |
| 328 | + g = load_genuine_datasets() |
| 329 | + built = build_observed_moments(g["uyap"], g["kap"], g["toki_result"]) |
| 330 | + ctx = context_from_datasets(g["uyap"], g["kap"]) |
| 331 | + sj = source_jacobian_ranks(StructuralParams(), ctx, DEFAULT_FREE, built["moments"], built["provenance"]) |
| 332 | + assert sj["J_TOKI"]["rank"] == 0 and sj["J_TOKI"]["n_moments"] == 0 |
0 commit comments