telegram: сервис поднимается без бота и работает одним входом
- Клиент бота собирается один раз и достаётся отправителю и транспорту; разрез прошёл по «ответил ли Telegram»: ответ «такого бота нет» роняет старт, недоступность даёт подъём без Telegram (ADR-2026-08-13). Ожидание при сборке ограничено сроком — иначе молчащий Telegram вешал подъём. - Недоставленный ответ не роняет шаг: пишется с job_id и считается метрикой, уровень по причине — WARN для неподнятого входа, ERROR для неназванного адресата. Заведены transcriber_intake_up и transcriber_undelivered_reply_count. - Закрыта утечка токена в журнал: отказ разбора адреса рождается раньше обращения к клиенту, то есть мимо чистки на его границе.
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
# Ревью изменения `start-without-telegram-token` — отчёт триажа
|
||||
|
||||
Прогон 2026-08-13. Отчёт сохранён оркестратором: агент триажа записывать
|
||||
`.md` не вправе.
|
||||
|
||||
## Сводка
|
||||
|
||||
- **Режим:** по графу; изменение не закоммичено, база диффа `origin/master`.
|
||||
- **Метка:** `large` — крупное × знакомое. Повторная разметка после правок
|
||||
дизайна: первая давала `medium`, исходя из того, что ядро не тронуто; правки
|
||||
ревью дизайна это допущение сняли.
|
||||
- **Гейт:** зелёный целиком, 13 шагов, включая `-race`, `golangci-lint`,
|
||||
`govulncheck`. Оракул снят проходом `autotests`, триаж гейт не перезапускал.
|
||||
- **Находок на входе:** 19 (specs 3, code 6, architecture 3, adversary 4,
|
||||
ops 3, autotests 0). **Осталось:** 6 в основном списке, 2 гипотезы,
|
||||
2 кандидата в промоут; срезы названы поимённо.
|
||||
|
||||
### План с исходом по каждой теме
|
||||
|
||||
| тема | дом | глубина | кто закрывает | исход |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| requirements | `openspec/specs` + дельты change | разбор | specs | закрыта, 3 находки |
|
||||
| autotests | `CLAUDE.md`, «Гейт» | — | autotests | закрыта, 0 находок |
|
||||
| conventions | `docs/conventions/` | разбор | code | закрыта, 6 находок |
|
||||
| architecture | `docs/architecture.md` + `passport.md` | доказательство | architecture | закрыта, 3 находки |
|
||||
| security | `docs/security.md` | доказательство | adversary | закрыта, 4 находки |
|
||||
| operations | `docs/architecture.md` «Эксплуатация» + `database.md` | доказательство | ops | закрыта, 3 находки |
|
||||
|
||||
Тем без дома нет, тем без отчёта нет. Своих тем у проекта нет, `basics` не
|
||||
запускался — все темы ядра закрыты именными проходами. Побочное следствие:
|
||||
независимого второго голоса о заниженности метки на прогоне не было.
|
||||
|
||||
## Блокирует мердж
|
||||
|
||||
### 1. Токен бота уезжает в журнал целиком при опечатке — critical
|
||||
|
||||
`internal/adapter/telegram/bot.go` возвращал отказ конструктора без чистки.
|
||||
Отказ рождается в `http.NewRequest` на разборе адреса — **до** обращения к
|
||||
клиенту, то есть мимо `safeClient` и `WithoutURL`. Токен с управляющим символом
|
||||
или неверной `%`-последовательностью печатался в журнал целиком.
|
||||
|
||||
Оракул: воспроизведено тремя проходами независимо и триажем отдельно. Нарушены
|
||||
инвариант `CLAUDE.md` «Секрет не покидает конфиг» (critical) и MUST дельта-спеки
|
||||
`intake`.
|
||||
|
||||
**Исход: починено инлайн.** Отказ конструктора пропущен через `WithoutURL`.
|
||||
Заодно закрыта дыра в собственной проверке: прежний тест судил запрет **годным**
|
||||
токеном, то есть случаем, который и так работал. Добавлен тест с токеном,
|
||||
ломающим разбор адреса.
|
||||
|
||||
### 2. Молчащий Telegram вешает старт навсегда — major
|
||||
|
||||
Клиент собран из `&http.Client{}` без срока ожидания, сборка стоит до подъёма
|
||||
сервера. При Telegram, отвечающем молчанием, процесс висит бесконечно: порт не
|
||||
слушается, `/health` не отвечает, воркеры не запущены, в журнале ни строки.
|
||||
|
||||
Поведение предсуществует изменению, но изменение **записывает его нормой**.
|
||||
Довод дизайна «различать нечем» проверяемо неверен: отказ Bot API приезжает
|
||||
типом `*tgbotapi.Error` с кодом, транспортный — нашим после чистки.
|
||||
|
||||
**Исход: развилка владельцу.** Цена дописана в таблицу отказов
|
||||
`docs/architecture.md`; выбор поведения — за владельцем.
|
||||
|
||||
## Стоит исправить сейчас
|
||||
|
||||
### 3. Сервис без Telegram выглядит здоровым — major
|
||||
|
||||
`/health` отдаёт статические `200 ok` и о входах не знает; серий `transcriber_*`
|
||||
на `/metrics` при неподнятом боте ноль; поля «доставлено» в схеме нет. Владелец
|
||||
узнаёт о потерянном входе только из журнала контейнера и только до ротации.
|
||||
|
||||
**Исход: развилка владельцу.** Попутно исправлено фактическое: обоснование нормы
|
||||
называло третьим последствием перезапись служебных полей завершённой задачи —
|
||||
такого не бывает, переход в терминальное состояние снимает захват, и повторная
|
||||
запись натыкается на «захват потерян». Довод сведён к двум последствиям.
|
||||
|
||||
### 4. Задача из Telegram, потерявшая чат, считается успешной — major
|
||||
|
||||
Было `ERROR` и отказ шага, стало `WARN` и успех. Тем самым снят самый громкий
|
||||
детектор класса, который `CLAUDE.md` называет самым коварным: колонка, выпавшая
|
||||
из пары `acquireColumns`/`acquiredRow`, обнуляет чат у задачи, попавшей к
|
||||
воркеру.
|
||||
|
||||
**Исход: развилка владельцу** — развести уровни по причине или оставить.
|
||||
|
||||
### 5. Разрез старта не держался ни одним тестом — minor
|
||||
|
||||
Инвертируй разрез — весь набор оставался зелёным.
|
||||
|
||||
**Исход: починено инлайн.** Решение вынесено из `main` в `telegramFromBot` и
|
||||
накрыто тремя случаями. Обращение к Telegram отделено от решения намеренно:
|
||||
обращение ходит в сеть и в проверке недоступно, а разрез проверять надо.
|
||||
|
||||
### 6. Образец конфига и три документа описывали снятое поведение — minor
|
||||
|
||||
`config.dist.toml` оставлял непустой плейсхолдер, хотя собственный комментарий
|
||||
рядом объявлял пустой токен режимом: копия образца старт роняла.
|
||||
`docs/conventions/config.md` описывал снятый механизм строкой «Расхождение», а
|
||||
она в этом проекте выдаёт индульгенцию будущим ревью. `docs/conventions/errors.md`
|
||||
перечислял удалённый тип. Маркер канона в `docs/architecture.md` ссылался на
|
||||
несуществующие capability.
|
||||
|
||||
**Исход: починено инлайн, все четыре места.**
|
||||
|
||||
## Чем кончились развилки — решения владельца 2026-08-13
|
||||
|
||||
- **Находка 2 (молчащий Telegram).** Выбран вариант сверх предложенных:
|
||||
недоступность Telegram на старт не влияет. Разрез перенесён с «пусто /
|
||||
непусто» на «ответил ли Telegram»: ответ «такого бота нет» роняет старт,
|
||||
недоступность даёт подъём без Telegram с записью `WARN`. Из решения следует
|
||||
срок ожидания при сборке — без него недоступность неотличима от подъёма.
|
||||
- **Находка 3 (наблюдаемость).** Выбран счётчик и признак входов: метрика
|
||||
поднятости по каждому входу и счётчик недоставленных ответов с причиной
|
||||
меткой. Колонку в задаче не заводили — это шаг схемы и необратимое.
|
||||
- **Находка 4 (уровень).** Уровни разведены: неподнятый вход — `WARN`,
|
||||
неназванный адресат — `ERROR`.
|
||||
|
||||
**Отдельно о самом прогоне.** Живой прогон, снятый проходом `adversary`, оставил
|
||||
процесс работающим на том же порту, и он держал его ещё час. Часть моих проверок
|
||||
после переделки мерила этот чужой процесс, а не новую сборку; обнаружено по
|
||||
отсутствию новой метрики, исправлено остановкой процесса и повторным прогоном.
|
||||
Кандидат в правило: прогон, поднимающий сервис, обязан снимать его за собой, а
|
||||
проверяющий — убеждаться, что порт занят его собственной сборкой.
|
||||
|
||||
## Гипотезы без доказательства
|
||||
|
||||
- **`{"ok":true,"result":null}` считается успешной доставкой.** Механизм доказан
|
||||
на подставном сервере, вторая половина — что живой Telegram так отвечает — не
|
||||
доказана и по правилам проекта недоказуема. Предсуществует изменению.
|
||||
- **Второй `os.Exit(1)` недостижим сегодня.** Приемлемая страховка, не дефект:
|
||||
конструктор объявляет отказ в сигнатуре, и разобрать его вызывающий обязан.
|
||||
|
||||
## Кандидаты в промоут
|
||||
|
||||
- **Порядок выкладки: конфиг с пустым токеном нельзя выкатывать раньше бинаря.**
|
||||
Воспроизведено на `origin/master` в отдельном worktree: откат бинаря при уже
|
||||
применённом пустом токене останавливает весь сервис. Это правило эксплуатации,
|
||||
которого в проекте нет; дом — `docs/architecture.md`, «Эксплуатация».
|
||||
- **Сверка документов на упоминания удалённых идентификаторов.** Три из четырёх
|
||||
мест находки 6 — прямые ссылки на снесённый код и несуществующие capability.
|
||||
Ловит это `av-dev:doc-healthcheck`, которого зовут руками.
|
||||
|
||||
## Границы покрытия
|
||||
|
||||
**Что не проверил ни один проход** (`docs/review.md`, «Недоступно проверке»):
|
||||
поведение SpeechKit и Object Storage под нагрузкой; реальный профиль нагрузки;
|
||||
стойкость `ffmpeg` к вредоносному входу; поведение настоящей Authelia; поведение
|
||||
браузера с куками.
|
||||
|
||||
**Перестали проверять сознательно:** разбор вывода настоящего `ffprobe`; работа
|
||||
сервиса с настоящими внешними собеседниками. Подъём живьём стал доступен как раз
|
||||
этим изменением, но остаток — приём из Telegram, расшифровка, заливка — не
|
||||
проверяет никто.
|
||||
|
||||
**Чего не принесёт ни один прогон:**
|
||||
|
||||
1. Решения проекта не сверялись — `docs/adr/` процессный, прогон его не
|
||||
открывает. Расхождение с записанным решением ловит `av-dev:doc-healthcheck`.
|
||||
2. Записанные наблюдения не использовались — `docs/research/` тоже процессный.
|
||||
Всякое число этого отчёта снято на этом прогоне.
|
||||
3. Поимённой сверки с руководствами по стилю Go не задавал ни один проход.
|
||||
4. Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет.
|
||||
|
||||
**Сработавшие потолки.** Потолок триажа: 19 находок → 6. Срезано поимённо:
|
||||
дубли в тестах (близко к вкусовщине; починено попутно, тот же файл правился
|
||||
находкой 1); избыточность представлений факта «Telegram не поднят» — шесть
|
||||
вместо четырёх, одно сократимо, последствие не названо; откат бинаря — уехал в
|
||||
промоут; вырожденный ответ библиотеки — в гипотезы.
|
||||
|
||||
**Отдельная находка о самом прогоне:** проходы отдавали сводки пересказом, и
|
||||
свои блоки «Coverage of this pass» с потолками до триажа дошли не все — узнать,
|
||||
срезал ли `code` или `adversary` что-то у себя, из отчёта нельзя.
|
||||
Reference in New Issue
Block a user