Files
transcriber/openspec/changes/archive/2026-08-11-fix-http-handler-tests/design.md
T
av 6c04c801c9 http: тесты приёма переписаны на подставные адаптеры
- проверки больше не зовут ffprobe и не меняют рабочий каталог процесса;
  добавлены случаи на отказ чтения метаданных и на отсутствие поля audio
- заведена спека intake на приём по HTTP, ADR о подставных адаптерах,
  запись в журнал ревью о проверке, которая не могла упасть
- go test снят из объявленных долгов CLAUDE.md, послабление errcheck
  для _test.go в .golangci.yml убрано
2026-08-11 08:35:11 +03:00

124 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Context
Тесты приёма записи по HTTP лежат в репозитории с коммита `87d8b05` и не проходили
ни разу. Отказов четыре, и причина у них одна: тест поднимает приём вместе с
**настоящим** источником метаданных — внешней программой `ffprobe`, — а
скармливает ему либо файл, которого нет, либо строку «test audio content».
`ffprobe` такой вход отвергает, приём отвечает отказом, тест ждал успеха.
Отсюда следствие дороже самих тестов: красный `go test` в проекте объявлен долгом,
и настоящий отказ приёма от него неотличим.
Второе, что мешает: каталог хранения приём берёт из строки `data/files`,
относительной к рабочему каталогу процесса. Чтобы файлы не улетали в репозиторий,
каждый тест зовёт `os.Chdir` — то есть меняет состояние **всего процесса**.
Параллельно такие тесты гонять нельзя, а `errcheck` на непроверенных `os.Chdir`
молчит только потому, что весь `_test.go` вынесен в исключения линтера.
## Goals / Non-Goals
**Goals:**
- проверка приёма судит **наш** код, а не способность `ffprobe` разобрать вход;
- отказ чтения метаданных проверен отдельным случаем: сегодня эту ветку не
проверяет ничто;
- проверки проходят на чистом клоне без подготовки файлов руками и без
установленного `ffprobe`;
- проверки не трогают состояние процесса и не мешают друг другу.
**Non-Goals:**
- поведение приёма не меняется — ни коды ответов, ни имена полей, ни раскладка
файлов на диске. Задача про то, чем поведение проверяется;
- код `internal/service` и `internal/controller/http` не правится: подстановка
туда уже заведена, и пользоваться ей — вопрос теста, а не сервиса;
- проверка настоящего `ffprobe` на настоящей записи здесь не заводится — см.
«Risks / Trade-offs».
## Decisions
### Подставной источник метаданных вместо настоящего `ffprobe`
Приём берёт длительность через интерфейс `contract.AudioMetaViewer`, а
реализацию получает снаружи при сборке. Тест подставляет свою: она отдаёт
заданную длительность и, когда тесту нужен отказ, — заданную ошибку. Одним
типом закрываются оба случая, и ветка отказа впервые становится проверяемой.
Рассмотрено и отвергнуто:
- **положить настоящую запись в `testdata`.** Отвергнуто дважды: `.gitignore`
строкой `*.m4a` её не пустит, а `CLAUDE.md` прямо говорит, что `testdata` в
проекте нет и тесты создают нужное во временном каталоге. Снимать запрет ради
теста — менять правило проекта под удобство одного файла;
- **порождать запись `ffmpeg` прямо в тесте.** Отвергнуто: проверка приёма
начинает требовать установленных `ffmpeg` и `ffprobe`, а критерий приёмки
требует обратного — прогона с `ffprobe`, убранным из `PATH`;
- **проверять приём сквозь настоящий `ffprobe`.** Отвергнуто: это проверка
внешней программы, а не нашего кода. Она найдёт отказ `ffprobe` и не найдёт
ошибку в приёме — ровно наоборот тому, зачем эти тесты писались.
### Каталог хранения задаётся снаружи, а не рабочим каталогом процесса
Каталог тесту даёт `t.TempDir()`, и он же уезжает в сборку сервиса. `os.Chdir`
уходит целиком. Человек увидит разницу в двух местах: тесты можно гонять
параллельно, и упавший тест больше не оставляет процесс в чужом каталоге, ломая
следующие за ним.
Рассмотрено и отвергнуто: **оставить `os.Chdir`, но восстанавливать каталог
надёжнее.** Отвергнуто — надёжного способа нет: рабочий каталог у процесса один
на все горутины, и любой параллельный тест увидит чужой.
### Запрос собирается из байтов, а не из файла на диске
Сегодня вспомогательная функция открывает файл по пути, поэтому каждому случаю
нужен файл в текущем каталоге. Она начинает принимать имя и содержимое —
диск из подготовки запроса уходит, а имя файла (то, чем проверяется выбор
расширения) задаётся прямо, без создания одноимённого файла.
### Внешних программ в этом тесте не остаётся ни одной
Конвертер в сборке теста тоже настоящий, хотя проверки его не зовут: приём
конвертацию не делает. Он заменяется подставным вместе с источником метаданных.
Смысл не в экономии: после этого «тест не зависит от внешних программ»
проверяется взглядом на список импортов, а не рассуждением о том, какие ветки
кода отработают.
### Послабление линтера для тестов снимается
Исключение `errcheck` на `_test.go` в `.golangci.yml` заведено под непроверенные
`os.Chdir`. Их не остаётся — исключение снимается вместе с ними. Держать
послабление после того, как ушла его причина, значит оставить весь будущий тестовый
код без проверки возвращаемых ошибок и не помнить почему.
## Risks / Trade-offs
- **Настоящий `ffprobe` теперь не проверяется ничем.** До этой правки его звал
тест приёма — правда, звал так, что тот всегда отказывал, то есть проверял
отказ и выдавал его за успех. Формально покрытие не теряется: проверялось и
раньше ничего. Но дыра называется прямо — у `internal/adapter/metaviewer/ffmpeg`
своего теста нет, и разбор вывода `ffprobe` не проверен. → Смягчение: находка
уходит в урожай отдельной задачей; здесь она не чинится, потому что требует
записи в репозитории либо отдельного вида проверок, а это своё решение.
- **Снятое послабление линтера действует на весь будущий тестовый код.** →
Смягчение: это и есть цель. Если оно окажется тяжёлым, его вернут осознанно и
с причиной в самом файле, а не по наследству.
- **Норма про имя отправителя накрывает хранилище, но не журнал.** Ревью дизайна
нашло, что приём пишет имя файла пользователя в журнал — это нарушение
инварианта «содержимое записи остаётся приватным», объявленного критическим и
необратимым, и идёт оно по общему пути, то есть и для записей из Telegram.
Решением человека на чекпоинте правка вынесена отдельной задачей: она про
поведение сервиса, а не про проверки. → Смягчение: находка уходит в урожай.
Спека при этом нормирует только хранилище и о журнале молчит намеренно —
писать норму, которой код заведомо не следует, значит завести спеку, которая
врёт с первого дня.
- **Отказ чтения метаданных оставляет файл на диске.** Соседняя ветка отказа
(не удалось завести учётную запись файла) файл убирает, эта — нет, как и
отказ записи на диск. Ни задачи, ни записи в учёте под такой файл нет, и
сопоставить его не с чем. Решением человека на чекпоинте вынесено отдельной
задачей: правка трогает три ветки отказа и меняет поведение на диске. →
Смягчение: находка уходит в урожай; сценарий отказа в спеке нормирует только
то, что проверяется здесь, — код ответа и незаведённую задачу.
- **Спека `intake` описывает только приём по HTTP.** Приём из Telegram остаётся
в коде и без требований. → Смягчение: назван в самой спеке. Требование,
написанное без проверки, было бы предположением, а не нормой.