test(audit): обмен и маппинг не пишут audit_event (OQ-55)

Негативное требование спеки audit-trail: ack, результат, маппинг и таймаут вахтёра не создают строк audit_event.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012vJkmBGKKqMoEMzvsXG3y2
This commit is contained in:
Andrey Epifancev 2026-10-06 00:20:07 +04:00
parent f9b8a21410
commit 24d556e41e

View File

@ -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