package activity import ( "os" "path/filepath" "testing" "time" ) // calSample matches the cal-events helper's output shape (ISO-8601 with // offset, one object per non-all-day event, tagged with its calendar name). const calSample = `[ {"title": "Standup", "start": "2026-07-07T09:00:00-05:00", "end": "2026-07-07T09:15:00-05:00", "calendar": "Archer"}, {"title": "Dentist", "start": "2026-07-07T13:00:00-05:00", "end": "2026-07-07T14:00:00-05:00", "calendar": "Personal"}, {"title": "Focus block", "start": "2026-07-07T15:00:00-05:00", "end": "2026-07-07T17:00:00-05:00", "calendar": "Focus"} ]` func TestParseEventsSample(t *testing.T) { events := parseEvents([]byte(calSample), CalendarFilter{}) if len(events) != 3 { t.Fatalf("want 3 events, got %d", len(events)) } if events[0].Title != "Standup" || events[0].Calendar != "Archer" { t.Errorf("event0 = %+v", events[0]) } wantStart, _ := time.Parse(time.RFC3339, "2026-07-07T09:00:00-05:00") if !events[0].Start.Equal(wantStart) { t.Errorf("start = %v, want %v", events[0].Start, wantStart) } if parseEvents([]byte("not json"), CalendarFilter{}) != nil { t.Error("bad output ⇒ empty slice") } } func TestCalendarFilterAllow(t *testing.T) { cases := []struct { name string filter CalendarFilter cal string want bool }{ {"empty filter passes all", CalendarFilter{}, "Personal", true}, {"show allowlist keeps listed", CalendarFilter{Show: []string{"Archer"}}, "Archer", true}, {"show allowlist drops unlisted", CalendarFilter{Show: []string{"Archer"}}, "Personal", false}, {"show is case-insensitive", CalendarFilter{Show: []string{"archer"}}, "Archer", true}, {"hide drops listed", CalendarFilter{Hide: []string{"Holidays"}}, "Holidays", false}, {"hide is case-insensitive", CalendarFilter{Hide: []string{"holidays"}}, "Holidays", false}, {"hide applies after show", CalendarFilter{Show: []string{"Archer", "Focus"}, Hide: []string{"Focus"}}, "Focus", false}, {"show+hide keeps the rest", CalendarFilter{Show: []string{"Archer", "Focus"}, Hide: []string{"Focus"}}, "Archer", true}, } for _, c := range cases { if got := c.filter.Allow(c.cal); got != c.want { t.Errorf("%s: Allow(%q)=%v, want %v", c.name, c.cal, got, c.want) } } } func TestParseEventsAppliesFilter(t *testing.T) { events := parseEvents([]byte(calSample), CalendarFilter{Show: []string{"Archer", "Focus"}}) if len(events) != 2 || events[0].Calendar != "Archer" || events[1].Calendar != "Focus" { t.Fatalf("filtered = %+v", events) } } func TestLoadCalendarDegradesGracefully(t *testing.T) { if ev, err := LoadCalendar("", "2026-07-07", CalendarFilter{}); err != nil || ev != nil { t.Errorf("empty bin ⇒ nil, nil; got %v, %v", ev, err) } if ev, err := LoadCalendar("/nonexistent/cal-events", "2026-07-07", CalendarFilter{}); err != nil || ev != nil { t.Errorf("missing helper ⇒ nil, nil; got %v, %v", ev, err) } } func TestLoadCalendarRunsHelper(t *testing.T) { // Stand in for cal-events with a shell script that echoes the sample. bin := filepath.Join(t.TempDir(), "cal-events") script := "#!/bin/sh\ncat <<'EOF'\n" + calSample + "\nEOF\n" if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { t.Fatal(err) } events, err := LoadCalendar(bin, "2026-07-07", CalendarFilter{Hide: []string{"Personal"}}) if err != nil { t.Fatal(err) } if len(events) != 2 { t.Fatalf("want 2 events after hide, got %+v", events) } }