diff --git a/tasks/BACKLOG.md b/tasks/BACKLOG.md index e838383..9c2fdb9 100644 --- a/tasks/BACKLOG.md +++ b/tasks/BACKLOG.md @@ -20,6 +20,7 @@ ## Очередь +- [🐞 Не писать имя файла пользователя в журнал](items/no-user-filename-in-log.md) — Приём кладёт имя файла, данное пользователем, в журнал контейнера на каждой принятой записи — инвариант приватности объявляет это критическим и необратимым. - [🧹 Сравнивать доменные ошибки через errors.As](items/errors-as-instead-of-typecast.md) — NoopJobError и JobNotFoundError проверяются приведением типа: первая же обёртка %w между слоями сломает проверку молча. - [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата. - [🔬 Очередь задач: своя таблица или готовая библиотека](items/job-queue-choice.md) — Очередь написана вручную: захват двумя запросами без транзакции, протухание временем, опрос раз в секунду вхолостую тремя воркерами. @@ -36,3 +37,8 @@ - [✨ Отправлять готовый текст через apprise и ntfy](items/ntfy-delivery.md) — Пользователь веба узнаёт о готовности только опросом с открытого экрана. - [🔬 Стоит ли брать OpenTelemetry вместо голого Prometheus](items/opentelemetry-fit.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 объявленным расхождением, но тут же пишет это имя как правило — документ противоречит сам себе, а образец расходится с конвенцией. diff --git a/tasks/items/config-example-toml.md b/tasks/items/config-example-toml.md new file mode 100644 index 0000000..1ee1203 --- /dev/null +++ b/tasks/items/config-example-toml.md @@ -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`. + +## Рамки + +Состав полей образца и его комментарии не пересматриваются — задача про имя и +про ссылки на него. Прорехи образца, помеченные в конвенции строками +«*Расхождение:*», остаются на месте. + diff --git a/tasks/items/http-transport-nits.md b/tasks/items/http-transport-nits.md new file mode 100644 index 0000000..5c50b97 --- /dev/null +++ b/tasks/items/http-transport-nits.md @@ -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). + diff --git a/tasks/items/json-api-for-spa.md b/tasks/items/json-api-for-spa.md index 96d35de..02409af 100644 --- a/tasks/items/json-api-for-spa.md +++ b/tasks/items/json-api-for-spa.md @@ -29,9 +29,10 @@ репозиторием, возвращающим ошибку драйвера. - Негодный файл даёт `400` с человекочитаемым текстом, а не `500`. Оракул — тест: файл, который отвергает разбор метаданных. -- Тело ошибки одной формы на всех эндпоинтах и не содержит сырого `err.Error()`. - Оракул — тест на четырёх ветвях отказа: форма совпадает, текста внутренней - ошибки в теле нет. +- Тело ошибки одной формы на всех эндпоинтах, не содержит сырого `err.Error()` и + опечатки `transcibe`. Оракул — тест на четырёх ветвях отказа (форма совпадает, + текста внутренней ошибки в теле нет) плюс пустой `grep -rn 'transcibe' + internal/`. - Список записей отдаётся страницами и упорядочен по времени создания. Оракул — тест на выборке больше страницы. - Отображение ошибки живёт в одной функции, и она названа в @@ -41,4 +42,7 @@ Аутентификацию и владельца не заводим — это `oidc-login` и `record-ownership`; задача про форму контракта. Публичный контракт после мерджа обратной правкой не -откатывается. +откатывается — поэтому сюда же приехала опечатка `transcibe` в тексте ошибки +приёма (нашёл проход `review-specs` на ревью change +`2026-08-11-fix-http-handler-tests`): в одиночку текст ошибки менять нельзя, +а здесь контракт переписывается целиком. diff --git a/tasks/items/metaviewer-adapter-tests.md b/tasks/items/metaviewer-adapter-tests.md new file mode 100644 index 0000000..c344e07 --- /dev/null +++ b/tasks/items/metaviewer-adapter-tests.md @@ -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` в проекте нет. Если такая проверка нужна, это своё +решение и своя задача. + diff --git a/tasks/items/no-user-filename-in-log.md b/tasks/items/no-user-filename-in-log.md new file mode 100644 index 0000000..609bc5d --- /dev/null +++ b/tasks/items/no-user-filename-in-log.md @@ -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` на месте. + +## Рамки + +Прочие журнальные строки проекта здесь не пересматриваются. Полная сверка +журнала с запретами — отдельная работа; эта задача закрывает названный +инвариантом случай. + diff --git a/tasks/items/orphan-file-on-failed-intake.md b/tasks/items/orphan-file-on-failed-intake.md new file mode 100644 index 0000000..87b6c36 --- /dev/null +++ b/tasks/items/orphan-file-on-failed-intake.md @@ -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` не меняется: имена и формат прежние. + +## Критерии приёмки + +- Отказ чтения метаданных не оставляет файла в каталоге хранения. Оракул — тест + на подставном источнике, возвращающем ошибку: каталог пуст после запроса. +- Отказ записи на диск на середине не оставляет частично записанного файла. + Оракул — тест с источником данных, обрывающимся на середине чтения. +- Уборка сама не заслоняет исходную ошибку: отправитель по-прежнему получает + отказ, а причина уезжает в журнал. Оракул — тот же тест, код ответа и запись в + перехваченном журнале. + +## Рамки + +Уборка файлов, осиротевших **до** этой правки, сюда не входит: их надо найти +сверкой каталога с учётом, и это отдельная работа с боевыми данными. + diff --git a/tasks/items/pipeline-step-tests.md b/tasks/items/pipeline-step-tests.md new file mode 100644 index 0000000..0cde118 --- /dev/null +++ b/tasks/items/pipeline-step-tests.md @@ -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): она может сменить модель очереди +целиком, и тесты, написанные под сегодняшний захват, придётся выбросить вместе с +ним. +