package config import ( "path/filepath" "testing" "time" ) func writeConfig(t *testing.T, body string) string { t.Helper() p := filepath.Join(t.TempDir(), "mavend.json") if err := writeFile(t, p, body); err != nil { t.Fatalf("write config: %v", err) } return p } func TestLoadDefaults(t *testing.T) { p := writeConfig(t, `{}`) c, err := Load(p) if err != nil { t.Fatalf("Load: %v", err) } if time.Duration(c.TickInterval) != DefaultTickInterval { t.Errorf("TickInterval default = %v, want %v", c.TickInterval, DefaultTickInterval) } if time.Duration(c.RepeatInterval) != DefaultRepeatInterval { t.Errorf("RepeatInterval default = %v, want %v", c.RepeatInterval, DefaultRepeatInterval) } if c.DBPath == "" { t.Error("DBPath default not applied") } if c.SocketPath == "" { t.Error("SocketPath default not applied") } } func TestLoadDurationsParse(t *testing.T) { p := writeConfig(t, `{"tick_interval":"90s","repeat_interval":"10m"}`) c, err := Load(p) if err != nil { t.Fatalf("Load: %v", err) } if time.Duration(c.TickInterval) != 90*time.Second { t.Errorf("TickInterval = %v, want 90s", c.TickInterval) } if time.Duration(c.RepeatInterval) != 10*time.Minute { t.Errorf("RepeatInterval = %v, want 10m", c.RepeatInterval) } } func TestLoadBadDurationRejected(t *testing.T) { p := writeConfig(t, `{"tick_interval":"not-a-duration"}`) if _, err := Load(p); err == nil { t.Fatal("Load succeeded for a bad duration; want error") } } func TestLoadMissingFile(t *testing.T) { p := filepath.Join(t.TempDir(), "nonexistent.json") if _, err := Load(p); err == nil { t.Fatal("Load succeeded for a missing file; want error") } } func TestVoiceEnabledRequiresBind(t *testing.T) { // enabled=true without bind is refused — the voice surface can't // default a bind (127.0.0.1 too relaxed for production, a wg addr is // the user's). surfacing the gap explicitly beats an idle listener. p := writeConfig(t, `{"voice":{"enabled":true}}`) if _, err := Load(p); err == nil { t.Fatal("Load succeeded for voice.enabled=true with no bind; want error") } } func TestVoiceEnabledWithBindOK(t *testing.T) { // voice surface fully configured — accepted (the daemon wires Stub tts + // voicesink; no models on disk required). p := writeConfig(t, `{"voice":{"enabled":true,"bind":"127.0.0.1:9100"}}`) if _, err := Load(p); err != nil { t.Fatalf("Load: %v", err) } } func TestVoicePresentDisabledOK(t *testing.T) { // voice block present but not enabled — acceptable (the listener stays // down; the routing table's ChannelVoice selections drop). p := writeConfig(t, `{"voice":{"enabled":false}}`) if _, err := Load(p); err != nil { t.Fatalf("Load: %v", err) } } func TestVoiceSttTtsConfigParsed(t *testing.T) { // the stt/tts worker sub-blocks parse + remember socket/lang. p := writeConfig(t, `{ "voice": { "enabled": true, "bind": "127.0.0.1:9100", "stt": {"socket": "/tmp/stt.sock", "lang": "ru"}, "tts": {"socket": "/tmp/tts.sock", "lang": "ru", "voice": "natasha"} } }`) c, err := Load(p) if err != nil { t.Fatalf("Load: %v", err) } if c.Voice.Stt == nil || c.Voice.Stt.Socket != "/tmp/stt.sock" { t.Fatalf("Stt config not parsed: %+v", c.Voice) } if c.Voice.Tts == nil || c.Voice.Tts.Socket != "/tmp/tts.sock" || c.Voice.Tts.Voice != "natasha" { t.Fatalf("Tts config not parsed: %+v", c.Voice) } } func TestDurationRoundTrip(t *testing.T) { d := Duration(15 * time.Minute) b, err := d.MarshalJSON() if err != nil { t.Fatalf("MarshalJSON: %v", err) } if got, want := string(b), `"15m0s"`; got != want { t.Errorf("MarshalJSON = %s, want %s", got, want) } var d2 Duration if err := d2.UnmarshalJSON(b); err != nil { t.Fatalf("UnmarshalJSON: %v", err) } if d2 != d { t.Errorf("round-trip = %v, want %v", d2, d) } }