Files
obsidian-vault/personal/plans/balda-mm-pr-plan.md
T

25 KiB
Executable File
Raw Blame History

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. На работу не влияет (проверено: ответ пришёл).

Деплой — как сделано (воспроизводимо)

# 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:<branch> (не полный 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 обязательно.

Команды (воспроизводимо)

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:<id> в 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 новых коммитов чисты — проверено:

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.

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.

Связанные заметки