]> git.giacomolli.uk - onechat/blobdiff - schedule_test.go
Check forced-disconnect test setup failures
[onechat] / schedule_test.go
index 6610c26b141a83a3f99bbc9f4a38946206c73f19..f93427e0fedae4661b5ed7e85b00da0ca9591b5a 100644 (file)
@@ -7,6 +7,7 @@ import (
        "strings"
        "sync"
        "testing"
        "strings"
        "sync"
        "testing"
+       "testing/synctest"
        "time"
 
        "github.com/stretchr/testify/require"
        "time"
 
        "github.com/stretchr/testify/require"
@@ -22,14 +23,14 @@ func TestSchedulesSet(t *testing.T) {
                listed string
        }{
                {
                listed string
        }{
                {
-                       name:   "a one-time schedule at a local time",
+                       name:   "sets a one-time run at its local time",
                        text:   "Remind the user to drink water.",
                        at:     new("2026-11-09 09:00"),
                        answer: "Schedule 1 set. Next: Monday 2026-11-09 09:00 -03:00.",
                        listed: "1 | Monday 2026-11-09 09:00 -03:00 | once | Remind the user to drink water.",
                },
                {
                        text:   "Remind the user to drink water.",
                        at:     new("2026-11-09 09:00"),
                        answer: "Schedule 1 set. Next: Monday 2026-11-09 09:00 -03:00.",
                        listed: "1 | Monday 2026-11-09 09:00 -03:00 | once | Remind the user to drink water.",
                },
                {
-                       name:   "a repeating schedule at the next match",
+                       name:   "sets a repeating run at its next match",
                        text:   "Tell the user\nthe time.",
                        cron:   new("*/5  * * * *"),
                        answer: "Schedule 1 set. Next: Sunday 2026-11-08 20:05 -03:00.",
                        text:   "Tell the user\nthe time.",
                        cron:   new("*/5  * * * *"),
                        answer: "Schedule 1 set. Next: Sunday 2026-11-08 20:05 -03:00.",
@@ -58,43 +59,43 @@ func TestSchedulesSetRefuses(t *testing.T) {
                message string
        }{
                {
                message string
        }{
                {
-                       name:    "an empty text",
+                       name:    "refuses an empty text",
                        text:    " \n",
                        at:      new("2026-11-09 09:00"),
                        message: "the text must have 1 to 4096 bytes",
                },
                {
                        text:    " \n",
                        at:      new("2026-11-09 09:00"),
                        message: "the text must have 1 to 4096 bytes",
                },
                {
-                       name:    "a text that is too long",
+                       name:    "refuses a text that is too long",
                        text:    strings.Repeat("x", scheduleTextBytes+1),
                        at:      new("2026-11-09 09:00"),
                        message: "the text must have 1 to 4096 bytes",
                },
                {
                        text:    strings.Repeat("x", scheduleTextBytes+1),
                        at:      new("2026-11-09 09:00"),
                        message: "the text must have 1 to 4096 bytes",
                },
                {
-                       name:    "neither at nor cron",
+                       name:    "refuses neither at nor cron",
                        text:    "Tell the user the time.",
                        message: "give either at or cron, and null for the other",
                },
                {
                        text:    "Tell the user the time.",
                        message: "give either at or cron, and null for the other",
                },
                {
-                       name:    "both at and cron",
+                       name:    "refuses both at and cron",
                        text:    "Tell the user the time.",
                        at:      new("2026-11-09 09:00"),
                        cron:    new("0 9 * * *"),
                        message: "give either at or cron, and null for the other",
                },
                {
                        text:    "Tell the user the time.",
                        at:      new("2026-11-09 09:00"),
                        cron:    new("0 9 * * *"),
                        message: "give either at or cron, and null for the other",
                },
                {
-                       name:    "a time in another layout",
+                       name:    "refuses a time in another layout",
                        text:    "Tell the user the time.",
                        at:      new("2026-11-09T09:00:00-03:00"),
                        message: "at must be a local time such as 2026-11-08 20:00",
                },
                {
                        text:    "Tell the user the time.",
                        at:      new("2026-11-09T09:00:00-03:00"),
                        message: "at must be a local time such as 2026-11-08 20:00",
                },
                {
-                       name:    "a time that is past",
+                       name:    "refuses a time that is past",
                        text:    "Tell the user the time.",
                        at:      new("2026-11-08 20:00"),
                        message: "the time Sunday 2026-11-08 20:00 -03:00 is not after now, Sunday 2026-11-08 20:00 -03:00",
                },
                {
                        text:    "Tell the user the time.",
                        at:      new("2026-11-08 20:00"),
                        message: "the time Sunday 2026-11-08 20:00 -03:00 is not after now, Sunday 2026-11-08 20:00 -03:00",
                },
                {
-                       name:    "runs less than 5 minutes apart",
+                       name:    "refuses runs less than 5 minutes apart",
                        text:    "Tell the user the time.",
                        cron:    new("* * * * *"),
                        message: "at least 5 minutes apart",
                        text:    "Tell the user the time.",
                        cron:    new("* * * * *"),
                        message: "at least 5 minutes apart",
@@ -198,7 +199,7 @@ func TestScheduleMessage(t *testing.T) {
                message  string
        }{
                {
                message  string
        }{
                {
-                       name: "a one-time run on time",
+                       name: "labels an on-time run as once",
                        schedule: Schedule{
                                ID:   3,
                                Text: "Remind the user to drink water.",
                        schedule: Schedule{
                                ID:   3,
                                Text: "Remind the user to drink water.",
@@ -208,7 +209,7 @@ func TestScheduleMessage(t *testing.T) {
                        message: "[Schedule 3] Monday 2026-11-09 09:00 -03:00, once\nRemind the user to drink water.\n" + scheduleEmptyReply,
                },
                {
                        message: "[Schedule 3] Monday 2026-11-09 09:00 -03:00, once\nRemind the user to drink water.\n" + scheduleEmptyReply,
                },
                {
-                       name: "a late repeating run",
+                       name: "marks a late run with its repeating expression",
                        schedule: Schedule{
                                ID:   4,
                                Text: "Tell the user the news.",
                        schedule: Schedule{
                                ID:   4,
                                Text: "Tell the user the news.",
@@ -251,6 +252,9 @@ func TestSchedulesRunStartsATurnWhenDue(t *testing.T) {
        require.NoError(t, err)
 
        clock.Set(clock.Now().Add(5 * time.Minute))
        require.NoError(t, err)
 
        clock.Set(clock.Now().Add(5 * time.Minute))
+
+       schedules.changed <- struct{}{}
+
        test.events.WaitFor(t, "idle", 1)
        require.Equal(t, 1, server.Calls())
 
        test.events.WaitFor(t, "idle", 1)
        require.Equal(t, 1, server.Calls())
 
@@ -283,6 +287,7 @@ func TestSchedulesRunMissedSchedulesOnceAtTheStart(t *testing.T) {
                "[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:])
                "[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, "<6>schedule ran id=1 late=11h30m0s\n<6>schedule ran id=2 late=10h30m0s\n", logs.String())
 }
        require.Equal(t, "2 | Sunday 2026-11-08 21:00 -03:00 | 0 * * * * | Tell the user the time.", schedules.List())
        require.Equal(t, "<6>schedule ran id=1 late=11h30m0s\n<6>schedule ran id=2 late=10h30m0s\n", logs.String())
 }
@@ -295,7 +300,6 @@ func TestDaemonRunsADueSchedule(t *testing.T) {
        schedules, err := openSchedules(filepath.Join(t.TempDir(), scheduleFileName), "OneChat", clock.Now, discardLog())
        require.NoError(t, err)
 
        schedules, err := openSchedules(filepath.Join(t.TempDir(), scheduleFileName), "OneChat", clock.Now, discardLog())
        require.NoError(t, err)
 
-       schedules.check = time.Millisecond
        whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
        socket, _ := daemonStartTestWithSchedules(t, newModelFake(t), nil, whatsapp, schedules, clock.Now)
        client := terminalStartTestClient(t, socket)
        whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
        socket, _ := daemonStartTestWithSchedules(t, newModelFake(t), nil, whatsapp, schedules, clock.Now)
        client := terminalStartTestClient(t, socket)
@@ -305,9 +309,43 @@ func TestDaemonRunsADueSchedule(t *testing.T) {
        require.NoError(t, err)
 
        clock.Set(clock.Now().Add(5 * time.Minute))
        require.NoError(t, err)
 
        clock.Set(clock.Now().Add(5 * time.Minute))
+
+       schedules.changed <- struct{}{}
+
        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]")
 }
 
        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 TestSchedulesCheckTheClockAfterItsWaitLimit(t *testing.T) {
+       synctest.Test(t, func(t *testing.T) {
+               whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
+               loop, events := newTestIdleTurnLoop(t, whatsapp)
+               clock := &ScheduleTestClock{now: time.Now()}
+               schedules, err := openSchedules(filepath.Join(t.TempDir(), scheduleFileName), "OneChat", clock.Now, discardLog())
+               require.NoError(t, err)
+
+               _, err = schedules.Set("Check the clock.", new(clock.Now().Add(5*time.Minute).Format(scheduleAtLayout)), nil)
+               require.NoError(t, err)
+
+               ctx, cancel := context.WithCancel(t.Context())
+               defer cancel()
+
+               done := make(chan error, 1)
+               go func() {
+                       done <- schedules.Run(ctx, loop)
+               }()
+
+               synctest.Wait()
+               require.Zero(t, loop.memory.Len())
+               clock.Set(clock.Now().Add(5 * time.Minute))
+               time.Sleep(scheduleClockWaitTimeout)
+               synctest.Wait()
+               require.Equal(t, 1, loop.memory.Len())
+               require.Contains(t, events.Last("work"), "Check the clock.")
+               cancel()
+               require.NoError(t, <-done)
+       })
+}
+
 func TestToolboxRunsTheScheduleTools(t *testing.T) {
        schedules, _ := scheduleTestOpen(t)
        box := &Toolbox{
 func TestToolboxRunsTheScheduleTools(t *testing.T) {
        schedules, _ := scheduleTestOpen(t)
        box := &Toolbox{
@@ -396,11 +434,10 @@ func scheduleTestOpen(t *testing.T) (*Schedules, string) {
 }
 
 // scheduleTestRun runs schedules in a new turn loop that calls server, until
 }
 
 // scheduleTestRun runs schedules in a new turn loop that calls server, until
-// the test ends. Run looks at the times every millisecond.
+// the test ends.
 func scheduleTestRun(t *testing.T, schedules *Schedules, server *ModelFake) *TurnTestLoop {
        t.Helper()
 
 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)
        test := newTestTurnLoop(t, server)
        ctx, cancel := context.WithCancel(t.Context())
        done := make(chan error)