From 716f6b2fc94b4d423935020cc390a5514830cde7 Mon Sep 17 00:00:00 2001 From: m3ta-chiron Date: Sun, 18 Oct 2026 17:10:26 +0200 Subject: [PATCH] feat(config): Daemon-Port server.port mit Env-Override TALK_PORT (Default 7800) --- internal/config/config.go | 33 ++++++++++++++++-- internal/config/config_test.go | 64 ++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index e6627cf..084704d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strconv" "github.com/BurntSushi/toml" ) @@ -16,13 +17,21 @@ const ( SinkNull = "null" ) +// DefaultServerPort ist der Default-Port des Daemon-HTTP-Servers auf +// 127.0.0.1 (Task-04-Brief: „Port Default 7800"). +const DefaultServerPort = 7800 + // Config ist der effektiv geladene Konfigurations-Stand von talk. Task 02 // führt audio.sink; Task 03 ergänzt den Provider-Pin, Provider-Endpunkte -// und das Voice-Mapping; spätere Tasks ergänzen weitere Felder (server, -// queue …), ohne die Auflösungsreihenfolge zu ändern. +// und das Voice-Mapping; Task 04 den Daemon-Port (server.port, TALK_PORT); +// spätere Tasks ergänzen weitere Felder (queue …), ohne die Auflösungs- +// reihenfolge zu ändern. type Config struct { Audio Audio `toml:"audio"` + // Server steuert den Daemon-HTTP-Server (talkd) auf 127.0.0.1. + Server Server `toml:"server"` + // Provider pinnt die Kette auf genau einen Provider (z. B. // `provider = "elevenlabs"`); leer = komplette Default-Kette // (elevenlabs → openai → edge → osbuiltin). Das CLI-Flag --provider @@ -48,6 +57,14 @@ type Audio struct { Sink string `toml:"sink"` } +// Server sind die Netz-Einstellungen des Daemons (nur Loopback; Binding +// auf 127.0.0.1 macht cmd/talkd, die Config liefert nur den Port). +type Server struct { + // Port des Daemon-HTTP-Servers auf 127.0.0.1 — config + // `[server] port = 7800`, Env-Override TALK_PORT, Default 7800. + Port int `toml:"port"` +} + // ProviderSettings sind die per Provider konfigurierbaren Endpunkte und // Modelle (TOML `[providers.]`). Leer = eingebaute Defaults; die // Env-Overrides TALK_ELEVENLABS_ENDPOINT / TALK_OPENAI_ENDPOINT / @@ -62,7 +79,10 @@ type ProviderSettings struct { // Defaults liefert die eingebaute Default-Konfiguration (unterste Schicht // der Auflösung). func Defaults() Config { - return Config{Audio: Audio{Sink: SinkOto}} + return Config{ + Audio: Audio{Sink: SinkOto}, + Server: Server{Port: DefaultServerPort}, + } } // Options steuert Load. Alle Felder sind injizierbar, damit die Unit-Tests @@ -116,6 +136,13 @@ func applyEnv(cfg Config, getenv func(string) string) (Config, error) { if v := getenv("TALK_PROVIDER"); v != "" { cfg.Provider = v } + if v := getenv("TALK_PORT"); v != "" { + port, err := strconv.Atoi(v) + if err != nil || port < 1 || port > 65535 { + return cfg, fmt.Errorf("TALK_PORT: ungültiger Wert %q (1–65535)", v) + } + cfg.Server.Port = port + } switch cfg.Audio.Sink { case SinkOto, SinkNull: return cfg, nil diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 17faa03..9623785 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -288,3 +288,67 @@ de = "de-DE-KatjaNeural" } }) } + +// TestLoadServerPort deckt die Task-04-Felder ab: Daemon-Port per Defaults +// (7800), TOML [server] port, Env-Override TALK_PORT samt Validierung. +func TestLoadServerPort(t *testing.T) { + writeTOML := func(t *testing.T, content string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "config.toml") + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + return path + } + + t.Run("Default 7800", func(t *testing.T) { + cfg, err := Load(Options{ + File: filepath.Join(t.TempDir(), "gibt-es-nicht.toml"), + Getenv: env(nil), + }) + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.Server.Port != DefaultServerPort { + t.Errorf("Server.Port = %d, want Default %d", cfg.Server.Port, DefaultServerPort) + } + }) + + t.Run("TOML überschreibt Default", func(t *testing.T) { + cfg, err := Load(Options{ + File: writeTOML(t, "[server]\nport = 7999\n"), + Getenv: env(nil), + }) + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.Server.Port != 7999 { + t.Errorf("Server.Port = %d, want 7999 aus TOML", cfg.Server.Port) + } + }) + + t.Run("TALK_PORT schlägt Datei", func(t *testing.T) { + cfg, err := Load(Options{ + File: writeTOML(t, "[server]\nport = 7999\n"), + Getenv: env(map[string]string{"TALK_PORT": "7912"}), + }) + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.Server.Port != 7912 { + t.Errorf("Server.Port = %d, want Env-Override 7912", cfg.Server.Port) + } + }) + + t.Run("ungültiger TALK_PORT-Wert ist klarer Fehler", func(t *testing.T) { + for _, bad := range []string{"null", "0", "-1", "70000"} { + _, err := Load(Options{ + File: filepath.Join(t.TempDir(), "gibt-es-nicht.toml"), + Getenv: env(map[string]string{"TALK_PORT": bad}), + }) + if err == nil || !strings.Contains(err.Error(), "TALK_PORT") { + t.Fatalf("TALK_PORT=%q: Fehler = %v, will klaren TALK_PORT-Fehler", bad, err) + } + } + }) +}