When using the default Speech Dispatcher address, try the user's Unix socket first and then /run/speech-dispatcher/speechd.sock if the user socket is missing or refuses the connection. This allows Fenrir to use an existing system service before attempting to autospawn a user daemon. Keep explicit Options.Socket, SPEECHD_ADDRESS, and SPEECHD_SOCKET settings authoritative. Do not fall back for permission errors, TCP connections, or a canceled context. If both sockets fail, preserve the user socket's error so autospawn continues to target the user's endpoint. After opening the data connection, use its selected Unix socket for the control connection and disable further fallback there. Both connections must belong to the same Speech Dispatcher server. Add tests for address overrides, user-first selection, missing and stale user sockets, permission errors, and preservation of the original error when neither socket is available. Validation: - go test ./internal/speech -count=1 (GOPROXY=off): passed - git diff --check: passed - gofmt -l on all five changed Go files: no output Live audible speech and physical keyboard behavior were not verified as part of this commit.
145 lines
4.9 KiB
Go
145 lines
4.9 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package speech
|
|
|
|
import (
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestResolveServerAddressPrecedence(t *testing.T) {
|
|
t.Setenv("SUDO_UID", "")
|
|
runtimeDirectory := t.TempDir()
|
|
cacheDirectory := t.TempDir()
|
|
t.Setenv("XDG_RUNTIME_DIR", runtimeDirectory)
|
|
t.Setenv("XDG_CACHE_HOME", cacheDirectory)
|
|
t.Setenv("SPEECHD_SOCKET", "/tmp/legacy-speech.sock")
|
|
t.Setenv("SPEECHD_ADDRESS", "unix_socket:/tmp/address-speech.sock")
|
|
|
|
for _, test := range []struct {
|
|
name, override, speechAddress, legacySocket, runtimeDir, want string
|
|
}{
|
|
{"explicit", "/tmp/explicit-speech.sock", "unix_socket:/tmp/address-speech.sock", "/tmp/legacy-speech.sock", runtimeDirectory, "/tmp/explicit-speech.sock"},
|
|
{"address", "", "unix_socket:/tmp/address-speech.sock", "/tmp/legacy-speech.sock", runtimeDirectory, "/tmp/address-speech.sock"},
|
|
{"legacy", "", "", "/tmp/legacy-speech.sock", runtimeDirectory, "/tmp/legacy-speech.sock"},
|
|
{"runtime", "", "", "", runtimeDirectory, filepath.Join(runtimeDirectory, "speech-dispatcher", "speechd.sock")},
|
|
{"cache", "", "", "", "", filepath.Join(cacheDirectory, "speech-dispatcher", "speechd.sock")},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Setenv("SPEECHD_ADDRESS", test.speechAddress)
|
|
t.Setenv("SPEECHD_SOCKET", test.legacySocket)
|
|
t.Setenv("XDG_RUNTIME_DIR", test.runtimeDir)
|
|
got, err := resolve_server_address(test.override)
|
|
if err != nil || got.network != "unix" || got.address != test.want {
|
|
t.Fatalf("address = %+v, error = %v; want %q", got, err, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseServerAddress(t *testing.T) {
|
|
for _, test := range []struct {
|
|
input, network, address string
|
|
}{
|
|
{"unix_socket:/tmp/speech.sock", "unix", "/tmp/speech.sock"},
|
|
{"inet_socket", "tcp4", "127.0.0.1:6560"},
|
|
{"inet_socket:localhost:6561", "tcp4", "localhost:6561"},
|
|
{"inet_socket::6561", "tcp4", "127.0.0.1:6561"},
|
|
} {
|
|
got, err := parse_server_address(test.input)
|
|
if err != nil || got.network != test.network || got.address != test.address {
|
|
t.Fatalf("parse %q = %+v, %v", test.input, got, err)
|
|
}
|
|
}
|
|
for _, address := range []string{"invalid:/tmp/speech.sock", "unix_socket:relative.sock", "inet_socket:localhost:0", "inet_socket:localhost:bad"} {
|
|
if _, err := parse_server_address(address); err == nil {
|
|
t.Fatalf("accepted invalid address %q", address)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSpeechAddressUsedByClientAndDiscovery(t *testing.T) {
|
|
for _, network := range []string{"unix", "tcp4"} {
|
|
t.Run(network, func(t *testing.T) {
|
|
var server *fakeServer
|
|
if network == "unix" {
|
|
server = new_fake_server(t, false)
|
|
} else {
|
|
listener, err := net.Listen("tcp4", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
server = new_fake_server_listener(t, false, listener)
|
|
}
|
|
address := "unix_socket:" + server.path
|
|
if network == "tcp4" {
|
|
address = "inet_socket:" + server.path
|
|
}
|
|
t.Setenv("SPEECHD_ADDRESS", address)
|
|
t.Setenv("SPEECHD_SOCKET", "/absent.sock")
|
|
t.Setenv("SPEECHD_CMD", "/nonexistent/speech-dispatcher")
|
|
client, err := Prepare("speech-dispatcher", Options{Module: "fixture", Voice: "Base Voice"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer client.Close()
|
|
if _, err := Discover(Options{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := client.Say("connection fixture"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wait_event(t, server.events, "say:connection fixture")
|
|
if err := client.Cancel(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wait_event(t, server.events, "CANCEL 41")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInvalidAddressDoesNotFallBack(t *testing.T) {
|
|
server := new_fake_server(t, false)
|
|
t.Setenv("SPEECHD_ADDRESS", "unsupported:address")
|
|
t.Setenv("SPEECHD_SOCKET", server.path)
|
|
if _, err := Open(Options{}); err == nil || !strings.Contains(err.Error(), "unsupported") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDefaultSocketWithoutRuntimeOrCacheEnvironment(t *testing.T) {
|
|
t.Setenv("SUDO_UID", "")
|
|
t.Setenv("XDG_RUNTIME_DIR", "")
|
|
t.Setenv("XDG_CACHE_HOME", "")
|
|
t.Setenv("HOME", t.TempDir())
|
|
want := filepath.Join(os.Getenv("HOME"), ".cache", "speech-dispatcher", "speechd.sock")
|
|
if got := default_socket_path(); got != want {
|
|
t.Fatalf("socket %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestAutomaticServerAddressHonorsOverrides(t *testing.T) {
|
|
t.Setenv("SPEECHD_ADDRESS", "")
|
|
t.Setenv("SPEECHD_SOCKET", "")
|
|
if !automatic_server_address("") {
|
|
t.Fatal("default address disables system fallback")
|
|
}
|
|
if automatic_server_address("/custom.sock") {
|
|
t.Fatal("explicit socket allows system fallback")
|
|
}
|
|
t.Setenv("SPEECHD_SOCKET", "/legacy.sock")
|
|
if automatic_server_address("") {
|
|
t.Fatal("legacy socket allows system fallback")
|
|
}
|
|
t.Setenv("SPEECHD_ADDRESS", "unix_socket:/address.sock")
|
|
t.Setenv("SPEECHD_SOCKET", "")
|
|
if automatic_server_address("") {
|
|
t.Fatal("explicit address allows system fallback")
|
|
}
|
|
}
|