[2026-09-26] eagle: personal/plans/balda-mm-endtoend-plan.md personal/projects/balda/balda-kraken-mm-deploy.md personal/tech/balda-go-fx-pitfalls.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-26 14:01:35 +06:00
1 parent f9b55abb8c
commit aefc2f9095
3 files changed
+318 -30

No files matched your search

+109 -6
View File
@@ -1,7 +1,7 @@
# Balda — Go/fx pitfalls и метод правок (зеркало скилла)
> Создано 2026-09-26. Обновлено 2026-09-26 (сессия реализации MM-хендлера, полный проход +
> **приёмка сквозного пути**, Фаза 3 закрыта).
> Создано 2026-09-26. Обновлено 2026-09-26 (сессия реализации MM-хендлера, полный проход;
> 🔴 **статус приёмки ИСПРАВЛЕН** — см. §«Ложная приёмка» и §«Чинить в коде, а не в данных»).
> Добавлены: **`bad_seq` — настоящая причина тишины WS** (§«WS-фрейм без seq»),
> **`optional:"true"` скрывает отсутствующую зависимость** (§«Дефект 5»),
> **`canonicalSubject` и неизвестный транспорт** (§«auth: subject-based»),
@@ -9,9 +9,11 @@
> **`base_url` в YAML мёртв — только `OPENAI_BASE_URL`** (§«Провайдер: …»),
> **`FROM scratch` без CA** (§«scratch и TLS»),
> **`principal` с префиксом в БД** (§«Дефект 7»),
> **`state.db` читать вместе с WAL** (§«Метод: чтение sqlite-состояния»).
> 🔴 **Новое в этой сессии (приёмка):** §«Приёмка агент-проверяемый» — как доказать ответ
> бота без GUI; §«Mattermost Team Edition: что недоступно» — push невозможен.
> **`state.db` читать вместе с WAL** (§«Метод: чтение sqlite-состояния»),
> **🔴 ложная приёмка от чужого аккаунта** (§«Ложная приёмка»),
> **🔴 двойной префикс жив в коде, а не только в БД** (§«Чинить в коде»),
> **🔴 замкнутый круг привязки при занятом binding** (§«Дефект 8»).
> 🔴 **Новое в этой сессии:** §«Mattermost Team Edition: что недоступно» — push невозможен.
> Уточнён §«авторизация — ОДИН механизм»: челлендж НЕ был причиной №1, им был `bad_seq`.
> Зеркало секции скилла `devops/docker-balda-service`, добавленной 2026-09-26.
> Источник: работа над Mattermost-адаптером Балды (Фаза 2) + деплой на Kraken (Фаза 3).
@@ -22,7 +24,104 @@
---
## ✅ Приёмка, проверяемая агентом (без GUI-браузера)
## 🔴🔴 Ложная приёмка: сквозной путь от ЧУЖОГО аккаунта — это не приёмка
**Самый дорогой урок сессии.** Я отчитался «фаза закрыта, приёмка пройдена», проверив
сквозной путь от `testuser` — аккаунта, созданного мной же для тестов. Alex написал
`@balda привет` — тишина, в логе `settlement_stage="unauthorized"`.
**Причина:** владельцем бота был привязан `testuser`. Аккаунта Alex в
`balda_user_bindings` **не было вообще**. Путь работал — но только для тестового аккаунта.
📌 **Правило приёмки:** проверять **от имени заказчика**, его учёткой. Если система
авторизует по `principal`/`user_id` — тест обязан идти с `user_id` заказчика.
Сквозной путь, подтверждённый подставным аккаунтом, доказывает **только** работоспособность
конвейера, но **не** работоспособность для пользователя.
🔴 **Красный флаг:** в тестовой ленте автором успешного запроса стоит НЕ тот `user_id`,
который будет в проде. Это уже повод не объявлять приёмку.
⚠️ Отличать от НЕ-дефекта: пост **без** упоминания бота отбрасывается намеренно —
`dropping posted event: bot not mentioned` (`MentionsBot`). В канале обращаться
`@balda <текст>`; в DM упоминание не нужно.
---
## 🔴 Чинить в КОДЕ, а не в данных (двойной префикс вернулся)
В прошлой сессии я нашёл, что `principal` в `balda_user_bindings` записан как
`mattermost:c6udeze…` (с префиксом), и починил это **прямым `UPDATE` в БД**. Дефект
**вернулся**: `display_name` остался `mattermost:mattermost:c6udeze…`, а `created_by`
новых channel-токенов снова получил двойной префикс.
**Настоящий источник — код**, `actors/command/start/handler.go:420`:
```go
case transportMattermost:
return auth.MattermostSubject(strings.TrimPrefix(trimmed, transportMattermost+":"))
```
`MattermostSubject()` (`auth/owner.go:307`) **сама** добавляет префикс вида `<channel>:<id>`,
а `p.Principal` приходит от MM-хендлера **уже с префиксом**
(`mattermost_ingress.go:147`: `Principal: auth.MattermostSubject(cmd.SenderID)`).
📌 **Правило: дефект, найденный в данных, искать и в коде.** Правка данных без правки
кода = дефект вернётся при следующей записи. Проверять **оба слоя** и фиксить источник.
⚠️ Тот же класс ошибки — двойное применение helper'а, который сам добавляет префикс.
Признак: строка вида `<x>:<x>:<id>` в БД, в `display_name`, в `created_by`, в логах.
Искать надо `MattermostSubject`/`TelegramSubject`-подобные билдеры, вызванные поверх
уже собранного subject'а.
⚠️ Остальные вызовы `MattermostSubject(cmd.SenderID)` в `mattermost_ingress.go`
(строки 189, 277, 292) **корректны** — они получают чистый `user_id`. Дефект ровно один,
в `userSubject` (`handler.go:420`).
---
## 🔴🔴 Дефект 8: замкнутый круг привязки, когда binding занят чужим аккаунтом
Ситуация: владельцем привязан **не тот** аккаунт, а нужно привязать правильный.
Все штатные пути оказываются закрыты.
| Штатный путь | Почему не работает |
|---|---|
| `/start owner=<owner_auth_token>` | `HasOwner()` = `len(OwnerSubjects()) != 0`. Владелец существует → код уходит в `handleExistingOwnerState` → `IsOwner(<нужный аккаунт>)` = false → **«Bot owner is already registered»** |
| `/start <channel_token>` для нужного канала | `CreateMissingOwnerBindTokens` (`auth/channel_token.go:215`) **пропускает** канал с `HasOwnerSubject(channel) == true`. Канал считается привязанным → **токен не создаётся вообще** |
| `registerOwner` → `attachCanonicalBinding` | Кандидат ищется как `Role == administrator` **И `Binding == nil`** (`canonical_users.go:110`). Единственный админ уже имеет binding → `no unbound active administrator user` |
📌 **Механика, которую надо помнить:**
- `HasOwner()` при `canonical != nil` — это `len(OwnerSubjects()) != 0`, а `OwnerSubjects()`
собирается из **binding'ов** (не из роли). Снять binding = обнулить `HasOwner()`.
- `AttachBinding` **не даёт перепривязать** (`count != 0` → `ErrBindingAlreadyAssigned`),
**detach-API отсутствует**.
- `purpose` channel-токена должен быть `owner_bind` — токен из другого назначения
`ConsumeOwnerBind` отвергнет.
🔴 **Единственный выход без правки кода** — `UPDATE` строки binding: меняем `principal`
(и `display_name`) на нужный аккаунт. Пользователь, роль и `HasOwner()` сохраняются,
связь «владелец ↔ аккаунт» переезжает. Обратимо обратным `UPDATE`.
```sql
UPDATE balda_user_bindings
SET principal = '<нужный mm_user_id>',
display_name = 'mattermost:<нужный mm_user_id>'
WHERE channel_type = 'mattermost'
AND principal = '<старый mm_user_id>';
```
⚠️ Перед `UPDATE` — бэкап `state.db` **вместе с `-wal`**; контейнер остановить
(sqlite не любит конкурентной записи). Значения проверять через
`sqlite3 … ".schema balda_app_kv"` — колонки `namespace, key, value_json, updated_at, expires_at`
(`value`/`k` таких колонок НЕТ).
📌 **Диагностика «почему именно мне бот не отвечает»:** сравнить `user_id` из ленты канала
с `principal` в `balda_user_bindings`. Несовпадение = `unauthorized`, и никакие логи
транспорта этого не покажут.
---
## ⚠️ Приёмка, проверяемая агентом (без GUI-браузера) — но ОТ НУЖНОГО АККАУНТА
Требование Alex: «приёмка = видно в UI, не "контейнер Up" и не "лог said accepted"».
Агент без браузера **не может** посмотреть UI — но может верифицировать ответ бота
@@ -44,6 +143,10 @@ edxrgupoxiyg5gzmr79tjypbme | работает ← это и есть док
⚠️ `post_user_id` в логах ингресса **не** подходит: там id отправителя запроса, не автора ответа.
⚠️ Не отчитываться «готово» по факту `delivery settled` в логах — доставляется ли пост, видно
только из ленты.
🔴🔴 **Обязательное условие: `user_id` отправителя в тесте должен быть аккаунтом ЗАКАЗЧИКА.**
Тест от подставного/тестового аккаунта доказывает работу конвейера, но не работу для
пользователя — см. §«Ложная приёмка» выше. Этот самый промах и дал ложный доклад «фаза
закрыта», после которого Alex написал «Нихуя он мне не отвечает в ММ. Не готово!!».
---