refactor: switch the whole project to English

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>
This commit is contained in:
m3ta-chironandClaude Fable 5.1 committed 2026-10-24 11:15:35 +02:00
1 parent 4d30bd95ad
commit 85fa64cfe6
95 files changed
+4707 -4854

No files matched your search

+76 -55
View File
@@ -1,8 +1,8 @@
// 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.
// 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 (
@@ -12,9 +12,9 @@ import (
"time"
)
// TestWrapErfolgMitDauer (Abnahmekriterium): `talk wrap -- sleep 1`
// annonciert Erfolg inklusive Dauer und exitet 0.
func TestWrapErfolgMitDauer(t *testing.T) {
// 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)
@@ -22,20 +22,21 @@ func TestWrapErfolgMitDauer(t *testing.T) {
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)
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)
// 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)
}
// 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) {
// 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)
@@ -43,19 +44,39 @@ func TestWrapFehlschlagCodeDurchgereicht(t *testing.T) {
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)
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 fehlgeschlagen, Code 2." {
t.Errorf("Ansage = %q, want %q", hits[0].Text, "sh fehlgeschlagen, Code 2.")
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)
}
// 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.
// 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)
@@ -64,20 +85,20 @@ func TestWrapFlagShielding(t *testing.T) {
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)
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 hat --bar als eigenes Flag geparsed — Shielding kaputt: %s", res.stderr)
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 erfolgreich.") {
t.Errorf("Ansage = %q, will Erfolg für true", hits[0].Text)
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 ersetzt den Basisnamen in der
// Ansage (Fleet-Muster: Env statt Config-Datei).
// 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)
@@ -86,18 +107,18 @@ func TestWrapLabelOverride(t *testing.T) {
res := runTalk(t, env, "wrap", "--", "false")
if res.code != 1 {
t.Fatalf("talk wrap -- false: Exit %d, want 1", res.code)
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.")
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 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.
// 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)
@@ -105,29 +126,29 @@ func TestWrapNonBlocking(t *testing.T) {
env := daemonEnv(root, d.port, fake.srv.URL, nil)
start := time.Now()
res := runTalk(t, env, "wrap", "--", "sh", "-c", "echo bauausgabe")
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)
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)
t.Fatalf("talk wrap was blocking (%s; announcement takes 600 ms) — the default must be non-blocking", elapsed)
}
if res.stdout != "bauausgabe\n" {
t.Errorf("stdout = %q, will ausschließlich die Befehlsausgabe (Item-ID gehört auf stderr)", res.stdout)
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, will mindestens die Item-ID der Ansage", res.stderr)
t.Errorf("stderr = %q, want at least the item ID of the announcement", res.stderr)
}
// Die Ansage läuft im Daemon trotzdem zu Ende.
// The announcement still runs to completion in the daemon.
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).
// 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)
@@ -139,30 +160,30 @@ func TestWrapWait(t *testing.T) {
elapsed := time.Since(start)
if res.code != 0 {
t.Fatalf("talk wrap --wait: Exit %d, stderr: %s", res.code, res.stderr)
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)
t.Fatalf("talk wrap --wait returned too early (%s; announcement takes 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) {
// 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 garantiert tot
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 (direkt): Exit %d, want 3 (Befehls-Code gewinnt)", res.code)
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 fehlgeschlagen, Code 3." {
t.Errorf("Ansage = %q, want %q", hits[0].Text, "sh fehlgeschlagen, Code 3.")
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, "spreche direkt") {
t.Errorf("stderr = %q, will Direkt-Hinweis (nichts bricht still)", res.stderr)
if !strings.Contains(res.stderr, "speaking directly") {
t.Errorf("stderr = %q, want the direct-path note (nothing fails silently)", res.stderr)
}
}