obm/internal/config/yaml_test.go
MermaidMan d080e107d0 feat: add cross-compile and release pipeline
- Enhanced Makefile with cross-compilation for linux/amd64, linux/arm64,
  darwin/arm64, windows/amd64, windows/arm64
- Added GitHub Actions CI workflow for testing on all platforms
- Added GitHub Actions Release workflow triggered by version tags
- Added VERSION file for version tracking
- Added scripts/release.sh for automated release process
- Added Dockerfile for containerized builds
- Added CONTRIBUTING.md with release process documentation
- Added CHANGELOG.md for version tracking
- Updated .gitignore to exclude build artifacts
- Fixed unused variable in cmd/obm/main.go
- Version now injected via ldflags (main.version, main.gitCommit, main.buildTime)
2026-05-22 15:38:55 +00:00

524 lines
No EOL
11 KiB
Go

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)
}
}