"""Per-field priority resolution tests.""" from datetime import datetime from app.services.matching.conflict_resolution import ( HOUSE_FIELD_PRIORITY, LISTING_FIELD_PRIORITY, resolve_house_field, resolve_listing_field, ) # --------------------------------------------------------------------------- # Pre-existing tests (preserved) # --------------------------------------------------------------------------- def test_house_year_built_prefers_cian_bti() -> None: out = resolve_house_field( "year_built", {"avito_houses_catalog": 2020, "cian_bti": 2021, "cian_serp": 2022}, ) assert out == 2021 def test_house_unknown_field_first_non_null() -> None: assert resolve_house_field("totally_unknown", {"x": None, "y": 5}) == 5 def test_listing_owners_count_avito_domoteka_only() -> None: out = resolve_listing_field("owners_count", {"avito_domoteka": 2, "cian_serp": 99}) assert out == 2 def test_listing_photo_urls_union() -> None: out = resolve_listing_field( "photo_urls", {"avito": ["a.jpg", "b.jpg"], "cian": ["b.jpg", "c.jpg"]}, ) assert set(out) == {"a.jpg", "b.jpg", "c.jpg"} def test_listing_kadastr_first_non_null() -> None: out = resolve_listing_field("kadastr_num", {"avito": None, "cian": "66:1:1:1"}) assert out == "66:1:1:1" def test_house_priority_dict_has_year_built() -> None: assert "cian_bti" in HOUSE_FIELD_PRIORITY["year_built"] def test_listing_priority_dict_has_owners() -> None: assert "avito_domoteka" in LISTING_FIELD_PRIORITY["owners_count"] # --------------------------------------------------------------------------- # Yandex house priority tests # --------------------------------------------------------------------------- class TestYandexHousePriority: def test_house_lat_yandex_realty_nb_when_cian_missing(self) -> None: out = resolve_house_field("lat", {"yandex_realty_nb": 56.85}) assert out == 56.85 def test_house_lat_cian_preferred_over_yandex(self) -> None: out = resolve_house_field("lat", {"cian_serp": 56.83, "yandex_realty_nb": 56.85}) assert out == 56.83 def test_house_year_built_yandex_valuation_picked(self) -> None: out = resolve_house_field("year_built", {"yandex_valuation": 1981}) assert out == 1981 def test_house_year_built_cian_bti_preferred(self) -> None: out = resolve_house_field("year_built", {"cian_bti": 1980, "yandex_valuation": 1981}) assert out == 1980 def test_house_text_reviews_count_yandex_only(self) -> None: out = resolve_house_field("text_reviews_count", {"yandex_realty_nb": 353}) assert out == 353 def test_house_corpus_count_yandex_only(self) -> None: out = resolve_house_field("corpus_count", {"yandex_realty_nb": 3}) assert out == 3 def test_house_commission_year_cian_serp_preferred(self) -> None: out = resolve_house_field("commission_year", {"cian_serp": 2022, "yandex_realty_nb": 2023}) assert out == 2022 def test_house_commission_month_yandex_only(self) -> None: out = resolve_house_field("commission_month", {"yandex_realty_nb": "июнь"}) assert out == "июнь" def test_house_developer_name_cian_preferred(self) -> None: out = resolve_house_field( "developer_name", {"cian": "PRINZIP", "yandex_realty_nb": "PRINZIP недвижимость"}, ) assert out == "PRINZIP" def test_house_has_lift_cian_bti_preferred_over_yandex_valuation(self) -> None: out = resolve_house_field("has_lift", {"cian_bti": True, "yandex_valuation": True}) assert out is True def test_house_ceiling_height_cian_detail_preferred(self) -> None: out = resolve_house_field("ceiling_height", {"cian_detail": 2.7, "yandex_valuation": 2.5}) assert out == 2.7 def test_house_has_panorama_yandex_valuation_only(self) -> None: out = resolve_house_field("has_panorama", {"yandex_valuation": True}) assert out is True def test_house_yandex_total_listings_yandex_valuation_only(self) -> None: out = resolve_house_field("yandex_total_listings", {"yandex_valuation": 42}) assert out == 42 def test_house_house_class_yandex_realty_nb_fallback(self) -> None: out = resolve_house_field("house_class", {"yandex_realty_nb": "бизнес"}) assert out == "бизнес" def test_house_house_class_avito_preferred_over_yandex(self) -> None: out = resolve_house_field( "house_class", {"avito_houses_catalog": "комфорт", "yandex_realty_nb": "бизнес"}, ) assert out == "комфорт" def test_house_total_area_ha_yandex_only(self) -> None: out = resolve_house_field("total_area_ha", {"yandex_realty_nb": 12.5}) assert out == 12.5 def test_house_has_lift_yandex_valuation_fallback_when_cian_missing(self) -> None: out = resolve_house_field("has_lift", {"yandex_valuation": False}) assert out is False # --------------------------------------------------------------------------- # Yandex listing priority tests # --------------------------------------------------------------------------- class TestYandexListingPriority: def test_listing_description_cian_preferred_over_yandex_detail(self) -> None: out = resolve_listing_field( "description", {"cian_serp": "cian text", "yandex_detail": "yandex text"}, ) assert out == "cian text" def test_listing_house_type_yandex_detail_used_when_cian_avito_missing(self) -> None: out = resolve_listing_field("house_type", {"yandex_detail": "brick"}) assert out == "brick" def test_listing_agency_name_yandex_only(self) -> None: out = resolve_listing_field("agency_name", {"yandex_detail": "Агентство «Диал»"}) assert out == "Агентство «Диал»" def test_listing_agency_founded_year_yandex_only(self) -> None: out = resolve_listing_field("agency_founded_year", {"yandex_detail": 2005}) assert out == 2005 def test_listing_agency_objects_count_yandex_only(self) -> None: out = resolve_listing_field("agency_objects_count", {"yandex_detail": 120}) assert out == 120 def test_listing_views_total_yandex_only(self) -> None: out = resolve_listing_field("views_total_yandex", {"yandex_detail": 874}) assert out == 874 def test_listing_publish_date_relative_yandex_only(self) -> None: out = resolve_listing_field("publish_date_relative", {"yandex_detail": "3 дня назад"}) assert out == "3 дня назад" def test_listing_agency_name_none_when_no_yandex(self) -> None: out = resolve_listing_field("agency_name", {"cian_serp": None}) assert out is None def test_listing_description_avito_detail_preferred_over_yandex(self) -> None: out = resolve_listing_field( "description", {"avito_detail": "avito desc", "yandex_detail": "yandex desc"}, ) assert out == "avito desc" def test_listing_house_type_cian_preferred_over_yandex_detail(self) -> None: out = resolve_listing_field( "house_type", {"cian_serp": "панель", "yandex_detail": "кирпич"}, ) assert out == "панель" def test_listing_yandex_detail_keys_registered(self) -> None: """Ensure all Yandex-unique listing keys are in priority dict.""" yandex_keys = [ "agency_name", "agency_founded_year", "agency_objects_count", "views_total_yandex", "publish_date_relative", ] for key in yandex_keys: assert key in LISTING_FIELD_PRIORITY, f"{key!r} missing from LISTING_FIELD_PRIORITY" rule = LISTING_FIELD_PRIORITY[key] assert rule == ["yandex_detail"], f"{key!r} rule mismatch: {rule!r}" def test_house_yandex_keys_registered(self) -> None: """Ensure all Yandex-unique house keys are in priority dict.""" yandex_keys = [ "text_reviews_count", "corpus_count", "total_area_ha", "commission_month", "has_panorama", "yandex_total_listings", ] for key in yandex_keys: assert key in HOUSE_FIELD_PRIORITY, f"{key!r} missing from HOUSE_FIELD_PRIORITY" # --------------------------------------------------------------------------- # Freshness-based conflict resolution (#1539) — last_seen_at wins, deterministic # --------------------------------------------------------------------------- class TestFreshnessResolution: """first_non_null / fallback must prefer the freshest source, not dict order.""" def test_first_non_null_fresher_source_wins(self) -> None: # kadastr_num uses the 'first_non_null' rule. candidates = {"avito": "66:1:1:OLD", "cian": "66:1:1:NEW"} timestamps = { "avito": datetime(2026, 1, 1), "cian": datetime(2026, 6, 1), # fresher } out = resolve_listing_field("kadastr_num", candidates, timestamps) assert out == "66:1:1:NEW" def test_first_non_null_fresher_source_wins_regardless_of_order(self) -> None: # Same data, opposite insertion order — result must not flip. candidates = {"cian": "66:1:1:NEW", "avito": "66:1:1:OLD"} timestamps = { "avito": datetime(2026, 1, 1), "cian": datetime(2026, 6, 1), } out = resolve_listing_field("kadastr_num", candidates, timestamps) assert out == "66:1:1:NEW" def test_freshness_picks_avito_when_avito_is_newer(self) -> None: candidates = {"avito": "AVITO", "cian": "CIAN"} timestamps = { "avito": datetime(2026, 6, 1), # fresher "cian": datetime(2026, 1, 1), } out = resolve_listing_field("kadastr_num", candidates, timestamps) assert out == "AVITO" def test_null_only_returns_none(self) -> None: out = resolve_listing_field( "kadastr_num", {"avito": None, "cian": None}, {"avito": datetime(2026, 6, 1), "cian": datetime(2026, 1, 1)}, ) assert out is None def test_null_source_skipped_even_if_fresher(self) -> None: # cian is fresher but null → avito's non-null value wins. candidates = {"avito": "VALUE", "cian": None} timestamps = { "avito": datetime(2026, 1, 1), "cian": datetime(2026, 6, 1), } out = resolve_listing_field("kadastr_num", candidates, timestamps) assert out == "VALUE" def test_tie_breaks_by_source_name_deterministic(self) -> None: # Equal timestamps → lexicographically smallest source name wins. ts = datetime(2026, 6, 1) out1 = resolve_listing_field( "kadastr_num", {"avito": "A", "cian": "C"}, {"avito": ts, "cian": ts}, ) out2 = resolve_listing_field( "kadastr_num", {"cian": "C", "avito": "A"}, {"cian": ts, "avito": ts}, ) assert out1 == "A" # "avito" < "cian" assert out1 == out2 # order-independent def test_no_timestamps_falls_back_deterministically(self) -> None: # Without timestamps, fallback is deterministic by sorted source name. out1 = resolve_listing_field("kadastr_num", {"avito": "A", "cian": "C"}) out2 = resolve_listing_field("kadastr_num", {"cian": "C", "avito": "A"}) assert out1 == "A" assert out1 == out2 def test_partial_timestamps_timed_source_preferred(self) -> None: # Only one source carries a timestamp → it is treated as the freshest. candidates = {"avito": "A", "cian": "C"} timestamps = {"cian": datetime(2026, 6, 1)} # avito has none out = resolve_listing_field("kadastr_num", candidates, timestamps) assert out == "C" def test_house_unknown_field_freshness(self) -> None: # Unknown house field also uses freshness fallback. candidates = {"src_a": 1, "src_b": 2} timestamps = {"src_a": datetime(2026, 1, 1), "src_b": datetime(2026, 6, 1)} assert resolve_house_field("totally_unknown", candidates, timestamps) == 2 def test_priority_list_fallback_uses_freshness(self) -> None: # No ranked source present for 'lat' → freshest non-null wins. candidates = {"unranked_old": 56.80, "unranked_new": 56.90} timestamps = { "unranked_old": datetime(2026, 1, 1), "unranked_new": datetime(2026, 6, 1), } out = resolve_house_field("lat", candidates, timestamps) assert out == 56.90 def test_priority_list_ranking_beats_freshness(self) -> None: # Explicit ranking is authoritative: cian_serp ranked above yandex even if # yandex is fresher. candidates = {"cian_serp": 56.83, "yandex_realty_nb": 56.85} timestamps = { "cian_serp": datetime(2026, 1, 1), "yandex_realty_nb": datetime(2026, 6, 1), # fresher but lower-ranked } out = resolve_house_field("lat", candidates, timestamps) assert out == 56.83