// 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. package test import ( "regexp" "strings" "testing" "time" ) // TestWrapSuccessWithDuration (acceptance criterion): `talk wrap -- sleep 1` // announces success including the duration and exits 0. func TestWrapSuccessWithDuration(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 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) } waitStatusIdle(t, d, 3*time.Second) } // 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) { 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 (passed through)", res.code) } hits := fake.waitCompleted(t, 1) if hits[0].Text != "sh failed, code 2." { t.Errorf("announcement = %q, want %q", hits[0].Text, "sh failed, code 2.") } waitStatusIdle(t, d, 3*time.Second) } // 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. 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 belongs to the command) — stderr: %s", res.code, res.stderr) } if strings.Contains(res.stderr, "flag provided but not defined") { t.Errorf("talk parsed --bar as its own flag — shielding broken: %s", res.stderr) } hits := fake.waitCompleted(t, 1) if !strings.HasPrefix(hits[0].Text, "true succeeded.") { t.Errorf("announcement = %q, want success for true", hits[0].Text) } waitStatusIdle(t, d, 3*time.Second) } // TestWrapLabelOverride: TALK_WRAP_LABEL replaces the base name in the // announcement (fleet pattern: env instead of config file). 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 failed, code 1." { t.Errorf("announcement = %q, want %q", hits[0].Text, "Deployment failed, code 1.") } waitStatusIdle(t, d, 3*time.Second) } // 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. 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 build-output") 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 was blocking (%s; announcement takes 600 ms) — the default must be non-blocking", elapsed) } if res.stdout != "build-output\n" { t.Errorf("stdout = %q, want exclusively the command output (the item ID belongs on stderr)", res.stdout) } if strings.TrimSpace(res.stderr) == "" { t.Errorf("stderr = %q, want at least the item ID of the announcement", res.stderr) } // The announcement still runs to completion in the daemon. fake.waitCompleted(t, 1) waitStatusIdle(t, d, 3*time.Second) } // TestWrapWait: `talk wrap --wait -- …` returns only after the announcement // ends (inherited say flag, here: 400 ms synthesis). 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 returned too early (%s; announcement takes 400 ms)", elapsed) } waitStatusIdle(t, d, 3*time.Second) } // TestWrapWithoutDaemon: daemon dead, autostart off → announcement directly // against the fake, and the command's exit code is still preserved. func TestWrapWithoutDaemon(t *testing.T) { root := t.TempDir() fake := newFakeProvider(t, 0) env := daemonEnv(root, 1, fake.srv.URL, nil) // port 1: daemon guaranteed dead res := runTalk(t, env, "wrap", "--", "sh", "-c", "exit 3") if res.code != 3 { t.Fatalf("talk wrap (direct): exit %d, want 3 (the command's code wins)", res.code) } hits := fake.waitCompleted(t, 1) if hits[0].Text != "sh failed, code 3." { t.Errorf("announcement = %q, want %q", hits[0].Text, "sh failed, code 3.") } if !strings.Contains(res.stderr, "speaking directly") { t.Errorf("stderr = %q, want the direct-path note (nothing fails silently)", res.stderr) } }