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

This commit is contained in:
Alexey Martemyanov committed 2026-09-26 10:44:19 +06:00
1 parent 47ff9bfacd
commit 06950241dd
4 files changed
+442 -13

No files matched your search

@@ -1,15 +1,25 @@
# Balda — Mattermost коннектор (архитектура)
> Создано 2026-09-26. Обновлено 2026-09-26. Статус: **КОД НАПИСАН, КОМПИЛИРУЕТСЯ**.
> Создано 2026-09-26. Обновлено 2026-09-26 (вечер). Статус: **КОД СОБИРАЕТСЯ, DI-ГРАФ ВАЛИДИРУЕТСЯ**.
> Часть проекта [[personal/projects/balda/balda-mm-delegation-plan]] (Фаза 2).
> Требование Alex: поддержка **всех** функций Балды, по новым API, production-качество.
>
> **Проверено:** `go build ./...` → exit 0, `go vet` чист. Тесты ещё не написаны.
> Осталось: **добавить импорт `mattermostfx` в `app.go`** (LSP подсветил, не успел),
> затем тесты + `go test -race ./...` + `golangci-lint`.
> **Проверено (2026-09-26):**
> - `go build ./...` → exit 0, `go vet ./...` → exit 0
> - **DI-граф собирается целиком** с `balda.mattermost.enabled=true` — проверено реальным
> `validateBaldaApplication(prepared)` через fx, а не только компиляцией
> - 3 новых теста зелёные (см. §«Тесты»)
>
> **Осталось:** 2 шага — дописать ожидание маршрута Mattermost в `TestBuiltinRoutes`
> (единственная регрессия от наших правок) + `go tool golangci-lint run`.
> Подробности в §«🔴 Текущее состояние и следующий шаг».
>
> Разделы ниже — сначала контракт/разведка, затем **фактически написанные файлы**. Ищи
> §«Что фактически написано» для реестра файлов.
>
> 🔧 **Инструмент правок:** `tools/apply-project-deltas.py` + `tools/run-project-deltas.sh` +
> `tools/deltas-mattermost*.json` — правки проекта идут **текстовыми дельтами с якорями**,
> не sed/in-place. См. §«Метод правок: дельта-апплаер».
---
@@ -252,6 +262,124 @@ MM: `Source` = новый `turncmd.SourceMattermost`, `DeliveryFormat` = `Delive
| `turncmd/turn.go` | `SourceMattermost = "mattermost"` |
| `internal/apps/balda/app.go` | `mattermostfx.Module` в список модулей (строка ~667) |
### Wiring-дельты (2026-09-26, вечер — закрыт блокер сборки)
Это то, что оставалось «незакрытым» в первой половине дня. Применено дельта-апплаером.
| Файл | Что добавлено | Зачем |
|---|---|---|
| `internal/apps/balda/app.go` | импорты `channel/mattermost` **и** `channel/mattermost/mattermostfx` | корневой пакет нужен для `mattermost.ValidateConfig`; `mattermostfx` — для `Module` |
| `internal/apps/balda/app.go` | **5 fx-провайдеров** зарегистрированных строк: `balda_mattermost_enabled`, `_server_url`, `_token`, `_bot_user_id`, `_bot_username` | модуль запрашивает их через `name:`-теги — без провайдеров fx падает «missing types» |
| `internal/apps/balda/app.go` | `validateMattermostConfig(cfg MattermostConfig) error` + вызов рядом с `validateSlackConfig` | fail-fast при `enabled=true` без URL/токена/bot_user_id |
| `internal/apps/balda/app.go` | функция вставлена **перед** `validateIdentifierValue` | порядок как у соседей |
| `internal/apps/balda/config.go` | поле `Mattermost MattermostConfig` в `BaldaConfig` + тип `MattermostConfig{Enabled, ServerURL, Token, BotUserID, BotUsername}` | конфиг-поверхность |
| `cmd/balda/command.go` | `!cfg.Balda.Mattermost.Enabled` в `validateBaldaChannelConfiguration` + подсказка про `BALDA_MATTERMOST_ENABLED` в тексте ошибки | иначе MM-only деплой отбивается «at least one channel is required» |
| `cmd/balda/balda.yaml` | дефолтный блок `balda.mattermost` с 5 ключами и комментариями | шаблон генератора |
| `docs/reference/configuration.md` | 5 строк описания `balda.mattermost.*` (перед `balda.slack.bot_token`) | документация ключей |
| `docs/commands.md` | абзац про поведение MM (mention в каналах, `/slash`-команды, нет typing) + колонка `Mattermost` в таблице + строка `/plugin ...` | таблица доступности команд |
⚠️ **`app.go` правился в 4 захода** (импорт → провайдер enabled → валидация → остальные 4 строки).
Каждый заход — отдельная дельта-группа; всё в `tools/deltas-mattermost*.json`.
### 🔴 Три реальных дефекта, найденных тестами (не компиляцией)
Компиляция проходила, но fx-граф был сломан. Поймано только запуском
`validateBaldaApplication` с включённым Mattermost.
**1. `fx.In` нельзя аннотировать внешним `fx.ParamTags`** — дефект в `mattermostfx/module.go`.
Симптом:
```
Failed: invalid annotation function func(mattermostfx.IngressParams) (*mattermost.Ingress, error):
fx.In structs cannot be annotated with fx.ParamTags or fx.From
```
Причина: в `module.go` `NewIngress` был обёрнут в `fx.Annotate(NewIngress, fx.ParamTags(...))`.
Но `IngressParams` **уже** содержит `fx.In` — его теги обязаны жить **внутри** структуры.
**Правильно (как в `zulipfx`/`zulip.ServerParams`):**
```go
// ingress.go — теги на полях структуры
type IngressParams struct {
fx.In
Processor mattermost.InboundProcessor `optional:"true"`
Client *mattermost.Client
Commands *commandcmd.Registry
Enabled bool `name:"balda_mattermost_enabled"`
BotUserID string `name:"balda_mattermost_bot_user_id"`
BotUsername string `name:"balda_mattermost_bot_username"`
Logger zerolog.Logger
}
// module.go — голое предоставление, БЕЗ fx.Annotate/fx.ParamTags
fx.Provide(NewIngress),
```
⚠️ `Commands` был `*commandcmd.Registry `optional:"true"`` — заменён на обязательный
`*commandcmd.Registry` (как у Zulip). `commandcmd.Registry` в графе есть всегда.
**2. Отсутствовали провайдеры зарегистрированных строк.**
Симптом: `missing types: string[name="balda_mattermost_server_url"] (did you mean to Provide it?)`
×3 (server_url, token, bot_user_id).
Причина: модуль читает значения через `fx.ParamTags`/`name:`, а провайдеров этих имён в
`app.go` не было. У Zulip они есть (`balda_zulip_*`) — для MM забыли.
**Правильно:** 5 `fx.Provide(fx.Annotate(func() ..., fx.ResultTags(`name:"balda_mattermost_*"`)))`.
Для несуществующих ключей YAML `strings.TrimSpace` даёт `""` — безопасно, как у Zulip.
**Каждый новый `name:`-тег в модуле = обязательный провайдер в `app.go`.**
**3. Отсутствовал импорт корневого пакета `mattermost`.**
`app.go:965: undefined: mattermost` — `validateMattermostConfig` вызывает
`mattermost.ValidateConfig`, а импортирован был только `mattermostfx`.
### Тесты (написаны 2026-09-26)
| Тест | Файл | Что проверяет |
|---|---|---|
| `TestLoadConfigDocument_AppliesMattermostEnvOverrides` | `cmd/balda/command_test.go` | 5 env-override `BALDA_MATTERMOST_*` доезжают в `doc.Balda.Mattermost` |
| `TestValidateCommandRunsValidationWithMattermostTransport` | `cmd/balda/command_test.go` | **реальный** `loadBaldaCommandConfig(true)` + `validateBaldaApplication(prepared)` — весь fx-граф с включённым MM |
| — (встроено в существующие) | — | регрессионная проверка `deliveryfmt` |
🔴 **Ключевой урок:** `go build ./...` = exit 0 **не значит**, что транспорт рабочий.
fx-граф проверяется только `validateBaldaApplication` (или `fx.ValidateApp`). Компиляция
не видит ни `fx.In`-несовместимость, ни отсутствующие провайдеры.
**Как воспроизвести проверку графа (эталон):**
```go
prepared, err := loadBaldaCommandConfig(true) // нужен os.Chdir в tempdir с .config/balda/config.yaml
err = validateBaldaApplication(prepared) // ← вот это и есть проверка графа
```
Конфиг для теста — `mattermost.enabled: true` + `server_url`/`token`/`bot_user_id`/`bot_username`,
провайдер `balda_agent: opencode_acp`. Якорь `func TestValidateCommandRunsValidation(t *testing.T) {`
использован как точка вставки.
### 🔴 Текущее состояние и следующий шаг
**Осталось 2 шага (~5 минут):**
1. **Дописать ожидание маршрута Mattermost в `TestBuiltinRoutes`.**
`internal/apps/balda/deliveryfmt/registry_test.go`, функция `TestBuiltinRoutes`.
В map `want` добавить:
```go
{transport: TransportMattermost, deliveryFormat: DeliveryFormatNone}: NamePlainText,
```
Симптом без правки: `len(BuiltinRoutes()) = 5, want 4`.
Причина: в `routes.go` мы добавили маршрут Mattermost (строка 32), а тест-ожидание не обновили.
**Это единственная регрессия, внесённая нашими правками.**
2. `go tool golangci-lint run` — ещё не запускался.
### ✅ Предсуществующие падения тестов — НЕ наши
13 пакетов падают **и на чистом `upstream/main`**. Проверено `git stash` (правки убраны →
тесты перезапущены → правки возвращены). Падающие:
`TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents` (cmd/balda),
`TestMigrateSessionMemoryLayout_*` ×4 (state), `TestWorkspaceMCPImport/Export_*` (session),
`TestPluginResolverResolvesContainedArgvFilteredEnvAndLaunchSecrets` (mcpruntime),
`TestResolveWorkspaceBaseBranch_ConfigPreferredWhenValid`,
`TestCanonicalSessionMemory*` (apps/balda).
⚠️ Не пытаться «чинить» их как часть MM-задачи — это дефекты апстрима, отдельная работа.
### Отклонения от плана разведки (важно)
1. **`server.go` → `ingress.go`.** Планировался `server.go` по образцу Zulip, но у MM нет
@@ -294,6 +422,59 @@ balda_mattermost_bot_username
Конкретные имена ключей yaml **проверить в `docs/reference/configuration.md`** перед Фазой 3 —
я их не сверял, только зафиксировал DI-теги.
✅ **Проверено 2026-09-26:** env-имена — `BALDA_MATTERMOST_ENABLED`, `BALDA_MATTERMOST_SERVER_URL`,
`BALDA_MATTERMOST_TOKEN`, `BALDA_MATTERMOST_BOT_USER_ID`, `BALDA_MATTERMOST_BOT_USERNAME`.
Механизм: `appconfig/loader.go:367` делает `SetEnvPrefix("BALDA")` +
`SetEnvKeyReplacer(".", "_")` на leaf-ключах. **Env применяется только к ключам, которые уже есть
в YAML** — поэтому блок `balda.mattermost` обязан присутствовать в `cmd/balda/balda.yaml`,
иначе env-переменные молча игнорируются. Это же верно для любого нового транспорта.
### Метод правок: дельта-апплаер (инструмент проекта)
Правки проекта идут **не sed/не in-place**, а текстовыми дельтами. Причина: правило Alex
«никогда не использовать in-place скрипты и sed; писать скрипт, потом исполнять» + нужна
идемпотентность при повторных заходах.
| Файл | Роль |
|---|---|
| `tools/apply-project-deltas.py` | движок: применяет JSON-дельты, ведёт состояние |
| `tools/run-project-deltas.sh` | обёртка: `check` / `apply` / `list` |
| `tools/deltas-mattermost.json` | основная группа (12 дельт: wiring + доки) |
| `tools/deltas-mattermost-2.json` | импорт корневого `mattermost` |
| `tools/deltas-mattermost-3.json` | тест env-override |
| `tools/deltas-mattermost-4.json` | тест fx-графа |
| `tools/deltas-mattermost-5.json` | фикс `fx.In` + `module.go` |
| `tools/deltas-mattermost-6.json` | 4 провайдера строк |
| `tools/.deltas-state.json` | состояние (sha256 применённых якорей) |
**Формат дельты:**
```json
{ "file": "путь/относительно/корня", "note": "что делаем",
"search": "уникальный фрагмент текущего текста",
"replace": "чем заменить" }
```
**Три состояния, которые различает апплаер:**
- `replace` уже есть в файле → `skipped` (идемпотентно, повторный прогон безопасен)
- `search` есть ровно один раз → `applied`
- ни `search`, ни `replace` → `error: ЯКОРЬ НЕ НАЙДЕН` (файл изменился не так, как ожидалось)
**Команды:**
```bash
cd ~/Developer/balda
bash tools/run-project-deltas.sh check tools/deltas-mattermost.json tools/.deltas-state.json
bash tools/run-project-deltas.sh apply tools/deltas-mattermost.json tools/.deltas-state.json
bash tools/run-project-deltas.sh list tools/deltas-mattermost.json tools/.deltas-state.json
```
📌 `check` **обязателен перед `apply`** — он показывает TODO/MISSING/OK по каждому якорю.
Именно на этой проверке я поймал, что дельта валидации указывала в несуществующий
`validate()` в `config.go` (валидация живёт в `app.go`) и переписал её до записи.
⚠️ **Не писать `search` с литеральными секретами** — значения собираются на хосте.
⚠️ Дельты с длинным `replace` писать **в JSON-файл через `write_file`**, не в heredoc:
кавычки и `\n` ломаются.
---
## Объём работ
@@ -352,7 +533,8 @@ Conventional Commits. Синхронизация общих веток — **mer
## Связанные заметки
- [[personal/projects/balda/mattermost-kraken-deploy]] — сервер MM на Kraken
- [[personal/projects/balda/mattermost-kraken-deploy]] — сервер MM на Kraken (Фаза 1, ✅ закрыта)
- [[personal/projects/balda/balda-mm-delegation-plan]] — общий план
- [[personal/tech/balda-go-fx-pitfalls]] — 🔴 fx-питфоллы (fx.In/ParamTags, провайдеры) + метод дельт
- [[personal/projects/balda/balda-valera]] — текущая эксплуатация Балды/Валеры
- [[personal/tech/hermes-secret-redaction-bypass]] — как обходить маскировку секретов