fix(provider): Chain.Speak läuft im Direkt-Endgame nur einmal durch die Kette
Speak rief bei ErrNoByteOutput SpeakDirect auf, das einen eigenen vollständigen walk machte: fehlgeschlagene Cloud-Stufen wurden zweimal angefragt (doppelte POSTs auf bezahlten APIs), Fehlschlag-Zeilen verdoppelten sich im Log, die Worst-Case-Latenz verdoppelte sich, und Bytes aus einer transienten Erholung im zweiten Durchlauf hätten einen Fehler statt Wiedergabe produziert. Speak entscheidet jetzt aus EINEM walk-Ergebnis (Bytes → Player, nur-Direkt-Stufe → SpeakDirect direkt daran, sonst gebündelter Stufen-Fehler aus dem geteilten Helper walkResult.noTierErr). Synthesize/SpeakDirect behalten ihre öffentlichen Verträge; null-Player verwirft tonlos mit Exit 0 wie zuvor. Test: TestChainDirektEndgameEinDurchlauf pinnt Cloud-Stufe genau 1× angefragt, jede Fehlschlag-Zeile genau 1×, Direktansage kommt zustande.
This commit is contained in:
1 parent
8351d40717
commit
3b44605234
2 files changed
+78
-14
No files matched your search
@@ -340,3 +340,56 @@ func TestChainVoicePinDurchgereicht(t *testing.T) {
|
||||
t.Errorf("Pfad = %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) {
|
||||
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.
|
||||
dir := t.TempDir()
|
||||
seen := filepath.Join(dir, "seen.txt")
|
||||
fakeProcBin(t, dir, "spd-say", `printf '%s\n' "$*" > "`+seen+`"`)
|
||||
t.Setenv("PATH", dir)
|
||||
|
||||
var log bytes.Buffer
|
||||
ch, err := NewChain(ChainConfig{
|
||||
Getenv: chainEnv(map[string]string{
|
||||
EnvElevenLabsKey: "k",
|
||||
EnvElevenLabsEndpoint: eleven,
|
||||
EnvOpenAIEndpoint: eleven, // ohne Key ohnehin übersprungen
|
||||
EnvEdgeEndpoint: "ws://127.0.0.1:1/",
|
||||
}),
|
||||
Player: &fakePlayer{name: "oto"},
|
||||
Log: &log,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewChain: %v", err)
|
||||
}
|
||||
|
||||
if err := ch.Speak(context.Background(), "Hallo Welt", "de"); err != nil {
|
||||
t.Fatalf("Speak (Direkt-Endgame): %v", err)
|
||||
}
|
||||
if got := elHits.Load(); got != 1 {
|
||||
t.Errorf("elevenlabs wurde %d× angefragt, want genau 1 (kein zweiter Durchlauf)", 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(), "Stufe openai übersprungen"); got != 1 {
|
||||
t.Errorf("openai-Skip steht %d× im Log, want genau 1", got)
|
||||
}
|
||||
args, err := os.ReadFile(seen)
|
||||
if err != nil {
|
||||
t.Fatalf("fake spd-say nicht gerufen: %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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user