// 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) } // 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 (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, "spreche direkt") { t.Errorf("stderr = %q, will Direkt-Hinweis (nichts bricht still)", res.stderr) } }