Add DNSRegistrar that creates/updates/deletes A records in MCNS during deploy and stop. When a service has routes, the agent ensures an A record exists in the configured zone pointing to the node's address. On stop, the record is removed. - Add MCNSConfig to agent config (server_url, ca_cert, token_path, zone, node_addr) with defaults and env overrides - Add DNSRegistrar (internal/agent/dns.go): REST client for MCNS record CRUD, nil-receiver safe - Wire into deploy flow (EnsureRecord after route registration) - Wire into stop flow (RemoveRecord before container stop) - 7 new tests, make all passes with 0 issues Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
639 lines
16 KiB
Go
639 lines
16 KiB
Go
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)
|
|
}
|
|
|
|
// MCNS defaults when section is omitted.
|
|
if cfg.MCNS.Zone != "svc.mcp.metacircular.net" {
|
|
t.Fatalf("mcns.zone default: got %q, want svc.mcp.metacircular.net", cfg.MCNS.Zone)
|
|
}
|
|
}
|
|
|
|
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 TestAgentConfigMCNS(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"
|
|
[mcns]
|
|
server_url = "https://localhost:28443"
|
|
ca_cert = "/srv/mcp/certs/metacircular-ca.pem"
|
|
token_path = "/srv/mcp/metacrypt-token"
|
|
zone = "custom.zone"
|
|
node_addr = "10.0.0.1"
|
|
`
|
|
path := writeTempConfig(t, cfgStr)
|
|
cfg, err := LoadAgentConfig(path)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
|
|
if cfg.MCNS.ServerURL != "https://localhost:28443" {
|
|
t.Fatalf("mcns.server_url: got %q", cfg.MCNS.ServerURL)
|
|
}
|
|
if cfg.MCNS.CACert != "/srv/mcp/certs/metacircular-ca.pem" {
|
|
t.Fatalf("mcns.ca_cert: got %q", cfg.MCNS.CACert)
|
|
}
|
|
if cfg.MCNS.Zone != "custom.zone" {
|
|
t.Fatalf("mcns.zone: got %q", cfg.MCNS.Zone)
|
|
}
|
|
if cfg.MCNS.NodeAddr != "10.0.0.1" {
|
|
t.Fatalf("mcns.node_addr: got %q", cfg.MCNS.NodeAddr)
|
|
}
|
|
}
|
|
|
|
func TestAgentConfigMCNSEnvOverrides(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_MCNS_SERVER_URL", "https://override:28443")
|
|
t.Setenv("MCP_AGENT_MCNS_TOKEN_PATH", "/override/token")
|
|
t.Setenv("MCP_AGENT_MCNS_NODE_ADDR", "10.0.0.99")
|
|
|
|
path := writeTempConfig(t, minimal)
|
|
cfg, err := LoadAgentConfig(path)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
|
|
if cfg.MCNS.ServerURL != "https://override:28443" {
|
|
t.Fatalf("mcns.server_url: got %q", cfg.MCNS.ServerURL)
|
|
}
|
|
if cfg.MCNS.TokenPath != "/override/token" {
|
|
t.Fatalf("mcns.token_path: got %q", cfg.MCNS.TokenPath)
|
|
}
|
|
if cfg.MCNS.NodeAddr != "10.0.0.99" {
|
|
t.Fatalf("mcns.node_addr: got %q", cfg.MCNS.NodeAddr)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|