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
+104
-105
@@ -13,8 +13,8 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakePlayer zeichnet Play-Aufrufe auf (Name steuert die null-Entscheidung
|
||||
// der Kette).
|
||||
// fakePlayer records Play calls (the name drives the chain's null
|
||||
// decision).
|
||||
type fakePlayer struct {
|
||||
name string
|
||||
played [][]byte
|
||||
@@ -26,8 +26,8 @@ func (f *fakePlayer) Play(_ context.Context, data []byte) error {
|
||||
}
|
||||
func (f *fakePlayer) Name() string { return f.name }
|
||||
|
||||
// countingTTSServer antwortet mit status + body und zählt die Requests
|
||||
// (atomic, der Server kann mehrere Stufen-Requests sehen).
|
||||
// countingTTSServer responds with status + body and counts the requests
|
||||
// (atomic; the server may see requests from several tiers).
|
||||
func countingTTSServer(t *testing.T, status int, body []byte) (url string, hits *atomic.Int32) {
|
||||
t.Helper()
|
||||
var n atomic.Int32
|
||||
@@ -40,13 +40,13 @@ func countingTTSServer(t *testing.T, status int, body []byte) (url string, hits
|
||||
return ts.URL, &n
|
||||
}
|
||||
|
||||
// chainEnv baut die Getenv-Seam für Tests mit gefakten Cloud-Endpunkten.
|
||||
// chainEnv builds the Getenv seam for tests with faked cloud endpoints.
|
||||
func chainEnv(kv map[string]string) func(string) string {
|
||||
return func(k string) string { return kv[k] }
|
||||
}
|
||||
|
||||
// fakeProcBin schreibt ein ausführbares Shell-Skript (gleicher Mechanismus
|
||||
// wie in osbuiltin_test.go — echter Prozess, PATH-Manipulation).
|
||||
// fakeProcBin writes an executable shell script (same mechanism as in
|
||||
// osbuiltin_test.go: real process, PATH manipulation).
|
||||
func fakeProcBin(t *testing.T, dir, name, body string) {
|
||||
t.Helper()
|
||||
script := "#!/bin/sh\n" + body + "\n"
|
||||
@@ -55,9 +55,9 @@ func fakeProcBin(t *testing.T, dir, name, body string) {
|
||||
}
|
||||
}
|
||||
|
||||
// cloudEnv liefert die Env-Map für eine Kette, deren elevenlabs-Stufe auf
|
||||
// elevenURL und deren openai-Stufe auf openaiURL zeigt (edge tot, damit
|
||||
// kein Test den echten Microsoft-Endpunkt anfasst).
|
||||
// cloudEnv returns the env map for a chain whose elevenlabs tier points at
|
||||
// elevenURL and whose openai tier points at openaiURL (edge is dead so that
|
||||
// no test touches the real Microsoft endpoint).
|
||||
func cloudEnv(elevenURL, openaiURL string, extra map[string]string) map[string]string {
|
||||
kv := map[string]string{
|
||||
EnvElevenLabsKey: "k",
|
||||
@@ -72,11 +72,10 @@ func cloudEnv(elevenURL, openaiURL string, extra map[string]string) map[string]s
|
||||
return kv
|
||||
}
|
||||
|
||||
// TestChainFallbackErster500ZweiterTreffer (Abnahmekriterium aus dem
|
||||
// Task-Brief): Stufe 1 (elevenlabs) antwortet 500 → die Kette fällt auf
|
||||
// Stufe 2 (openai) durch, deren Bytes kommen zurück; der Fallback ist im
|
||||
// Log sichtbar, nie still.
|
||||
func TestChainFallbackErster500ZweiterTreffer(t *testing.T) {
|
||||
// TestChainFallbackFirst500SecondHit (acceptance criterion): tier 1
|
||||
// (elevenlabs) answers 500 → the chain falls through to tier 2 (openai),
|
||||
// whose bytes come back; the fallback is visible in the log, never silent.
|
||||
func TestChainFallbackFirst500SecondHit(t *testing.T) {
|
||||
eleven, elHits := countingTTSServer(t, 500, []byte("boom"))
|
||||
openai, oaHits := countingTTSServer(t, 200, fakeMP3)
|
||||
|
||||
@@ -89,30 +88,30 @@ func TestChainFallbackErster500ZweiterTreffer(t *testing.T) {
|
||||
t.Fatalf("NewChain: %v", err)
|
||||
}
|
||||
|
||||
data, err := ch.Synthesize(context.Background(), "Hallo Welt", "de")
|
||||
data, err := ch.Synthesize(context.Background(), "Hello world", "de")
|
||||
if err != nil {
|
||||
t.Fatalf("Synthesize: %v", err)
|
||||
}
|
||||
if string(data) != string(fakeMP3) {
|
||||
t.Errorf("Synthesize = %q, want Bytes der openai-Stufe", data)
|
||||
t.Errorf("Synthesize = %q, want bytes of the openai tier", data)
|
||||
}
|
||||
if elHits.Load() != 1 || oaHits.Load() != 1 {
|
||||
t.Errorf("Hits: elevenlabs = %d, openai = %d — want je 1 (500 → Fallback)", elHits.Load(), oaHits.Load())
|
||||
t.Errorf("hits: elevenlabs = %d, openai = %d; want 1 each (500 → fallback)", elHits.Load(), oaHits.Load())
|
||||
}
|
||||
for _, want := range []string{"elevenlabs", "500", "Fallback", "Ansage über openai"} {
|
||||
for _, want := range []string{"elevenlabs", "500", "falling back", "spoken via openai"} {
|
||||
if !strings.Contains(log.String(), want) {
|
||||
t.Errorf("Log = %q, will %q", log.String(), want)
|
||||
t.Errorf("log = %q, want %q", log.String(), want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestChainAllesFehlt: beide Cloud-Stufen 500, edge tot, osbuiltin nicht
|
||||
// verfügbar → Fehler nennt die fehlgeschlagenen Stufen; Log zeigt jeden.
|
||||
func TestChainAllesFehlt(t *testing.T) {
|
||||
// TestChainEverythingFails: both cloud tiers 500, edge dead, osbuiltin not
|
||||
// available → the error names the failed tiers; the log shows each one.
|
||||
func TestChainEverythingFails(t *testing.T) {
|
||||
eleven, _ := countingTTSServer(t, 500, []byte("el-boom"))
|
||||
openai, _ := countingTTSServer(t, 500, []byte("oa-boom"))
|
||||
|
||||
// PATH leer: osbuiltin nicht verfügbar.
|
||||
// Empty PATH: osbuiltin not available.
|
||||
t.Setenv("PATH", t.TempDir())
|
||||
|
||||
var log bytes.Buffer
|
||||
@@ -124,21 +123,21 @@ func TestChainAllesFehlt(t *testing.T) {
|
||||
t.Fatalf("NewChain: %v", err)
|
||||
}
|
||||
|
||||
_, err = ch.Synthesize(context.Background(), "Hallo", "de")
|
||||
_, err = ch.Synthesize(context.Background(), "Hello", "de")
|
||||
if err == nil {
|
||||
t.Fatal("Synthesize mit allen Stufen kaputt: kein Fehler")
|
||||
t.Fatal("Synthesize with all tiers broken: no error")
|
||||
}
|
||||
for _, want := range []string{"elevenlabs", "openai", "osbuiltin", "500"} {
|
||||
if !strings.Contains(err.Error(), want) && !strings.Contains(log.String(), want) {
|
||||
t.Errorf("Weder Fehler (%v) noch Log nennen %q", err, want)
|
||||
t.Errorf("neither error (%v) nor log mention %q", err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestChainOhneKeysEdgeOsbuiltin (Abnahmekriterium): ohne jeden Key und
|
||||
// mit totem edge wählt die Kette osbuiltin — mit gefaktem espeak kommen
|
||||
// dessen Bytes zurück.
|
||||
func TestChainOhneKeysEdgeOsbuiltin(t *testing.T) {
|
||||
// TestChainWithoutKeysEdgeOsbuiltin (acceptance criterion): without any
|
||||
// key and with a dead edge, the chain picks osbuiltin; with a faked espeak
|
||||
// its bytes come back.
|
||||
func TestChainWithoutKeysEdgeOsbuiltin(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fakeProcBin(t, dir, "espeak", `printf 'RIFFxxxxWAVEfmt '`)
|
||||
t.Setenv("PATH", dir)
|
||||
@@ -151,20 +150,20 @@ func TestChainOhneKeysEdgeOsbuiltin(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("NewChain: %v", err)
|
||||
}
|
||||
data, err := ch.Synthesize(context.Background(), "Hallo", "de")
|
||||
data, err := ch.Synthesize(context.Background(), "Hello", "de")
|
||||
if err != nil {
|
||||
t.Fatalf("Synthesize: %v", err)
|
||||
}
|
||||
if !bytes.HasPrefix(data, []byte("RIFF")) {
|
||||
t.Errorf("Synthesize = %q, want WAV-Bytes des fake espeak", data)
|
||||
t.Errorf("Synthesize = %q, want WAV bytes of the fake espeak", data)
|
||||
}
|
||||
if !strings.Contains(log.String(), "übersprungen (nicht verfügbar)") {
|
||||
t.Errorf("Log = %q, will Skip-Hinweise für die Key-Stufen", log.String())
|
||||
if !strings.Contains(log.String(), "skipped (not available)") {
|
||||
t.Errorf("log = %q, want skip notes for the key tiers", log.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestChainPin: --provider/config pinnen die Kette auf genau eine Stufe —
|
||||
// andere Stufen werden nicht einmal angefragt.
|
||||
// TestChainPin: --provider/config pin the chain to exactly one tier; other
|
||||
// tiers are not even requested.
|
||||
func TestChainPin(t *testing.T) {
|
||||
eleven, elHits := countingTTSServer(t, 200, fakeMP3)
|
||||
openai, oaHits := countingTTSServer(t, 500, []byte("boom"))
|
||||
@@ -176,24 +175,24 @@ func TestChainPin(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("NewChain: %v", err)
|
||||
}
|
||||
if _, err := ch.Synthesize(context.Background(), "Hallo", "de"); err == nil {
|
||||
t.Fatal("Gepinnte kaputte Stufe soll fehlschlagen (kein Fallback bei Pin)")
|
||||
if _, err := ch.Synthesize(context.Background(), "Hello", "de"); err == nil {
|
||||
t.Fatal("pinned broken tier should fail (no fallback when pinned)")
|
||||
}
|
||||
if elHits.Load() != 0 || oaHits.Load() != 1 {
|
||||
t.Errorf("Hits: elevenlabs = %d (want 0), openai = %d (want 1)", elHits.Load(), oaHits.Load())
|
||||
t.Errorf("hits: elevenlabs = %d (want 0), openai = %d (want 1)", elHits.Load(), oaHits.Load())
|
||||
}
|
||||
}
|
||||
|
||||
// TestChainUnbekannterPin ist ein klarer Fehler mit Namensliste.
|
||||
func TestChainUnbekannterPin(t *testing.T) {
|
||||
if _, err := NewChain(ChainConfig{Pin: "gibtsnicht"}); err == nil || !strings.Contains(err.Error(), "elevenlabs") {
|
||||
t.Errorf("NewChain-Fehler = %v, will verfügbare Namen nennen", err)
|
||||
// TestChainUnknownPin is a clear error listing the names.
|
||||
func TestChainUnknownPin(t *testing.T) {
|
||||
if _, err := NewChain(ChainConfig{Pin: "doesnotexist"}); err == nil || !strings.Contains(err.Error(), "elevenlabs") {
|
||||
t.Errorf("NewChain error = %v, want the available names", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestChainSpeakBytesUeberPlayer: erfolgreiche Synthese wird über den
|
||||
// Player abgespielt.
|
||||
func TestChainSpeakBytesUeberPlayer(t *testing.T) {
|
||||
// TestChainSpeakBytesViaPlayer: a successful synthesis is played via the
|
||||
// Player.
|
||||
func TestChainSpeakBytesViaPlayer(t *testing.T) {
|
||||
eleven, _ := countingTTSServer(t, 200, fakeMP3)
|
||||
player := &fakePlayer{name: "oto"}
|
||||
ch, err := NewChain(ChainConfig{
|
||||
@@ -203,22 +202,22 @@ func TestChainSpeakBytesUeberPlayer(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("NewChain: %v", err)
|
||||
}
|
||||
if err := ch.Speak(context.Background(), "Hallo", "de"); err != nil {
|
||||
if err := ch.Speak(context.Background(), "Hello", "de"); err != nil {
|
||||
t.Fatalf("Speak: %v", err)
|
||||
}
|
||||
if len(player.played) != 1 || string(player.played[0]) != string(fakeMP3) {
|
||||
t.Errorf("Player.Play = %x, want die Synthese-Bytes", player.played)
|
||||
t.Errorf("Player.Play = %x, want the synthesis bytes", player.played)
|
||||
}
|
||||
}
|
||||
|
||||
// TestChainDirektMitNullPlayer: fällt die Kette auf die Direkt-Stufe
|
||||
// (osbuiltin ohne espeak), verwirft ein null-Player die Ansage tonlos und
|
||||
// SUCCESS — spd-say wird nicht gerufen.
|
||||
func TestChainDirektMitNullPlayer(t *testing.T) {
|
||||
// TestChainDirectWithNullPlayer: when the chain falls back to the direct
|
||||
// tier (osbuiltin without espeak), a null player discards the announcement
|
||||
// silently and SUCCEEDS; spd-say is not called.
|
||||
func TestChainDirectWithNullPlayer(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
seen := filepath.Join(dir, "seen.txt")
|
||||
fakeProcBin(t, dir, "spd-say", `printf '%s\n' "$*" > "`+seen+`"`)
|
||||
t.Setenv("PATH", dir) // kein espeak → keine Bytes, aber spd-say da
|
||||
t.Setenv("PATH", dir) // no espeak → no bytes, but spd-say present
|
||||
|
||||
var log bytes.Buffer
|
||||
ch, err := NewChain(ChainConfig{
|
||||
@@ -230,20 +229,21 @@ func TestChainDirektMitNullPlayer(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("NewChain: %v", err)
|
||||
}
|
||||
if err := ch.Speak(context.Background(), "Hallo Welt", "de"); err != nil {
|
||||
t.Fatalf("Speak (null-Player): %v", err)
|
||||
if err := ch.Speak(context.Background(), "Hello world", "de"); err != nil {
|
||||
t.Fatalf("Speak (null player): %v", err)
|
||||
}
|
||||
if _, err := os.Stat(seen); err == nil {
|
||||
t.Error("spd-say wurde gerufen — null-Player hätte verwerfen müssen")
|
||||
t.Error("spd-say was called; the null player should have discarded")
|
||||
}
|
||||
if !strings.Contains(log.String(), "verworfen") {
|
||||
t.Errorf("Log = %q, will Verwurf-Zeile", log.String())
|
||||
if !strings.Contains(log.String(), "discarded") {
|
||||
t.Errorf("log = %q, want discard line", log.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestChainDirektMitTonPlayer: mit echtem Player (Name ≠ null) läuft die
|
||||
// Direktansage über SpeakDirect — blockierend (-w) mit Sprache und Text.
|
||||
func TestChainDirektMitTonPlayer(t *testing.T) {
|
||||
// TestChainDirectWithSoundPlayer: with a real player (name != null) the
|
||||
// direct announcement runs via SpeakDirect, blocking (-w) with language and
|
||||
// text.
|
||||
func TestChainDirectWithSoundPlayer(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
seen := filepath.Join(dir, "seen.txt")
|
||||
fakeProcBin(t, dir, "spd-say", `printf '%s\n' "$*" > "`+seen+`"`)
|
||||
@@ -258,24 +258,24 @@ func TestChainDirektMitTonPlayer(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("NewChain: %v", err)
|
||||
}
|
||||
if err := ch.Speak(context.Background(), "Hallo Welt", "de"); err != nil {
|
||||
t.Fatalf("Speak (Direkt): %v", err)
|
||||
if err := ch.Speak(context.Background(), "Hello world", "de"); err != nil {
|
||||
t.Fatalf("Speak (direct): %v", err)
|
||||
}
|
||||
args, err := os.ReadFile(seen)
|
||||
if err != nil {
|
||||
t.Fatalf("fake spd-say nicht gerufen: %v", err)
|
||||
t.Fatalf("fake spd-say not called: %v", err)
|
||||
}
|
||||
if got := strings.TrimSpace(string(args)); got != "-w -l de Hallo Welt" {
|
||||
t.Errorf("spd-say ARGS = %q, want %q", got, "-w -l de Hallo Welt")
|
||||
if got := strings.TrimSpace(string(args)); got != "-w -l de Hello world" {
|
||||
t.Errorf("spd-say ARGS = %q, want %q", got, "-w -l de Hello world")
|
||||
}
|
||||
if len(player.played) != 0 {
|
||||
t.Errorf("Player.Play wurde gerufen (%x) — Direktansage geht am Player vorbei", player.played)
|
||||
t.Errorf("Player.Play was called (%x); a direct announcement bypasses the player", player.played)
|
||||
}
|
||||
}
|
||||
|
||||
// TestChainSynthesizeErrNoByteOutput: Die Kette als Provider meldet
|
||||
// ErrNoByteOutput, wenn nur eine Direkt-Stufe übrig bleibt — der
|
||||
// DirectSpeaker-Vertrag bleibt am Aufrufer erhalten.
|
||||
// TestChainSynthesizeErrNoByteOutput: the chain as a Provider reports
|
||||
// ErrNoByteOutput when only a direct tier remains; the DirectSpeaker
|
||||
// contract is preserved towards the caller.
|
||||
func TestChainSynthesizeErrNoByteOutput(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fakeProcBin(t, dir, "spd-say", "true")
|
||||
@@ -285,27 +285,27 @@ func TestChainSynthesizeErrNoByteOutput(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("NewChain: %v", err)
|
||||
}
|
||||
_, err = ch.Synthesize(context.Background(), "Hallo", "de")
|
||||
_, err = ch.Synthesize(context.Background(), "Hello", "de")
|
||||
if !errors.Is(err, ErrNoByteOutput) {
|
||||
t.Fatalf("Synthesize-Fehler = %v, want ErrNoByteOutput", err)
|
||||
t.Fatalf("Synthesize error = %v, want ErrNoByteOutput", err)
|
||||
}
|
||||
// Chain selbst ist DirectSpeaker.
|
||||
// Chain itself is a DirectSpeaker.
|
||||
if _, ok := any(ch).(DirectSpeaker); !ok {
|
||||
t.Error("Chain implementiert DirectSpeaker nicht")
|
||||
t.Error("Chain does not implement DirectSpeaker")
|
||||
}
|
||||
}
|
||||
|
||||
// TestChainAvailableUndNamen: Available true sobald eine Stufe verfügbar;
|
||||
// Default-Reihenfolge ist die bindende aus der Spec.
|
||||
func TestChainAvailableUndNamen(t *testing.T) {
|
||||
// TestChainAvailableAndNames: Available is true as soon as one tier is
|
||||
// available; the default order is the binding one from the spec.
|
||||
func TestChainAvailableAndNames(t *testing.T) {
|
||||
ch, err := NewChain(ChainConfig{Getenv: chainEnv(map[string]string{
|
||||
EnvEdgeEndpoint: "wss://edge.example.com", // edge: immer verfügbar mit Endpunkt
|
||||
EnvEdgeEndpoint: "wss://edge.example.com", // edge: always available with an endpoint
|
||||
})})
|
||||
if err != nil {
|
||||
t.Fatalf("NewChain: %v", err)
|
||||
}
|
||||
if !ch.Available(context.Background()) {
|
||||
t.Error("Available = false trotz verfügbarer edge-Stufe")
|
||||
t.Error("Available = false despite an available edge tier")
|
||||
}
|
||||
want := []string{"elevenlabs", "openai", "edge", "osbuiltin"}
|
||||
if len(ch.Tiers()) != len(want) {
|
||||
@@ -318,9 +318,9 @@ func TestChainAvailableUndNamen(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestChainVoicePinDurchgereicht: ChainConfig.Voice wird via VoicePinner an
|
||||
// die Stufen gepinnt — beobachtbar am Request-Pfad des ElevenLabs-Fakes.
|
||||
func TestChainVoicePinDurchgereicht(t *testing.T) {
|
||||
// TestChainVoicePinForwarded: ChainConfig.Voice is pinned on the tiers via
|
||||
// VoicePinner; observable in the request path of the ElevenLabs fake.
|
||||
func TestChainVoicePinForwarded(t *testing.T) {
|
||||
ts, rec := fakeTTSServer(t, 200, fakeMP3)
|
||||
ch, err := NewChain(ChainConfig{
|
||||
Pin: "elevenlabs",
|
||||
@@ -333,24 +333,23 @@ func TestChainVoicePinDurchgereicht(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("NewChain: %v", err)
|
||||
}
|
||||
if _, err := ch.Synthesize(context.Background(), "Hallo", "de"); err != nil {
|
||||
if _, err := ch.Synthesize(context.Background(), "Hello", "de"); err != nil {
|
||||
t.Fatalf("Synthesize: %v", err)
|
||||
}
|
||||
if want := "/v1/text-to-speech/ChainVoice"; rec.path != want {
|
||||
t.Errorf("Pfad = %q, want %q", rec.path, want)
|
||||
t.Errorf("path = %q, want %q", rec.path, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestChainDirektEndgameEinDurchlauf (Review-Fix): Im DirectSpeaker-
|
||||
// Endgame läuft die Kette genau EINMAL — die fehlgeschlagene Cloud-Stufe
|
||||
// wird nur einmal angefragt (keine doppelten POSTs auf bezahlten APIs,
|
||||
// keine verdoppelte Latenz), jede Stufe protokolliert ihren Fehlschlag
|
||||
// genau einmal, und die Direktansage kommt zustande.
|
||||
func TestChainDirektEndgameEinDurchlauf(t *testing.T) {
|
||||
// TestChainDirectEndgameSingleWalk (review fix): in the DirectSpeaker
|
||||
// endgame the chain walks exactly ONCE; the failed cloud tier is requested
|
||||
// only once (no duplicate POSTs to paid APIs, no doubled latency), every
|
||||
// tier logs its failure exactly once, and the direct announcement happens.
|
||||
func TestChainDirectEndgameSingleWalk(t *testing.T) {
|
||||
eleven, elHits := countingTTSServer(t, 500, []byte("boom"))
|
||||
// elevenlabs: Key + Fake (500). openai: Endpunkt, aber KEIN Key →
|
||||
// übersprungen. edge: toter Loopback-Port. osbuiltin: PATH nur mit
|
||||
// fake spd-say (kein espeak) → keine Bytes, Direktansage.
|
||||
// elevenlabs: key + fake (500). openai: endpoint but NO key → skipped.
|
||||
// edge: dead loopback port. osbuiltin: PATH with only a fake spd-say (no
|
||||
// espeak) → no bytes, direct announcement.
|
||||
dir := t.TempDir()
|
||||
seen := filepath.Join(dir, "seen.txt")
|
||||
fakeProcBin(t, dir, "spd-say", `printf '%s\n' "$*" > "`+seen+`"`)
|
||||
@@ -361,7 +360,7 @@ func TestChainDirektEndgameEinDurchlauf(t *testing.T) {
|
||||
Getenv: chainEnv(map[string]string{
|
||||
EnvElevenLabsKey: "k",
|
||||
EnvElevenLabsEndpoint: eleven,
|
||||
EnvOpenAIEndpoint: eleven, // ohne Key ohnehin übersprungen
|
||||
EnvOpenAIEndpoint: eleven, // skipped anyway without a key
|
||||
EnvEdgeEndpoint: "ws://127.0.0.1:1/",
|
||||
}),
|
||||
Player: &fakePlayer{name: "oto"},
|
||||
@@ -371,25 +370,25 @@ func TestChainDirektEndgameEinDurchlauf(t *testing.T) {
|
||||
t.Fatalf("NewChain: %v", err)
|
||||
}
|
||||
|
||||
if err := ch.Speak(context.Background(), "Hallo Welt", "de"); err != nil {
|
||||
t.Fatalf("Speak (Direkt-Endgame): %v", err)
|
||||
if err := ch.Speak(context.Background(), "Hello world", "de"); err != nil {
|
||||
t.Fatalf("Speak (direct endgame): %v", err)
|
||||
}
|
||||
if got := elHits.Load(); got != 1 {
|
||||
t.Errorf("elevenlabs wurde %d× angefragt, want genau 1 (kein zweiter Durchlauf)", got)
|
||||
t.Errorf("elevenlabs was requested %d times, want exactly 1 (no second walk)", got)
|
||||
}
|
||||
for _, tier := range []string{"elevenlabs", "edge"} {
|
||||
if got := strings.Count(log.String(), "Stufe "+tier+" fehlgeschlagen"); got != 1 {
|
||||
t.Errorf("„Stufe %s fehlgeschlagen“ steht %d× im Log, want genau 1:\n%s", tier, got, log.String())
|
||||
if got := strings.Count(log.String(), "tier "+tier+" failed"); got != 1 {
|
||||
t.Errorf("\"tier %s failed\" appears %d times in the log, want exactly 1:\n%s", tier, got, log.String())
|
||||
}
|
||||
}
|
||||
if got := strings.Count(log.String(), "Stufe openai übersprungen"); got != 1 {
|
||||
t.Errorf("openai-Skip steht %d× im Log, want genau 1", got)
|
||||
if got := strings.Count(log.String(), "tier openai skipped"); got != 1 {
|
||||
t.Errorf("openai skip appears %d times in the log, want exactly 1", got)
|
||||
}
|
||||
args, err := os.ReadFile(seen)
|
||||
if err != nil {
|
||||
t.Fatalf("fake spd-say nicht gerufen: %v", err)
|
||||
t.Fatalf("fake spd-say not called: %v", err)
|
||||
}
|
||||
if got := strings.TrimSpace(string(args)); got != "-w -l de Hallo Welt" {
|
||||
t.Errorf("spd-say ARGS = %q, want %q", got, "-w -l de Hallo Welt")
|
||||
if got := strings.TrimSpace(string(args)); got != "-w -l de Hello world" {
|
||||
t.Errorf("spd-say ARGS = %q, want %q", got, "-w -l de Hello world")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user