test, docs: порядок старта и проверка бакета в тестах приложения; README/CLAUDE.md без устаревших «добавляется задачей», spec без номера рулинга

T9.2: события старта worker.start → nats.start; _connect_s3 — info/warning/error по ответу проверки бакета, клиент
остаётся. T9.3: spec без «R-17». T9.4: README и CLAUDE.md описывают уже существующие файлы без отсылок к будущим задачам.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-04 03:18:09 +04:00
parent 18276e607e
commit b7d4f0c17a
4 changed files with 72 additions and 16 deletions

View File

@ -37,11 +37,10 @@ CORS, регистрация SMB-сессии, импорт `raisa_fastapi_prote
`requirements.txt` — `asyncpg`, `SQLAlchemy`, `alembic`, `smbprotocol`, `argon2-cffi`, `greenlet`, `Mako`,
`raisa_fastapi_protected_api`; шаблонный `README.md` заменён описанием сервиса.
**Добавлено** (целевое состояние среза; каждый пункт появляется своей задачей `openspec/changes/slice-0-skeleton/tasks.md`,
до этого файла нет): `api/src/parser/` (интерфейс `Parser` и заглушка `NullParser`; разбор ОСВ §1 и форм §12 — чистые
**Добавлено** (в скобках — задача `openspec/changes/slice-0-skeleton/tasks.md`): `api/src/parser/` (интерфейс `Parser` и заглушка `NullParser`; разбор ОСВ §1 и форм §12 — чистые
функции над книгой Excel, Срезы 2+), `api/src/contract/` (pydantic-модели `recognition-result` v1.0) и `api/contracts/`
(копии схем; идентичность с `../docs/contracts` проверяется в `make check`) — задача 3.1, `api/rules/*.yaml` (словари
синонимов — в документах это `rules/*.yaml` относительно `api/`) — 4.1, `api/src/core/nats.py` и `api/src/core/worker.py`
(копии схем; идентичность с `../docs/contracts` проверяется в `make check`) — задача 3.1, `api/rules/` (README о назначении;
сами словари `*.yaml` — в документах это `rules/*.yaml` относительно `api/` — появятся в Срезе 2) — 4.1, `api/src/core/nats.py` и `api/src/core/worker.py`
(подписка queue group `tabular-parser` на `recognition.tabular.request` → ack → S3 `uploads/` → парсер → S3 `results/` →
`recognition.result`) — 4.2–4.3, `api/pyproject.toml` (ruff, mypy) и `Makefile` — 2.1, `Dockerfile`, `.env.example`,
`.env.test` — 2.2. Проверки: `make check` (`pytest`, `ruff check . && ruff format --check .`, `mypy src`),

View File

@ -7,7 +7,7 @@ Stateless-сервис распознавания табличных файло
`arm-core`.
Обмен с `arm-core` идёт по контракту **v1.0**: схемы — [`../docs/contracts/`](../docs/contracts/), копия — в
`api/contracts/` (добавляется задачей 3.1 среза и проверяется на идентичность в `make check`).
`api/contracts/` (идентичность с источником проверяется в `make check`).
**Срез 0 — скелет.** Транспорт работает полностью (подписка, ack, чтение S3, публикация результата, ошибки транспорта),
а сам разбор заменён заглушкой: на любой корректный запрос сервис отвечает конвертом `status = error` с
@ -51,15 +51,15 @@ api/ бэкенд (все команды разработки
src/app.py FastAPI: lifespan (MinIO, NATS, воркер), GET /healthcheck, middleware, обработчики ошибок
src/core/ config.py (настройки), clients.py (S3), nats.py (соединение NATS), worker.py (воркер
распознавания), logger.py, exception_handlers.py
src/parser/ интерфейс Parser и реализации (добавляется задачей 4.1)
src/contract/ pydantic-модели контракта v1.0 (добавляется задачей 3.1)
contracts/ копии JSON-схем контракта (добавляется задачей 3.1)
rules/ словари синонимов *.yaml для парсера (добавляется задачей 4.1)
src/parser/ интерфейс Parser и заглушка NullParser (разбор — Срез 2)
src/contract/ pydantic-модели контракта v1.0
contracts/ копии JSON-схем контракта и примеры
rules/ словари синонимов *.yaml для парсера (пока только README; словари — Срез 2)
tests/ pytest: unit и integration (маркер `integration`)
setup/ pip.conf и setup.sh (создание venv без uv)
raisa_config.yml конфигурация платформы РАИСА
Makefile команды разработки (добавляется задачей 2.1)
Dockerfile образ сервиса, python:3.11-slim (добавляется задачей 2.2)
Makefile команды разработки
Dockerfile образ сервиса, python:3.11-slim
openspec/ требования и дельта текущего среза (openspec/changes/slice-0-skeleton/)
```
@ -87,8 +87,6 @@ openspec/ требования и дельта текущего ср
| `make check-full` | `make check` + `make test-integration` + `docker build`; запускать перед PR |
| `make up` | `docker compose -f ../docs/dev/docker-compose.yml up -d --wait postgres nats minio` |
Все цели `Makefile` (включая `check-full`) создаются задачей 2.1; `docker build` в `check-full` заработает после задачи 2.2 (`Dockerfile`).
## Запуск локально против compose
```bash
@ -100,7 +98,7 @@ cd api && .venv/bin/python run_server.py
```
Значения из `api/.env.example` уже совпадают с `../docs/dev/.env` (стандартные порты и учётные данные compose); править
`api/.env` нужно, только если вы меняли порты или пароли compose. Файл `api/.env.example` создаётся задачей 2.2.
`api/.env` нужно, только если вы меняли порты или пароли compose.
Подробности про compose, смену портов и сброс — [`../docs/dev/README.md`](../docs/dev/README.md).
Сервис слушает `0.0.0.0:8000` (`SERVER__HOST`, `SERVER__PORT`). Проверка:

View File

@ -15,8 +15,10 @@ from typing import Any
import httpx
import pytest
from loguru import logger
from src.contract import NATS_MSG_ID_HEADER, RESULT_SUBJECT
from src.core.clients import S3Storage
from src.core.worker import RecognitionWorker
EXAMPLES_DIR = Path(__file__).resolve().parents[2] / "contracts" / "examples"
@ -108,6 +110,10 @@ def patched_app(monkeypatch: pytest.MonkeyPatch, events: list[str]) -> Iterator[
return storage
class RecordingWorker(RecognitionWorker):
def start(self) -> None:
events.append("worker.start")
super().start()
async def stop(self, timeout: float = 10.0) -> None:
events.append("worker.stop")
await super().stop(timeout)
@ -164,8 +170,9 @@ async def test_nats_enabled_subscribes_worker_and_reports_ok(
]
assert connection.published[1][1]["error"]["code"] == "NOT_IMPLEMENTED"
assert connection.published[1][1]["service"]["name"] == "tabular-parser"
# Старт: воркер готов до подключения (on_connected сразу подписывает worker.on_request).
# Остановка: воркер дорабатывает очередь, пока NATS ещё жив, затем NATS, затем S3
assert events == ["nats.start", "worker.stop", "nats.stop", "s3.close"]
assert events == ["worker.start", "nats.start", "worker.stop", "nats.stop", "s3.close"]
async def test_nats_enabled_but_not_connected_reports_down(patched_app: Any, monkeypatch: pytest.MonkeyPatch) -> None:
@ -177,3 +184,55 @@ async def test_nats_enabled_but_not_connected_reports_down(patched_app: Any, mon
(connection,) = FakeNatsConnection.instances
assert connection.handlers == [] # подписка — только после подключения
assert patched_app.app.state.worker is not None
# --- проверка бакета при старте (_connect_s3) ------------------------------------------------------------------------
@pytest.fixture
def log_records() -> Iterator[list[Any]]:
records: list[Any] = []
handler_id = logger.add(lambda message: records.append(message.record), level="DEBUG")
yield records
logger.remove(handler_id)
class FakeMinio:
"""Подмена `miniopy_async.Minio`: `bucket_exists` отдаёт заданный ответ или бросает исключение."""
answer: bool | Exception = True
closed = 0
def __init__(self, *_args: Any, **_kwargs: Any) -> None:
pass
async def bucket_exists(self, bucket: str) -> bool:
if isinstance(FakeMinio.answer, Exception):
raise FakeMinio.answer
return FakeMinio.answer
async def close_session(self) -> None:
FakeMinio.closed += 1
@pytest.mark.parametrize(
("answer", "level", "fragment"),
[
pytest.param(True, "INFO", "Бакет проверен", id="bucket-exists"),
pytest.param(False, "WARNING", "Бакет не найден", id="bucket-missing"),
pytest.param(ConnectionError("MinIO недоступен"), "ERROR", "Не удалось проверить", id="minio-down"),
],
)
async def test_connect_s3_logs_bucket_check_and_keeps_client(
monkeypatch: pytest.MonkeyPatch, log_records: list[Any], answer: Any, level: str, fragment: str
) -> None:
monkeypatch.setattr(FakeMinio, "answer", answer)
monkeypatch.setattr(FakeMinio, "closed", 0)
monkeypatch.setattr(app_module, "Minio", FakeMinio)
storage = await app_module._connect_s3()
# Клиент остаётся в любом случае: MinIO может подняться позже, воркер прочитает файл без рестарта
assert isinstance(storage, S3Storage)
assert storage.bucket == app_module.settings.OPENBAO.MINIO.MINIO_BUCKET
assert FakeMinio.closed == 0
messages = [record["message"] for record in log_records if record["level"].name == level]
assert any(fragment in message for message in messages), messages

View File

@ -81,7 +81,7 @@ NATS и S3; воркер MUST NOT зависеть от конкретной р
При пустых `NATS_SEED`/`NATS_JWT` сервис SHALL подключаться без учётных данных, иначе — с nkeys/JWT. Сервис MUST NOT
требовать stream'ов или consumer'ов JetStream; при разрыве соединения SHALL переподключаться автоматически и восстанавливать
подписку; `/healthcheck` SHALL отражать состояние (`nats = ok | down`; `off` — NATS выключен флагом
`NATS_ENABLED=false`, R-17).
`NATS_ENABLED=false`).
#### Scenario: NATS перезапущен
- **WHEN** контейнер NATS перезапущен во время работы сервиса