diff --git a/personal/projects/balda/balda-mattermost-connector.md b/personal/projects/balda/balda-mattermost-connector.md index e4a2150b..36be74af 100644 --- a/personal/projects/balda/balda-mattermost-connector.md +++ b/personal/projects/balda/balda-mattermost-connector.md @@ -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]] — как обходить маскировку секретов diff --git a/personal/projects/balda/balda-mm-delegation-plan.md b/personal/projects/balda/balda-mm-delegation-plan.md index 3a59a90f..d44333a7 100644 --- a/personal/projects/balda/balda-mm-delegation-plan.md +++ b/personal/projects/balda/balda-mm-delegation-plan.md @@ -196,9 +196,12 @@ Mattermost (Alex) — итог + артефакты 5. ✅ Админ `admin@qentra.top` создан (пароль в `.adminpw`). 6. ✅ **Регистрация закрыта** — проверено живым POST → HTTP 501 `signup_email_disabled`. 7. ✅ CF-маршрут `mm → localhost:8065` добавлен Alex'ом. -8. ⏳ `MM_SERVICESETTINGS_SITEURL` → `https://mm.qentra.top`, затем приёмка входом в браузере. +8. ✅ **`SITEURL` → `https://mm.qentra.top` — сделано 2026-09-26**: бэкап compose → правка + скриптом (не sed) → `docker compose up -d` → `healthy restarts=0` (~45 с) → ping через + туннель `{"status":"OK"}`. ⏳ Осталась приёмка входом в браузере (делает Alex). + → [[personal/projects/balda/mattermost-kraken-deploy]] -### Фаза 2 — адаптер Mattermost в Балде — ✅ КОД НАПИСАН, ⏳ ТЕСТЫ +### Фаза 2 — адаптер Mattermost в Балде — ✅ КОД СОБИРАЕТСЯ, DI-ГРАФ ВАЛИДИРУЕТСЯ Детальный док: [[personal/projects/balda/balda-mattermost-connector]]. 1. ✅ Ветка **`feat/mattermost-channel`** от чистого `upstream/main` (`de273d8f`). @@ -209,9 +212,20 @@ Mattermost (Alex) — итог + артефакты 4. ✅ Shared-слой: `ChannelTypeMattermost`, `TransportMattermost`, `NameMattermostMarkdown`, `turncmd.SourceMattermost`, `mattermostfx.Module` в `app.go`. ⚠️ `channel/router.go` **не** правился — он map-based (подтверждено). -5. ✅ `go build ./...` = exit 0, `go vet` чист. -6. 🔴 **Незакрыто:** импорт `mattermostfx` в `app.go` (LSP: `undefined: mattermostfx`). -7. ⏳ Тесты по образцу `zulip/*_test.go` + `go test -race ./...` + `golangci-lint run`. +5. ✅ `go build ./...` = exit 0, `go vet ./...` = exit 0. +6. ✅ **Импорт `mattermostfx` + корневого `mattermost` в `app.go` добавлен** (блокер снят). +7. ✅ **Wiring закрыт:** 5 fx-провайдеров `balda_mattermost_*`, `MattermostConfig` + валидация, + дефолты в `balda.yaml`, доки `configuration.md`/`commands.md`, проверка канала в `command.go`. +8. ✅ **DI-граф проверен реальным `validateBaldaApplication`** с `mattermost.enabled=true` — + найдены и починены 3 дефекта (`fx.In` + `fx.ParamTags`, отсутствующие провайдеры строк, + отсутствующий импорт). +9. ✅ 2 новых теста зелёные (env-override + fx-граф). +10. ⏳ **Осталось:** дописать ожидание маршрута Mattermost в `TestBuiltinRoutes` + (`deliveryfmt/registry_test.go`) + `go tool golangci-lint run`. + +🔴 **Урок Фазы 2:** `go build` = exit 0 **не значит** «транспорт рабочий». fx-несовместимости +и отсутствующие провайдеры видны только при `fx.ValidateApp`/`validateBaldaApplication`. +13 падающих пакетов в апстриме — **предсуществующие**, проверено `git stash`, не наши. ### Фаза 3 — оркестратор на Kraken (~1-2 дня) 1. Развернуть Балду с MM-адаптером на Kraken, провайдер — **DeepSeek** (через API-ключ, не подписка). @@ -297,10 +311,29 @@ Mattermost (Alex) — итог + артефакты - Найдены и починены 3 дефекта деплоя: `CMD ["mattermost","server"]`, `--strip-components` в tar, двойной uid-конфликт (`postgres/` vs остальное). - 🔴 Незакрыто: импорт `mattermostfx` в `app.go` + тесты коннектора. +- **2026-09-26 (вечер)**: **Фаза 1 закрыта полностью, Фаза 2 — блокер сборки снят.** + - `mm.qentra.top` подтверждён Alex'ом как работающий. `SITEURL` → `https://mm.qentra.top` + (бэкап compose → скрипт `set-siteurl.py` → `compose up -d` → `healthy restarts=0` → + ping через туннель OK). + - Коннектор: импорт `mattermostfx` **и** корневого `mattermost` в `app.go` добавлен → + `go build ./...` = exit 0. + - Закрыт весь wiring: `MattermostConfig` + `validateMattermostConfig`, 5 fx-провайдеров + `balda_mattermost_*`, дефолты в `balda.yaml`, доки `configuration.md` + `commands.md`, + Mattermost в `validateBaldaChannelConfiguration`. + - 🔴 **Внедрён метод правок через дельта-апплаер** (`tools/apply-project-deltas.py`) — + идемпотентные текстовые дельты с якорями вместо sed/in-place. + → [[personal/projects/balda/balda-mattermost-connector]] §«Метод правок» + - **Тесты поймали 3 дефекта, которых не видел `go build`:** `fx.In` + внешний + `fx.ParamTags`, отсутствующие провайдеры зарегистрированных строк, отсутствующий импорт. + Проверка графа — реальный `validateBaldaApplication`, не компиляция. + - Внесена 1 регрессия: `TestBuiltinRoutes` ждёт 4 маршрута, в `BuiltinRoutes()` их 5. + Фикс известен (дописать строку в map `want`), не применён — лимит шагов сессии. + - ⚠️ 13 падающих пакетов — **предсуществующий дефект апстрима**, подтверждено `git stash`. ## Связанные заметки - [[personal/projects/balda/mattermost-kraken-deploy]] — Фаза 1: MM на Kraken (arm64-блокер, деплой) - [[personal/projects/balda/balda-mattermost-connector]] — Фаза 2: архитектура MM-коннектора +- [[personal/tech/balda-go-fx-pitfalls]] — 🔴 fx-питфоллы + метод правок дельтами - [[personal/tech/hermes-secret-redaction-bypass]] — обход маскировки секретов Hermes - [[personal/tech/docker-data-root-mount-race]] — mount-race на Kraken (закрыт, reboot-безопасен) - [[personal/projects/balda/balda-valera]] — текущая эксплуатация Балды/Валеры (форк, старая архитектура) diff --git a/personal/projects/balda/mattermost-kraken-deploy.md b/personal/projects/balda/mattermost-kraken-deploy.md index 972a57dd..b48af0d2 100644 --- a/personal/projects/balda/mattermost-kraken-deploy.md +++ b/personal/projects/balda/mattermost-kraken-deploy.md @@ -5,8 +5,9 @@ > админ создан, **регистрация закрыта** (проверено живым запросом → HTTP 501). > Часть проекта [[personal/projects/balda/balda-mm-delegation-plan]]. > -> **Осталось:** Alex добавил маршрут `mm → 8065` в CF-туннель (подтвердил 2026-09-26). -> Нужно: `SITEURL` → `https://mm.qentra.top` + приёмка входом в браузере. +> **Осталось:** Alex подтвердил, что `mm.qentra.top` работает (2026-09-26). +> **Сделано:** `SITEURL` → `https://mm.qentra.top`, MM перезапущен, проверено через туннель. +> **Осталось:** приёмка входом в браузере (делает Alex). --- @@ -297,9 +298,33 @@ ssh kraken "docker ps --format '{{.Names}}\t{{.Status}}' | grep -E 'mattermost|m | **Регистрация закрыта** | ✅ **HTTP 501 на самописную регистрацию (живая проверка)** | | `config.json` = рантайм | ✅ приведены к согласию (`EnableUserCreation/OpeNserver: false`) | | Cloudflare `mm → 8065` | ✅ **добавил Alex 2026-09-26** («up») | -| `SITEURL` → `https://mm.qentra.top` | ⏳ **следующий шаг** | +| `SITEURL` → `https://mm.qentra.top` | ✅ **сделано 2026-09-26** (бэкап → скрипт → `compose up -d` → проверка) | +| Ping через туннель `https://mm.qentra.top/api/v4/system/ping` | ✅ `{"status":"OK"}` | +| `mmctl config get ServiceSettings.SiteURL` | ✅ `"https://mm.qentra.top"` | | Приёмка входом в браузере | ⏳ **делает Alex** | +### Как менялся SITEURL (2026-09-26, выполнено) + +Порядок по правилу «коммит ДО → правка → заливка → проверка»: + +1. **Бэкап compose** на хосте: `cp docker-compose.yml backups/docker-compose.yml.bak-$(date +%Y%m%d-%H%M%S)` + → `/home/kraken/mattermost/backups/docker-compose.yml.bak-20260926-103937`. +2. **Правка скриптом, не sed.** `/tmp/mm-deploy/set-siteurl.py` (локально на Mac) → + `ssh kraken 'cat > /tmp/set-siteurl.py' < set-siteurl.py` → `ssh kraken 'python3 /tmp/set-siteurl.py'`. + Скрипт идемпотентен (NOOP при совпадении), пишет во временный файл и делает атомарный `replace`. + Вывод: `OK: MM_SERVICESETTINGS_SITEURL: "http://192.168.1.14:8065" -> MM_SERVICESETTINGS_SITEURL: "https://mm.qentra.top"`. +3. **Перезапуск:** `ssh kraken 'cd /home/kraken/mattermost && docker compose up -d mattermost'` + → `mattermost Recreated → Started`, `mm-postgres Healthy`. +4. **Ожидание health** (`docker inspect mattermost --format '{{.State.Health.Status}} restarts={{.RestartCount}}'`): + `starting` ×8 итераций по 5 с → **`healthy restarts=0`** на 9-й (~45 с). +5. **Проверка с Mac:** `curl -s https://mm.qentra.top/api/v4/system/ping` → `{"status":"OK",...}`; + `docker exec mattermost mmctl --local config get ServiceSettings.SiteURL` → `"https://mm.qentra.top"`. + +⚠️ `sed -i` из старой инструкции ниже **не использовать** — он оставлен как история. Правка +compose — только скриптом с бэкапом. +⚠️ При пересборке compose из генератора SITEURL перезапишется на LAN-значение. Долгосрочно — + менять значение в генераторе, а не в файле. + ### Команды для следующего шага ```bash diff --git a/personal/tech/balda-go-fx-pitfalls.md b/personal/tech/balda-go-fx-pitfalls.md new file mode 100644 index 00000000..ead0c97e --- /dev/null +++ b/personal/tech/balda-go-fx-pitfalls.md @@ -0,0 +1,189 @@ +# Balda — Go/fx pitfalls и метод правок (зеркало скилла) + +> Создано 2026-09-26. Зеркало секции скилла `devops/docker-balda-service`, добавленной 2026-09-26. +> Источник: работа над Mattermost-адаптером Балды (Фаза 2). +> Проектные доки: [[personal/projects/balda/balda-mattermost-connector]], +> [[personal/projects/balda/balda-mm-delegation-plan]]. + +--- + +## 🔴 `go build` = exit 0 ≠ рабочий транспорт + +Компилятор не видит ошибок DI-графа. `go build ./...` проходит, а приложение падает +на старте с `fx.ValidateApp`. Проверять транспорт **только** реальным запуском валидации: + +```go +prepared, err := loadBaldaCommandConfig(true) // нужен os.Chdir в tempdir с .config/balda/config.yaml +err = validateBaldaApplication(prepared) // ← вот это и есть проверка графа +``` + +Эталонный тест: `TestValidateCommandRunsValidationWithMattermostTransport` +в `cmd/balda/command_test.go`. На нём поймано 3 дефекта, которые `go build` пропустил. + +--- + +## 🔴 Дефект 1: `fx.In` + внешний `fx.ParamTags` + +**Симптом:** +``` +Failed: invalid annotation function func(mattermostfx.IngressParams) (*mattermost.Ingress, error): + fx.In structs cannot be annotated with fx.ParamTags or fx.From +``` + +**Причина:** `fx.Annotate(NewIngress, fx.ParamTags(...))`, где `IngressParams` **уже** содержит `fx.In`. +Теги обязаны жить внутри структуры, снаружи аннотировать нельзя. + +**Неправильно:** +```go +fx.Annotate(NewIngress, + fx.ParamTags(``, ``, ``, `name:"balda_mattermost_enabled"`, + `name:"balda_mattermost_bot_user_id"`, + `name:"balda_mattermost_bot_username"`, ``), +), +``` + +**Правильно:** +```go +// структура — теги на полях +type IngressParams struct { + fx.In + Processor mattermost.InboundProcessor `optional:"true"` + Client *mattermost.Client + Commands *commandcmd.Registry // обязательный, не optional + Enabled bool `name:"balda_mattermost_enabled"` + BotUserID string `name:"balda_mattermost_bot_user_id"` + BotUsername string `name:"balda_mattermost_bot_username"` + Logger zerolog.Logger +} + +// предоставление — голое, без fx.Annotate +fx.Provide(NewIngress), +``` + +Эталон в апстриме: `zulip.ServerParams` + `zulip.NewServer` в `zulipfx/module.go` (строка 24) — +`zulip.NewServer` объявлен **без** `fx.Annotate`. + +⚠️ Поле-порт (`*commandcmd.Registry`) делать **обязательным**. `optional:"true"` на нём — лишнее: +в графе он есть всегда, а optional маскирует ошибки конфигурации. + +--- + +## 🔴 Дефект 2: нет провайдеров зарегистрированных строк + +**Симптом:** +``` +missing types: + - string[name="balda_mattermost_server_url"] (did you mean to Provide it?) + - string[name="balda_mattermost_token"] + - string[name="balda_mattermost_bot_user_id"] +``` + +**Причина:** модуль читает значения через `name:`-теги, а провайдеров этих имён в composition root нет. +У Zulip они есть (`balda_zulip_*`), для нового транспорта забыли. + +**Правило: каждый `name:`-тег в модуле = обязательный провайдер в `app.go`.** + +```go +fx.Provide( + fx.Annotate( + func() string { return strings.TrimSpace(cfg.Balda.Mattermost.ServerURL) }, + fx.ResultTags(`name:"balda_mattermost_server_url"`), + ), +), +``` + +`strings.TrimSpace` для отсутствующего ключа YAML даёт `""` — без паники, как у Zulip. +Образец: блок `balda_zulip_*` в `internal/apps/balda/app.go` (~строки 576-606). + +--- + +## 🔴 Дефект 3: забытый импорт корневого пакета + +`app.go:965: undefined: mattermost` при вызове `mattermost.ValidateConfig`, если импортирован +только `mattermostfx`. Транспорт требует **оба** импорта: +```go +"github.com/baldaworks/balda/internal/apps/balda/channel/mattermost" +"github.com/baldaworks/balda/internal/apps/balda/channel/mattermost/mattermostfx" +``` + +--- + +## ⚠️ Env-override работает только по ключам, уже присутствующим в YAML + +`appconfig/loader.go:367` — `SetEnvPrefix("BALDA")` + `SetEnvKeyReplacer(".", "_")` + +`AutomaticEnv()` **по leaf-ключам конфига**. Поэтому блок нового транспорта обязан быть +в `cmd/balda/balda.yaml` — иначе `BALDA__*` переменные **молча игнорируются**. + +Порядок для нового транспорта: +1. блок в `cmd/balda/balda.yaml` (иначе env мёртв) +2. `MattermostConfig`-подобная структура + поле в `BaldaConfig` +3. провайдеры в `app.go` +4. `validateConfig` + вызов рядом с `validateSlackConfig` +5. включить в `validateBaldaChannelConfiguration` (`cmd/balda/command.go`) — + иначе деплой только на этом транспорте отбивается «at least one channel is required» + +--- + +## ✅ 13 пакетов падают в апстриме изначально + +Не пытаться «чинить» как часть своей задачи — это дефект апстрима. Проверять `git stash` +перед выводами о регрессии. + +Падающие в `baldaworks/balda@de273d8f`: +- `TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents` (cmd/balda) +- `TestMigrateSessionMemoryLayout_*` ×4 (state) +- `TestWorkspaceMCPImport/Export_UsesPersistedSessionMetadata` (session) +- `TestPluginResolverResolvesContainedArgvFilteredEnvAndLaunchSecrets` (mcpruntime) +- `TestResolveWorkspaceBaseBranch_ConfigPreferredWhenValidVal`, `TestCanonicalSessionMemory*` (apps/balda) + +**Метод проверки регрессии:** +```bash +git stash push -q -m baseline --include-untracked +go test ./<пакет>/ -count=1 # смотрим падения БЕЗ правок +git stash pop -q # правки возвращаются +``` +⚠️ `git stash` может задеть чужие стэши — в форке лежат `whale-20260623` и +`feat/zulip-events-polling`. Проверять `git stash list` до и после. + +--- + +## Метод правок: дельта-апплаер вместо sed + +Правило Alex: **никаких in-place скриптов и sed**. Инструмент в `~/Developer/balda/tools/`: + +| Файл | Роль | +|---|---| +| `apply-project-deltas.py` | движок (JSON-дельты + состояние) | +| `run-project-deltas.sh` | обёртка `check` / `apply` / `list` | +| `deltas-.json` | группы правок | +| `.deltas-state.json` | sha256 применённых якорей | + +```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 +``` + +**Формат дельты:** +```json +{ "file": "путь/от/корня", "note": "что делаем", + "search": "уникальный фрагмент текущего текста", + "replace": "чем заменить" } +``` + +**Три исхода:** `replace` уже есть → `skipped` (идемпотентно); `search` ровно 1 раз → `applied`; +ни того ни другого → `error: ЯКОРЬ НЕ НАЙДЕН`. + +📌 `check` **обязателен перед `apply`** — на нём поймано, что дельта валидации целилась +в несуществующий `validate()` в `config.go` (валидация живёт в `app.go`), и переписана до записи. + +⚠️ Дельты с длинным `replace` писать в JSON через `write_file`, **не** heredoc — кавычки и `\n` +ломаются молча. Это тот же питфолл, что у SSH-heredoc с вложенными кавычками. + +--- + +## Связанные заметки + +- [[personal/projects/balda/balda-mattermost-connector]] — Фаза 2, где всё это найдено +- [[personal/projects/balda/balda-mm-delegation-plan]] — общий план +- [[personal/tech/hermes-secret-redaction-bypass]] — маскировка секретов в tool-выводе