Files
talk/cmd/talk/control_test.go
T
m3ta-chironandClaude Fable 5.1 bc26a73aeb refactor: ein Binary — talk startet den Daemon selbst, talkd und Degraded Mode entfallen
Zwei Binaries erzeugten den halben Komplexitätsballast: Degraded Mode mit
Lock-Datei und Stale-Lock-Race, „läuft talkd?", Autostart-Konfiguration je
OS, doppelte Chain-Konstruktion in cmd/talk und cmd/talkd — und der
Degraded Mode kannte weder Rate-Limit noch Ruhezeiten.

Jetzt:
- internal/speak: der eine Sprech-Kern (Config + Sink + Kette → Ansage);
  Daemon und Direkt-Pfad benutzen ihn, es gibt keine zweite
  Zusammensetzung der Kette mehr.
- internal/daemon: Run (der bisherige talkd-Kern), Spawn (detached per
  Setsid bzw. DETACHED_PROCESS, stdout/stderr → daemon.log), PID-Datei
  daemon.pid, Shutdown-Trigger.
- `talk daemon run|start|stop`: Vordergrund (systemd/launchd), expliziter
  Hintergrund-Start, Stop per POST /shutdown — stop wartet, bis HTTP weg
  und die PID-Datei gelöscht ist (Prozess wirklich beendet).
- Autostart: say/wrap starten den Daemon beim ersten Sprechbefehl selbst
  (daemon.autostart, Default an; Env TALK_AUTOSTART). Verliert ein
  paralleler Aufruf das Port-Rennen, erkennt er das am Kind-Ende und nutzt
  den Daemon des anderen — kein Timeout-Warten.
- Direkt-Pfad statt Degraded Mode: ohne erreichbaren oder startbaren
  Daemon spricht talk direkt (Mute respektiert, kein Lock, Hinweis +
  Grund + Log-Pfad auf stderr). Steuerbefehle starten den Daemon bewusst
  nicht; `talk status` zeigt „läuft nicht" (Exit 1).
- cmd/talkd und cmd/talk/degraded.go entfernt; README-Daemon-Abschnitt
  neu.

Tests: Harness baut nur noch talk und startet `talk daemon run`;
TALK_AUTOSTART=false als Grundausstattung aller Tests (kein Test
hinterlässt Daemons), autostart_test.go schaltet es bewusst an: Autostart
beim ersten say, Idempotenz, paralleler Start in EINE Queue, expliziter
start/stop, Port fremd belegt → Direkt-Pfad ohne Timeout. Cross-Compile
aller sechs Targets mit CGO_ENABLED=0 verifiziert.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 09:49:16 +02:00

121 lines
4.2 KiB
Go

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
// (Exit 1) mit Hinweis auf stderr — und stdout nennt den Zustand trotzdem
// als Status-Zeile, damit der Block nie leer bleibt. Status startet den
// Daemon nicht.
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(), "kein Daemon") {
t.Errorf("stderr = %q, will Hinweis auf fehlenden Daemon", errBuf.String())
}
if !strings.HasPrefix(out.String(), "daemon: läuft nicht") {
t.Errorf("stdout = %q, will Status-Zeile „läuft nicht“", 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(), "spreche direkt") {
t.Errorf("stderr = %q, will Degraded-Hinweis", errBuf.String())
}
}