test(statements): помощники golden — ocr_result, все документы файла в map_golden_snapshot

Добавлены:
- Константы GOLDEN_STATEMENTS и OCR_EXPECTED в fixtures_helpers
- Функция ocr_result для создания RecognitionResult из эталона нормализатора
- Тест test_ocr_result_wraps_normalizer_golden, проверяющий порядок документов

Изменены:
- map_golden_snapshot теперь принимает параметр result_builder для переопределения
  функции создания RecognitionResult (по умолчанию golden_result)
- Документы сортируются по (document_type, osv_account_code) вместо только
  osv_account_code

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-07 11:24:40 +04:00
parent 3a72702c3d
commit 29f516db3c
2 changed files with 61 additions and 8 deletions

View File

@ -3,6 +3,7 @@
import json import json
import os import os
import uuid import uuid
from collections.abc import Callable
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -16,6 +17,8 @@ if TYPE_CHECKING:
from src.models.files import Document from src.models.files import Document
FIXTURES_ENV = "ARM_KA_FIXTURES" FIXTURES_ENV = "ARM_KA_FIXTURES"
GOLDEN_STATEMENTS = Path("golden") / "expected_statement_parse"
OCR_EXPECTED = Path("ocr") / "expected_normalize"
def resolve_fixtures_dir() -> Path: def resolve_fixtures_dir() -> Path:
@ -40,6 +43,28 @@ def fixtures_skip_reason() -> str | None:
) )
def ocr_result(path: Path, *, job_id: uuid.UUID, file_id: uuid.UUID) -> "RecognitionResult":
"""Эталон нормализатора scan-gateway (`ocr/expected_normalize/<name>.json`)
как `RecognitionResult` контракта v1.2.
"""
from src.contract.result import RecognitionResult # noqa: PLC0415 - цикл с conftest
data = json.loads(path.read_text("utf-8"))
payload: dict[str, object] = {
"schema_version": "1.2",
"job_id": str(job_id),
"file_id": str(file_id),
"service": {"name": "scan-gateway", "version": "golden"},
"status": data["status"],
"documents": data["documents"],
}
if data.get("error"):
payload["error"] = data["error"]
if data.get("metrics"):
payload["metrics"] = data["metrics"]
return RecognitionResult.model_validate(payload)
def golden_result(path: Path, *, job_id: uuid.UUID, file_id: uuid.UUID) -> "RecognitionResult": def golden_result(path: Path, *, job_id: uuid.UUID, file_id: uuid.UUID) -> "RecognitionResult":
"""Golden-снимок парсера как `RecognitionResult`: подставляет `job_id`/`file_id` и версию сервиса.""" """Golden-снимок парсера как `RecognitionResult`: подставляет `job_id`/`file_id` и версию сервиса."""
from src.contract.result import RecognitionResult # noqa: PLC0415 - тяжёлые импорты лениво: цикл с conftest from src.contract.result import RecognitionResult # noqa: PLC0415 - тяжёлые импорты лениво: цикл с conftest
@ -50,13 +75,19 @@ def golden_result(path: Path, *, job_id: uuid.UUID, file_id: uuid.UUID) -> "Reco
async def map_golden_snapshot( async def map_golden_snapshot(
session: AsyncSession, *, entity: "LegalEntity", path: Path, uploaded_by: uuid.UUID | None session: AsyncSession,
*,
entity: "LegalEntity",
path: Path,
uploaded_by: uuid.UUID | None,
result_builder: Callable[..., "RecognitionResult"] | None = None,
) -> "list[Document]": ) -> "list[Document]":
"""Golden-снимок `path` → файл ЮЛ `entity` и его документы ОСВ в БД теста (`flush`, без commit). """Golden-снимок `path` → файл ЮЛ `entity` и его документы в БД теста (`flush`, без commit).
Как маппер: файл `PROCESSING` с текущим заданием `done`, затем боевой `map_file` с планом счетов seed и настоящим Как маппер: файл `PROCESSING` с текущим заданием `done`, затем боевой `map_file` с планом счетов seed и
`CounterpartyIndex` (справочник читается на момент вызова — ЮЛ и алиасы должны быть созданы до маппинга). настоящим `CounterpartyIndex` (справочник читается на момент вызова — ЮЛ и алиасы должны быть созданы
Возвращает документы файла (после автозамены прежних версий — только документы этого файла). до маппинга). Возвращает документы файла отсортированные по (document_type, osv_account_code).
`result_builder` — функция создания RecognitionResult (по умолчанию golden_result).
""" """
# ленивые импорты: `conftest` импортирует этот модуль раньше `tests.factories` (цикл) # ленивые импорты: `conftest` импортирует этот модуль раньше `tests.factories` (цикл)
from src.models.accounting import ChartOfAccount # noqa: PLC0415 from src.models.accounting import ChartOfAccount # noqa: PLC0415
@ -78,9 +109,12 @@ async def map_golden_snapshot(
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))
} }
index = await CounterpartyIndex.load(session) index = await CounterpartyIndex.load(session)
result = golden_result(path, job_id=job.id, file_id=file.id) builder = result_builder or golden_result
result = builder(path, job_id=job.id, file_id=file.id)
await map_file(session, job=job, file=file, result=result, chart=chart, index=index, now=datetime.now(UTC)) await map_file(session, job=job, file=file, result=result, chart=chart, index=index, now=datetime.now(UTC))
documents = await session.execute( documents = await session.execute(
select(Document).where(Document.stored_file_id == file.id).order_by(Document.osv_account_code) select(Document)
.where(Document.stored_file_id == file.id)
.order_by(Document.document_type, Document.osv_account_code)
) )
return list(documents.scalars()) return list(documents.scalars())

View File

@ -1,10 +1,17 @@
"""Маркер `fixtures`: путь к реальным данным и пропуск с причиной (spec quality-checks, «Фикстуры … опциональны»).""" """Маркер `fixtures`: путь к реальным данным и пропуск с причиной (spec quality-checks, «Фикстуры … опциональны»)."""
import uuid
from pathlib import Path from pathlib import Path
import pytest import pytest
from tests.fixtures_helpers import FIXTURES_ENV, fixtures_skip_reason, resolve_fixtures_dir from tests.fixtures_helpers import (
FIXTURES_ENV,
OCR_EXPECTED,
fixtures_skip_reason,
ocr_result,
resolve_fixtures_dir,
)
@pytest.mark.fixtures @pytest.mark.fixtures
@ -34,3 +41,15 @@ def test_skip_reason_names_variable_and_path(monkeypatch: pytest.MonkeyPatch, tm
assert reason is not None assert reason is not None
assert "ARM_KA_FIXTURES недоступен" in reason assert "ARM_KA_FIXTURES недоступен" in reason
assert str(missing) in reason assert str(missing) in reason
@pytest.mark.fixtures
def test_ocr_result_wraps_normalizer_golden(fixtures_dir: Path) -> None:
"""ocr_result читает эталон нормализатора и создаёт RecognitionResult
с правильным порядком документов.
"""
job_id, file_id = uuid.uuid4(), uuid.uuid4()
result = ocr_result(fixtures_dir / OCR_EXPECTED / "aep_6.json", job_id=job_id, file_id=file_id)
assert result.job_id == job_id and result.file_id == file_id
assert result.service.name == "scan-gateway"
assert [doc.document_type for doc in result.documents] == ["BALANCE", "INCOME"]