Better notification organization.

This commit is contained in:
Storm Dragon committed 2026-11-07 12:47:26 -05:00
1 parent 81cc630319
commit 2c02a79655
14 files changed
+1276 -40

No files matched your search

+120
View File
@@ -13,6 +13,7 @@ import (
"path/filepath"
"reflect"
"strings"
"sync"
"testing"
"time"
@@ -23,6 +24,125 @@ import (
"git.stormux.org/storm/fenrir/internal/presentation"
)
func TestNotificationSoundCooldownBoundaryAndConcurrentAdmission(t *testing.T) {
var gate notificationSoundGate
now := time.Now()
var workers sync.WaitGroup
accepted := make(chan bool, 32)
for range cap(accepted) {
workers.Go(func() { accepted <- gate.allow(now) })
}
workers.Wait()
close(accepted)
count := 0
for allowed := range accepted {
if allowed {
count++
}
}
if count != 1 {
t.Fatal("concurrent notifications admitted multiple cues", count)
}
for _, trial := range []struct {
after time.Duration
want bool
}{
{time.Second, false},
{30*time.Second - time.Nanosecond, false},
{30 * time.Second, true},
{31 * time.Second, false},
{60 * time.Second, true},
} {
if got := gate.allow(now.Add(trial.after)); got != trial.want {
t.Fatalf("after %v: allowed=%v, want %v", trial.after, got, trial.want)
}
}
}
func TestNotificationBurstLimitsCuesWithoutLosingSpeechOrHistory(t *testing.T) {
for _, mode := range []string{"enabled", "muted", "silenced"} {
t.Run(mode, func(t *testing.T) {
a, voice := persistence_application(t, true)
voice.messages = nil
sounds := &fakeSounds{}
a.cues = sounds
a.muted, voice.muted = mode == "muted", mode == "muted"
a.temporarySilence = mode == "silenced"
var want []string
for index := range 10 {
text := fmt.Sprintf("Notification %d", index)
want = append(want, text)
a.notify(text, time.Now())
if len(sounds.cues) != 1 || len(a.notifications) != index+1 || a.notifications[index].text != text {
t.Fatal("burst cue or retained message", sounds.cues, a.notifications)
}
}
if mode != "enabled" {
want = nil
}
if !reflect.DeepEqual(voice.messages, want) {
t.Fatal("burst changed speech policy", voice.messages)
}
// A mute transition shares the cooldown with ordinary notifications.
a.muted, voice.muted, a.temporarySilence = false, false, false
a.notify("Still cooling down", time.Now())
if len(sounds.cues) != 1 || voice.messages[len(voice.messages)-1] != "Still cooling down" {
t.Fatal("unmuting reset the cooldown", sounds.cues, voice.messages)
}
a.notificationSound.mu.Lock()
a.notificationSound.lastPlay = time.Now().Add(-notificationSoundInterval)
a.notificationSound.mu.Unlock()
a.notify("After cooldown", time.Now())
if len(sounds.cues) != 2 || voice.messages[len(voice.messages)-1] != "After cooldown" || len(a.notifications) != 12 {
t.Fatal("cue did not resume after cooldown", sounds.cues, voice.messages, a.notifications)
}
})
}
}
type queuedNotificationVoice struct {
fakeVoice
requests []struct {
text string
before func(context.Context)
}
}
func (v *queuedNotificationVoice) Say_after(text string, before func(context.Context)) {
v.requests = append(v.requests, struct {
text string
before func(context.Context)
}{text, before})
}
func TestQueuedNotificationsCheckCooldownAtPlaybackAndCancellation(t *testing.T) {
a, _ := persistence_application(t, true)
voice := &queuedNotificationVoice{}
a.voice.Speaker = voice
sounds := &fakeSounds{}
a.cues = sounds
a.notify("Canceled", time.Now())
ctx, cancel := context.WithCancel(context.Background())
cancel()
voice.requests[0].before(ctx)
if !a.notificationSound.lastPlay.IsZero() || len(sounds.cues) != 0 {
t.Fatal("canceled cue started the cooldown")
}
a.notify("First queued", time.Now())
// More than 30 seconds can pass while notifications wait in the speech queue.
a.notify("Second queued", time.Now().Add(time.Minute))
if !a.notificationSound.lastPlay.IsZero() || len(sounds.cues) != 0 {
t.Fatal("cue cooldown started before playback")
}
for _, request := range voice.requests[1:] {
request.before(context.Background())
voice.Say(request.text)
}
if !reflect.DeepEqual(sounds.cues, []presentation.Cue{presentation.Notification}) || !reflect.DeepEqual(voice.messages, []string{"First queued", "Second queued"}) {
t.Fatal("queued notifications accumulated sound waits or lost speech", sounds.cues, voice.messages)
}
}
func TestNotificationCuePrecedesSpeechAndOnlyPlaysForNewNotifications(t *testing.T) {
for _, mode := range []string{"enabled", "disabled", "zero volume", "missing cue", "unavailable", "muted", "silenced"} {
t.Run(mode, func(t *testing.T) {