fix: пути (db, worktree) и config.yaml резолвятся рядом с .exe, а не от CWD
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user