package config import ( "os" "path/filepath" "strings" "testing" "time" ) func TestResolvePathPrecedence(t *testing.T) { t.Setenv("AMCS_CONFIG", "/tmp/from-env.yaml") if got := ResolvePath("/tmp/explicit.yaml"); got != "/tmp/explicit.yaml" { t.Fatalf("ResolvePath explicit = %q, want %q", got, "/tmp/explicit.yaml") } if got := ResolvePath(""); got != "/tmp/from-env.yaml" { t.Fatalf("ResolvePath env = %q, want %q", got, "/tmp/from-env.yaml") } } func TestResolvePathIgnoresBareYAMLExtension(t *testing.T) { t.Setenv("AMCS_CONFIG", "/tmp/from-env.yaml") if got := ResolvePath(".yaml"); got != "/tmp/from-env.yaml" { t.Fatalf("ResolvePath(.yaml) = %q, want %q", got, "/tmp/from-env.yaml") } if got := ResolvePath(".yml"); got != "/tmp/from-env.yaml" { t.Fatalf("ResolvePath(.yml) = %q, want %q", got, "/tmp/from-env.yaml") } } const v2ConfigYAML = ` version: 2 server: port: 8080 mcp: path: "/mcp" session_timeout: "30m" auth: keys: - id: "test" value: "secret" database: url: "postgres://from-file" ai: providers: default: type: "litellm" base_url: "http://localhost:4000/v1" api_key: "file-key" embeddings: dimensions: 1536 primary: provider: "default" model: "text-embed" metadata: primary: provider: "default" model: "gpt-4" search: default_limit: 10 max_limit: 50 logging: level: "info" ` func TestLoadAppliesEnvOverrides(t *testing.T) { configPath := filepath.Join(t.TempDir(), "test.yaml") if err := os.WriteFile(configPath, []byte(v2ConfigYAML), 0o600); err != nil { t.Fatalf("write config: %v", err) } t.Setenv("AMCS_DATABASE_URL", "postgres://from-env") t.Setenv("AMCS_LITELLM_API_KEY", "env-key") t.Setenv("AMCS_SERVER_PORT", "9090") cfg, loadedFrom, err := Load(configPath) if err != nil { t.Fatalf("Load() error = %v", err) } if loadedFrom != configPath { t.Fatalf("loadedFrom = %q, want %q", loadedFrom, configPath) } if cfg.Database.URL != "postgres://from-env" { t.Fatalf("database url = %q, want env override", cfg.Database.URL) } if cfg.AI.Providers["default"].APIKey != "env-key" { t.Fatalf("litellm api key = %q, want env override", cfg.AI.Providers["default"].APIKey) } if cfg.Server.Port != 9090 { t.Fatalf("server port = %d, want 9090", cfg.Server.Port) } if cfg.MCP.SessionTimeout != 30*time.Minute { t.Fatalf("mcp session timeout = %v, want 30m", cfg.MCP.SessionTimeout) } } func TestLoadAppliesOllamaEnvOverrides(t *testing.T) { configPath := filepath.Join(t.TempDir(), "test.yaml") if err := os.WriteFile(configPath, []byte(` version: 2 server: port: 8080 mcp: path: "/mcp" session_timeout: "10m" auth: keys: - id: "test" value: "secret" database: url: "postgres://from-file" ai: providers: local: type: "ollama" base_url: "http://localhost:11434/v1" api_key: "ollama" embeddings: dimensions: 768 primary: provider: "local" model: "nomic-embed-text" metadata: primary: provider: "local" model: "llama3.2" search: default_limit: 10 max_limit: 50 logging: level: "info" `), 0o600); err != nil { t.Fatalf("write config: %v", err) } t.Setenv("AMCS_OLLAMA_BASE_URL", "https://ollama.example.com/v1") t.Setenv("AMCS_OLLAMA_API_KEY", "remote-key") cfg, _, err := Load(configPath) if err != nil { t.Fatalf("Load() error = %v", err) } p := cfg.AI.Providers["local"] if p.BaseURL != "https://ollama.example.com/v1" { t.Fatalf("ollama base url = %q, want env override", p.BaseURL) } if p.APIKey != "remote-key" { t.Fatalf("ollama api key = %q, want env override", p.APIKey) } } func TestLoadMigratesV1Config(t *testing.T) { configPath := filepath.Join(t.TempDir(), "v1.yaml") v1 := ` server: port: 8080 mcp: path: "/mcp" session_timeout: "10m" auth: keys: - id: "test" value: "secret" database: url: "postgres://from-file" ai: provider: "litellm" embeddings: model: "text-embed" dimensions: 1536 metadata: model: "gpt-4" temperature: 0.2 fallback_models: ["gpt-3.5"] litellm: base_url: "http://localhost:4000/v1" api_key: "file-key" search: default_limit: 10 max_limit: 50 logging: level: "info" ` if err := os.WriteFile(configPath, []byte(v1), 0o600); err != nil { t.Fatalf("write config: %v", err) } cfg, _, err := Load(configPath) if err != nil { t.Fatalf("Load() error = %v", err) } if cfg.Version != CurrentConfigVersion { t.Fatalf("version = %d, want %d", cfg.Version, CurrentConfigVersion) } if p, ok := cfg.AI.Providers["default"]; !ok || p.Type != "litellm" || p.APIKey != "file-key" { t.Fatalf("providers[default] = %+v, want litellm/file-key", p) } if cfg.AI.Embeddings.Primary.Model != "text-embed" || cfg.AI.Embeddings.Primary.Provider != "default" { t.Fatalf("embeddings.primary = %+v, want default/text-embed", cfg.AI.Embeddings.Primary) } if cfg.AI.Metadata.Primary.Model != "gpt-4" || cfg.AI.Metadata.Primary.Provider != "default" { t.Fatalf("metadata.primary = %+v, want default/gpt-4", cfg.AI.Metadata.Primary) } if len(cfg.AI.Metadata.Fallbacks) != 1 || cfg.AI.Metadata.Fallbacks[0].Model != "gpt-3.5" { t.Fatalf("metadata.fallbacks = %+v, want [default/gpt-3.5]", cfg.AI.Metadata.Fallbacks) } entries, err := filepath.Glob(configPath + ".bak.*") if err != nil { t.Fatalf("glob backups: %v", err) } if len(entries) != 0 { t.Fatalf("backup files = %d, want 0 (load should not rewrite config)", len(entries)) } originalOnDisk, err := os.ReadFile(configPath) if err != nil { t.Fatalf("read original config: %v", err) } if !strings.Contains(string(originalOnDisk), "provider: \"litellm\"") { t.Fatalf("expected source config to remain unchanged on disk") } }