package main import ( "bytes" "os" "path/filepath" "strings" "testing" "code.m3ta.dev/m3tam3re/talk/internal/queue" ) // Diese Tests decken die Steuer-Befehle ohne Daemon ab (in-process über // die run-Seam; TALK_PORT=1 aus der setGetenv-Grundausstattung → Daemon- // Erkennung schlägt sofort fehl). Das Verhalten MIT Daemon testen die // Prozessgrenzen-Tests in /test. // TestStatusOhneDaemon: `talk status` ohne Daemon ist ein klarer Fehler // mit Hinweis (kein Stillversagen). func TestStatusOhneDaemon(t *testing.T) { setGetenv(t, nil) var out, errBuf bytes.Buffer code := runCLI([]string{"status"}, &out, &errBuf) if code != exitFailure { t.Fatalf("talk status ohne Daemon: Exit-Code = %d, want %d", code, exitFailure) } if !strings.Contains(errBuf.String(), "nicht erreichbar") { t.Errorf("stderr = %q, will Hinweis auf fehlenden Daemon", errBuf.String()) } if out.Len() != 0 { t.Errorf("stdout = %q, will leer", out.String()) } } // TestCancelOhneDaemon: ohne Daemon gibt es nichts zu abbrechen — klarer // Fehler. func TestCancelOhneDaemon(t *testing.T) { setGetenv(t, nil) var out, errBuf bytes.Buffer code := runCLI([]string{"cancel"}, &out, &errBuf) if code != exitFailure { t.Fatalf("talk cancel ohne Daemon: Exit-Code = %d, want %d", code, exitFailure) } if !strings.Contains(errBuf.String(), "nichts zu abbrechen") { t.Errorf("stderr = %q, will Hinweis", errBuf.String()) } } // TestMuteOhneDaemonSchreibtDatei: `talk mute` ohne Daemon schreibt die // Mute-Datei direkt — und genau diese Datei respektiert der Degraded Mode // von say anschließend. func TestMuteOhneDaemonSchreibtDatei(t *testing.T) { root := t.TempDir() setGetenv(t, map[string]string{"XDG_CONFIG_HOME": root}) mutePath := filepath.Join(root, "talk", queue.MuteFileName) var out, errBuf bytes.Buffer if code := runCLI([]string{"mute"}, &out, &errBuf); code != exitOK { t.Fatalf("talk mute ohne Daemon: Exit-Code = %d, want %d (stderr: %q)", code, exitOK, errBuf.String()) } if got := out.String(); got != "muted\n" { t.Errorf("talk mute stdout = %q, want %q", got, "muted\n") } if !strings.Contains(errBuf.String(), "direkt") { t.Errorf("stderr = %q, will Hinweis auf direkten Datei-Schreib", errBuf.String()) } b, err := os.ReadFile(mutePath) if err != nil { t.Fatalf("Mute-Datei fehlt: %v", err) } if strings.TrimSpace(string(b)) != "true" { t.Errorf("Mute-Datei = %q, want true", b) } // say im Degraded Mode respektiert dieselbe Datei (tonlos verworfen, // ohne irgendeinen Provider zu brauchen). out.Reset() errBuf.Reset() if code := runCLI([]string{"say", "stille bitte"}, &out, &errBuf); code != exitOK { t.Fatalf("talk say (mute an): Exit-Code = %d, want %d (stderr: %q)", code, exitOK, errBuf.String()) } if got := out.String(); got != "muted\n" { t.Errorf("talk say (mute an) stdout = %q, want %q", got, "muted\n") } // unmute hebt es auf (Datei false). out.Reset() errBuf.Reset() if code := runCLI([]string{"unmute"}, &out, &errBuf); code != exitOK { t.Fatalf("talk unmute: Exit-Code = %d (stderr: %q)", code, errBuf.String()) } if got := out.String(); got != "unmuted\n" { t.Errorf("talk unmute stdout = %q, want %q", got, "unmuted\n") } b, err = os.ReadFile(mutePath) if err != nil || strings.TrimSpace(string(b)) != "false" { t.Errorf("Mute-Datei nach unmute = %q (%v), want false", b, err) } } // TestSayOhneConfigDir: Degraded Mode ohne Config-Dir (kein HOME) spielt // weiter statt zu scheitern — Best-effort ohne Lock und Mute. func TestSayOhneConfigDir(t *testing.T) { seen := withFakeEspeak(t) setGetenv(t, map[string]string{"TALK_AUDIO_SINK": "null"}) var out, errBuf bytes.Buffer code := runCLI([]string{"Hallo"}, &out, &errBuf) if code != exitOK { t.Fatalf("talk ohne HOME: Exit-Code = %d, want %d; stderr = %q", code, exitOK, errBuf.String()) } if _, err := os.Stat(seen); err != nil { t.Errorf("fake espeak nicht gerufen: %v", err) } if !strings.Contains(errBuf.String(), "Degraded Mode") { t.Errorf("stderr = %q, will Degraded-Hinweis", errBuf.String()) } }