# Balda — Mattermost connector: PR-подготовка (закрыто) > Создан 2026-09-26. Обновлён 2026-09-26 (PR открыт). > Статус: **✅ PR ОТКРЫТ — baldaworks/balda#8, 8/8 CI PASS, MERGEABLE.** > https://github.com/baldaworks/balda/pull/8 > Команда Alex: «Коммит текущего стейта и продолжай пока не стыдно будет открывать PR mattermost connector. > Со всеми поддерживаемыми фичами и тестами. Вперёд! Без вопросов и требующих апрува команд.» > Часть [[personal/projects/balda/balda-mm-delegation-plan]] (Фаза 2 → PR в апстрим). ## ✅ БОЕВАЯ ПРИЁМКА ПРОЙДЕНА — 2026-09-26 (образ `0.1.10`, коммит `9f1e6c9c`) **Сквозной путь работает ОТ АККАУНТА ALEX.** Это первая настоящая приёмка (прошлая была ложной — от `testuser`). ### Доказательство — лента канала `balda-test` ``` 1790413414689 | edxrgupoxiyg5gzmr79tjypbme | слово ← ОТВЕТ БОТА 1790413409055 | cbwsorzwoigbtrdnteugnx4aqo | @balda скажи одно слово ← пост ALEX ``` Запрос — `cbwsorzwoigbtrdnteugnx4aqo` (**аккаунт Alex**, `admin`), ответ — бот. В логах: **`settlement_stage=accepted`** (НЕ `unauthorized`!), `assembled provider user content`, `delivery settled`. ### Состояние привязки владельца — починено ``` balda_user_bindings: channel_type = mattermost principal = cbwsorzwoigbtrdnteugnx4aqo ← БЕЗ префикса ✅ (иначе unauthorized) display_name = mattermost:cbwsorzwoigbtrdnteugnx4aqo ← ОДИНАРНЫЙ префикс ✅ (нет мусора) ``` Прошлый дефект двойного префикса вылечен **в коде** (идемпотентный `MattermostSubject`), в БД мусора нет. ### WS-здоровье | Метрика | Значение | |---|---| | `bad_seq` | **0** ✅ | | `Invalid sequence` | **0** ✅ | | `pong` | **6** (пинг раз в 30 с работает) | | `RestartCount` | **0** ✅ | | Контейнер | `balda-mm:0.1.10` Up стабильно | ### Старт — чистый ``` mattermost transport configured server_url=http://mattermost:8065 bot_user_id_set=true session manager ready balda_provider=deepseek mattermost websocket connected ws://mattermost:8065/api/v4/websocket Balda bot started. ``` Никакого crash-loop, `advertised commands have no actor handler` **отсутствует**. ### ⚠️ Два не-блокера, которые стоит знать 1. **`warn: getMe response missing result`** при старте. Косметика: startup-лог не смог прочитать identity бота. На работу НЕ влияет — `bot_user_id_set=true` из конфига, ответы идут. Стоит починить отдельно (low priority, в апстриме). 2. **`transport_user_id = c6udeze8dfrdxm5jbgq55bd58r` в логах сессии** — это **`testuser`**, id прошлого владельца, зашитый в **persisted-метаданные сессии** `mm-c-7aafea13` (создана в прошлой сессии от testuser). Авторизация при этом прошла верно (по `principal` из bindings), ответ ушёл Alex'у. **Но это грязь:** сессия хранит чужой `user_id`. 🔴 Если Alex хочет чистую сессию — `/reset` в канале создаст новую с его `user_id`. На работу не влияет (проверено: ответ пришёл). ### Деплой — как сделано (воспроизводимо) ```bash # 1. образ из PR-коммита (на Mac) cd ~/Developer/balda docker buildx build --builder arm64builder --platform linux/arm64 \ -f Dockerfile.release --build-arg VERSION=v0.3.1-mm-pr8 \ --build-arg COMMIT=$(git rev-parse --short HEAD) \ --build-arg BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) \ --provenance=false --load -t balda-mm:0.1.10 . # 2. перенос (распаковка zstd НА MAC — на хосте zstd 1.5.4 не читает фрейм macOS) docker save balda-mm:0.1.10 -o /tmp/balda-0110.tar zstd -3 -T0 -o /tmp/balda-0110.tar.zst --force /tmp/balda-0110.tar zstd -dc /tmp/balda-0110.tar.zst | ssh kraken 'docker load' # 3. переключение с бэкапом compose (скрипт tools/mm-switch-image.sh) ssh kraken "cat > /tmp/mm-switch-image.sh" < tools/mm-switch-image.sh ssh kraken 'bash -n /tmp/mm-switch-image.sh' < /dev/null # SYNTAX_OK ssh kraken 'bash /tmp/mm-switch-image.sh' < /dev/null ``` **Скрипты, созданные для этого деплоя:** | Скрипт | Что делает | |---|---| | `tools/mm-switch-image.sh` | смена тега образа в compose + бэкап + `--force-recreate` | | `tools/mm-whoami.sh` | какой MM-аккаунт у PAT (`/users/me`) — нужен для приёмки от заказчика | ⚠️ **Проверка CA в образе:** `docker save` даёт **OCI**-формат, где файлы в `blobs/sha256/*`; `tar -tf image.tar | grep ca-certificates` даст **0** (ложный минус). Проверять по слоям: распаковать и искать в `blobs/`, или верить `docker inspect` → `SSL_CERT_FILE` в `Config.Env`. ⚠️ **Приёмка обязывает иметь PAT заказчика.** `/tmp/admin-tok` = PAT Alex (`cbwsorzwoigbtrdnteugnx4aqo`, `admin`) — сверено через `mm-whoami.sh` **до** постинга, а не на глаз. Без этого тест снова был бы от `testuser` = ложная приёмка. --- ## ✅ PR ОТКРЫТ — https://github.com/baldaworks/balda/pull/8 (2026-09-26) **Статус: OPEN · MERGEABLE · все 8 CI-джобов PASS.** | | | |---|---| | PR | **baldaworks/balda#8** | | База | `baldaworks/balda:main` | | Голова | `mallexxx:feat/mattermost-channel` | | Объём | **41 файл, +5622 / −18** | | Коммитов | 7 | | Mergeable | ✅ MERGEABLE | ### 🔴 Целевой репозиторий — `baldaworks/balda`, не `normahq/balda` Проверено через API, а не по названию remote'а: ``` gh repo view mallexxx/balda --json parent → parent: baldaworks/balda ← форк от него gh repo view normahq/balda --json owner → owner.id: O_kgDOEgAHrw (= baldaworks) ← редирект, не отдельный репо ``` `origin` = `mallexxx/balda` (форк), `upstream` = `normahq/balda` (редирект на baldaworks). **PR создавать в `baldaworks/balda`**, head указывать как `mallexxx:` (не полный URL форка — иначе `gh` путается). ### База ветки — идеальная `git rev-list --left-right --count upstream/main...HEAD` → `0 7` (0 позади, 7 впереди). `merge-base HEAD upstream/main` == `upstream/main` == `de273d8f`. Rebase не потребовался. ### CI — 8/8 PASS (прогон на чистом раннере) ``` architecture-lint pass 2m2s ingress-e2e pass 30s jetstream-integration pass 1m0s lint pass 2m46s postgres-integration pass 57s projection-replay pass 26s sqlite-integration pass 1m15s test pass 6m0s ← главный: полный go test на чистом раннере ``` 📌 **Это и есть настоящая приёмка сборки.** Локально `cmd/balda` падал на `TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents` — в CI он **прошёл**, то есть падение было особенностью моей локальной среды, а не дефектом. CI на чистом раннере закрывает ровно тот класс проблем, который локальные гейты не видят. ⚠️ **CI-пайплайн в репо ЕСТЬ** (`architecture-lint`, `lint`, `test`, `ingress-e2e`, `jetstream-integration`, `postgres-integration`, `sqlite-integration`, `projection-replay`). Значит локальные гейты — необходимое, но не достаточное условие; ждать CI обязательно. ### Команды (воспроизводимо) ```bash cd ~/Developer/balda git fetch upstream --quiet git rev-list --left-right --count upstream/main...HEAD # 0 7 git push -u origin feat/mattermost-channel gh pr create --repo baldaworks/balda --base main \ --head mallexxx:feat/mattermost-channel \ --title "feat(mattermost): add Mattermost channel transport" \ --body-file /tmp/mm-pr-body.md gh pr view 8 --repo baldaworks/balda --json state,mergeable,changedFiles,additions gh pr checks 8 --repo baldaworks/balda ``` ### Что осталось 1. **Ревью апстрима** — ждать. Правки по замечаниям делать в этой же ветке (push обновит PR). 2. **Боевая проверка на Kraken** — PR-merge ≠ работает на стенде. Пересобрать образ из `9f1e6c9c`, задеплоить, `@balda` → ответ в ленте канала (см. [[personal/projects/balda/balda-kraken-mm-deploy]]). 3. Фаза 4 (исполнитель) — [[personal/projects/balda/balda-mm-delegation-plan]]. --- ## ✅ Итог: ветка PR-clean (2026-09-26, до пуша) **7 коммитов**, рабочее дерево **чистое**, `tools/` **исключён** из ветки. ``` 9f1e6c9c chore: ignore local project-editing tooling 58f27120 docs(mattermost): document the transport setup and behavior 80d4f41d test(mattermost): cover the transport, the ingress and the handler c7f18654 fix(mattermost): send a monotonic seq on every client frame 6dc3077a feat(mattermost): add the inbound handler and wire it into DI fa1a2855 feat(auth): support the Mattermost channel subject 004fda2f feat(mattermost): add Mattermost transport adapter ← база прошлой сессии ``` ### 🔴 Уборка `tools/` — важный урок про PR База `004fda2f` (прошлая сессия, **не мои 5 коммитов**) затащила в ветку **16 файлов `tools/`**: `apply-project-deltas.py`, `run-project-deltas.sh`, `deltas-mattermost*.json`, `.deltas-state.json`. **В апстриме каталога `tools/` нет вообще** — проверено `git ls-tree upstream/main tools/` → пусто. **Как найдено:** `git diff --stat upstream/main...HEAD` показал `tools/` в списке файлов PR. **Фикс (коммит `9f1e6c9c`):** 1. `/tools/` добавлен в `.gitignore`; 2. `git rm -r --cached tools/` — файлы **остались на диске** (39 файлов), ушли только из git. ⚠️ **`gitignore` НЕ отвязывает уже отслеживаемые файлы** — нужен именно `git rm --cached`. ⚠️ **Проверять состав PR через `git diff upstream/main...HEAD`, а не по своим коммитам.** Свои 5 коммитов были чистыми (`git log upstream..HEAD -- tools/` → пусто), но ветка в целом — нет, из-за базового коммита. **Результат:** в PR-diff **41 файл**, все — файлы проекта, ни одного `tools/`. ### Финальные гейты — все зелёные на закоммиченном дереве ``` go build ./... → exit 0 go vet ./... → exit 0 go tool golangci-lint run → 0 issues go tool go-arch-lint check --project-path . → OK - No warnings found go test -race (затронутые пакеты) → все ok тесты mattermost + handlersfx (verbose) → 126 PASS, 0 FAIL, 0 SKIP ``` **Тестов добавлено:** **103** (`grep -c '^func Test'`), по файлам: | Файл | Тестов | |---|---| | `channel/mattermost/ingress_test.go` | 23 | | `channel/mattermost/inbound_test.go` | 18 | | `channel/mattermost/adapter_test.go` | 17 | | `handlersfx/mattermost_ingress_test.go` | 17 | | `channel/mattermost/locator_codec_test.go` | 15 | | `channel/mattermost/client_test.go` | 13 | ### Предсуществующие падения — НЕ наши (проверено `git stash`) 5 пакетов: `cmd/balda`, `apps/balda`, `mcpruntime`, `session`, `state`. Те же 4 теста падают **без** моих правок: `TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents`, `TestCanonicalSessionMemoryRuntimeComposesPortableCapabilities`, `TestCanonicalSessionMemoryRuntimeMigratesAndReopensGroupedStore`, `TestResolveWorkspaceBaseBranch_ConfigPreferredWhenValid`. ### Лишнее, что я НЕ отправляю в PR `tools/mm-*.sh` (9 ops-скриптов), `tools/deltas-*.json` (16 файлов дельта-групп), `tools/apply-project-deltas.py`, `tools/run-project-deltas.sh`, `tools/.deltas-state.json` — локальный инструментарий, остаётся на диске, в git не идёт. ### Осталось (по желанию Alex) 1. `git push -u origin feat/mattermost-channel` → открыть PR в `baldaworks/balda`. 2. Фаза 3/4 — [[personal/projects/balda/balda-kraken-mm-deploy]]. --- ## Итог (промежуточный, до уборки tools/) Ветка `feat/mattermost-channel`, **6 коммитов** (базовый `004fda2f` + 5 новых). Готово к `git push` и PR. ``` 58f27120 docs(mattermost): document the transport setup and behavior 80d4f41d test(mattermost): cover the transport, the ingress and the handler c7f18654 fix(mattermost): send a monotonic seq on every client frame 6dc3077a feat(mattermost): add the inbound handler and wire it into DI fa1a2855 feat(auth): support the Mattermost channel subject 004fda2f feat(mattermost): add Mattermost transport adapter ← база ``` ## Quality gates — все зелёные (проверено на закоммиченном дереве) ``` go build ./... → exit 0 go vet ./... → exit 0 go tool golangci-lint run → 0 issues go tool go-arch-lint check --project-path . → OK - No warnings found go test -race ./channel/mattermost/ ./handlersfx/ ./auth/ ./deliveryfmt/ ./actors/command/... → все ok, включая start и auth ``` ## Что сделано в этом проходе ### 1. Дефект двойного префикса — починен В КОДЕ, а не в БД (главное) Прошлый фикс был прямым `UPDATE` в `state.db`, поэтому дефект вернулся. Теперь источник закрыт: - `auth.MattermostSubject()` **идемпотентен** — срезает уже имеющийся префикс `mattermost:` перед повторной квалификацией; - из `actors/command/start/handler.go` убран локальный `TrimPrefix`-костыль, ветка стала как у остальных транспортов: `auth.MattermostSubject(trimmed)`. Почему это ломало прод: `canonicalSubject` режет subject по **первому** `:`, а `GetUserByBinding` сравнивает **точно**. Значение `mattermost:mattermost:` в `principal` не совпадает ни с чем → каждый пост Alex отбивался `settlement_stage=unauthorized`. **Тест-регрессия:** `TestMattermostSubjectIsIdempotent` (`handlersfx/mattermost_ingress_test.go`) + `TestMattermostHandlerSendsChannelQualifiedPrincipalExactlyOnce`. ### 2. Исправлено противоречие `ProgressPolicy.Typing` Command-путь ставил `Typing: true`, тогда как inbound-путь — `false`, а `SendTyping` у MM-адаптера **осознанный no-op** (у ботов MM нет typing API). Мёртвая ветка + warn-шум на каждом ходу. Приведено к `false`. ### 3. Тесты транспорта — 6 новых файлов (~90 тестов) | Файл | Покрытие | |---|---| | `channel/mattermost/locator_codec_test.go` | локаторы, **thread↔channel изоляция**, `LocatorFromAddressKey`, scope, UserID | | `channel/mattermost/inbound_test.go` | `NormalizeInbound`, mention-фильтр, `ParseCommand`, whitelist команд, `PostIDFromWebSocket` | | `channel/mattermost/adapter_test.go` | все `deliverycmd.Operation`, **plain-fallback только на content-rejection**, draft-as-post, typing no-op без сети, `root_id` в треде | | `channel/mattermost/client_test.go` | ws/wss derivation, `Bearer`, `APIError`, `isContentRejectedError`, контекст | | `channel/mattermost/ingress_test.go` | 🔴 **`bad_seq`-регрессия**, монотонность и уникальность `seq` под 16 горутинами, фильтры, panic-recovery, семафор | | `handlersfx/mattermost_ingress_test.go` | публикация всех команд, **одиночный** префикс principal, `/start` до авторизации, отказ чужим, nil-safety | ### 4. Доки — `README.md`, `docs/balda.md`, новый `docs/mattermost.md` `docs/mattermost.md` — полный гайд: создание бота, минтинг токена (отдаётся ОДИН раз), membership в команде/канале, 5 ключей конфига, поведение в DM/канале/треде, troubleshooting (`bad_seq`, `unauthorized`, `x509`). ## Уроки метода (дополнить `balda-go-fx-pitfalls`) 1. **Тест-ассерт — это гипотеза, а не истина.** Дважды падал тест, и оба раза неверным был ассерт, а не код: (а) «тред и корневой пост = одна сессия» — на самом деле тред **отдельная** сессия от канала (проверено пробником: `mm-c-a4aa02ef` vs `mm-t-89bbf64f`, два разных ответа в треде дают ОДНУ сессию); (b) «`NewIngress` фолбэчит `botUserID` из клиента» — на самом деле `onStart` **отказывается стартовать** без `bot_user_id`. Прежде чем править код под падающий тест — проверить, чему код **обязан** соответствовать. 2. **`goconst` в тестах** — повторяющиеся строковые литералы (`"channel-1"` ×15) валят линтер. Заводить константы (`testChannelID` и т.п.) сразу. 3. **`dupword`** — сообщение `"thread channel = %q, channel channel = %q"` поймано линтером как повтор слова. Формулировать иначе. 4. **`write_file` + `***` в доке** — плейсхолдеры вида `BALDA_MATTERMOST_TOKEN=***` в примерах конфига остаются на диске **целыми**; искажается только вывод чтения. Проверять `awk 'NR==N'`, не глазами (см. `hermes-secret-redaction-bypass`). 5. **Артефакты деплоя — вон из PR.** `tools/mm-*.sh` (9 скриптов) и `tools/deltas-*.json` (15 файлов) оставлены **незакоммиченными**: это ops-инструменты и метод правок, в апстрим-репозиторий им не место. ## 🔴 Открытое решение: `tools/` закоммичены в ветку (выявлено в конце сессии) **Найдено проверкой `git diff --stat upstream/main...HEAD`:** в диапазоне PR — 56 файлов, и среди них **16 файлов `tools/`**, которых в апстриме **нет вообще** (`git ls-tree upstream/main tools/` → пусто). Кто занёс: **базовый коммит `004fda2f`** (из прошлой сессии, не из этого прохода). Мои 5 новых коммитов чисты — проверено: ```bash git log --oneline --name-only fa1a2855^..HEAD -- 'tools/' # → пусто ``` Что именно закоммичено в `004fda2f` (лишнее для PR): ``` tools/.deltas-state.json tools/apply-project-deltas.py tools/run-project-deltas.sh tools/deltas-mattermost.json tools/deltas-mattermost-2.json … -13.json (12 файлов) ``` **✅ РЕШЕНО (2026-09-26):** выбран вариант (a), выполнен по команде Alex «Добавь в ignored». Коммит `9f1e6c9c chore: ignore local project-editing tooling`. ```bash echo '/tools/' >> .gitignore # + комментарий, что это локальный инструментарий git rm -r --cached tools/ # 16 файлов ушли из индекса, ОСТАЛИСЬ на диске git add .gitignore && git commit # → 9f1e6c9c ``` Результат: `git diff --stat upstream/main...HEAD -- tools/` → **пусто**; в PR-diff **41 файл**, все — файлы проекта. `tools/` на диске цел (**39 файлов**), `git status --short` чист. ⚠️ **`git rm -r --cached tools/` уходит на апрув** (рекурсивное удаление). Первая попытка истекла по таймауту и **не применилась** — состояние было целое. Проверять `git diff --cached --name-only` перед повтором. 🔴 **`gitignore` НЕ отвязывает уже отслеживаемые файлы** — без `git rm --cached` они останутся в PR навсегда. Нужны оба шага, в этом порядке. 📌 **Правило на будущее:** проверять состав PR через `git diff --stat upstream/main...HEAD` **перед** объявлением готовности, а не только `git status` (последний показывает рабочее дерево, не содержимое коммитов). Иначе мусор из прошлой сессии всплывает последним. ## Что осталось **По PR — ничего, кроме ревью.** Всё выполнено: | Шаг | Статус | |---|---| | `tools/` убран из ветки, `/tools/` в `.gitignore` | ✅ коммит `9f1e6c9c` | | `git push -u origin feat/mattermost-channel` | ✅ запушено | | PR в `baldaworks/balda` открыт | ✅ **#8**, 41 файл, +5622/−18 | | CI | ✅ **8/8 PASS** | | Локальные гейты (build/vet/lint/arch-lint/no-race) | ✅ все зелёные | **Осталось (по желанию Alex):** 1. **Ревью апстрима** — ждать. Правки по замечаниям делать **в эту же ветку**: push обновит PR автоматически. 2. 🔴 **Боевая проверка на Kraken — НЕ сделана.** PR-merge ≠ «бот работает на стенде». Нужно: пересобрать образ из `9f1e6c9c`, задеплоить, `@balda` → ответ в ленте канала. См. [[personal/projects/balda/balda-kraken-mm-deploy]]. 3. **Замержить PR** (после ревью) — тогда Фаза 2 закрывается официально. ⚠️ Скрипты `tools/mm-*.sh` (9 шт.), дельты `tools/deltas-mattermost-14…26` + `deltas-goal-command-fix.json` + `deltas-mm-prefixfix.json`, `apply-project-deltas.py`, `run-project-deltas.sh`, `.deltas-state.json` — **untracked, локальный инструментарий**, git-ignored. Существенное продублировано в [[personal/projects/balda/balda-kraken-mm-deploy]] и [[personal/tech/balda-go-fx-pitfalls]]. ## Связанные заметки - [[personal/projects/balda/balda-mattermost-connector]] — Фаза 2 (код коннектора) - [[personal/projects/balda/balda-kraken-mm-deploy]] — Фаза 3 (деплой, боевые причины тишины) - [[personal/tech/balda-go-fx-pitfalls]] — fx-питфоллы + дельта-апплаер - [[personal/plans/balda-mm-endtoend-plan]] — сквозной путь - [[personal/tech/hermes-secret-redaction-bypass]] — редакция секретов