Files
m3ta-chironandClaude Fable 5.1 85fa64cfe6 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>
2026-10-24 11:15:35 +02:00

321 lines
11 KiB
Go

package provider
import (
"context"
"encoding/hex"
"net/http"
"net/http/httptest"
"os"
"strings"
"sync"
"testing"
"time"
"github.com/gorilla/websocket"
)
// TestEdgeAvailable: Edge needs only an endpoint, no key. The constructor
// bakes in the default endpoint, so an Edge built via NewEdge is always
// available; only the zero value (built directly, without an endpoint) is
// not.
func TestEdgeAvailable(t *testing.T) {
if (&Edge{}).Available(context.Background()) {
t.Error("Available without endpoint (zero value) = true, want false")
}
p := NewEdge(ChainConfig{Getenv: envMap(map[string]string{
EnvEdgeEndpoint: "wss://edge.example.com",
})})
if !p.Available(context.Background()) {
t.Error("Available with endpoint = false, want true")
}
if !NewEdge(ChainConfig{Getenv: envMap(nil)}).Available(context.Background()) {
t.Error("Available with default endpoint = false, want true (edge is the key-free fallback)")
}
}
// TestEdgeConnectionFails (error-path mock): a guaranteed dead loopback
// port yields a clear error, immediately and without network.
func TestEdgeConnectionFails(t *testing.T) {
p := NewEdge(ChainConfig{Getenv: envMap(map[string]string{
EnvEdgeEndpoint: "ws://127.0.0.1:1/",
})})
_, err := p.Synthesize(context.Background(), "Hello", "de")
if err == nil {
t.Fatal("Synthesize against dead port: no error")
}
if !strings.Contains(err.Error(), "edge") || !strings.Contains(err.Error(), "WebSocket") {
t.Errorf("error = %v, want a clear edge/WebSocket error", err)
}
}
// TestEdgeInvalidScheme: a non-ws(s) endpoint is a clear error.
func TestEdgeInvalidScheme(t *testing.T) {
p := NewEdge(ChainConfig{Getenv: envMap(map[string]string{
EnvEdgeEndpoint: "http://127.0.0.1:1234/",
})})
_, err := p.Synthesize(context.Background(), "Hello", "de")
if err == nil || !strings.Contains(err.Error(), "ws://") {
t.Errorf("error = %v, want scheme hint ws://", err)
}
}
// TestEdgeSSML: the SSML request contains the voice and the text stripped
// of control characters and XML-escaped; xml:lang is fixed to "en-US" as in
// the reference.
func TestEdgeSSML(t *testing.T) {
got := edgeSSML("de-DE-KatjaNeural", "AT&T <customer> „done“")
for _, want := range []string{
"voice name='de-DE-KatjaNeural'",
"xml:lang='en-US'",
"AT&amp;T &lt;customer&gt;",
} {
if !strings.Contains(got, want) {
t.Errorf("SSML = %q, want %q", got, want)
}
}
if strings.Contains(got, "AT&T") {
t.Errorf("SSML contains unescaped &: %q", got)
}
}
// TestEdgeEscape: control characters (e.g. vertical tab from OCR texts) are
// replaced by spaces, & < > are escaped.
func TestEdgeEscape(t *testing.T) {
in := "a\x0bb&c<d>\x08e"
if want := "a b&amp;c&lt;d&gt; e"; edgeEscape(in) != want {
t.Errorf("edgeEscape(%q) = %q, want %q", in, edgeEscape(in), want)
}
}
// TestEdgeSpeechConfigMsg: output format (mono mp3) and path are correct.
func TestEdgeSpeechConfigMsg(t *testing.T) {
msg := edgeSpeechConfigMsg(time.Unix(0, 0).UTC())
if !strings.Contains(msg, "Path:speech.config") {
t.Errorf("speech.config message without Path: %q", msg)
}
if !strings.Contains(msg, edgeOutputFormat) {
t.Errorf("speech.config message without output format %q: %q", edgeOutputFormat, msg)
}
}
// TestEdgeRequestURL: query parameters (token, Sec-MS-GEC, version) are
// added; the token does not end up in the redacted URL output.
func TestEdgeRequestURL(t *testing.T) {
p := &Edge{Endpoint: "wss://edge.example.com/path"}
raw, err := p.requestURL(time.Unix(1700000000, 0).UTC())
if err != nil {
t.Fatalf("requestURL: %v", err)
}
for _, want := range []string{
"TrustedClientToken=" + edgeTrustedClientToken,
"Sec-MS-GEC=",
"Sec-MS-GEC-Version=" + edgeSecMSGECVersion,
} {
if !strings.Contains(raw, want) {
t.Errorf("requestURL = %q, want %q", raw, want)
}
}
if got := redactURL(raw); strings.Contains(got, edgeTrustedClientToken) {
t.Errorf("redactURL leaks the token: %q", got)
}
}
// TestEdgeSecMSGEC: format "<16 hex>-<64 hex>" (upper case), 5-minute grid
// (stable within the window, different across the boundary) and exact
// reference vectors of the edge-tts DRM implementation (v2).
func TestEdgeSecMSGEC(t *testing.T) {
// Reference vectors: (unix seconds, expected token), generated with the
// Python reference (edge-tts src/edge_tts/drm.py, as of 2026-10).
vectors := []struct {
ts int64
want string
}{
{1700000400, "73C7C85FE76CF347-452672184D494A9F80E179C2FBFEE2E98480C4D4AAFC1F4A577E43B023BEEEC9"},
{1700000599, "73C7C85FE76CF347-452672184D494A9F80E179C2FBFEE2E98480C4D4AAFC1F4A577E43B023BEEEC9"},
{1765800000, "B9B5447115B4FD1A-6BB36279F5B3D1B53F84F786AF05C81A86909ABE30E695496BBE88FFAE2E3E83"},
{0, "52BC7A17B50DFEC9-28C4394775C8751CD2F6859C030C06DB1489ACAB3B3A7651831FC26895177ED6"},
}
for _, v := range vectors {
if got := edgeSecMSGEC(time.Unix(v.ts, 0).UTC()); got != v.want {
t.Errorf("edgeSecMSGEC(%d) =\n %s\nwant\n %s", v.ts, got, v.want)
}
}
// Window stability (base at window start, mod 300 == 0).
base := time.Unix(1700000400, 0).UTC()
token := edgeSecMSGEC(base)
if edgeSecMSGEC(base.Add(4*time.Minute)) != token {
t.Error("Sec-MS-GEC differs within the 5-minute window")
}
if edgeSecMSGEC(base.Add(6*time.Minute)) == token {
t.Error("Sec-MS-GEC does not change across the window boundary")
}
if len(token) != 16+1+64 || !strings.Contains(token, "-") {
t.Errorf("token format = %q, want <16 hex>-<64 hex>", token)
}
}
// TestEdgeRequestID: 32 hex characters, unique.
func TestEdgeRequestID(t *testing.T) {
a, b := edgeRequestID(), edgeRequestID()
if len(a) != 32 {
t.Errorf("request ID length = %d, want 32", len(a))
}
if _, err := hex.DecodeString(a); err != nil {
t.Errorf("request ID is not hex: %v", err)
}
if a == b {
t.Error("request ID not unique")
}
}
// TestEdgeSplitBinary: binary frame: 2 bytes big-endian header length, then
// header (Path:audio), then payload.
func TestEdgeSplitBinary(t *testing.T) {
msg := []byte{0, 12, 'P', 'a', 't', 'h', ':', 'a', 'u', 'd', 'i', 'o', '\r', '\n', 1, 2, 3}
header, payload, ok := splitEdgeBinary(msg)
if !ok || header != "Path:audio\r\n" || string(payload) != "\x01\x02\x03" {
t.Errorf("splitEdgeBinary = %q, %x, %v", header, payload, ok)
}
if _, _, ok := splitEdgeBinary([]byte{0, 99, 'x'}); ok {
t.Error("oversized header length should be rejected")
}
}
// fakeEdgeServer is a loopback fake of the Edge WSS protocol (no real
// network call; everything stays on 127.0.0.1). It checks the two client
// messages and answers per protocol: a binary audio frame with Path:audio,
// then turn.end. audio is the payload to deliver; closing closes the
// connection before turn.end (error path).
func fakeEdgeServer(t *testing.T, audio []byte, closing bool) (*httptest.Server, *fakeEdgeSeen) {
t.Helper()
seen := &fakeEdgeSeen{}
upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.URL.Query().Get("TrustedClientToken"); got != edgeTrustedClientToken {
t.Errorf("TrustedClientToken = %q", got)
}
if r.Header.Get("Origin") != edgeOrigin {
t.Errorf("Origin = %q", r.Header.Get("Origin"))
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
t.Errorf("Upgrade: %v", err)
return
}
defer conn.Close()
if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
return
}
if _, cfg, err := conn.ReadMessage(); err != nil {
t.Errorf("reading speech.config: %v", err)
} else {
seen.mu.Lock()
seen.config = string(cfg)
seen.mu.Unlock()
}
if _, req, err := conn.ReadMessage(); err != nil {
t.Errorf("reading ssml: %v", err)
} else {
seen.mu.Lock()
seen.ssml = string(req)
seen.mu.Unlock()
}
if closing {
return // close the connection without answering
}
header := "X-RequestId:dummy\r\nPath:audio\r\n"
frame := make([]byte, 0, 2+len(header)+len(audio))
frame = append(frame, byte(len(header)>>8), byte(len(header)))
frame = append(frame, header...)
frame = append(frame, audio...)
if err := conn.WriteMessage(websocket.BinaryMessage, frame); err != nil {
return
}
if err := conn.WriteMessage(websocket.TextMessage, []byte("X-RequestId:dummy\r\nPath:turn.end\r\n\r\n")); err != nil {
return
}
}))
t.Cleanup(ts.Close)
return ts, seen
}
type fakeEdgeSeen struct {
mu sync.Mutex
config string
ssml string
}
func (s *fakeEdgeSeen) snapshot() (configMsg, ssml string) {
s.mu.Lock()
defer s.mu.Unlock()
return s.config, s.ssml
}
// TestEdgeLoopbackFakeServer: complete protocol run against the loopback
// fake (tested fakeably: handshake parameters, both client messages,
// binary frame parsing, turn.end termination).
func TestEdgeLoopbackFakeServer(t *testing.T) {
wantAudio := []byte("\xff\xfb\x90\x00" + "edge-audio")
ts, seen := fakeEdgeServer(t, wantAudio, false)
p := NewEdge(ChainConfig{Getenv: envMap(map[string]string{
EnvEdgeEndpoint: "ws://" + ts.Listener.Addr().String(),
})})
data, err := p.Synthesize(context.Background(), "Hello & done", "de")
if err != nil {
t.Fatalf("Synthesize against loopback fake: %v", err)
}
if string(data) != string(wantAudio) {
t.Errorf("Synthesize = %x, want %x", data, wantAudio)
}
configMsg, ssml := seen.snapshot()
if !strings.Contains(configMsg, "Path:speech.config") {
t.Errorf("speech.config = %q", configMsg)
}
if !strings.Contains(ssml, "Path:ssml") {
t.Errorf("ssml request without Path:ssml: %q", ssml)
}
if !strings.Contains(ssml, "voice name='de-DE-KatjaNeural'") {
t.Errorf("ssml without default voice de: %q", ssml)
}
if !strings.Contains(ssml, "Hello &amp; done") {
t.Errorf("ssml without escaped text: %q", ssml)
}
}
// TestEdgeLoopbackConnectionAbort: the server closes after the requests
// without audio → clear "protocol changed?" error.
func TestEdgeLoopbackConnectionAbort(t *testing.T) {
ts, _ := fakeEdgeServer(t, nil, true)
p := NewEdge(ChainConfig{Getenv: envMap(map[string]string{
EnvEdgeEndpoint: "ws://" + ts.Listener.Addr().String(),
})})
_, err := p.Synthesize(context.Background(), "Hello", "de")
if err == nil || !strings.Contains(err.Error(), "protocol") {
t.Errorf("error = %v, want protocol-change hint", err)
}
}
// TestEdgeIntegrationReal is the integration marker against the REAL
// Microsoft endpoint, skippable via testing.Short (free, but real network;
// standard test runs and -short skip it; one-off manual acceptance with
// TALK_TEST_REAL_EDGE=1).
func TestEdgeIntegrationReal(t *testing.T) {
if testing.Short() {
t.Skip("skipped (-short)")
}
if os.Getenv("TALK_TEST_REAL_EDGE") != "1" {
t.Skip("skipped (real network); run with TALK_TEST_REAL_EDGE=1")
}
p := NewEdge(ChainConfig{})
data, err := p.Synthesize(context.Background(), "Short speech test via Edge.", "de")
if err != nil {
t.Fatalf("Synthesize against real endpoint: %v", err)
}
if len(data) == 0 {
t.Fatal("Synthesize against real endpoint: 0 bytes")
}
t.Logf("Edge integration: %d bytes of audio", len(data))
}