package config import ( "os" "path/filepath" "strings" "testing" "time" ) const testCLIConfig = ` [services] dir = "/home/kyle/.config/mcp/services" [mcias] server_url = "https://mcias.metacircular.net:8443" ca_cert = "/etc/mcp/ca.pem" service_name = "mcp" [auth] token_path = "/home/kyle/.config/mcp/token" username = "kyle" password_file = "/home/kyle/.config/mcp/password" [[nodes]] name = "rift" address = "100.95.252.120:9444" [[nodes]] name = "cascade" address = "100.95.252.121:9444" ` const testAgentConfig = ` [server] grpc_addr = "100.95.252.120:9444" tls_cert = "/srv/mcp/certs/cert.pem" tls_key = "/srv/mcp/certs/key.pem" [database] path = "/srv/mcp/mcp.db" [mcias] server_url = "https://mcias.metacircular.net:8443" ca_cert = "/etc/mcp/ca.pem" service_name = "mcp-agent" [agent] node_name = "rift" container_runtime = "podman" [monitor] interval = "60s" alert_command = ["notify-send", "MCP Alert"] cooldown = "15m" flap_threshold = 3 flap_window = "10m" retention = "30d" [log] level = "debug" ` func writeTempConfig(t *testing.T, content string) string { t.Helper() path := filepath.Join(t.TempDir(), "config.toml") if err := os.WriteFile(path, []byte(content), 0o600); err != nil { t.Fatalf("write temp config: %v", err) } return path } func TestLoadCLIConfig(t *testing.T) { path := writeTempConfig(t, testCLIConfig) cfg, err := LoadCLIConfig(path) if err != nil { t.Fatalf("load: %v", err) } if cfg.Services.Dir != "/home/kyle/.config/mcp/services" { t.Fatalf("services.dir: got %q", cfg.Services.Dir) } if cfg.MCIAS.ServerURL != "https://mcias.metacircular.net:8443" { t.Fatalf("mcias.server_url: got %q", cfg.MCIAS.ServerURL) } if cfg.MCIAS.CACert != "/etc/mcp/ca.pem" { t.Fatalf("mcias.ca_cert: got %q", cfg.MCIAS.CACert) } if cfg.MCIAS.ServiceName != "mcp" { t.Fatalf("mcias.service_name: got %q", cfg.MCIAS.ServiceName) } if cfg.Auth.TokenPath != "/home/kyle/.config/mcp/token" { t.Fatalf("auth.token_path: got %q", cfg.Auth.TokenPath) } if cfg.Auth.Username != "kyle" { t.Fatalf("auth.username: got %q", cfg.Auth.Username) } if cfg.Auth.PasswordFile != "/home/kyle/.config/mcp/password" { t.Fatalf("auth.password_file: got %q", cfg.Auth.PasswordFile) } if len(cfg.Nodes) != 2 { t.Fatalf("nodes: got %d, want 2", len(cfg.Nodes)) } if cfg.Nodes[0].Name != "rift" || cfg.Nodes[0].Address != "100.95.252.120:9444" { t.Fatalf("nodes[0]: got %+v", cfg.Nodes[0]) } if cfg.Nodes[1].Name != "cascade" || cfg.Nodes[1].Address != "100.95.252.121:9444" { t.Fatalf("nodes[1]: got %+v", cfg.Nodes[1]) } } func TestLoadAgentConfig(t *testing.T) { path := writeTempConfig(t, testAgentConfig) cfg, err := LoadAgentConfig(path) if err != nil { t.Fatalf("load: %v", err) } if cfg.Server.GRPCAddr != "100.95.252.120:9444" { t.Fatalf("server.grpc_addr: got %q", cfg.Server.GRPCAddr) } if cfg.Server.TLSCert != "/srv/mcp/certs/cert.pem" { t.Fatalf("server.tls_cert: got %q", cfg.Server.TLSCert) } if cfg.Server.TLSKey != "/srv/mcp/certs/key.pem" { t.Fatalf("server.tls_key: got %q", cfg.Server.TLSKey) } if cfg.Database.Path != "/srv/mcp/mcp.db" { t.Fatalf("database.path: got %q", cfg.Database.Path) } if cfg.MCIAS.ServerURL != "https://mcias.metacircular.net:8443" { t.Fatalf("mcias.server_url: got %q", cfg.MCIAS.ServerURL) } if cfg.MCIAS.ServiceName != "mcp-agent" { t.Fatalf("mcias.service_name: got %q", cfg.MCIAS.ServiceName) } if cfg.Agent.NodeName != "rift" { t.Fatalf("agent.node_name: got %q", cfg.Agent.NodeName) } if cfg.Agent.ContainerRuntime != "podman" { t.Fatalf("agent.container_runtime: got %q", cfg.Agent.ContainerRuntime) } if cfg.Monitor.Interval.Duration != 60*time.Second { t.Fatalf("monitor.interval: got %v", cfg.Monitor.Interval.Duration) } if len(cfg.Monitor.AlertCommand) != 2 || cfg.Monitor.AlertCommand[0] != "notify-send" { t.Fatalf("monitor.alert_command: got %v", cfg.Monitor.AlertCommand) } if cfg.Monitor.Cooldown.Duration != 15*time.Minute { t.Fatalf("monitor.cooldown: got %v", cfg.Monitor.Cooldown.Duration) } if cfg.Monitor.FlapThreshold != 3 { t.Fatalf("monitor.flap_threshold: got %d", cfg.Monitor.FlapThreshold) } if cfg.Monitor.FlapWindow.Duration != 10*time.Minute { t.Fatalf("monitor.flap_window: got %v", cfg.Monitor.FlapWindow.Duration) } if cfg.Monitor.Retention.Duration != 30*24*time.Hour { t.Fatalf("monitor.retention: got %v", cfg.Monitor.Retention.Duration) } if cfg.Log.Level != "debug" { t.Fatalf("log.level: got %q", cfg.Log.Level) } // Metacrypt defaults when section is omitted. if cfg.Metacrypt.Mount != "pki" { t.Fatalf("metacrypt.mount default: got %q, want pki", cfg.Metacrypt.Mount) } if cfg.Metacrypt.Issuer != "infra" { t.Fatalf("metacrypt.issuer default: got %q, want infra", cfg.Metacrypt.Issuer) } } func TestCLIConfigValidation(t *testing.T) { tests := []struct { name string config string errMsg string }{ { name: "missing services.dir", config: ` [mcias] server_url = "https://mcias.metacircular.net:8443" service_name = "mcp" [auth] token_path = "/tmp/token" `, errMsg: "services.dir is required", }, { name: "missing mcias.server_url", config: ` [services] dir = "/tmp/services" [mcias] service_name = "mcp" [auth] token_path = "/tmp/token" `, errMsg: "mcias.server_url is required", }, { name: "missing mcias.service_name", config: ` [services] dir = "/tmp/services" [mcias] server_url = "https://mcias.metacircular.net:8443" [auth] token_path = "/tmp/token" `, errMsg: "mcias.service_name is required", }, { name: "missing auth.token_path", config: ` [services] dir = "/tmp/services" [mcias] server_url = "https://mcias.metacircular.net:8443" service_name = "mcp" `, errMsg: "auth.token_path is required", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { path := writeTempConfig(t, tt.config) _, err := LoadCLIConfig(path) if err == nil { t.Fatal("expected error, got nil") } if got := err.Error(); !strings.Contains(got, tt.errMsg) { t.Fatalf("error %q does not contain %q", got, tt.errMsg) } }) } } func TestAgentConfigValidation(t *testing.T) { // Minimal valid agent config to start from. base := ` [server] grpc_addr = "0.0.0.0:9444" tls_cert = "/srv/mcp/cert.pem" tls_key = "/srv/mcp/key.pem" [database] path = "/srv/mcp/mcp.db" [mcias] server_url = "https://mcias.metacircular.net:8443" service_name = "mcp-agent" [agent] node_name = "rift" ` tests := []struct { name string config string errMsg string }{ { name: "missing server.grpc_addr", config: ` [server] tls_cert = "/srv/mcp/cert.pem" tls_key = "/srv/mcp/key.pem" [database] path = "/srv/mcp/mcp.db" [mcias] server_url = "https://mcias.metacircular.net:8443" service_name = "mcp-agent" [agent] node_name = "rift" `, errMsg: "server.grpc_addr is required", }, { name: "missing server.tls_cert", config: ` [server] grpc_addr = "0.0.0.0:9444" tls_key = "/srv/mcp/key.pem" [database] path = "/srv/mcp/mcp.db" [mcias] server_url = "https://mcias.metacircular.net:8443" service_name = "mcp-agent" [agent] node_name = "rift" `, errMsg: "server.tls_cert is required", }, { name: "missing database.path", config: ` [server] grpc_addr = "0.0.0.0:9444" tls_cert = "/srv/mcp/cert.pem" tls_key = "/srv/mcp/key.pem" [mcias] server_url = "https://mcias.metacircular.net:8443" service_name = "mcp-agent" [agent] node_name = "rift" `, errMsg: "database.path is required", }, { name: "missing agent.node_name", config: ` [server] grpc_addr = "0.0.0.0:9444" tls_cert = "/srv/mcp/cert.pem" tls_key = "/srv/mcp/key.pem" [database] path = "/srv/mcp/mcp.db" [mcias] server_url = "https://mcias.metacircular.net:8443" service_name = "mcp-agent" `, errMsg: "agent.node_name is required", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { path := writeTempConfig(t, tt.config) _, err := LoadAgentConfig(path) if err == nil { t.Fatal("expected error, got nil") } if got := err.Error(); !strings.Contains(got, tt.errMsg) { t.Fatalf("error %q does not contain %q", got, tt.errMsg) } }) } // Verify base config actually loads fine. t.Run("valid base config", func(t *testing.T) { path := writeTempConfig(t, base) if _, err := LoadAgentConfig(path); err != nil { t.Fatalf("base config should be valid: %v", err) } }) } func TestAgentConfigDefaults(t *testing.T) { // Only required fields, no monitor/log/runtime settings. minimal := ` [server] grpc_addr = "0.0.0.0:9444" tls_cert = "/srv/mcp/cert.pem" tls_key = "/srv/mcp/key.pem" [database] path = "/srv/mcp/mcp.db" [mcias] server_url = "https://mcias.metacircular.net:8443" service_name = "mcp-agent" [agent] node_name = "rift" ` path := writeTempConfig(t, minimal) cfg, err := LoadAgentConfig(path) if err != nil { t.Fatalf("load: %v", err) } if cfg.Monitor.Interval.Duration != 60*time.Second { t.Fatalf("default interval: got %v, want 60s", cfg.Monitor.Interval.Duration) } if cfg.Monitor.Cooldown.Duration != 15*time.Minute { t.Fatalf("default cooldown: got %v, want 15m", cfg.Monitor.Cooldown.Duration) } if cfg.Monitor.FlapThreshold != 3 { t.Fatalf("default flap_threshold: got %d, want 3", cfg.Monitor.FlapThreshold) } if cfg.Monitor.FlapWindow.Duration != 10*time.Minute { t.Fatalf("default flap_window: got %v, want 10m", cfg.Monitor.FlapWindow.Duration) } if cfg.Monitor.Retention.Duration != 30*24*time.Hour { t.Fatalf("default retention: got %v, want 720h", cfg.Monitor.Retention.Duration) } if cfg.Log.Level != "info" { t.Fatalf("default log level: got %q, want info", cfg.Log.Level) } if cfg.Agent.ContainerRuntime != "podman" { t.Fatalf("default container_runtime: got %q, want podman", cfg.Agent.ContainerRuntime) } } func TestEnvVarOverrides(t *testing.T) { // Test agent env var override. minimal := ` [server] grpc_addr = "0.0.0.0:9444" tls_cert = "/srv/mcp/cert.pem" tls_key = "/srv/mcp/key.pem" [database] path = "/srv/mcp/mcp.db" [mcias] server_url = "https://mcias.metacircular.net:8443" service_name = "mcp-agent" [agent] node_name = "rift" [log] level = "info" ` t.Run("agent log level override", func(t *testing.T) { t.Setenv("MCP_AGENT_LOG_LEVEL", "debug") path := writeTempConfig(t, minimal) cfg, err := LoadAgentConfig(path) if err != nil { t.Fatalf("load: %v", err) } if cfg.Log.Level != "debug" { t.Fatalf("log level: got %q, want debug", cfg.Log.Level) } }) t.Run("agent node name override", func(t *testing.T) { t.Setenv("MCP_AGENT_NODE_NAME", "override-node") path := writeTempConfig(t, minimal) cfg, err := LoadAgentConfig(path) if err != nil { t.Fatalf("load: %v", err) } if cfg.Agent.NodeName != "override-node" { t.Fatalf("node_name: got %q, want override-node", cfg.Agent.NodeName) } }) t.Run("CLI services dir override", func(t *testing.T) { t.Setenv("MCP_SERVICES_DIR", "/override/services") path := writeTempConfig(t, testCLIConfig) cfg, err := LoadCLIConfig(path) if err != nil { t.Fatalf("load: %v", err) } if cfg.Services.Dir != "/override/services" { t.Fatalf("services.dir: got %q, want /override/services", cfg.Services.Dir) } }) } func TestAgentConfigMetacrypt(t *testing.T) { cfgStr := ` [server] grpc_addr = "0.0.0.0:9444" tls_cert = "/srv/mcp/cert.pem" tls_key = "/srv/mcp/key.pem" [database] path = "/srv/mcp/mcp.db" [mcias] server_url = "https://mcias.metacircular.net:8443" service_name = "mcp-agent" [agent] node_name = "rift" [metacrypt] server_url = "https://metacrypt.metacircular.net:8443" ca_cert = "/etc/mcp/metacircular-ca.pem" mount = "custom-pki" issuer = "custom-issuer" token_path = "/srv/mcp/metacrypt-token" ` path := writeTempConfig(t, cfgStr) cfg, err := LoadAgentConfig(path) if err != nil { t.Fatalf("load: %v", err) } if cfg.Metacrypt.ServerURL != "https://metacrypt.metacircular.net:8443" { t.Fatalf("metacrypt.server_url: got %q", cfg.Metacrypt.ServerURL) } if cfg.Metacrypt.CACert != "/etc/mcp/metacircular-ca.pem" { t.Fatalf("metacrypt.ca_cert: got %q", cfg.Metacrypt.CACert) } if cfg.Metacrypt.Mount != "custom-pki" { t.Fatalf("metacrypt.mount: got %q", cfg.Metacrypt.Mount) } if cfg.Metacrypt.Issuer != "custom-issuer" { t.Fatalf("metacrypt.issuer: got %q", cfg.Metacrypt.Issuer) } if cfg.Metacrypt.TokenPath != "/srv/mcp/metacrypt-token" { t.Fatalf("metacrypt.token_path: got %q", cfg.Metacrypt.TokenPath) } } func TestAgentConfigMetacryptEnvOverrides(t *testing.T) { minimal := ` [server] grpc_addr = "0.0.0.0:9444" tls_cert = "/srv/mcp/cert.pem" tls_key = "/srv/mcp/key.pem" [database] path = "/srv/mcp/mcp.db" [mcias] server_url = "https://mcias.metacircular.net:8443" service_name = "mcp-agent" [agent] node_name = "rift" ` t.Setenv("MCP_AGENT_METACRYPT_SERVER_URL", "https://override.metacrypt:8443") t.Setenv("MCP_AGENT_METACRYPT_TOKEN_PATH", "/override/token") path := writeTempConfig(t, minimal) cfg, err := LoadAgentConfig(path) if err != nil { t.Fatalf("load: %v", err) } if cfg.Metacrypt.ServerURL != "https://override.metacrypt:8443" { t.Fatalf("metacrypt.server_url: got %q", cfg.Metacrypt.ServerURL) } if cfg.Metacrypt.TokenPath != "/override/token" { t.Fatalf("metacrypt.token_path: got %q", cfg.Metacrypt.TokenPath) } } func TestDurationParsing(t *testing.T) { tests := []struct { input string want time.Duration fail bool }{ {input: "60s", want: 60 * time.Second}, {input: "15m", want: 15 * time.Minute}, {input: "24h", want: 24 * time.Hour}, {input: "30d", want: 30 * 24 * time.Hour}, {input: "1d", want: 24 * time.Hour}, {input: "", want: 0}, {input: "bogus", fail: true}, {input: "notanumber-d", fail: true}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { var d Duration err := d.UnmarshalText([]byte(tt.input)) if tt.fail { if err == nil { t.Fatalf("expected error for %q, got nil", tt.input) } return } if err != nil { t.Fatalf("unexpected error for %q: %v", tt.input, err) } if d.Duration != tt.want { t.Fatalf("for %q: got %v, want %v", tt.input, d.Duration, tt.want) } }) } }