tasks: заведены шесть задач из урожая ревью и переименования образца конфига

- пять из урожая change 2026-08-11-fix-http-handler-tests, тег review-2026-08-11;
  утечка имени файла в журнал поставлена первой строкой очереди
- опечатка transcibe дописана в json-api-for-spa: текст ошибки — часть
  необратимого контракта, и в одиночку он не правится
This commit is contained in:
av
2026-08-11 08:59:45 +03:00
parent 1bd6d03188
commit 2333e80633
8 changed files with 300 additions and 4 deletions
+6
View File
@@ -20,6 +20,7 @@
## Очередь ## Очередь
- [🐞 Не писать имя файла пользователя в журнал](items/no-user-filename-in-log.md) — Приём кладёт имя файла, данное пользователем, в журнал контейнера на каждой принятой записи — инвариант приватности объявляет это критическим и необратимым.
- [🧹 Сравнивать доменные ошибки через errors.As](items/errors-as-instead-of-typecast.md) — NoopJobError и JobNotFoundError проверяются приведением типа: первая же обёртка %w между слоями сломает проверку молча. - [🧹 Сравнивать доменные ошибки через errors.As](items/errors-as-instead-of-typecast.md) — NoopJobError и JobNotFoundError проверяются приведением типа: первая же обёртка %w между слоями сломает проверку молча.
- [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата. - [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата.
- [🔬 Очередь задач: своя таблица или готовая библиотека](items/job-queue-choice.md) — Очередь написана вручную: захват двумя запросами без транзакции, протухание временем, опрос раз в секунду вхолостую тремя воркерами. - [🔬 Очередь задач: своя таблица или готовая библиотека](items/job-queue-choice.md) — Очередь написана вручную: захват двумя запросами без транзакции, протухание временем, опрос раз в секунду вхолостую тремя воркерами.
@@ -36,3 +37,8 @@
- [✨ Отправлять готовый текст через apprise и ntfy](items/ntfy-delivery.md) — Пользователь веба узнаёт о готовности только опросом с открытого экрана. - [✨ Отправлять готовый текст через apprise и ntfy](items/ntfy-delivery.md) — Пользователь веба узнаёт о готовности только опросом с открытого экрана.
- [🔬 Стоит ли брать OpenTelemetry вместо голого Prometheus](items/opentelemetry-fit.md) — Метрик одиннадцать штук на пять счётчиков, трассировки нет вовсе: путь одной записи по конвейеру собирается только чтением логов глазами. - [🔬 Стоит ли брать OpenTelemetry вместо голого Prometheus](items/opentelemetry-fit.md) — Метрик одиннадцать штук на пять счётчиков, трассировки нет вовсе: путь одной записи по конвейеру собирается только чтением логов глазами.
- [🔬 Потолки SpeechKit по длине записи и по формату](items/speechkit-limits.md) — Потолок длины записи и перечень принимаемых форматов неизвестны, а цель про долгие записи без них не начинается. - [🔬 Потолки SpeechKit по длине записи и по формату](items/speechkit-limits.md) — Потолок длины записи и перечень принимаемых форматов неизвестны, а цель про долгие записи без них не начинается.
- [🐞 Убирать записанный файл, когда приём отказал на середине](items/orphan-file-on-failed-intake.md) — Отказ чтения метаданных и отказ записи на диск оставляют файл в каталоге хранения без задачи и без учёта: сопоставить его не с чем, удалять приходится руками.
- [🧹 Покрыть тестами разбор вывода ffprobe](items/metaviewer-adapter-tests.md) — Проверки приёма перестали звать настоящий ffprobe 2026-08-11, а своего теста у адаптера метаданных нет: разбор JSON и отличие «программы нет в PATH» от «обработка отказала» не проверяет ничто.
- [🧹 Покрыть тестами шаги конвейера и захват задачи](items/pipeline-step-tests.md) — Тестовых файлов в проекте два, и оба мимо конвейера: потеря ссылки на файл, двойной ответ пользователю и гонка при захвате не поймаются ничем.
- [🧹 Разобрать мелочи http-транспорта](items/http-transport-nits.md) — Маршруты зарегистрированы дважды, и переименование пути в main.go проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
- [🧹 Переименовать образец конфига в config.example.toml](items/config-example-toml.md) — Конвенция называет config.dist.toml объявленным расхождением, но тут же пишет это имя как правило — документ противоречит сам себе, а образец расходится с конвенцией.
+45
View File
@@ -0,0 +1,45 @@
# 🧹 Переименовать образец конфига в config.example.toml
- **Тип:** chore
- **Категория:** Очередь
- **Зачем:** Конвенция называет config.dist.toml объявленным расхождением, но тут же пишет это имя как правило — документ противоречит сам себе, а образец расходится с конвенцией.
Конвенция конфигурации взята из проекта jellybit и **сама называет сегодняшнее
имя расхождением**: `docs/conventions/config.md`, строка 7 — «образец называется
`config.dist.toml`, а не `config.example.toml`». Но строки 31 и 34 того же
документа пишут `config.dist.toml` как правило, с заголовком раздела и всем
прочим. Документ противоречит сам себе, и который из двух читать — не выводится.
Задача закрывает расхождение в пользу конвенции: файл переименовывается, а
документ перестаёт спорить сам с собой.
## Затрагивает
- `config.dist.toml` в корне — переименование;
- `docs/conventions/config.md` — строка расхождения, заголовок раздела и все
упоминания имени;
- `CLAUDE.md`, раздел «Команды» — строка про то, что копировать;
- `README.md` — команда `cp` и абзац про недостающий ключ;
- `docs/review.md`, «Триггеры метки» — упоминание образца;
- `docs/conventions/README.md` — строка про самодокументируемый образец;
- `tasks/items/external-call-timeouts.md`, `telegram-account-link.md`,
`oidc-login.md` — разделы «Затрагивает» ссылаются на имя.
## Критерии приёмки
- Имени `config.dist.toml` в репозитории не осталось. Оракул —
`grep -rn 'config\.dist\.toml' . --exclude-dir=.git` пуст.
- Образец лежит под именем `config.example.toml` и по-прежнему не даёт
закоммитить реальный конфиг. Оракул — `git ls-files config.example.toml`
отдаёт файл, `git check-ignore config.toml` отдаёт `config.toml`.
- Конвенция больше не называет имя образца расхождением. Оракул —
`grep -n 'config\.example\.toml' docs/conventions/config.md` не находит строки,
противопоставляющей одно имя другому (сегодня это строка 7).
- Гейт зелёный: битых ссылок правка не оставила. Оракул — `task docs`.
## Рамки
Состав полей образца и его комментарии не пересматриваются — задача про имя и
про ссылки на него. Прорехи образца, помеченные в конвенции строками
«*Расхождение:*», остаются на месте.
+46
View File
@@ -0,0 +1,46 @@
# 🧹 Разобрать мелочи http-транспорта
- **Тип:** chore
- **Категория:** Очередь
- **Зачем:** Маршруты зарегистрированы дважды, и переименование пути в main.go проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
- **Теги:** review-2026-08-11
Два дефекта, найденные ревью change `2026-08-11-fix-http-handler-tests`. Оба в
`internal/controller/http`, мерджатся одним заходом.
**Маршруты зарегистрированы дважды.** `main.go` объявляет пути приёма и опроса,
и сборка теста объявляет их же своей копией. Проверки ходят по копии, поэтому
переименование пути в `main.go` их не роняет — а публичный контракт HTTP API
проект объявил необратимым. Проверено мутацией на прогоне триажа: замена
`api.POST("/audio", …)` на `api.POST("/upload", …)` оставляет тесты зелёными.
Нашёл проход `review-specs`.
**Обработчик пишет в журнал мимо `slog` и дублирует сервис.** `log.Printf("Err:
%v", err)` — стандартный `log`, тогда как весь проект пишет через `slog`; и эту
же ошибку строкой выше уже записал сервис. Типовой узел «Транспорт» в
[review.md](../../docs/review.md) прямо требует обратного: «не логирует ошибку,
которую уже залогировал доменный слой». Нашёл проход `review-code`.
## Затрагивает
- `internal/controller/http` и `main.go` — регистрация маршрутов приёма и
опроса, сегодня продублированная в обоих местах;
- `internal/controller/http/transcribe.go` — журнальная строка обработчика;
- `internal/controller/http/transcribe_test.go` — сборка теста, держащая свою
копию маршрутов.
## Критерии приёмки
- Пути и методы объявлены в одном месте, и переименование пути роняет проверки.
Оракул — мутация: заменить `/api/audio` на `/api/upload`, прогнать
`go test ./internal/controller/http/` и увидеть красное.
- Обработчик не пишет в журнал через стандартный `log`. Оракул —
`grep -rn 'log\.' internal/controller/http/*.go` без импорта `log/slog` пуст.
- Об одном отказе приёма в журнале одна запись, а не две. Оракул — прогон
отказа с перехваченным журналом: строк об этой ошибке ровно одна.
## Рамки
Форма ответа и коды не меняются — этим занимается
[json-api-for-spa](json-api-for-spa.md).
+8 -4
View File
@@ -29,9 +29,10 @@
репозиторием, возвращающим ошибку драйвера. репозиторием, возвращающим ошибку драйвера.
- Негодный файл даёт `400` с человекочитаемым текстом, а не `500`. Оракул — - Негодный файл даёт `400` с человекочитаемым текстом, а не `500`. Оракул —
тест: файл, который отвергает разбор метаданных. тест: файл, который отвергает разбор метаданных.
- Тело ошибки одной формы на всех эндпоинтах и не содержит сырого `err.Error()`. - Тело ошибки одной формы на всех эндпоинтах, не содержит сырого `err.Error()` и
Оракул — тест на четырёх ветвях отказа: форма совпадает, текста внутренней опечатки `transcibe`. Оракул — тест на четырёх ветвях отказа (форма совпадает,
ошибки в теле нет. текста внутренней ошибки в теле нет) плюс пустой `grep -rn 'transcibe'
internal/`.
- Список записей отдаётся страницами и упорядочен по времени создания. Оракул — - Список записей отдаётся страницами и упорядочен по времени создания. Оракул —
тест на выборке больше страницы. тест на выборке больше страницы.
- Отображение ошибки живёт в одной функции, и она названа в - Отображение ошибки живёт в одной функции, и она названа в
@@ -41,4 +42,7 @@
Аутентификацию и владельца не заводим — это `oidc-login` и `record-ownership`; Аутентификацию и владельца не заводим — это `oidc-login` и `record-ownership`;
задача про форму контракта. Публичный контракт после мерджа обратной правкой не задача про форму контракта. Публичный контракт после мерджа обратной правкой не
откатывается. откатывается — поэтому сюда же приехала опечатка `transcibe` в тексте ошибки
приёма (нашёл проход `review-specs` на ревью change
`2026-08-11-fix-http-handler-tests`): в одиночку текст ошибки менять нельзя,
а здесь контракт переписывается целиком.
+39
View File
@@ -0,0 +1,39 @@
# 🧹 Покрыть тестами разбор вывода ffprobe
- **Тип:** chore
- **Категория:** Очередь
- **Зачем:** Проверки приёма перестали звать настоящий ffprobe 2026-08-11, а своего теста у адаптера метаданных нет: разбор JSON и отличие «программы нет в PATH» от «обработка отказала» не проверяет ничто.
- **Теги:** review-2026-08-11
До 2026-08-11 настоящий `ffprobe` звали проверки приёма — правда, звали так, что
он всегда отказывал, то есть проверяли отказ и выдавали его за успех. Теперь
приём получает длительность от подставного источника, а у самого адаптера своего
теста нет: разбор JSON, перевод длительности из строки в секунды и отличие
«программы нет в `PATH`» от «обработка отказала» не проверяет ничто.
Решение и его цена записаны в
[ADR-2026-08-11-stub-adapters-in-tests](../../docs/adr/ADR-2026-08-11-stub-adapters-in-tests.md);
дыра названа в [review.md](../../docs/review.md), «Перестали проверять
сознательно».
## Затрагивает
- `internal/adapter/metaviewer/ffmpeg` — новый тестовый файл.
## Критерии приёмки
- Разбор годного вывода `ffprobe` проверен без запуска самой программы: длина в
секундах получается из заданного JSON. Оракул — тест на подставленном выводе.
- Отсутствие программы в `PATH` даёт отличимую ошибку, а не ту же, что отказ
обработки. Оракул — прогон с `PATH`, не содержащим `ffprobe`: текст ошибки
называет именно ненайденную программу.
- Вырожденный вывод — пустой, без поля длительности, с нечисловой строкой — не
превращается в нулевую длительность молча. Оракул — тест на трёх таких входах,
каждый даёт ошибку.
## Рамки
Проверка на настоящей записи здесь не заводится: она требует файла в
репозитории, а `testdata` в проекте нет. Если такая проверка нужна, это своё
решение и своя задача.
+50
View File
@@ -0,0 +1,50 @@
# 🐞 Не писать имя файла пользователя в журнал
- **Тип:** fix
- **Категория:** Очередь — сломано сейчас: каждая принятая запись кладёт имя файла живого пользователя в журнал контейнера, нарушение объявлено критическим и необратимым
- **Зачем:** Приём кладёт имя файла, данное пользователем, в журнал контейнера на каждой принятой записи — инвариант приватности объявляет это критическим и необратимым.
- **Теги:** review-2026-08-11
Инвариант «Содержимое записи остаётся приватным» (`CLAUDE.md`) запрещает писать
имя файла пользователя в лог: «только длина и идентификаторы». Приём пишет.
Строка идёт на **общем** шаге заведения задачи, то есть и для записей из
Telegram, и для записей по HTTP.
Найдено проходом `review-specs` ревью дизайна change
`2026-08-11-fix-http-handler-tests`; отчёт триажа —
`openspec/changes/archive/2026-08-11-fix-http-handler-tests/review/triage.md`.
Правка вынесена сюда решением человека на контрольной точке: задача была про проверки.
## Воспроизведение
```
grep -n 'file_name' internal/service/transcribe.go
```
Отдаёт `107: "file_name", fileName,` — поле в `s.logger.Info("Creating
transcribe job", ...)`. Уровень `Info`, то есть в боевой конфигурации строка
пишется. Приходит `fileName` из `CreateJobFromApi` и `CreateJobFromTelegram`
без изменений — это то самое имя, которое дал отправитель.
## Затрагивает
- `internal/service/transcribe.go`, функция `createTranscribeJob` — журнальные
строки шага заведения;
- `docs/conventions/logging.md` — если после правки понадобится строка про то,
чем имя заменяется.
## Критерии приёмки
- Имени, данного отправителем, нет ни в одной журнальной строке приёма. Оракул —
прогон приёма с записью, чьё имя содержит опознаваемую строку, и `grep` этой
строки по перехваченному журналу: ничего не найдено.
- Идентификатор файла, его расширение и длина в журнале остаются: по ним путь
записи прослеживается. Оракул — тот же перехваченный журнал, `file_id` и
`size` на месте.
## Рамки
Прочие журнальные строки проекта здесь не пересматриваются. Полная сверка
журнала с запретами — отдельная работа; эта задача закрывает названный
инвариантом случай.
@@ -0,0 +1,61 @@
# 🐞 Убирать записанный файл, когда приём отказал на середине
- **Тип:** fix
- **Категория:** Очередь
- **Зачем:** Отказ чтения метаданных и отказ записи на диск оставляют файл в каталоге хранения без задачи и без учёта: сопоставить его не с чем, удалять приходится руками.
- **Теги:** review-2026-08-11
Приём пишет файл на диск, потом спрашивает у источника метаданных длительность,
потом заводит запись в учёте и задачу. Уборка при отказе есть **только на
последнем шаге**: отказ `fileRepo.Create` зовёт `os.Remove`, а отказы записи на
диск и чтения метаданных возвращают ошибку, оставляя файл лежать.
Сопоставить такой файл не с чем: записи в `files` под него нет, задачи нет,
имя — случайный идентификатор. На сервере это `data/files`, где лежат голосовые
сообщения живых людей, а проект по паспорту «не хранит записи как архив».
Найдено проходом `review-specs` ревью дизайна change
`2026-08-11-fix-http-handler-tests`; отчёт триажа —
`openspec/changes/archive/2026-08-11-fix-http-handler-tests/review/triage.md`.
Вынесено сюда решением человека на контрольной точке.
## Воспроизведение
Асимметрия видна в самом коде:
```
sed -n '110,160p' internal/service/transcribe.go
```
Ветка `fileRepo.Create` (строки 152–157) содержит `os.Remove(storageFilePath)` с
комментарием «Удаляем файл если не удалось создать запись в БД». Ветки
`io.Copy`, `dst.Close` и `metaviewer.GetInfo` (строки 116–133) возвращают ошибку
без уборки, хотя файл к этому моменту уже создан.
Прогоном: `TestCreateTranscribeJob_MetaViewerFailure` в
`internal/controller/http/transcribe_test.go` доводит приём до этой ветки —
после него в каталоге хранения лежит файл, а `countJobs` равен нулю.
## Затрагивает
- `internal/service/transcribe.go`, функция `createTranscribeJob` — три ветки
отказа между созданием файла и заведением записи в учёте;
- `internal/controller/http/transcribe_test.go` — случай отказа метаданных
получает проверку на отсутствие файла;
- раскладка `data/files` не меняется: имена и формат прежние.
## Критерии приёмки
- Отказ чтения метаданных не оставляет файла в каталоге хранения. Оракул — тест
на подставном источнике, возвращающем ошибку: каталог пуст после запроса.
- Отказ записи на диск на середине не оставляет частично записанного файла.
Оракул — тест с источником данных, обрывающимся на середине чтения.
- Уборка сама не заслоняет исходную ошибку: отправитель по-прежнему получает
отказ, а причина уезжает в журнал. Оракул — тот же тест, код ответа и запись в
перехваченном журнале.
## Рамки
Уборка файлов, осиротевших **до** этой правки, сюда не входит: их надо найти
сверкой каталога с учётом, и это отдельная работа с боевыми данными.
+45
View File
@@ -0,0 +1,45 @@
# 🧹 Покрыть тестами шаги конвейера и захват задачи
- **Тип:** chore
- **Категория:** Очередь
- **Зачем:** Тестовых файлов в проекте два, и оба мимо конвейера: потеря ссылки на файл, двойной ответ пользователю и гонка при захвате не поймаются ничем.
- **Теги:** review-2026-08-11
Тестовых файлов в проекте два: приём по HTTP и деление длинного сообщения
Telegram. Шаги конвейера — `FindAndRunConversionJob`, `FindAndRunTranscribeJob`,
`FindAndRunTranscribeCheckJob` — и воркеры, которые их выполняют, не покрыты ничем.
Свойства, названные типовым узлом «Шаг конвейера» в
[review.md](../../docs/review.md), сегодня не проверяет ни одна команда: что
«задач нет» отличается от отказа, что ссылка на файл не теряется, что повтор не
плодит лишних записей, что пользователю отвечают ровно один раз. Три воркера
читают общий `*sql.DB`, и теста с параллельным доступом нет тоже.
Найдено проходом `review-autotests` ревью кода change
`2026-08-11-fix-http-handler-tests`, вопросом темы `autotests`.
## Затрагивает
- `internal/service` — новые тесты шагов конвейера;
- `internal/controller/worker` — тест на одновременную работу трёх воркеров
против одной базы;
- подстановки: `internal/adapter/recognizer/memory.go` уже есть, конвертеру и
источнику метаданных нужны свои.
## Критерии приёмки
- «Задач нет» отличается от отказа и не считается ошибкой. Оракул — тест на
пустой базе: шаг возвращает `NoopJobError`, метрика отказов не растёт.
- Задача, брошенная на середине шага, достаётся снова по истечении захвата и
проходит шаг заново, не плодя вторых файлов. Оракул — тест: прервать шаг,
отмотать время захвата, прогнать снова, сверить число записей в `files`.
- Три воркера против одной базы не выдают одну задачу дважды. Оракул — тест с
параллельным прогоном под `-race`.
## Рамки
Задачу лучше брать **после** разведки
[job-queue-choice](job-queue-choice.md): она может сменить модель очереди
целиком, и тесты, написанные под сегодняшний захват, придётся выбросить вместе с
ним.