package test import ( "os" "path/filepath" "strings" "testing" "time" ) // 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) { root := t.TempDir() fake := newFakeProvider(t, 0) d := startDaemon(t, root, fake.srv.URL, nil) // Mute via the HTTP API: the mute file is in the config dir. 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 { t.Fatalf("mute file missing in the config dir: %v", err) } if strings.TrimSpace(string(b)) != "true" { t.Errorf("mute file = %q, want true", b) } // Announcements are discarded: 202, muted:true, empty ID, no hit. code, m = speak(t, d, `{"text":"silence"}`) if code != 202 || m["muted"] != true || m["id"] != "" { t.Fatalf("speak while muted: %d %v, want 202 {id:, muted:true}", code, m) } fake.assertNoHits(t, 300*time.Millisecond) if st := getStatus(t, d); !st.Muted { t.Fatal("/status reports muted=false") } // Daemon restart with the same config dir: still muted. d.stop(t) d2 := startDaemon(t, root, fake.srv.URL, nil) if st := getStatus(t, d2); !st.Muted { t.Fatalf("after restart muted = false, want true (persistent)") } code, m = speak(t, d2, `{"text":"still silent"}`) if code != 202 || m["muted"] != true { t.Fatalf("speak after restart while muted: %d %v", code, m) } fake.assertNoHits(t, 300*time.Millisecond) // Unmute: announcements play again. code, m = postPath(t, d2, "/unmute") if code != 200 || m["muted"] != false { t.Fatalf("POST /unmute: %d %v", code, m) } code, m = speak(t, d2, `{"text":"back again"}`) if code != 202 || m["id"] == "" { t.Fatalf("speak after unmute: %d %v, want 202 with id", code, m) } hits := fake.waitHits(t, 1) if hits[0].Text != "back again" { t.Errorf("hit = %q, want back again", hits[0].Text) } } // 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) { root := t.TempDir() fake := newFakeProvider(t, 0) d := startDaemon(t, root, fake.srv.URL, nil) deadEnv := daemonEnv(root, 1, fake.srv.URL, nil) // Mute offline (talk mute without daemon → file directly). 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) } // The running daemon sees the state on the next announcement. if st := getStatus(t, d); !st.Muted { t.Fatal("daemon does not see the mute file written offline") } code, m := speak(t, d, `{"text":"should be discarded"}`) if code != 202 || m["muted"] != true { t.Fatalf("speak: %d %v, want discarded", code, m) } 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 { t.Fatal("daemon does not see the unmute file written offline") } }