Files
zero/internal/config/paths_test.go
smallnestandsmallnest 027a8f2768 fix(config): use ~/.config on macOS and enter setup when no provider (#371) (#372)
On macOS, DefaultUserConfigPath used Go's os.UserConfigDir which returns
~/Library/Application Support, while users (and Claude Code) place config
at ~/.config/zero/config.json. The user config was never read, leaving
activeProvider empty and the launch failing with `active provider "" not
found` on a fresh `go run cmd/zero setup`.

- Add config.UserConfigDir(): on darwin, resolve to ~/.config (honoring
  XDG_CONFIG_HOME), matching Linux and Claude Code. DefaultUserConfigPath,
  the TUI user-commands dir, and specialist manifests all route through it.
- Add config.ErrNoActiveProvider sentinel; the interactive TUI now drops
  into the setup wizard when resolve fails solely due to a missing/
  unresolvable active provider, instead of exiting fatally. Other resolve
  errors (malformed JSON, directory conflicts) remain fatal.

Note: `zero setup`/`zero auth` already MkdirAll the config dir before
writing, so the directory is auto-created on first write.

Co-authored-by: smallnest <smallnest@gmail>
2026-07-02 20:03:44 +08:00

151 lines
4.0 KiB
Go

package config
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestDefaultResolveOptionsUsesExistingConfigPathsAndProviderCommand(t *testing.T) {
userConfigRoot := setUserConfigRoot(t)
workspaceRoot := t.TempDir()
userPath := filepath.Join(userConfigRoot, "zero", "config.json")
projectPath := filepath.Join(workspaceRoot, ".zero", "config.json")
writeFileAt(t, userPath, "{}")
writeFileAt(t, projectPath, "{}")
t.Setenv("ZERO_PROVIDER_COMMAND", " zero-provider ")
options, err := DefaultResolveOptions(workspaceRoot)
if err != nil {
t.Fatalf("DefaultResolveOptions() error = %v", err)
}
if options.UserConfigPath != userPath {
t.Fatalf("UserConfigPath = %q, want %q", options.UserConfigPath, userPath)
}
if options.ProjectConfigPath != projectPath {
t.Fatalf("ProjectConfigPath = %q, want %q", options.ProjectConfigPath, projectPath)
}
if options.ProviderCommand != "zero-provider" {
t.Fatalf("ProviderCommand = %q, want zero-provider", options.ProviderCommand)
}
}
func TestDefaultResolveOptionsIgnoresMissingConfigFiles(t *testing.T) {
setUserConfigRoot(t)
workspaceRoot := t.TempDir()
options, err := DefaultResolveOptions(workspaceRoot)
if err != nil {
t.Fatalf("DefaultResolveOptions() error = %v", err)
}
if options.UserConfigPath != "" {
t.Fatalf("UserConfigPath = %q, want empty for missing file", options.UserConfigPath)
}
if options.ProjectConfigPath != "" {
t.Fatalf("ProjectConfigPath = %q, want empty for missing file", options.ProjectConfigPath)
}
}
func TestDefaultResolveOptionsRejectsDirectoryConfigPaths(t *testing.T) {
tests := []struct {
name string
makeDirectory func(t *testing.T, userConfigRoot string, workspaceRoot string) string
}{
{
name: "user config",
makeDirectory: func(t *testing.T, userConfigRoot string, workspaceRoot string) string {
t.Helper()
path := filepath.Join(userConfigRoot, "zero", "config.json")
mkdirAll(t, path)
return path
},
},
{
name: "project config",
makeDirectory: func(t *testing.T, userConfigRoot string, workspaceRoot string) string {
t.Helper()
path := filepath.Join(workspaceRoot, ".zero", "config.json")
mkdirAll(t, path)
return path
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
userConfigRoot := setUserConfigRoot(t)
workspaceRoot := t.TempDir()
path := tc.makeDirectory(t, userConfigRoot, workspaceRoot)
_, err := DefaultResolveOptions(workspaceRoot)
if err == nil {
t.Fatal("DefaultResolveOptions() error = nil, want directory error")
}
if !strings.Contains(err.Error(), path) || !strings.Contains(err.Error(), "is a directory") {
t.Fatalf("error = %q, want clear directory message for %q", err.Error(), path)
}
})
}
}
func setUserConfigRoot(t *testing.T) string {
t.Helper()
root := t.TempDir()
switch runtime.GOOS {
case "windows":
t.Setenv("APPDATA", root)
case "darwin":
t.Setenv("HOME", root)
default:
t.Setenv("XDG_CONFIG_HOME", root)
}
configRoot, err := UserConfigDir()
if err != nil {
t.Fatalf("UserConfigDir() error = %v", err)
}
return configRoot
}
func writeFileAt(t *testing.T, path string, body string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatalf("create parent directories: %v", err)
}
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatalf("write file %s: %v", path, err)
}
}
func mkdirAll(t *testing.T, path string) {
t.Helper()
if err := os.MkdirAll(path, 0o700); err != nil {
t.Fatalf("create directory %s: %v", path, err)
}
}
func TestDefaultUserConfigPathUsesXDGConfigOnMacOS(t *testing.T) {
if runtime.GOOS != "darwin" {
t.Skip("macOS-specific config path behavior")
}
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", "")
path, err := DefaultUserConfigPath()
if err != nil {
t.Fatalf("DefaultUserConfigPath() error = %v", err)
}
want := filepath.Join(home, ".config", "zero", "config.json")
if path != want {
t.Fatalf("DefaultUserConfigPath() = %q, want %q", path, want)
}
}