- шаги PocketBase переехали из файла в пакет internal/adapter/repo/pocketbase/migrations, файл на шаг с именем зарегистрированного шага; туда же имена коллекций, срок сессии — в provider.go - ключ migrations в docs/.docs.json наведён на этот каталог: прежнее значение указывало на несуществующий migrations/, и шаг гейта проходил зелёным при всякой правке схемы - app.go подключает пакет шагов явным пустым импортом: пропавшая ссылка на константы унесла бы регистрацию, и хранилище поднялось бы без коллекций
450 lines
41 KiB
Markdown
450 lines
41 KiB
Markdown
# Ревью: настройка и журнал
|
||
|
||
## Как настроен конвейер
|
||
|
||
Конвейер ревью прогонялся один раз — 2026-08-11, на изменении
|
||
`fix-http-handler-tests`; его триаж лежит в
|
||
`openspec/changes/archive/2026-08-11-fix-http-handler-tests/review/triage.md`.
|
||
Разделы ниже заполнены наперёд по коду и правятся по итогам прогонов: «Типовые
|
||
ложноположительные» первым прогоном уже пользовались.
|
||
|
||
### Типовые узлы
|
||
|
||
Рода узлов проекта и проверяемые свойства к каждому.
|
||
|
||
**Шаг конвейера** (`FindAndRunConversionJob`, `FindAndRunTranscribeJob`,
|
||
`FindAndRunTranscribeCheckJob`):
|
||
|
||
- отличает «задач нет» от отказа и не считает первое ошибкой;
|
||
- при отказе на середине оставляет задачу в состоянии, из которого повтор
|
||
корректен, либо переводит в `failed` осознанно;
|
||
- не теряет ссылку на файл: `job.FileID` переставляется только после того, как
|
||
запись о новом файле создана;
|
||
- повтор шага на той же задаче не создаёт лишних файлов и записей;
|
||
- отвечает пользователю ровно один раз.
|
||
|
||
**Транспорт** (`internal/controller/tg`, `internal/controller/http`):
|
||
|
||
- проверяет право отправителя до всякой работы;
|
||
- не логирует ошибку, которую уже залогировал доменный слой;
|
||
- переводит доменную ошибку в свой ответ, а не отдаёт сырой текст;
|
||
- закрывает то, что открыл, на всех ветках выхода.
|
||
|
||
**Клиент внешнего сервиса** (`adapter/recognizer/yandex`, `adapter/telegram`):
|
||
|
||
- имеет таймаут и не виснет, когда внешний сервис не отвечает;
|
||
- не кладёт секрет в URL и не даёт ему утечь через ошибку транспорта;
|
||
- различает «сервис ответил отказом» и «сервис недоступен»;
|
||
- вырожденный ответ (пустой, усечённый, без ожидаемого поля) не превращает в
|
||
успех молча.
|
||
|
||
**Репозиторий хранилища** (`internal/adapter/repo/pocketbase`; шаги схемы —
|
||
подпакетом `migrations`):
|
||
|
||
- список колонок совпадает во всех четырёх местах — `applyToRecord`,
|
||
`recordToJob`, `acquireColumns`, `acquiredRow` — и в шаге схемы (инвариант
|
||
[CLAUDE.md](../CLAUDE.md), «Инварианты»);
|
||
- захват задачи не выдаёт одну строку двум вызывающим, а результат пишет только
|
||
держатель захвата;
|
||
- репозиторий кладёт время в сыром запросе тем же видом, каким хранилище пишет
|
||
свои `created`/`updated` ([database.md](database.md), «Представление данных»);
|
||
- отказ хранилища не выходит наружу дословно: он несёт ключ файла целиком.
|
||
|
||
**Обёртка над внешним процессом** (`adapter/converter/ffmpeg`,
|
||
`adapter/metaviewer/ffmpeg`):
|
||
|
||
- отсутствие программы в `PATH` отличается от отказа обработки;
|
||
- вход, пришедший от пользователя, не попадает в аргументы командной строки
|
||
неразобранным;
|
||
- пустой или частично записанный выходной файл считается отказом;
|
||
- процесс не висит вечно.
|
||
|
||
**Любой узел** — сверх свойств своего рода:
|
||
|
||
- изменённое место покрыто хоть одним **проходящим** тестом. Тест, который
|
||
никогда не был зелёным, обнуляет сигнал всего пакета: настоящий отказ в нём
|
||
становится неотличим от привычного шума (журнал, запись 2026-08-10);
|
||
- проверка **способна упасть**. Утверждение, разбирающее ответ в ту же
|
||
структуру, чьи теги и составляют проверяемый контракт, меняется вместе с ним
|
||
и никогда не ловит поломку; такое судят по сырому виду ответа. Признак ищется
|
||
мутацией: сломай проверяемое свойство и убедись, что тест краснеет (журнал,
|
||
запись 2026-08-11);
|
||
- **то же и об оракуле критерия приёмки, не только о тесте.** Критерий, чей
|
||
единственный оракул — молчание линтера, годится ровно тогда, когда линтер
|
||
краснеет на **всех** негодных реализациях; проверяется той же мутацией.
|
||
Прецедент: «отказ `Close` не теряется молча» принимался молчанием `errcheck`,
|
||
а тот пропускал `_ = conn.Close()` — реализацию, теряющую отказ целиком
|
||
(журнал, запись 2026-08-11 про недостижимую норму; закрыто
|
||
[решением](adr/ADR-2026-08-11-errcheck-check-blank.md));
|
||
- **требование без сценария не имеет оракула** и потому не может быть нарушено
|
||
заметно. Норма, которую нечем уронить, расходится с кодом молча — и расходится
|
||
тем вернее, чем убедительнее написана (журнал, запись 2026-08-11).
|
||
|
||
### Типовые ложноположительные
|
||
|
||
- **«Воркер глотает ошибку `NoopJobError`».** Не дефект: этот тип означает «задач
|
||
в этом состоянии нет», и `internal/controller/worker/worker.go` намеренно не
|
||
логирует его и не считает в метрику. Норма записана требованием
|
||
[pipeline](../openspec/specs/pipeline/spec.md).
|
||
|
||
**Оговорка, и она тут главная:** ложноположительным считается только само
|
||
молчание воркера. Проверка **формы** узнавания ложноположительной не является:
|
||
приведение типа на этом месте — настоящий дефект, закрытый 2026-08-11 задачей
|
||
`errors-as-instead-of-typecast`. Появилось снова — это регрессия, и выбрасывать
|
||
её как известную нельзя.
|
||
- **«Захват задачи не в транзакции — гонка двух воркеров».** По построению её
|
||
нет: три воркера читают три разных состояния, и одну строку они не делят.
|
||
Механика захвата и её слабые места — [database.md](database.md),
|
||
«Представление данных». Находка становится настоящей ровно тогда, когда
|
||
появится второй экземпляр процесса или второй воркер на то же состояние.
|
||
- **«Файлы и объекты не удаляются, диск растёт».** Факт верный и записан в
|
||
[database.md](database.md); срок хранения не задан сознательно, задачи на него нет.
|
||
Новой находкой это не считается, пока не измерен рост.
|
||
- **«У записи нет владельца: вошедший видит чужие записи».** Не дефект и не
|
||
новость: приём, опрос и файл закрыты сессией OIDC с 2026-08-12, а
|
||
разграничения по владельцу нет сознательно — [security.md](security.md),
|
||
«Периметр», и `openspec/specs/access`, «Purpose». Находкой считается новая
|
||
поверхность, выставленная наружу, либо путь к содержимому записи **без**
|
||
сессии, а не повторение этого факта.
|
||
|
||
### Вопросы по темам
|
||
|
||
Форма: `<тема>: <вопрос> (<провенанс>)`.
|
||
|
||
- `operations`: пережил ли шаг конвейера отмену контекста на середине — воркеры
|
||
получают `ctx`, но ни один шаг его внутрь не передаёт (чтение `worker.go` и
|
||
`transcribe.go`, 2026-08-10).
|
||
- `operations`: появился ли таймаут у обращения к Telegram, S3 и SpeechKit — ни у
|
||
одного из них таймаута нет (чтение `tg.go`, `s3.go`, `speechkit.go`,
|
||
2026-08-10).
|
||
- `operations`: не удвоилась ли запись об одном сбое — шаг логирует ошибку и
|
||
возвращает её воркеру, который логирует снова (чтение `transcribe.go`,
|
||
2026-08-10).
|
||
- `security`: не попал ли в лог текст расшифровки, имя файла пользователя или
|
||
URL с токеном бота (запрет в [security.md](security.md) и
|
||
[conventions/logging.md](conventions/logging.md)).
|
||
- `security`: не строится ли путь на диске или ключ объекта из значения,
|
||
пришедшего снаружи, — расширение файла сегодня берётся из имени отправителя
|
||
(чтение `service/transcribe.go`, 2026-08-10).
|
||
- `security`: не уходит ли значение, пришедшее снаружи, меткой метрики — страница
|
||
метрик отдаётся без проверки отправителя, и метка это поверхность пошире
|
||
журнала (журнал, запись 2026-08-11 про хвост имени).
|
||
- `architecture`: не появился ли второй путь приёма мимо
|
||
`createTranscribeJob` — сегодня через него идут оба входа
|
||
([architecture.md](architecture.md), «Единые точки проекта»).
|
||
- `architecture`: не поехало ли поведение в `architecture.md` вместо спеки —
|
||
заведены две capability (`openspec/specs/intake` и `openspec/specs/pipeline`),
|
||
и каждая описана частично. Поведение прочих узлов живёт в обзоре под маркерами
|
||
долга, а соблазн дописать туда ещё — самый большой.
|
||
- `conventions`: новая колонка правится во всех четырёх местах репозитория
|
||
(CLAUDE.md, «Инварианты»).
|
||
- `autotests`: покрыт ли изменённый шаг конвейера хоть одним тестом — сегодня
|
||
тестов два файла, и оба мимо конвейера.
|
||
- `autotests`: судит ли проверка ответа по готовому ответу, а не по изменяемому
|
||
состоянию обработчика — класс всплывал трижды, последний раз 2026-08-12 на
|
||
уборке носителя состояния входа.
|
||
- `security`: не открылась ли снова поверхность, которую приносит хранилище, —
|
||
собственная регистрация, вход по паролю, одноразовый код, восстановление
|
||
доступа, продление сессии. Всё это приходит включённым и закрывается нами
|
||
(задача `oidc-login` 2026-08-12).
|
||
- `security`: не появился ли второй способ получить сессию к тому же человеку —
|
||
заголовок вместо куки назван осознанно, прочие способы обязаны быть закрыты.
|
||
- `operations`: доходит ли отзыв доступа у провайдера до сервиса и за какой срок —
|
||
после входа сервис к провайдеру не обращается, и канал здесь один
|
||
(ADR-2026-08-12-session-without-refresh).
|
||
- `architecture`: не зовётся ли на каждый запрос то, что меняет состояние
|
||
приложения, — сборка роутера хранилища оказалась именно такой.
|
||
|
||
### Триггеры метки
|
||
|
||
Проектная конкретизация правила выбора метки. Умолчание — `medium`.
|
||
|
||
**Крупное здесь** (поднимает до `large`, ось объёма):
|
||
|
||
- изменение, трогающее конвейер задач целиком: состояние, воркер, шаг сервиса и
|
||
колонку разом;
|
||
- замена хранилища или переход на PocketBase — любой её кусок;
|
||
- смена модели очереди: захват, повторы и воркеры разом;
|
||
- каркас приложения: сборка фронтенда, раздача статики и шаг гейта разом;
|
||
- изменение, трогающее оба входа сразу — Telegram и HTTP.
|
||
|
||
**Незнакомое здесь** (поднимает до `large`, ось формы решения):
|
||
|
||
- вход через OIDC и разграничение доступа: как связаны пользователь Telegram и
|
||
пользователь приложения, до начала работы назвать нельзя;
|
||
- всё, что делается на выбранном фреймворке впервые: форма решения нащупывается
|
||
по ходу, пока конвенция веб-UI пуста;
|
||
- установка на телефон: service worker перехватывает запросы, и что он кэширует,
|
||
до работы назвать нельзя;
|
||
- работа с записями в несколько часов: потолки внешних сервисов не замерены,
|
||
форма решения зависит от замера;
|
||
- приём дорожки из видео и форматов, которых `ffmpeg` не берёт текущей командой;
|
||
- всё, что требует записи в `research/` прежде, чем начать.
|
||
|
||
**Мелкое здесь** (опускает до `small`):
|
||
|
||
- правка текста, который видит пользователь Telegram;
|
||
- новая метрика в `internal/metrics`;
|
||
- правка `config.dist.toml` и умолчаний `defaultConfig()` без нового поля;
|
||
- правка документов канона.
|
||
|
||
Помни отрицательный тест: миграция, формат файла на диске, публичный контракт
|
||
API и имя не откатываются обратной правкой после мерджа — какими бы маленькими
|
||
ни были, они не `small`.
|
||
|
||
### Недоступно проверке
|
||
|
||
**Не проверит ни один проход:**
|
||
|
||
- `operations`: поведение внешних сервисов под нагрузкой и на границах —
|
||
SpeechKit и Object Storage поднять в тесте нечем;
|
||
- `operations`: реальный профиль нагрузки. Проект работает на единицах записей в
|
||
день, и утверждения о росте остаются условиями, а не замерами;
|
||
- `security`: стойкость `ffmpeg` к вредоносному входу — разбор чужого формата
|
||
отдан внешней программе, и она вне нашей границы;
|
||
- `security`: поведение настоящей Authelia и её правило на нашего клиента.
|
||
Провайдера в прогоне нет, подменяет его свой сервер; кто допущен — настройка
|
||
выкладки вне репозитория, и по коду её не проверить
|
||
([adr/ADR-2026-08-12-access-delegated-to-provider.md](adr/ADR-2026-08-12-access-delegated-to-provider.md));
|
||
- `security`: поведение браузера с куками — применение `SameSite`, приём
|
||
`Set-Cookie` при переходе с чужого сайта. Браузера в прогоне нет, и находки
|
||
этого рода остаются гипотезами.
|
||
|
||
**Перестали проверять сознательно:**
|
||
|
||
- `autotests`: разбор вывода настоящего `ffprobe`. Проверки приёма звали его до
|
||
2026-08-11 — правда, звали так, что он всегда отказывал, — а теперь получают
|
||
длительность от подставного источника. Своего теста у
|
||
`adapter/metaviewer/ffmpeg` нет; решение и его цена — в
|
||
[adr/ADR-2026-08-11-stub-adapters-in-tests.md](adr/ADR-2026-08-11-stub-adapters-in-tests.md);
|
||
- **всё, что требует поднять сервис целиком.** Локальный запуск роняет адаптер
|
||
Telegram: он проверяет токен обращением к Telegram, а боевым токеном
|
||
запускаться запрещено. Значит поведенческая верификация живым прогоном
|
||
недоступна ни одной задаче, и заменяют её проверки поверх настоящего роутера
|
||
хранилища. Замечено 2026-08-12 задачей `oidc-login`; своей задачи на это пока
|
||
нет.
|
||
|
||
## Журнал дефектов
|
||
|
||
Верхняя запись найдена конвейером ревью на первом же его прогоне, вторая —
|
||
прогоном гейта при заведении канона 2026-08-10, две нижние восстановлены по
|
||
истории git тогда же. Три нижние помечены `проскочил`: ревью тогда не было, и
|
||
поймать их было некому. У восстановленных нет поля «Чем воспроизведён», и
|
||
выдумывать его задним числом нельзя.
|
||
|
||
## 2026-08-12 — закрыли поверхность так, что войти не мог никто [пойман ревью]
|
||
|
||
- **Где:** шаг схемы `202608120001` задачи `oidc-login`, правило создания записи
|
||
в коллекции пользователей
|
||
- **Симптом:** `users.CreateRule = nil` закрывало создание записи для всех, кроме
|
||
владельца панели. Запись при первом входе заводит внутренний запрос самого
|
||
обмена, идущий без таких прав, — значит после выкладки вход не сработал бы ни
|
||
у кого, включая владельца, а приём и опрос уже были закрыты. Сервис остался бы
|
||
доступен только через Telegram, и чинилось бы это руками в панели
|
||
- **Причина:** закрывали ровно то, ради чего задача затевалась, — самостоятельную
|
||
регистрацию, которую хранилище приносит открытой. Глухое `nil` выглядит самым
|
||
надёжным её закрытием и отвергает заодно единственный законный путь заведения
|
||
записи. Различить их можно: обмен помечает свой запрос контекстом `oauth2`
|
||
- **Чем воспроизведён:** тестом против настоящего хранилища с подставным
|
||
провайдером: возврат от провайдера отвечал `401`, обращений к токен-эндпоинту
|
||
`1`, учётных записей после входа `0`. Причина изолирована тем же прогоном —
|
||
с открытым правилом возврат давал `302` и запись появлялась
|
||
- **Почему не поймали раньше:** все проверки задачи заводили учётную запись
|
||
прямым сохранением, мимо входа, и потому шли по коду, который в бою не
|
||
исполняется. Гейт был зелёным. Поймали два прохода независимо — разбор кода по
|
||
исходникам библиотеки и враждебный проход падающим тестом
|
||
- **Что меняем:** правило сузили до контекста обмена
|
||
(`@request.context = "oauth2"`), а в набор проверок добавили вход целиком через
|
||
подставного провайдера — от увода до куки сессии. Проверка, заводящая запись
|
||
мимо входа, больше не считается покрытием входа
|
||
|
||
## 2026-08-12 — проверка не могла упасть: читала живую карту заголовков вместо ответа [пойман ревью]
|
||
|
||
- **Где:** `internal/controller/http/auth_test.go`, проверка уборки носителя
|
||
состояния входа; сам дефект — в `auth.go`, уборка стояла в `defer`
|
||
- **Симптом:** носитель состояния и проверочного кода не убирался ни на успешном
|
||
возврате, ни на отказном, и жил свои десять минут. Одноразовость возврата
|
||
держалась ровно на этой уборке, то есть тоже не работала. Проверка при этом
|
||
была зелёной и утверждала обратное
|
||
- **Причина:** двойная. В коде — `defer` исполняется после того, как ответ уже
|
||
начали писать, а заголовки к этому моменту зафиксированы снимком, и позднейшая
|
||
правка их карты до браузера не доезжает. В проверке — `httptest` устроен
|
||
зеркально: `Header()` отдаёт живую карту, а снимок лежит отдельно и читается
|
||
через `Result()`. Проверка смотрела в живую карту и видела то, чего клиент не
|
||
получит
|
||
- **Чем воспроизведён:** отдельной программой вне проекта: на настоящем сервере
|
||
ответ приходил с пустым `Set-Cookie`, а тот же обработчик под `httptest`
|
||
показывал куку в `Header()` и не показывал в `Result()`
|
||
- **Почему не поймали раньше:** оракул был ложным по построению, и никакая
|
||
регрессия его не разбудила бы. Гейт зелёный. Поймали два прохода — сверка
|
||
требований и разбор кода, — оба воспроизведением, а не чтением
|
||
- **Что меняем:** уборка перенесена до записи ответа; все проверки этого файла
|
||
судят по `Result()`. Класс всплывает **третий раз** (2026-08-10 «тесты
|
||
http-обработчика ни разу не были зелёными», 2026-08-11 «проверка приёма не
|
||
могла упасть»), поэтому он же уходит кандидатом в конвенции: проверка ответа
|
||
судит по готовому ответу, а не по изменяемому состоянию обработчика
|
||
|
||
## 2026-08-12 — каждый анонимный запрос навсегда замедлял запись в хранилище [пойман ревью]
|
||
|
||
- **Где:** `internal/controller/http/auth.go`, обмен кода собирал роутер
|
||
хранилища на каждый вызов
|
||
- **Симптом:** сборка роутера вешает девять обработчиков на само приложение и
|
||
без идентификатора, поэтому повторная не заменяет прежние, а добавляет.
|
||
Обработчики исполняются на каждой записи в хранилище, а конвейер пишет задачу на
|
||
каждом шаге. Освобождения нет — только перезапуск. Раскачивалось анонимно:
|
||
атакующий ставит себе куку состояния сам, и сверка сравнивает две его же
|
||
величины, а обмен исполняется раньше обращения к провайдеру
|
||
- **Причина:** функция сборки выглядит чистой — она возвращает роутер, и по имени
|
||
не видно, что она правит приложение. Решение звать собственный адрес хранилища
|
||
внутри процесса сделало эту сборку частью горячего пути
|
||
- **Чем воспроизведён:** замером на настоящем приложении: пять вызовов подряд
|
||
подняли число обработчиков одного события с 4 до 14; 3000 анонимных возвратов
|
||
довели сотню сохранений записи с 3.86 мс до 59.8 мс и кучу на 5013 КиБ. При
|
||
недоступном провайдере утечка сохранялась
|
||
- **Почему не поймали раньше:** ни один шаг гейта не смотрит на побочные эффекты
|
||
вызова библиотеки, а замер требует прогона. Поймали три прохода — архитектурный
|
||
зондом, враждебный падающим тестом, сверка требований чтением
|
||
- **Что меняем:** роутер собирается один раз и живёт полем обработчика; в набор
|
||
проверок добавлена та, что считает длину очереди обработчиков после двадцати
|
||
входов
|
||
|
||
## 2026-08-12 — образ не собирался, и этого не увидел никто [проскочил]
|
||
|
||
**Что сломалось.** `go mod tidy` поднял директиву `go` в `go.mod` до `1.25.0` —
|
||
её требует PocketBase, — а `Dockerfile` продолжал собирать на `golang:1.24-alpine`
|
||
с `GOTOOLCHAIN=local`. `task image` упал бы на шаге сборки: выкладки задачи
|
||
`pocketbase-storage` не существовало бы вовсе.
|
||
|
||
**Почему не поймали.** Все шесть проходов ревью и весь гейт видели зелёное:
|
||
`go build ./...` идёт на хостовом Go, а образ не собирает **ни один шаг гейта**.
|
||
Расхождение выглядело согласованным ещё и потому, что `CLAUDE.md` и `README.md`
|
||
обещали Go 1.24 — то есть три места из четырёх говорили одно и то же, и неверными
|
||
были именно они.
|
||
|
||
Нашлось не проходом, а триажем — при проверке чужих починок на месте, когда он
|
||
собрал образ руками. То есть поймано случайным свойством прогона, а не
|
||
устройством конвейера: проверь триаж починки чтением, дефект уехал бы в мердж.
|
||
|
||
**Чем чинится на будущее.** Сборка образа гейтом не проверяется намеренно —
|
||
дорого. Дешёвая замена: шаг, сверяющий версию сборщика в `Dockerfile` с
|
||
директивой `go` в `go.mod`. Строкой сравнения, без docker. Заведено урожаем
|
||
ревью.
|
||
|
||
**Закрыто** задачей `go-1-26-upgrade` 2026-08-12: шаг `go-version` в `task gate`
|
||
(`scripts/check-go-version.sh`). Сверяются четыре места, а не два, — `go.mod`,
|
||
`Dockerfile`, `CLAUDE.md`, `README.md`: в этом дефекте трое из четырёх врали
|
||
согласованно, и парная сверка не увидела бы документ, разошедшийся с
|
||
согласованным кодом. Норма — capability `toolchain`.
|
||
|
||
## 2026-08-11 — норма требовала от сервиса недостижимого [пойман ревью]
|
||
|
||
- **Где:** дельта-спека `pipeline` задачи `errors-as-instead-of-typecast`, абзац
|
||
об отказе шага
|
||
- **Симптом:** требование гласило «отказ MUST быть записан **ровно один раз**
|
||
единственной логирующей точкой». Сервис пишет дважды — сначала шаг конвейера,
|
||
следом воркер, — то есть норма не выполнялась бы с первого дня, а после
|
||
архивации стала бы посылкой для следующих задач
|
||
- **Причина:** дефект родился при починке соседнего. Первая редакция назначала
|
||
логирующей точкой воркера и фиксировала уровень `ERROR`, чем закрепляла
|
||
контрактом долг `conventions/logging.md`. Правка по этой находке ушла в
|
||
противоположную крайность: вместо «норма молчит о числе записей» получилось
|
||
«норма требует одной». Двойная запись — записанный системный долг, и обе
|
||
редакции с ним расходились, только в разные стороны
|
||
- **Чем воспроизведён:** прогоном пробы через `go test -overlay`: один отказ
|
||
хранилища даёт две записи — `Failed to find and acquire job` из шага и
|
||
`Worker error` из воркера
|
||
- **Почему не поймали раньше:** требование не имело сценария, а значит и оракула
|
||
— упасть ему было нечем. Ревью дизайна абзац читало, но код с ним не сверяло:
|
||
кода тогда не существовало. Поймал проход `specs` на ревью кода, направлением
|
||
`code → spec`, и поймал прогоном, а не чтением
|
||
- **Что меняем:** норма говорит только проверяемое сегодня — отказ виден
|
||
владельцу и засчитан в счётчик; число записей и уровень названы долгом с
|
||
адресом. В критерии приёмки добавлена строка: норма не объявляет обязательным
|
||
недостижимое — ни в ту, ни в другую сторону
|
||
|
||
## 2026-08-11 — хвост имени отправителя уезжал на открытую страницу метрик [пойман ревью]
|
||
|
||
- **Где:** `internal/service/transcribe.go`, метки `file_extension` у размера
|
||
принятой записи и `source_format` у длительности конвертации
|
||
- **Симптом:** имя `запись.тайное-слово` клало `тайное-слово` меткой метрики, а
|
||
`GET /metrics` отдаётся без проверки отправителя. Тем же каналом множеством значений
|
||
метки распоряжался анонимный отправитель
|
||
- **Причина:** расширение берётся из имени отправителя дословно (`filepath.Ext`)
|
||
и употреблялось меткой без приведения. Канал старше задачи, которая его нашла
|
||
- **Чем воспроизведён:** прогон `filepath.Ext` на именах вида
|
||
`запись.тайное-слово`, `Разговор с Петровым 11.08`, затем чтение реестра
|
||
метрик после приёма — метка несла хвост дословно
|
||
- **Почему не поймали:** метрику никто не считал выходом приватного значения.
|
||
Тема `security` смотрела журнал, ответ и пути на диске; вопроса про метку в
|
||
перечне вопросов не было, и ни один проход её не открывал. Поймали три прохода
|
||
разом на задаче, которая закрывала соседний канал
|
||
- **Что меняем:** вопрос про метку добавлен в «Вопросы по темам»; правило
|
||
приведения нормировано спекой `intake` и записано
|
||
[решением](adr/ADR-2026-08-11-known-format-label.md)
|
||
|
||
## 2026-08-11 — проверка приёма не могла упасть [пойман ревью]
|
||
|
||
- **Где:** `internal/controller/http/transcribe_test.go`, случай успеха приёма
|
||
- **Симптом:** тест не поймал ни одного настоящего дефекта приёма, хотя был
|
||
зелёным и выглядел содержательным
|
||
- **Причина:** две штуки одного рода. Тест разбирал ответ в
|
||
`CreateTranscribeJobResponse` — ту самую структуру, чьи теги `json` и
|
||
составляют публичный контракт: переименование тега меняло и проверяемое, и
|
||
ожидаемое разом. И заведение задачи тест подтверждал только эхом ответа, а не
|
||
чтением базы
|
||
- **Чем воспроизведён:** мутацией. Замена тега на `json:"jobId"` и удаление
|
||
`s.jobRepo.Create(job)` из `internal/service/transcribe.go` — тесты в обоих
|
||
случаях оставались зелёными; после правки обе мутации их роняют
|
||
- **Почему не поймали:** проверки писались тем же заходом, что и правились, а
|
||
«зелено» на новом тесте читается как подтверждение. Поймал проход `specs`
|
||
ревью кода, и поймал ровно тем, что добыл оракул мутацией, а не рассуждением
|
||
- **Что меняем:** успех судится по сырому JSON и по строке в базе. В типовые
|
||
узлы, «Любой узел», добавлено свойство «проверка способна упасть» с указанием
|
||
на мутацию как способ его проверить
|
||
|
||
## 2026-08-10 — тесты http-обработчика ни разу не были зелёными [проскочил]
|
||
|
||
- **Где:** `internal/controller/http/transcribe_test.go`
|
||
- **Симптом:** `go test ./...` падает четырьмя случаями; обнаружено первым же
|
||
прогоном гейта при заведении канона
|
||
- **Причина:** тест требует `testdata/sample.m4a`, которого в репозитории нет и
|
||
не могло быть — `.gitignore` содержит `*.m4a`. Остальные случаи записывают в
|
||
файл строку `test audio content` и ждут `201`, а обработчик зовёт настоящий
|
||
`ffprobe`, который такой вход отвергает
|
||
- **Чем воспроизведён:** `go test ./internal/controller/http/` — четыре отказа,
|
||
из них один по отсутствию файла и три по коду `500` вместо `201`
|
||
- **Почему не поймали:** гейта не было вовсе, а `go test` руками, судя по
|
||
результату, не гоняли ни разу с коммита `87d8b05`
|
||
- **Что меняем:** заведена задача `http-handler-tests-never-green`; в гейт
|
||
добавлен шаг `go test ./...`, и красный тест теперь виден. Настоящий остаток
|
||
шире: **тест, который никогда не проходил, обнуляет сигнал всего пакета** — в
|
||
типовые узлы добавлено свойство «покрыт хоть одним проходящим тестом», а в
|
||
вопросы темы `autotests` — вопрос про изменённый шаг конвейера
|
||
- **Закрыт** 2026-08-11: проверки переписаны, `go test ./...` зелёный и из
|
||
списка объявленных долгов в [CLAUDE.md](../CLAUDE.md) снят
|
||
|
||
## 2025-10-23 — пустой ответ вместо текста расшифровки [проскочил]
|
||
|
||
- **Где:** `internal/service/transcribe.go`, ветка завершения задачи
|
||
- **Симптом:** пользователь Telegram получал пустое сообщение вместо текста
|
||
- **Причина:** SpeechKit возвращал операцию успешной, но с пустым текстом, и
|
||
задача завершалась этим пустым значением
|
||
- **Чем воспроизведён:** восстановлено по коммиту `ec637c0`, оракула нет
|
||
- **Почему не поймали:** конвейера ревью не существовало
|
||
- **Что меняем:** уже сделано — пустой текст подменяется фразой «на записи нет
|
||
текста». Настоящий остаток в другом: свойство «вырожденный ответ внешнего
|
||
сервиса не превращается в успех молча» вынесено в типовой узел «клиент
|
||
внешнего сервиса» выше
|
||
|
||
## 2025-08-17 — длинная расшифровка не доходила до пользователя [проскочил]
|
||
|
||
- **Где:** `internal/adapter/telegram/sender.go`
|
||
- **Симптом:** отправка текста длиннее предела сообщения Telegram завершалась ошибкой
|
||
целиком, пользователь не получал ничего
|
||
- **Причина:** предел длины сообщения на стороне Telegram не учитывался
|
||
- **Чем воспроизведён:** восстановлено по коммиту `822e168`, который тем же
|
||
заходом завёл `internal/adapter/telegram/split_test.go`
|
||
- **Почему не поймали:** конвейера ревью не существовало
|
||
- **Что меняем:** уже сделано — деление по словам с пределом 4000 символов,
|
||
число записано в [database.md](database.md)
|