All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
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 1m53s
CI / backend-tests (pull_request) Successful in 17m21s
Каждое проверено против кода или прод-данных, а не переписано по впечатлению. 1. macro_coefficient:99 — «СУММА backed-весов = 0.45». С #946 inflation стал backed-каналом с весом 0.08: 0.18+0.12+0.10+0.05+0.08 = 0.53. Сумму не обновили. 2. macro_series:305 и 3. sales_series:496 — оба обещали пустой результат «при months_back < 0». Код клампит через max(0, months_back), поэтому сетка всегда содержит текущий месяц. Проверено прогоном: months_back=-5 → 1 месяц. Документировалось поведение, которого нет. 4. analytics_queries._velocity_baseline — «objective_corpus_room_month.district matches domrf_kn_objects.district_name». Неверно, и соседний _elasticity_coef описывает ту же колонку правильно (МИКРО-вокабуляр). Замер прода: district (микро) Академический, ВИЗ, Юго-Западный, Уктус, Втузгородок… district_name (админ) Академический, Чкаловский, Верх-Исетский, Ленинский… Из 8 админ-имён в микро-колонке встречаются 4, и с меньшим объёмом (Ленинский 55 точек против 621 у Академического; Чкаловский и Верх-Исетский — ноль). Вызывающий передаёт админ-имя. Резолв admin→micros тут НЕ делаю — это отдельная задача; docstring лишь перестаёт утверждать обратное. 5. nspd_denorm.denorm_dump — «Caller отвечает за commit/close», при том что функция сама вызывает db.commit() на 373. Вызывающий, понадеявшийся обернуть это в свою транзакцию, получил бы уже зафиксированные строки. 6. nspd_client.search_by_quarter — смета «6/11/22 запроса, ~3.6с/~6.6с/~13с». Фактически три из пяти core-слоёв и ВСЕ zouit/risk идут grid-walk'ом по 49 запросов: 150/395/934 запроса, ~90с/~237с/~560с. Занижение в 25-42 раза, и это не безобидно: по такой оценке слои включают не задумываясь, а объём запросов здесь — прямой фактор WAF-риска (ср. #2956, где НСПД сейчас отдаёт 403 на IP VPS). Два из шести чисел проверяемы автоматически, и на них поставлен гейт: сумма backed-весов сверяется с константами, смета запросов — с _GRID_WALK_LAYERS. Мутационно проверен: вернуть 0.45 → красный, изменить вес канала не тронув комментарий → красный, вернуть 6/11/22 → красный, контроль → 3 passed rc=0. Плюс контроль на сам гейт: если _GRID_WALK_LAYERS опустеет, расчёт совпал бы с любой мелкой цифрой тавтологически. Прогоны: tests/services — 3116 passed rc=0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
94 lines
4.7 KiB
Python
94 lines
4.7 KiB
Python
"""Числа в документации сверяются с кодом, а не живут отдельно (#2464).
|
||
|
||
Оба расхождения ниже — реальные, найденные 20.08.2026, и оба были незаметны: цифра в
|
||
комментарии не проверяется ничем, а расходится тихо при первой же правке констант.
|
||
|
||
• macro_coefficient: комментарий обещал сумму backed-весов 0.45. С #946 inflation
|
||
стал backed-каналом с весом 0.08, сумма стала 0.53 — комментарий не обновили.
|
||
|
||
• nspd_client.search_by_quarter: docstring обещал 6/11/22 запроса и ~3.6с/~6.6с/~13с.
|
||
Фактически три из пяти core-слоёв и ВСЕ zouit/risk идут grid-walk'ом по 49 запросов:
|
||
150/395/934 запроса, ~90с/~237с/~560с. Занижение в 25-42 раза — а объём запросов
|
||
здесь прямой фактор WAF-риска.
|
||
|
||
Гейт сверяет то, что НАПИСАНО, с тем, что ВЫЧИСЛЯЕТСЯ из констант. Красное здесь
|
||
означает «текст разошёлся с кодом», а не «текст непривычно отформатирован».
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
|
||
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||
|
||
import re
|
||
from pathlib import Path
|
||
|
||
|
||
def test_backed_weight_sum_in_comment_matches_constants() -> None:
|
||
"""Сумма из комментария обязана совпасть с суммой backed-констант."""
|
||
from app.services.forecasting import macro_coefficient as mc
|
||
|
||
src = Path(mc.__file__).read_text()
|
||
m = re.search(r"СУММА backed-весов = ([0-9.]+)", src)
|
||
assert m is not None, "в файле пропала строка «СУММА backed-весов = …» — гейт ослеп"
|
||
documented = float(m.group(1))
|
||
|
||
actual = round(
|
||
mc._W_RATE + mc._W_MORTG_RATE + mc._W_ISSUANCE + mc._W_OVERDUE + mc._W_INFLATION, 4
|
||
)
|
||
assert documented == actual, (
|
||
f"в комментарии сумма backed-весов {documented}, по константам {actual} — "
|
||
"текст разошёлся с кодом"
|
||
)
|
||
|
||
|
||
def _documented_request_counts(src: str) -> tuple[int, int, int]:
|
||
core = re.search(r"core only:.*?= (\d+) запрос", src)
|
||
zouit = re.search(r"\+zouit:\s*\+\d+\*\d+\s*= (\d+) запрос", src)
|
||
risks = re.search(r"\+risks:\s*\+\d+\*\d+\s*= (\d+) запрос", src)
|
||
assert core and zouit and risks, "в docstring пропала смета HTTP-запросов — гейт ослеп"
|
||
return int(core.group(1)), int(zouit.group(1)), int(risks.group(1))
|
||
|
||
|
||
def test_request_cost_in_docstring_matches_layer_dispatch() -> None:
|
||
"""Смета запросов обязана следовать из _GRID_WALK_LAYERS, а не из памяти автора."""
|
||
from app.services.scrapers import nspd_client as nc
|
||
|
||
src = Path(nc.__file__).read_text()
|
||
doc_core, doc_zouit, doc_risks = _documented_request_counts(src)
|
||
|
||
grid = set(nc._GRID_WALK_LAYERS)
|
||
core_layers = [
|
||
"parcels",
|
||
"buildings",
|
||
"territorial_zones",
|
||
"red_lines",
|
||
"engineering_structures",
|
||
]
|
||
grid_n = 7
|
||
per_grid = grid_n * grid_n
|
||
|
||
core_grid = sum(1 for c in core_layers if c in grid)
|
||
core_legacy = len(core_layers) - core_grid
|
||
n_zouit = len(nc.NSPDClient.QUARTER_ZOUIT_LAYERS)
|
||
n_risks = len(nc.NSPDClient.QUARTER_RISK_LAYERS)
|
||
|
||
exp_core = 1 + core_legacy + core_grid * per_grid
|
||
exp_zouit = exp_core + n_zouit * per_grid
|
||
exp_risks = exp_zouit + n_risks * per_grid
|
||
|
||
assert (doc_core, doc_zouit, doc_risks) == (exp_core, exp_zouit, exp_risks), (
|
||
f"docstring обещает {doc_core}/{doc_zouit}/{doc_risks} запросов, "
|
||
f"по диспетчеризации слоёв выходит {exp_core}/{exp_zouit}/{exp_risks}"
|
||
)
|
||
|
||
|
||
def test_grid_walk_membership_is_not_empty() -> None:
|
||
"""Контроль на сам гейт: если _GRID_WALK_LAYERS опустеет, расчёт станет
|
||
тавтологически совпадать с любой мелкой цифрой в docstring."""
|
||
from app.services.scrapers import nspd_client as nc
|
||
|
||
grid = set(nc._GRID_WALK_LAYERS)
|
||
assert len(grid) >= 10, f"grid-walk слоёв всего {len(grid)} — смета считается не по тому"
|
||
assert "territorial_zones" in grid, "core-слой выпал из grid-walk — пересчитать смету"
|