package main import ( "testing" "time" ) func TestScheduleNext(t *testing.T) { loc := time.FixedZone("CEST", 2*3600) // Wednesday 2026-09-30 10:00 now := time.Date(2026, 9, 30, 10, 0, 0, 0, loc) cases := []struct { name string sc Schedule last time.Time want time.Time }{ {"weekly monday 8:00", Schedule{Every: "week", Weekday: 1, Hour: 8}, time.Time{}, time.Date(2026, 10, 5, 8, 0, 0, 0, loc)}, {"weekly today later", Schedule{Every: "week", Weekday: 3, Hour: 18, Minute: 30}, time.Time{}, time.Date(2026, 9, 30, 18, 30, 0, 0, loc)}, {"weekly today passed", Schedule{Every: "week", Weekday: 3, Hour: 9}, time.Time{}, time.Date(2026, 10, 7, 9, 0, 0, 0, loc)}, {"daily passed", Schedule{Every: "day", Hour: 7}, time.Time{}, time.Date(2026, 10, 1, 7, 0, 0, 0, loc)}, {"hours first", Schedule{Every: "hours", Hours: 6}, time.Time{}, now.Add(time.Minute)}, {"hours after last", Schedule{Every: "hours", Hours: 6}, now.Add(-2 * time.Hour), now.Add(4 * time.Hour)}, {"hours late", Schedule{Every: "hours", Hours: 6}, now.Add(-7 * time.Hour), now.Add(time.Minute)}, } for _, c := range cases { if got := c.sc.Next(now, c.last); !got.Equal(c.want) { t.Errorf("%s: got %v, want %v", c.name, got, c.want) } } } func TestScheduleValid(t *testing.T) { for _, sc := range []Schedule{{Every: "hours"}, {Every: "hours", Hours: 200}, {Every: "week", Weekday: 7}, {Every: "day", Hour: 24}, {Every: "month"}} { if sc.Valid() { t.Errorf("%+v should be invalid", sc) } } if !(Schedule{Every: "week", Weekday: 1, Hour: 8}).Valid() { t.Error("weekly schedule should be valid") } } func TestStoreRuns(t *testing.T) { s, err := OpenStore(t.TempDir(), 2) if err != nil { t.Fatal(err) } x, err := s.Create(SavedSearch{Name: "go", Params: "language=Go", Schedule: Schedule{Every: "day"}, MaxResults: 30, Enabled: true}) if err != nil { t.Fatal(err) } t0 := time.Date(2026, 9, 1, 8, 0, 0, 0, time.UTC) add := func(id string, at time.Time, repos ...Repo) Run { t.Helper() r, err := s.AddRun(x.ID, RunInfo{ID: id, At: at}, repos) if err != nil { t.Fatal(err) } return r } r1 := add("r1", t0, Repo{FullName: "a/a", Stars: 10}, Repo{FullName: "b/b", Stars: 5}) if r1.New != 0 || r1.Items[0].New || r1.Items[0].StarsDelta != nil { t.Errorf("first run must be a baseline: %+v", r1) } r2 := add("r2", t0.Add(24*time.Hour), Repo{FullName: "a/a", Stars: 25}, Repo{FullName: "c/c", Stars: 3}) if r2.New != 1 || !r2.Items[1].New || r2.Items[0].New { t.Errorf("c/c should be the only new repository: %+v", r2.Items) } if d := r2.Items[0].StarsDelta; d == nil || *d != 15 || r2.Rising != 1 { t.Errorf("a/a should have gained 15 stars: %+v", r2.Items[0]) } // A failed run is recorded but does not break the comparison. s.AddRun(x.ID, RunInfo{ID: "r3", At: t0.Add(48 * time.Hour), Error: "boom"}, nil) r4 := add("r4", t0.Add(72*time.Hour), Repo{FullName: "b/b", Stars: 6}, Repo{FullName: "c/c", Stars: 4}) if r4.New != 0 { t.Errorf("b/b was seen in the first run, not new: %+v", r4.Items) } idx, _ := s.Runs(x.ID) if len(idx) != 2 || idx[0].ID != "r4" { t.Errorf("history should keep the 2 newest runs, got %+v", idx) } if _, err := s.Run(x.ID, "r1"); err == nil { t.Error("r1 should have been pruned") } latest, err := s.Run(x.ID, "latest") if err != nil || latest.ID != "r4" { t.Errorf("latest = %v, %v", latest.ID, err) } if err := s.Delete(x.ID); err != nil { t.Fatal(err) } if _, err := s.Get(x.ID); err == nil { t.Error("search should be deleted") } }