Files
mesh-catalog/modules/dunst/cmd/dunst-tools/actions_test.go
T
jochen f00b5a19b1
mesh/merge-gate pass: builds dunst, messenger → g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group feat/plain-notifications delivered: every member is delivered
Say what the operator needs to do, keep quiet what needs nothing, and take answers from the desk
The operator could not tell from a notification whether to act, nor act without
opening a session. Each message now opens with a verdict, a warning that needs
nothing is not sent, and a notification offers answers the messenger performs
through their seat verbs; dunst's send gains actions and says the chosen one
(hq ADR 0253).
2026-10-08 13:57:33 +02:00

93 lines
3.0 KiB
Go

package main
import (
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
)
// **A notification offers answers, and the chosen one is said as an event** (novox/hq ADR 0253): the id
// is answered at once, the answer comes when the operator chooses, and nothing is said for a
// notification closed without one.
func TestANotificationsChosenAnswerIsSaidAsAnEvent(t *testing.T) {
withBus(t)
bin := fakeBinaries(t, map[string]string{"notify-send": `for a in "$@"; do printf '[%s]' "$a"; done > "$LOG"; echo 42; sleep 0.2; echo "$CHOSEN"`})
t.Setenv("LOG", filepath.Join(bin, "log"))
t.Setenv("MESH_NODE", "g14")
var mu sync.Mutex
var said []map[string]any
before := emit
emit = func(event string, body any) error {
mu.Lock()
defer mu.Unlock()
if event != ChosenEvent {
t.Errorf("event %q", event)
}
said = append(said, body.(map[string]any))
return nil
}
t.Cleanup(func() { emit = before })
n, err := notificationOf(map[string]any{"summary": "Delivery of hq held for 36 hours", "body": "Needs you: release it, or stop it.",
"actions": []any{map[string]any{"token": "a1b2", "label": "Release"}, map[string]any{"token": "c3d4", "label": "Stop"}}})
if err != nil {
t.Fatal(err)
}
t.Setenv("CHOSEN", "c3d4")
start := time.Now()
got, err := Send(n)
if err != nil || got.ID != 42 || time.Since(start) > 150*time.Millisecond {
t.Fatalf("%+v, %v, after %s: the id is answered before the answer", got, err, time.Since(start))
}
asked, _ := os.ReadFile(filepath.Join(bin, "log"))
if !strings.Contains(string(asked), "[--wait][--action=a1b2=Release][--action=c3d4=Stop][--]") {
t.Fatalf("notify-send was asked %s", asked)
}
deadline := time.Now().Add(3 * time.Second)
for {
mu.Lock()
n := len(said)
mu.Unlock()
if n > 0 || time.Now().After(deadline) {
break
}
time.Sleep(20 * time.Millisecond)
}
mu.Lock()
if len(said) != 1 || said[0]["token"] != "c3d4" || said[0]["id"] != 42 || said[0]["machine"] != "g14" {
t.Fatalf("said %v", said)
}
said = nil
mu.Unlock()
// Closed without an answer, or an answer nobody offered: nothing is said.
for _, chosen := range []string{"", "zzzz"} {
t.Setenv("CHOSEN", chosen)
if _, err := Send(n); err != nil {
t.Fatal(err)
}
}
time.Sleep(600 * time.Millisecond)
mu.Lock()
defer mu.Unlock()
if len(said) != 0 {
t.Fatalf("an answer was invented: %v", said)
}
}
func TestActionsAreRefusedForWhatTheyCannotBe(t *testing.T) {
for _, bad := range []any{"x", []any{"x"}, []any{map[string]any{"token": "a b", "label": "x"}},
[]any{map[string]any{"token": "a", "label": ""}}, []any{map[string]any{"token": "a", "label": "x=y"}},
[]any{map[string]any{"token": "a", "label": "x"}, map[string]any{"token": "a", "label": "y"}},
[]any{map[string]any{"token": "a", "label": "1"}, map[string]any{"token": "b", "label": "2"},
map[string]any{"token": "c", "label": "3"}, map[string]any{"token": "d", "label": "4"},
map[string]any{"token": "e", "label": "5"}}} {
if _, err := notificationOf(map[string]any{"summary": "x", "actions": bad}); err == nil {
t.Errorf("accepted %v", bad)
}
}
}