fix(ptica): число ЗОУИТ перестаёт подписываться как число типов (#2464)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m21s
CI / backend-tests (pull_request) Successful in 17m16s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m21s
CI / backend-tests (pull_request) Successful in 17m16s
encumbrance.zouit_count приходит из parcels.py как len(zouit_rows) — число ЗАПИСЕЙ cad_zouit, пересёкших участок. Типы лежат отдельно, в zouit_types, и показаны строкой ниже. Оба экспортёра подписывали это число как «Кол-во типов ЗОУИТ». Расхождение не редкое — померил по реальным отчётам (analysis_runs): разборов с ЗОУИТ 1637 где записей != типов 717 (43.8%) среднее отношение 1.35x максимум по проду 7 записей при 3 типах То есть почти в половине отчётов с ЗОУИТ читателю показывали число, которое на треть больше того, что обещает подпись. Пункт эпика предлагал обратное — заменить значение на len(zouit_types). Это сделало бы фолбэк несогласованным с основным путём: у источника счётчик считает записи, и таким он приходит в чат-контекст (chat/retrieval.py) тоже. Врёт подпись, её и правлю. Тест формулирует инвариант по смыслу, а не сверкой с выбранной строкой: ЕСЛИ подпись обещает типы, показанное число обязано равняться числу типов. Такая формулировка переживёт разумное переименование и не даст «починить» тест подгонкой подписи. Проверяются оба формата — HTML и DOCX. Против origin/main (5 записей, 2 типа): html: подпись «Кол-во типов ЗОУИТ» показывает 5, типов 2 → падает docx: то же самое → падает число записей всё ещё в сводке — контроль, зелёный с обеих сторон перечисление типов на месте — контроль, зелёный с обеих сторон Контроли не для симметрии: первый ловит «починку», которая выкинула бы счётчик вместо переименования, второй — потерю перечисления типов. Прогоны: tests/services/exporters + tests/services/chat — 302 passed rc=0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
07b5e5e6da
commit
be446482d0
3 changed files with 130 additions and 2 deletions
|
|
@ -298,7 +298,13 @@ def _build_zouit(doc: _DocxDocument, result: dict[str, Any]) -> None:
|
|||
|
||||
summary_pairs: list[tuple[str, Any]] = [
|
||||
("Есть ЗОУИТ", has_zouit),
|
||||
("Кол-во типов ЗОУИТ", zouit_count),
|
||||
# Подпись именно «Кол-во ЗОУИТ», а не «типов»: значение приходит из
|
||||
# encumbrance.zouit_count, а там `len(zouit_rows)` — число ЗАПИСЕЙ cad_zouit,
|
||||
# пересёкших участок (parcels.py). Типы лежат отдельно, в zouit_types, и
|
||||
# показаны строкой ниже. Прежняя подпись «Кол-во типов ЗОУИТ» расходилась со
|
||||
# значением в 717 разборах из 1637 с ЗОУИТ — 43.8%, в среднем завышая «типы»
|
||||
# в 1.35 раза (#2464).
|
||||
("Кол-во ЗОУИТ", zouit_count),
|
||||
]
|
||||
if zouit_types:
|
||||
summary_pairs.append(("Типы", ", ".join(str(t) for t in zouit_types)))
|
||||
|
|
|
|||
|
|
@ -538,7 +538,13 @@ def _build_zouit(result: dict[str, Any]) -> str:
|
|||
|
||||
summary_pairs: list[tuple[str, Any]] = [
|
||||
("Есть ЗОУИТ", has_zouit),
|
||||
("Кол-во типов ЗОУИТ", zouit_count),
|
||||
# Подпись именно «Кол-во ЗОУИТ», а не «типов»: значение приходит из
|
||||
# encumbrance.zouit_count, а там `len(zouit_rows)` — число ЗАПИСЕЙ cad_zouit,
|
||||
# пересёкших участок (parcels.py). Типы лежат отдельно, в zouit_types, и
|
||||
# показаны строкой ниже. Прежняя подпись «Кол-во типов ЗОУИТ» расходилась со
|
||||
# значением в 717 разборах из 1637 с ЗОУИТ — 43.8%, в среднем завышая «типы»
|
||||
# в 1.35 раза (#2464).
|
||||
("Кол-во ЗОУИТ", zouit_count),
|
||||
]
|
||||
if zouit_types:
|
||||
summary_pairs.append(("Типы", ", ".join(str(t) for t in zouit_types)))
|
||||
|
|
|
|||
116
backend/tests/services/exporters/test_2464_zouit_count_label.py
Normal file
116
backend/tests/services/exporters/test_2464_zouit_count_label.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""Подпись к числу ЗОУИТ не должна обещать «типы», когда там записи (#2464).
|
||||
|
||||
`encumbrance.zouit_count` приходит из `parcels.py` как `len(zouit_rows)` — число ЗАПИСЕЙ
|
||||
`cad_zouit`, пересёкших участок. Типы лежат отдельно, в `zouit_types`. Оба экспортёра
|
||||
подписывали это число как «Кол-во типов ЗОУИТ».
|
||||
|
||||
Расхождение не редкое: из 1637 разборов с ЗОУИТ в 717 (43.8 %) число записей не равно
|
||||
числу типов, в среднем завышая «типы» в 1.35 раза (максимум по проду — 7 записей при
|
||||
3 типах).
|
||||
|
||||
Инвариант проверяется по смыслу, а не сверкой с выбранной строкой: **если подпись обещает
|
||||
типы, показанное число обязано равняться числу типов**. Такая формулировка переживёт любое
|
||||
разумное переименование и не даст «починить» тест подгонкой подписи.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
# Пять записей ЗОУИТ, но всего два различных типа — ровно тот случай, где подпись врала.
|
||||
_OVERLAPS: list[dict[str, Any]] = [
|
||||
{"type_zone": "Охранная зона ЛЭП", "name": "ВЛ-110 №1", "coverage_pct": 12.0},
|
||||
{"type_zone": "Охранная зона ЛЭП", "name": "ВЛ-110 №2", "coverage_pct": 8.0},
|
||||
{"type_zone": "Охранная зона ЛЭП", "name": "ВЛ-110 №3", "coverage_pct": 3.0},
|
||||
{"type_zone": "Приаэродромная территория", "name": "ПАТ подзона 3", "coverage_pct": 100.0},
|
||||
{"type_zone": "Приаэродромная территория", "name": "ПАТ подзона 4", "coverage_pct": 100.0},
|
||||
]
|
||||
_RESULT: dict[str, Any] = {
|
||||
"encumbrance": {
|
||||
"has_zouit": True,
|
||||
"zouit_count": 5,
|
||||
"zouit_types": ["Охранная зона ЛЭП", "Приаэродромная территория"],
|
||||
},
|
||||
"nspd_zouit_overlaps": _OVERLAPS,
|
||||
}
|
||||
|
||||
_RECORDS = 5
|
||||
_TYPES = 2
|
||||
|
||||
|
||||
def _html_pairs() -> list[tuple[str, str]]:
|
||||
"""KV-пары из HTML-блока ЗОУИТ: [(подпись, значение), …]."""
|
||||
from app.services.exporters.full_report_html import _build_zouit
|
||||
|
||||
out = _build_zouit(dict(_RESULT))
|
||||
return [
|
||||
(re.sub(r"<[^>]+>", "", k).strip(), re.sub(r"<[^>]+>", "", v).strip())
|
||||
for k, v in re.findall(r"<tr>\s*<th>(.*?)</th>\s*<td>(.*?)</td>\s*</tr>", out, re.S)
|
||||
] or [
|
||||
(re.sub(r"<[^>]+>", "", k).strip(), re.sub(r"<[^>]+>", "", v).strip())
|
||||
for k, v in re.findall(
|
||||
r"<tr>\s*<td[^>]*>(.*?)</td>\s*<td[^>]*>(.*?)</td>\s*</tr>", out, re.S
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def _docx_pairs() -> list[tuple[str, str]]:
|
||||
"""KV-пары из DOCX-блока ЗОУИТ."""
|
||||
from docx import Document
|
||||
|
||||
from app.services.exporters.full_report_docx import _build_zouit
|
||||
|
||||
doc = Document()
|
||||
_build_zouit(doc, dict(_RESULT))
|
||||
pairs: list[tuple[str, str]] = []
|
||||
for table in doc.tables:
|
||||
for row in table.rows:
|
||||
cells = [c.text.strip() for c in row.cells]
|
||||
if len(cells) == 2:
|
||||
pairs.append((cells[0], cells[1]))
|
||||
return pairs
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pairs_fn,fmt", [(_html_pairs, "html"), (_docx_pairs, "docx")])
|
||||
def test_label_promising_types_must_show_types(pairs_fn: Any, fmt: str) -> None:
|
||||
"""Подпись со словом «тип» обязана нести число типов, а не записей."""
|
||||
pairs = pairs_fn()
|
||||
assert pairs, f"{fmt}: не разобрал ни одной KV-пары — тест ослеп"
|
||||
|
||||
for label, value in pairs:
|
||||
if "тип" not in label.lower():
|
||||
continue
|
||||
digits = re.fullmatch(r"\d+", value.strip())
|
||||
if not digits:
|
||||
continue # строка «Типы: A, B» — это перечисление, а не счётчик
|
||||
assert int(value) == _TYPES, (
|
||||
f"{fmt}: подпись {label!r} обещает типы, а показывает {value} — "
|
||||
f"это число ЗАПИСЕЙ ({_RECORDS}), типов же {_TYPES}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pairs_fn,fmt", [(_html_pairs, "html"), (_docx_pairs, "docx")])
|
||||
def test_record_count_is_still_shown(pairs_fn: Any, fmt: str) -> None:
|
||||
"""Контроль: само число записей из отчёта не пропало — подпись правится, не значение."""
|
||||
pairs = pairs_fn()
|
||||
counters = [(k, v) for k, v in pairs if re.fullmatch(r"\d+", v.strip())]
|
||||
assert any(
|
||||
int(v) == _RECORDS for _, v in counters
|
||||
), f"{fmt}: число ЗОУИТ-записей ({_RECORDS}) исчезло из сводки: {counters}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pairs_fn,fmt", [(_html_pairs, "html"), (_docx_pairs, "docx")])
|
||||
def test_types_are_still_listed(pairs_fn: Any, fmt: str) -> None:
|
||||
"""Контроль: перечисление типов на месте — читатель по-прежнему видит, какие они."""
|
||||
pairs = pairs_fn()
|
||||
joined = " ".join(f"{k} {v}" for k, v in pairs)
|
||||
assert (
|
||||
"Охранная зона ЛЭП" in joined and "Приаэродромная территория" in joined
|
||||
), f"{fmt}: типы ЗОУИТ пропали из сводки"
|
||||
Loading…
Add table
Reference in a new issue