package config import ( "os" "path/filepath" "testing" ) func TestLoadYAMLConfig(t *testing.T) { tests := []struct { name string content string wantErr bool errMsg string }{ { name: "valid minimal config", content: `framework: hermes provider: name: hetzner token: test-token-123 ssh: names: - my-ssh-key server: name: test-server inference: provider: venice api_key: venice-api-key-123 `, wantErr: false, }, { name: "valid full config", content: `framework: hermes provider: name: hetzner token: test-token-123 ssh: names: - my-ssh-key server: name: test-server agent_name: my-agent timezone: America/New_York location: fsn1 type: cpx31 inference: provider: venice api_key: venice-api-key-123 primary_model: zai-org-glm-5 fallback_models: - openai/gpt-4o tailscale: enabled: true auth_key: tskey-123 tailnet: mytailnet discord: enabled: true bot_token: discord-token-123 server_id: "123456789" user_ids: - "111222333" hermes: docker_enabled: true `, wantErr: false, }, { name: "digitalocean config", content: `framework: openclaw provider: name: digitalocean token: do-token-123 ssh: fingerprints: - "aa:bb:cc:dd:ee:ff" server: name: do-server region: nyc3 size: s-2vcpu-4gb inference: provider: openai api_key: sk-openai-123 primary_model: gpt-4o `, wantErr: false, }, { name: "missing framework", content: `provider: name: hetzner token: test-token-123 ssh: names: - my-ssh-key inference: provider: venice api_key: key-123 `, wantErr: true, errMsg: "framework is required", }, { name: "invalid framework", content: `framework: invalid-framework provider: name: hetzner token: test-token-123 ssh: names: - my-ssh-key inference: provider: venice api_key: key-123 `, wantErr: true, errMsg: "invalid framework", }, { name: "missing provider name", content: `framework: hermes provider: token: test-token-123 ssh: names: - my-ssh-key inference: provider: venice api_key: key-123 `, wantErr: true, errMsg: "provider.name is required", }, { name: "missing provider token", content: `framework: hermes provider: name: hetzner ssh: names: - my-ssh-key inference: provider: venice api_key: key-123 `, wantErr: true, errMsg: "provider.token is required", }, { name: "missing ssh keys", content: `framework: hermes provider: name: hetzner token: test-token-123 inference: provider: venice api_key: key-123 `, wantErr: true, errMsg: "ssh.names", }, { name: "missing inference api key", content: `framework: hermes provider: name: hetzner token: test-token-123 ssh: names: - my-ssh-key inference: provider: venice `, wantErr: true, errMsg: "inference.api_key", }, { name: "custom provider requires base url", content: `framework: hermes provider: name: hetzner token: test-token-123 ssh: names: - my-ssh-key inference: provider: custom api_key: key-123 `, wantErr: true, errMsg: "base_url is required", }, { name: "custom provider with base url", content: `framework: hermes provider: name: hetzner token: test-token-123 ssh: names: - my-ssh-key inference: provider: custom api_key: key-123 base_url: https://api.custom.com/v1 `, wantErr: false, }, { name: "tailscale enabled but no auth key", content: `framework: hermes provider: name: hetzner token: test-token-123 ssh: names: - my-ssh-key inference: provider: venice api_key: key-123 tailscale: enabled: true `, wantErr: true, errMsg: "tailscale.auth_key", }, { name: "discord enabled but no bot token", content: `framework: hermes provider: name: hetzner token: test-token-123 ssh: names: - my-ssh-key inference: provider: venice api_key: key-123 discord: enabled: true `, wantErr: true, errMsg: "discord.bot_token", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tmpdir := t.TempDir() path := filepath.Join(tmpdir, "config.yaml") if err := os.WriteFile(path, []byte(tt.content), 0644); err != nil { t.Fatal(err) } cfg, err := LoadYAMLConfig(path) if (err != nil) != tt.wantErr { t.Errorf("LoadYAMLConfig() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr && tt.errMsg != "" { if err == nil || !contains(err.Error(), tt.errMsg) { t.Errorf("LoadYAMLConfig() error = %v, want error containing %q", err, tt.errMsg) } return } if !tt.wantErr && cfg == nil { t.Error("LoadYAMLConfig() returned nil config without error") } }) } } func TestYAMLConfigToDeploymentConfig(t *testing.T) { yamlContent := `framework: hermes provider: name: hetzner token: htoken-123 ssh: names: - my-key server: name: test-server agent_name: my-agent location: fsn1 type: cpx31 inference: provider: venice api_key: vkey-123 primary_model: zai-org-glm-5 fallback_models: - openai/gpt-4o tailscale: enabled: true auth_key: tskey-123 tailnet: mytailnet discord: enabled: true bot_token: dtoken-123 server_id: "123456" user_ids: - "111222333" hermes: docker_enabled: true ` tmpdir := t.TempDir() path := filepath.Join(tmpdir, "config.yaml") if err := os.WriteFile(path, []byte(yamlContent), 0644); err != nil { t.Fatal(err) } cfg, err := LoadYAMLConfig(path) if err != nil { t.Fatalf("LoadYAMLConfig() error = %v", err) } deploy := cfg.ToDeploymentConfig() // Check provider if deploy.CloudProvider != "hetzner" { t.Errorf("CloudProvider = %q, want hetzner", deploy.CloudProvider) } if deploy.HetznerToken != "htoken-123" { t.Errorf("HetznerToken = %q, want htoken-123", deploy.HetznerToken) } // Check framework if deploy.Framework != "hermes" { t.Errorf("Framework = %q, want hermes", deploy.Framework) } // Check server if deploy.ServerName != "test-server" { t.Errorf("ServerName = %q, want test-server", deploy.ServerName) } if deploy.Location != "fsn1" { t.Errorf("Location = %q, want fsn1", deploy.Location) } if deploy.ServerType != "cpx31" { t.Errorf("ServerType = %q, want cpx31", deploy.ServerType) } // Check inference if deploy.InferenceProvider != "venice" { t.Errorf("InferenceProvider = %q, want venice", deploy.InferenceProvider) } if deploy.InferenceAPIKey != "vkey-123" { t.Errorf("InferenceAPIKey = %q, want vkey-123", deploy.InferenceAPIKey) } if deploy.PrimaryModel != "zai-org-glm-5" { t.Errorf("PrimaryModel = %q, want zai-org-glm-5", deploy.PrimaryModel) } if len(deploy.FallbackModels) != 1 || deploy.FallbackModels[0] != "openai/gpt-4o" { t.Errorf("FallbackModels = %v, want [openai/gpt-4o]", deploy.FallbackModels) } // Check Tailscale if !deploy.EnableTailscale { t.Error("EnableTailscale = false, want true") } if deploy.TailscaleAuthKey != "tskey-123" { t.Errorf("TailscaleAuthKey = %q, want tskey-123", deploy.TailscaleAuthKey) } // Check Discord if !deploy.EnableDiscord { t.Error("EnableDiscord = false, want true") } if deploy.DiscordBotToken != "dtoken-123" { t.Errorf("DiscordBotToken = %q, want dtoken-123", deploy.DiscordBotToken) } // Check Hermes if !deploy.DockerEnabled { t.Error("DockerEnabled = false, want true") } } func TestYAMLConfigDefaults(t *testing.T) { // Test minimal config with defaults yamlContent := `framework: hermes provider: name: hetzner token: test-token ssh: names: - my-key inference: provider: venice api_key: key-123 ` tmpdir := t.TempDir() path := filepath.Join(tmpdir, "config.yaml") if err := os.WriteFile(path, []byte(yamlContent), 0644); err != nil { t.Fatal(err) } cfg, err := LoadYAMLConfig(path) if err != nil { t.Fatalf("LoadYAMLConfig() error = %v", err) } // Check defaults if cfg.Server.Name != "agent-gateway" { t.Errorf("Server.Name default = %q, want agent-gateway", cfg.Server.Name) } if cfg.Server.Timezone != "UTC" { t.Errorf("Server.Timezone default = %q, want UTC", cfg.Server.Timezone) } if cfg.Server.Location != "ash" { t.Errorf("Server.Location default = %q, want ash (Hetzner default)", cfg.Server.Location) } if cfg.Server.Type != "cpx21" { t.Errorf("Server.Type default = %q, want cpx21 (Hetzner default)", cfg.Server.Type) } if cfg.Inference.PrimaryModel != "zai-org-glm-5" { t.Errorf("Inference.PrimaryModel default = %q, want zai-org-glm-5 (Venice default)", cfg.Inference.PrimaryModel) } if cfg.Server.AgentName != "hermes" { t.Errorf("Server.AgentName default = %q, want hermes (framework default)", cfg.Server.AgentName) } } func TestDigitalOceanConfig(t *testing.T) { yamlContent := `framework: openclaw provider: name: digitalocean token: do-token-123 ssh: fingerprints: - "aa:bb:cc:dd:ee:ff" server: name: do-server region: sgp1 size: s-4vcpu-8gb inference: provider: openai api_key: sk-123 primary_model: gpt-4o ` tmpdir := t.TempDir() path := filepath.Join(tmpdir, "config.yaml") if err := os.WriteFile(path, []byte(yamlContent), 0644); err != nil { t.Fatal(err) } cfg, err := LoadYAMLConfig(path) if err != nil { t.Fatalf("LoadYAMLConfig() error = %v", err) } deploy := cfg.ToDeploymentConfig() if deploy.CloudProvider != "digitalocean" { t.Errorf("CloudProvider = %q, want digitalocean", deploy.CloudProvider) } if deploy.DOToken != "do-token-123" { t.Errorf("DOToken = %q, want do-token-123", deploy.DOToken) } if deploy.Region != "sgp1" { t.Errorf("Region = %q, want sgp1", deploy.Region) } if deploy.DropletSize != "s-4vcpu-8gb" { t.Errorf("DropletSize = %q, want s-4vcpu-8gb", deploy.DropletSize) } if len(deploy.SSHKeyFingerprints) != 1 { t.Errorf("SSHKeyFingerprints = %v, want 1 element", deploy.SSHKeyFingerprints) } } func TestOpenClawDefaults(t *testing.T) { yamlContent := `framework: openclaw provider: name: hetzner token: test-token ssh: names: - my-key inference: provider: openai api_key: sk-123 ` tmpdir := t.TempDir() path := filepath.Join(tmpdir, "config.yaml") if err := os.WriteFile(path, []byte(yamlContent), 0644); err != nil { t.Fatal(err) } cfg, err := LoadYAMLConfig(path) if err != nil { t.Fatalf("LoadYAMLConfig() error = %v", err) } // OpenClaw defaults if cfg.OpenClaw == nil { t.Fatal("OpenClaw config is nil") } if cfg.OpenClaw.Version != "lts" { t.Errorf("OpenClaw.Version default = %q, want lts", cfg.OpenClaw.Version) } if cfg.OpenClaw.NodeVersion != "22" { t.Errorf("OpenClaw.NodeVersion default = %q, want 22", cfg.OpenClaw.NodeVersion) } if !cfg.OpenClaw.EnableSwap { t.Error("OpenClaw.EnableSwap default = false, want true") } if cfg.OpenClaw.SwapSizeGB != 2 { t.Errorf("OpenClaw.SwapSizeGB default = %d, want 2", cfg.OpenClaw.SwapSizeGB) } if !cfg.OpenClaw.EnableFail2ban { t.Error("OpenClaw.EnableFail2ban default = false, want true") } if !cfg.OpenClaw.EnableUnattendedUpgrades { t.Error("OpenClaw.EnableUnattendedUpgrades default = false, want true") } if cfg.Server.AgentName != "openclaw" { t.Errorf("Server.AgentName default = %q, want openclaw", cfg.Server.AgentName) } }