Files
ratatoskr-go/internal/config/config_test.go
Hermes ad9c2dd522
All checks were successful
CI / test (push) Successful in 41s
CI / build-and-package (amd64, linux) (push) Successful in 38s
CI / build-and-package (amd64, windows) (push) Successful in 35s
feat(config): уровень логирования log.level (info|debug), по умолчанию info
Отладочные логи API-вызовов к opencode serve теперь включаются только при
log.level=debug (гейт через Client.Debug <- Runner.Debug <- cfg.Log.Debug()).
По умолчанию 'info' — отладочных логов нет.

- config: добавлен LogCfg{Level}, дефолт 'info', валидация (info|debug)
- opencode: Client.Debug и Runner.Debug — гейттят логи do()
- app: Runner.Debug из cfg.Log.Debug()
- config.yaml.example: задокументирован log.level
- тесты: дефолт=info, level=debug, невалидный уровень
2026-08-18 18:44:37 +05:00

313 lines
7.6 KiB
Go
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.
package config
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func writeCfg(t *testing.T, content string) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "cfg*.yaml")
if err != nil {
t.Fatalf("temp: %v", err)
}
if _, err := f.WriteString(content); err != nil {
t.Fatalf("write: %v", err)
}
f.Close()
return f.Name()
}
func TestLoad_FullConfig(t *testing.T) {
t.Setenv("TG_TOKEN", "tok123")
yaml := `
telegram:
token: "${TG_TOKEN}"
chat_id: "123"
opencode:
bin: "my-opencode"
hard_timeout: "10m"
`
path := writeCfg(t, yaml)
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Telegram.Token != "tok123" {
t.Errorf("tg token = %q", cfg.Telegram.Token)
}
if cfg.Telegram.ChatID != "123" {
t.Errorf("tg chat_id = %q", cfg.Telegram.ChatID)
}
if cfg.OpenCode.Bin != "my-opencode" {
t.Errorf("opencode bin = %q", cfg.OpenCode.Bin)
}
if cfg.OpenCode.HardTimeout.Duration() != 10*time.Minute {
t.Errorf("hard timeout = %v", cfg.OpenCode.HardTimeout)
}
}
func TestLoad_Defaults(t *testing.T) {
t.Setenv("TG_TOKEN", "tok")
t.Setenv("TG_CHAT_ID", "42")
yaml := `
telegram:
token: "${TG_TOKEN}"
chat_id: "${TG_CHAT_ID}"
`
path := writeCfg(t, yaml)
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
// default opencode.bin
if cfg.OpenCode.Bin != "opencode" {
t.Errorf("bin = %q, want opencode", cfg.OpenCode.Bin)
}
if cfg.OpenCode.HardTimeout.Duration() != 20*time.Minute {
t.Errorf("hard timeout = %v", cfg.OpenCode.HardTimeout)
}
if cfg.OpenCode.IdleTimeout.Duration() != 5*time.Minute {
t.Errorf("idle timeout = %v", cfg.OpenCode.IdleTimeout)
}
if cfg.OpenCode.Serve.Enabled {
t.Errorf("serve.enabled = true, want false (дефолт)")
}
if cfg.OpenCode.Serve.Hostname != "127.0.0.1" {
t.Errorf("serve.hostname = %q, want 127.0.0.1", cfg.OpenCode.Serve.Hostname)
}
if cfg.OpenCode.Serve.Port != 4096 {
t.Errorf("serve.port = %d, want 4096", cfg.OpenCode.Serve.Port)
}
if cfg.Paths.Worktree != "./worktrees" {
t.Errorf("worktree = %q, want ./worktrees", cfg.Paths.Worktree)
}
}
func TestLoad_LogLevelDefault(t *testing.T) {
t.Setenv("TG_TOKEN", "tok")
t.Setenv("TG_CHAT_ID", "42")
yaml := `telegram:
token: "${TG_TOKEN}"
chat_id: "${TG_CHAT_ID}"
`
cfg, err := Load(writeCfg(t, yaml))
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Log.Level != "info" {
t.Errorf("log.level = %q, want info (дефолт)", cfg.Log.Level)
}
if cfg.Log.Debug() {
t.Errorf("Debug() = true при уровне info, want false")
}
}
func TestLoad_LogLevelDebug(t *testing.T) {
t.Setenv("TG_TOKEN", "tok")
t.Setenv("TG_CHAT_ID", "42")
yaml := `telegram:
token: "${TG_TOKEN}"
chat_id: "${TG_CHAT_ID}"
log:
level: debug
`
cfg, err := Load(writeCfg(t, yaml))
if err != nil {
t.Fatalf("Load: %v", err)
}
if !cfg.Log.Debug() {
t.Errorf("Debug() = false при level=debug, want true")
}
}
func TestLoad_LogLevelInvalid(t *testing.T) {
t.Setenv("TG_TOKEN", "tok")
t.Setenv("TG_CHAT_ID", "42")
yaml := `telegram:
token: "${TG_TOKEN}"
chat_id: "${TG_CHAT_ID}"
log:
level: warn
`
_, err := Load(writeCfg(t, yaml))
if !errors.Is(err, ErrInvalidFormat) {
t.Fatalf("Load: err = %v, want ErrInvalidFormat", err)
}
}
func TestLoad_OpenCodeConfigDir(t *testing.T) {
t.Setenv("TG_TOKEN", "tok")
t.Setenv("TG_CHAT_ID", "42")
// без config_dir — пусто
yaml := `telegram:
token: "${TG_TOKEN}"
chat_id: "${TG_CHAT_ID}"
`
cfg, err := Load(writeCfg(t, yaml))
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.OpenCode.ConfigDir != "" {
t.Errorf("config_dir = %q, want пусто", cfg.OpenCode.ConfigDir)
}
// через env OPENCODE_CONFIG_DIR
t.Setenv("OPENCODE_CONFIG_DIR", "/opt/agents")
cfg, err = Load(writeCfg(t, yaml))
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.OpenCode.ConfigDir != "/opt/agents" {
t.Errorf("config_dir = %q, want /opt/agents", cfg.OpenCode.ConfigDir)
}
}
func TestLoad_EnvOverride(t *testing.T) {
t.Setenv("TG_TOKEN", "tok")
t.Setenv("TG_CHAT_ID", "42")
// env override для opencode.timeout
t.Setenv("OPENCODE_CONFIG", "/custom/opencode.json")
yaml := `telegram:
token: "${TG_TOKEN}"
chat_id: "${TG_CHAT_ID}"
`
path := writeCfg(t, yaml)
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.OpenCode.Config != "/custom/opencode.json" {
t.Errorf("opencode config = %q", cfg.OpenCode.Config)
}
}
func TestLoad_MissingEnvVarUsesDefault(t *testing.T) {
t.Setenv("TG_TOKEN", "tok")
t.Setenv("TG_CHAT_ID", "42")
yaml := `telegram:
token: "${TG_TOKEN}"
chat_id: "${TG_CHAT_ID}"
opencode:
db_path: "${OPENCODE_DB:-/default/db.sqlite}"
`
path := writeCfg(t, yaml)
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.OpenCode.DBPath != "/default/db.sqlite" {
t.Errorf("db_path = %q, want /default/db.sqlite", cfg.OpenCode.DBPath)
}
}
func TestLoad_FileNotFound(t *testing.T) {
_, err := Load("/nonexistent/config.yaml")
if err == nil || !strings.Contains(err.Error(), "file not found") {
t.Fatalf("ожидалась ошибка file not found, got %v", err)
}
}
func TestResolveExePaths_RelativeToExe(t *testing.T) {
t.Setenv("TG_TOKEN", "tok")
t.Setenv("TG_CHAT_ID", "42")
yaml := `telegram:
token: "${TG_TOKEN}"
chat_id: "${TG_CHAT_ID}"
paths:
db: "./ratatoskr.db"
worktree: "./worktrees"
`
cfg, err := Load(writeCfg(t, yaml))
if err != nil {
t.Fatalf("Load: %v", err)
}
cfg.ResolveExePaths()
base := ExeDir()
if cfg.Paths.DB != filepath.Join(base, "ratatoskr.db") {
t.Errorf("db = %q, want %q", cfg.Paths.DB, filepath.Join(base, "ratatoskr.db"))
}
if cfg.Paths.Worktree != filepath.Join(base, "worktrees") {
t.Errorf("worktree = %q, want %q", cfg.Paths.Worktree, filepath.Join(base, "worktrees"))
}
}
func TestResolveExePaths_AbsoluteKept(t *testing.T) {
t.Setenv("TG_TOKEN", "tok")
t.Setenv("TG_CHAT_ID", "42")
absDB := filepath.Join(string(filepath.Separator), "data", "ratatoskr.db") // абсолютный для текущей ОС
absWt := filepath.Join(string(filepath.Separator), "worktrees")
yaml := `telegram:
username: "${TG_TOKEN}"
chat_id: "${TG_CHAT_ID}"
paths:
db: "` + absDB + `"
worktree: "` + absWt + `"
`
cfg, err := Load(writeCfg(t, yaml))
if err != nil {
t.Fatalf("Load: %v", err)
}
cfg.ResolveExePaths()
if cfg.Paths.DB != absDB {
t.Errorf("db = %q, want абсолютный путь сохранён %q", cfg.Paths.DB, absDB)
}
if cfg.Paths.Worktree != absWt {
t.Errorf("worktree = %q, want абсолютный путь сохранён %q", cfg.Paths.Worktree, absWt)
}
}
func TestLoad_MissingRequired(t *testing.T) {
// пустой конфиг — все обязательные поля отсутствуют
t.Setenv("TG_TOKEN", "")
t.Setenv("TG_CHAT_ID", "")
yaml := `telegram:
token: ""
chat_id: ""
`
path := writeCfg(t, yaml)
_, err := Load(path)
if err == nil {
t.Fatal("ожидалась ошибка валидации")
}
if !strings.Contains(err.Error(), "telegram.token") {
t.Errorf("ожидалось telegram.token в ошибке, got %v", err)
}
if !strings.Contains(err.Error(), "telegram.chat_id") {
t.Errorf("ожидалось telegram.chat_id в ошибке, got %v", err)
}
}
func TestLoad_InvalidDuration(t *testing.T) {
t.Setenv("TG_TOKEN", "x")
t.Setenv("TG_CHAT_ID", "x")
yaml := `telegram:
token: "x"
chat_id: "x"
opencode:
hard_timeout: "не-длительность"
`
path := writeCfg(t, yaml)
_, err := Load(path)
if err == nil || !strings.Contains(err.Error(), "parse duration") {
t.Fatalf("ожидалась ошибка парсинга duration, got %v", err)
}
}