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
@@ -11,7 +11,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakeTTServation zeichnet einen Request am Fake-Server auf.
|
||||
// recordedRequest records a request seen by the fake server.
|
||||
type recordedRequest struct {
|
||||
method string
|
||||
path string
|
||||
@@ -19,8 +19,8 @@ type recordedRequest struct {
|
||||
body []byte
|
||||
}
|
||||
|
||||
// fakeTTSServer ist der httptest-Fake eines Cloud-Providers: antwortet mit
|
||||
// status + body und zeichnet den (einzigen) gesehenen Request auf.
|
||||
// fakeTTSServer is the httptest fake of a cloud provider: responds with
|
||||
// status + body and records the (single) request it saw.
|
||||
func fakeTTSServer(t *testing.T, status int, body []byte) (*httptest.Server, *recordedRequest) {
|
||||
t.Helper()
|
||||
rec := &recordedRequest{}
|
||||
@@ -28,7 +28,7 @@ func fakeTTSServer(t *testing.T, status int, body []byte) (*httptest.Server, *re
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body2, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Fake-Server: Body lesen: %v", err)
|
||||
t.Errorf("fake server: reading body: %v", err)
|
||||
}
|
||||
mu.Lock()
|
||||
rec.method, rec.path, rec.header, rec.body = r.Method, r.URL.Path, r.Header.Clone(), body2
|
||||
@@ -44,14 +44,14 @@ func envMap(kv map[string]string) func(string) string {
|
||||
return func(k string) string { return kv[k] }
|
||||
}
|
||||
|
||||
// fakeMP3 ist eine beliebige Byte-Folge — die Provider geben Antwort-Bytes
|
||||
// unverändert zurück, der audio-Sink dekodiert (eigene Tests).
|
||||
var fakeMP3 = []byte("\xff\xfb\x90\x00" + "audio-bytes-von-elf")
|
||||
// fakeMP3 is an arbitrary byte sequence; the providers return response
|
||||
// bytes unchanged, the audio sink decodes (its own tests).
|
||||
var fakeMP3 = []byte("\xff\xfb\x90\x00" + "audio-bytes-from-eleven")
|
||||
|
||||
// TestElevenLabsSynthese200: 200 mit Bytes → Bytes kommen unverändert
|
||||
// zurück; Methode, Pfad (inkl. Voice-ID), Header (xi-api-key) und der
|
||||
// JSON-Body (model_id, voice_settings) entsprechen dem Task-Brief.
|
||||
func TestElevenLabsSynthese200(t *testing.T) {
|
||||
// TestElevenLabsSynthesize200: 200 with bytes → bytes come back unchanged;
|
||||
// method, path (incl. voice ID), header (xi-api-key) and the JSON body
|
||||
// (model_id, voice_settings) match the API contract.
|
||||
func TestElevenLabsSynthesize200(t *testing.T) {
|
||||
ts, rec := fakeTTSServer(t, 200, fakeMP3)
|
||||
p := NewElevenLabs(ChainConfig{
|
||||
Getenv: envMap(map[string]string{
|
||||
@@ -60,18 +60,18 @@ func TestElevenLabsSynthese200(t *testing.T) {
|
||||
}),
|
||||
})
|
||||
|
||||
data, err := p.Synthesize(context.Background(), "Hallo Welt", "de")
|
||||
data, err := p.Synthesize(context.Background(), "Hello world", "de")
|
||||
if err != nil {
|
||||
t.Fatalf("Synthesize: %v", err)
|
||||
}
|
||||
if string(data) != string(fakeMP3) {
|
||||
t.Errorf("Synthesize = %q, want Fake-Bytes %q", data, fakeMP3)
|
||||
t.Errorf("Synthesize = %q, want fake bytes %q", data, fakeMP3)
|
||||
}
|
||||
if rec.method != http.MethodPost {
|
||||
t.Errorf("Methode = %s, want POST", rec.method)
|
||||
t.Errorf("method = %s, want POST", rec.method)
|
||||
}
|
||||
if want := "/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM"; rec.path != want {
|
||||
t.Errorf("Pfad = %q, want %q (neutrales Repo-Default „Rachel“)", rec.path, want)
|
||||
t.Errorf("path = %q, want %q (neutral repo default \"Rachel\")", rec.path, want)
|
||||
}
|
||||
if got := rec.header.Get("xi-api-key"); got != "testkey" {
|
||||
t.Errorf("xi-api-key = %q, want %q", got, "testkey")
|
||||
@@ -86,9 +86,9 @@ func TestElevenLabsSynthese200(t *testing.T) {
|
||||
} `json:"voice_settings"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.body, &body); err != nil {
|
||||
t.Fatalf("Antwort-Body ist kein JSON: %v (%s)", err, rec.body)
|
||||
t.Fatalf("request body is not JSON: %v (%s)", err, rec.body)
|
||||
}
|
||||
if body.Text != "Hallo Welt" {
|
||||
if body.Text != "Hello world" {
|
||||
t.Errorf("text = %q", body.Text)
|
||||
}
|
||||
if body.ModelID != "eleven_multilingual_v2" {
|
||||
@@ -99,9 +99,9 @@ func TestElevenLabsSynthese200(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestElevenLabsHTTPFehler: 500 und 401 sind klare Fehler, die Status und
|
||||
// einen Body-Ausschnitt nennen (die Kette fällt daran weiter).
|
||||
func TestElevenLabsHTTPFehler(t *testing.T) {
|
||||
// TestElevenLabsHTTPError: 500 and 401 are clear errors naming the status
|
||||
// and a body snippet (the chain falls back on them).
|
||||
func TestElevenLabsHTTPError(t *testing.T) {
|
||||
cases := []struct {
|
||||
status int
|
||||
body string
|
||||
@@ -117,32 +117,32 @@ func TestElevenLabsHTTPFehler(t *testing.T) {
|
||||
EnvElevenLabsKey: "testkey",
|
||||
EnvElevenLabsEndpoint: ts.URL,
|
||||
})})
|
||||
_, err := p.Synthesize(context.Background(), "Hallo", "de")
|
||||
_, err := p.Synthesize(context.Background(), "Hello", "de")
|
||||
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Fatalf("Synthesize-Fehler = %v, will HTTP %s", err, tc.want)
|
||||
t.Fatalf("Synthesize error = %v, want HTTP %s", err, tc.want)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "elevenlabs") {
|
||||
t.Errorf("Fehler soll den Provider nennen: %v", err)
|
||||
t.Errorf("error should name the provider: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestElevenLabsAvailable: ohne Key nicht verfügbar, mit Key verfügbar —
|
||||
// ohne jeden Netz-Call.
|
||||
// TestElevenLabsAvailable: not available without a key, available with
|
||||
// one; without any network call.
|
||||
func TestElevenLabsAvailable(t *testing.T) {
|
||||
if NewElevenLabs(ChainConfig{Getenv: envMap(nil)}).Available(context.Background()) {
|
||||
t.Error("Available ohne Key = true, want false")
|
||||
t.Error("Available without key = true, want false")
|
||||
}
|
||||
p := NewElevenLabs(ChainConfig{Getenv: envMap(map[string]string{EnvElevenLabsKey: "k"})})
|
||||
if !p.Available(context.Background()) {
|
||||
t.Error("Available mit Key = false, want true")
|
||||
t.Error("Available with key = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
// TestElevenLabsVoiceRangfolge: --voice-Pin schlägt config-Mapping, Mapping
|
||||
// schlägt Repo-Default (beobachtbar am Request-Pfad).
|
||||
func TestElevenLabsVoiceRangfolge(t *testing.T) {
|
||||
// TestElevenLabsVoicePrecedence: the --voice pin beats the config mapping,
|
||||
// the mapping beats the repo default (observable in the request path).
|
||||
func TestElevenLabsVoicePrecedence(t *testing.T) {
|
||||
mk := func(t *testing.T, voices map[string]string, pin string) (*ElevenLabs, *recordedRequest) {
|
||||
t.Helper()
|
||||
ts, rec := fakeTTSServer(t, 200, fakeMP3)
|
||||
@@ -159,46 +159,46 @@ func TestElevenLabsVoiceRangfolge(t *testing.T) {
|
||||
return p, rec
|
||||
}
|
||||
|
||||
t.Run("config-Mapping schlägt Default", func(t *testing.T) {
|
||||
p, rec := mk(t, map[string]string{"de": "AusConfig"}, "")
|
||||
if _, err := p.Synthesize(context.Background(), "Hallo", "de"); err != nil {
|
||||
t.Run("config mapping beats default", func(t *testing.T) {
|
||||
p, rec := mk(t, map[string]string{"de": "FromConfig"}, "")
|
||||
if _, err := p.Synthesize(context.Background(), "Hello", "de"); err != nil {
|
||||
t.Fatalf("Synthesize: %v", err)
|
||||
}
|
||||
if want := "/v1/text-to-speech/AusConfig"; rec.path != want {
|
||||
t.Errorf("Pfad = %q, want %q", rec.path, want)
|
||||
if want := "/v1/text-to-speech/FromConfig"; rec.path != want {
|
||||
t.Errorf("path = %q, want %q", rec.path, want)
|
||||
}
|
||||
})
|
||||
t.Run("Pin (--voice) schlägt config-Mapping", func(t *testing.T) {
|
||||
p, rec := mk(t, map[string]string{"de": "AusConfig"}, "Gepinnt")
|
||||
if _, err := p.Synthesize(context.Background(), "Hallo", "de"); err != nil {
|
||||
t.Run("pin (--voice) beats config mapping", func(t *testing.T) {
|
||||
p, rec := mk(t, map[string]string{"de": "FromConfig"}, "Pinned")
|
||||
if _, err := p.Synthesize(context.Background(), "Hello", "de"); err != nil {
|
||||
t.Fatalf("Synthesize: %v", err)
|
||||
}
|
||||
if want := "/v1/text-to-speech/Gepinnt"; rec.path != want {
|
||||
t.Errorf("Pfad = %q, want %q", rec.path, want)
|
||||
if want := "/v1/text-to-speech/Pinned"; rec.path != want {
|
||||
t.Errorf("path = %q, want %q", rec.path, want)
|
||||
}
|
||||
})
|
||||
t.Run("unbekannte Sprache fällt auf en-Default", func(t *testing.T) {
|
||||
t.Run("unknown language falls back to en default", func(t *testing.T) {
|
||||
p, rec := mk(t, nil, "")
|
||||
if _, err := p.Synthesize(context.Background(), "Hallo", "fr"); err != nil {
|
||||
if _, err := p.Synthesize(context.Background(), "Hello", "fr"); err != nil {
|
||||
t.Fatalf("Synthesize: %v", err)
|
||||
}
|
||||
if want := "/v1/text-to-speech/21m00Tcm4TlvDq8ikWAM"; rec.path != want {
|
||||
t.Errorf("Pfad = %q, want en-Default %q", rec.path, want)
|
||||
t.Errorf("path = %q, want en default %q", rec.path, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestElevenLabsLeererText ist ein klarer Fehler ohne Request.
|
||||
func TestElevenLabsLeererText(t *testing.T) {
|
||||
// TestElevenLabsEmptyText is a clear error without a request.
|
||||
func TestElevenLabsEmptyText(t *testing.T) {
|
||||
ts, rec := fakeTTSServer(t, 200, fakeMP3)
|
||||
p := NewElevenLabs(ChainConfig{Getenv: envMap(map[string]string{
|
||||
EnvElevenLabsKey: "k",
|
||||
EnvElevenLabsEndpoint: ts.URL,
|
||||
})})
|
||||
if _, err := p.Synthesize(context.Background(), "", "de"); err == nil {
|
||||
t.Error("Synthesize(\"\") soll fehlschlagen")
|
||||
t.Error("Synthesize(\"\") should fail")
|
||||
}
|
||||
if rec.method != "" {
|
||||
t.Error("Bei leerem Text darf kein Request gehen")
|
||||
t.Error("no request must be sent for empty text")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user