diff --git a/test/wrap_test.go b/test/wrap_test.go new file mode 100644 index 0000000..a3b9e60 --- /dev/null +++ b/test/wrap_test.go @@ -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) + } +}