package config import ( "os" "path/filepath" "testing" ) // ── Helpers ────────────────────────────────────────────────────────────── // writeYAML creates a temporary YAML file and returns its path. func writeYAML(t *testing.T, dir, name, content string) string { t.Helper() path := filepath.Join(dir, name) if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatal(err) } return path } // ── 2.1: Parse minimal config ─────────────────────────────────────────── func TestLoad_MinimalConfig(t *testing.T) { dir := t.TempDir() path := writeYAML(t, dir, "config.yaml", ` agent: id: test-bot matrix: homeserver: https://matrix.example.com user_id: "@bot:example.com" llm: primary: provider: openai `) cfg, err := Load(path) if err != nil { t.Fatalf("Load() error: %v", err) } if cfg.Agent.ID != "test-bot" { t.Errorf("Agent.ID = %q, want %q", cfg.Agent.ID, "test-bot") } if cfg.Matrix.Homeserver != "https://matrix.example.com" { t.Errorf("Matrix.Homeserver = %q, want %q", cfg.Matrix.Homeserver, "https://matrix.example.com") } if cfg.Matrix.UserID != "@bot:example.com" { t.Errorf("Matrix.UserID = %q, want %q", cfg.Matrix.UserID, "@bot:example.com") } if cfg.LLM.Primary.Provider != "openai" { t.Errorf("LLM.Primary.Provider = %q, want %q", cfg.LLM.Primary.Provider, "openai") } } // ── 2.1b: ConfigDir populated from file path ─────────────────────────── func TestLoad_ConfigDir(t *testing.T) { // Create a nested directory to simulate agents/_specials/father-bot/ dir := filepath.Join(t.TempDir(), "agents", "_specials", "father-bot") if err := os.MkdirAll(dir, 0755); err != nil { t.Fatal(err) } path := writeYAML(t, dir, "config.yaml", ` agent: id: father-bot matrix: homeserver: https://matrix.example.com user_id: "@father-bot:example.com" llm: primary: provider: claude-code claude_code: binary: claude reasoning: system_prompt_file: prompts/system.md `) cfg, err := Load(path) if err != nil { t.Fatalf("Load() error: %v", err) } if cfg.ConfigDir != dir { t.Errorf("ConfigDir = %q, want %q", cfg.ConfigDir, dir) } // Verify that joining ConfigDir + system_prompt_file gives the right path spPath := filepath.Join(cfg.ConfigDir, cfg.LLM.Reasoning.SystemPromptFile) wantSuffix := filepath.Join("agents", "_specials", "father-bot", "prompts", "system.md") if !filepath.IsAbs(spPath) { // When running from TempDir, path will be absolute t.Logf("spPath = %q (expected to end with %q)", spPath, wantSuffix) } if cfg.LLM.Reasoning.SystemPromptFile != "prompts/system.md" { t.Errorf("SystemPromptFile = %q, want %q", cfg.LLM.Reasoning.SystemPromptFile, "prompts/system.md") } } // ── 2.2: Parse full config with all sections ──────────────────────────── func TestLoad_FullConfig(t *testing.T) { dir := t.TempDir() path := writeYAML(t, dir, "config.yaml", ` agent: id: full-agent name: "Full Agent" version: "1.0.0" type: agent enabled: true description: "A fully configured agent" tags: ["test", "full"] personality: tone: friendly language: es prefix: "!" role: "test assistant" communication: formality: semiformal humor: subtle 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: 10 reasoning: system_prompt_file: prompts/system.md context_window: 16000 matrix: homeserver: https://matrix.example.com user_id: "@bot:example.com" device_id: TESTDEVICE encryption: enabled: true store_path: ./data/crypto trust_mode: tofu rooms: listen: ["!room1:example.com"] respond: ["!room1:example.com"] filters: command_prefix: "!" mention_respond: true dm_respond: true ignore_bots: true threads: enabled: true auto_thread: false tools: ssh: enabled: true allowed_targets: ["prod-01"] http: enabled: true allowed_domains: ["api.example.com"] security: sanitize: enabled: true mode: warn min_severity: medium tool_rate_limit: enabled: true max_calls_per_min: 20 memory: enabled: true window_size: 30 storage: base_path: /tmp/test-data `) cfg, err := Load(path) if err != nil { t.Fatalf("Load() error: %v", err) } // Agent identity if cfg.Agent.ID != "full-agent" { t.Errorf("Agent.ID = %q, want %q", cfg.Agent.ID, "full-agent") } if cfg.Agent.Name != "Full Agent" { t.Errorf("Agent.Name = %q, want %q", cfg.Agent.Name, "Full Agent") } if cfg.Agent.Version != "1.0.0" { t.Errorf("Agent.Version = %q, want %q", cfg.Agent.Version, "1.0.0") } if !cfg.Agent.Enabled { t.Error("Agent.Enabled = false, want true") } if len(cfg.Agent.Tags) != 2 { t.Errorf("Agent.Tags len = %d, want 2", len(cfg.Agent.Tags)) } // Personality if cfg.Personality.Tone != "friendly" { t.Errorf("Personality.Tone = %q, want %q", cfg.Personality.Tone, "friendly") } if cfg.Personality.Role != "test assistant" { t.Errorf("Personality.Role = %q, want %q", cfg.Personality.Role, "test assistant") } if cfg.Personality.Communication.Formality != "semiformal" { t.Errorf("Communication.Formality = %q, want %q", cfg.Personality.Communication.Formality, "semiformal") } // LLM if cfg.LLM.Primary.Model != "gpt-4o" { t.Errorf("LLM.Primary.Model = %q, want %q", cfg.LLM.Primary.Model, "gpt-4o") } if cfg.LLM.Primary.MaxTokens != 4096 { t.Errorf("LLM.Primary.MaxTokens = %d, want 4096", cfg.LLM.Primary.MaxTokens) } if cfg.LLM.Primary.Temperature != 0.7 { t.Errorf("LLM.Primary.Temperature = %f, want 0.7", cfg.LLM.Primary.Temperature) } if !cfg.LLM.ToolUse.Enabled { t.Error("LLM.ToolUse.Enabled = false, want true") } if cfg.LLM.ToolUse.MaxIterations != 10 { t.Errorf("LLM.ToolUse.MaxIterations = %d, want 10", cfg.LLM.ToolUse.MaxIterations) } // Matrix if !cfg.Matrix.Encryption.Enabled { t.Error("Matrix.Encryption.Enabled = false, want true") } if cfg.Matrix.Encryption.TrustMode != "tofu" { t.Errorf("Encryption.TrustMode = %q, want %q", cfg.Matrix.Encryption.TrustMode, "tofu") } if len(cfg.Matrix.Rooms.Listen) != 1 { t.Errorf("Rooms.Listen len = %d, want 1", len(cfg.Matrix.Rooms.Listen)) } if !cfg.Matrix.Threads.Enabled { t.Error("Matrix.Threads.Enabled = false, want true") } // Tools if !cfg.Tools.SSH.Enabled { t.Error("Tools.SSH.Enabled = false, want true") } if !cfg.Tools.HTTP.Enabled { t.Error("Tools.HTTP.Enabled = false, want true") } // Security if !cfg.Security.Sanitize.Enabled { t.Error("Security.Sanitize.Enabled = false, want true") } if cfg.Security.Sanitize.Mode != "warn" { t.Errorf("Sanitize.Mode = %q, want %q", cfg.Security.Sanitize.Mode, "warn") } if !cfg.Security.ToolRateLimit.Enabled { t.Error("ToolRateLimit.Enabled = false, want true") } // Memory if !cfg.Memory.Enabled { t.Error("Memory.Enabled = false, want true") } if cfg.Memory.WindowSize != 30 { t.Errorf("Memory.WindowSize = %d, want 30", cfg.Memory.WindowSize) } // Storage if cfg.Storage.BasePath != "/tmp/test-data" { t.Errorf("Storage.BasePath = %q, want %q", cfg.Storage.BasePath, "/tmp/test-data") } } // ── 2.3: Env var expansion ────────────────────────────────────────────── func TestLoad_EnvVarExpansion(t *testing.T) { // Set env vars for the test t.Setenv("TEST_HOMESERVER", "https://expanded.example.com") t.Setenv("TEST_USER_ID", "@expanded:example.com") t.Setenv("TEST_PROVIDER", "anthropic") dir := t.TempDir() path := writeYAML(t, dir, "config.yaml", ` agent: id: env-test matrix: homeserver: "$TEST_HOMESERVER" user_id: "${TEST_USER_ID}" llm: primary: provider: "$TEST_PROVIDER" `) cfg, err := Load(path) if err != nil { t.Fatalf("Load() error: %v", err) } if cfg.Matrix.Homeserver != "https://expanded.example.com" { t.Errorf("Homeserver = %q, want %q", cfg.Matrix.Homeserver, "https://expanded.example.com") } if cfg.Matrix.UserID != "@expanded:example.com" { t.Errorf("UserID = %q, want %q", cfg.Matrix.UserID, "@expanded:example.com") } if cfg.LLM.Primary.Provider != "anthropic" { t.Errorf("Provider = %q, want %q", cfg.LLM.Primary.Provider, "anthropic") } } // ── 2.4: Unknown fields (forward compat) ──────────────────────────────── func TestLoad_UnknownFieldsIgnored(t *testing.T) { dir := t.TempDir() path := writeYAML(t, dir, "config.yaml", ` agent: id: unknown-fields-test future_field: "should be ignored" matrix: homeserver: https://matrix.example.com user_id: "@bot:example.com" new_section: enabled: true llm: primary: provider: openai `) cfg, err := Load(path) if err != nil { t.Fatalf("Load() error: %v", err) } if cfg.Agent.ID != "unknown-fields-test" { t.Errorf("Agent.ID = %q, want %q", cfg.Agent.ID, "unknown-fields-test") } } // ── 2.5: Default values ───────────────────────────────────────────────── func TestLoad_DefaultValues(t *testing.T) { dir := t.TempDir() path := writeYAML(t, dir, "config.yaml", ` agent: id: defaults-test matrix: homeserver: https://matrix.example.com user_id: "@bot:example.com" llm: primary: provider: openai `) cfg, err := Load(path) if err != nil { t.Fatalf("Load() error: %v", err) } // Boolean defaults should be false (Go zero values) if cfg.Agent.Enabled { t.Error("Agent.Enabled should default to false") } if cfg.Memory.Enabled { t.Error("Memory.Enabled should default to false") } if cfg.LLM.ToolUse.Enabled { t.Error("ToolUse.Enabled should default to false") } if cfg.Security.Sanitize.Enabled { t.Error("Sanitize.Enabled should default to false") } // Numeric defaults should be zero if cfg.LLM.Primary.MaxTokens != 0 { t.Errorf("MaxTokens should default to 0, got %d", cfg.LLM.Primary.MaxTokens) } if cfg.Memory.WindowSize != 0 { t.Errorf("Memory.WindowSize should default to 0, got %d", cfg.Memory.WindowSize) } // String defaults should be empty if cfg.Agent.Version != "" { t.Errorf("Agent.Version should default to empty, got %q", cfg.Agent.Version) } if cfg.Agent.Type != "" { t.Errorf("Agent.Type should default to empty, got %q", cfg.Agent.Type) } } // ── Validation tests ──────────────────────────────────────────────────── func TestLoad_MissingAgentID(t *testing.T) { dir := t.TempDir() path := writeYAML(t, dir, "config.yaml", ` matrix: homeserver: https://matrix.example.com user_id: "@bot:example.com" llm: primary: provider: openai `) _, err := Load(path) if err == nil { t.Fatal("Load() should fail when agent.id is missing") } } func TestLoad_MissingHomeserver(t *testing.T) { dir := t.TempDir() path := writeYAML(t, dir, "config.yaml", ` agent: id: test-bot matrix: user_id: "@bot:example.com" llm: primary: provider: openai `) _, err := Load(path) if err == nil { t.Fatal("Load() should fail when matrix.homeserver is missing") } } func TestLoad_MissingUserID(t *testing.T) { dir := t.TempDir() path := writeYAML(t, dir, "config.yaml", ` agent: id: test-bot matrix: homeserver: https://matrix.example.com llm: primary: provider: openai `) _, err := Load(path) if err == nil { t.Fatal("Load() should fail when matrix.user_id is missing") } } func TestLoad_MissingProvider(t *testing.T) { dir := t.TempDir() path := writeYAML(t, dir, "config.yaml", ` agent: id: test-bot matrix: homeserver: https://matrix.example.com user_id: "@bot:example.com" `) _, err := Load(path) if err == nil { t.Fatal("Load() should fail when llm.primary.provider is missing") } } func TestLoad_FileNotFound(t *testing.T) { _, err := Load("/nonexistent/path/config.yaml") if err == nil { t.Fatal("Load() should fail when file does not exist") } } func TestLoad_InvalidYAML(t *testing.T) { dir := t.TempDir() path := writeYAML(t, dir, "config.yaml", ` agent: id: [invalid yaml content this is broken `) _, err := Load(path) if err == nil { t.Fatal("Load() should fail on invalid YAML") } } // ── LoadMeta tests ────────────────────────────────────────────────────── func TestLoadMeta_BasicParsing(t *testing.T) { dir := t.TempDir() path := writeYAML(t, dir, "config.yaml", ` agent: id: meta-test name: "Meta Test" type: robot enabled: true description: "A test robot" matrix: homeserver: https://matrix.example.com user_id: "@bot:example.com" access_token_env: "$NONEXISTENT_VAR" `) cfg, err := LoadMeta(path) if err != nil { t.Fatalf("LoadMeta() error: %v", err) } if cfg.Agent.ID != "meta-test" { t.Errorf("Agent.ID = %q, want %q", cfg.Agent.ID, "meta-test") } if cfg.Agent.Type != "robot" { t.Errorf("Agent.Type = %q, want %q", cfg.Agent.Type, "robot") } // LoadMeta does NOT expand env vars — raw $NONEXISTENT_VAR kept if cfg.Matrix.AccessTokenEnv != "$NONEXISTENT_VAR" { t.Errorf("AccessTokenEnv = %q, want %q", cfg.Matrix.AccessTokenEnv, "$NONEXISTENT_VAR") } } func TestLoadMeta_MissingAgentID(t *testing.T) { dir := t.TempDir() path := writeYAML(t, dir, "config.yaml", ` agent: name: "No ID" `) _, err := LoadMeta(path) if err == nil { t.Fatal("LoadMeta() should fail when agent.id is missing") } } // ── LoadSpecial tests ─────────────────────────────────────────────────── func TestLoadSpecial_MinimalConfig(t *testing.T) { dir := t.TempDir() path := writeYAML(t, dir, "special.yaml", ` special: id: orchestrator type: orchestrator enabled: true llm: primary: provider: openai `) cfg, err := LoadSpecial(path) if err != nil { t.Fatalf("LoadSpecial() error: %v", err) } if cfg.Special.ID != "orchestrator" { t.Errorf("Special.ID = %q, want %q", cfg.Special.ID, "orchestrator") } if cfg.Special.Type != "orchestrator" { t.Errorf("Special.Type = %q, want %q", cfg.Special.Type, "orchestrator") } } func TestLoadSpecial_MissingSpecialID(t *testing.T) { dir := t.TempDir() path := writeYAML(t, dir, "special.yaml", ` special: type: orchestrator llm: primary: provider: openai `) _, err := LoadSpecial(path) if err == nil { t.Fatal("LoadSpecial() should fail when special.id is missing") } } func TestLoadSpecial_MissingType(t *testing.T) { dir := t.TempDir() path := writeYAML(t, dir, "special.yaml", ` special: id: orchestrator llm: primary: provider: openai `) _, err := LoadSpecial(path) if err == nil { t.Fatal("LoadSpecial() should fail when special.type is missing") } } func TestLoadSpecial_MissingProvider(t *testing.T) { dir := t.TempDir() path := writeYAML(t, dir, "special.yaml", ` special: id: orchestrator type: orchestrator `) _, err := LoadSpecial(path) if err == nil { t.Fatal("LoadSpecial() should fail when llm.primary.provider is missing") } }