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

434 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:<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 обязательно.
### Команды (воспроизводимо)
```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:<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 новых коммитов чисты — проверено:
```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]] — редакция секретов