- проверки больше не зовут ffprobe и не меняют рабочий каталог процесса; добавлены случаи на отказ чтения метаданных и на отсутствие поля audio - заведена спека intake на приём по HTTP, ADR о подставных адаптерах, запись в журнал ревью о проверке, которая не могла упасть - go test снят из объявленных долгов CLAUDE.md, послабление errcheck для _test.go в .golangci.yml убрано
34 KiB
Триаж ревью: fix-http-handler-tests
Сводка
- Размер: малое. Сложность: знакомое. Метка:
small. Обоснование разметки (review-scope): изменение трогает один тестовый файл и одну строку конфига линтера, поведение сервиса не меняется, отрицательный тест метки (миграция, формат файла на диске, публичный контракт API, имя ключа) не срабатывает — контракт HTTP API спекой фиксируется, а не меняется. - Режим прогона: по графу. Дизайн — одна стадия (
specs), код —autotests→specs+code→ триаж. - Сигнал о заниженной метке: пришёл от
review-code— возражений нет, меткуsmallпроход счёл обоснованной.review-basicsне запускался (своих тем у проекта нет), второго независимого подтверждения метки нет. - Состояние гейта (проверено мной на этом прогоне):
go test ./...— зелёный (ok internal/controller/http 0.013s); это и есть цель изменения;golangci-lint run— 4 замечания, ровно объявленный долг (speechkit.go:55,main.go:124,worker.go:51,service/transcribe.go:394). Ни одного нового, в том числе после снятия исключенияerrcheckдля_test.go;- критерий приёмки «не зависит от
ffprobe» подтверждён: собранныйgo test -cбинарь проходит подenv -i PATH=<пустой каталог>; - критерий «
os.Chdirне остаётся» подтверждён:grep -rn 'os.Chdir' internal/пуст. - Итог: гейт красный только унаследованным долгом; новых красных шагов нет.
План разметки с исходом по каждой теме
| тема | дом | глубина | закрывает | исход |
|---|---|---|---|---|
| requirements | openspec/changes/fix-http-handler-tests/specs/intake/spec.md |
сверка | specs |
закрыта, 3 находки (потолок 3/3 сработал), 2 из них починены и мной перепроверены мутацией |
| autotests | CLAUDE.md § Гейт |
— | autotests |
закрыта, 0 находок; отчёт о гейте + 1 строка в границы покрытия. О своём потолке проход не сообщил |
| conventions | docs/conventions/README.md (на small — только README) |
сверка | code |
закрыта, 1 находка (потолок 1/2), не починена — единственный блокер ниже |
| architecture | CLAUDE.md § Инварианты |
сверка | code |
закрыта, 0 находок (потолок инвариантов 0/1) |
| security | CLAUDE.md § Инварианты |
сверка | code |
закрыта, 0 находок — но см. предупреждение ниже |
| operations | CLAUDE.md § Инварианты |
сверка | code |
закрыта, 0 находок |
Тем без отчёта нет. Тем без дома нет.
Предупреждение по теме security. «0 находок» здесь не значит «чисто».
Нарушение critical-инварианта «содержимое записи остаётся приватным»
(internal/service/transcribe.go:107 пишет "file_name", fileName — имя файла
пользователя — в журнал) найдено на стадии дизайна и сознательно отложено
решением человека на чекпоинте, поэтому проход кода вернул пустой итог: находка
уже известна и вынесена. Она в урожае, не в блокерах, и это решение человека, а не
моё. Тот же путь проходят записи из Telegram.
Арифметика
- На входе: 14 позиций — 7 именованных находок (
specs3,code4), 3 названные ниже потолка, 3 отложенные решением человека, 1 замечание о непокрытом конвейере отautotests. - После дедупликации, добычи оракулов и отсева: 2 позиции, требующие действия (1 блокер + 1 развилка). Остальное — в урожай, гипотезы и promote, ничего не выброшено молча.
- Починенное проверено мной независимо: обе
major-находкиspecsзакрыты, мутации их роняют (оракулы ниже).
Блокирует мердж
CLAUDE.md продолжает объявлять долгом отказ, которого больше нет, — и следующий настоящий отказ тестов приёма спишут на него молча
- Файл:
CLAUDE.md:96-101; сопутствующее:tasks/BACKLOG.md:23,tasks/items/http-handler-tests-never-green.md - Severity: major
- Confidence: high
- Действие: инлайн
- Оракул (мой, на этом прогоне):
- дословно
CLAUDE.md:98-101: «go test ./...падает вinternal/controller/http: тесты требуютtestdata/sample.m4a, которого в репозитории нет и не было… Заведено задачейhttp-handler-tests-never-green»; go test ./...→ok git.vakhrushev.me/av/transcriber/internal/controller/http 0.013s;- дословно
CLAUDE.md§ Работа: «Два объявленных долга из раздела „Гейт“ сломанным состоянием не считаются, пока их не закрыли задачами».
- дословно
- Последствие: после мерджа проект будет письменно утверждать, что красный
go testвinternal/controller/http— это норма. Ровно этот механизм записан в журнале дефектов (docs/review.md, запись 2026-08-10): «тест, который никогда не проходил, обнуляет сигнал всего пакета: настоящий отказ в нём становится неотличим от привычного шума». Изменение восстанавливает сигнал в коде и оставляет его выключенным в документе, по которому судят «сломано ли». Отказ будет молчаливым: никто не станет разбираться в отказе, объявленном известным. - Предложение: убрать первый из двух известных отказов в
CLAUDE.md§ Гейт (остаётся толькоgolangci-lint), закрыть задачу штатным путём каталога (tasks.py close— реализованные вREJECTED.mdне идут, у них есть коммит), снять строку изtasks/BACKLOG.md. Задачаtasks.mdэтого шага не содержит — добавить его в чек-лист. - Найдено проходом:
review-code/конвенции; оракул и провенанс — триаж. - Почему блокер, а не «стоит исправить»:
CLAUDE.md§ Работа — единственное место, где записано, что считается сломанным. Пока оно врёт, определение сделанного у следующей задачи опирается на неверный список. Правка механическая, путь документирован, цена — минуты.
Замечание о разделении обязанностей. Общая согласованность документов между
собой и с кодом — работа скилла av-dev-docs:healthcheck, а не ревью
(CLAUDE.md § Гейт говорит это прямо). Здесь исключение узкое и обосновано: речь
не о дрейфе документации вообще, а о том, что это самое изменение закрывает
долг, поимённо перечисленный в CLAUDE.md, и без правки определение «сломано»
становится ложным в момент мерджа.
Стоит исправить сейчас
Переименование маршрута POST /api/audio в main.go уедет зелёным: тест ходит по своей копии регистрации
- Файл:
main.go:198-202противinternal/controller/http/transcribe_test.go:140-144 - Severity: minor
- Confidence: high
- Действие: развилка
- Оракул (мой, мутация в копии дерева,
/tmp/.../scratchpad/mut):api.POST("/audio", …)→api.POST("/upload", …)вmain.go;go build ./...проходит,go test ./internal/controller/http/ -count=1→ok … 0.017s. Тест зелёный при сломанном контракте. Для сравнения — то, что теперь ловится: переименование тегаjson:"job_id"→json:"jobId"роняетTestCreateTranscribeJob_Success(«does not contain "job_id"»), удалениеs.jobRepo.Create(job)роняет его же. - Последствие: имена полей ответа изменение защитило (это и была починенная
major-находка), а путь маршрута — часть того же публичного контракта HTTP API, объявленного вCLAUDE.mdнеобратимым, — остался незащищённым. Внешняя программа сломается молча, машина промолчит. Вероятность невысока (мутация видна в диффеmain.go), но класс тот же самый. - Развилка для человека — правка трогает продуктовый код, а
design.mdобъявил это Non-Goal («кодinternal/serviceиinternal/controller/httpне правится»):- Вынести регистрацию маршрутов в экспортируемую функцию пакета
internal/controller/http(например,RegisterRoutes(r gin.IRouter, h *TranscribeHandler)), звать её изmain.goи из сборки теста. Цена: ~10 строк продуктового кода, выход за объявленный Non-Goal задачи, зато контракт маршрута закрыт машиной. - Оставить как есть, записать в урожай отдельной задачей. Цена: контракт
маршрута остаётся на человеке до следующей задачи, которая и так трогает
main.go(например,json-api-for-spa). - Оставить как есть и не заводить задачу, приняв, что маршрут проверяется глазами. Цена: класс дефекта известен, но не записан нигде — при следующем промахе оракула не будет.
- Вынести регистрацию маршрутов в экспортируемую функцию пакета
- Найдено проходом:
review-specs(там —minor, «цена исправления выше цены дефекта»); оракул мутацией — триаж.
Второго пункта в этой секции нет: остальное либо починено и перепроверено, либо не имеет цены, оправдывающей правку (см. «Отсеяно» и «Урожай»).
Гипотезы без доказательства
Понижено оракулом: «зелёный прогон печатает ERROR-строки в stderr» — заявленного последствия нет
- Исходно:
review-code/техника,minor. Часть починена (логгер сборки теста уведён вio.Discard), остаток —log.Printf("Err: %v", err)вinternal/controller/http/transcribe.go:45. - Мой оракул:
go test ./internal/controller/http/ -count=1 2>&1 | grep -c 'Err:'→ 0.go testбуферизует вывод пакета и на успехе его не печатает. Строка видна только под-vили при прямом запуске собранного бинаря — то есть на зелёномtask gateпризнак ничем не размывается. - Итог: последствие в формулировке находки не воспроизводится, находка снята.
Остаётся факт «продуктовый код пишет мимо
slog» — он уже записан вdocs/conventions/logging.md:161как расхождение, новой находкой не является, ушёл в promote (механизация правила).
Понижено: «база :memory: без ограничения пула»
- Исходно:
review-code/техника,minor,Confidence: low, «сегодня не срабатывает». Оракула, показывающего отказ, нет ни у прохода, ни у меня. Починка (db.SetMaxOpenConns(1), одна строка, с комментарием почему) уже внесена, безвредна и оставлена как есть. Действия не требует.
Не понижалось, но перепроверено: две major-находки specs
Обе были заявлены с оракулом и обе починены. Я не поверил на слово и повторил мутации в копии дерева — см. оракул в секции «Стоит исправить сейчас». Обе мутации теперь роняют тест. Находки закрыты.
Отсеяно
- Мёртвая строка
router.MaxMultipartMemoryв сборке теста (transcribe_test.go:138, названоreview-codeниже потолка). Проверено: гиновскийMaxMultipartMemoryчитается только вc.FormFile/c.MultipartForm, а обработчик зовётc.Request.FormFile, который использует собственныйdefaultMaxMemory= 32 MiB — то же число. Поведение не меняется ни в тесте, ни вmain.go, стоимость следующего изменения не растёт, записанной конвенции нет. Выброшено, а не смягчено. - Проектных ложноположительных (
docs/review.md→ «Типовые ложноположительные», 4 пункта) в выводах не оказалось ни одного. Ближайший сосед — «Файлы и объекты не удаляются, диск растёт» — к отложенной находке про файл-сироту не относится: та запись про отсутствие срока хранения, а находка — про файл, на который нет ни задачи, ни записи в учёте. По этому пункту ничего не отсеяно.
Promote candidates
- Имена полей публичного ответа судятся по сырому JSON, а не по разобранной
структуре. Приём в разобранную структуру переименовывает тег вместе с
ожиданием, и проверка теряет способность упасть — это ровно та
major, что нашлась здесь. Приём (map[string]json.RawMessage+assert.Contains) сработал дважды в одном файле. Дома у правила пока нет: вdocs/conventions/файла про тесты нет. Кандидат в новый раздел конвенций. - Стандартный
logв продуктовом коде — механизировать, а не помнить.docs/conventions/logging.mdуже пишет «Механизировано: ничего. Ниsloglint, ниforbidigoв.golangci.ymlне заведено» и поимённо называет расхождениеinternal/controller/http/transcribe.go. Правило записано и механизируемо, значит это не находка ревью, аPromote candidate:forbidigoнаlog.в.golangci.yml. - Регистрация маршрутов — одна на процесс и на тест (производное от развилки выше; актуально, если человек выберет вариант 2 или 3).
Урожай
Формулировка → оракул → провенанс. Ничего из этого не чинится в этом изменении.
- Приём пишет имя файла пользователя в журнал — нарушение
critical-инварианта.internal/service/transcribe.go:107:s.logger.Info("Creating transcribe job", "file_id", …, "file_name", fileName, …). Оракул: дословноCLAUDE.md§ Инварианты — «Содержимое записи остаётся приватным. Текст расшифровки, имя файла пользователя и его сообщение в лог не пишутся — только длина и идентификаторы. Нарушение необратимо: строки уже уехали в журнал контейнера. critical». Плюс вопрос темыsecurityвdocs/review.md. Путь общий с Telegram, то есть касается живых записей. Провенанс: ревью дизайна; отложено решением человека на чекпоинте, записано вdesign.md§ Risks. Спекаintakeнормирует только хранилище и о журнале молчит намеренно. - Отказ чтения метаданных оставляет файл на диске без уборки.
internal/service/transcribe.go:129-133(веткаmetaviewer.GetInfo) против152-157(веткаfileRepo.Create, гдеos.Removeесть). Оракул: чтение кода; ни задачи, ни записи в учёте под такой файл нет — сопоставить его не с чем. Провенанс: ревью дизайна; отложено решением человека (правка трогает три ветки отказа и меняет поведение на диске). - Настоящий
ffprobeпосле этой правки не проверяется ничем. Уinternal/adapter/metaviewer/ffmpegсвоего теста нет; разбор выводаffprobeне покрыт. Оракул:go test ./...→? …/adapter/metaviewer/ffmpeg [no test files]. Провенанс:design.md§ Risks, названо прямо. Формально покрытие не потеряно — прежний тест проверял отказffprobeи выдавал его за проверку приёма. - Конвейер задач не покрыт ни одним тестом.
FindAndRunConversionJob,FindAndRunTranscribeJob,FindAndRunTranscribeCheckJob,internal/controller/worker. Три воркера читают общий*sql.DB, теста с параллельным доступом нет. Оракул:go test ./...→[no test files]уinternal/serviceиinternal/controller/worker. Провенанс:autotests, вне scope задачи. Совпадает со свойством, добавленным вdocs/review.md(«изменённое место покрыто хоть одним проходящим тестом») и с вопросом темыautotests. - Опечатка
transcibeв тексте ошибки теперь закреплена проверкой.internal/controller/http/transcribe.go:46иtranscribe_test.go:379—"Failed to create transcibe job". Оракул: обе строки дословно. Исправление меняет тело ответа, то есть публичный контракт HTTP API, объявленный вCLAUDE.mdнеобратимым, — значит спрашивается у человека и не делается походя. Провенанс:review-specs, ниже потолка. Действия сейчас не требует: статус-кво зафиксирован сознательно. - Требование «имя отправителя не попадает в хранилище» проверяется только
благополучными именами. Проверено мной попутно (вопрос темы
securityизdocs/review.md: «не строится ли путь на диске из значения, пришедшего снаружи»): обхода каталога нет по построению —filepath.Extне пересекает разделитель пути, и на входах../../../etc/passwd.m4a,evil.m4a/../../x,/etc/passwd,a.b/../../c,..результат всегда<uuid><ext>внутри каталога хранения (оракул: прогонfilepath.Ext+filepath.Joinна этих восьми входах, вывод снят на этом прогоне). Дефекта нет; недостающее — сторожевой случай, который зафиксирует это свойство. Провенанс: триаж.
Границы покрытия
План: темы, дома, глубины
Полностью воспроизведён в сводке выше вместе с исходом каждой темы. Тем без дома
нет, тем без отчёта нет. Дома тем architecture, security и operations —
раздел «Инварианты» CLAUDE.md, глубина «сверка».
Какие проходы запускались
- Ревью дизайна:
review-specs, одна стадия, меткаsmall. - Ревью кода:
review-autotests→review-specs+review-code→ триаж. Режим — по графу, меткаsmall. - Не запускался
review-basics: своих тем у проекта нет — так сказал план. Следствие названо ниже, в строке про корректор метки.
Сработавшие потолки
review-specs(код): 3 из 3, потолок сработал. Ниже среза остались названными: литералы сообщений об ошибке, включая опечаткуtranscibe; расхождение сценария «Поля с записью нет» с тем, что делал тест (починено).review-code: техника 3/3 — сработал; конвенции 1/2; инварианты 0/1. Ниже среза осталась названной мёртвая строкаrouter.MaxMultipartMemory.review-autotests: о своём потолке не сообщил. Это находка о прогоне: по контракту проход обязан сказать, сколько нашёл, каков был потолок и что осталось за срезом. Судить, есть ли за его срезом что-то ещё, нечем.- Триаж: потолок 3/4 не исчерпан (1 блокер, 1 в «стоит исправить»). Из-за потолка ничего не выброшено.
Что каждый запущенный проход не мог проверить в принципе
Ниже — по отчётам проходов; charter'ы агентов мне дословно не подавались, поэтому это пересказ их собственных заявлений, а не цитата устава.
review-autotests: судит наличие и зелёность проверок, а не правильность нормы, которую они проверяют. Прогналgo test5× подряд и с-race— флаки не обнаружен; это отсутствие сигнала на пяти прогонах, а не доказательство детерминированности.review-specs: судит соответствие кода дельта-спеке; правильность самой спеки вне его входа. Приём из Telegram спекойintakeне описан сознательно — значит, и не проверялся.review-code: на меткеsmallконвенции сверялись только сdocs/conventions/README.md, аarchitecture/security/operations— только с записанными инвариантамиCLAUDE.md.- Триаж: ничего нового не находит по определению. Я не читаю код в поисках дефектов, я работаю с чужими выводами. Пропуск любого прохода — мой пропуск тоже; всё, что я могу, — назвать его поимённо, что и сделано выше.
Что осталось целиком на человеке
Из docs/review.md → «Недоступно проверке», двумя отдельными списками, как
записано:
Не проверит ни один проход:
operations: поведение внешних сервисов под нагрузкой и на границах — SpeechKit и Object Storage поднять в тесте нечем;operations: реальный профиль нагрузки. Проект работает на единицах записей в день, и утверждения о росте остаются условиями, а не замерами;security: стойкостьffmpegк вредоносному входу — разбор чужого формата отдан внешней программе, и она вне нашей границы.
Перестали проверять сознательно:
- по записи в
docs/review.md— «Ничего не отключали: проверять пока и не начинали». Однако этим изменением список пополняется фактически: приём перестал проверяться сквозь настоящийffprobe(решение записано вdesign.md§ Risks и обосновано — прежняя проверка проверяла отказ внешней программы и выдавала его за проверку приёма). Разделdocs/review.mdэтого ещё не знает; строку туда добавляет синк документации, не я.
Сверх записанного в проекте — общее, чего не видит ни один прогон: история инцидентов, поведение под реальным потоком, поведение внешних систем в их версиях, завязка потребителей на текущее поведение и вопрос «а нужна ли эта функциональность вообще».
Каких документов проекта не хватило
Строкой на каждый, с причиной — деградация поразрядная:
docs/conventions/про тесты файла нет: конвенции покрывают конфиг, базу, ошибки, журнал и веб-UI. Изменение целиком про тесты, и сверять его форму было не с чем — отсюда promote-кандидат №1, а не находка.docs/adr/пуст: толькоREADME.mdиtemplate.md, ни одного решения. См. обязательную строку 1 ниже.docs/research/— толькоREADME.md, записанных замеров нет. См. строку 2.docs/review.md§ «Как настроен конвейер» устарел с этого прогона: там написано «Конвейера ревью в проекте пока нет: плагин не подключён, ни одного прогона не было». Прогон был — этот. Отсев ложноположительных при этом не был слепым: раздел «Типовые ложноположительные» заполнен наперёд, четыре пункта, и я им пользовался.docs/security.mdв проекте есть, но на меткеsmallплан отправил темуsecurityв инвариантыCLAUDE.md, и как дом темыsecurity.mdне открывался. См. строку 5 ниже.
Четыре строки, которых не принесёт ни один проход
- Решения проекта не сверялись.
docs/adr/*— процессный документ, прогон его не открывает. Расхождение изменения с записанным решением ловит сверка документации (скиллav-dev-docs:healthcheck), а не ревью. Здесь у этого есть и вторая сторона: каталог решений пуст, сверять было бы не с чем. - Записанные наблюдения проекта не использовались.
docs/research/— тоже процессный. Всякое число в этом отчёте снято командой на этом прогоне; чисел без приложенной команды в отчёте нет. - Поимённая сверка с руководствами по стилю Go не задавалась ни одним проходом. Различение «идиоматично против просто распространено» на этом прогоне не спрашивал никто.
- Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет. Проход независимой реализации снят по стоимости, а не по замеру. «Не знаю, чего не знаю» здесь никто не достаёт: например, вопрос «а верна ли сама форма подстановки в сборке теста» не задал никто, кроме автора дизайна.
Пятая строка — следствие метки small
Темы security, operations и architecture сверялись только с записанными
инвариантами CLAUDE.md; дома этих тем (docs/security.md,
docs/architecture.md, docs/conventions/logging.md как источник норм журнала)
не открывались. Свойство, которого нет в семи пунктах инвариантов, на этом
прогоне не проверил никто.
Отдельно про корректор метки
review-code метку small подтвердил, сигнала о занижении не подал.
review-basics не запускался, поэтому второго, независимого от review-code
подтверждения метки нет. Согласия двух проходов здесь не было бы и при запуске:
несколько агентов — один источник, высказавшийся несколько раз; совпадение
подняло бы приоритет, но не confidence.