feat(queue): Gesetze max_text und max_age — gekürzte Langtexte, verfallene Stau-Ansagen
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>
This commit is contained in:
1 parent
79314dfb0f
commit
97650ad0ec
13 files changed
+430
-26
No files matched your search
@@ -0,0 +1,114 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestTruncateText: Kürzung auf max Runen an einer Wortgrenze (nicht in der
|
||||
// ersten Hälfte), ohne hängende Satzzeichen; kurze Texte und max 0 bleiben
|
||||
// unangetastet; Runen zählen, nicht Bytes.
|
||||
func TestTruncateText(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in string
|
||||
max int
|
||||
want string
|
||||
truncated bool
|
||||
}{
|
||||
{"kurz bleibt", "Build fertig", 400, "Build fertig", false},
|
||||
{"genau max bleibt", "abcde", 5, "abcde", false},
|
||||
{"unbegrenzt", strings.Repeat("x", 1000), 0, strings.Repeat("x", 1000), false},
|
||||
{"wortgrenze", "Der Build ist fertig und bereit", 14, "Der Build ist", true},
|
||||
{"satzzeichen weg", "Fertig, aber dann kam noch mehr", 8, "Fertig", true},
|
||||
{"keine wortgrenze in zweiter hälfte → hart", "abcdefghijklmnop qrs", 10, "abcdefghij", true},
|
||||
{"umlaute als eine rune", "Übergröße ärgert", 9, "Übergröße", true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got, tr := truncateText(c.in, c.max)
|
||||
if got != c.want || tr != c.truncated {
|
||||
t.Errorf("truncateText(%q, %d) = %q/%v, want %q/%v", c.in, c.max, got, tr, c.want, c.truncated)
|
||||
}
|
||||
if c.max > 0 && len([]rune(got)) > c.max {
|
||||
t.Errorf("Ergebnis %q länger als %d Runen", got, c.max)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnqueueKuerztText: Das Gesetz MaxText greift beim Eintritt — die
|
||||
// gekürzte Fassung wird gesprochen, Admission meldet Truncated.
|
||||
func TestEnqueueKuerztText(t *testing.T) {
|
||||
g := neueSpielerin()
|
||||
q := New(Laws{MaxText: 20}, g.speak, nil)
|
||||
defer q.Close()
|
||||
|
||||
adm, err := q.Enqueue(Item{Text: "Dieser Text ist deutlich länger als zwanzig Zeichen"})
|
||||
if err != nil || adm.ID == "" || !adm.Truncated {
|
||||
t.Fatalf("Enqueue = %+v, %v; want aufgenommen + Truncated", adm, err)
|
||||
}
|
||||
items := g.warteBis(t, 1)
|
||||
if items[0].Text != "Dieser Text ist" {
|
||||
t.Errorf("gesprochen = %q, want gekürzte Fassung", items[0].Text)
|
||||
}
|
||||
g.step <- struct{}{}
|
||||
|
||||
adm, _ = q.Enqueue(Item{Text: "kurz"})
|
||||
if adm.Truncated {
|
||||
t.Error("kurzer Text als gekürzt gemeldet")
|
||||
}
|
||||
g.warteBis(t, 2)
|
||||
g.step <- struct{}{}
|
||||
}
|
||||
|
||||
// TestDecideExpired: Ein Item, das länger als MaxAge gewartet hat, wird
|
||||
// verworfen — vor Ruhezeit und Rate-Limit, aber nach now (now bricht aus).
|
||||
func TestDecideExpired(t *testing.T) {
|
||||
laws := Laws{MaxAge: time.Minute, RateLimit: time.Hour}
|
||||
now := time.Now()
|
||||
alt := Item{Text: "alt", Priority: PriorityNormal, enqueued: now.Add(-2 * time.Minute)}
|
||||
frisch := Item{Text: "frisch", Priority: PriorityNormal, enqueued: now.Add(-10 * time.Second)}
|
||||
|
||||
if v := laws.decide(alt, now, true); v != verdictDiscardExpired {
|
||||
t.Errorf("altes Item bei freiem Slot = %v, want verdictDiscardExpired", v)
|
||||
}
|
||||
if v := laws.decide(frisch, now, true); v != verdictPlay {
|
||||
t.Errorf("frisches Item bei freiem Slot = %v, want verdictPlay", v)
|
||||
}
|
||||
if v := laws.decide(frisch, now, false); v != verdictWait {
|
||||
t.Errorf("frisches Item ohne Slot = %v, want verdictWait", v)
|
||||
}
|
||||
altNow := alt
|
||||
altNow.Now = true
|
||||
if v := laws.decide(altNow, now, false); v != verdictPlay {
|
||||
t.Errorf("altes now-Item = %v, want verdictPlay (now bricht aus)", v)
|
||||
}
|
||||
if v := (Laws{}).decide(alt, now, true); v != verdictPlay {
|
||||
t.Errorf("MaxAge 0 (unbegrenzt) verwirft altes Item: %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGeparktesItemVerfaellt: Ein normal-Item parkt auf den Rate-Limit-Slot;
|
||||
// ist es beim Fälligwerden älter als MaxAge, wird es verworfen (Protokoll
|
||||
// nennt max_age) und nicht mehr gesprochen.
|
||||
func TestGeparktesItemVerfaellt(t *testing.T) {
|
||||
logb := &syncLog{}
|
||||
g := neueSpielerin()
|
||||
q := New(Laws{RateLimit: 300 * time.Millisecond, MaxAge: 150 * time.Millisecond}, g.speak, logb)
|
||||
defer q.Close()
|
||||
|
||||
q.Enqueue(Item{Text: "erste"}) // bucht das Fenster
|
||||
g.warteBis(t, 1)
|
||||
g.step <- struct{}{}
|
||||
adm, _ := q.Enqueue(Item{Text: "parkt zu lange"}) // parkt ~300 ms > MaxAge
|
||||
if adm.ID == "" || adm.Discarded != "" {
|
||||
t.Fatalf("beim Eintritt darf nichts verfallen: %+v", adm)
|
||||
}
|
||||
waitFor(t, func() bool { return strings.Contains(logb.String(), "max_age") })
|
||||
g.keineWeitere(t)
|
||||
if !strings.Contains(logb.String(), adm.ID) {
|
||||
t.Errorf("Protokoll nennt die verfallene Ansage nicht:\n%s", logb.String())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user