2026-10-18 17:10:36 +02:00
|
|
|
package test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// TestMutePersistenceAcrossRestart (acceptance criterion): `talk mute` →
|
|
|
|
|
// daemon restart → still muted. Mute discards announcements as documented
|
|
|
|
|
// with 202 {id:"", muted:true} and without any provider contact.
|
|
|
|
|
func TestMutePersistenceAcrossRestart(t *testing.T) {
|
2026-10-18 17:10:36 +02:00
|
|
|
root := t.TempDir()
|
|
|
|
|
fake := newFakeProvider(t, 0)
|
|
|
|
|
d := startDaemon(t, root, fake.srv.URL, nil)
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// Mute via the HTTP API: the mute file is in the config dir.
|
2026-10-18 17:10:36 +02:00
|
|
|
code, m := postPath(t, d, "/mute")
|
|
|
|
|
if code != 200 || m["muted"] != true {
|
|
|
|
|
t.Fatalf("POST /mute: %d %v", code, m)
|
|
|
|
|
}
|
|
|
|
|
muteFile := filepath.Join(configDir(root), "mute")
|
|
|
|
|
b, err := os.ReadFile(muteFile)
|
|
|
|
|
if err != nil {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("mute file missing in the config dir: %v", err)
|
2026-10-18 17:10:36 +02:00
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(string(b)) != "true" {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Errorf("mute file = %q, want true", b)
|
2026-10-18 17:10:36 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// Announcements are discarded: 202, muted:true, empty ID, no hit.
|
|
|
|
|
code, m = speak(t, d, `{"text":"silence"}`)
|
2026-10-18 17:10:36 +02:00
|
|
|
if code != 202 || m["muted"] != true || m["id"] != "" {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("speak while muted: %d %v, want 202 {id:, muted:true}", code, m)
|
2026-10-18 17:10:36 +02:00
|
|
|
}
|
|
|
|
|
fake.assertNoHits(t, 300*time.Millisecond)
|
|
|
|
|
if st := getStatus(t, d); !st.Muted {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatal("/status reports muted=false")
|
2026-10-18 17:10:36 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// Daemon restart with the same config dir: still muted.
|
2026-10-18 17:10:36 +02:00
|
|
|
d.stop(t)
|
|
|
|
|
d2 := startDaemon(t, root, fake.srv.URL, nil)
|
|
|
|
|
|
|
|
|
|
if st := getStatus(t, d2); !st.Muted {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("after restart muted = false, want true (persistent)")
|
2026-10-18 17:10:36 +02:00
|
|
|
}
|
2026-10-24 11:15:35 +02:00
|
|
|
code, m = speak(t, d2, `{"text":"still silent"}`)
|
2026-10-18 17:10:36 +02:00
|
|
|
if code != 202 || m["muted"] != true {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("speak after restart while muted: %d %v", code, m)
|
2026-10-18 17:10:36 +02:00
|
|
|
}
|
|
|
|
|
fake.assertNoHits(t, 300*time.Millisecond)
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// Unmute: announcements play again.
|
2026-10-18 17:10:36 +02:00
|
|
|
code, m = postPath(t, d2, "/unmute")
|
|
|
|
|
if code != 200 || m["muted"] != false {
|
|
|
|
|
t.Fatalf("POST /unmute: %d %v", code, m)
|
|
|
|
|
}
|
2026-10-24 11:15:35 +02:00
|
|
|
code, m = speak(t, d2, `{"text":"back again"}`)
|
2026-10-18 17:10:36 +02:00
|
|
|
if code != 202 || m["id"] == "" {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("speak after unmute: %d %v, want 202 with id", code, m)
|
2026-10-18 17:10:36 +02:00
|
|
|
}
|
|
|
|
|
hits := fake.waitHits(t, 1)
|
2026-10-24 11:15:35 +02:00
|
|
|
if hits[0].Text != "back again" {
|
|
|
|
|
t.Errorf("hit = %q, want back again", hits[0].Text)
|
2026-10-18 17:10:36 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// TestOfflineMuteFileIsReadByDaemon: a mute file written offline (CLI,
|
|
|
|
|
// without daemon) takes effect in the daemon immediately — both read the
|
|
|
|
|
// same source.
|
|
|
|
|
func TestOfflineMuteFileIsReadByDaemon(t *testing.T) {
|
2026-10-18 17:10:36 +02:00
|
|
|
root := t.TempDir()
|
|
|
|
|
fake := newFakeProvider(t, 0)
|
|
|
|
|
d := startDaemon(t, root, fake.srv.URL, nil)
|
|
|
|
|
deadEnv := daemonEnv(root, 1, fake.srv.URL, nil)
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// Mute offline (talk mute without daemon → file directly).
|
2026-10-18 17:10:36 +02:00
|
|
|
res := runTalk(t, deadEnv, "mute")
|
|
|
|
|
if res.code != 0 || res.stdout != "muted\n" {
|
|
|
|
|
t.Fatalf("talk mute (offline): %d %q — stderr: %s", res.code, res.stdout, res.stderr)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// The running daemon sees the state on the next announcement.
|
2026-10-18 17:10:36 +02:00
|
|
|
if st := getStatus(t, d); !st.Muted {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatal("daemon does not see the mute file written offline")
|
2026-10-18 17:10:36 +02:00
|
|
|
}
|
2026-10-24 11:15:35 +02:00
|
|
|
code, m := speak(t, d, `{"text":"should be discarded"}`)
|
2026-10-18 17:10:36 +02:00
|
|
|
if code != 202 || m["muted"] != true {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatalf("speak: %d %v, want discarded", code, m)
|
2026-10-18 17:10:36 +02:00
|
|
|
}
|
|
|
|
|
fake.assertNoHits(t, 300*time.Millisecond)
|
|
|
|
|
|
|
|
|
|
res = runTalk(t, deadEnv, "unmute")
|
|
|
|
|
if res.code != 0 {
|
|
|
|
|
t.Fatalf("talk unmute (offline): %d — stderr: %s", res.code, res.stderr)
|
|
|
|
|
}
|
|
|
|
|
if st := getStatus(t, d); st.Muted {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatal("daemon does not see the unmute file written offline")
|
2026-10-18 17:10:36 +02:00
|
|
|
}
|
|
|
|
|
}
|