2026-10-24 11:15:35 +02:00
|
|
|
// wrap process-boundary tests (Task 07): talk runs as a real process, talkd
|
|
|
|
|
// as a real daemon, fake provider + sink null as in the harness — only the
|
|
|
|
|
// externally observable is checked: exit codes, command stdout, announcement
|
|
|
|
|
// text at the fake. The wrapped commands are real binaries (sleep/sh/true),
|
|
|
|
|
// the durations deliberately small.
|
2026-10-18 21:01:13 +02:00
|
|
|
package test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"regexp"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// TestWrapSuccessWithDuration (acceptance criterion): `talk wrap -- sleep 1`
|
|
|
|
|
// announces success including the duration and exits 0.
|
|
|
|
|
func TestWrapSuccessWithDuration(t *testing.T) {
|
2026-10-18 21:01:13 +02:00
|
|
|
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 {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("talk wrap -- sleep 1: exit %d, stderr: %s", res.code, res.stderr)
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
hits := fake.waitCompleted(t, 1)
|
2026-10-24 11:15:35 +02:00
|
|
|
// sleep 1 measures ~1 s (often more under load) — the number is open,
|
|
|
|
|
// the form is not.
|
|
|
|
|
if !regexp.MustCompile(`^sleep succeeded\. Duration: \d+ seconds?\.$`).MatchString(hits[0].Text) {
|
|
|
|
|
t.Errorf("announcement = %q, want \"sleep succeeded. Duration: N second(s).\"", hits[0].Text)
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
waitStatusIdle(t, d, 3*time.Second)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// TestWrapFailureCodePassedThrough (acceptance criterion): when the command
|
|
|
|
|
// fails with code 2, wrap exits 2 and announces the failure with the code —
|
|
|
|
|
// without duration, word for word as briefed.
|
|
|
|
|
func TestWrapFailureCodePassedThrough(t *testing.T) {
|
2026-10-18 21:01:13 +02:00
|
|
|
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 {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("talk wrap -- sh -c 'exit 2': exit %d, want 2 (passed through)", res.code)
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
hits := fake.waitCompleted(t, 1)
|
2026-10-24 11:15:35 +02:00
|
|
|
if hits[0].Text != "sh failed, code 2." {
|
|
|
|
|
t.Errorf("announcement = %q, want %q", hits[0].Text, "sh failed, code 2.")
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
waitStatusIdle(t, d, 3*time.Second)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// TestWrapGermanAnnouncement: TALK_LANG=de switches talk's own announcement
|
|
|
|
|
// to German (the detection cannot decide on a bare label like "sh" and
|
|
|
|
|
// falls back to the configured language).
|
|
|
|
|
func TestWrapGermanAnnouncement(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_LANG": "de"})
|
|
|
|
|
|
|
|
|
|
res := runTalk(t, env, "wrap", "--", "sh", "-c", "exit 2")
|
|
|
|
|
if res.code != 2 {
|
|
|
|
|
t.Fatalf("talk wrap -- sh -c 'exit 2' (de): exit %d, want 2", res.code)
|
|
|
|
|
}
|
|
|
|
|
hits := fake.waitCompleted(t, 1)
|
|
|
|
|
if hits[0].Text != "sh fehlgeschlagen, Code 2." {
|
|
|
|
|
t.Errorf("announcement = %q, want %q", hits[0].Text, "sh fehlgeschlagen, Code 2.")
|
|
|
|
|
}
|
|
|
|
|
waitStatusIdle(t, d, 3*time.Second)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestWrapFlagShielding (regression, core of Task 07): `talk wrap -- true
|
|
|
|
|
// --bar` passes --bar on to the command — talk must NOT parse --bar as its
|
|
|
|
|
// own flag (that would be exit 2 with a flag error). GNU true ignores
|
|
|
|
|
// operands, so it exits 0, and the success announcement runs.
|
2026-10-18 21:01:13 +02:00
|
|
|
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 {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("talk wrap -- true --bar: exit %d, want 0 (--bar belongs to the command) — stderr: %s", res.code, res.stderr)
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
if strings.Contains(res.stderr, "flag provided but not defined") {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Errorf("talk parsed --bar as its own flag — shielding broken: %s", res.stderr)
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
hits := fake.waitCompleted(t, 1)
|
2026-10-24 11:15:35 +02:00
|
|
|
if !strings.HasPrefix(hits[0].Text, "true succeeded.") {
|
|
|
|
|
t.Errorf("announcement = %q, want success for true", hits[0].Text)
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
waitStatusIdle(t, d, 3*time.Second)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// TestWrapLabelOverride: TALK_WRAP_LABEL replaces the base name in the
|
|
|
|
|
// announcement (fleet pattern: env instead of config file).
|
2026-10-18 21:01:13 +02:00
|
|
|
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 {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("talk wrap -- false: exit %d, want 1", res.code)
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
hits := fake.waitCompleted(t, 1)
|
2026-10-24 11:15:35 +02:00
|
|
|
if hits[0].Text != "Deployment failed, code 1." {
|
|
|
|
|
t.Errorf("announcement = %q, want %q", hits[0].Text, "Deployment failed, code 1.")
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
waitStatusIdle(t, d, 3*time.Second)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// TestWrapNonBlocking: wrap returns after the enqueue, before the
|
|
|
|
|
// announcement has finished playing (the announcement takes 600 ms at the
|
|
|
|
|
// fake) — stdout belongs to the command alone, the item ID goes to stderr.
|
2026-10-18 21:01:13 +02:00
|
|
|
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()
|
2026-10-24 11:15:35 +02:00
|
|
|
res := runTalk(t, env, "wrap", "--", "sh", "-c", "echo build-output")
|
2026-10-18 21:01:13 +02:00
|
|
|
elapsed := time.Since(start)
|
|
|
|
|
|
|
|
|
|
if res.code != 0 {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("talk wrap: exit %d, stderr: %s", res.code, res.stderr)
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
if elapsed >= 500*time.Millisecond {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("talk wrap was blocking (%s; announcement takes 600 ms) — the default must be non-blocking", elapsed)
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
2026-10-24 11:15:35 +02:00
|
|
|
if res.stdout != "build-output\n" {
|
|
|
|
|
t.Errorf("stdout = %q, want exclusively the command output (the item ID belongs on stderr)", res.stdout)
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(res.stderr) == "" {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Errorf("stderr = %q, want at least the item ID of the announcement", res.stderr)
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// The announcement still runs to completion in the daemon.
|
2026-10-18 21:01:13 +02:00
|
|
|
fake.waitCompleted(t, 1)
|
|
|
|
|
waitStatusIdle(t, d, 3*time.Second)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// TestWrapWait: `talk wrap --wait -- …` returns only after the announcement
|
|
|
|
|
// ends (inherited say flag, here: 400 ms synthesis).
|
2026-10-18 21:01:13 +02:00
|
|
|
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 {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("talk wrap --wait: exit %d, stderr: %s", res.code, res.stderr)
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
if elapsed < 350*time.Millisecond {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("talk wrap --wait returned too early (%s; announcement takes 400 ms)", elapsed)
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
waitStatusIdle(t, d, 3*time.Second)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// TestWrapWithoutDaemon: daemon dead, autostart off → announcement directly
|
|
|
|
|
// against the fake, and the command's exit code is still preserved.
|
|
|
|
|
func TestWrapWithoutDaemon(t *testing.T) {
|
2026-10-18 21:01:13 +02:00
|
|
|
root := t.TempDir()
|
|
|
|
|
fake := newFakeProvider(t, 0)
|
2026-10-24 11:15:35 +02:00
|
|
|
env := daemonEnv(root, 1, fake.srv.URL, nil) // port 1: daemon guaranteed dead
|
2026-10-18 21:01:13 +02:00
|
|
|
|
|
|
|
|
res := runTalk(t, env, "wrap", "--", "sh", "-c", "exit 3")
|
|
|
|
|
if res.code != 3 {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("talk wrap (direct): exit %d, want 3 (the command's code wins)", res.code)
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
hits := fake.waitCompleted(t, 1)
|
2026-10-24 11:15:35 +02:00
|
|
|
if hits[0].Text != "sh failed, code 3." {
|
|
|
|
|
t.Errorf("announcement = %q, want %q", hits[0].Text, "sh failed, code 3.")
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
2026-10-24 11:15:35 +02:00
|
|
|
if !strings.Contains(res.stderr, "speaking directly") {
|
|
|
|
|
t.Errorf("stderr = %q, want the direct-path note (nothing fails silently)", res.stderr)
|
2026-10-18 21:01:13 +02:00
|
|
|
}
|
|
|
|
|
}
|