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>
This commit is contained in:
1 parent
b86632006e
commit
bc26a73aeb
29 files changed
+1145
-584
No files matched your search
@@ -0,0 +1,215 @@
|
||||
package test
|
||||
|
||||
// Prozessgrenzen-Tests für das Ein-Binary-Modell: talk startet den Daemon
|
||||
// beim ersten Sprechbefehl selbst (daemon.autostart), parallele Aufrufe
|
||||
// landen in EINER Queue, `talk daemon start|stop` steuern ihn explizit, und
|
||||
// ein nicht startbarer Daemon (Port von einem fremden Dienst belegt) führt
|
||||
// in den Direkt-Pfad statt ins Schweigen.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.m3ta.dev/m3tam3re/talk/internal/api"
|
||||
"code.m3ta.dev/m3tam3re/talk/internal/daemon"
|
||||
)
|
||||
|
||||
var itemID = regexp.MustCompile(`^[0-9a-f]{6}\n$`)
|
||||
|
||||
// autostartEnv ist daemonEnv mit eingeschaltetem Autostart auf einem
|
||||
// freien Port — und einem Cleanup, das den gespawnten Daemon garantiert
|
||||
// wieder einsammelt (POST /shutdown; Notbremse: PID-Datei + Kill), damit
|
||||
// kein Testlauf Hintergrundprozesse hinterlässt.
|
||||
func autostartEnv(t *testing.T, root, fakeURL string, extra map[string]string) ([]string, *api.Client) {
|
||||
t.Helper()
|
||||
port := freePort(t)
|
||||
set := map[string]string{"TALK_AUTOSTART": "true"}
|
||||
for k, v := range extra {
|
||||
set[k] = v
|
||||
}
|
||||
env := daemonEnv(root, port, fakeURL, set)
|
||||
client := api.NewClient(port)
|
||||
t.Cleanup(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
_ = client.Shutdown(ctx)
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if _, err := client.Status(ctx); err != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
if pid := daemon.ReadPID(configDir(root)); pid > 0 {
|
||||
if p, err := os.FindProcess(pid); err == nil {
|
||||
_ = p.Kill()
|
||||
}
|
||||
}
|
||||
})
|
||||
return env, client
|
||||
}
|
||||
|
||||
// TestAutostartStartetDaemon (Abnahmekriterium Ein-Binary): ohne laufenden
|
||||
// Daemon startet `talk say` ihn selbst, meldet das auf stderr, liefert eine
|
||||
// Item-ID auf stdout, und die Ansage läuft durch Queue und Fake-Provider.
|
||||
// Danach ist der Daemon da (status), schreibt Log und PID, und `talk daemon
|
||||
// stop` beendet ihn wieder.
|
||||
func TestAutostartStartetDaemon(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 0)
|
||||
env, client := autostartEnv(t, root, fake.srv.URL, nil)
|
||||
|
||||
res := runTalk(t, env, "say", "erster aufruf startet")
|
||||
if res.code != 0 {
|
||||
t.Fatalf("talk say: Exit %d, stderr: %s", res.code, res.stderr)
|
||||
}
|
||||
if !itemID.MatchString(res.stdout) {
|
||||
t.Errorf("stdout = %q, want Item-ID (Daemon-Pfad, nicht direkt)", res.stdout)
|
||||
}
|
||||
if !strings.Contains(res.stderr, "Daemon gestartet") {
|
||||
t.Errorf("stderr = %q, will Autostart-Hinweis", res.stderr)
|
||||
}
|
||||
if hits := fake.waitCompleted(t, 1); hits[0].Text != "erster aufruf startet" {
|
||||
t.Errorf("Treffer = %q", hits[0].Text)
|
||||
}
|
||||
|
||||
// Zweiter Aufruf: Daemon läuft schon — kein zweiter Start.
|
||||
res = runTalk(t, env, "say", "zweiter aufruf")
|
||||
if res.code != 0 || strings.Contains(res.stderr, "Daemon gestartet") {
|
||||
t.Errorf("zweiter Aufruf: Exit %d, stderr %q — darf nicht erneut starten", res.code, res.stderr)
|
||||
}
|
||||
fake.waitCompleted(t, 2)
|
||||
|
||||
// Der Daemon ist sichtbar: status, Log- und PID-Datei.
|
||||
res = runTalk(t, env, "status")
|
||||
if res.code != 0 || !strings.Contains(res.stdout, "daemon: http://127.0.0.1:") {
|
||||
t.Errorf("talk status: Exit %d, stdout %q", res.code, res.stdout)
|
||||
}
|
||||
if _, err := os.Stat(configDir(root) + "/" + daemon.LogFileName); err != nil {
|
||||
t.Errorf("daemon.log fehlt: %v", err)
|
||||
}
|
||||
if pid := daemon.ReadPID(configDir(root)); pid <= 0 {
|
||||
t.Errorf("daemon.pid fehlt oder leer")
|
||||
}
|
||||
|
||||
// stop fährt ihn herunter; danach läuft keiner mehr, PID-Datei weg.
|
||||
res = runTalk(t, env, "daemon", "stop")
|
||||
if res.code != 0 || res.stdout != "daemon: gestoppt\n" {
|
||||
t.Fatalf("talk daemon stop: Exit %d, stdout %q, stderr %s", res.code, res.stdout, res.stderr)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if _, err := client.Status(ctx); err == nil {
|
||||
t.Errorf("Daemon antwortet nach stop noch")
|
||||
}
|
||||
if pid := daemon.ReadPID(configDir(root)); pid != 0 {
|
||||
t.Errorf("daemon.pid nach stop noch da (%d)", pid)
|
||||
}
|
||||
// Nochmal stop: idempotent.
|
||||
res = runTalk(t, env, "daemon", "stop")
|
||||
if res.code != 0 || !strings.Contains(res.stdout, "läuft nicht") {
|
||||
t.Errorf("zweites stop: Exit %d, stdout %q", res.code, res.stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAutostartSerialisiertParallele: zwei gleichzeitige talk-Aufrufe ohne
|
||||
// Daemon — einer startet ihn, der andere findet ihn (oder verliert das
|
||||
// Port-Rennen und findet ihn trotzdem). Beide Ansagen laufen durch EINE
|
||||
// Queue: nacheinander, nicht überlappend.
|
||||
func TestAutostartSerialisiertParallele(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 250*time.Millisecond)
|
||||
env, _ := autostartEnv(t, root, fake.srv.URL, nil)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
results := make([]talkResult, 2)
|
||||
for i, text := range []string{"eins", "zwei"} {
|
||||
wg.Add(1)
|
||||
go func(i int, text string) {
|
||||
defer wg.Done()
|
||||
results[i] = runTalk(t, env, "say", "--wait", text)
|
||||
}(i, text)
|
||||
}
|
||||
wg.Wait()
|
||||
for i, res := range results {
|
||||
if res.code != 0 {
|
||||
t.Fatalf("talk %d (parallel, autostart): Exit %d, stderr: %s", i, res.code, res.stderr)
|
||||
}
|
||||
if !itemID.MatchString(res.stdout) {
|
||||
t.Errorf("talk %d stdout = %q, want Item-ID (beide über den Daemon)", i, res.stdout)
|
||||
}
|
||||
}
|
||||
hits := fake.waitHits(t, 2)
|
||||
const slack = 25 * time.Millisecond
|
||||
if hits[1].Start.Before(hits[0].End.Add(-slack)) {
|
||||
t.Errorf("Ansagen überlappten: Start(2)=%v < Ende(1)=%v — nicht EINE Queue", hits[1].Start, hits[0].End)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDaemonStartExplizit: `talk daemon start` startet im Hintergrund und
|
||||
// ist idempotent; der Port steht in der Meldung.
|
||||
func TestDaemonStartExplizit(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 0)
|
||||
env, client := autostartEnv(t, root, fake.srv.URL, map[string]string{"TALK_AUTOSTART": "false"})
|
||||
|
||||
res := runTalk(t, env, "daemon", "start")
|
||||
if res.code != 0 || res.stdout != "daemon: gestartet unter "+client.BaseURL+"\n" {
|
||||
t.Fatalf("talk daemon start: Exit %d, stdout %q, stderr %s", res.code, res.stdout, res.stderr)
|
||||
}
|
||||
res = runTalk(t, env, "daemon", "start")
|
||||
if res.code != 0 || !strings.Contains(res.stdout, "läuft bereits") {
|
||||
t.Errorf("zweites start: Exit %d, stdout %q", res.code, res.stdout)
|
||||
}
|
||||
// Autostart aus, aber Daemon da: say nutzt ihn.
|
||||
res = runTalk(t, env, "say", "über explizit gestarteten daemon")
|
||||
if res.code != 0 || !itemID.MatchString(res.stdout) {
|
||||
t.Errorf("say: Exit %d, stdout %q", res.code, res.stdout)
|
||||
}
|
||||
fake.waitCompleted(t, 1)
|
||||
}
|
||||
|
||||
// TestAutostartScheitertSprichtDirekt: Der Port gehört einem fremden Dienst
|
||||
// (antwortet nicht wie talk) — der gespawnte Daemon kann nicht binden und
|
||||
// endet sofort; talk erkennt das am Kind-Ende (kein Timeout-Warten) und
|
||||
// spricht direkt. Der Benutzer sieht den Grund und den Log-Pfad.
|
||||
func TestAutostartScheitertSprichtDirekt(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 0)
|
||||
fremd := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
t.Cleanup(fremd.Close)
|
||||
port, _ := strconv.Atoi(fremd.URL[strings.LastIndex(fremd.URL, ":")+1:])
|
||||
env := daemonEnv(root, port, fake.srv.URL, map[string]string{"TALK_AUTOSTART": "true"})
|
||||
|
||||
start := time.Now()
|
||||
res := runTalk(t, env, "say", "port ist fremd belegt")
|
||||
if res.code != 0 {
|
||||
t.Fatalf("talk say: Exit %d, stderr: %s", res.code, res.stderr)
|
||||
}
|
||||
if d := time.Since(start); d > 4*time.Second {
|
||||
t.Errorf("Dauer %v — Start-Fehlschlag soll am Kind-Ende erkannt werden, nicht per Timeout", d)
|
||||
}
|
||||
if !strings.Contains(res.stderr, "Daemon-Start fehlgeschlagen") || !strings.Contains(res.stderr, "spreche direkt") {
|
||||
t.Errorf("stderr = %q, will Fehlschlag-Grund und Direkt-Hinweis", res.stderr)
|
||||
}
|
||||
if !strings.Contains(res.stderr, daemon.LogFileName) {
|
||||
t.Errorf("stderr = %q, will Log-Pfad-Hinweis", res.stderr)
|
||||
}
|
||||
if hits := fake.waitHits(t, 1); hits[0].Text != "port ist fremd belegt" {
|
||||
t.Errorf("Treffer = %q", hits[0].Text)
|
||||
}
|
||||
logBytes, err := os.ReadFile(configDir(root) + "/" + daemon.LogFileName)
|
||||
if err != nil || !strings.Contains(string(logBytes), "Port belegen") {
|
||||
t.Errorf("daemon.log = %q (%v), will Port-belegt-Fehler", logBytes, err)
|
||||
}
|
||||
}
|
||||
+14
-50
@@ -64,68 +64,32 @@ func TestSayWait(t *testing.T) {
|
||||
waitStatusIdle(t, d, 3*time.Second)
|
||||
}
|
||||
|
||||
// TestSayDegradedMode (Abnahmekriterium Degraded Mode): Daemon tot →
|
||||
// Lock-Datei im Config-Dir, sequenzielles lokales Abspielen (hier: direkt
|
||||
// gegen den Fake-Provider), Lock danach wieder freigegeben.
|
||||
func TestSayDegradedMode(t *testing.T) {
|
||||
// TestSayOhneDaemonSprichtDirekt: Daemon tot und Autostart aus → talk
|
||||
// spricht direkt im eigenen Prozess (hier: gegen den Fake-Provider) und
|
||||
// sagt das auf stderr — nichts bricht still.
|
||||
func TestSayOhneDaemonSprichtDirekt(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 0)
|
||||
env := daemonEnv(root, 1, fake.srv.URL, nil) // Port 1: Daemon garantiert tot
|
||||
|
||||
res := runTalk(t, env, "say", "offline ansage")
|
||||
if res.code != 0 {
|
||||
t.Fatalf("talk say (Degraded): Exit %d, stderr: %s", res.code, res.stderr)
|
||||
t.Fatalf("talk say (direkt): Exit %d, stderr: %s", res.code, res.stderr)
|
||||
}
|
||||
if hits := fake.waitHits(t, 1); hits[0].Text != "offline ansage" {
|
||||
t.Errorf("Treffer = %q", hits[0].Text)
|
||||
}
|
||||
if !strings.Contains(res.stderr, "Degraded Mode") {
|
||||
t.Errorf("stderr = %q, will Degraded-Hinweis (nichts bricht still)", res.stderr)
|
||||
if !strings.Contains(res.stderr, "spreche direkt") {
|
||||
t.Errorf("stderr = %q, will Direkt-Hinweis (nichts bricht still)", res.stderr)
|
||||
}
|
||||
|
||||
// Lock-Datei wurde nach dem Abspielen freigegeben.
|
||||
lockPath := filepath.Join(configDir(root), "talk.lock")
|
||||
if _, err := os.Stat(lockPath); !os.IsNotExist(err) {
|
||||
t.Errorf("Lock-Datei %s nicht freigegeben (err=%v)", lockPath, err)
|
||||
if strings.Contains(res.stderr, "Daemon gestartet") {
|
||||
t.Errorf("stderr = %q — Autostart war aus, es darf kein Daemon gestartet werden", res.stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSayDegradedSerialisiertUeberLock: zwei gleichzeitige talk-Prozesse
|
||||
// ohne Daemon spielen nacheinander (Kollision = warten), nicht überlappend.
|
||||
func TestSayDegradedSerialisiertUeberLock(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 250*time.Millisecond)
|
||||
env := daemonEnv(root, 1, fake.srv.URL, nil)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
results := make([]talkResult, 2)
|
||||
for i, text := range []string{"eins", "zwei"} {
|
||||
wg.Add(1)
|
||||
go func(i int, text string) {
|
||||
defer wg.Done()
|
||||
results[i] = runTalk(t, env, "say", text)
|
||||
}(i, text)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for i, res := range results {
|
||||
if res.code != 0 {
|
||||
t.Fatalf("talk %d (Degraded, parallel): Exit %d, stderr: %s", i, res.code, res.stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// Reihenfolge offen (wer zuerst das Lock nimmt), aber serialisiert:
|
||||
// die zweite Synthese startet erst nach der Antwort der ersten.
|
||||
hits := fake.waitHits(t, 2)
|
||||
const slack = 25 * time.Millisecond
|
||||
if hits[1].Start.Before(hits[0].End.Add(-slack)) {
|
||||
t.Errorf("Degraded-Ansagen überlappten: Start(2)=%v < Ende(1)=%v — Lock-Serialisierung kaputt", hits[1].Start, hits[0].End)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSayDegradedRespektiertMute: ohne Daemon verwirft der Degraded Mode
|
||||
// TestSayOhneDaemonRespektiertMute: ohne Daemon verwirft der Direkt-Pfad
|
||||
// Ansagen laut Mute-Datei tonlos (und spielt nach Unmute wieder).
|
||||
func TestSayDegradedRespektiertMute(t *testing.T) {
|
||||
func TestSayOhneDaemonRespektiertMute(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 0)
|
||||
env := daemonEnv(root, 1, fake.srv.URL, nil)
|
||||
@@ -279,14 +243,14 @@ func TestDreiParalleleTalkAufrufe(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestCliSteuerbefehleOhneDaemon: status/cancel ohne Daemon sind klare
|
||||
// Fehler (kein Stillversagen) — say fällt dagegen in den Degraded Mode.
|
||||
// Fehler (kein Stillversagen) — say spricht dagegen direkt.
|
||||
func TestCliSteuerbefehleOhneDaemon(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 0)
|
||||
env := daemonEnv(root, 1, fake.srv.URL, nil)
|
||||
|
||||
if res := runTalk(t, env, "status"); res.code != 1 || !strings.Contains(res.stderr, "nicht erreichbar") {
|
||||
t.Errorf("talk status ohne Daemon: %d %q", res.code, res.stderr)
|
||||
if res := runTalk(t, env, "status"); res.code != 1 || !strings.Contains(res.stderr, "kein Daemon") || !strings.Contains(res.stdout, "läuft nicht") {
|
||||
t.Errorf("talk status ohne Daemon: %d stdout %q stderr %q", res.code, res.stdout, res.stderr)
|
||||
}
|
||||
if res := runTalk(t, env, "cancel"); res.code != 1 || !strings.Contains(res.stderr, "nichts zu abbrechen") {
|
||||
t.Errorf("talk cancel ohne Daemon: %d %q", res.code, res.stderr)
|
||||
|
||||
+16
-23
@@ -29,10 +29,7 @@ import (
|
||||
|
||||
// Die Binaries werden einmal pro Testlauf gebaut (Build-Cache macht es
|
||||
// billig) und von allen Tests geteilt.
|
||||
var (
|
||||
talkBin string
|
||||
talkdBin string
|
||||
)
|
||||
var talkBin string
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
dir, err := os.MkdirTemp("", "talk-bins-*")
|
||||
@@ -45,19 +42,14 @@ func TestMain(m *testing.M) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
for _, b := range []struct{ out, pkg string }{
|
||||
{"talk", "./cmd/talk"},
|
||||
{"talkd", "./cmd/talkd"},
|
||||
} {
|
||||
cmd := exec.Command("go", "build", "-o", filepath.Join(dir, b.out), b.pkg)
|
||||
cmd.Dir = repo
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "go build %s: %v\n%s", b.pkg, err, out)
|
||||
os.RemoveAll(dir)
|
||||
os.Exit(1)
|
||||
}
|
||||
build := exec.Command("go", "build", "-o", filepath.Join(dir, "talk"), "./cmd/talk")
|
||||
build.Dir = repo
|
||||
if out, err := build.CombinedOutput(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "go build ./cmd/talk: %v\n%s", err, out)
|
||||
os.RemoveAll(dir)
|
||||
os.Exit(1)
|
||||
}
|
||||
talkBin, talkdBin = filepath.Join(dir, "talk"), filepath.Join(dir, "talkd")
|
||||
talkBin = filepath.Join(dir, "talk")
|
||||
|
||||
code := m.Run()
|
||||
os.RemoveAll(dir)
|
||||
@@ -254,19 +246,19 @@ type daemonProc struct {
|
||||
baseURL string
|
||||
}
|
||||
|
||||
// startDaemon startet talkd als echten Prozess (freier Port per TALK_PORT,
|
||||
// nie der Default-Port) und wartet, bis GET /status antwortet. Stop per
|
||||
// startDaemon startet `talk daemon run` als echten Prozess (freier Port per
|
||||
// TALK_PORT, nie der Default-Port) und wartet, bis GET /status antwortet. Stop per
|
||||
// t.Cleanup: SIGINT, nach Frist Kill.
|
||||
func startDaemon(t *testing.T, root, fakeURL string, extra map[string]string) *daemonProc {
|
||||
t.Helper()
|
||||
port := freePort(t)
|
||||
env := daemonEnv(root, port, fakeURL, extra)
|
||||
stderr := &syncBuffer{}
|
||||
cmd := exec.Command(talkdBin)
|
||||
cmd := exec.Command(talkBin, "daemon", "run")
|
||||
cmd.Env = env
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("talkd starten: %v", err)
|
||||
t.Fatalf("talk daemon run starten: %v", err)
|
||||
}
|
||||
d := &daemonProc{
|
||||
cmd: cmd,
|
||||
@@ -288,7 +280,7 @@ func startDaemon(t *testing.T, root, fakeURL string, extra map[string]string) *d
|
||||
}
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("talkd antwortet nicht auf %s — stderr:\n%s", d.baseURL+"/status", stderr.String())
|
||||
t.Fatalf("talk daemon antwortet nicht auf %s — stderr:\n%s", d.baseURL+"/status", stderr.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -310,7 +302,7 @@ func (d *daemonProc) stop(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// daemonEnv baut die Prozess-Umgebung für talk- und talkd-Prozesse:
|
||||
// daemonEnv baut die Prozess-Umgebung für talk-Prozesse (CLI und Daemon):
|
||||
// geerbtes Environment ohne fremde TALK_*-Variablen (Isolation von Keys/
|
||||
// Ports der Außenwelt), isoliertes HOME/XDG_CONFIG_HOME (Config-Dir liegt
|
||||
// damit unter <root>/talk), audio.sink=null und der Fake-Provider als
|
||||
@@ -338,7 +330,8 @@ func daemonEnv(root string, port int, fakeURL string, extra map[string]string) [
|
||||
"TALK_ELEVENLABS_ENDPOINT": fakeURL,
|
||||
"TALK_EDGE_ENDPOINT": "ws://127.0.0.1:1/", // Kette soll nie auf echte Fremd-Endpunkte fallen
|
||||
"TALK_PORT": strconv.Itoa(port),
|
||||
"TALK_RATE_LIMIT": "0s", // Gesetz aus, bis ein Test es anstellt (siehe oben)
|
||||
"TALK_RATE_LIMIT": "0s", // Gesetz aus, bis ein Test es anstellt (siehe oben)
|
||||
"TALK_AUTOSTART": "false", // kein Test darf unbemerkt einen Daemon hinterlassen; autostart_test.go schaltet es bewusst an
|
||||
}
|
||||
for k, v := range extra {
|
||||
set[k] = v
|
||||
|
||||
+2
-2
@@ -68,10 +68,10 @@ func TestMutePersistenzUeberRestart(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDegradedMuteDateiWirdVonDaemonGelesen: eine offline (CLI, ohne
|
||||
// TestOfflineMuteDateiWirdVonDaemonGelesen: eine offline (CLI, ohne
|
||||
// Daemon) geschriebene Mute-Datei wirkt sofort im Daemon — beide lesen
|
||||
// dieselbe Quelle.
|
||||
func TestDegradedMuteDateiWirdVonDaemonGelesen(t *testing.T) {
|
||||
func TestOfflineMuteDateiWirdVonDaemonGelesen(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 0)
|
||||
d := startDaemon(t, root, fake.srv.URL, nil)
|
||||
|
||||
+6
-6
@@ -147,22 +147,22 @@ func TestWrapWait(t *testing.T) {
|
||||
waitStatusIdle(t, d, 3*time.Second)
|
||||
}
|
||||
|
||||
// TestWrapDegradedMode: Daemon tot → Ansage im Degraded Mode (Lock-Datei,
|
||||
// direkt gegen den Fake), und der Exit-Code des Befehls bleibt trotzdem.
|
||||
func TestWrapDegradedMode(t *testing.T) {
|
||||
// TestWrapOhneDaemon: Daemon tot, Autostart aus → Ansage direkt gegen den
|
||||
// Fake, und der Exit-Code des Befehls bleibt trotzdem.
|
||||
func TestWrapOhneDaemon(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 0)
|
||||
env := daemonEnv(root, 1, fake.srv.URL, nil) // Port 1: Daemon garantiert tot
|
||||
|
||||
res := runTalk(t, env, "wrap", "--", "sh", "-c", "exit 3")
|
||||
if res.code != 3 {
|
||||
t.Fatalf("talk wrap (Degraded): Exit %d, want 3 (Befehls-Code gewinnt)", res.code)
|
||||
t.Fatalf("talk wrap (direkt): Exit %d, want 3 (Befehls-Code gewinnt)", res.code)
|
||||
}
|
||||
hits := fake.waitCompleted(t, 1)
|
||||
if hits[0].Text != "sh fehlgeschlagen, Code 3." {
|
||||
t.Errorf("Ansage = %q, want %q", hits[0].Text, "sh fehlgeschlagen, Code 3.")
|
||||
}
|
||||
if !strings.Contains(res.stderr, "Degraded Mode") {
|
||||
t.Errorf("stderr = %q, will Degraded-Hinweis (nichts bricht still)", res.stderr)
|
||||
if !strings.Contains(res.stderr, "spreche direkt") {
|
||||
t.Errorf("stderr = %q, will Direkt-Hinweis (nichts bricht still)", res.stderr)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user