From 24d556e41ecfa5791b0e7d6e74323e446f2c4c3b Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Tue, 6 Oct 2026 00:20:07 +0400 Subject: [PATCH] =?UTF-8?q?test(audit):=20=D0=BE=D0=B1=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=20=D0=B8=20=D0=BC=D0=B0=D0=BF=D0=BF=D0=B8=D0=BD=D0=B3=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=B8=D1=88=D1=83=D1=82=20audit=5Fevent=20(OQ-5?= =?UTF-8?q?5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Негативное требование спеки audit-trail: ack, результат, маппинг и таймаут вахтёра не создают строк audit_event. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2 --- api/tests/test_result_mapping.py | 52 +++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/api/tests/test_result_mapping.py b/api/tests/test_result_mapping.py index 8905423..a678d3d 100644 --- a/api/tests/test_result_mapping.py +++ b/api/tests/test_result_mapping.py @@ -13,7 +13,7 @@ import json import time import uuid from collections.abc import Callable -from datetime import UTC, datetime +from datetime import UTC, datetime, timedelta from decimal import Decimal from pathlib import Path from typing import Any @@ -23,6 +23,7 @@ from loguru import logger from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker +from src.contract import ack_msg_id, result_msg_id from src.core.config import Settings, get_settings from src.models.access import AppUser from src.models.accounting import ChartOfAccount @@ -43,6 +44,7 @@ from src.models.enums import ( from src.models.files import Document, RecognitionJob, StoredFile from src.models.osv import OsvEntry from src.services import result_mapping as rm +from src.services.recognition_exchange import expire_deadlines, handle_message from src.services.result_mapping import ( MAPPING_ATTEMPTS_KEY, MAPPING_ERROR_KEY, @@ -56,6 +58,7 @@ from src.services.result_mapping import ( ) from tests.conftest import FakeStorage from tests.factories import make_document, make_legal_entity, make_result_object, make_stored_file +from tests.test_recognition_inbox import envelope, fake_msg API_DIR = Path(__file__).resolve().parents[1] MINIMAL_RESULT = API_DIR / "contracts" / "examples" / "result-osv-minimal.json" @@ -1020,3 +1023,50 @@ async def test_new_file_supersedes_several_files(committed_sessionmaker: async_s for event in events: assert event.user_id is None assert event.after_value == {"file_id": str(new_id), "version_no": expected_version} + + +# --- audit-trail: обмен и маппинг не пишут audit_event (ASSUMPTION(OQ-55)) ---------------------------------------- + + +async def _audit_count(maker: async_sessionmaker[AsyncSession]) -> int: + async with maker() as session: + return (await session.execute(select(func.count()).select_from(AuditEvent))).scalar_one() + + +async def test_exchange_and_mapping_write_no_audit_events(committed_sessionmaker: async_sessionmaker[AsyncSession]): + """spec audit-trail «События файлов и документов»: смена статусов задания и файла (ack, результат, маппинг) и + автозамена без заменяемых файлов `audit_event` не пишут: путь `UPLOADED → PROCESSING → PROCESSED` оставляет таблицу + аудита неизменной.""" + maker, storage = committed_sessionmaker, FakeStorage() + file_id, job_id, _ = await _prepare( + maker, storage, _minimal(), job_status=RecognitionStatus.published, file_status=FileStatus.UPLOADED + ) + before = await _audit_count(maker) + + await handle_message( + fake_msg(envelope(job_id, file_id, "processing"), ack_msg_id(job_id, 1)), session_factory=maker + ) + assert (await _file(maker, file_id)).status is FileStatus.PROCESSING + await handle_message(fake_msg(envelope(job_id, file_id, "done"), result_msg_id(job_id, 1)), session_factory=maker) + assert (await _job(maker, job_id)).status is RecognitionStatus.done + assert await process_once(maker, storage, _settings()) is True + + assert (await _file(maker, file_id)).status is FileStatus.PROCESSED + assert await _audit_count(maker) == before + + +async def test_watchdog_timeout_writes_no_audit_event(committed_sessionmaker: async_sessionmaker[AsyncSession]): + """Путь ошибки: вахтёр переводит задание в `timeout`, файл — в `ERROR`, `audit_event` не пишется.""" + maker, storage = committed_sessionmaker, FakeStorage() + file_id, job_id, _ = await _prepare(maker, storage, _minimal(), job_status=RecognitionStatus.processing) + async with maker() as session, session.begin(): + job = await session.get(RecognitionJob, job_id) + assert job is not None + job.reply_deadline = _now() - timedelta(minutes=1) + before = await _audit_count(maker) + + assert await expire_deadlines(maker, now=_now) >= 1 + + assert (await _file(maker, file_id)).status is FileStatus.ERROR + assert (await _job(maker, job_id)).status is RecognitionStatus.timeout + assert await _audit_count(maker) == before