06501e2bcc
Test con directorio anidado (agents/_specials/father-bot/) que confirma que ConfigDir se resuelve al directorio padre del config y que system_prompt_file se puede resolver relativo a el. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
596 lines
15 KiB
Go
596 lines
15 KiB
Go
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")
|
|
}
|
|
}
|