Skip to content

Commit db22134

Browse files
committed
test(uyap): logical row-boundary + actionable-ownership + pre-opened list (Live Fix 7, offline)
26 offline tests (no network/live): the Run-7 shape (label + doc icon + download arrow inside one button, plus a sibling eye button) now yields one DocumentRow whose logical ancestor captures TWO button controls (actionable_control_tags==['button','button']) with view+download both resolved; a nested document icon and a nested fa-arrow-down are not their own ActionSpecs; two icons inside one button => one actionable control (fa-arrow-down in icon_tokens); one download button + one view button => two controls; actionable-sibling expansion captures the eye when the label button has a nested clickable; the logical row contains exactly one recognized document identity and is never the whole modal/body/html; row-locality holds (each document gets only its own view+download, no global/Nth/index mapping); bare-icon rows still work via the icon fallback; an unknown/positional/right-most sibling is never inferred as view; the same-row Fix-6.1 fallback uses the corrected row's own download action; pre-opened reuse: a valid visible list is reusable and flows through the Fix-7 boundary, while hidden templates, raw-HTML-only labels without a container, an unrelated candidate, and unsupported page states are all rejected; result-card Satis Tutari not substituted; no known-truth injection; pilot non-mutating (count 7, uyap.json unchanged, never admits); structural freeze (4 SMM moments, no uyap_sale_prob, TOKI external 0, conditional_on_trade). Full suite: 440 passed (414 baseline + 26).
1 parent 68d10ea commit db22134

1 file changed

Lines changed: 332 additions & 0 deletions

File tree

tests/test_uyap_live_fix7.py

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
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

Comments
 (0)