Zwei Löcher im Anti-Spam-Gesetz, die ein Prompt trivial nutzt: Ein Agent
kann 64 KB Text einreichen (ElevenLabs stellt die Rechnung, die Ansage
dauert Minuten), und geparkte normal-Ansagen spielen unbegrenzt spät —
„Build fertig" zweieinhalb Minuten nach dem Build ist keine Nachricht.
- queue.max_text (Default 400 Zeichen, Env TALK_MAX_TEXT): längere Texte
werden beim Eintritt an einer Wortgrenze gekürzt und so gesprochen; die
/speak-Antwort meldet truncated, die CLI warnt auf stderr, das MCP-
Ergebnis sagt es im Text. Runen, nicht Bytes.
- queue.max_age (Default 2m, Env TALK_MAX_AGE): Items, die beim
Fälligwerden länger gewartet haben, werden verworfen (discarded
"expired", Protokoll nennt Wartezeit und Grenze). now:true ist
ausgenommen. Rang: now > veraltet > Ruhezeit > Rate-Limit.
- /status trägt "limits": {max_text, max_age}; `talk status` zeigt sie.
- Negative Werte sind Config-Fehler; 0 = unbegrenzt.
Tests: truncateText-Tabelle (Wortgrenze, Satzzeichen, Runen), Enqueue-
Kürzung, decide-Vorrang inkl. now-Ausnahme, geparktes Item verfällt;
Config-Schichten; Prozessgrenze: Provider bekommt die gekürzte Fassung,
dritte von drei Stau-Ansagen wird nie gesprochen.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
79 lines
2.9 KiB
Go
79 lines
2.9 KiB
Go
package test
|
|
|
|
// Prozessgrenzen-Tests für die Längen- und Alters-Gesetze: queue.max_text
|
|
// kürzt überlange Texte beim Eintritt (Antwort truncated, CLI-Hinweis, der
|
|
// Provider sieht die gekürzte Fassung); queue.max_age verwirft Ansagen, die
|
|
// zu lange geparkt haben (expired, nie gesprochen).
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestMaxTextKuerzt: Ein 60-Wörter-Text gegen max_text=40 — /speak meldet
|
|
// truncated, die CLI warnt auf stderr, der Fake-Provider bekommt höchstens
|
|
// 40 Zeichen, und /status zeigt das Limit.
|
|
func TestMaxTextKuerzt(t *testing.T) {
|
|
root := t.TempDir()
|
|
fake := newFakeProvider(t, 0)
|
|
d := startDaemon(t, root, fake.srv.URL, map[string]string{"TALK_MAX_TEXT": "40"})
|
|
lang := strings.Repeat("viel zu lange Antwort ", 12)
|
|
|
|
code, m := speak(t, d, `{"text":"`+lang+`"}`)
|
|
if code != 202 || m["id"] == "" || m["truncated"] != true {
|
|
t.Fatalf("speak: %d %v, want 202 mit id und truncated=true", code, m)
|
|
}
|
|
hits := fake.waitCompleted(t, 1)
|
|
if n := len([]rune(hits[0].Text)); n > 40 || !strings.HasPrefix(hits[0].Text, "viel zu lange Antwort") {
|
|
t.Errorf("Provider bekam %q (%d Zeichen), want ≤ 40 Zeichen vom Anfang", hits[0].Text, n)
|
|
}
|
|
|
|
env := daemonEnv(root, d.port, fake.srv.URL, nil)
|
|
res := runTalk(t, env, "say", lang)
|
|
if res.code != 0 || !strings.Contains(res.stderr, "gekürzt") {
|
|
t.Errorf("talk say (lang): Exit %d, stderr %q — will Kürzungs-Hinweis", res.code, res.stderr)
|
|
}
|
|
fake.waitCompleted(t, 2)
|
|
|
|
res = runTalk(t, env, "say", "kurz genug")
|
|
if strings.Contains(res.stderr, "gekürzt") {
|
|
t.Errorf("kurzer Text mit Kürzungs-Hinweis: %q", res.stderr)
|
|
}
|
|
fake.waitCompleted(t, 3)
|
|
|
|
res = runTalk(t, env, "status")
|
|
if !strings.Contains(res.stdout, "limits: max. 40 Zeichen") {
|
|
t.Errorf("talk status ohne Limits-Zeile: %q", res.stdout)
|
|
}
|
|
}
|
|
|
|
// TestMaxAgeVerwirftGeparkte: Rate-Limit 1 s, max_age 1,5 s, drei normal-
|
|
// Ansagen direkt hintereinander: die erste spielt sofort, die zweite parkt
|
|
// 1 s (frisch genug), die dritte wäre erst nach 2 s fällig — veraltet,
|
|
// verworfen, nie beim Provider.
|
|
func TestMaxAgeVerwirftGeparkte(t *testing.T) {
|
|
root := t.TempDir()
|
|
fake := newFakeProvider(t, 0)
|
|
d := startDaemon(t, root, fake.srv.URL, map[string]string{
|
|
"TALK_RATE_LIMIT": "1s",
|
|
"TALK_MAX_AGE": "1500ms",
|
|
})
|
|
|
|
for _, text := range []string{"eins", "zwei", "drei"} {
|
|
code, m := speak(t, d, `{"text":"`+text+`"}`)
|
|
if code != 202 || m["id"] == "" {
|
|
t.Fatalf("speak %s: %d %v — beim Eintritt darf nichts verfallen", text, code, m)
|
|
}
|
|
}
|
|
hits := fake.waitCompleted(t, 2)
|
|
if hits[0].Text != "eins" || hits[1].Text != "zwei" {
|
|
t.Errorf("gesprochen = %q, %q; want eins, zwei", hits[0].Text, hits[1].Text)
|
|
}
|
|
fake.assertNoFurtherArrivals(t, 1500*time.Millisecond)
|
|
waitStatusIdle(t, d, 3*time.Second)
|
|
if !strings.Contains(d.stderr.String(), "max_age") {
|
|
t.Errorf("Daemon-Protokoll nennt den Verfall nicht:\n%s", d.stderr.String())
|
|
}
|
|
}
|