- Исправлена критическая ошибка: NullNormalizer считал ответ ошибкой при `error is not None`,
а у реального успеха ИС error = {"code": "", "message": ""} — aep_3.json и aep_6.json давали
UNKNOWN_ERROR. Успех файла теперь — error.code == "" (отсутствующий/null/не-объектный error
и error без кода ошибкой не считаются); регрессия — на реальных фикстурах (маркер fixtures).
- src/gateway/merge.py: merge_answers(job_id, files) -> MergedResponse
{"job_id", "files": [{"name", "response", "parse_error"}]} по возрастанию имени; не JSON —
parse_error файла; dump_merged — UTF-8 (с ASCII-экранированием при одиночном суррогате).
- Воркер: опрос отдаёт готовый набор → склейка → raw/{job_id}.json (application/json) до
нормализации → Normalizer.normalize(склейка). Перечитывание «не JSON» убрано (R-24).
- NullNormalizer: все файлы ошибочны — код/сообщение первого по имени (parse_error и
не-объект — INVALID_FORMAT, код вне контракта — UNKNOWN_ERROR с исходным кодом); есть
успешный — NOT_IMPLEMENTED; ошибочные файлы — строки `<имя>: <код ИС>: <сообщение>`.
- NormalizeOutcome.warnings (R-25) → metrics.warnings конверта и файла результата; пустые
опускаются; не больше 100 строк по 1000 символов, лишние — сводной строкой (R-30).
- Тесты: test_merge.py, test_normalizer.py переписан под склейку, test_worker.py — склейка в
raw, ошибка одного файла, parse_error в warnings, все ошибочны, реальные aep_3+aep_3_ofr и
aep_6, settle и таймаут ненастоявшегося набора; SUCCESS_ANSWER — в форме реального успеха.
Интеграционный test_roundtrip.py минимально переведён на формат склейки (прогон — Task 5).
Co-Authored-By: Claude Sonnet 5.5 <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>
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>