test: Prozessgrenzen-Tests für Rate-Limit und Ruhezeiten
Kurzes Fenster (1 s): fünf Ansagen → genau eine spielt sofort, low verfällt beim Eintritt (202 + CLI 'discarded: rate_limit'), normal parkt (sichtbar in queue_ids/next_slot) und spielt frühestens zum Slot (Timing-am-Fake gemessen). High-Burst an/aus; now bricht durch Rate-Limit UND Ruhezeit; Ruhezeit-Fenster künstlich um die lokale Jetztzeit gelegt → discard (auch high) und toast (Protokoll-Zeile, kein Provider-Kontakt); /status- und talk-status-Felder im Default. Harness-Basis: TALK_RATE_LIMIT=0s — die Task-04-Semantik-Tests erwarten dichtes Durchspielen, Gesetz-Tests überschreiben bewusst.
This commit is contained in:
1 parent
b56c8abfae
commit
5979d45710
2 files changed
+357
No files matched your search
@@ -188,6 +188,25 @@ func (f *fakeProvider) waitArrived(t *testing.T, n int) {
|
|||||||
t.Fatalf("nur %d von %d Ankünften am Fake innerhalb 5 s", len(f.snapshot()), n)
|
t.Fatalf("nur %d von %d Ankünften am Fake innerhalb 5 s", len(f.snapshot()), n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// arrivedCount zählt die angekommten Synthese-Requests (auch abgebrochene)
|
||||||
|
// — Beobachtungspunkt für „genau N spielen, der Rest parkt/verfällt".
|
||||||
|
func (f *fakeProvider) arrivedCount() int {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
return len(f.arrived)
|
||||||
|
}
|
||||||
|
|
||||||
|
// assertNoFurtherArrivals prüft, dass innerhalb der Frist kein weiterer
|
||||||
|
// Synthese-Request ankommt (geparkte/verworfene Ansagen bleiben stumm).
|
||||||
|
func (f *fakeProvider) assertNoFurtherArrivals(t *testing.T, wait time.Duration) {
|
||||||
|
t.Helper()
|
||||||
|
n := f.arrivedCount()
|
||||||
|
time.Sleep(wait)
|
||||||
|
if got := f.arrivedCount(); got != n {
|
||||||
|
t.Fatalf("%d zusätzliche Ankünfte (want keine) — Treffer: %+v", got-n, f.snapshot())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// waitHits pollt, bis n Requests abgeschlossen wurden (Abbrüche zählen).
|
// waitHits pollt, bis n Requests abgeschlossen wurden (Abbrüche zählen).
|
||||||
func (f *fakeProvider) waitHits(t *testing.T, n int) []fakeHit {
|
func (f *fakeProvider) waitHits(t *testing.T, n int) []fakeHit {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@@ -296,6 +315,12 @@ func (d *daemonProc) stop(t *testing.T) {
|
|||||||
// damit unter <root>/talk), audio.sink=null und der Fake-Provider als
|
// damit unter <root>/talk), audio.sink=null und der Fake-Provider als
|
||||||
// ElevenLabs-Endpunkt. port=1 heißt „garantiert toter Daemon" (Verbindung
|
// ElevenLabs-Endpunkt. port=1 heißt „garantiert toter Daemon" (Verbindung
|
||||||
// wird sofort abgelehnt).
|
// wird sofort abgelehnt).
|
||||||
|
//
|
||||||
|
// TALK_RATE_LIMIT=0s ist der Basis-Default der Prozessgrenzen-Tests: Die
|
||||||
|
// Task-04-Semantik-Tests (Serialisierung, Priorität, …) enqueueen dicht
|
||||||
|
// hintereinander und erwarten sofortiges Durchspielen — das Gesetz
|
||||||
|
// (Default 30 s) würde sie parken. Die Gesetz-Tests in ratelimit_test.go
|
||||||
|
// überschreiben den Wert bewusst per extra.
|
||||||
func daemonEnv(root string, port int, fakeURL string, extra map[string]string) []string {
|
func daemonEnv(root string, port int, fakeURL string, extra map[string]string) []string {
|
||||||
env := make([]string, 0, 32)
|
env := make([]string, 0, 32)
|
||||||
for _, kv := range os.Environ() {
|
for _, kv := range os.Environ() {
|
||||||
@@ -312,6 +337,7 @@ func daemonEnv(root string, port int, fakeURL string, extra map[string]string) [
|
|||||||
"TALK_ELEVENLABS_ENDPOINT": fakeURL,
|
"TALK_ELEVENLABS_ENDPOINT": fakeURL,
|
||||||
"TALK_EDGE_ENDPOINT": "ws://127.0.0.1:1/", // Kette soll nie auf echte Fremd-Endpunkte fallen
|
"TALK_EDGE_ENDPOINT": "ws://127.0.0.1:1/", // Kette soll nie auf echte Fremd-Endpunkte fallen
|
||||||
"TALK_PORT": strconv.Itoa(port),
|
"TALK_PORT": strconv.Itoa(port),
|
||||||
|
"TALK_RATE_LIMIT": "0s", // Gesetz aus, bis ein Test es anstellt (siehe oben)
|
||||||
}
|
}
|
||||||
for k, v := range extra {
|
for k, v := range extra {
|
||||||
set[k] = v
|
set[k] = v
|
||||||
|
|||||||
@@ -0,0 +1,331 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
// Prozessgrenzen-Tests für das Anti-Spam-Gesetz (Task 05): Rate-Limit
|
||||||
|
// (low verworfen, normal geparkt, high-burst, now-Ausbruch) und Ruhezeiten
|
||||||
|
// (discard/toast) — beobachtet als Black Box über HTTP/CLI, Fake-Provider
|
||||||
|
// und Daemon-Protokoll. Der Harness stellt dafür TALK_RATE_LIMIT=0s als
|
||||||
|
// Basis; diese Tests stellen das Gesetz bewusst per extra an.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fensterUm legt ein Ruhezeit-Fenster um die aktuelle lokale Zeit (±1 Min,
|
||||||
|
// Minute-Auflösung reicht) — so ist das Fenster unabhängig von der Uhrzeit
|
||||||
|
// des Testlaufs sicher aktiv.
|
||||||
|
func fensterUm() string {
|
||||||
|
now := time.Now()
|
||||||
|
return now.Add(-time.Minute).Format("15:04") + "-" + now.Add(time.Minute).Format("15:04")
|
||||||
|
}
|
||||||
|
|
||||||
|
// lawStatus ist die Gesetz-Sicht aus GET /status.
|
||||||
|
type lawStatus struct {
|
||||||
|
RateLimit struct {
|
||||||
|
Interval string `json:"interval"`
|
||||||
|
AllowHighBurst bool `json:"allow_high_burst"`
|
||||||
|
NextSlot *string `json:"next_slot"`
|
||||||
|
} `json:"rate_limit"`
|
||||||
|
QuietHours struct {
|
||||||
|
Window string `json:"window"`
|
||||||
|
Active bool `json:"active"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Until *string `json:"until"`
|
||||||
|
} `json:"quiet_hours"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLawStatus ruft GET /status und dekodiert nur die Gesetz-Felder.
|
||||||
|
func getLawStatus(t *testing.T, d *daemonProc) lawStatus {
|
||||||
|
t.Helper()
|
||||||
|
resp, err := http.Get(d.baseURL + "/status")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GET /status: %v — stderr:\n%s", err, d.stderr.String())
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var st lawStatus
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&st); err != nil {
|
||||||
|
t.Fatalf("GET /status dekodieren: %v", err)
|
||||||
|
}
|
||||||
|
return st
|
||||||
|
}
|
||||||
|
|
||||||
|
// discardedOf liest das discarded-Feld einer /speak-Antwort-Map — fehlt der
|
||||||
|
// Schlüssel (JSON-Decode in map[string]any liefert nil), ist er leer.
|
||||||
|
func discardedOf(m map[string]any) string {
|
||||||
|
if v, ok := m["discarded"].(string); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRateLimitGesetz (Abnahmekriterium): kurzes Rate-Limit (1/s) — von
|
||||||
|
// fünf Ansagen spielt genau die erste sofort; low verfällt noch beim
|
||||||
|
// Eintritt (202 nennt den Grund), normal parkt und spielt nach dem Fenster.
|
||||||
|
func TestRateLimitGesetz(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
fake := newFakeProvider(t, 100*time.Millisecond)
|
||||||
|
d := startDaemon(t, root, fake.srv.URL, map[string]string{"TALK_RATE_LIMIT": "1s"})
|
||||||
|
env := daemonEnv(root, d.port, fake.srv.URL, map[string]string{"TALK_RATE_LIMIT": "1s"})
|
||||||
|
|
||||||
|
code, erste := speak(t, d, `{"text":"erste ansage"}`)
|
||||||
|
if code != 202 || erste["id"] == "" {
|
||||||
|
t.Fatalf("erste: Status=%d Body=%v, want 202 mit id", code, erste)
|
||||||
|
}
|
||||||
|
fake.waitArrived(t, 1) // erste läuft — das Fenster (1 s) ist gebucht.
|
||||||
|
|
||||||
|
// CLI-Beobachtung: talk say --priority low meldet den Verwurf plain.
|
||||||
|
res := runTalk(t, env, "say", "--priority", "low", "spam eins")
|
||||||
|
if res.code != 0 {
|
||||||
|
t.Fatalf("talk say --priority low: Exit %d, stderr: %s", res.code, res.stderr)
|
||||||
|
}
|
||||||
|
if res.stdout != "discarded: rate_limit\n" {
|
||||||
|
t.Errorf("stdout = %q, want %q", res.stdout, "discarded: rate_limit\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zwei weitere low-Ansagen verfallen ebenso beim Eintritt.
|
||||||
|
for _, text := range []string{"spam zwei", "spam drei"} {
|
||||||
|
code, m := speak(t, d, `{"text":"`+text+`","priority":"low"}`)
|
||||||
|
if code != 202 || m["id"] != "" || m["discarded"] != "rate_limit" {
|
||||||
|
t.Fatalf("low %s: Status=%d Body=%v, want 202 mit leerer id und discarded=rate_limit", text, code, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal wird aufgenommen …
|
||||||
|
code, geparkte := speak(t, d, `{"text":"geparkte ansage"}`)
|
||||||
|
if code != 202 || geparkte["id"] == "" || discardedOf(geparkte) != "" {
|
||||||
|
t.Fatalf("normal: Status=%d Body=%v, want 202 mit id", code, geparkte)
|
||||||
|
}
|
||||||
|
|
||||||
|
// … und sichtbar geparkt: Queue zeigt sie, der nächste Slot liegt in
|
||||||
|
// der Zukunft.
|
||||||
|
st := getStatus(t, d)
|
||||||
|
if st.Queue != 1 || len(st.QueueIDs) != 1 || st.QueueIDs[0] != geparkte["id"] {
|
||||||
|
t.Fatalf("queue = %v (%d), will die geparkte Ansage", st.QueueIDs, st.Queue)
|
||||||
|
}
|
||||||
|
law := getLawStatus(t, d)
|
||||||
|
if law.RateLimit.Interval != "1s" || !law.RateLimit.AllowHighBurst {
|
||||||
|
t.Errorf("rate_limit = %+v, want Interval 1s mit Burst-Recht", law.RateLimit)
|
||||||
|
}
|
||||||
|
if law.RateLimit.NextSlot == nil {
|
||||||
|
t.Errorf("next_slot = null während geparkter Ansage, want Termin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Genau eine ist bislang angekommen: die geparkte spielt NICHT vor
|
||||||
|
// dem Fenster-Ablauf (Abstand zum Fenster-Rand reichlich).
|
||||||
|
fake.assertNoFurtherArrivals(t, 400*time.Millisecond)
|
||||||
|
|
||||||
|
// Nach dem Fenster spielt die geparkte — und zwar frühestens zum Slot.
|
||||||
|
hits := fake.waitCompleted(t, 2)
|
||||||
|
if hits[1].Text != "geparkte ansage" {
|
||||||
|
t.Errorf("zweiter Treffer = %q, will die geparkte Ansage", hits[1].Text)
|
||||||
|
}
|
||||||
|
if gap := hits[1].Start.Sub(hits[0].Start); gap < 850*time.Millisecond {
|
||||||
|
t.Errorf("zweiter Start nach %v, want >= Fenster (1 s, Toleranz 150 ms)", gap)
|
||||||
|
}
|
||||||
|
waitStatusIdle(t, d, 3*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHighBurstProzess: mit Burst-Recht (Default) startet high trotz
|
||||||
|
// vollem Fensters sofort nach der laufenden Ansage; ohne Recht parkt high
|
||||||
|
// wie normal. (Das spätere Abspielen nach langem Fenster deckt der
|
||||||
|
// Park-Test oben mit kurzem Fenster.)
|
||||||
|
func TestHighBurstProzess(t *testing.T) {
|
||||||
|
t.Run("burst an (Default)", func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
fake := newFakeProvider(t, 50*time.Millisecond)
|
||||||
|
d := startDaemon(t, root, fake.srv.URL, map[string]string{"TALK_RATE_LIMIT": "8s"})
|
||||||
|
|
||||||
|
speak(t, d, `{"text":"erste"}`)
|
||||||
|
fake.waitArrived(t, 1) // Fenster für 8 s gebucht.
|
||||||
|
|
||||||
|
code, m := speak(t, d, `{"text":"dringend","priority":"high"}`)
|
||||||
|
if code != 202 || m["id"] == "" {
|
||||||
|
t.Fatalf("high: Status=%d Body=%v, want 202 mit id", code, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// high startet, sobald die erste fertig ist — bewusst innerhalb
|
||||||
|
// des Fensters (Verstoß mit Recht).
|
||||||
|
hits := fake.waitCompleted(t, 2)
|
||||||
|
if hits[1].Text != "dringend" {
|
||||||
|
t.Errorf("zweiter Treffer = %q, will dringend", hits[1].Text)
|
||||||
|
}
|
||||||
|
if gap := hits[1].Start.Sub(hits[0].Start); gap >= 4*time.Second {
|
||||||
|
t.Errorf("high startete nach %v — Burst greift nicht (Fenster 8 s)", gap)
|
||||||
|
}
|
||||||
|
waitStatusIdle(t, d, 3*time.Second)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("burst aus", func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
fake := newFakeProvider(t, 0)
|
||||||
|
d := startDaemon(t, root, fake.srv.URL, map[string]string{
|
||||||
|
"TALK_RATE_LIMIT": "8s",
|
||||||
|
"TALK_ALLOW_HIGH_BURST": "false",
|
||||||
|
})
|
||||||
|
|
||||||
|
speak(t, d, `{"text":"erste"}`)
|
||||||
|
fake.waitArrived(t, 1)
|
||||||
|
|
||||||
|
code, m := speak(t, d, `{"text":"dringend","priority":"high"}`)
|
||||||
|
if code != 202 || m["id"] == "" {
|
||||||
|
t.Fatalf("high: Status=%d Body=%v, want 202 mit id (parkt)", code, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// high bleibt geparkt: keine zweite Ankunft, Queue zeigt es.
|
||||||
|
fake.assertNoFurtherArrivals(t, 700*time.Millisecond)
|
||||||
|
st := getStatus(t, d)
|
||||||
|
if st.Queue != 1 || len(st.QueueIDs) != 1 || st.QueueIDs[0] != m["id"] {
|
||||||
|
t.Fatalf("queue = %v (%d), will geparktes high", st.QueueIDs, st.Queue)
|
||||||
|
}
|
||||||
|
if law := getLawStatus(t, d); law.RateLimit.AllowHighBurst {
|
||||||
|
t.Error("allow_high_burst = true, want false")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aufräumen über cancel — und der Daemon bleibt gesund (now
|
||||||
|
// umgeht das weiter laufende 8-s-Fenster).
|
||||||
|
code, cm := postPath(t, d, "/cancel")
|
||||||
|
if code != 200 || cm["cancelled"] != float64(1) {
|
||||||
|
t.Fatalf("cancel: %d %v, want 1 geparkte Ansage", code, cm)
|
||||||
|
}
|
||||||
|
speak(t, d, `{"text":"danach","now":true}`)
|
||||||
|
hits := fake.waitCompleted(t, 2)
|
||||||
|
if hits[1].Text != "danach" {
|
||||||
|
t.Errorf("Treffer nach cancel = %q, want danach", hits[1].Text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNowBypassesRateUndRuhe: now ist der dokumentierte Ausbruch — es
|
||||||
|
// spielt trotz vollem Rate-Fensters UND aktiver Ruhezeit.
|
||||||
|
func TestNowBypassesRateUndRuhe(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
fake := newFakeProvider(t, 0)
|
||||||
|
d := startDaemon(t, root, fake.srv.URL, map[string]string{
|
||||||
|
"TALK_RATE_LIMIT": "30s",
|
||||||
|
"TALK_QUIET_HOURS": fensterUm(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Normal verfällt in der Ruhezeit (hier: still).
|
||||||
|
code, m := speak(t, d, `{"text":"ruhig bitte"}`)
|
||||||
|
if code != 202 || m["id"] != "" || m["discarded"] != "quiet_hours" {
|
||||||
|
t.Fatalf("normal in Ruhezeit: Status=%d Body=%v, want 202 mit leerer id und discarded=quiet_hours", code, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// now bricht durch: Aufnahme mit ID, Synthese trotz beider Gesetze.
|
||||||
|
code, m = speak(t, d, `{"text":"notfall","now":true}`)
|
||||||
|
if code != 202 || m["id"] == "" || discardedOf(m) != "" {
|
||||||
|
t.Fatalf("now: Status=%d Body=%v, want 202 mit id", code, m)
|
||||||
|
}
|
||||||
|
hits := fake.waitCompleted(t, 1)
|
||||||
|
if hits[0].Text != "notfall" {
|
||||||
|
t.Errorf("Treffer = %q, will notfall", hits[0].Text)
|
||||||
|
}
|
||||||
|
fake.assertNoFurtherArrivals(t, 300*time.Millisecond)
|
||||||
|
|
||||||
|
if law := getLawStatus(t, d); !law.QuietHours.Active {
|
||||||
|
t.Errorf("quiet_hours.active = false, want true (Fenster um jetzt): %+v", law.QuietHours)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRuhezeitDiscard: aktives Fenster verwirft normal UND high still
|
||||||
|
// (Burst ist ein Rate-Recht, kein Ruhe-Recht) — ohne Provider-Kontakt,
|
||||||
|
// mit Verwurf-Zeile im Daemon-Protokoll und Fenster-Ende in /status.
|
||||||
|
func TestRuhezeitDiscard(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
fake := newFakeProvider(t, 0)
|
||||||
|
fenster := fensterUm()
|
||||||
|
d := startDaemon(t, root, fake.srv.URL, map[string]string{"TALK_QUIET_HOURS": fenster})
|
||||||
|
|
||||||
|
for _, body := range []string{
|
||||||
|
`{"text":"abends eins"}`,
|
||||||
|
`{"text":"abends dringend","priority":"high"}`,
|
||||||
|
} {
|
||||||
|
code, m := speak(t, d, body)
|
||||||
|
if code != 202 || m["id"] != "" || m["discarded"] != "quiet_hours" || m["toast"] != nil {
|
||||||
|
t.Fatalf("%s: Status=%d Body=%v, want 202 mit leerer id und discarded=quiet_hours", body, code, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fake.assertNoHits(t, 400*time.Millisecond)
|
||||||
|
|
||||||
|
log := d.stderr.String()
|
||||||
|
if !strings.Contains(log, "ruhezeit") || !strings.Contains(log, "verworfen") {
|
||||||
|
t.Errorf("Daemon-Log will Verwurf-Zeile mit ruhezeit, hat:\n%s", log)
|
||||||
|
}
|
||||||
|
|
||||||
|
law := getLawStatus(t, d)
|
||||||
|
if law.QuietHours.Window != fenster || !law.QuietHours.Active || law.QuietHours.Mode != "discard" {
|
||||||
|
t.Errorf("quiet_hours = %+v, want Fenster %q aktiv im Modus discard", law.QuietHours, fenster)
|
||||||
|
}
|
||||||
|
if law.QuietHours.Until == nil {
|
||||||
|
t.Error("quiet_hours.until = null im aktiven Fenster, want Termin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRuhezeitToast: quiet_mode=toast ersetzt Audio durch eine
|
||||||
|
// Benachrichtigung — derzeit eine Daemon-Protokoll-Zeile über den
|
||||||
|
// Notification-Pfad (dokumentierte Einschränkung bis zur OS-Anbindung).
|
||||||
|
func TestRuhezeitToast(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
fake := newFakeProvider(t, 0)
|
||||||
|
d := startDaemon(t, root, fake.srv.URL, map[string]string{
|
||||||
|
"TALK_QUIET_HOURS": fensterUm(),
|
||||||
|
"TALK_QUIET_MODE": "toast",
|
||||||
|
})
|
||||||
|
|
||||||
|
code, m := speak(t, d, `{"text":"abend kurz"}`)
|
||||||
|
if code != 202 || m["id"] != "" || m["discarded"] != "quiet_hours" || m["toast"] != true {
|
||||||
|
t.Fatalf("toast: Status=%d Body=%v, want 202 mit leerer id, discarded=quiet_hours und toast=true", code, m)
|
||||||
|
}
|
||||||
|
fake.assertNoHits(t, 300*time.Millisecond)
|
||||||
|
|
||||||
|
if log := d.stderr.String(); !strings.Contains(log, "toast") {
|
||||||
|
t.Errorf("Daemon-Log will Toast-Zeile (Notification-Pfad), hat:\n%s", log)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStatusGesetzeFelder (Abnahmekriterium): /status und `talk status`
|
||||||
|
// zeigen Rate-Limit- und Ruhezeit-State — Defaults im Leerlauf, gebuchter
|
||||||
|
// Slot nach einer Ansage.
|
||||||
|
func TestStatusGesetzeFelder(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
fake := newFakeProvider(t, 0)
|
||||||
|
d := startDaemon(t, root, fake.srv.URL, map[string]string{"TALK_RATE_LIMIT": "30s"})
|
||||||
|
env := daemonEnv(root, d.port, fake.srv.URL, map[string]string{"TALK_RATE_LIMIT": "30s"})
|
||||||
|
|
||||||
|
law := getLawStatus(t, d)
|
||||||
|
if law.RateLimit.Interval != "30s" || !law.RateLimit.AllowHighBurst || law.RateLimit.NextSlot != nil {
|
||||||
|
t.Errorf("rate_limit = %+v, want Default 30s, Burst an, Slot frei", law.RateLimit)
|
||||||
|
}
|
||||||
|
if law.QuietHours.Window != "" || law.QuietHours.Active || law.QuietHours.Mode != "discard" || law.QuietHours.Until != nil {
|
||||||
|
t.Errorf("quiet_hours = %+v, want deaktiviert im Modus discard", law.QuietHours)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := runTalk(t, env, "status")
|
||||||
|
if res.code != 0 {
|
||||||
|
t.Fatalf("talk status: Exit %d — stderr: %s", res.code, res.stderr)
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
"rate-limit: 30s",
|
||||||
|
"high-burst erlaubt",
|
||||||
|
"ruhezeit: aus",
|
||||||
|
"modus discard",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(res.stdout, want) {
|
||||||
|
t.Errorf("talk status stdout = %q, will %q", res.stdout, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nach einer Ansage steht der nächste Slot: HTTP-Feld + CLI-Zeile.
|
||||||
|
speak(t, d, `{"text":"einmal reicht"}`)
|
||||||
|
fake.waitArrived(t, 1)
|
||||||
|
if law := getLawStatus(t, d); law.RateLimit.NextSlot == nil {
|
||||||
|
t.Error("next_slot = null nach abgespielter Ansage, want Termin")
|
||||||
|
}
|
||||||
|
res = runTalk(t, env, "status")
|
||||||
|
if res.code != 0 || !strings.Contains(res.stdout, "nächster slot") {
|
||||||
|
t.Errorf("talk status nach Ansage = %q (Exit %d), will Slot-Zeile", res.stdout, res.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user