Files
mesh-catalog/modules/dunst/cmd/dunst-tools/actions_test.go
T
jochen 2a16d39a3d
mesh/merge-gate fail: builds dunst → g14, shanks; no bus step; 2 problem(s) the change brings; the first: a machine (f35): nothing could be sent to it with…
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
dunst: a tap chooses a notification's action, so a touchpad can answer the desk (hq issue 360)
The actions sat on the middle click, which the operator's touchpads cannot give, and the context
menu named dmenu, which dunst's PATH does not hold. The left click is now do_action, close_current
(one action runs, several open the menu, none closes), the menu is rofi, declared as a package,
Super+N opens it from the keyboard through i3's contribution, and send refuses the token default,
which dunst would run at a tap without the menu.
2026-10-10 00:07:07 +02:00

112 lines
3.6 KiB
Go

package main
import (
"fmt"
"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), "[--expire-time=0][--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": "default", "label": "x"}, map[string]any{"token": "b", "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)
}
}
}
// **A bundle that started says so**, so the operator channel shows again the answers lost with the one
// before.
func TestABundleThatStartedSaysSo(t *testing.T) {
t.Setenv("MESH_NODE", "g14")
var said []string
before := emit
emit = func(event string, body any) error {
said = append(said, fmt.Sprintf("%s %v", event, body))
return nil
}
t.Cleanup(func() { emit = before })
sayStarted()
if len(said) != 1 || said[0] != "started map[machine:g14]" {
t.Fatalf("%v", said)
}
}