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 }