+package main
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestWhatsAppBatchesFollowTheTimesOfTheMessages(t *testing.T) {
+ testCases := []struct {
+ name string
+ times []time.Duration
+ sizes []int
+ }{
+ {
+ name: "messages close together form one batch",
+ times: []time.Duration{0, 10 * time.Second, 30 * time.Second},
+ sizes: []int{3},
+ },
+ {
+ name: "a quiet chat starts a new batch",
+ times: []time.Duration{0, 10 * time.Second, 31 * time.Second},
+ sizes: []int{2, 1},
+ },
+ {
+ name: "a batch ends after two minutes",
+ times: []time.Duration{0, 20 * time.Second, 40 * time.Second, 60 * time.Second, 80 * time.Second, 100 * time.Second, 120 * time.Second, 140 * time.Second},
+ sizes: []int{7, 1},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ batches := whatsappTestBatches(t, filepath.Join(t.TempDir(), whatsappStateFileName))
+ start := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
+ for index, offset := range tc.times {
+ _, err := batches.Add(whatsappTestChat, WhatsAppMessage{
+ ID: string(rune('a' + index)),
+ Time: start.Add(offset),
+ Text: "hi",
+ }, time.Now())
+ require.NoError(t, err)
+ }
+
+ require.NoError(t, batches.CloseAll())
+ var sizes []int
+ for _, batch := range batches.Closed() {
+ sizes = append(sizes, len(batch.Messages))
+ }
+
+ require.Equal(t, tc.sizes, sizes)
+ })
+ }
+}
+
+func TestWhatsAppBatchClosesAtFiftyMessages(t *testing.T) {
+ batches := whatsappTestBatches(t, filepath.Join(t.TempDir(), whatsappStateFileName))
+ start := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
+ for index := range whatsappBatchMessages {
+ closed, err := batches.Add(whatsappTestChat, WhatsAppMessage{
+ ID: time.Duration(index).String(),
+ Time: start,
+ Text: "hi",
+ }, time.Now())
+ require.NoError(t, err)
+ require.Equal(t, index == whatsappBatchMessages-1, closed)
+ }
+
+ require.Len(t, batches.Closed(), 1)
+}
+
+func TestWhatsAppBatchClosesInRealTime(t *testing.T) {
+ batches := whatsappTestBatches(t, filepath.Join(t.TempDir(), whatsappStateFileName))
+ arrival := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
+ for index, offset := range []time.Duration{0, 15 * time.Second, 30 * time.Second} {
+ _, err := batches.Add(whatsappTestChat, WhatsAppMessage{
+ ID: string(rune('a' + index)),
+ Time: arrival,
+ Text: "hi",
+ }, arrival.Add(offset))
+ require.NoError(t, err)
+ }
+
+ wait, open := batches.Wait(arrival.Add(40 * time.Second))
+ require.True(t, open)
+ require.Equal(t, 10*time.Second, wait)
+
+ closed, err := batches.CloseDue(arrival.Add(49 * time.Second))
+ require.NoError(t, err)
+ require.False(t, closed)
+
+ closed, err = batches.CloseDue(arrival.Add(50 * time.Second))
+ require.NoError(t, err)
+ require.True(t, closed)
+
+ _, open = batches.Wait(arrival.Add(50 * time.Second))
+ require.False(t, open)
+}
+
+func TestWhatsAppBatchesSurviveARestart(t *testing.T) {
+ path := filepath.Join(t.TempDir(), whatsappDirName, whatsappStateFileName)
+ batches := whatsappTestBatches(t, path)
+ for _, id := range []string{"a", "a", "b"} {
+ _, err := batches.Add(whatsappTestChat, WhatsAppMessage{
+ ID: id,
+ Time: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC),
+ Text: "hi " + id,
+ }, time.Now())
+ require.NoError(t, err)
+ }
+
+ info, err := os.Stat(path)
+ require.NoError(t, err)
+ require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
+
+ again := whatsappTestBatches(t, path)
+ require.NoError(t, again.CloseAll())
+ closed := again.Closed()
+ require.Len(t, closed, 1)
+ require.Equal(t, []string{"hi a", "hi b"}, []string{closed[0].Messages[0].Text, closed[0].Messages[1].Text})
+
+ require.NoError(t, again.Remove(closed[0].Number))
+ require.Empty(t, whatsappTestBatches(t, path).Closed())
+}
+
+func TestWhatsAppBatchesRefuseABrokenStateFile(t *testing.T) {
+ path := filepath.Join(t.TempDir(), whatsappStateFileName)
+ require.NoError(t, os.WriteFile(path, []byte("{"), 0o600))
+
+ _, err := openWhatsAppBatches(path, whatsappBatchQuiet, whatsappBatchWindow)
+ require.ErrorContains(t, err, path)
+}
+
+func TestWhatsAppBatchText(t *testing.T) {
+ at := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
+ testCases := []struct {
+ name string
+ batch WhatsAppBatch
+ text string
+ }{
+ {
+ name: "a private chat with a message of another person starts a turn",
+ batch: WhatsAppBatch{
+ Chat: whatsappTestChat,
+ Messages: []WhatsAppMessage{
+ {
+ Sender: "Ana Souza",
+ Time: at,
+ Text: "Are you at home?",
+ },
+ {
+ Time: at.Add(3 * time.Minute),
+ Text: "Yes",
+ Mine: true,
+ },
+ },
+ },
+ text: "[WhatsApp] Ana Souza, private chat 15550002222@s.whatsapp.net, Thursday 2026-01-01\n12:00 Ana Souza: Are you at home?\n12:03 You: Yes\n" + whatsappBatchEmptyReply,
+ },
+ {
+ name: "a private chat with messages of the user only starts no turn",
+ batch: WhatsAppBatch{
+ Chat: whatsappTestChat,
+ Messages: []WhatsAppMessage{
+ {
+ Time: at,
+ Text: "On my way.",
+ Mine: true,
+ },
+ },
+ },
+ text: "[WhatsApp] Ana Souza, private chat 15550002222@s.whatsapp.net, Thursday 2026-01-01\n12:00 You: On my way.",
+ },
+ {
+ name: "a group with no mention of the user starts no turn",
+ batch: WhatsAppBatch{
+ Chat: whatsappTestGroup,
+ Messages: []WhatsAppMessage{
+ {
+ Sender: "Bruno Lima",
+ Time: at,
+ Text: "Who brings the cake?",
+ },
+ },
+ },
+ text: "[WhatsApp] Family, group 120363000000000001@g.us, Thursday 2026-01-01\n12:00 Bruno Lima: Who brings the cake?",
+ },
+ {
+ name: "a group with a mention of the user starts a turn",
+ batch: WhatsAppBatch{
+ Chat: whatsappTestGroup,
+ Messages: []WhatsAppMessage{
+ {
+ Sender: "Bruno Lima",
+ Time: at,
+ Text: "@you can you bring the cake?",
+ Mention: true,
+ },
+ },
+ },
+ text: "[WhatsApp] Family, group 120363000000000001@g.us, Thursday 2026-01-01\n12:00 Bruno Lima (mentions you): @you can you bring the cake?\n" + whatsappBatchEmptyReply,
+ },
+ {
+ name: "a group with a reply to the user starts a turn",
+ batch: WhatsAppBatch{
+ Chat: whatsappTestGroup,
+ Messages: []WhatsAppMessage{
+ {
+ Sender: "Bruno Lima",
+ Time: at,
+ Text: "Thanks!",
+ Reply: true,
+ },
+ },
+ },
+ text: "[WhatsApp] Family, group 120363000000000001@g.us, Thursday 2026-01-01\n12:00 Bruno Lima (replies to you): Thanks!\n" + whatsappBatchEmptyReply,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ require.Equal(t, tc.text, tc.batch.Text(time.UTC))
+ })
+ }
+}
+
+// whatsappTestBatches opens the batches of the state file at path with the
+// times of the plan.
+func whatsappTestBatches(t *testing.T, path string) *WhatsAppBatches {
+ t.Helper()
+
+ batches, err := openWhatsAppBatches(path, whatsappBatchQuiet, whatsappBatchWindow)
+ require.NoError(t, err)
+
+ return batches
+}