refactor: switch the whole project to English
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>
This commit is contained in:
1 parent
4d30bd95ad
commit
85fa64cfe6
95 files changed
+4707
-4854
No files matched your search
@@ -10,9 +10,9 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakeBin schreibt ein ausführbares Shell-Skript namens name in dir, das
|
||||
// body ausführt. Damit werden die externen Prozesse des Providers an der
|
||||
// echten Prozessgrenze gefaked (PATH-Manipulation, kein Mock-Framework).
|
||||
// fakeBin writes an executable shell script called name into dir that runs
|
||||
// body. This fakes the provider's external processes at the real process
|
||||
// boundary (PATH manipulation, no mock framework).
|
||||
func fakeBin(t *testing.T, dir, name, body string) {
|
||||
t.Helper()
|
||||
script := "#!/bin/sh\n" + body + "\n"
|
||||
@@ -22,8 +22,8 @@ func fakeBin(t *testing.T, dir, name, body string) {
|
||||
}
|
||||
}
|
||||
|
||||
// emptyPATH setzt PATH auf ein Verzeichnis ohne einzige Binärdatei —
|
||||
// kein espeak, kein spd-say.
|
||||
// emptyPATH sets PATH to a directory without a single binary: no espeak,
|
||||
// no spd-say.
|
||||
func emptyPATH(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
@@ -31,15 +31,15 @@ func emptyPATH(t *testing.T) string {
|
||||
return dir
|
||||
}
|
||||
|
||||
// prependPATH stellt dir vor den bestehenden PATH: Binaries darin
|
||||
// überschatten System-Binaries gleichen Namens (fake espeak vor einem
|
||||
// echten), während Hilfsprogramme wie cat weiterhin erreichbar bleiben.
|
||||
// prependPATH puts dir in front of the existing PATH: binaries in it shadow
|
||||
// system binaries of the same name (fake espeak before a real one) while
|
||||
// helpers such as cat stay reachable.
|
||||
func prependPATH(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
}
|
||||
|
||||
// TestOSBuiltinName und TestNewRegistry pinnen die Registry-Sicht.
|
||||
// TestOSBuiltinName and TestNewRegistry pin the registry view.
|
||||
func TestOSBuiltinName(t *testing.T) {
|
||||
if got := (OSBuiltin{}).Name(); got != "osbuiltin" {
|
||||
t.Errorf("Name() = %q, want %q", got, "osbuiltin")
|
||||
@@ -56,7 +56,7 @@ func TestNewRegistry(t *testing.T) {
|
||||
t.Errorf("New(%q).Name() = %q", name, p.Name())
|
||||
}
|
||||
}
|
||||
// Task 03: die Cloud-Provider stehen in der Registry.
|
||||
// The cloud providers are in the registry.
|
||||
for _, name := range []string{"elevenlabs", "openai", "edge"} {
|
||||
p, err := New(name, ChainConfig{})
|
||||
if err != nil {
|
||||
@@ -66,38 +66,38 @@ func TestNewRegistry(t *testing.T) {
|
||||
t.Errorf("New(%q).Name() = %q", name, p.Name())
|
||||
}
|
||||
}
|
||||
if _, err := New("gibtsnicht", ChainConfig{}); err == nil {
|
||||
t.Error("New(\"gibtsnicht\") soll fehlschlagen")
|
||||
if _, err := New("doesnotexist", ChainConfig{}); err == nil {
|
||||
t.Error("New(\"doesnotexist\") should fail")
|
||||
} else if !strings.Contains(err.Error(), "osbuiltin") || !strings.Contains(err.Error(), "elevenlabs") {
|
||||
t.Errorf("New-Fehler soll verfügbare Provider nennen: %v", err)
|
||||
t.Errorf("New error should name the available providers: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAvailableOhneTTS: leere PATH-Umgebung → Available false.
|
||||
func TestAvailableOhneTTS(t *testing.T) {
|
||||
// TestAvailableWithoutTTS: empty PATH environment → Available false.
|
||||
func TestAvailableWithoutTTS(t *testing.T) {
|
||||
emptyPATH(t)
|
||||
if (OSBuiltin{}).Available(context.Background()) {
|
||||
t.Error("Available() = true ohne jedes TTS-Binary, want false")
|
||||
t.Error("Available() = true without any TTS binary, want false")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSynthesizeOhneTTS: fehlt byte-fähiges TTS, muss der Fehler
|
||||
// ErrNoByteOutput sein (kein Panik, kein anderer Fehlertyp).
|
||||
func TestSynthesizeOhneTTS(t *testing.T) {
|
||||
// TestSynthesizeWithoutTTS: if byte-capable TTS is missing, the error must
|
||||
// be ErrNoByteOutput (no panic, no other error type).
|
||||
func TestSynthesizeWithoutTTS(t *testing.T) {
|
||||
emptyPATH(t)
|
||||
_, err := (OSBuiltin{}).Synthesize(context.Background(), "Hallo", "de")
|
||||
_, err := (OSBuiltin{}).Synthesize(context.Background(), "Hello", "de")
|
||||
if err == nil {
|
||||
t.Fatal("Synthesize ohne TTS: kein Fehler")
|
||||
t.Fatal("Synthesize without TTS: no error")
|
||||
}
|
||||
if !errors.Is(err, ErrNoByteOutput) {
|
||||
t.Fatalf("Synthesize-Fehler = %v, want ErrNoByteOutput", err)
|
||||
t.Fatalf("Synthesize error = %v, want ErrNoByteOutput", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSynthesizeMitFakeEspeak: ein gefaktes espeak liefert WAV-Bytes auf
|
||||
// stdout; Synthesize muss genau diese Bytes durchreichen und die Stimme
|
||||
// (-v <lang>) sowie den Text übergeben bekommen haben.
|
||||
func TestSynthesizeMitFakeEspeak(t *testing.T) {
|
||||
// TestSynthesizeWithFakeEspeak: a faked espeak returns WAV bytes on stdout;
|
||||
// Synthesize must pass exactly these bytes through and must have received
|
||||
// the voice (-v <lang>) and the text.
|
||||
func TestSynthesizeWithFakeEspeak(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
wav := filepath.Join(dir, "probe.wav")
|
||||
if err := os.WriteFile(wav, minimalWAV, 0o644); err != nil {
|
||||
@@ -108,95 +108,95 @@ func TestSynthesizeMitFakeEspeak(t *testing.T) {
|
||||
cat "`+wav+`"`)
|
||||
prependPATH(t, dir)
|
||||
|
||||
data, err := (OSBuiltin{}).Synthesize(context.Background(), "Hallo Welt", "de")
|
||||
data, err := (OSBuiltin{}).Synthesize(context.Background(), "Hello world", "de")
|
||||
if err != nil {
|
||||
t.Fatalf("Synthesize: %v", err)
|
||||
}
|
||||
if string(data) != string(minimalWAV) {
|
||||
t.Errorf("Synthesize lieferte %d Bytes, want die WAV-Bytes des Fake-espeak (%d)", len(data), len(minimalWAV))
|
||||
t.Errorf("Synthesize returned %d bytes, want the WAV bytes of the fake espeak (%d)", len(data), len(minimalWAV))
|
||||
}
|
||||
args, err := os.ReadFile(seen)
|
||||
if err != nil {
|
||||
t.Fatalf("Fake-espeak wurde nicht aufgerufen: %v", err)
|
||||
t.Fatalf("fake espeak was not called: %v", err)
|
||||
}
|
||||
if got := string(args); got != "-v de --stdout Hallo Welt\n" {
|
||||
t.Errorf("Fake-espeak ARGS = %q, want %q", got, "-v de --stdout Hallo Welt\n")
|
||||
if got := string(args); got != "-v de --stdout Hello world\n" {
|
||||
t.Errorf("fake espeak ARGS = %q, want %q", got, "-v de --stdout Hello world\n")
|
||||
}
|
||||
if !(OSBuiltin{}).Available(context.Background()) {
|
||||
t.Error("Available() = false trotz espeak im PATH")
|
||||
t.Error("Available() = false despite espeak in PATH")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSynthesizeLeererText ist ein klarer Fehler.
|
||||
func TestSynthesizeLeererText(t *testing.T) {
|
||||
// TestSynthesizeEmptyText is a clear error.
|
||||
func TestSynthesizeEmptyText(t *testing.T) {
|
||||
if _, err := (OSBuiltin{}).Synthesize(context.Background(), "", "de"); err == nil {
|
||||
t.Error("Synthesize(\"\") soll fehlschlagen")
|
||||
t.Error("Synthesize(\"\") should fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSynthesizeFehlgeschlagenesEspeak: espeak crasht (exit 1) → klarer
|
||||
// Fehler, kein ErrNoByteOutput.
|
||||
func TestSynthesizeFehlgeschlagenesEspeak(t *testing.T) {
|
||||
// TestSynthesizeFailingEspeak: espeak crashes (exit 1) → clear error, not
|
||||
// ErrNoByteOutput.
|
||||
func TestSynthesizeFailingEspeak(t *testing.T) {
|
||||
dir := emptyPATH(t)
|
||||
fakeBin(t, dir, "espeak", `echo "boom" >&2; exit 1`)
|
||||
_, err := (OSBuiltin{}).Synthesize(context.Background(), "Hallo", "de")
|
||||
_, err := (OSBuiltin{}).Synthesize(context.Background(), "Hello", "de")
|
||||
if err == nil {
|
||||
t.Fatal("Synthesize mit crashendem espeak: kein Fehler")
|
||||
t.Fatal("Synthesize with crashing espeak: no error")
|
||||
}
|
||||
if errors.Is(err, ErrNoByteOutput) {
|
||||
t.Errorf("Crashender espeak soll kein ErrNoByteOutput sein: %v", err)
|
||||
t.Errorf("crashing espeak must not be ErrNoByteOutput: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "boom") {
|
||||
t.Errorf("Fehler soll stderr des Prozesses enthalten: %v", err)
|
||||
t.Errorf("error should contain the process stderr: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSpeakDirectMitFakeSpdSay: SpeakDirect ruft spd-say blockierend
|
||||
// (-w) mit Sprache (-l <lang>) und Text auf.
|
||||
func TestSpeakDirectMitFakeSpdSay(t *testing.T) {
|
||||
// TestSpeakDirectWithFakeSpdSay: SpeakDirect calls spd-say blocking (-w)
|
||||
// with language (-l <lang>) and text.
|
||||
func TestSpeakDirectWithFakeSpdSay(t *testing.T) {
|
||||
dir := emptyPATH(t)
|
||||
seen := filepath.Join(dir, "seen.txt")
|
||||
fakeBin(t, dir, "spd-say", `printf '%s\n' "$*" > "`+seen+`"`)
|
||||
prependPATH(t, dir)
|
||||
|
||||
if err := (OSBuiltin{}).SpeakDirect(context.Background(), "Hallo Welt", "de"); err != nil {
|
||||
if err := (OSBuiltin{}).SpeakDirect(context.Background(), "Hello world", "de"); err != nil {
|
||||
t.Fatalf("SpeakDirect: %v", err)
|
||||
}
|
||||
args, err := os.ReadFile(seen)
|
||||
if err != nil {
|
||||
t.Fatalf("Fake-spd-say wurde nicht aufgerufen: %v", err)
|
||||
t.Fatalf("fake spd-say was not called: %v", err)
|
||||
}
|
||||
if got := string(args); got != "-w -l de Hallo Welt\n" {
|
||||
t.Errorf("Fake-spd-say ARGS = %q, want %q", got, "-w -l de Hallo Welt\n")
|
||||
if got := string(args); got != "-w -l de Hello world\n" {
|
||||
t.Errorf("fake spd-say ARGS = %q, want %q", got, "-w -l de Hello world\n")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSpeakDirectOhneSpdSay ist ein klarer Fehler.
|
||||
func TestSpeakDirectOhneSpdSay(t *testing.T) {
|
||||
// TestSpeakDirectWithoutSpdSay is a clear error.
|
||||
func TestSpeakDirectWithoutSpdSay(t *testing.T) {
|
||||
emptyPATH(t)
|
||||
if err := (OSBuiltin{}).SpeakDirect(context.Background(), "Hallo", "de"); err == nil {
|
||||
t.Error("SpeakDirect ohne spd-say soll fehlschlagen")
|
||||
if err := (OSBuiltin{}).SpeakDirect(context.Background(), "Hello", "de"); err == nil {
|
||||
t.Error("SpeakDirect without spd-say should fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSpeakDirectEchtesSpdSay läuft nur, wenn TALK_TEST_REAL_SPD=1 gesetzt
|
||||
// ist (erzeugt echte, hörbare Ausgabe über speech-dispatcher — einmalige
|
||||
// manuelle Abnahme; Standard-Testläufe bleiben stumm).
|
||||
func TestSpeakDirectEchtesSpdSay(t *testing.T) {
|
||||
// TestSpeakDirectRealSpdSay runs only when TALK_TEST_REAL_SPD=1 is set (it
|
||||
// produces real, audible output through speech-dispatcher; one-off manual
|
||||
// acceptance, standard test runs stay silent).
|
||||
func TestSpeakDirectRealSpdSay(t *testing.T) {
|
||||
if os.Getenv("TALK_TEST_REAL_SPD") != "1" {
|
||||
t.Skip("Übersprungen (hörbar) — mit TALK_TEST_REAL_SPD=1 ausführen")
|
||||
t.Skip("skipped (audible); run with TALK_TEST_REAL_SPD=1")
|
||||
}
|
||||
if _, err := exec.LookPath("spd-say"); err != nil {
|
||||
t.Skip("spd-say nicht installiert")
|
||||
t.Skip("spd-say not installed")
|
||||
}
|
||||
if err := (OSBuiltin{}).SpeakDirect(context.Background(), "Sprachtest über speech dispatcher", "de"); err != nil {
|
||||
t.Fatalf("SpeakDirect gegen echtes spd-say: %v", err)
|
||||
if err := (OSBuiltin{}).SpeakDirect(context.Background(), "Speech test via speech dispatcher", "de"); err != nil {
|
||||
t.Fatalf("SpeakDirect against real spd-say: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// minimalWAV ist eine minimal gültige RIFF/WAVE-Datei (PCM 16 Bit, mono,
|
||||
// 8000 Hz) mit vier Samples — handgebaut nach der RIFF-Spezifikation,
|
||||
// unabhängig vom Parser im audio-Paket.
|
||||
// minimalWAV is a minimal valid RIFF/WAVE file (PCM 16 bit, mono, 8000 Hz)
|
||||
// with four samples, hand-built per the RIFF specification, independent of
|
||||
// the parser in the audio package.
|
||||
var minimalWAV = []byte{
|
||||
'R', 'I', 'F', 'F', 44, 0, 0, 0, // ChunkSize = 4 + (8+16) + (8+8)
|
||||
'W', 'A', 'V', 'E',
|
||||
@@ -208,5 +208,5 @@ var minimalWAV = []byte{
|
||||
2, 0, // BlockAlign
|
||||
16, 0, // Bits
|
||||
'd', 'a', 't', 'a', 8, 0, 0, 0,
|
||||
0, 0, 0x10, 0x7F, 0xF0, 0x80, 0x00, 0x40, // 4 Samples
|
||||
0, 0, 0x10, 0x7F, 0xF0, 0x80, 0x00, 0x40, // 4 samples
|
||||
}
|
||||
Reference in new issue
Block a user