feat(client): config search chain, --repo zero-config override, actionable missing-config error
sync/status now resolve the work repository from a fallback chain: --repo flag > --config flag > AGENT_LIB_CONFIG env > user-level config (XDG / AppData) > admin platform default. --repo works without any config file; tokens fall back to AGENT_LIB_GIT_TOKEN/AGENT_LIB_GIT_USER without ever overriding a configured token. A completely empty chain yields an error listing every searched location, a minimal config example and the --repo shortcut. Closes beads: agent-lib-9o8
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeConfigFile(t *testing.T, path, repoURL string) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := json.Marshal(map[string]string{"repo_url": repoURL, "ref": "main"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// runIsolated points HOME/XDG at empty temp dirs and clears the override env
|
||||
// so chain tests never see the real machine's config.
|
||||
func runIsolated(t *testing.T, fn func()) {
|
||||
t.Helper()
|
||||
base := t.TempDir()
|
||||
home := filepath.Join(base, "home")
|
||||
xdg := filepath.Join(base, "xdg-config")
|
||||
for _, d := range []string{home, xdg} {
|
||||
if err := os.MkdirAll(d, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("XDG_CONFIG_HOME", xdg)
|
||||
t.Setenv(ConfigEnv, "")
|
||||
fn()
|
||||
}
|
||||
|
||||
func TestUserConfigPathHonorsXDG(t *testing.T) {
|
||||
runIsolated(t, func() {
|
||||
base := t.TempDir()
|
||||
t.Setenv("XDG_CONFIG_HOME", base)
|
||||
want := filepath.Join(base, "agent-lib", "config.json")
|
||||
if got := UserConfigPath(); got != want {
|
||||
t.Errorf("UserConfigPath() = %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadClientConfigChainOrder(t *testing.T) {
|
||||
runIsolated(t, func() {
|
||||
base := t.TempDir()
|
||||
flagCfg := filepath.Join(base, "flag.json")
|
||||
envCfg := filepath.Join(base, "env.json")
|
||||
userCfg := filepath.Join(os.Getenv("XDG_CONFIG_HOME"), "agent-lib", "config.json")
|
||||
writeConfigFile(t, flagCfg, "https://flag.example/repo")
|
||||
writeConfigFile(t, envCfg, "https://env.example/repo")
|
||||
writeConfigFile(t, userCfg, "https://user.example/repo")
|
||||
|
||||
assertRepo := func(name, got, want string) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Errorf("%s: repo_url = %q, want %q", name, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := LoadClientConfig(flagCfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertRepo("flag beats env and user", cfg.RepoURL, "https://flag.example/repo")
|
||||
|
||||
t.Setenv(ConfigEnv, envCfg)
|
||||
cfg, err = LoadClientConfig("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertRepo("env beats user", cfg.RepoURL, "https://env.example/repo")
|
||||
|
||||
t.Setenv(ConfigEnv, "")
|
||||
cfg, err = LoadClientConfig("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertRepo("user picked up without flags", cfg.RepoURL, "https://user.example/repo")
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadClientConfigMissingIsActionable(t *testing.T) {
|
||||
runIsolated(t, func() {
|
||||
_, err := LoadClientConfig("")
|
||||
if err == nil {
|
||||
t.Fatal("empty chain must fail")
|
||||
}
|
||||
var missing *MissingConfigError
|
||||
if e, ok := err.(*MissingConfigError); !ok {
|
||||
t.Fatalf("error must be MissingConfigError, got %T: %v", err, err)
|
||||
} else {
|
||||
missing = e
|
||||
}
|
||||
msg := missing.Error()
|
||||
userPath := UserConfigPath()
|
||||
for _, want := range []string{
|
||||
"no client config found",
|
||||
userPath,
|
||||
DefaultConfigPath(),
|
||||
`"repo_url"`,
|
||||
"--repo",
|
||||
} {
|
||||
if !strings.Contains(msg, want) {
|
||||
t.Errorf("error message must mention %q:\n%s", want, msg)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadOptionalConfigNilWhenEmpty(t *testing.T) {
|
||||
runIsolated(t, func() {
|
||||
cfg, err := LoadOptionalConfig("")
|
||||
if err != nil {
|
||||
t.Fatalf("empty chain must be (nil, nil), got %v", err)
|
||||
}
|
||||
if cfg != nil {
|
||||
t.Errorf("cfg must be nil, got %+v", cfg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadOptionalConfigMalformedStillFails(t *testing.T) {
|
||||
runIsolated(t, func() {
|
||||
userCfg := filepath.Join(os.Getenv("XDG_CONFIG_HOME"), "agent-lib", "config.json")
|
||||
writeConfigFile(t, userCfg, "")
|
||||
if _, err := LoadOptionalConfig(""); err == nil {
|
||||
t.Error("user config with empty repo_url must fail even in optional mode")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadClientConfigExplicitFlagMustExist(t *testing.T) {
|
||||
runIsolated(t, func() {
|
||||
_, err := LoadClientConfig(filepath.Join(t.TempDir(), "nope.json"))
|
||||
if err == nil || !strings.Contains(err.Error(), "nope.json") {
|
||||
t.Errorf("explicit --config pointing nowhere must name the path, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFillTokenAuthEnvOnlyFillsEmptyToken(t *testing.T) {
|
||||
t.Setenv("AGENT_LIB_GIT_TOKEN", "env-token")
|
||||
t.Setenv("AGENT_LIB_GIT_USER", "env-user")
|
||||
fromEnv := &ClientConfig{}
|
||||
fromEnv.FillTokenAuth()
|
||||
if fromEnv.Token != "env-token" || fromEnv.TokenUser != "env-user" {
|
||||
t.Errorf("empty token must be filled from env: %+v", fromEnv)
|
||||
}
|
||||
configured := &ClientConfig{Token: "config-token", TokenUser: "config-user"}
|
||||
configured.FillTokenAuth()
|
||||
if configured.Token != "config-token" || configured.TokenUser != "config-user" {
|
||||
t.Errorf("env must never override a config token: %+v", configured)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user