diff --git a/api/tests/fixtures_helpers.py b/api/tests/fixtures_helpers.py index 2987c65..62d0d1d 100644 --- a/api/tests/fixtures_helpers.py +++ b/api/tests/fixtures_helpers.py @@ -3,6 +3,7 @@ import json import os import uuid +from collections.abc import Callable from datetime import UTC, datetime from pathlib import Path from typing import TYPE_CHECKING @@ -16,6 +17,8 @@ if TYPE_CHECKING: from src.models.files import Document FIXTURES_ENV = "ARM_KA_FIXTURES" +GOLDEN_STATEMENTS = Path("golden") / "expected_statement_parse" +OCR_EXPECTED = Path("ocr") / "expected_normalize" 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/.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": """Golden-снимок парсера как `RecognitionResult`: подставляет `job_id`/`file_id` и версию сервиса.""" 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( - 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]": - """Golden-снимок `path` → файл ЮЛ `entity` и его документы ОСВ в БД теста (`flush`, без commit). + """Golden-снимок `path` → файл ЮЛ `entity` и его документы в БД теста (`flush`, без commit). - Как маппер: файл `PROCESSING` с текущим заданием `done`, затем боевой `map_file` с планом счетов seed и настоящим - `CounterpartyIndex` (справочник читается на момент вызова — ЮЛ и алиасы должны быть созданы до маппинга). - Возвращает документы файла (после автозамены прежних версий — только документы этого файла). + Как маппер: файл `PROCESSING` с текущим заданием `done`, затем боевой `map_file` с планом счетов seed и + настоящим `CounterpartyIndex` (справочник читается на момент вызова — ЮЛ и алиасы должны быть созданы + до маппинга). Возвращает документы файла отсортированные по (document_type, osv_account_code). + `result_builder` — функция создания RecognitionResult (по умолчанию golden_result). """ # ленивые импорты: `conftest` импортирует этот модуль раньше `tests.factories` (цикл) 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)) } 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)) 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()) diff --git a/api/tests/test_fixtures_marker.py b/api/tests/test_fixtures_marker.py index 6206e97..88b03a4 100644 --- a/api/tests/test_fixtures_marker.py +++ b/api/tests/test_fixtures_marker.py @@ -1,10 +1,17 @@ """Маркер `fixtures`: путь к реальным данным и пропуск с причиной (spec quality-checks, «Фикстуры … опциональны»).""" +import uuid from pathlib import Path 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 @@ -34,3 +41,15 @@ def test_skip_reason_names_variable_and_path(monkeypatch: pytest.MonkeyPatch, tm assert reason is not None assert "ARM_KA_FIXTURES недоступен" 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"]