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:
parent
d14295b6d0
commit
75feab6015
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user