Files
talk/internal/queue/quiet_test.go
T
m3ta-chironandClaude Fable 5.1 85fa64cfe6 refactor: switch the whole project to English
talk is going to be published. Code comments, CLI output, error messages,
daemon log lines, MCP descriptions and results, test names and assertions,
README, spec, skill, build scripts and flake are now English. Only the
spoken announcement texts stay bilingual: the wrap result and the test
sentence are German or English depending on the detected language.

Language fallback is now configurable: `lang = "en" | "de"` (env
TALK_LANG, default en) is used when detection cannot tell (numbers, single
words, ties) and for talk's own announcements. langdetect.Detect takes the
fallback as an argument instead of hard-coding German. `talk setup` asks for
it; `talk doctor` and `talk status` show it.

Vocabulary pinned across packages so the process-boundary tests observe
stable strings: chain log lines ("chain: tier X skipped (not available)",
"falling back to the next tier", "spoken via X"), queue lines ("discarded:
quiet hours ... active", "replaced by toast", "max_age"), CLI notes
("speaking directly", "daemon started at", "no daemon", "nothing to cancel"),
status block with aligned English labels, MCP messages, origin guard 403.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 11:15:35 +02:00

133 lines
4.1 KiB
Go

package queue
import (
"testing"
"time"
)
// local builds a local test time (hour and minute suffice; quiet windows
// work at minute resolution).
func local(hour, minute int) time.Time {
return time.Date(2026, 10, 18, hour, minute, 0, 0, time.Local)
}
// TestParseQuietHours: format HH:MM-HH:MM (may span midnight), clear errors
// for garbage.
func TestParseQuietHours(t *testing.T) {
cases := []struct {
in string
start clockTime
end clockTime
bad bool
}{
{in: "18:00-08:00", start: clockTime{18, 0}, end: clockTime{8, 0}},
{in: "9:30-17:45", start: clockTime{9, 30}, end: clockTime{17, 45}},
{in: "22:00-06:00", start: clockTime{22, 0}, end: clockTime{6, 0}},
{in: "08:00-08:00", start: clockTime{8, 0}, end: clockTime{8, 0}}, // disabled but valid
{in: ""}, // empty = disabled, no error
{in: "18:00", bad: true},
{in: "18:00-", bad: true},
{in: "-08:00", bad: true},
{in: "24:00-08:00", bad: true},
{in: "18:60-08:00", bad: true},
{in: "1800-0800", bad: true},
{in: "ab:00-08:00", bad: true},
}
for _, tc := range cases {
q, err := ParseQuietHours(tc.in)
if tc.bad {
if err == nil {
t.Errorf("ParseQuietHours(%q) = %+v, want error", tc.in, q)
}
continue
}
if err != nil {
t.Errorf("ParseQuietHours(%q): unexpected error %v", tc.in, err)
continue
}
if q.Start != tc.start || q.End != tc.end {
t.Errorf("ParseQuietHours(%q) = %v-%v, want %v-%v", tc.in, q.Start, q.End, tc.start, tc.end)
}
}
}
// TestQuietHoursActive: day window, night window across midnight and the
// disabled zero value across all boundaries (start inclusive, end
// exclusive).
func TestQuietHoursActive(t *testing.T) {
cases := []struct {
window string
hour, minute int
want bool
}{
// day window 09:00-17:00
{"09:00-17:00", 8, 59, false},
{"09:00-17:00", 9, 0, true}, // start inclusive
{"09:00-17:00", 12, 30, true},
{"09:00-17:00", 16, 59, true},
{"09:00-17:00", 17, 0, false}, // end exclusive
{"09:00-17:00", 23, 0, false},
// night window 18:00-08:00 (spans midnight)
{"18:00-08:00", 17, 59, false},
{"18:00-08:00", 18, 0, true},
{"18:00-08:00", 23, 59, true},
{"18:00-08:00", 0, 0, true}, // past midnight
{"18:00-08:00", 7, 59, true},
{"18:00-08:00", 8, 0, false}, // end exclusive
{"18:00-08:00", 12, 0, false},
// another night window
{"22:00-06:00", 3, 0, true},
{"22:00-06:00", 21, 59, false},
// start == end: disabled
{"08:00-08:00", 8, 0, false},
{"08:00-08:00", 20, 0, false},
}
for _, tc := range cases {
q := mustQuiet(t, tc.window)
if got := q.Active(local(tc.hour, tc.minute)); got != tc.want {
t.Errorf("%s at %02d:%02d: Active = %v, want %v", tc.window, tc.hour, tc.minute, got, tc.want)
}
}
}
// TestQuietHoursUntil: window end (today, or tomorrow for night windows),
// zero time when inactive.
func TestQuietHoursUntil(t *testing.T) {
// Day window: ends today.
q := mustQuiet(t, "09:00-17:00")
if want := local(17, 0); !q.Until(local(12, 0)).Equal(want) {
t.Errorf("Until(12:00) = %v, want %v (today 17:00)", q.Until(local(12, 0)), want)
}
if !q.Until(local(3, 0)).IsZero() {
t.Error("Until outside the window: want zero time")
}
// Night window: from 18:00 the end is tomorrow morning.
n := mustQuiet(t, "18:00-08:00")
if want := local(0, 0).Add(32 * time.Hour); !n.Until(local(20, 0)).Equal(want) {
t.Errorf("Until(20:00) = %v, want %v (tomorrow 08:00)", n.Until(local(20, 0)), want)
}
// Past midnight (00:30) the end is today 08:00.
if want := local(8, 0); !n.Until(local(0, 30)).Equal(want) {
t.Errorf("Until(00:30) = %v, want %v (today 08:00)", n.Until(local(0, 30)), want)
}
}
// TestQuietHoursStringRoundtrip: String returns the configured form and
// parses back identically; the zero value renders empty.
func TestQuietHoursStringRoundtrip(t *testing.T) {
for _, s := range []string{"18:00-08:00", "09:00-17:00", "22:00-06:00"} {
q := mustQuiet(t, s)
if q.String() != s {
t.Errorf("String() = %q, want %q", q.String(), s)
}
q2 := mustQuiet(t, q.String())
if q2 != q {
t.Errorf("round trip of %q: %+v != %+v", s, q2, q)
}
}
if (QuietHours{}).String() != "" {
t.Error("zero value String is not empty")
}
}