+
+func TestSchedulesKeepTheirOwnerAcrossRestart(t *testing.T) {
+ path := filepath.Join(t.TempDir(), scheduleFileName)
+ clock := newTestClock()
+ schedules, err := openSchedules(path, "GreenOtter", clock.Now, discardLog())
+ require.NoError(t, err)
+
+ _, err = schedules.Set("Check the plants.", new(clock.Now().Add(time.Minute).Format(scheduleAtLayout)), nil)
+ require.NoError(t, err)
+
+ reopened, err := openSchedules(path, "GreenOtter", clock.Now, discardLog())
+ require.NoError(t, err)
+ require.Equal(t, "GreenOtter", reopened.sorted()[0].Owner)
+
+ _, err = openSchedules(path, "OneChat", clock.Now, discardLog())
+ require.ErrorContains(t, err, "belongs to")
+}
+
+func TestSchedulesAssignTheOwnerToSavedRootSchedules(t *testing.T) {
+ path := filepath.Join(t.TempDir(), scheduleFileName)
+ require.NoError(t, os.WriteFile(path, []byte(`{"next":1,"schedules":[{"id":1,"text":"Check the news.","due":"2026-11-09T09:00:00-03:00"}]}`), 0o600))
+
+ schedules, err := openSchedules(path, "OneChat", newTestClock().Now, discardLog())
+ require.NoError(t, err)
+ require.Equal(t, "OneChat", schedules.sorted()[0].Owner)
+
+ data, err := os.ReadFile(path)
+ require.NoError(t, err)
+ require.Contains(t, string(data), `"owner":"OneChat"`)
+}
+
+// ScheduleTestClock is a clock that a test sets while Run reads it.
+type ScheduleTestClock struct {
+ mutex sync.Mutex
+ now time.Time
+}
+
+// Now returns the time of the clock.
+func (c *ScheduleTestClock) Now() time.Time {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+
+ return c.now
+}
+
+// Set sets the time of the clock.
+func (c *ScheduleTestClock) Set(now time.Time) {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+
+ c.now = now
+}
+
+// scheduleTestOpen opens the schedules of a new file, with a clock at Sunday
+// 2026-11-08 20:00 -03:00, and returns them and the path of the file.
+func scheduleTestOpen(t *testing.T) (*Schedules, string) {
+ path := filepath.Join(t.TempDir(), scheduleFileName)
+ clock := &TestClock{
+ now: time.Date(2026, time.November, 8, 20, 0, 0, 0, time.FixedZone("", -3*60*60)),
+ }
+
+ schedules, err := openSchedules(path, "OneChat", clock.Now, discardLog())
+ require.NoError(t, err)
+
+ return schedules, path
+}
+
+// scheduleTestRun runs schedules in a new turn loop that calls server, until
+// the test ends. Run looks at the times every millisecond.
+func scheduleTestRun(t *testing.T, schedules *Schedules, server *ModelFake) *TurnTestLoop {
+ t.Helper()
+
+ schedules.check = time.Millisecond
+ test := newTestTurnLoop(t, server)
+ ctx, cancel := context.WithCancel(t.Context())
+ done := make(chan error)
+ go func() {
+ done <- schedules.Run(ctx, test.loop)
+ }()
+
+ t.Cleanup(func() {
+ cancel()
+ require.NoError(t, <-done)
+ })
+
+ return test
+}