test: гигиена тестов Среза 2c и нормализация email seed в одном месте

- test_api_documents: тавтология по counterparty_name строки 18 ОСВ_66 заменена
  значениями снимка (T9.3).
- test_mapping_golden: план счетов не пуст и есть строки с account_id (T8.6).
- e2e: код ответа DELETE проверяется после try/finally — не маскирует причину
  падения (T11.2).
- test_counterparty_resolve: единственное точное имя побеждает неоднозначность
  нормализованного (T6.1).
- test_domain_osv_mapping: шапка «ООО» без ИНН → UNKNOWN (T5.1).
- seed_lib/apply: email пользователя нормализуется один раз для upsert и ролей,
  поведение не меняется (T1.1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-06 00:39:40 +04:00
parent d14295b6d0
commit 75feab6015
6 changed files with 26 additions and 8 deletions

View File

@ -192,8 +192,8 @@ async def _apply_users(session: AsyncSession, data: SeedData, user_stat: TableSt
).all() ).all()
} }
user_ids: dict[str, Any] = {} user_ids: dict[str, Any] = {}
for user in data.users: users = [(user.email.strip().lower(), user) for user in data.users] # email нормализуется один раз
email = user.email.strip().lower() for email, user in users:
values = { values = {
"full_name": user.full_name, "full_name": user.full_name,
"employee_no": user.employee_no, "employee_no": user.employee_no,
@ -219,9 +219,9 @@ async def _apply_users(session: AsyncSession, data: SeedData, user_stat: TableSt
user_stat.unchanged += 1 user_stat.unchanged += 1
existing = set((await session.execute(select(UserRole.user_id, UserRole.role_id))).all()) existing = set((await session.execute(select(UserRole.user_id, UserRole.role_id))).all())
new_rows = [] new_rows = []
for user in data.users: for email, user in users:
for role_code in user.role_codes: for role_code in user.role_codes:
pair = (user_ids[user.email.strip().lower()], role_ids[role_code]) pair = (user_ids[email], role_ids[role_code])
if pair in existing: if pair in existing:
link_stat.unchanged += 1 link_stat.unchanged += 1
else: else:

View File

@ -157,7 +157,8 @@ async def test_real_osv_is_parsed_end_to_end(
root = entries["items"][0] root = entries["items"][0]
assert (root["account_code"], root["level"]) == ("66", 0) assert (root["account_code"], root["level"]) == ("66", 0)
finally: finally:
# очистка: PROCESSED без подтверждения удаляется (204); временная БД снимается фикстурой # очистка: PROCESSED без подтверждения удаляется (204); временная БД снимается фикстурой. Код ответа
# проверяется после блока: assert в finally заменил бы исключение проверки выше и скрыл причину падения
deleted = await client.delete(f"/files/{file_id}") deleted = await client.delete(f"/files/{file_id}")
assert deleted.status_code == HTTP_NO_CONTENT, deleted.text assert deleted.status_code == HTTP_NO_CONTENT, deleted.text
assert (await client.get(f"/files/{file_id}")).status_code == HTTP_NOT_FOUND assert (await client.get(f"/files/{file_id}")).status_code == HTTP_NOT_FOUND

View File

@ -192,7 +192,8 @@ async def test_entries_as_tree_on_golden_osv_66(
assert row["group_type"] == "COUNTERPARTY" assert row["group_type"] == "COUNTERPARTY"
yug = next(row for row in rows if row["row_number"] == 18) # noqa: PLR2004 yug = next(row for row in rows if row["row_number"] == 18) # noqa: PLR2004
assert yug["account_id"] is not None assert yug["account_id"] is not None
assert yug["counterparty_name"] is None or isinstance(yug["counterparty_name"], str) # значения снимка ОСВ_66 за 2024, строка 18: контрагент без ИНН в колонке
assert (yug["counterparty_name"], yug["counterparty_inn"]) == ("Юг-Финанс / ПЕКАРНЯ СЧАСТЛИВОЧНАЯ", None)
assert set(first) == { assert set(first) == {
"id", "id",
"row_number", "row_number",

View File

@ -69,6 +69,18 @@ async def test_exact_name_unique_and_ambiguous(db_session: AsyncSession) -> None
assert index.resolve(None, "ООО Дубль Имени") is None assert index.resolve(None, "ООО Дубль Имени") is None
async def test_unique_exact_name_wins_over_ambiguous_normalized(db_session: AsyncSession) -> None:
"""Приоритет шагов: единственное точное имя побеждает неоднозначность нормализованного (оба ЮЛ нормализуются в
одно имя, но точное совпадение у одного)."""
ooo = await make_legal_entity(db_session, name="ООО Альфа Приоритет")
await make_legal_entity(db_session, name="АО Альфа Приоритет")
index = await CounterpartyIndex.load(db_session)
assert index.resolve(None, "ООО Альфа Приоритет") == ooo.id
# без точного совпадения нормализованное имя неоднозначно → None
assert index.resolve(None, "Альфа Приоритет") is None
async def test_exact_name_zero_candidates_goes_to_normalized(db_session: AsyncSession) -> None: async def test_exact_name_zero_candidates_goes_to_normalized(db_session: AsyncSession) -> None:
entity = await make_legal_entity(db_session, name="ООО «Нормализуемое Имя»") entity = await make_legal_entity(db_session, name="ООО «Нормализуемое Имя»")
index = await CounterpartyIndex.load(db_session) index = await CounterpartyIndex.load(db_session)

View File

@ -252,6 +252,7 @@ def test_document_fields_on_golden_osv_66(fixtures_dir: Path) -> None:
({"organization_name": "Пекарня Счастливочная"}, DocumentCheckResult.MISMATCH), ({"organization_name": "Пекарня Счастливочная"}, DocumentCheckResult.MISMATCH),
({"organization_name": "", "organization_inn": None}, DocumentCheckResult.UNKNOWN), # пустая шапка ({"organization_name": "", "organization_inn": None}, DocumentCheckResult.UNKNOWN), # пустая шапка
({"organization_name": " ", "organization_inn": ""}, DocumentCheckResult.UNKNOWN), ({"organization_name": " ", "organization_inn": ""}, DocumentCheckResult.UNKNOWN),
({"organization_name": "ООО"}, DocumentCheckResult.UNKNOWN), # только ОПФ: нормализуется в пустое имя
], ],
) )
def test_counterparty_check(header: dict[str, Any], expected: DocumentCheckResult) -> None: def test_counterparty_check(header: dict[str, Any], expected: DocumentCheckResult) -> None:

View File

@ -101,6 +101,7 @@ async def test_all_golden_snapshots_mapped(seeded: AsyncSession, fixtures_dir: P
chart = { chart = {
code: account_id for code, account_id in await session.execute(select(ChartOfAccount.code, ChartOfAccount.id)) code: account_id for code, account_id in await session.execute(select(ChartOfAccount.code, ChartOfAccount.id))
} }
assert chart, "план счетов seed пуст — сверка account_id ничего не проверит"
index = await CounterpartyIndex.load(session) index = await CounterpartyIndex.load(session)
mapped: list[tuple[Path, uuid.UUID, RecognitionResult]] = [] mapped: list[tuple[Path, uuid.UUID, RecognitionResult]] = []
@ -121,7 +122,7 @@ async def test_all_golden_snapshots_mapped(seeded: AsyncSession, fixtures_dir: P
problems: list[str] = [] problems: list[str] = []
missing_codes: set[str] = set() missing_codes: set[str] = set()
documents_total = entries_total = 0 documents_total = entries_total = with_account = 0
for path, file_id, _ in mapped: for path, file_id, _ in mapped:
name = str(path.relative_to(fixtures_dir / GOLDEN_DIR)) name = str(path.relative_to(fixtures_dir / GOLDEN_DIR))
raw = json.loads(path.read_text("utf-8")) raw = json.loads(path.read_text("utf-8"))
@ -148,6 +149,7 @@ async def test_all_golden_snapshots_mapped(seeded: AsyncSession, fixtures_dir: P
missing_codes |= sheet_missing missing_codes |= sheet_missing
documents_total += 1 documents_total += 1
entries_total += len(entries) entries_total += len(entries)
with_account += sum(1 for entry in entries if entry.account_id is not None)
file_status = (await session.execute(select(StoredFile.status).where(StoredFile.id == file_id))).scalar_one() file_status = (await session.execute(select(StoredFile.status).where(StoredFile.id == file_id))).scalar_one()
if file_status not in {FileStatus.PROCESSED, FileStatus.SUPERSEDED}: if file_status not in {FileStatus.PROCESSED, FileStatus.SUPERSEDED}:
problems.append(f"{name}: статус файла {file_status}") problems.append(f"{name}: статус файла {file_status}")
@ -158,4 +160,5 @@ async def test_all_golden_snapshots_mapped(seeded: AsyncSession, fixtures_dir: P
f"{sorted(missing_codes)}" f"{sorted(missing_codes)}"
) )
assert problems == [] assert problems == []
assert with_account > 0 # строки со счётом плана счетов есть — сверка account_id не вырождена
assert mapping_seconds <= TIME_BUDGET_SECONDS assert mapping_seconds <= TIME_BUDGET_SECONDS