Срез 0: скелет ядра — ORM и миграция 0001 по DBML v5.13, seed, auth с dev-эмитентом, обмен через recognition_job #3

Merged
aep merged 97 commits from feat/1-slice-0-skeleton into main 2026-10-04 18:40:48 +03:00
2 changed files with 45 additions and 3 deletions
Showing only changes of commit 7a6a7fb36b - Show all commits

View File

@ -1,5 +1,6 @@
import os
from pydantic import NonNegativeInt, PositiveInt
from pydantic_settings import BaseSettings, SettingsConfigDict
@ -68,9 +69,12 @@ class Settings(BaseSettings):
# NATS и outbox/inbox обмена recognition_job
NATS_ENABLED: bool = False
OUTBOX_POLL_SECONDS: int = 5 # период опроса outbox
ACK_TIMEOUT_SECONDS: int = 30 # ожидание ack сервиса распознавания до повторной публикации
OUTBOX_MAX_REPUBLISH: int = 3 # число повторных публикаций до перевода в dlq
# 0 и отрицательные значения отвергаются при старте: период 0 — горячий цикл, ack-таймаут 0 — повтор каждый проход
OUTBOX_POLL_SECONDS: PositiveInt = 5 # период опроса outbox
ACK_TIMEOUT_SECONDS: PositiveInt = 30 # ожидание ack сервиса распознавания до повторной публикации
OUTBOX_MAX_REPUBLISH: NonNegativeInt = (
3 # число повторных публикаций до перевода в dlq (0 — сразу dlq без повторов)
)
model_config = SettingsConfigDict(
extra="ignore",

View File

@ -4,6 +4,7 @@ import os
from pathlib import Path
import pytest
from pydantic import ValidationError
from src.core.config import Settings
@ -67,3 +68,40 @@ def test_empty_nats_seed_and_jwt_allowed(clean_env: pytest.MonkeyPatch) -> None:
assert s.OPENBAO.NATS.NATS_JWT == ""
assert s.NATS_ENABLED is True
assert s.OPENBAO.SMB.SMB_HOST == ""
REQUIRED_ENV = {
"OPENBAO__DATABASE__DATABASE_URL": "postgresql+asyncpg://u:p@localhost:5432/d",
"OPENBAO__MINIO__MINIO_URL": "localhost:9000",
"OPENBAO__MINIO__MINIO_ADMIN_LOGIN": "login",
"OPENBAO__MINIO__MINIO_ADMIN_PASSWORD": "password",
"OPENBAO__MINIO__MINIO_BUCKET": "bucket",
"OPENBAO__NATS__NATS_URL": "nats://localhost:4222",
}
@pytest.mark.parametrize(
("key", "value"),
[
("OUTBOX_POLL_SECONDS", "0"),
("OUTBOX_POLL_SECONDS", "-1"),
("ACK_TIMEOUT_SECONDS", "0"),
("ACK_TIMEOUT_SECONDS", "-5"),
("OUTBOX_MAX_REPUBLISH", "-1"),
],
)
def test_exchange_settings_reject_invalid(clean_env: pytest.MonkeyPatch, key: str, value: str) -> None:
"""Период опроса и ack-таймаут — строго положительные, число повторов — неотрицательное (финальное ревью M-4)."""
for name, required in REQUIRED_ENV.items():
clean_env.setenv(name, required)
clean_env.setenv(key, value)
with pytest.raises(ValidationError, match=key):
Settings(_env_file=None) # type: ignore[call-arg]
def test_zero_republish_allowed(clean_env: pytest.MonkeyPatch) -> None:
"""OUTBOX_MAX_REPUBLISH=0 допустим: без повторов, сразу dlq после ack-таймаута."""
for name, required in REQUIRED_ENV.items():
clean_env.setenv(name, required)
clean_env.setenv("OUTBOX_MAX_REPUBLISH", "0")
assert Settings(_env_file=None).OUTBOX_MAX_REPUBLISH == 0 # type: ignore[call-arg]