talk is going to be published. Code comments, CLI output, error messages,
daemon log lines, MCP descriptions and results, test names and assertions,
README, spec, skill, build scripts and flake are now English. Only the
spoken announcement texts stay bilingual: the wrap result and the test
sentence are German or English depending on the detected language.
Language fallback is now configurable: `lang = "en" | "de"` (env
TALK_LANG, default en) is used when detection cannot tell (numbers, single
words, ties) and for talk's own announcements. langdetect.Detect takes the
fallback as an argument instead of hard-coding German. `talk setup` asks for
it; `talk doctor` and `talk status` show it.
Vocabulary pinned across packages so the process-boundary tests observe
stable strings: chain log lines ("chain: tier X skipped (not available)",
"falling back to the next tier", "spoken via X"), queue lines ("discarded:
quiet hours ... active", "replaced by toast", "max_age"), CLI notes
("speaking directly", "daemon started at", "no daemon", "nothing to cancel"),
status block with aligned English labels, MCP messages, origin guard 403.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
190 lines
7.1 KiB
Go
190 lines
7.1 KiB
Go
// 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)
|
|
}
|
|
}
|