feat(queue): Rate-Limit und Ruhezeiten als Gesetz im Worker-Loop
Das Anti-Spam-Gesetz wird an den Loop verwired (die Rate-Limiter-Struktur aus Task 04): Jede gestartete Ansage bucht das Fenster — low über dem Budget verfällt (beim Eintritt, Admission nennt den Grund für die 202-Antwort), normal parkt bis zum Slot (Slot-Wecker broadcastet die Cond, geparkte bleiben in der Queue und in /status sichtbar), high darf mit Burst-Recht verstoßen, now bricht bewusst aus Rate-Limit UND Ruhezeit aus und bucht trotzdem ein neues Fenster. Ruhezeiten (quiet.go: lokales Fenster HH:MM-HH:MM über Mitternacht) verwerfen still oder ersetzen durch Toast — gilt auch für high. Rangordnung: mute (API) > now > Ruhezeit > Rate-Limit. Enqueue liefert Admission statt nur ID; LawState für /status. Lock-Ordnung Queue → RateLimiter bleibt gewahrt.
This commit is contained in:
1 parent
b20e6dd319
commit
5385a4b87d
6 files changed
+785
-81
No files matched your search
@@ -0,0 +1,131 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// lokal baut eine lokale Test-Zeit (Stunde/Minute reichen — Ruhefenster
|
||||
// arbeiten auf Minuten-Auflösung).
|
||||
func lokal(hour, minute int) time.Time {
|
||||
return time.Date(2026, 10, 18, hour, minute, 0, 0, time.Local)
|
||||
}
|
||||
|
||||
// TestParseQuietHours: Format HH:MM-HH:MM (überspannt Mitternacht),
|
||||
// klare Fehler für Müll.
|
||||
func TestParseQuietHours(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
start clockTime
|
||||
end clockTime
|
||||
bad bool
|
||||
}{
|
||||
{in: "18:00-08:00", start: clockTime{18, 0}, end: clockTime{8, 0}},
|
||||
{in: "9:30-17:45", start: clockTime{9, 30}, end: clockTime{17, 45}},
|
||||
{in: "22:00-06:00", start: clockTime{22, 0}, end: clockTime{6, 0}},
|
||||
{in: "08:00-08:00", start: clockTime{8, 0}, end: clockTime{8, 0}}, // deaktiviert, aber gültig
|
||||
{in: ""}, // Leerstring = deaktiviert, kein Fehler
|
||||
{in: "18:00", bad: true},
|
||||
{in: "18:00-", bad: true},
|
||||
{in: "-08:00", bad: true},
|
||||
{in: "24:00-08:00", bad: true},
|
||||
{in: "18:60-08:00", bad: true},
|
||||
{in: "1800-0800", bad: true},
|
||||
{in: "ab:00-08:00", bad: true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
q, err := ParseQuietHours(tc.in)
|
||||
if tc.bad {
|
||||
if err == nil {
|
||||
t.Errorf("ParseQuietHours(%q) = %+v, want Fehler", tc.in, q)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("ParseQuietHours(%q): unerwarteter Fehler %v", tc.in, err)
|
||||
continue
|
||||
}
|
||||
if q.Start != tc.start || q.End != tc.end {
|
||||
t.Errorf("ParseQuietHours(%q) = %v-%v, want %v-%v", tc.in, q.Start, q.End, tc.start, tc.end)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestQuietHoursActive: Tagfenster, Nachtfenster über Mitternacht und der
|
||||
// deaktivierte Nullwert über alle Grenzen (Start inklusiv, End exklusiv).
|
||||
func TestQuietHoursActive(t *testing.T) {
|
||||
cases := []struct {
|
||||
fenster string
|
||||
hour, minute int
|
||||
want bool
|
||||
}{
|
||||
// Tagfenster 09:00-17:00
|
||||
{"09:00-17:00", 8, 59, false},
|
||||
{"09:00-17:00", 9, 0, true}, // Start inklusiv
|
||||
{"09:00-17:00", 12, 30, true},
|
||||
{"09:00-17:00", 16, 59, true},
|
||||
{"09:00-17:00", 17, 0, false}, // End exklusiv
|
||||
{"09:00-17:00", 23, 0, false},
|
||||
// Nachtfenster 18:00-08:00 (überspannt Mitternacht)
|
||||
{"18:00-08:00", 17, 59, false},
|
||||
{"18:00-08:00", 18, 0, true},
|
||||
{"18:00-08:00", 23, 59, true},
|
||||
{"18:00-08:00", 0, 0, true}, // über Mitternacht
|
||||
{"18:00-08:00", 7, 59, true},
|
||||
{"18:00-08:00", 8, 0, false}, // End exklusiv
|
||||
{"18:00-08:00", 12, 0, false},
|
||||
// Zweites Nachtfenster
|
||||
{"22:00-06:00", 3, 0, true},
|
||||
{"22:00-06:00", 21, 59, false},
|
||||
// Start == End: deaktiviert
|
||||
{"08:00-08:00", 8, 0, false},
|
||||
{"08:00-08:00", 20, 0, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
q := mustQuiet(t, tc.fenster)
|
||||
if got := q.Active(lokal(tc.hour, tc.minute)); got != tc.want {
|
||||
t.Errorf("%s um %02d:%02d: Active = %v, want %v", tc.fenster, tc.hour, tc.minute, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestQuietHoursUntil: Fenster-Ende (heute bzw. morgen bei Nachtfenstern),
|
||||
// Nullzeit wenn inaktiv.
|
||||
func TestQuietHoursUntil(t *testing.T) {
|
||||
// Tagfenster: Ende heute.
|
||||
q := mustQuiet(t, "09:00-17:00")
|
||||
if want := lokal(17, 0); !q.Until(lokal(12, 0)).Equal(want) {
|
||||
t.Errorf("Until(12:00) = %v, want %v (heute 17:00)", q.Until(lokal(12, 0)), want)
|
||||
}
|
||||
if !q.Until(lokal(3, 0)).IsZero() {
|
||||
t.Error("Until außerhalb des Fensters: want Nullzeit")
|
||||
}
|
||||
|
||||
// Nachtfenster: ab 18:00 ist das Ende morgen früh.
|
||||
n := mustQuiet(t, "18:00-08:00")
|
||||
if want := lokal(0, 0).Add(32 * time.Hour); !n.Until(lokal(20, 0)).Equal(want) {
|
||||
t.Errorf("Until(20:00) = %v, want %v (morgen 08:00)", n.Until(lokal(20, 0)), want)
|
||||
}
|
||||
// Nach Mitternacht (00:30) ist das Ende heute 08:00.
|
||||
if want := lokal(8, 0); !n.Until(lokal(0, 30)).Equal(want) {
|
||||
t.Errorf("Until(00:30) = %v, want %v (heute 08:00)", n.Until(lokal(0, 30)), want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestQuietHoursStringRoundtrip: String liefert die konfigurierte Form
|
||||
// zurück und parst identisch; der Nullwert rendert leer.
|
||||
func TestQuietHoursStringRoundtrip(t *testing.T) {
|
||||
for _, s := range []string{"18:00-08:00", "09:00-17:00", "22:00-06:00"} {
|
||||
q := mustQuiet(t, s)
|
||||
if q.String() != s {
|
||||
t.Errorf("String() = %q, want %q", q.String(), s)
|
||||
}
|
||||
q2 := mustQuiet(t, q.String())
|
||||
if q2 != q {
|
||||
t.Errorf("Roundtrip von %q: %+v ≠ %+v", s, q2, q)
|
||||
}
|
||||
}
|
||||
if (QuietHours{}).String() != "" {
|
||||
t.Error("Nullwert-String nicht leer")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user