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
+101
-100
@@ -1,12 +1,12 @@
|
||||
// Package test enthält die Prozessgrenzen-Tests von talk 2.0 — die primäre
|
||||
// Test-Seam laut Spec: talkd und talk laufen als echte Prozesse (gebuildet
|
||||
// aus diesem Repo), angetrieben über HTTP und die CLI, mit zwei injizierten
|
||||
// Fakes: einem ElevenLabs-Fake-Endpunkt (httptest-Server mit Audio-Bytes)
|
||||
// und audio.sink=null. Getestet wird nur extern beobachtbares Verhalten:
|
||||
// HTTP-Antworten, CLI-Exit-Codes/-Output, Fake-Treffer (Reihenfolge und
|
||||
// Zeit), Dateien im Config-Dir. Tests binden NIEMALS den Default-Port 7800
|
||||
// und hängen nie von einem echten Daemon ab — talkd bekommt einen freien
|
||||
// Port per TALK_PORT, CLI-Prozesse denselben (oder Port 1 für „Daemon tot").
|
||||
// Package test contains the process-boundary tests of talk 2.0 — the primary
|
||||
// test seam according to the spec: talkd and talk run as real processes
|
||||
// (built from this repo), driven via HTTP and the CLI, with two injected
|
||||
// fakes: an ElevenLabs fake endpoint (httptest server with audio bytes)
|
||||
// and audio.sink=null. Only externally observable behaviour is tested:
|
||||
// HTTP responses, CLI exit codes/output, fake hits (order and timing),
|
||||
// files in the config dir. Tests NEVER bind the default port 7800 and never
|
||||
// depend on a real daemon — talkd gets a free port via TALK_PORT, CLI
|
||||
// processes the same one (or port 1 for "daemon dead").
|
||||
package test
|
||||
|
||||
import (
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Die Binaries werden einmal pro Testlauf gebaut (Build-Cache macht es
|
||||
// billig) und von allen Tests geteilt.
|
||||
// The binaries are built once per test run (the build cache makes it cheap)
|
||||
// and shared by all tests.
|
||||
var talkBin string
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -56,8 +56,8 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
// syncBuffer ist ein nebenläufigkeitssicherer Buffer (Daemon-stderr wird
|
||||
// gelesen, während der Prozess läuft).
|
||||
// syncBuffer is a concurrency-safe buffer (daemon stderr is read while the
|
||||
// process is running).
|
||||
type syncBuffer struct {
|
||||
mu sync.Mutex
|
||||
buf bytes.Buffer
|
||||
@@ -75,23 +75,23 @@ func (b *syncBuffer) String() string {
|
||||
return b.buf.String()
|
||||
}
|
||||
|
||||
// fakeHit ist ein beobachteter Synthese-Request am Fake-Provider.
|
||||
// fakeHit is an observed synthesis request at the fake provider.
|
||||
type fakeHit struct {
|
||||
Text string // Text aus dem Request-Body
|
||||
Start time.Time // Ankunft am Fake
|
||||
End time.Time // Antwort abgeschickt bzw. Abbruch gesehen
|
||||
Completed bool // true: Antwort geschrieben; false: Client hat abgebrochen
|
||||
Text string // text from the request body
|
||||
Start time.Time // arrival at the fake
|
||||
End time.Time // response sent or cancellation seen
|
||||
Completed bool // true: response written; false: client cancelled
|
||||
}
|
||||
|
||||
// fakeProvider ist ein ElevenLabs-Fake (POST /v1/text-to-speech/<voice>):
|
||||
// Er antwortet mit ein paar Bytes „Audio" und protokolliert jeden Request
|
||||
// mit Ankunfts- und Antwort-Zeit — daraus sind Reihenfolge (Textfolge) und
|
||||
// Serialisierung (Ankunft des nächsten nach Antwort des vorigen) ablesbar.
|
||||
// delay ist zur Laufzeit umschaltbar, um blockierende Synthesen zu
|
||||
// simulieren (Serialisierung, Priorität, cancel). Ankünfte werden separat
|
||||
// und sofort protokolliert: Sie zeigen, dass der Daemon mit einer Synthese
|
||||
// BESCHÄFTIGT ist (Queue deterministisch befüllbar), noch bevor die
|
||||
// (verzögerte) Antwort als Treffer landet.
|
||||
// fakeProvider is an ElevenLabs fake (POST /v1/text-to-speech/<voice>): it
|
||||
// answers with a few bytes of "audio" and logs every request with arrival
|
||||
// and response time — from which order (text sequence) and serialization
|
||||
// (arrival of the next after the response of the previous) can be read.
|
||||
// delay can be switched at runtime to simulate blocking syntheses
|
||||
// (serialization, priority, cancel). Arrivals are logged separately and
|
||||
// immediately: they show that the daemon is BUSY with a synthesis (queue
|
||||
// can be filled deterministically) before the (delayed) response lands as
|
||||
// a hit.
|
||||
type fakeProvider struct {
|
||||
srv *httptest.Server
|
||||
mu sync.Mutex
|
||||
@@ -124,7 +124,7 @@ func (f *fakeProvider) serve(w http.ResponseWriter, r *http.Request) {
|
||||
if d > 0 {
|
||||
select {
|
||||
case <-time.After(d):
|
||||
case <-r.Context().Done(): // Client (Daemon) hat abgebrochen
|
||||
case <-r.Context().Done(): // client (daemon) cancelled
|
||||
f.record(fakeHit{Text: body.Text, Start: start, End: time.Now()})
|
||||
return
|
||||
}
|
||||
@@ -153,7 +153,7 @@ func (f *fakeProvider) snapshot() []fakeHit {
|
||||
return out
|
||||
}
|
||||
|
||||
// completed liefert nur die abgeschlossenen (nicht abgebrochenen) Treffer.
|
||||
// completed returns only the completed (not cancelled) hits.
|
||||
func completed(hits []fakeHit) []fakeHit {
|
||||
var out []fakeHit
|
||||
for _, h := range hits {
|
||||
@@ -164,8 +164,8 @@ func completed(hits []fakeHit) []fakeHit {
|
||||
return out
|
||||
}
|
||||
|
||||
// waitArrived pollt, bis n Synthese-Requests angekommen sind — der Daemon
|
||||
// ist damit nachweisbar beschäftigt und die Queue deterministisch befüllbar.
|
||||
// waitArrived polls until n synthesis requests have arrived — the daemon is
|
||||
// then demonstrably busy and the queue can be filled deterministically.
|
||||
func (f *fakeProvider) waitArrived(t *testing.T, n int) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
@@ -178,29 +178,30 @@ func (f *fakeProvider) waitArrived(t *testing.T, n int) {
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("nur %d von %d Ankünften am Fake innerhalb 5 s", len(f.snapshot()), n)
|
||||
t.Fatalf("only %d of %d arrivals at the fake within 5 s", len(f.snapshot()), n)
|
||||
}
|
||||
|
||||
// arrivedCount zählt die angekommten Synthese-Requests (auch abgebrochene)
|
||||
// — Beobachtungspunkt für „genau N spielen, der Rest parkt/verfällt".
|
||||
// arrivedCount counts the synthesis requests that arrived (including
|
||||
// cancelled ones) — observation point for "exactly N play, the rest
|
||||
// parks/expires".
|
||||
func (f *fakeProvider) arrivedCount() int {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return len(f.arrived)
|
||||
}
|
||||
|
||||
// assertNoFurtherArrivals prüft, dass innerhalb der Frist kein weiterer
|
||||
// Synthese-Request ankommt (geparkte/verworfene Ansagen bleiben stumm).
|
||||
// assertNoFurtherArrivals checks that no further synthesis request arrives
|
||||
// within the deadline (parked/discarded announcements stay silent).
|
||||
func (f *fakeProvider) assertNoFurtherArrivals(t *testing.T, wait time.Duration) {
|
||||
t.Helper()
|
||||
n := f.arrivedCount()
|
||||
time.Sleep(wait)
|
||||
if got := f.arrivedCount(); got != n {
|
||||
t.Fatalf("%d zusätzliche Ankünfte (want keine) — Treffer: %+v", got-n, f.snapshot())
|
||||
t.Fatalf("%d additional arrivals (want none) — hits: %+v", got-n, f.snapshot())
|
||||
}
|
||||
}
|
||||
|
||||
// waitHits pollt, bis n Requests abgeschlossen wurden (Abbrüche zählen).
|
||||
// waitHits polls until n requests have finished (cancellations count).
|
||||
func (f *fakeProvider) waitHits(t *testing.T, n int) []fakeHit {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
@@ -210,12 +211,12 @@ func (f *fakeProvider) waitHits(t *testing.T, n int) []fakeHit {
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("nur %d von %d Fake-Treffern innerhalb 5 s (Treffer: %+v)", len(f.snapshot()), n, f.snapshot())
|
||||
t.Fatalf("only %d of %d fake hits within 5 s (hits: %+v)", len(f.snapshot()), n, f.snapshot())
|
||||
return nil
|
||||
}
|
||||
|
||||
// waitCompleted pollt, bis n Requests ERFOLGREICH abgeschlossen wurden
|
||||
// (abgebrochene Synthesen zählen nicht).
|
||||
// waitCompleted polls until n requests have finished SUCCESSFULLY
|
||||
// (cancelled syntheses do not count).
|
||||
func (f *fakeProvider) waitCompleted(t *testing.T, n int) []fakeHit {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
@@ -225,20 +226,20 @@ func (f *fakeProvider) waitCompleted(t *testing.T, n int) []fakeHit {
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("nur %d von %d abgeschlossenen Treffern innerhalb 5 s (alle: %+v)", len(completed(f.snapshot())), n, f.snapshot())
|
||||
t.Fatalf("only %d of %d completed hits within 5 s (all: %+v)", len(completed(f.snapshot())), n, f.snapshot())
|
||||
return nil
|
||||
}
|
||||
|
||||
// assertNoHits prüft, dass innerhalb der Frist kein Request ankam.
|
||||
// assertNoHits checks that no request arrived within the deadline.
|
||||
func (f *fakeProvider) assertNoHits(t *testing.T, wait time.Duration) {
|
||||
t.Helper()
|
||||
time.Sleep(wait)
|
||||
if hits := f.snapshot(); len(hits) > 0 {
|
||||
t.Fatalf("unerwartete Fake-Treffer: %+v", hits)
|
||||
t.Fatalf("unexpected fake hits: %+v", hits)
|
||||
}
|
||||
}
|
||||
|
||||
// daemonProc ist ein laufender talkd-Prozess.
|
||||
// daemonProc is a running talkd process.
|
||||
type daemonProc struct {
|
||||
cmd *exec.Cmd
|
||||
port int
|
||||
@@ -246,9 +247,9 @@ type daemonProc struct {
|
||||
baseURL string
|
||||
}
|
||||
|
||||
// startDaemon startet `talk daemon run` als echten Prozess (freier Port per
|
||||
// TALK_PORT, nie der Default-Port) und wartet, bis GET /status antwortet. Stop per
|
||||
// t.Cleanup: SIGINT, nach Frist Kill.
|
||||
// startDaemon starts `talk daemon run` as a real process (free port via
|
||||
// TALK_PORT, never the default port) and waits until GET /status answers.
|
||||
// Stopped via t.Cleanup: SIGINT, kill after a deadline.
|
||||
func startDaemon(t *testing.T, root, fakeURL string, extra map[string]string) *daemonProc {
|
||||
t.Helper()
|
||||
port := freePort(t)
|
||||
@@ -258,7 +259,7 @@ func startDaemon(t *testing.T, root, fakeURL string, extra map[string]string) *d
|
||||
cmd.Env = env
|
||||
cmd.Stderr = stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("talk daemon run starten: %v", err)
|
||||
t.Fatalf("starting talk daemon run: %v", err)
|
||||
}
|
||||
d := &daemonProc{
|
||||
cmd: cmd,
|
||||
@@ -280,12 +281,12 @@ func startDaemon(t *testing.T, root, fakeURL string, extra map[string]string) *d
|
||||
}
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("talk daemon antwortet nicht auf %s — stderr:\n%s", d.baseURL+"/status", stderr.String())
|
||||
t.Fatalf("talk daemon does not answer on %s — stderr:\n%s", d.baseURL+"/status", stderr.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
// stop fährt den Daemon-Prozess herunter (SIGINT wie ein menschlicher
|
||||
// Stop; nach 3 s Kill-Fallback) und wartet auf sein Ende.
|
||||
// stop shuts the daemon process down (SIGINT like a human stop; kill
|
||||
// fallback after 3 s) and waits for it to end.
|
||||
func (d *daemonProc) stop(t *testing.T) {
|
||||
t.Helper()
|
||||
if d.cmd.Process == nil {
|
||||
@@ -302,18 +303,18 @@ func (d *daemonProc) stop(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// daemonEnv baut die Prozess-Umgebung für talk-Prozesse (CLI und Daemon):
|
||||
// geerbtes Environment ohne fremde TALK_*-Variablen (Isolation von Keys/
|
||||
// Ports der Außenwelt), isoliertes HOME/XDG_CONFIG_HOME (Config-Dir liegt
|
||||
// damit unter <root>/talk), audio.sink=null und der Fake-Provider als
|
||||
// ElevenLabs-Endpunkt. port=1 heißt „garantiert toter Daemon" (Verbindung
|
||||
// wird sofort abgelehnt).
|
||||
// daemonEnv builds the process environment for talk processes (CLI and
|
||||
// daemon): the inherited environment without foreign TALK_* variables
|
||||
// (isolation from keys/ports of the outside world), isolated HOME/
|
||||
// XDG_CONFIG_HOME (the config dir is thus <root>/talk), audio.sink=null and
|
||||
// the fake provider as ElevenLabs endpoint. port=1 means "guaranteed dead
|
||||
// daemon" (connection is refused immediately).
|
||||
//
|
||||
// TALK_RATE_LIMIT=0s ist der Basis-Default der Prozessgrenzen-Tests: Die
|
||||
// Task-04-Semantik-Tests (Serialisierung, Priorität, …) enqueueen dicht
|
||||
// hintereinander und erwarten sofortiges Durchspielen — das Gesetz
|
||||
// (Default 30 s) würde sie parken. Die Gesetz-Tests in ratelimit_test.go
|
||||
// überschreiben den Wert bewusst per extra.
|
||||
// TALK_RATE_LIMIT=0s is the base default of the process-boundary tests: the
|
||||
// Task-04 semantics tests (serialization, priority, …) enqueue in quick
|
||||
// succession and expect immediate playback — the law (default 30 s) would
|
||||
// park them. The law tests in ratelimit_test.go deliberately override the
|
||||
// value via extra.
|
||||
func daemonEnv(root string, port int, fakeURL string, extra map[string]string) []string {
|
||||
env := make([]string, 0, 32)
|
||||
for _, kv := range os.Environ() {
|
||||
@@ -326,12 +327,12 @@ func daemonEnv(root string, port int, fakeURL string, extra map[string]string) [
|
||||
"HOME": root,
|
||||
"XDG_CONFIG_HOME": root,
|
||||
"TALK_AUDIO_SINK": "null",
|
||||
"TALK_ELEVENLABS_KEY": "prozesstest-key",
|
||||
"TALK_ELEVENLABS_KEY": "process-test-key",
|
||||
"TALK_ELEVENLABS_ENDPOINT": fakeURL,
|
||||
"TALK_EDGE_ENDPOINT": "ws://127.0.0.1:1/", // Kette soll nie auf echte Fremd-Endpunkte fallen
|
||||
"TALK_EDGE_ENDPOINT": "ws://127.0.0.1:1/", // the chain must never fall back to real third-party endpoints
|
||||
"TALK_PORT": strconv.Itoa(port),
|
||||
"TALK_RATE_LIMIT": "0s", // Gesetz aus, bis ein Test es anstellt (siehe oben)
|
||||
"TALK_AUTOSTART": "false", // kein Test darf unbemerkt einen Daemon hinterlassen; autostart_test.go schaltet es bewusst an
|
||||
"TALK_RATE_LIMIT": "0s", // law off until a test turns it on (see above)
|
||||
"TALK_AUTOSTART": "false", // no test may leave a daemon behind unnoticed; autostart_test.go turns it on deliberately
|
||||
}
|
||||
for k, v := range extra {
|
||||
set[k] = v
|
||||
@@ -342,32 +343,32 @@ func daemonEnv(root string, port int, fakeURL string, extra map[string]string) [
|
||||
return env
|
||||
}
|
||||
|
||||
// freePort besorgt einen freien Loopback-Port (bind-and-close; der Daemon
|
||||
// bindet ihn sofort danach neu).
|
||||
// freePort obtains a free loopback port (bind-and-close; the daemon rebinds
|
||||
// it right afterwards).
|
||||
func freePort(t *testing.T) int {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("freien Port finden: %v", err)
|
||||
t.Fatalf("finding a free port: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
return ln.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
// configDir ist das Config-Verzeichnis unter dem isolierten Root
|
||||
// (Linux-Pfadlogik: XDG_CONFIG_HOME/<root> → <root>/talk).
|
||||
// configDir is the config directory under the isolated root (Linux path
|
||||
// logic: XDG_CONFIG_HOME/<root> → <root>/talk).
|
||||
func configDir(root string) string {
|
||||
return filepath.Join(root, "talk")
|
||||
}
|
||||
|
||||
// talkResult ist das Beobachtungsergebnis eines talk-Prozesses.
|
||||
// talkResult is the observed result of a talk process.
|
||||
type talkResult struct {
|
||||
code int
|
||||
stdout string
|
||||
stderr string
|
||||
}
|
||||
|
||||
// runTalk führt die talk-CLI als echten Prozess aus.
|
||||
// runTalk runs the talk CLI as a real process.
|
||||
func runTalk(t *testing.T, env []string, args ...string) talkResult {
|
||||
t.Helper()
|
||||
cmd := exec.Command(talkBin, args...)
|
||||
@@ -380,12 +381,12 @@ func runTalk(t *testing.T, env []string, args ...string) talkResult {
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
code = ee.ExitCode()
|
||||
} else if err != nil {
|
||||
t.Fatalf("talk ausführen: %v (stderr: %s)", err, errBuf.String())
|
||||
t.Fatalf("running talk: %v (stderr: %s)", err, errBuf.String())
|
||||
}
|
||||
return talkResult{code: code, stdout: out.String(), stderr: errBuf.String()}
|
||||
}
|
||||
|
||||
// statusBody ist die dekodierte GET /status-Antwort.
|
||||
// statusBody is the decoded GET /status response.
|
||||
type statusBody struct {
|
||||
Version string `json:"version"`
|
||||
Queue int `json:"queue"`
|
||||
@@ -401,8 +402,8 @@ type statusBody struct {
|
||||
Cors []string `json:"cors"`
|
||||
}
|
||||
|
||||
// getStatus ruft GET /status auf (Deadline: eine Sekunde — der Daemon ist
|
||||
// lokal und muss antworten, sonst ist das selbst ein Testergebnis).
|
||||
// getStatus calls GET /status (deadline: one second — the daemon is local
|
||||
// and must answer, otherwise that is a test result in itself).
|
||||
func getStatus(t *testing.T, d *daemonProc) statusBody {
|
||||
t.Helper()
|
||||
client := &http.Client{Timeout: time.Second}
|
||||
@@ -412,16 +413,16 @@ func getStatus(t *testing.T, d *daemonProc) statusBody {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("GET /status: Status %d", resp.StatusCode)
|
||||
t.Fatalf("GET /status: status %d", resp.StatusCode)
|
||||
}
|
||||
var st statusBody
|
||||
if err := json.NewDecoder(resp.Body).Decode(&st); err != nil {
|
||||
t.Fatalf("GET /status dekodieren: %v", err)
|
||||
t.Fatalf("decoding GET /status: %v", err)
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
// waitStatusIdle pollt, bis weder ein Item läuft noch wartet.
|
||||
// waitStatusIdle polls until neither an item is running nor waiting.
|
||||
func waitStatusIdle(t *testing.T, d *daemonProc, timeout time.Duration) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
@@ -431,13 +432,13 @@ func waitStatusIdle(t *testing.T, d *daemonProc, timeout time.Duration) {
|
||||
return
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("Queue wird nicht idle: %+v — stderr:\n%s", st, d.stderr.String())
|
||||
t.Fatalf("queue does not become idle: %+v — stderr:\n%s", st, d.stderr.String())
|
||||
}
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// speak enqueue per rohem HTTP (Prozessgrenze ohne CLI).
|
||||
// speak enqueues via raw HTTP (process boundary without the CLI).
|
||||
func speak(t *testing.T, d *daemonProc, body string) (int, map[string]any) {
|
||||
t.Helper()
|
||||
resp, err := http.Post(d.baseURL+"/speak", "application/json", strings.NewReader(body))
|
||||
@@ -447,13 +448,13 @@ func speak(t *testing.T, d *daemonProc, body string) (int, map[string]any) {
|
||||
defer resp.Body.Close()
|
||||
var m map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&m); err != nil {
|
||||
t.Fatalf("POST /speak Antwort dekodieren: %v", err)
|
||||
t.Fatalf("decoding POST /speak response: %v", err)
|
||||
}
|
||||
return resp.StatusCode, m
|
||||
}
|
||||
|
||||
// postPath macht einen leeren POST auf einen Daemon-Endpunkt (/mute,
|
||||
// /unmute, /cancel) und dekodiert die JSON-Antwort.
|
||||
// postPath makes an empty POST to a daemon endpoint (/mute, /unmute,
|
||||
// /cancel) and decodes the JSON response.
|
||||
func postPath(t *testing.T, d *daemonProc, path string) (int, map[string]any) {
|
||||
t.Helper()
|
||||
resp, err := http.Post(d.baseURL+path, "application/json", strings.NewReader("{}"))
|
||||
@@ -463,22 +464,22 @@ func postPath(t *testing.T, d *daemonProc, path string) (int, map[string]any) {
|
||||
defer resp.Body.Close()
|
||||
var m map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&m); err != nil {
|
||||
t.Fatalf("POST %s Antwort dekodieren: %v", path, err)
|
||||
t.Fatalf("decoding POST %s response: %v", path, err)
|
||||
}
|
||||
return resp.StatusCode, m
|
||||
}
|
||||
|
||||
// rawResponse ist eine vollständig gelesene HTTP-Antwort (Status, Header,
|
||||
// Body als String) — Beobachtungsform der rohen CORS-/MCP-Tests.
|
||||
// rawResponse is a fully read HTTP response (status, header, body as a
|
||||
// string) — the observation form of the raw CORS/MCP tests.
|
||||
type rawResponse struct {
|
||||
status int
|
||||
header http.Header
|
||||
body string
|
||||
}
|
||||
|
||||
// doRequest macht einen rohen HTTP-Request mit beliebigen Headern und
|
||||
// optionalem Body und liest die Antwort komplett (CORS-Preflights und
|
||||
// MCP-JSON-RPC-Payloads sind literal, kein Client-SDK dazwischen).
|
||||
// doRequest makes a raw HTTP request with arbitrary headers and an optional
|
||||
// body and reads the response completely (CORS preflights and MCP JSON-RPC
|
||||
// payloads are literal, no client SDK in between).
|
||||
func doRequest(t *testing.T, method, url string, headers map[string]string, body string) rawResponse {
|
||||
t.Helper()
|
||||
var rd io.Reader
|
||||
@@ -487,7 +488,7 @@ func doRequest(t *testing.T, method, url string, headers map[string]string, body
|
||||
}
|
||||
req, err := http.NewRequest(method, url, rd)
|
||||
if err != nil {
|
||||
t.Fatalf("%s %s bauen: %v", method, url, err)
|
||||
t.Fatalf("building %s %s: %v", method, url, err)
|
||||
}
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
@@ -499,13 +500,13 @@ func doRequest(t *testing.T, method, url string, headers map[string]string, body
|
||||
defer resp.Body.Close()
|
||||
raw, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("%s %s Antwort lesen: %v", method, url, err)
|
||||
t.Fatalf("reading %s %s response: %v", method, url, err)
|
||||
}
|
||||
return rawResponse{status: resp.StatusCode, header: resp.Header, body: string(raw)}
|
||||
}
|
||||
|
||||
// preflight macht einen OPTIONS-Preflight gegen /speak (so wie ein Browser
|
||||
// es vor einem echten POST tut).
|
||||
// preflight makes an OPTIONS preflight against /speak (the way a browser
|
||||
// does before a real POST).
|
||||
func preflight(t *testing.T, d *daemonProc, origin string) rawResponse {
|
||||
t.Helper()
|
||||
return doRequest(t, http.MethodOptions, d.baseURL+"/speak", map[string]string{
|
||||
@@ -514,7 +515,7 @@ func preflight(t *testing.T, d *daemonProc, origin string) rawResponse {
|
||||
}, "")
|
||||
}
|
||||
|
||||
// mcpPost POSTet einen literalen JSON-RPC-Payload an /mcp.
|
||||
// mcpPost POSTs a literal JSON-RPC payload to /mcp.
|
||||
func mcpPost(t *testing.T, d *daemonProc, payload string) rawResponse {
|
||||
t.Helper()
|
||||
return doRequest(t, http.MethodPost, d.baseURL+"/mcp", map[string]string{
|
||||
@@ -522,13 +523,13 @@ func mcpPost(t *testing.T, d *daemonProc, payload string) rawResponse {
|
||||
}, payload)
|
||||
}
|
||||
|
||||
// mcpDecode dekodiert eine JSON-RPC-Antwort in eine generische Map (für
|
||||
// Feld-Zugriffe der literalen Payload-Tests).
|
||||
// mcpDecode decodes a JSON-RPC response into a generic map (for field
|
||||
// access in the literal payload tests).
|
||||
func mcpDecode(t *testing.T, raw rawResponse) map[string]any {
|
||||
t.Helper()
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal([]byte(raw.body), &m); err != nil {
|
||||
t.Fatalf("JSON-RPC-Antwort dekodieren: %v — Body: %s", err, raw.body)
|
||||
t.Fatalf("decoding JSON-RPC response: %v — body: %s", err, raw.body)
|
||||
}
|
||||
return m
|
||||
}
|
||||
Reference in new issue
Block a user