feat(config): Gesetz-Felder rate_limit, allow_high_burst, quiet_hours, quiet_mode

Queue-Section ([queue] rate_limit = "30s", allow_high_burst = true) und
Ruhezeiten (quiet_hours = "18:00-08:00" lokal über Mitternacht,
quiet_mode = discard|toast) mit Env-Overrides TALK_RATE_LIMIT,
TALK_ALLOW_HIGH_BURST, TALK_QUIET_HOURS ("off" = aus), TALK_QUIET_MODE.
Default: 1 Ansage / 30 s, Burst an, Ruhezeit aus (Opt-in wie Mute —
ein Uhrzeit-Gesetz als Default würde jeden Cron in den Abendstunden
stilllegen). Validierung gegen queue.ParseQuietHours als eine Quelle;
partielle [queue]-Tabellen behalten Defaults (tischgetestet).
This commit is contained in:
m3ta-chiron committed 2026-10-18 17:40:35 +02:00
1 parent dc94f45d11
commit b20e6dd319
2 files changed
+263 -8

No files matched your search

+179
View File
@@ -5,6 +5,9 @@ import (
"path/filepath"
"strings"
"testing"
"time"
"code.m3ta.dev/m3tam3re/talk/internal/queue"
)
func env(kv map[string]string) func(string) string {
@@ -352,3 +355,179 @@ func TestLoadServerPort(t *testing.T) {
}
})
}
// TestLoadGesetze deckt die Task-05-Felder ab: Rate-Limit (Default 30 s,
// "0s" = unbegrenzt), High-Burst (Default an), Ruhezeiten (Default aus;
// Format validiert) samt Env-Overrides und klaren Fehlern.
func TestLoadGesetze(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
}
fehlendeDatei := func(t *testing.T) string {
t.Helper()
return filepath.Join(t.TempDir(), "gibt-es-nicht.toml")
}
t.Run("Defaults: 30 s, Burst an, Ruhezeit aus, Modus discard", func(t *testing.T) {
cfg, err := Load(Options{File: fehlendeDatei(t), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Queue.RateLimit != DefaultRateLimit {
t.Errorf("Queue.RateLimit = %v, want Default %v", cfg.Queue.RateLimit, DefaultRateLimit)
}
if !cfg.Queue.AllowHighBurst {
t.Error("Queue.AllowHighBurst = false, want Default true")
}
if cfg.QuietHours != "" {
t.Errorf("QuietHours = %q, want Default leer (aus)", cfg.QuietHours)
}
if cfg.QuietMode != queue.QuietModeDiscard {
t.Errorf("QuietMode = %q, want Default %q", cfg.QuietMode, queue.QuietModeDiscard)
}
})
t.Run("TOML setzt alle Gesetz-Felder", func(t *testing.T) {
cfg, err := Load(Options{
File: writeTOML(t, `
quiet_hours = "18:00-08:00"
quiet_mode = "toast"
[queue]
rate_limit = "10s"
allow_high_burst = false
`),
Getenv: env(nil),
})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Queue.RateLimit != 10*time.Second {
t.Errorf("Queue.RateLimit = %v, want 10s", cfg.Queue.RateLimit)
}
if cfg.Queue.AllowHighBurst {
t.Error("Queue.AllowHighBurst = true, want false aus TOML")
}
if cfg.QuietHours != "18:00-08:00" {
t.Errorf("QuietHours = %q, want Fenster aus TOML", cfg.QuietHours)
}
if cfg.QuietMode != queue.QuietModeToast {
t.Errorf("QuietMode = %q, want toast", cfg.QuietMode)
}
})
t.Run("partielle [queue]-Tabelle behält Defaults", func(t *testing.T) {
cfg, err := Load(Options{
File: writeTOML(t, "[queue]\nrate_limit = \"0s\"\n"),
Getenv: env(nil),
})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Queue.RateLimit != 0 {
t.Errorf("Queue.RateLimit = %v, want 0 (unbegrenzt)", cfg.Queue.RateLimit)
}
if !cfg.Queue.AllowHighBurst {
t.Error("Queue.AllowHighBurst = false, want unveränderten Default true")
}
if cfg.QuietMode != queue.QuietModeDiscard {
t.Errorf("QuietMode = %q, want unveränderten Default discard", cfg.QuietMode)
}
})
t.Run("Env überschreibt Datei (inkl. off)", func(t *testing.T) {
cfg, err := Load(Options{
File: writeTOML(t, `
quiet_hours = "18:00-08:00"
quiet_mode = "discard"
[queue]
rate_limit = "10s"
allow_high_burst = false
`),
Getenv: env(map[string]string{
"TALK_RATE_LIMIT": "1s",
"TALK_ALLOW_HIGH_BURST": "true",
"TALK_QUIET_HOURS": "off",
"TALK_QUIET_MODE": "toast",
}),
})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Queue.RateLimit != time.Second {
t.Errorf("Queue.RateLimit = %v, want 1s aus Env", cfg.Queue.RateLimit)
}
if !cfg.Queue.AllowHighBurst {
t.Error("Queue.AllowHighBurst = false, want true aus Env")
}
if cfg.QuietHours != "" {
t.Errorf("QuietHours = %q, want leer (off aus Env)", cfg.QuietHours)
}
if cfg.QuietMode != queue.QuietModeToast {
t.Errorf("QuietMode = %q, want toast aus Env", cfg.QuietMode)
}
})
t.Run("ungültige Gesetz-Werte sind klare Fehler", func(t *testing.T) {
cases := []struct {
name string
toml string
envs map[string]string
want string
}{
{
name: "TALK_RATE_LIMIT Müll",
envs: map[string]string{"TALK_RATE_LIMIT": "schnell"},
want: "TALK_RATE_LIMIT",
},
{
name: "TALK_RATE_LIMIT negativ",
envs: map[string]string{"TALK_RATE_LIMIT": "-5s"},
want: "TALK_RATE_LIMIT",
},
{
name: "rate_limit Müll in Datei",
toml: "[queue]\nrate_limit = \"müll\"\n",
want: "rate_limit",
},
{
name: "TALK_ALLOW_HIGH_BURST Müll",
envs: map[string]string{"TALK_ALLOW_HIGH_BURST": "vielleicht"},
want: "TALK_ALLOW_HIGH_BURST",
},
{
name: "quiet_hours Müll in Datei",
toml: "quiet_hours = \"abends\"\n",
want: "quiet_hours",
},
{
name: "TALK_QUIET_HOURS Müll",
envs: map[string]string{"TALK_QUIET_HOURS": "25:00-26:00"},
want: "TALK_QUIET_HOURS",
},
{
name: "TALK_QUIET_MODE Müll",
envs: map[string]string{"TALK_QUIET_MODE": "vibrieren"},
want: "TALK_QUIET_MODE",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
file := fehlendeDatei(t)
if tc.toml != "" {
file = writeTOML(t, tc.toml)
}
_, err := Load(Options{File: file, Getenv: env(tc.envs)})
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("Fehler = %v, will klaren Fehler mit %q", err, tc.want)
}
})
}
})
}