diff --git a/backend/tests/services/generative/test_program_placement.py b/backend/tests/services/generative/test_program_placement.py index 3d928ee0..22bfd42d 100644 --- a/backend/tests/services/generative/test_program_placement.py +++ b/backend/tests/services/generative/test_program_placement.py @@ -51,31 +51,58 @@ def _payload(coords: list[list[float]], **overrides: object) -> ConceptInput: return ConceptInput(**base) # type: ignore[arg-type] -# ── (a) backward-compat: building_program=None reproduces greedy output ───────────── +# ── (a) backward-compat: greedy output is byte-identical after the _Placer refactor ── + +# GOLDEN PIN — детерминированный жадный выход на фиксированном участке _BIG_PARCEL +# (comfort / 9 эт. / mid_rise). Захвачен ПОСЛЕ извлечения _Placer и сверен ревьювером +# как идентичный до-рефактора. Любой будущий ДРЕЙФ жадной геометрии (число секций / +# пятно / GFA / квартиры) уронит этот тест — это и есть anti-regression guard, которого +# не давали инвариантные тесты в test_placement.py. Формат на стратегию: +# (features, built_area_sqm, total_floor_area_sqm, apartments_count). +_GREEDY_GOLDEN: dict[str, tuple[int, float, float, int]] = { + "max_area": (83, 35856.0, 358560.0, 4830), + "max_insolation": (70, 15120.0, 120960.0, 1629), + "balanced": (88, 27720.0, 249480.0, 3361), +} -def test_no_program_reproduces_greedy_output_unchanged() -> None: - # Тот же участок: с building_program=None раскладка ДОЛЖНА совпасть с жадной 1b - # (три стратегии, те же ТЭП/число секций) — program-ветка не трогает greedy-путь. +def test_greedy_output_matches_golden_pin() -> None: + # Реальный golden-pin: жадный путь (building_program не задан) должен выдать РОВНО + # замороженные литералы. Так фиксируем, что _Placer-рефактор не сдвинул раскладку. + payload = _payload(_BIG_PARCEL) + parcel = geometry.parse_parcel(payload) + variants = {v.strategy: v for v in placement.place_all_strategies(parcel, payload)} + assert set(variants) == set(_GREEDY_GOLDEN) + for name, (features, built, gfa, apts) in _GREEDY_GOLDEN.items(): + v = variants[name] + assert len(v.buildings_geojson["features"]) == features, name + assert round(v.teap.built_area_sqm, 1) == built, name + assert round(v.teap.total_floor_area_sqm, 1) == gfa, name + assert v.teap.apartments_count == apts, name + # Greedy-режим не выставляет partial-fit сигнал (остаётся None). + assert v.placed_count is None + assert v.requested_count is None + + +def test_explicit_none_program_equals_default_greedy() -> None: + # building_program=None (явно) и дефолт (поле опущено) идут одним greedy-путём → + # должны совпасть покомпонентно. Дополняет golden-pin: ветка None не разветвляется. payload_default = _payload(_BIG_PARCEL) payload_none = _payload(_BIG_PARCEL, building_program=None) - - parcel = geometry.parse_parcel(payload_default) - greedy = placement.place_all_strategies(parcel, payload_default) - none_branch = placement.place_all_strategies(geometry.parse_parcel(payload_none), payload_none) - - assert {v.strategy for v in greedy} == {"max_area", "max_insolation", "balanced"} - assert {v.strategy for v in none_branch} == {"max_area", "max_insolation", "balanced"} - by_strategy_g = {v.strategy: v for v in greedy} - by_strategy_n = {v.strategy: v for v in none_branch} - for name, gv in by_strategy_g.items(): - nv = by_strategy_n[name] - assert gv.teap == nv.teap - assert gv.financial == nv.financial - assert len(gv.buildings_geojson["features"]) == len(nv.buildings_geojson["features"]) - # Greedy-режим не выставляет partial-fit сигнал (остаётся None). - assert gv.placed_count is None - assert gv.requested_count is None + variants_default = placement.place_all_strategies( + geometry.parse_parcel(payload_default), payload_default + ) + variants_none = placement.place_all_strategies( + geometry.parse_parcel(payload_none), payload_none + ) + by_default = {v.strategy: v for v in variants_default} + by_none = {v.strategy: v for v in variants_none} + assert set(by_default) == set(by_none) + for name, dv in by_default.items(): + nv = by_none[name] + assert dv.teap == nv.teap + assert dv.financial == nv.financial + assert len(dv.buildings_geojson["features"]) == len(nv.buildings_geojson["features"]) # ── (b) 2-item program places EXACTLY the requested sections; TEAP reflects them ─────