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:
1 parent
4d30bd95ad
commit
85fa64cfe6
95 files changed
+4707
-4854
No files matched your search
@@ -9,10 +9,10 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestOpenAISynthese200: 200 mit Bytes → Bytes unverändert; POST
|
||||
// /v1/audio/speech, Bearer-Key, Body mit model/voice/input/response_format
|
||||
// laut Task-Brief (Default-Modell tts-1, Default-Stimme alloy).
|
||||
func TestOpenAISynthese200(t *testing.T) {
|
||||
// TestOpenAISynthesize200: 200 with bytes → bytes unchanged; POST
|
||||
// /v1/audio/speech, bearer key, body with model/voice/input/response_format
|
||||
// per the API contract (default model tts-1, default voice alloy).
|
||||
func TestOpenAISynthesize200(t *testing.T) {
|
||||
ts, rec := fakeTTSServer(t, 200, fakeMP3)
|
||||
p := NewOpenAI(ChainConfig{
|
||||
Getenv: envMap(map[string]string{
|
||||
@@ -21,18 +21,18 @@ func TestOpenAISynthese200(t *testing.T) {
|
||||
}),
|
||||
})
|
||||
|
||||
data, err := p.Synthesize(context.Background(), "Build fertig", "en")
|
||||
data, err := p.Synthesize(context.Background(), "Build finished", "en")
|
||||
if err != nil {
|
||||
t.Fatalf("Synthesize: %v", err)
|
||||
}
|
||||
if string(data) != string(fakeMP3) {
|
||||
t.Errorf("Synthesize = %q, want Fake-Bytes", data)
|
||||
t.Errorf("Synthesize = %q, want fake bytes", data)
|
||||
}
|
||||
if rec.method != http.MethodPost {
|
||||
t.Errorf("Methode = %s, want POST", rec.method)
|
||||
t.Errorf("method = %s, want POST", rec.method)
|
||||
}
|
||||
if want := "/v1/audio/speech"; rec.path != want {
|
||||
t.Errorf("Pfad = %q, want %q", rec.path, want)
|
||||
t.Errorf("path = %q, want %q", rec.path, want)
|
||||
}
|
||||
if got := rec.header.Get("Authorization"); got != "Bearer sk-test" {
|
||||
t.Errorf("Authorization = %q, want Bearer sk-test", got)
|
||||
@@ -45,15 +45,15 @@ func TestOpenAISynthese200(t *testing.T) {
|
||||
ResponseFormat string `json:"response_format"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.body, &body); err != nil {
|
||||
t.Fatalf("Body ist kein JSON: %v (%s)", err, rec.body)
|
||||
t.Fatalf("body is not JSON: %v (%s)", err, rec.body)
|
||||
}
|
||||
if body.Model != "tts-1" {
|
||||
t.Errorf("model = %q, want Default tts-1", body.Model)
|
||||
t.Errorf("model = %q, want default tts-1", body.Model)
|
||||
}
|
||||
if body.Voice != "alloy" {
|
||||
t.Errorf("voice = %q, want Default alloy", body.Voice)
|
||||
t.Errorf("voice = %q, want default alloy", body.Voice)
|
||||
}
|
||||
if body.Input != "Build fertig" {
|
||||
if body.Input != "Build finished" {
|
||||
t.Errorf("input = %q", body.Input)
|
||||
}
|
||||
if body.ResponseFormat != "mp3" {
|
||||
@@ -61,8 +61,8 @@ func TestOpenAISynthese200(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpenAIHTTPFehler: 500 und 401 sind klare Fehler mit Status.
|
||||
func TestOpenAIHTTPFehler(t *testing.T) {
|
||||
// TestOpenAIHTTPError: 500 and 401 are clear errors with the status.
|
||||
func TestOpenAIHTTPError(t *testing.T) {
|
||||
for _, status := range []int{500, 401} {
|
||||
t.Run(http.StatusText(status), func(t *testing.T) {
|
||||
ts, _ := fakeTTSServer(t, status, []byte("boom"))
|
||||
@@ -70,21 +70,21 @@ func TestOpenAIHTTPFehler(t *testing.T) {
|
||||
EnvOpenAIKey: "sk-test",
|
||||
EnvOpenAIEndpoint: ts.URL,
|
||||
})})
|
||||
_, err := p.Synthesize(context.Background(), "Hallo", "de")
|
||||
_, err := p.Synthesize(context.Background(), "Hello", "de")
|
||||
if err == nil || !strings.Contains(err.Error(), fmt.Sprintf("%d", status)) {
|
||||
t.Fatalf("Synthesize-Fehler = %v, will HTTP %d", err, status)
|
||||
t.Fatalf("Synthesize error = %v, want HTTP %d", err, status)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "openai") {
|
||||
t.Errorf("Fehler soll den Provider nennen: %v", err)
|
||||
t.Errorf("error should name the provider: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpenAIModelKonfigurierbar: [providers.openai].model ändert den
|
||||
// Modell-String im Body (Entscheidungsnotiz: Default tts-1, Alternativen
|
||||
// wie gpt-4o-mini-tts erlaubt).
|
||||
func TestOpenAIModelKonfigurierbar(t *testing.T) {
|
||||
// TestOpenAIModelConfigurable: [providers.openai].model changes the model
|
||||
// string in the body (decision note: default tts-1, alternatives such as
|
||||
// gpt-4o-mini-tts allowed).
|
||||
func TestOpenAIModelConfigurable(t *testing.T) {
|
||||
ts, rec := fakeTTSServer(t, 200, fakeMP3)
|
||||
p := NewOpenAI(ChainConfig{
|
||||
Getenv: envMap(map[string]string{
|
||||
@@ -93,7 +93,7 @@ func TestOpenAIModelKonfigurierbar(t *testing.T) {
|
||||
}),
|
||||
Model: "gpt-4o-mini-tts",
|
||||
})
|
||||
if _, err := p.Synthesize(context.Background(), "Hallo", "de"); err != nil {
|
||||
if _, err := p.Synthesize(context.Background(), "Hello", "de"); err != nil {
|
||||
t.Fatalf("Synthesize: %v", err)
|
||||
}
|
||||
var body struct {
|
||||
@@ -107,20 +107,20 @@ func TestOpenAIModelKonfigurierbar(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpenAIAvailable: Key entscheidet — ohne Netz-Call.
|
||||
// TestOpenAIAvailable: the key decides; without a network call.
|
||||
func TestOpenAIAvailable(t *testing.T) {
|
||||
if NewOpenAI(ChainConfig{Getenv: envMap(nil)}).Available(context.Background()) {
|
||||
t.Error("Available ohne Key = true, want false")
|
||||
t.Error("Available without key = true, want false")
|
||||
}
|
||||
p := NewOpenAI(ChainConfig{Getenv: envMap(map[string]string{EnvOpenAIKey: "sk"})})
|
||||
if !p.Available(context.Background()) {
|
||||
t.Error("Available mit Key = false, want true")
|
||||
t.Error("Available with key = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpenAIVoiceRangfolge: Pin schlägt Mapping schlägt Default.
|
||||
func TestOpenAIVoiceRangfolge(t *testing.T) {
|
||||
t.Run("Mapping schlägt Default", func(t *testing.T) {
|
||||
// TestOpenAIVoicePrecedence: pin beats mapping beats default.
|
||||
func TestOpenAIVoicePrecedence(t *testing.T) {
|
||||
t.Run("mapping beats default", func(t *testing.T) {
|
||||
ts, rec := fakeTTSServer(t, 200, fakeMP3)
|
||||
p := NewOpenAI(ChainConfig{
|
||||
Getenv: envMap(map[string]string{
|
||||
@@ -140,7 +140,7 @@ func TestOpenAIVoiceRangfolge(t *testing.T) {
|
||||
t.Errorf("voice = %q, want nova (config)", body.Voice)
|
||||
}
|
||||
})
|
||||
t.Run("Pin schlägt Mapping", func(t *testing.T) {
|
||||
t.Run("pin beats mapping", func(t *testing.T) {
|
||||
ts, rec := fakeTTSServer(t, 200, fakeMP3)
|
||||
p := NewOpenAI(ChainConfig{
|
||||
Getenv: envMap(map[string]string{
|
||||
|
||||
Reference in new issue
Block a user