Usability-Schicht für Nutzer, die kein Terminal-Handbuch lesen wollen: - `talk doctor`: eine Zeile je Prüfpunkt mit klarem Satz, was Sache ist und was zu tun wäre — Config (kaputt → ✘, die übrigen Prüfungen laufen mit Defaults weiter), Daemon (läuft / startet beim nächsten Sprechbefehl / fremder Dienst auf dem Port → ✘ mit Port-Hinweis), Audio (oto-Kontext wirklich initialisieren; null → ⚠), jede Provider-Stufe (Key-Env-Name, Endpunkt, Default-Stimme; OS-Stimme nennt den gefundenen Mechanismus), Mute (an → ⚠ mit unmute-Hinweis), Gesetze inkl. gerade aktiver Ruhezeit. --probe macht echte Synthese-Proben, --json liefert die Liste. Exit 0 ohne ✘. Windows-Konsolen bekommen [OK]/[WARN]/[FAIL]. - `talk test`: Testsatz direkt über den Sprech-Kern (nicht über den Daemon), nennt die Stufe, die wirklich gesprochen hat, Synthese- und Wiedergabezeit; Mute/Ruhezeit gelten bewusst nicht, Mute wird genannt. - `talk voices`: kuratierter Katalog mit Namen (ElevenLabs-Premade, OpenAI, Edge-Neural de/en-Locales) als Tabelle, „*“ = effektive Default-Stimme (Config vor Repo-Default), Config-IDs außerhalb des Katalogs als eigene Zeile, Filter --provider/--lang, --json. - `talk setup`: wenige Fragen statt Bubble Tea — Keys nur als Hinweis (ohne Key läuft die Edge-Stimme), Stimme je Sprache aus dem Katalog mit Probehören, Ruhezeit; schreibt die config.toml zusammengeführt mit bestehenden Werten (.bak), spricht einen Testsatz. q bricht ab, bevor etwas geschrieben wird; ohne Terminal keine Fragen, nur die wirksame Konfiguration. Unterbau: provider.Catalog/VoiceName/DefaultVoice, Chain.SpeakOutcome (Stufe, Bytes, Zeiten) durch speak.Engine bis in Daemon-Log und test, audio.Check (oto-Initialisierung ohne Ton), config.Path, OSBuiltin.Describe. README: Abschnitt „Erste Schritte“. Tests: voices-Zeilen (Default-Markierung, Config-Override, Filter, JSON), doctor (ohne Daemon, Mute/Key via JSON, fremder Dienst → Exit 1, kaputte Config → ✘ aber vollständiger Bericht), test (fake espeak), setup (stdin-Antworten → config.toml-Inhalt, Merge mit .bak, Abbruch schreibt nichts, Pipe stellt keine Fragen), Prozessgrenze: doctor/--probe/voices/ test gegen echten Daemon. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
117 lines
4.2 KiB
Go
117 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func checkByName(t *testing.T, checks []check, name string) check {
|
|
t.Helper()
|
|
for _, c := range checks {
|
|
if c.Check == name {
|
|
return c
|
|
}
|
|
}
|
|
t.Fatalf("Prüfung %q fehlt in %+v", name, checks)
|
|
return check{}
|
|
}
|
|
|
|
// TestDoctorOhneDaemon: toter Daemon ist ⚠ (kein Fehler), sink null ist ⚠,
|
|
// Cloud-Stufen ohne Key ⚠, edge ✔ — Exit 0, Abschlusszeile „Alles bereit".
|
|
func TestDoctorOhneDaemon(t *testing.T) {
|
|
root := t.TempDir()
|
|
setGetenv(t, map[string]string{"TALK_AUDIO_SINK": "null", "XDG_CONFIG_HOME": root})
|
|
var out, errBuf bytes.Buffer
|
|
code := runCLI([]string{"doctor"}, &out, &errBuf)
|
|
if code != exitOK {
|
|
t.Fatalf("talk doctor: Exit %d, stdout %q, stderr %q", code, out.String(), errBuf.String())
|
|
}
|
|
for _, want := range []string{"⚠ Daemon", "läuft nicht", "⚠ Audio", "⚠ elevenlabs", "TALK_ELEVENLABS_KEY", "✔ edge", "✔ Mute", "✔ Gesetze", "Alles bereit"} {
|
|
if !strings.Contains(out.String(), want) {
|
|
t.Errorf("stdout ohne %q:\n%s", want, out.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDoctorJSONUndMute: --json liefert die Prüfliste; eine Mute-Datei
|
|
// macht Mute zu warn; ein Key macht die Stufe ok.
|
|
func TestDoctorJSONUndMute(t *testing.T) {
|
|
root := t.TempDir()
|
|
setGetenv(t, map[string]string{
|
|
"TALK_AUDIO_SINK": "null",
|
|
"XDG_CONFIG_HOME": root,
|
|
"TALK_ELEVENLABS_KEY": "k",
|
|
})
|
|
if err := os.MkdirAll(filepath.Join(root, "talk"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, "talk", "mute"), []byte("true\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var out, errBuf bytes.Buffer
|
|
if code := runCLI([]string{"doctor", "--json"}, &out, &errBuf); code != exitOK {
|
|
t.Fatalf("talk doctor --json: Exit %d, stderr %q", code, errBuf.String())
|
|
}
|
|
var checks []check
|
|
if err := json.Unmarshal(out.Bytes(), &checks); err != nil {
|
|
t.Fatalf("JSON = %q (%v)", out.String(), err)
|
|
}
|
|
if c := checkByName(t, checks, "Mute"); c.Status != statusWarn || !strings.Contains(c.Detail, "unmute") {
|
|
t.Errorf("Mute = %+v, want warn mit unmute-Hinweis", c)
|
|
}
|
|
if c := checkByName(t, checks, "elevenlabs"); c.Status != statusOK || !strings.Contains(c.Detail, "Rachel") {
|
|
t.Errorf("elevenlabs = %+v, want ok mit Default-Stimme", c)
|
|
}
|
|
if c := checkByName(t, checks, "Daemon"); c.Status != statusWarn {
|
|
t.Errorf("Daemon = %+v, want warn", c)
|
|
}
|
|
}
|
|
|
|
// TestDoctorFremderDienstAufPort: Antwortet unter dem Port etwas anderes als
|
|
// talk, ist das ✘ mit Port-Hinweis — Exit 1.
|
|
func TestDoctorFremderDienstAufPort(t *testing.T) {
|
|
fremd := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
t.Cleanup(fremd.Close)
|
|
port := fremd.URL[strings.LastIndex(fremd.URL, ":")+1:]
|
|
setGetenv(t, map[string]string{"TALK_AUDIO_SINK": "null", "TALK_PORT": port})
|
|
var out, errBuf bytes.Buffer
|
|
code := runCLI([]string{"doctor"}, &out, &errBuf)
|
|
if code != exitFailure {
|
|
t.Fatalf("talk doctor (fremder Dienst): Exit %d, want 1\n%s", code, out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "✘ Daemon") || !strings.Contains(out.String(), "fremder Dienst") || !strings.Contains(out.String(), "TALK_PORT") {
|
|
t.Errorf("stdout = %q", out.String())
|
|
}
|
|
}
|
|
|
|
// TestDoctorKaputteConfig: Eine unlesbare config.toml ist ✘, die übrigen
|
|
// Prüfungen laufen trotzdem (mit Defaults) — der Bericht bleibt vollständig.
|
|
func TestDoctorKaputteConfig(t *testing.T) {
|
|
root := t.TempDir()
|
|
setGetenv(t, map[string]string{"TALK_AUDIO_SINK": "null", "XDG_CONFIG_HOME": root})
|
|
if err := os.MkdirAll(filepath.Join(root, "talk"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, "talk", "config.toml"), []byte("audio = [broken\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var out, errBuf bytes.Buffer
|
|
code := runCLI([]string{"doctor"}, &out, &errBuf)
|
|
if code != exitFailure || !strings.Contains(out.String(), "✘ Config") {
|
|
t.Errorf("Exit %d, stdout %q — want ✘ Config und Exit 1", code, out.String())
|
|
}
|
|
for _, want := range []string{"Daemon", "edge", "Mute", "Gesetze"} {
|
|
if !strings.Contains(out.String(), want) {
|
|
t.Errorf("Prüfung %q fehlt trotz Config-Fehler:\n%s", want, out.String())
|
|
}
|
|
}
|
|
}
|