Files
kit/internal/config/integration_test.go
Ed Zynda 71bdc768be feat: replace catwalk with models.dev, auto-route openai-compatible providers, fix all lint issues
Replace catwalk dependency with direct models.dev integration (97 providers,
3039 models vs catwalk's 22/679). Auto-route @ai-sdk/openai-compatible
providers through fantasy's openaicompat using the api URL from models.dev,
eliminating the need for --provider-url. Add --all flag to 'mcphost models'
to show all providers vs just fantasy-compatible ones.

Fix all 74 golangci-lint issues: errcheck (53), staticcheck SA4006 (24),
SA9003 (2), ST1005 (5), ineffassign (3). Restructure styles.go color
handling into a colorScheme struct to eliminate SA4006 false positives
from new(x) syntax.
2026-02-25 22:51:45 +03:00

228 lines
6.8 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/spf13/viper"
)
func TestConfigLoadingWithEnvSubstitution(t *testing.T) {
// Create a temporary config file with environment variables
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "test-config.yaml")
configContent := `
mcpServers:
github:
type: local
command: ["docker", "run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN=${env://GITHUB_TOKEN}", "ghcr.io/github/github-mcp-server"]
environment:
DEBUG: "${env://DEBUG:-false}"
LOG_LEVEL: "${env://LOG_LEVEL:-info}"
database:
type: local
command: ["python", "db-server.py"]
environment:
DATABASE_URL: "${env://DATABASE_URL:-sqlite:///tmp/default.db}"
API_KEY: "${env://DB_API_KEY}"
model: "${env://MODEL:-anthropic/claude-sonnet-4-5-20250929}"
provider-api-key: "${env://OPENAI_API_KEY:-}"
debug: ${env://DEBUG:-false}
`
err := os.WriteFile(configPath, []byte(configContent), 0644)
if err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
// Set up environment variables
_ = os.Setenv("GITHUB_TOKEN", "ghp_test_token")
_ = os.Setenv("DEBUG", "true")
_ = os.Setenv("DB_API_KEY", "secret_key")
defer func() {
_ = os.Unsetenv("GITHUB_TOKEN")
_ = os.Unsetenv("DEBUG")
_ = os.Unsetenv("DB_API_KEY")
}()
// Test the loadConfigWithEnvSubstitution function
viper.Reset() // Clear any existing config
err = loadConfigWithEnvSubstitution(configPath)
if err != nil {
t.Fatalf("Failed to load config with env substitution: %v", err)
}
// Verify that environment variables were substituted correctly
var config Config
err = viper.Unmarshal(&config)
if err != nil {
t.Fatalf("Failed to unmarshal config: %v", err)
}
// Check GitHub server config
githubServer, exists := config.MCPServers["github"]
if !exists {
t.Fatal("GitHub server not found in config")
}
// Check that GITHUB_TOKEN was substituted in command
expectedCommand := []string{"docker", "run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN=ghp_test_token", "ghcr.io/github/github-mcp-server"}
if len(githubServer.Command) != len(expectedCommand) {
t.Errorf("Expected command length %d, got %d", len(expectedCommand), len(githubServer.Command))
}
for i, expected := range expectedCommand {
if i < len(githubServer.Command) && githubServer.Command[i] != expected {
t.Errorf("Expected command[%d] = %s, got %s", i, expected, githubServer.Command[i])
}
}
// Check environment variables (viper converts keys to lowercase)
if githubServer.Environment["debug"] != "true" {
t.Errorf("Expected debug=true, got %s", githubServer.Environment["debug"])
}
if githubServer.Environment["log_level"] != "info" {
t.Errorf("Expected log_level=info, got %s", githubServer.Environment["log_level"])
}
// Check database server config
dbServer, exists := config.MCPServers["database"]
if !exists {
t.Fatal("Database server not found in config")
}
if dbServer.Environment["database_url"] != "sqlite:///tmp/default.db" {
t.Errorf("Expected database_url=sqlite:///tmp/default.db, got %s", dbServer.Environment["database_url"])
}
if dbServer.Environment["api_key"] != "secret_key" {
t.Errorf("Expected api_key=secret_key, got %s", dbServer.Environment["api_key"])
}
// Check global config values
if config.Model != "anthropic/claude-sonnet-4-5-20250929" {
t.Errorf("Expected model=anthropic/claude-sonnet-4-5-20250929, got %s", config.Model)
}
if !config.Debug {
t.Error("Expected debug=true")
}
}
func TestConfigLoadingWithMissingRequiredEnvVar(t *testing.T) {
// Create a temporary config file with a required environment variable
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "test-config.yaml")
configContent := `
mcpServers:
github:
type: local
command: ["gh", "api"]
environment:
GITHUB_TOKEN: "${env://REQUIRED_GITHUB_TOKEN}"
`
err := os.WriteFile(configPath, []byte(configContent), 0644)
if err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
// Make sure the environment variable is not set
_ = os.Unsetenv("REQUIRED_GITHUB_TOKEN")
// Test that loading fails with a clear error
viper.Reset()
err = loadConfigWithEnvSubstitution(configPath)
if err == nil {
t.Fatal("Expected error for missing required environment variable")
}
if !strings.Contains(err.Error(), "required environment variable REQUIRED_GITHUB_TOKEN not set") {
t.Errorf("Expected error about missing REQUIRED_GITHUB_TOKEN, got: %v", err)
}
}
func TestJSONConfigWithEnvSubstitution(t *testing.T) {
// Test JSON format as well
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "test-config.json")
configContent := `{
"mcpServers": {
"test": {
"type": "local",
"command": ["echo", "${env://TEST_VALUE:-hello}"],
"environment": {
"VAR1": "${env://VAR1:-default1}",
"VAR2": "${env://VAR2}"
}
}
},
"model": "${env://MODEL:-anthropic/claude-sonnet-4-5-20250929}"
}`
err := os.WriteFile(configPath, []byte(configContent), 0644)
if err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
// Set one environment variable, leave others to use defaults
_ = os.Setenv("VAR2", "custom_value")
defer func() { _ = os.Unsetenv("VAR2") }()
viper.Reset()
err = loadConfigWithEnvSubstitution(configPath)
if err != nil {
t.Fatalf("Failed to load JSON config with env substitution: %v", err)
}
var config Config
err = viper.Unmarshal(&config)
if err != nil {
t.Fatalf("Failed to unmarshal config: %v", err)
}
testServer := config.MCPServers["test"]
expectedCommand := []string{"echo", "hello"}
if len(testServer.Command) != len(expectedCommand) || testServer.Command[1] != "hello" {
t.Errorf("Expected command %v, got %v", expectedCommand, testServer.Command)
}
if testServer.Environment["var1"] != "default1" {
t.Errorf("Expected var1=default1, got %s", testServer.Environment["var1"])
}
if testServer.Environment["var2"] != "custom_value" {
t.Errorf("Expected var2=custom_value, got %s", testServer.Environment["var2"])
}
}
// Helper function to simulate the loadConfigWithEnvSubstitution function
// This is needed because the function is in cmd/root.go and we can't easily test it directly
func loadConfigWithEnvSubstitution(configPath string) error {
// Read raw config file content
rawContent, err := os.ReadFile(configPath)
if err != nil {
return err
}
// Apply environment variable substitution
substituter := &EnvSubstituter{}
processedContent, err := substituter.SubstituteEnvVars(string(rawContent))
if err != nil {
return err
}
// Determine config type from file extension
configType := "yaml"
if strings.HasSuffix(configPath, ".json") {
configType = "json"
}
// Use viper to parse the processed content
viper.SetConfigType(configType)
return viper.ReadConfig(strings.NewReader(processedContent))
}