test: Prozessgrenzen-Tests für talk wrap an echter CLI + Daemon

talk läuft als echter Prozess gegen talkd mit Fake-Provider und sink null
(Harness aus Task 04, unverändert erweitert): Erfolg mit Dauer
(sleep-Ansage), Exit-Code-Durchreichung (sh -c 'exit 2' → 2 +
Fehler-Ansage wortgenau), --bar-Shielding-Regression (true --bar →
Exit 0 + Erfolg-Ansage, kein talk-Flag-Fehler), Label-Override per
TALK_WRAP_LABEL, non-blocking (wrap kehrt vor Ansage-Ende zurück,
Befehls-stdout bleibt frei von talk-Zeilen), --wait und Degraded Mode
ohne Daemon (Befehls-Code bleibt).
This commit is contained in:
m3ta-chiron committed 2026-10-18 21:01:13 +02:00
1 parent 51cd5a686c
commit 6877d64042
1 file changed
+168
+168
View File
@@ -0,0 +1,168 @@
// wrap-Prozessgrenzen-Tests (Task 07): talk läuft als echter Prozess,
// talkd als echter Daemon, Fake-Provider + sink null wie im Harness —
// beobachtet wird nur extern: Exit-Codes, Befehls-stdout, Ansage-Text am
// Fake. Die gewrappten Befehle sind echte Binaries (sleep/sh/true), die
// Dauern bewusst klein.
package test
import (
"regexp"
"strings"
"testing"
"time"
)
// TestWrapErfolgMitDauer (Abnahmekriterium): `talk wrap -- sleep 1`
// annonciert Erfolg inklusive Dauer und exitet 0.
func TestWrapErfolgMitDauer(t *testing.T) {
root := t.TempDir()
fake := newFakeProvider(t, 0)
d := startDaemon(t, root, fake.srv.URL, nil)
env := daemonEnv(root, d.port, fake.srv.URL, nil)
res := runTalk(t, env, "wrap", "--", "sleep", "1")
if res.code != 0 {
t.Fatalf("talk wrap -- sleep 1: Exit %d, stderr: %s", res.code, res.stderr)
}
hits := fake.waitCompleted(t, 1)
// sleep 1 misst ~1 s (unter Last gern mehr) — Zahl offen, Form nicht.
if !regexp.MustCompile(`^sleep erfolgreich\. Dauer: \d+ Sekunden?\.$`).MatchString(hits[0].Text) {
t.Errorf("Ansage = %q, will „sleep erfolgreich. Dauer: N Sekunde(n).“", hits[0].Text)
}
waitStatusIdle(t, d, 3*time.Second)
}
// TestWrapFehlschlagCodeDurchgereicht (Abnahmekriterium): scheitert der
// Befehl mit Code 2, exitet wrap mit 2 und sagt den Misserfolg mit Code —
// ohne Dauer, wortgenau nach Brief.
func TestWrapFehlschlagCodeDurchgereicht(t *testing.T) {
root := t.TempDir()
fake := newFakeProvider(t, 0)
d := startDaemon(t, root, fake.srv.URL, nil)
env := daemonEnv(root, d.port, fake.srv.URL, nil)
res := runTalk(t, env, "wrap", "--", "sh", "-c", "exit 2")
if res.code != 2 {
t.Fatalf("talk wrap -- sh -c 'exit 2': Exit %d, want 2 (Durchreichung)", res.code)
}
hits := fake.waitCompleted(t, 1)
if hits[0].Text != "sh fehlgeschlagen, Code 2." {
t.Errorf("Ansage = %q, want %q", hits[0].Text, "sh fehlgeschlagen, Code 2.")
}
waitStatusIdle(t, d, 3*time.Second)
}
// TestWrapFlagShielding (Regression, Task-07-Kern): `talk wrap -- true
// --bar` reicht --bar an den Befehl weiter — talk darf --bar NICHT als
// eigenes Flag parsen (wäre Exit 2 mit Flag-Fehler). GNU true ignoriert
// Operanden, exitet also 0, und die Erfolg-Ansage läuft.
func TestWrapFlagShielding(t *testing.T) {
root := t.TempDir()
fake := newFakeProvider(t, 0)
d := startDaemon(t, root, fake.srv.URL, nil)
env := daemonEnv(root, d.port, fake.srv.URL, nil)
res := runTalk(t, env, "wrap", "--", "true", "--bar")
if res.code != 0 {
t.Fatalf("talk wrap -- true --bar: Exit %d, want 0 (--bar gehört dem Befehl) — stderr: %s", res.code, res.stderr)
}
if strings.Contains(res.stderr, "flag provided but not defined") {
t.Errorf("talk hat --bar als eigenes Flag geparsed — Shielding kaputt: %s", res.stderr)
}
hits := fake.waitCompleted(t, 1)
if !strings.HasPrefix(hits[0].Text, "true erfolgreich.") {
t.Errorf("Ansage = %q, will Erfolg für true", hits[0].Text)
}
waitStatusIdle(t, d, 3*time.Second)
}
// TestWrapLabelOverride: TALK_WRAP_LABEL ersetzt den Basisnamen in der
// Ansage (Fleet-Muster: Env statt Config-Datei).
func TestWrapLabelOverride(t *testing.T) {
root := t.TempDir()
fake := newFakeProvider(t, 0)
d := startDaemon(t, root, fake.srv.URL, nil)
env := daemonEnv(root, d.port, fake.srv.URL, map[string]string{"TALK_WRAP_LABEL": "Deployment"})
res := runTalk(t, env, "wrap", "--", "false")
if res.code != 1 {
t.Fatalf("talk wrap -- false: Exit %d, want 1", res.code)
}
hits := fake.waitCompleted(t, 1)
if hits[0].Text != "Deployment fehlgeschlagen, Code 1." {
t.Errorf("Ansage = %q, want %q", hits[0].Text, "Deployment fehlgeschlagen, Code 1.")
}
waitStatusIdle(t, d, 3*time.Second)
}
// TestWrapNonBlocking: wrap kehrt nach dem enqueue zurück, bevor die
// Ansage zu Ende gespielt ist (Ansage dauert 600 ms am Fake) — stdout
// gehört dabei allein dem Befehl, die Item-ID geht auf stderr.
func TestWrapNonBlocking(t *testing.T) {
root := t.TempDir()
fake := newFakeProvider(t, 600*time.Millisecond)
d := startDaemon(t, root, fake.srv.URL, nil)
env := daemonEnv(root, d.port, fake.srv.URL, nil)
start := time.Now()
res := runTalk(t, env, "wrap", "--", "sh", "-c", "echo bauausgabe")
elapsed := time.Since(start)
if res.code != 0 {
t.Fatalf("talk wrap: Exit %d, stderr: %s", res.code, res.stderr)
}
if elapsed >= 500*time.Millisecond {
t.Fatalf("talk wrap war blockierend (%s; Ansage dauert 600 ms) — Default muss non-blocking sein", elapsed)
}
if res.stdout != "bauausgabe\n" {
t.Errorf("stdout = %q, will ausschließlich die Befehlsausgabe (Item-ID gehört auf stderr)", res.stdout)
}
if strings.TrimSpace(res.stderr) == "" {
t.Errorf("stderr = %q, will mindestens die Item-ID der Ansage", res.stderr)
}
// Die Ansage läuft im Daemon trotzdem zu Ende.
fake.waitCompleted(t, 1)
waitStatusIdle(t, d, 3*time.Second)
}
// TestWrapWait: `talk wrap --wait -- …` kehrt erst nach Ende der Ansage
// zurück (geerbtes say-Flag, hier: 400 ms Synthese).
func TestWrapWait(t *testing.T) {
root := t.TempDir()
fake := newFakeProvider(t, 400*time.Millisecond)
d := startDaemon(t, root, fake.srv.URL, nil)
env := daemonEnv(root, d.port, fake.srv.URL, nil)
start := time.Now()
res := runTalk(t, env, "wrap", "--wait", "--", "true")
elapsed := time.Since(start)
if res.code != 0 {
t.Fatalf("talk wrap --wait: Exit %d, stderr: %s", res.code, res.stderr)
}
if elapsed < 350*time.Millisecond {
t.Fatalf("talk wrap --wait kam zu früh zurück (%s; Ansage dauert 400 ms)", elapsed)
}
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) {
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)
}
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)
}
}