talk is going to be published. Code comments, CLI output, error messages,
daemon log lines, MCP descriptions and results, test names and assertions,
README, spec, skill, build scripts and flake are now English. Only the
spoken announcement texts stay bilingual: the wrap result and the test
sentence are German or English depending on the detected language.
Language fallback is now configurable: `lang = "en" | "de"` (env
TALK_LANG, default en) is used when detection cannot tell (numbers, single
words, ties) and for talk's own announcements. langdetect.Detect takes the
fallback as an argument instead of hard-coding German. `talk setup` asks for
it; `talk doctor` and `talk status` show it.
Vocabulary pinned across packages so the process-boundary tests observe
stable strings: chain log lines ("chain: tier X skipped (not available)",
"falling back to the next tier", "spoken via X"), queue lines ("discarded:
quiet hours ... active", "replaced by toast", "max_age"), CLI notes
("speaking directly", "daemon started at", "no daemon", "nothing to cancel"),
status block with aligned English labels, MCP messages, origin guard 403.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
79 lines
2.9 KiB
Go
79 lines
2.9 KiB
Go
package test
|
|
|
|
// Process-boundary tests for the length and age laws: queue.max_text
|
|
// truncates overlong texts on admission (response truncated, CLI note, the
|
|
// provider sees the truncated version); queue.max_age discards
|
|
// announcements that parked too long (expired, never spoken).
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestMaxTextTruncates: a 60-word text against max_text=40 — /speak reports
|
|
// truncated, the CLI warns on stderr, the fake provider receives at most 40
|
|
// characters, and /status shows the limit.
|
|
func TestMaxTextTruncates(t *testing.T) {
|
|
root := t.TempDir()
|
|
fake := newFakeProvider(t, 0)
|
|
d := startDaemon(t, root, fake.srv.URL, map[string]string{"TALK_MAX_TEXT": "40"})
|
|
long := strings.Repeat("far too long an answer ", 12)
|
|
|
|
code, m := speak(t, d, `{"text":"`+long+`"}`)
|
|
if code != 202 || m["id"] == "" || m["truncated"] != true {
|
|
t.Fatalf("speak: %d %v, want 202 with id and truncated=true", code, m)
|
|
}
|
|
hits := fake.waitCompleted(t, 1)
|
|
if n := len([]rune(hits[0].Text)); n > 40 || !strings.HasPrefix(hits[0].Text, "far too long an answer") {
|
|
t.Errorf("provider received %q (%d characters), want ≤ 40 characters from the start", hits[0].Text, n)
|
|
}
|
|
|
|
env := daemonEnv(root, d.port, fake.srv.URL, nil)
|
|
res := runTalk(t, env, "say", long)
|
|
if res.code != 0 || !strings.Contains(res.stderr, "truncated") {
|
|
t.Errorf("talk say (long): exit %d, stderr %q — want the truncation note", res.code, res.stderr)
|
|
}
|
|
fake.waitCompleted(t, 2)
|
|
|
|
res = runTalk(t, env, "say", "short enough")
|
|
if strings.Contains(res.stderr, "truncated") {
|
|
t.Errorf("short text with truncation note: %q", res.stderr)
|
|
}
|
|
fake.waitCompleted(t, 3)
|
|
|
|
res = runTalk(t, env, "status")
|
|
if !strings.Contains(res.stdout, "limits: max 40 characters") {
|
|
t.Errorf("talk status without limits line: %q", res.stdout)
|
|
}
|
|
}
|
|
|
|
// TestMaxAgeDiscardsParked: rate limit 1 s, max_age 1.5 s, three normal
|
|
// announcements right after one another: the first plays immediately, the
|
|
// second parks 1 s (fresh enough), the third would be due only after 2 s —
|
|
// stale, discarded, never at the provider.
|
|
func TestMaxAgeDiscardsParked(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{"one", "two", "three"} {
|
|
code, m := speak(t, d, `{"text":"`+text+`"}`)
|
|
if code != 202 || m["id"] == "" {
|
|
t.Fatalf("speak %s: %d %v — nothing may expire on admission", text, code, m)
|
|
}
|
|
}
|
|
hits := fake.waitCompleted(t, 2)
|
|
if hits[0].Text != "one" || hits[1].Text != "two" {
|
|
t.Errorf("spoken = %q, %q; want one, two", 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 log does not mention the expiry:\n%s", d.stderr.String())
|
|
}
|
|
}
|