X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/e737005f89c7c663e1636830001730ad79396bdc..2daf91497c2cc70d7156f7c0d99b1451933a818a:/schedule_test.go?ds=sidebyside diff --git a/schedule_test.go b/schedule_test.go index c13f023..96063aa 100644 --- a/schedule_test.go +++ b/schedule_test.go @@ -1,9 +1,12 @@ package main import ( + "context" + "log" "os" "path/filepath" "strings" + "sync" "testing" "time" @@ -126,7 +129,7 @@ func TestSchedulesSurviveARestart(t *testing.T) { require.NoError(t, err) require.Equal(t, os.FileMode(0o600), info.Mode().Perm()) - reopened, err := openSchedules(path, schedules.clock) + reopened, err := openSchedules(path, schedules.clock, discardLog()) require.NoError(t, err) require.Equal(t, "2 | Monday 2026-11-09 08:00 -03:00 | 0 8 * * 1-5 | Tell the user the news.", reopened.List()) @@ -228,22 +231,81 @@ func TestOpenSchedulesRefusesABadCron(t *testing.T) { path := filepath.Join(t.TempDir(), scheduleFileName) require.NoError(t, os.WriteFile(path, []byte(`{"next":1,"schedules":[{"id":1,"text":"x","cron":"* * * * *","due":"2026-11-09T09:00:00-03:00"}]}`), 0o600)) - _, err := openSchedules(path, time.Now) + _, err := openSchedules(path, time.Now, discardLog()) require.ErrorContains(t, err, "schedule 1: the runs of") } -// 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) { +func TestSchedulesRunStartsATurnWhenDue(t *testing.T) { + clock := &ScheduleTestClock{ + now: time.Date(2026, time.November, 8, 20, 0, 0, 0, time.FixedZone("", -3*60*60)), + } + + logs := &SyncBuffer{} + schedules, err := openSchedules(filepath.Join(t.TempDir(), scheduleFileName), clock.Now, log.New(logs, "", 0)) + require.NoError(t, err) + + server := newModelFake(t) + test := scheduleTestRun(t, schedules, server) + + _, err = schedules.Set("Remind the user to drink water.", new("2026-11-08 20:05"), nil) + require.NoError(t, err) + + clock.Set(clock.Now().Add(5 * time.Minute)) + test.events.WaitFor(t, "idle", 1) + require.Equal(t, 1, server.Calls()) + + texts := turnInputTexts(t, server.Call(0)) + require.Equal(t, "[Schedule 1] Sunday 2026-11-08 20:05 -03:00, once\nRemind the user to drink water.\n"+scheduleEmptyReply, texts[len(texts)-1]) + require.Equal(t, scheduleSource, test.events.LastEvent("work").Source) + require.Equal(t, "No schedules.", schedules.List()) + require.Equal(t, "schedule 1 runs\n", logs.String()) +} + +func TestSchedulesRunMissedSchedulesOnceAtTheStart(t *testing.T) { path := filepath.Join(t.TempDir(), scheduleFileName) - clock := &TestClock{ + require.NoError(t, os.WriteFile(path, []byte(`{"next":2,"schedules":[{"id":1,"text":"Remind the user to call.","cron":"","due":"2026-11-08T09:00:00-03:00"},{"id":2,"text":"Tell the user the time.","cron":"0 * * * *","due":"2026-11-08T10:00:00-03:00"}]}`), 0o600)) + + clock := &ScheduleTestClock{ + now: time.Date(2026, time.November, 8, 20, 30, 0, 0, time.FixedZone("", -3*60*60)), + } + + logs := &SyncBuffer{} + schedules, err := openSchedules(path, clock.Now, log.New(logs, "", 0)) + require.NoError(t, err) + + server := newModelFake(t) + test := scheduleTestRun(t, schedules, server) + test.events.WaitFor(t, "idle", 1) + require.Equal(t, 1, server.Calls()) + + texts := turnInputTexts(t, server.Call(0)) + require.Equal(t, []string{ + "[Schedule 1] Sunday 2026-11-08 09:00 -03:00, once\nRemind the user to call.\n" + scheduleLateLine + "\n" + scheduleEmptyReply, + "[Schedule 2] Sunday 2026-11-08 10:00 -03:00, repeats 0 * * * *\nTell the user the time.\n" + scheduleLateLine + "\n" + scheduleEmptyReply, + }, texts[len(texts)-2:]) + require.Equal(t, "2 | Sunday 2026-11-08 21:00 -03:00 | 0 * * * * | Tell the user the time.", schedules.List()) + require.Equal(t, "schedule 1 runs 11h30m0s late, because the daemon was stopped at its time\nschedule 2 runs 10h30m0s late, because the daemon was stopped at its time\n", logs.String()) +} + +func TestDaemonRunsADueSchedule(t *testing.T) { + clock := &ScheduleTestClock{ now: time.Date(2026, time.November, 8, 20, 0, 0, 0, time.FixedZone("", -3*60*60)), } - schedules, err := openSchedules(path, clock.Now) + schedules, err := openSchedules(filepath.Join(t.TempDir(), scheduleFileName), clock.Now, discardLog()) require.NoError(t, err) - return schedules, path + schedules.check = time.Millisecond + whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock()) + socket, _ := daemonStartTestWithSchedules(t, newModelFake(t), nil, whatsapp, schedules, clock.Now) + client := terminalStartTestClient(t, socket) + client.WaitFor(t, "[connected to") + + _, err = schedules.Set("Remind the user to drink water.", new("2026-11-08 20:05"), nil) + require.NoError(t, err) + + clock.Set(clock.Now().Add(5 * time.Minute)) + client.WaitFor(t, "[Schedule 1] Sunday 2026-11-08 20:05 -03:00, once\nRemind the user to drink water.\n"+scheduleEmptyReply+"\n[working]\na line\n[done]") } func TestToolboxRunsTheScheduleTools(t *testing.T) { @@ -266,3 +328,60 @@ func TestSubagentHasNoScheduleTools(t *testing.T) { require.Equal(t, `error: unknown tool "schedule_list"`, box.run(t.Context(), "schedule_list", `{}`)) require.Equal(t, `error: unknown tool "schedule_remove"`, box.run(t.Context(), "schedule_remove", `{"id":1}`)) } + +// 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, 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 +}