mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
71bdc768be
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.
228 lines
6.8 KiB
Go
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))
|
|
}
|