Запрос с одиночным суррогатом в `original_name` отвергается схемой (pydantic `string_unicode`), а не «не ломает
публикацию»: тест переименован в `..._is_schema_mismatch` и проверяет код SCHEMA_MISMATCH, отсутствие ack и `put`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Конверт `result:<job_id>:<attempt>` битого запроса совпадал по Nats-Msg-Id с настоящим результатом, и ядро
(дедуп по result_msg_id) отбросило бы настоящий итог. `_reject` нормализует job_id (`uuid.UUID`, нижний регистр)
и при `_seen` только пишет предупреждение. Тест: битый запрос с тем же job_id во время ожидания ИС и после итога,
в том числе UUID в верхнем регистре — опубликованы только ack и настоящий итог.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`api/src/__init__.py` оставлен только с docstring: `python -m src.scan_emulator` больше не импортирует `src.app`
и не требует `OPENBAO__*`. `run_server.py` импортирует `app` из `src.app`, `get_settings` — из `src.core.config`.
Регрессионный тест: subprocess эмулятора с окружением только PATH/ENV_FILE/SCAN_HOTFOLDER_PATH -> код 2,
сообщение про корень горячей папки, без ValidationError.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Lifespan приложения и эмулятор ИС в процессе теста (R-7) против compose NATS/MinIO, горячая папка в tmp-каталоге.
Три сценария (R-10): aep_5.pdf (реальная фикстура) — INVALID_DOCUMENT от ИС, повтор во время ожидания и после
результата даёт ещё один ack без второго файла; ok_doc.pdf (синтетический успех) — NOT_IMPLEMENTED; unknown.pdf —
INVALID_DOCUMENT эмулятора. Проверяются схема конверта, service.name, Nats-Msg-Id, raw/<job_id>.json байт в байт,
входящий файл против объекта S3, /healthcheck, остановка без незавершённых задач и ошибок в логе.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Сценарий spec «Пять заданий подряд» переписан детерминированно: паузы опроса горячей
папки управляются тестом (SteppedSleep), ожидания — по событиям с тайм-аутом. Проверяется:
пять ack'ов до освобождения слота, ровно четыре файла и четыре чтения S3 при занятых слотах,
пятый файл — только после итога освобождённого задания, у всех пяти — итоговый конверт.
Добавлены строгая очерёдность при SCAN_MAX_INFLIGHT = 1 и остановка воркера при очереди
на слот (T12.9): задачи отменены, утечек задач и слотов нет, новых файлов нет.
Прежний test_five_jobs_with_four_slots удалён из test_worker.py (без дублирования).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Фейки S3/NATS, шпион горячей папки, Env, эмулятор ИС и eventually вынесены из test_worker.py
без изменения логики, чтобы ими пользовался отдельный тест параллелизма. make_env получил
подмену sleep (пауза между опросами горячей папки).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
src/core/health.py: HealthProvider — nats ok/down/disabled по is_connected; hotfolder —
healthy() в потоке под wait_for (2 с) с кэшем 5 с, зависшая проба не запускается
повторно (T8.4). Сбой регистрации SMB-сессии виден естественно: SmbHotFolder.healthy()
обращается к шаре с теми же учётными данными (T2.3).
app.py: в lifespan S3Storage, NatsGateway, Worker(NullNormalizer); подключение к NATS
и подписка — фоновой задачей (connect ждёт сервер), её сбой — logger.exception и
nats=down без падения HTTP; остановка отменяет подключение, останавливает воркер,
закрывает NATS и S3. Объекты lifespan — в app.state.
test_app.py: тесты lifespan переведены на NATS_ENABLED=false и tmp-корень (иначе фоновое
подключение шло бы в compose NATS), проверка полей healthcheck расширена nats/hotfolder.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
on_request проверяет тело (SCHEMA_MISMATCH без ack, R-16), дедуплицирует по job_id
(задания в работе + LRU DONE_CACHE_SIZE, R-17) и запускает задачу: ack до чтения S3 и
до ожидания слота, семафор SCAN_MAX_INFLIGHT, handle. handle: S3 (один бакет, R-19) ->
горячая папка через asyncio.to_thread (R-15) -> опрос до min(reply_deadline, старт +
SCAN_TIMEOUT_SECONDS) (R-14, R-20) -> raw/{job_id}.json до нормализации (R-18) ->
нормализатор -> results/{file_id}/{job_id}/result.json -> конверт. Таблица ошибок D-7,
публикация через NatsGateway с повторами, PublishFailed логируется. Текст ошибки
очищается от одиночных суррогатов и ограничен 4000 символами (T10.1, T10.2).
Тесты: фейки S3/NATS с общим журналом событий, настоящий LocalHotFolder и process_once
эмулятора; каждый конверт проверяется JSON Schema контракта. Фикстура log_lines в
conftest перехватывает loguru. src.core.worker и src.core.health — в строгом mypy.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
publish делает первую попытку и до retries повторов (retries=0 — одна попытка), PublishFailed.attempts —
фактическое число попыток. Комментарий PUBLISH_RETRIES в config.py уточнён. Докстринги: PublishFailed не значит
«не доставлено» (T11.3), колбэк подписки должен возвращаться быстро (T11.4).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
NatsGateway: подключение с nkeys/JWT или без учётных данных, NATS_ENABLED=false — без подключения,
автопереподключение nats-py, subscribe_requests в queue group scan-gateway, publish с Nats-Msg-Id и
повторами (PUBLISH_RETRIES попыток всего) -> PublishFailed, publish_result для конвертов ack/result.
src.core.clients и src.core.nats — в strict-списке mypy. Интеграционные тесты против compose-NATS/MinIO:
queue group, заголовок Nats-Msg-Id, roundtrip S3, отсутствующий ключ.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ошибки не проглатываются: S3ObjectNotFoundError (NoSuchKey) и S3ReadError для чтения, S3WriteError для записи,
исходная причина — в __cause__. JSON пишется в UTF-8 без экранирования, Content-Type application/json.
Шаблонные put_object/get_object удалены (не использовались); удалён и шаблонный nats_callback.py — его
единственный импорт S3Storage, сам он нигде не используется (слой NATS — следующим коммитом в src/core/nats.py).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ошибка ИС пробрасывается с кодом из ответа, неизвестный код/вид ошибки -> UNKNOWN_ERROR с исходным
представлением, успех -> NOT_IMPLEMENTED, не объект -> INVALID_FORMAT. Пакет добавлен в strict-список mypy.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Пара фикстур a_b-1.json / a_b.json: префикс по алфавиту раньше точного имени, поэтому тест падает,
если шаг «сначала <name>.json» убран. Убран посторонний комментарий в соседнем тесте.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
process_once(root, fixtures, delay): новые <uuid>_<имя> -> ответ из фикстуры (точное имя, затем префикс,
file_uid подставляется) либо INVALID_DOCUMENT, атомарная запись ответа, затем переименование в .done.
Цикл python -m src.scan_emulator с корректной остановкой по SIGINT/SIGTERM.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
T8.1: inbox_name заменяет `: < > " | ? *` и управляющие символы на `_`, убирает завершающие точки/пробелы;
ValueError только если имени не осталось. validate_name/put остаются строгими.
T8.2: SmbHotFolder.poll возвращает None на STATUS_SHARING_VIOLATION/STATUS_DELETE_PENDING (ИС ещё пишет ответ),
монопольное открытие сохранено.
T8.3: при ошибке mkdir, кроме EEXIST/ENOENT, put продолжает, если smbclient.path.isdir(Input); иначе исходная ошибка.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SmbHotFolder: UNC-корень \\host\SMB_PATH (или полный UNC в SMB_PATH), put — `.<name>.part` + smbclient.replace,
poll — open_file(rb), ENOENT (в т.ч. нет каталога) -> None, healthy — listdir корня без исключений.
create_hotfolder(settings): local -> LocalHotFolder, smb -> SmbHotFolder с ленивым импортом модуля (R-6);
отсутствие импорта src.gateway.smb в режиме local проверяется в отдельном интерпретаторе.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
validate_name/validate_job_id вынесены из LocalHotFolder для повторного использования в SmbHotFolder.
Дополнительно отвергаются `: < > " | ? *` и управляющие символы 0x00-0x1F (в put, answer_path и inbox_name);
пробелы и кириллица в именах допустимы.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>