Files
talk/cmd/talk/cli_test.go
T
m3ta-chiron 51cd5a686c feat(cli): talk wrap — Befehlsummwicklung mit Ansage und Code-Durchreichung
talk wrap -- <Befehl> [Argumente …] führt den Befehl aus (stdin/stdout/
stderr durchgereicht — stdout gehört allein dem Befehl, talk-Informations-
zeilen gehen auf stderr), misst die Wandzeit und sagt danach das Ergebnis
an: „<label> erfolgreich. Dauer: X.“ bzw. „<label> fehlgeschlagen,
Code N.“, menschlich formatiert (Stunden/Minuten/Sekunden, Einzahl
dekliniert) und lokalisiert de/en nach langdetect des Labels (--lang
überschreibt). Label: config wrap.label bzw. TALK_WRAP_LABEL oder der
Basisname des Befehls.

Exit-Code-Politik: wrap exitet immer mit dem Code des Befehls — scheitert
die Ansage selbst (Daemon lehnt ab, Kette leer), bleibt der Befehls-Code
mit Warning auf stderr (talk-Fehler ≠ Befehls-Fehler). Nicht startbare
Befehle zählen als 127 (Shell-Konvention), Signaltode portabel als 1
(syscall.WaitStatus wäre nicht frei cross-compilierbar; dokumentiert).
Benutzungsfehler von talk (kein Befehl, ungültige say-Flags) scheitern
vor dem Befehlsstart; Config-Fehler nach dem Lauf — Ansage ausgesetzt.

Ansageweg wie say: Daemon-Pfad (enqueue, non-blocking; --wait pollt),
ohne Daemon Degraded Mode; wrap erbt die say-Flags und reicht sie durch.
Dafür bekommen sayViaDaemon/sayDegraded einen info-Writer: say schreibt
Item-ID/muted/discarded weiter auf stdout, wrap auf stderr — Pipes des
Befehls bleiben sauber. Unit-Tests: Dauer-Formatierung (de/en), Ansage-
Texte, Label-Auflösung, In-Prozess-Durchreichung gegen Fake-Provider
(Erfolg/Fehlschlag/127/Label/Ansage-scheitert/stdout-Reinheit).
2026-10-18 21:01:07 +02:00

161 lines
5.2 KiB
Go

