Every playing announcement now shows a desktop notification with its text and, on Linux, a Cancel button (Abbrechen for German) that stops exactly that announcement; the notification is closed when the announcement ends. This brings back what the old talk.sh had with notify-send --action, as a default: a long announcement can be cut short without a terminal. Implementation: the freedesktop Notifications service on the session bus via godbus (pure Go): one connection per process, Notify with the action list, ActionInvoked dispatched to the announcement, CloseNotification at the end, expire_timeout 0 so the button stays for the whole announcement. Without a session bus the notify-send fallback shows a plain notification; macOS and Windows show one without a button for now. queue.CancelItem(id) cancels only the playing item. Config [notify] on_speak (default true, env TALK_NOTIFY). `talk test --toast` now shows the real thing and waits for a click; `talk doctor` names the service and whether it supports the button. Config hygiene found on the way: a key written below a [table] header belongs to that table in TOML (lang under [voice] was silently ignored). config.Load now collects unknown keys (undecoded ones and non-language keys under [voice.<provider>]) and `talk doctor` warns with the hint. Tests: process test on a private dbus-daemon with a fake org.freedesktop.Notifications service (Notify payload, cancel click aborts the synthesis, CloseNotification), queue.CancelItem, config keys. All test binaries point DBUS_SESSION_BUS_ADDRESS at a dead socket so no test ever pops a notification on the developer's desktop. flake vendorHash updated for the new dependency. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
69 lines
2.0 KiB
Go
69 lines
2.0 KiB
Go
package notify
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestMain cuts the tests off from the developer's real session bus so they
|
|
// observe the notify-send fallback instead of popping real notifications.
|
|
func TestMain(m *testing.M) {
|
|
os.Setenv("DBUS_SESSION_BUS_ADDRESS", "unix:path=/nonexistent/talk-test-bus")
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
// TestShowCallsNotifySend: on Linux, Show goes through notify-send; here a
|
|
// fake in PATH that records its arguments (a real process, no mock
|
|
// framework). Title and text arrive unchanged.
|
|
func TestShowCallsNotifySend(t *testing.T) {
|
|
if runtime.GOOS != "linux" {
|
|
t.Skip("the notify-send path exists only on Linux")
|
|
}
|
|
dir := t.TempDir()
|
|
seen := filepath.Join(dir, "seen.txt")
|
|
script := "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"" + seen + "\"\n"
|
|
if err := os.WriteFile(filepath.Join(dir, "notify-send"), []byte(script), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("PATH", dir)
|
|
|
|
if !Available() {
|
|
t.Fatal("Available = false with fake in PATH")
|
|
}
|
|
if err := Show(context.Background(), "quiet hours: build done"); err != nil {
|
|
t.Fatalf("Show: %v", err)
|
|
}
|
|
b, err := os.ReadFile(seen)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
args := strings.Split(strings.TrimSpace(string(b)), "\n")
|
|
if args[len(args)-1] != "quiet hours: build done" || args[len(args)-2] != Title {
|
|
t.Errorf("notify-send arguments = %q, want ... %q %q", args, Title, "quiet hours: build done")
|
|
}
|
|
}
|
|
|
|
// TestShowWithoutTool: a missing tool is a clear error.
|
|
func TestShowWithoutTool(t *testing.T) {
|
|
t.Setenv("PATH", t.TempDir())
|
|
if Available() {
|
|
t.Fatal("Available = true without tool")
|
|
}
|
|
if err := Show(context.Background(), "x"); err == nil {
|
|
t.Error("Show without tool: no error")
|
|
}
|
|
if !strings.Contains(Describe(), "not found") {
|
|
t.Errorf("Describe = %q", Describe())
|
|
}
|
|
}
|
|
|
|
func TestAppleString(t *testing.T) {
|
|
if got := appleString(`say "hello" \ bye`); got != `"say \"hello\" \\ bye"` {
|
|
t.Errorf("appleString = %s", got)
|
|
}
|
|
}
|