gendesign/tradein-mvp/backend/tests/matching/test_conflict_resolution.py
bot-backend 17d558b18c
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m27s
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / deploy (push) Successful in 2m12s
fix(tradein/yandex): снести признак «панорама» — его нет на площадке (#2851)
2026-08-12 20:44:11 +00:00

344 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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_has_no_ceiling_height_rule(self) -> None:
"""#2699: правила для houses.ceiling_height быть не должно — колонки нет.
Раньше здесь стояло `resolve_house_field("ceiling_height", ...) == 2.7`.
Тест зеленел, но проверял ФАНТОМ: колонки `ceiling_height` в таблице
`houses` не существует (прод: 0 колонок LIKE '%ceiling%'), правило не
могло сработать ни разу. Высота потолков — атрибут объявления.
"""
from app.services.matching.conflict_resolution import HOUSE_FIELD_PRIORITY
assert "ceiling_height" not in HOUSE_FIELD_PRIORITY
def test_has_panorama_removed_from_house_priority(self) -> None:
"""#2674 (хвост): правило снято вместе с колонкой houses.has_panorama (мигр. 259).
В отличие от ceiling_height выше, это правило было ИСПОЛНИМО — колонка
существовала, единственный источник её писал. Разрешать было нечего:
yandex_valuation отдавал False всегда (0 true из 1536 страниц на проде),
потому что слова «панорам» на странице оценки нет вовсе.
"""
from app.services.matching.conflict_resolution import HOUSE_FIELD_PRIORITY
assert "has_panorama" not in HOUSE_FIELD_PRIORITY
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",
"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