619 lines
11 KiB
Go
619 lines
11 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// validConfig returns a minimal valid YAML configuration using the
|
|
// multi-provider format.
|
|
func validConfig() string {
|
|
return `
|
|
address: ":9090"
|
|
shutdown_timeout: "60s"
|
|
|
|
default_model:
|
|
provider: testprovider
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
voice: af_heart
|
|
`
|
|
}
|
|
|
|
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",
|
|
config: validConfig(),
|
|
check: func(t *testing.T, cfg *Config) {
|
|
if cfg.Address != ":9090" {
|
|
t.Errorf(
|
|
"Address = %q, want %q",
|
|
cfg.Address, ":9090",
|
|
)
|
|
}
|
|
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(),
|
|
)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "defaults applied",
|
|
config: `
|
|
default_model:
|
|
provider: testprovider
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
voice: af_heart
|
|
`,
|
|
check: func(t *testing.T, cfg *Config) {
|
|
if cfg.Address != ":8080" {
|
|
t.Errorf(
|
|
"Address = %q, want %q",
|
|
cfg.Address, ":8080",
|
|
)
|
|
}
|
|
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(),
|
|
)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "env expansion",
|
|
config: `
|
|
default_model:
|
|
provider: testprovider
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
key: ${TEST_LLM_KEY}
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
voice: af_heart
|
|
`,
|
|
setup: func(t *testing.T) {
|
|
t.Setenv("TEST_LLM_KEY", "secret-api-key")
|
|
},
|
|
check: func(t *testing.T, cfg *Config) {
|
|
if len(cfg.Providers) != 1 {
|
|
t.Fatalf("len(Providers) = %d, want 1", len(cfg.Providers))
|
|
}
|
|
if cfg.Providers[0].Key != "secret-api-key" {
|
|
t.Errorf(
|
|
"Providers[0].Key = %q, want %q",
|
|
cfg.Providers[0].Key, "secret-api-key",
|
|
)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "env expansion unset var becomes empty",
|
|
config: `
|
|
default_model:
|
|
provider: testprovider
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
key: ${TEST_UNSET_VAR}
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
voice: af_heart
|
|
`,
|
|
check: func(t *testing.T, cfg *Config) {
|
|
if len(cfg.Providers) != 1 {
|
|
t.Fatalf("len(Providers) = %d, want 1", len(cfg.Providers))
|
|
}
|
|
if cfg.Providers[0].Key != "" {
|
|
t.Errorf(
|
|
"Providers[0].Key = %q, want empty",
|
|
cfg.Providers[0].Key,
|
|
)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "invalid yaml",
|
|
config: `providers: [invalid yaml`,
|
|
shouldErr: true,
|
|
},
|
|
{
|
|
name: "invalid shutdown_timeout",
|
|
config: `
|
|
shutdown_timeout: "not-a-duration"
|
|
|
|
default_model:
|
|
provider: testprovider
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
voice: af_heart
|
|
`,
|
|
shouldErr: true,
|
|
},
|
|
{
|
|
name: "missing default_model.provider",
|
|
config: `
|
|
default_model:
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
voice: af_heart
|
|
`,
|
|
shouldErr: true,
|
|
},
|
|
{
|
|
name: "missing default_model.model",
|
|
config: `
|
|
default_model:
|
|
provider: testprovider
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
voice: af_heart
|
|
`,
|
|
shouldErr: true,
|
|
},
|
|
{
|
|
name: "missing provider url",
|
|
config: `
|
|
default_model:
|
|
provider: testprovider
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
voice: af_heart
|
|
`,
|
|
shouldErr: true,
|
|
},
|
|
{
|
|
name: "missing stt url",
|
|
config: `
|
|
default_model:
|
|
provider: testprovider
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
|
|
stt:
|
|
url: ""
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
voice: af_heart
|
|
`,
|
|
shouldErr: true,
|
|
},
|
|
{
|
|
name: "missing tts url",
|
|
config: `
|
|
default_model:
|
|
provider: testprovider
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
voice: af_heart
|
|
`,
|
|
shouldErr: true,
|
|
},
|
|
{
|
|
name: "missing tts voice",
|
|
config: `
|
|
default_model:
|
|
provider: testprovider
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
`,
|
|
shouldErr: true,
|
|
},
|
|
{
|
|
name: "config with tools",
|
|
config: `
|
|
default_model:
|
|
provider: testprovider
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
voice: af_heart
|
|
|
|
tools:
|
|
- name: echo
|
|
description: echoes input
|
|
command: echo
|
|
arguments:
|
|
- "{{.message}}"
|
|
`,
|
|
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: "invalid tool",
|
|
config: `
|
|
default_model:
|
|
provider: testprovider
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
voice: af_heart
|
|
|
|
tools:
|
|
- name: ""
|
|
description: missing name
|
|
command: echo
|
|
`,
|
|
shouldErr: true,
|
|
},
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
func TestApplyDefaults(t *testing.T) {
|
|
cfg := &Config{}
|
|
cfg.ApplyDefaults()
|
|
|
|
if cfg.Address != ":8080" {
|
|
t.Errorf("Address = %q, want %q", cfg.Address, ":8080")
|
|
}
|
|
if cfg.ShutdownTimeout != "30s" {
|
|
t.Errorf(
|
|
"ShutdownTimeout = %q, want %q",
|
|
cfg.ShutdownTimeout, "30s",
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestApplyDefaults_NoOverwrite(t *testing.T) {
|
|
cfg := &Config{
|
|
Address: ":9999",
|
|
ShutdownTimeout: "120s",
|
|
}
|
|
cfg.ApplyDefaults()
|
|
|
|
if cfg.Address != ":9999" {
|
|
t.Errorf("Address = %q, want %q", cfg.Address, ":9999")
|
|
}
|
|
if cfg.ShutdownTimeout != "120s" {
|
|
t.Errorf(
|
|
"ShutdownTimeout = %q, want %q",
|
|
cfg.ShutdownTimeout, "120s",
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestLoad_Jobs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config string
|
|
shouldErr bool
|
|
check func(t *testing.T, cfg *Config)
|
|
}{
|
|
{
|
|
name: "valid job config",
|
|
config: `
|
|
default_model:
|
|
provider: testprovider
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
voice: af_heart
|
|
|
|
jobs:
|
|
- name: daily_summary
|
|
schedule: "0 9 * * *"
|
|
system_message: "You are a research assistant."
|
|
task: "Summarize today's news about AI."
|
|
timeout: "10m"
|
|
max_iterations: 20
|
|
`,
|
|
check: func(t *testing.T, cfg *Config) {
|
|
if len(cfg.Jobs) != 1 {
|
|
t.Fatalf("len(Jobs) = %d, want 1", len(cfg.Jobs))
|
|
}
|
|
j := cfg.Jobs[0]
|
|
if j.Name != "daily_summary" {
|
|
t.Errorf("Name = %q, want %q", j.Name, "daily_summary")
|
|
}
|
|
if j.Task != "Summarize today's news about AI." {
|
|
t.Errorf("Task = %q, want %q", j.Task, "Summarize today's news about AI.")
|
|
}
|
|
if j.Schedule != "0 9 * * *" {
|
|
t.Errorf("Schedule = %q, want %q", j.Schedule, "0 9 * * *")
|
|
}
|
|
timeout, err := time.ParseDuration(j.Timeout)
|
|
if err != nil {
|
|
t.Fatalf("ParseDuration(%q) error = %v", j.Timeout, err)
|
|
}
|
|
if timeout.Minutes() != 10 {
|
|
t.Errorf("Timeout = %v, want 10m", timeout)
|
|
}
|
|
if j.MaxIterations != 20 {
|
|
t.Errorf("MaxIterations = %d, want 20", j.MaxIterations)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "job with minimal config (defaults)",
|
|
config: `
|
|
default_model:
|
|
provider: testprovider
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
voice: af_heart
|
|
|
|
jobs:
|
|
- name: log_job
|
|
schedule: "*/5 * * * *"
|
|
task: "Check system health."
|
|
`,
|
|
check: func(t *testing.T, cfg *Config) {
|
|
if len(cfg.Jobs) != 1 {
|
|
t.Fatalf("len(Jobs) = %d, want 1", len(cfg.Jobs))
|
|
}
|
|
j := cfg.Jobs[0]
|
|
if j.MaxIterations != 0 {
|
|
t.Errorf("MaxIterations = %d, want 0 (unlimited)", j.MaxIterations)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "job missing name",
|
|
config: `
|
|
default_model:
|
|
provider: testprovider
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
voice: af_heart
|
|
|
|
jobs:
|
|
- schedule: "0 9 * * *"
|
|
task: "do something"
|
|
`,
|
|
shouldErr: true,
|
|
},
|
|
{
|
|
name: "job missing task",
|
|
config: `
|
|
default_model:
|
|
provider: testprovider
|
|
model: test-model
|
|
|
|
providers:
|
|
- name: testprovider
|
|
url: http://localhost:8080
|
|
|
|
stt:
|
|
url: http://localhost:8178
|
|
|
|
tts:
|
|
url: http://localhost:8880
|
|
voice: af_heart
|
|
|
|
jobs:
|
|
- name: no_task
|
|
schedule: "0 9 * * *"
|
|
`,
|
|
shouldErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.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)
|
|
}
|
|
})
|
|
}
|
|
}
|