feat(notify): Desktop-Benachrichtigung für den Ruhezeit-Toast

quiet_mode = "toast" war bisher nur eine Protokollzeile. Jetzt zeigt der
Daemon eine Desktop-Benachrichtigung — über die Bordmittel des Systems,
exec-basiert wie osbuiltin, ohne Bibliothek und ohne CGO: notify-send
(Linux), osascript display notification (macOS), PowerShell Toast
(Windows; Text base64, kein Quoting-Problem). Best effort: fehlt das
Werkzeug, bleibt es bei der Protokollzeile plus Start-Warnung; `talk
doctor` zeigt den Weg unter „Toast". Der Aufruf läuft asynchron, ein
hängender Notification-Daemon blockiert die Queue nie.

Queue: OnToast-Hook, gefeuert bei Toast-Verwurf beim Eintritt und im Worker
(nie unter dem Mutex). Tests: notify-Paket mit fake notify-send im PATH,
Prozessgrenze: Ruhezeit + toast → fake notify-send bekommt Titel und Text,
der Provider keinen Treffer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
m3ta-chironandClaude Fable 5.1 committed 2026-10-24 10:37:46 +02:00
1 parent a9ca32bed9
commit 9e2aa506a6
7 files changed
+279 -6

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
package notify
import (
"context"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
// TestShowRuftNotifySend: Auf Linux geht Show über notify-send — hier ein
// Fake im PATH, der seine Argumente protokolliert (echter Prozess, kein
// Mock-Framework). Titel und Text kommen unverändert an.
func TestShowRuftNotifySend(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("notify-send-Weg gibt es nur auf 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 mit Fake im PATH")
}
if err := Show(context.Background(), "Ruhezeit: Build fertig"); 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] != "Ruhezeit: Build fertig" || args[len(args)-2] != Title {
t.Errorf("notify-send-Argumente = %q, want … %q %q", args, Title, "Ruhezeit: Build fertig")
}
}
// TestShowOhneWerkzeug: fehlt das Werkzeug, ist das ein klarer Fehler.
func TestShowOhneWerkzeug(t *testing.T) {
t.Setenv("PATH", t.TempDir())
if Available() {
t.Fatal("Available = true ohne Werkzeug")
}
if err := Show(context.Background(), "x"); err == nil {
t.Error("Show ohne Werkzeug: kein Fehler")
}
if !strings.Contains(Describe(), "nicht gefunden") {
t.Errorf("Describe = %q", Describe())
}
}
func TestAppleString(t *testing.T) {
if got := appleString(`sag "hallo" \ tschüss`); got != `"sag \"hallo\" \\ tschüss"` {
t.Errorf("appleString = %s", got)
}
}