package main
import (
"bytes"
"io"
"strings"
"testing"
)
// wantSubcommands ist die bindende CLI-Surface aus der Spec — bewusst als
// hartkodierte Liste getestet (nicht aus der Befehlstabelle abgeleitet),
// damit die Tests anschlagen, wenn Tabelle und Spec auseinanderlaufen.
var wantSubcommands = []string{
"say", "wrap", "mute", "unmute", "status", "cancel", "voices", "test", "doctor", "setup",
}
// wantStubCommands ist wantSubcommands ohne die implementierten Befehle —
// say (Task 02/04), mute/unmute/status/cancel (Task 04, Daemon-Aufrufe)
// und wrap (Task 07, Befehlsummichtung mit Ansage) sind real; die
// restlichen bleiben Stubs bis zu ihren Tasks.
var wantStubCommands = []string{
"voices", "test", "doctor", "setup",
}
// runCLI ist der Test-Einstieg in run mit leerem stdin (kein Text über
// Pipe) — dem üblichen Aufruf ohne Eingabe.
func runCLI(argv []string, stdout, stderr io.Writer) int {
return run(argv, strings.NewReader(""), stdout, stderr)
}
func TestHelpListetAlleSubcommands(t *testing.T) {
var out, errBuf bytes.Buffer
code := runCLI([]string{"--help"}, &out, &errBuf)
if code != exitOK {
t.Fatalf("--help: Exit-Code = %d, want %d (stderr: %q)", code, exitOK, errBuf.String())
}
lines := strings.Split(out.String(), "\n")
for _, name := range wantSubcommands {
found := false
for _, ln := range lines {
if strings.HasPrefix(strings.TrimLeft(ln, " "), name+" ") {
found = true
break
}
}
if !found {
t.Errorf("--help listet Befehl %q nicht (Output: %q)", name, out.String())
}
}
}
func TestVersionDefault(t *testing.T) {
var out, errBuf bytes.Buffer
code := runCLI([]string{"--version"}, &out, &errBuf)
if code != exitOK {
t.Fatalf("--version: Exit-Code = %d, want %d", code, exitOK)
}
if got, want := out.String(), "talk version dev\n"; got != want {
t.Errorf("--version Output = %q, want %q", got, want)
}
}
func TestVersionStringMitCommit(t *testing.T) {
oldCommit := commit
commit = "c0ffee1"
defer func() { commit = oldCommit }()
if got, want := versionString(), "talk version dev (c0ffee1)"; got != want {
t.Errorf("versionString() = %q, want %q", got, want)
}
}
func TestSubcommandStubs(t *testing.T) {
for _, name := range wantStubCommands {
t.Run(name, func(t *testing.T) {
var out, errBuf bytes.Buffer
code := runCLI([]string{name}, &out, &errBuf)
if code == exitOK {
t.Errorf("talk %s: Exit-Code 0, want non-zero für Stub", name)
}
if !strings.Contains(errBuf.String(), "not implemented yet") {
t.Errorf("talk %s: stderr = %q, will Hinweis auf nicht implementiert", name, errBuf.String())
}
if !strings.Contains(errBuf.String(), name) {
t.Errorf("talk %s: stderr nennt den Befehlsnamen nicht: %q", name, errBuf.String())
}
if out.Len() != 0 {
t.Errorf("talk %s: unerwartete stdout-Ausgabe %q", name, out.String())
}
})
}
}
func TestSayHelpZeigtFlags(t *testing.T) {
var out, errBuf bytes.Buffer
code := runCLI([]string{"say", "--help"}, &out, &errBuf)
if code != exitOK {
t.Fatalf("talk say --help: Exit-Code = %d, want %d", code, exitOK)
}
for _, f := range []string{"-wait", "-now", "-priority", "-voice", "-lang", "-provider"} {
if !strings.Contains(out.String(), f) {
t.Errorf("talk say --help zeigt Flag %s nicht (Output: %q)", f, out.String())
}
}
if strings.Contains(out.String(), "noch nicht implementiert") {
t.Errorf("talk say --help soll say nicht mehr als Stub markieren: %q", out.String())
}
}
// TestWrapHelpZeigtSayFlags: wrap erbt die say-Flags (mindestens --wait)
// und ist kein Stub mehr.
func TestWrapHelpZeigtSayFlags(t *testing.T) {
var out, errBuf bytes.Buffer
code := runCLI([]string{"wrap", "--help"}, &out, &errBuf)
if code != exitOK {
t.Fatalf("talk wrap --help: Exit-Code = %d, want %d (stderr: %q)", code, exitOK, errBuf.String())
}
for _, f := range []string{"-wait", "-provider"} {
if !strings.Contains(out.String(), f) {
t.Errorf("talk wrap --help zeigt Flag %s nicht (Output: %q)", f, out.String())
}
}
if strings.Contains(out.String(), "noch nicht implementiert") {
t.Errorf("talk wrap --help soll wrap nicht als Stub markieren: %q", out.String())
}
}
func TestHelpSubcommand(t *testing.T) {
t.Run("ohne Argument", func(t *testing.T) {
var out, errBuf bytes.Buffer
if code := runCLI([]string{"help"}, &out, &errBuf); code != exitOK {
t.Fatalf("talk help: Exit-Code = %d, want %d", code, exitOK)
}
for _, name := range wantSubcommands {
if !strings.Contains(out.String(), name) {
t.Errorf("talk help listet %q nicht", name)
}
}
})
t.Run("mit Befehl", func(t *testing.T) {
var out, errBuf bytes.Buffer
if code := runCLI([]string{"help", "mute"}, &out, &errBuf); code != exitOK {
t.Fatalf("talk help mute: Exit-Code = %d, want %d", code, exitOK)
}
if !strings.Contains(out.String(), "talk mute") {
t.Errorf("talk help mute zeigt Usage von mute nicht: %q", out.String())
}
})
}
func TestUnbekanntesFlag(t *testing.T) {
var out, errBuf bytes.Buffer
code := runCLI([]string{"--bogus"}, &out, &errBuf)
if code != exitUsage {
t.Fatalf("talk --bogus: Exit-Code = %d, want %d", code, exitUsage)
}
if !strings.Contains(errBuf.String(), "bogus") {
t.Errorf("talk --bogus: Fehlermeldung fehlt auf stderr: %q", errBuf.String())
}
}