Files
talk/test/mute_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

104 lines
3.3 KiB
Go

package test
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// TestMutePersistenzUeberRestart (Abnahmekriterium): `talk mute` → Daemon-
// Restart → weiter stumm. Mute verwirft Ansagen dokumentiert mit
// 202 {id:"", muted:true} und ohne jeden Provider-Kontakt.
func TestMutePersistenzUeberRestart(t *testing.T) {
root := t.TempDir()
fake := newFakeProvider(t, 0)
d := startDaemon(t, root, fake.srv.URL, nil)
// Über die HTTP-API stummschalten: Datei mute liegt im Config-Dir.
code, m := postPath(t, d, "/mute")
if code != 200 || m["muted"] != true {
t.Fatalf("POST /mute: %d %v", code, m)
}
muteFile := filepath.Join(configDir(root), "mute")
b, err := os.ReadFile(muteFile)
if err != nil {
t.Fatalf("Mute-Datei fehlt im Config-Dir: %v", err)
}
if strings.TrimSpace(string(b)) != "true" {
t.Errorf("Mute-Datei = %q, want true", b)
}
// Ansagen werden verworfen: 202, muted:true, leere ID, kein Treffer.
code, m = speak(t, d, `{"text":"stille"}`)
if code != 202 || m["muted"] != true || m["id"] != "" {
t.Fatalf("speak bei Mute: %d %v, want 202 {id:, muted:true}", code, m)
}
fake.assertNoHits(t, 300*time.Millisecond)
if st := getStatus(t, d); !st.Muted {
t.Fatal("/status meldet muted=false")
}
// Daemon-Restart mit demselben Config-Dir: weiter stumm.
d.stop(t)
d2 := startDaemon(t, root, fake.srv.URL, nil)
if st := getStatus(t, d2); !st.Muted {
t.Fatalf("nach Restart muted = false, want true (persistent)")
}
code, m = speak(t, d2, `{"text":"immer noch stumm"}`)
if code != 202 || m["muted"] != true {
t.Fatalf("speak nach Restart bei Mute: %d %v", code, m)
}
fake.assertNoHits(t, 300*time.Millisecond)
// Unmute: Ansagen laufen wieder.
code, m = postPath(t, d2, "/unmute")
if code != 200 || m["muted"] != false {
t.Fatalf("POST /unmute: %d %v", code, m)
}
code, m = speak(t, d2, `{"text":"wieder da"}`)
if code != 202 || m["id"] == "" {
t.Fatalf("speak nach unmute: %d %v, want 202 mit id", code, m)
}
hits := fake.waitHits(t, 1)
if hits[0].Text != "wieder da" {
t.Errorf("Treffer = %q, want wieder da", hits[0].Text)
}
}
// TestOfflineMuteDateiWirdVonDaemonGelesen: eine offline (CLI, ohne
// Daemon) geschriebene Mute-Datei wirkt sofort im Daemon — beide lesen
// dieselbe Quelle.
func TestOfflineMuteDateiWirdVonDaemonGelesen(t *testing.T) {
root := t.TempDir()
fake := newFakeProvider(t, 0)
d := startDaemon(t, root, fake.srv.URL, nil)
deadEnv := daemonEnv(root, 1, fake.srv.URL, nil)
// Offline stummschalten (talk mute ohne Daemon → Datei direkt).
res := runTalk(t, deadEnv, "mute")
if res.code != 0 || res.stdout != "muted\n" {
t.Fatalf("talk mute (offline): %d %q — stderr: %s", res.code, res.stdout, res.stderr)
}
// Der laufende Daemon sieht den Zustand bei der nächsten Ansage.
if st := getStatus(t, d); !st.Muted {
t.Fatal("Daemon sieht offline geschriebene Mute-Datei nicht")
}
code, m := speak(t, d, `{"text":"soll verworfen werden"}`)
if code != 202 || m["muted"] != true {
t.Fatalf("speak: %d %v, want verworfen", code, m)
}
fake.assertNoHits(t, 300*time.Millisecond)
res = runTalk(t, deadEnv, "unmute")
if res.code != 0 {
t.Fatalf("talk unmute (offline): %d — stderr: %s", res.code, res.stderr)
}
if st := getStatus(t, d); st.Muted {
t.Fatal("Daemon sieht offline geschriebene Unmute-Datei nicht")
}
}