package config import ( "os" "path/filepath" "testing" "time" ) // validConfig returns a minimal valid YAML configuration. func validConfig() string { return ` imap: host: imap.example.com port: 993 user: test@example.com password: secret mailbox: INBOX intake: mode: poll poll_interval: 30s llm: provider: llama.cpp model: qwen3-4b url: http://localhost:8080 smtp: host: smtp.example.com port: 587 ` } func writeConfig(t *testing.T, content string) string { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "config.yaml") if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("write config file: %v", err) } return path } func TestLoad(t *testing.T) { tests := []struct { name string config string setup func(t *testing.T) shouldErr bool check func(t *testing.T, cfg *Config) }{ { name: "valid config with defaults", config: validConfig(), check: func(t *testing.T, cfg *Config) { if cfg.Concurrency != 1 { t.Errorf( "Concurrency = %d, want 1", cfg.Concurrency, ) } if cfg.ShutdownTimeout != "30s" { t.Errorf( "ShutdownTimeout = %q, want %q", cfg.ShutdownTimeout, "30s", ) } if cfg.GetShutdownTimeout() != 30*time.Second { t.Errorf( "GetShutdownTimeout() = %v, want 30s", cfg.GetShutdownTimeout(), ) } if cfg.SMTP.User != cfg.IMAP.User { t.Errorf( "SMTP.User = %q, want %q", cfg.SMTP.User, cfg.IMAP.User, ) } if cfg.SMTP.Password != cfg.IMAP.Password { t.Errorf( "SMTP.Password not inherited from IMAP", ) } if cfg.SMTP.From != cfg.IMAP.User { t.Errorf( "SMTP.From = %q, want %q", cfg.SMTP.From, cfg.IMAP.User, ) } if cfg.Intake.Mode != "poll" { t.Errorf( "Intake.Mode = %q, want %q", cfg.Intake.Mode, "poll", ) } if cfg.Intake.PollInterval != "30s" { t.Errorf( "Intake.PollInterval = %q, want %q", cfg.Intake.PollInterval, "30s", ) } }, }, { name: "explicit values not overwritten", config: ` concurrency: 5 shutdown_timeout: "60s" imap: host: imap.example.com port: 993 user: test@example.com password: secret mailbox: INBOX intake: mode: idle llm: provider: llama.cpp model: qwen3-4b url: http://localhost:8080 smtp: host: smtp.example.com port: 587 user: smtp-user password: smtp-password from: sender@example.com `, check: func(t *testing.T, cfg *Config) { if cfg.Concurrency != 5 { t.Errorf( "Concurrency = %d, want 5", cfg.Concurrency, ) } if cfg.ShutdownTimeout != "60s" { t.Errorf( "ShutdownTimeout = %q, want %q", cfg.ShutdownTimeout, "60s", ) } if cfg.GetShutdownTimeout() != 60*time.Second { t.Errorf( "GetShutdownTimeout() = %v, want 60s", cfg.GetShutdownTimeout(), ) } if cfg.SMTP.User != "smtp-user" { t.Errorf( "SMTP.User = %q, want %q", cfg.SMTP.User, "smtp-user", ) } if cfg.SMTP.Password != "smtp-password" { t.Errorf( "SMTP.Password = %q, want %q", cfg.SMTP.Password, "smtp-password", ) } if cfg.SMTP.From != "sender@example.com" { t.Errorf( "SMTP.From = %q, want %q", cfg.SMTP.From, "sender@example.com", ) } }, }, { name: "env expansion", config: ` imap: host: imap.example.com port: 993 user: test@example.com password: ${TEST_IMAP_PASSWORD} mailbox: INBOX intake: mode: poll poll_interval: 30s llm: provider: llama.cpp model: qwen3-4b url: http://localhost:8080 smtp: host: smtp.example.com port: 587 `, setup: func(t *testing.T) { t.Setenv("TEST_IMAP_PASSWORD", "env-secret") }, check: func(t *testing.T, cfg *Config) { if cfg.IMAP.Password != "env-secret" { t.Errorf( "IMAP.Password = %q, want %q", cfg.IMAP.Password, "env-secret", ) } }, }, { name: "env expansion unset var becomes empty", config: ` imap: host: imap.example.com port: 993 user: test@example.com password: ${TEST_UNSET_VAR} mailbox: INBOX intake: mode: poll poll_interval: 30s llm: provider: llama.cpp model: qwen3-4b url: http://localhost:8080 smtp: host: smtp.example.com port: 587 `, setup: func(t *testing.T) { t.Setenv("TEST_UNSET_VAR", "") }, shouldErr: true, }, { name: "invalid yaml", config: `imap: [invalid yaml`, shouldErr: true, }, { name: "invalid shutdown_timeout", config: ` imap: host: imap.example.com port: 993 user: test@example.com password: secret mailbox: INBOX intake: mode: poll poll_interval: 30s llm: provider: llama.cpp model: qwen3-4b url: http://localhost:8080 smtp: host: smtp.example.com port: 587 shutdown_timeout: "not-a-duration" `, shouldErr: true, }, { name: "config with tools", config: ` imap: host: imap.example.com port: 993 user: test@example.com password: secret mailbox: INBOX intake: mode: poll poll_interval: 30s llm: provider: llama.cpp model: qwen3-4b url: http://localhost:8080 smtp: host: smtp.example.com port: 587 tools: - name: echo description: echoes input command: echo `, check: func(t *testing.T, cfg *Config) { if len(cfg.Tools) != 1 { t.Errorf( "len(Tools) = %d, want 1", len(cfg.Tools), ) } if cfg.Tools[0].Name != "echo" { t.Errorf( "Tools[0].Name = %q, want %q", cfg.Tools[0].Name, "echo", ) } }, }, { name: "intake mode defaults when empty", config: ` imap: host: imap.example.com port: 993 user: test@example.com password: secret mailbox: INBOX llm: provider: llama.cpp model: qwen3-4b url: http://localhost:8080 smtp: host: smtp.example.com port: 587 `, check: func(t *testing.T, cfg *Config) { if cfg.Intake.Mode != "poll" { t.Errorf( "Intake.Mode = %q, want %q", cfg.Intake.Mode, "poll", ) } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.setup != nil { tt.setup(t) } path := writeConfig(t, tt.config) cfg, err := Load(path) if tt.shouldErr { if err == nil { t.Fatal("Load() expected error") } return } if err != nil { t.Fatalf("Load() error = %v", err) } if tt.check != nil { tt.check(t, cfg) } }) } } func TestLoad_FileNotFound(t *testing.T) { if _, err := Load("/nonexistent/path/config.yaml"); err == nil { t.Fatal("Load() expected error for missing file") } }