diff --git a/internal/config/schema_test.go b/internal/config/schema_test.go new file mode 100644 index 0000000..17b1cfb --- /dev/null +++ b/internal/config/schema_test.go @@ -0,0 +1,211 @@ +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") + } +}