fix: пути (db, worktree) и config.yaml резолвятся рядом с .exe, а не от CWD
All checks were successful
CI / test (push) Successful in 1m5s
CI / build-and-package (amd64, linux) (push) Successful in 44s
CI / build-and-package (amd64, windows) (push) Successful in 43s

This commit is contained in:
Hermes
2026-08-16 20:31:08 +05:00
parent 91e91ac3ce
commit 130dc59840
3 changed files with 95 additions and 2 deletions

View File

@@ -55,6 +55,9 @@ func New(configPath, version, updateToken string) (*App, error) {
return nil, fmt.Errorf("%w: %v", ErrConfig, err)
}
// Относительные пути (db, worktree) — рядом с .exe, а не от CWD запуска.
cfg.ResolveExePaths()
// SQLite для задач и трасс
ctx := context.Background()

View File

@@ -2,6 +2,7 @@ package config
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -155,6 +156,58 @@ func TestLoad_FileNotFound(t *testing.T) {
}
}
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", "")

View File

@@ -3,6 +3,7 @@ package config
import (
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
@@ -11,11 +12,13 @@ import (
// Load читает конфиг из yaml-файла, подставляет ${VAR:-default}, накладывает
// env-переменные (тег env) и валидирует. Если path пустой — ищет config.yaml
// в CWD. Ошибка C4 если файл не существует (но не fatal при path="").
// сначала в каталоге бинаря (exeDir), затем в CWD (fallback). Ошибка C4 если
// файл не существует (но не fatal при path="").
func Load(path string) (*Config, error) {
noFile := false
if path == "" {
path = "config.yaml"
// приоритет — рядом с .exe, фоллбэк на CWD
path = firstExistingPath(filepath.Join(ExeDir(), "config.yaml"), "config.yaml")
}
raw, err := os.ReadFile(path)
if err != nil {
@@ -47,6 +50,40 @@ func Load(path string) (*Config, error) {
return cfg, nil
}
// ExeDir возвращает каталог исполняемого файла (бинаря). Используется для
// поиска config.yaml и резолва относительных путей "рядом с .exe".
func ExeDir() string {
exe, err := os.Executable()
if err != nil {
return "."
}
return filepath.Dir(exe)
}
// firstExistingPath возвращает первый существующий путь из списка.
func firstExistingPath(paths ...string) string {
for _, p := range paths {
if _, err := os.Stat(p); err == nil {
return p
}
}
return paths[len(paths)-1] // фоллбэк на последний
}
// ResolveExePaths подставляет относительные пути ПУТЕЙ (db, worktree) к каталогу
// бинаря, чтобы они были стабильны независимо от CWD запуска. Абсолютные пути
// не трогает. Вызывается после Load (в app.New), а не внутри Load, чтобы
// персист-тесты дефолтов оставались независимыми от окружения.
func (c *Config) ResolveExePaths() {
base := ExeDir()
if c.Paths.DB != "" && !filepath.IsAbs(c.Paths.DB) {
c.Paths.DB = filepath.Join(base, c.Paths.DB)
}
if c.Paths.Worktree != "" && !filepath.IsAbs(c.Paths.Worktree) {
c.Paths.Worktree = filepath.Join(base, c.Paths.Worktree)
}
}
// envLookup обрабатывает ${VAR:-default} в YAML.
func envLookup(key string) string {
parts := strings.SplitN(key, ":-", 2)