gendesign/tradein-mvp/backend/tests/test_estimator_price_trend_dedup.py
bot-backend d5df5fdb9c
All checks were successful
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 53s
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 8s
tech-debt(tradein/estimator): collapse won estimate_* flags into defaults (#1970)
Collapse 14 Class-A boolean feature gates (all default True, never overridden in
prod) into their unconditional ON behavior: inline the ON-path, drop the guard +
any OFF branch, delete the config field, and remove/adjust OFF-path tests.

Collapsed (14):
  estimate_imv_blend_enabled, estimate_same_building_anchor_enabled,
  estimate_calibrated_pi_enabled, estimate_corridor_clamp_enabled,
  estimate_radius_floor_enabled, estimate_sb_low_conf_gate_enabled,
  estimate_price_trend_dedup_enabled, estimate_confidence_floor_no_analogs,
  estimate_manual_review_enabled, estimate_radius_dedup_enabled,
  estimate_wide_corridor_disclosure_enabled, estimate_sb_clip_after_weight,
  estimate_quarter_index_enabled,
  estimate_sb_tier_a_allow_primary_if_secondary_present

Kept as flags (2 of the requested set) — they are the OFF-baseline toggle for the
dedicated A/B magnitude suite test_estimator_hedonic.py and isolation helpers
across ~6 other estimator test files; collapsing them would force rewriting numeric
assertions everywhere (risk of a wrong-number regression):
  estimate_hedonic_correction_enabled, estimate_expected_sold_le_asking

Untouched per task: estimate_dedup_analogs_enabled (gate depends on its False
branch), estimate_kitchen_area_signal_enabled, estimate_ceiling_height_signal_enabled,
estimate_is_apartments_filter_enabled, estimate_segment_multiplier_enabled.

Also removed the now-unconditional `enabled` param from _apply_corridor_clamp and
collapsed the same-building-anchor pre-fetch gate in scripts/backtest_estimator.py.

Regression gate (test_backtest_regression_gate.py) stays byte-green: all collapsed
flags defaulted True, so unconditional == prior prod behavior; the frozen 277-DKP
replay is byte-identical to the committed baseline. Full run: 458 passed (gate +
estimator suite) + 93 passed (same_building / 781 / tier_a / segment_guard).
2026-07-12 15:42:02 +03:00

102 lines
3.8 KiB
Python
Raw 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.

"""Mera-audit fix-3: cross-source dedup в _fetch_price_trend.
Проверяет что один объект на avito_imv + yandex_valuation с разными ext_item_id
не double-count'ится в месячной медиане (dedup всегда включён).
"""
from __future__ import annotations
import os
from typing import Any
from unittest.mock import MagicMock, patch
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
import pytest
from app.services.estimator import _fetch_price_trend
def _make_mock_db(source1_rows: list[dict], source2_rows: list[dict]) -> MagicMock:
"""Возвращает mock db: Source1 (houses_price_dynamics) → source1_rows,
Source2 (house_placement_history) → source2_rows."""
mock_db = MagicMock()
def side_effect(*args: Any, **kwargs: Any) -> MagicMock:
call_text = str(args[0].text) if args else ""
result = MagicMock()
if "houses_price_dynamics" in call_text:
result.mappings.return_value.all.return_value = source1_rows
else:
result.mappings.return_value.all.return_value = source2_rows
return result
mock_db.execute.side_effect = side_effect
return mock_db
# ---------------------------------------------------------------------------
# Fix-3: dedup enabled (default True)
# ---------------------------------------------------------------------------
def test_price_trend_dedup_sql_uses_distinct_on() -> None:
"""SQL-запрос к house_placement_history всегда содержит DISTINCT ON (dedup)."""
mock_db = _make_mock_db([], [{"month": "2026-01", "ppm2": 150_000}])
with patch("app.services.estimator.settings") as mock_settings:
mock_settings.estimate_price_trend_max_age_months = 6
_fetch_price_trend(mock_db, target_house_id=42, min_points=1)
calls = mock_db.execute.call_args_list
# Второй вызов — Source 2 (house_placement_history)
assert len(calls) >= 2
second_sql = str(calls[1].args[0].text) if calls[1].args else ""
assert "DISTINCT ON" in second_sql.upper() or "distinct on" in second_sql.lower()
def test_price_trend_returns_result_with_dedup() -> None:
"""Функция корректно возвращает точки тренда (dedup включён)."""
rows = [
{"month": "2025-11", "ppm2": 148_000},
{"month": "2025-12", "ppm2": 152_000},
{"month": "2026-01", "ppm2": 155_000},
]
mock_db = _make_mock_db([], rows)
with patch("app.services.estimator.settings") as mock_settings:
mock_settings.estimate_price_trend_max_age_months = 6
result = _fetch_price_trend(mock_db, target_house_id=42, min_points=3)
assert result is not None
assert len(result) == 3
assert result[0]["month"] == "2025-11"
assert result[0]["ppm2"] == 148_000
def test_price_trend_source1_preferred_over_dedup_path() -> None:
"""Если Source1 (houses_price_dynamics) вернул ≥ min_points → Source2 не вызывается."""
source1_rows = [
{"month": "2025-11", "ppm2": 140_000},
{"month": "2025-12", "ppm2": 145_000},
{"month": "2026-01", "ppm2": 150_000},
]
mock_db = _make_mock_db(source1_rows, [])
with patch("app.services.estimator.settings") as mock_settings:
mock_settings.estimate_price_trend_max_age_months = 6
result = _fetch_price_trend(mock_db, target_house_id=42, min_points=3)
# Source1 вернул 3 точки — Source2 не должен вызываться
assert result is not None
assert len(result) == 3
calls = mock_db.execute.call_args_list
# Ровно один вызов (Source1)
assert len(calls) == 1
if __name__ == "__main__": # pragma: no cover
raise SystemExit(pytest.main([__file__, "-q"]))