package config import ( "testing" "gopkg.in/yaml.v3" ) // TestAgentConfigParseMinimal verifies that a minimal config YAML (with only // required fields) parses into AgentConfig without error. func TestAgentConfigParseMinimal(t *testing.T) { const minimalYAML = ` agent: id: test-bot name: Test Bot enabled: true matrix: homeserver: "https://matrix.example.com" user_id: "@test:matrix.example.com" llm: primary: provider: openai model: gpt-4o ` var cfg AgentConfig if err := yaml.Unmarshal([]byte(minimalYAML), &cfg); err != nil { t.Fatalf("failed to parse minimal config: %v", err) } if cfg.Agent.ID != "test-bot" { t.Errorf("expected agent.id=test-bot, got %q", cfg.Agent.ID) } if cfg.Matrix.Homeserver != "https://matrix.example.com" { t.Errorf("expected homeserver, got %q", cfg.Matrix.Homeserver) } if cfg.LLM.Primary.Provider != "openai" { t.Errorf("expected provider=openai, got %q", cfg.LLM.Primary.Provider) } } // TestAgentConfigIgnoresRemovedSections verifies that YAML containing the // removed sections (agents, observability, resilience) still parses without // error. yaml.v3 silently ignores unknown keys. func TestAgentConfigIgnoresRemovedSections(t *testing.T) { const yamlWithRemoved = ` agent: id: legacy-bot name: Legacy Bot enabled: true matrix: homeserver: "https://matrix.example.com" user_id: "@legacy:matrix.example.com" llm: primary: provider: openai model: gpt-4o # These sections were removed from the schema but may still exist in old YAMLs. agents: peers: - id: other-bot capabilities: [general] room: "!abc:server.com" delegation: enabled: false protocol: format: json channel: matrix observability: logging: level: info format: json metrics: enabled: false health: enabled: true port: 8080 tracing: enabled: false resilience: circuit_breaker: failure_threshold: 5 timeout: 30s retry: max_attempts: 2 backoff: exponential shutdown: timeout: 10s queue: enabled: true max_size: 100 ` var cfg AgentConfig if err := yaml.Unmarshal([]byte(yamlWithRemoved), &cfg); err != nil { t.Fatalf("parsing config with removed sections should succeed, got: %v", err) } if cfg.Agent.ID != "legacy-bot" { t.Errorf("expected agent.id=legacy-bot, got %q", cfg.Agent.ID) } } // TestAgentConfigParseFull verifies that a config YAML with all active sections // parses correctly, including personality with communication. func TestAgentConfigParseFull(t *testing.T) { const fullYAML = ` agent: id: full-bot name: Full Bot version: "1.0.0" enabled: true description: "A fully configured bot" tags: [test, full] personality: tone: friendly verbosity: concise language: es role: "asistente general" communication: formality: semiformal humor: subtle personality: pragmatic response_style: structured quirks: ["usa analogias"] avoid_topics: ["politica"] catchphrases: ["interesante"] llm: primary: provider: openai model: gpt-4o api_key_env: OPENAI_API_KEY max_tokens: 4096 temperature: 0.7 tool_use: enabled: true max_iterations: 5 matrix: homeserver: "https://matrix.example.com" user_id: "@full:matrix.example.com" access_token_env: MATRIX_TOKEN threads: enabled: true auto_thread: false tools: ssh: enabled: false http: enabled: true allowed_domains: ["api.example.com"] timeout: 10s security: sanitize: enabled: true mode: warn min_severity: medium tool_rate_limit: enabled: true max_calls_per_min: 10 storage: base_path: "/data/full-bot" memory: enabled: true window_size: 30 skills: enabled: true path: "skills/" categories: ["devops"] timeout: 60s ` var cfg AgentConfig if err := yaml.Unmarshal([]byte(fullYAML), &cfg); err != nil { t.Fatalf("failed to parse full config: %v", err) } // Verify key fields if cfg.Agent.ID != "full-bot" { t.Errorf("agent.id: got %q", cfg.Agent.ID) } if cfg.Personality.Communication.Humor != "subtle" { t.Errorf("personality.communication.humor: got %q", cfg.Personality.Communication.Humor) } if len(cfg.Personality.Communication.Quirks) != 1 { t.Errorf("personality.communication.quirks: expected 1, got %d", len(cfg.Personality.Communication.Quirks)) } if !cfg.LLM.ToolUse.Enabled { t.Error("llm.tool_use.enabled should be true") } if !cfg.Tools.HTTP.Enabled { t.Error("tools.http.enabled should be true") } if cfg.Storage.BasePath != "/data/full-bot" { t.Errorf("storage.base_path: got %q", cfg.Storage.BasePath) } if !cfg.Memory.Enabled { t.Error("memory.enabled should be true") } if !cfg.Skills.Enabled { t.Error("skills.enabled should be true") } if !cfg.Security.Sanitize.Enabled { t.Error("security.sanitize.enabled should be true") } } // TestDeviceMeshConfig_Parse verifies that the device_mesh block parses into // the expected DeviceMeshConfig pointer with both YAML key variants (host vs // device_id, timeout_seconds vs client_timeout_s, tools_allowed list). func TestDeviceMeshConfig_Parse(t *testing.T) { const yamlBody = ` agent: id: agent-home-wsl name: home wsl enabled: true matrix: homeserver: "https://matrix.example.com" user_id: "@agent-home-wsl:matrix.example.com" llm: primary: provider: anthropic model: claude-sonnet device_mesh: enabled: true device_id: home-wsl mode: user device_agent_url: "http://10.42.0.10:7474" device_agent_url_env: AGENT_HOME_WSL_DEVICE_MESH_URL manifest_id: manifest_home-wsl_v1 client_timeout_s: 60 tools_allowed: - exec - fs.read - fs.list ` var cfg AgentConfig if err := yaml.Unmarshal([]byte(yamlBody), &cfg); err != nil { t.Fatalf("parse: %v", err) } if cfg.DeviceMesh == nil { t.Fatalf("expected DeviceMesh to be non-nil") } dm := cfg.DeviceMesh if !dm.Enabled { t.Error("enabled should be true") } if dm.DeviceID != "home-wsl" { t.Errorf("device_id: got %q", dm.DeviceID) } if dm.ResolvedHost() != "home-wsl" { t.Errorf("ResolvedHost(): got %q", dm.ResolvedHost()) } if dm.Mode != "user" { t.Errorf("mode: got %q", dm.Mode) } if dm.DeviceAgentURL != "http://10.42.0.10:7474" { t.Errorf("device_agent_url: got %q", dm.DeviceAgentURL) } if dm.URLEnv != "AGENT_HOME_WSL_DEVICE_MESH_URL" { t.Errorf("device_agent_url_env: got %q", dm.URLEnv) } if dm.ManifestID != "manifest_home-wsl_v1" { t.Errorf("manifest_id: got %q", dm.ManifestID) } if dm.ResolvedTimeoutSeconds() != 60 { t.Errorf("ResolvedTimeoutSeconds(): got %d", dm.ResolvedTimeoutSeconds()) } if len(dm.ToolsAllowed) != 3 { t.Errorf("tools_allowed: got %d entries", len(dm.ToolsAllowed)) } } // TestDeviceMeshConfig_Absent ensures the field stays nil when the block is // not present in YAML — the runtime relies on the nil-check to short-circuit. func TestDeviceMeshConfig_Absent(t *testing.T) { const yamlBody = ` agent: id: plain-bot enabled: true matrix: homeserver: "https://matrix.example.com" user_id: "@plain-bot:matrix.example.com" llm: primary: provider: openai model: gpt-4o ` var cfg AgentConfig if err := yaml.Unmarshal([]byte(yamlBody), &cfg); err != nil { t.Fatalf("parse: %v", err) } if cfg.DeviceMesh != nil { t.Errorf("expected nil DeviceMesh, got %+v", cfg.DeviceMesh) } } // TestDeviceMeshConfig_TimeoutFallback verifies that timeout_seconds is used // when client_timeout_s is absent. func TestDeviceMeshConfig_TimeoutFallback(t *testing.T) { dm := &DeviceMeshConfig{TimeoutSeconds: 45} if got := dm.ResolvedTimeoutSeconds(); got != 45 { t.Errorf("expected 45, got %d", got) } dm2 := &DeviceMeshConfig{ClientTimeoutS: 90} if got := dm2.ResolvedTimeoutSeconds(); got != 90 { t.Errorf("expected 90, got %d", got) } // TimeoutSeconds wins when both set. dm3 := &DeviceMeshConfig{TimeoutSeconds: 30, ClientTimeoutS: 60} if got := dm3.ResolvedTimeoutSeconds(); got != 30 { t.Errorf("expected 30, got %d", got) } if (*DeviceMeshConfig)(nil).ResolvedTimeoutSeconds() != 0 { t.Errorf("nil receiver should return 0") } }