package config import ( "strings" "testing" ) func modelCfg(env string, m ModelConfig) *Config { c := &Config{AppEnv: env} c.Model = m return c } func TestValidateModelProvider(t *testing.T) { for _, tc := range []struct { name string cfg *Config wantErr bool }{ { "unset provider is anthropic, which is what every existing deployment has", modelCfg("development", ModelConfig{}), false, }, {"anthropic named explicitly", modelCfg("development", ModelConfig{Provider: "anthropic"}), false}, {"openai", modelCfg("development", ModelConfig{Provider: "openai"}), false}, {"a typo is caught once at startup, not once per run", modelCfg("development", ModelConfig{Provider: "openal"}), true}, {"a provider that does not exist", modelCfg("development", ModelConfig{Provider: "groq"}), true}, } { t.Run(tc.name, func(t *testing.T) { err := tc.cfg.validateModel() if tc.wantErr != (err != nil) { t.Fatalf("validateModel() = %v, wantErr = %v", err, tc.wantErr) } }) } } // THE EXPENSIVE MISTAKE. // // A deployment that sets MODEL_BASE_URL and forgets MODEL_PROVIDER believes it // has moved off Claude. It has not: the anthropic path has one endpoint and // ignores the field entirely, so every run keeps going to Anthropic and keeps // being billed there, with nothing in the logs to say so. The whole point of // this change is cost, and that is the one misconfiguration that silently // defeats it. func TestBaseURLWithoutOpenAIProviderIsRefused(t *testing.T) { err := modelCfg("development", ModelConfig{ BaseURL: "https://api.groq.com/openai/v1", }).validateModel() if err == nil { t.Fatal("a base URL on the anthropic provider was accepted; every run would still go to Anthropic") } for _, want := range []string{"MODEL_BASE_URL", "MODEL_PROVIDER=openai", "Anthropic"} { if !strings.Contains(err.Error(), want) { t.Errorf("the message does not mention %q:\n %v", want, err) } } // The same URL with the provider set is exactly the intended configuration. if err := modelCfg("development", ModelConfig{ Provider: "openai", BaseURL: "https://api.groq.com/openai/v1", }).validateModel(); err != nil { t.Fatalf("the intended configuration was refused: %v", err) } } func TestBaseURLMustBeAURL(t *testing.T) { for _, raw := range []string{"api.groq.com", "ftp://x.test", "not a url", "://broken"} { err := modelCfg("development", ModelConfig{Provider: "openai", BaseURL: raw}).validateModel() if err == nil { t.Errorf("MODEL_BASE_URL=%q was accepted", raw) } } for _, raw := range []string{"http://localhost:11434/v1", "https://api.groq.com/openai/v1"} { if err := modelCfg("development", ModelConfig{Provider: "openai", BaseURL: raw}).validateModel(); err != nil { t.Errorf("MODEL_BASE_URL=%q was refused: %v", raw, err) } } } // Production without a credential fails every run at the gateway, which is a // misconfiguration wearing a runtime error's clothes. A local model is the one // exception: it needs no key, and demanding one would make the zero-cost path // impossible to configure. func TestProductionCredentialRequirement(t *testing.T) { for _, tc := range []struct { name string cfg *Config wantErr bool }{ {"production with no key", modelCfg("production", ModelConfig{}), true}, {"production with a key", modelCfg("production", ModelConfig{APIKey: "k"}), false}, { "production against a local model needs no key", modelCfg("production", ModelConfig{Provider: "openai", BaseURL: "http://localhost:11434/v1"}), false, }, { "production against a hosted provider still does", modelCfg("production", ModelConfig{Provider: "openai", BaseURL: "https://api.groq.com/openai/v1"}), true, }, {"development needs nothing", modelCfg("development", ModelConfig{}), false}, } { t.Run(tc.name, func(t *testing.T) { err := tc.cfg.validateModel() if tc.wantErr != (err != nil) { t.Fatalf("validateModel() = %v, wantErr = %v", err, tc.wantErr) } }) } } func TestIsLoopback(t *testing.T) { for raw, want := range map[string]bool{ "http://localhost:11434/v1": true, "http://127.0.0.1:11434/v1": true, "https://api.groq.com/v1": false, "": false, // A remote host that merely mentions localhost in its path is not local. "https://x.test/localhost/v1": false, } { if got := isLoopback(raw); got != want { t.Errorf("isLoopback(%q) = %v, want %v", raw, got, want) } } }