]> git.giacomolli.uk - onechat/blob - whatsapp_batch_test.go
Wait for the WhatsApp catch-up before a turn
[onechat] / whatsapp_batch_test.go
1 package main
2
3 import (
4 "os"
5 "path/filepath"
6 "testing"
7 "time"
8
9 "github.com/stretchr/testify/require"
10 )
11
12 func TestWhatsAppBatchesFollowTheTimesOfTheMessages(t *testing.T) {
13 testCases := []struct {
14 name string
15 times []time.Duration
16 sizes []int
17 }{
18 {
19 name: "messages close together form one batch",
20 times: []time.Duration{0, 10 * time.Second, 30 * time.Second},
21 sizes: []int{3},
22 },
23 {
24 name: "a quiet chat starts a new batch",
25 times: []time.Duration{0, 10 * time.Second, 31 * time.Second},
26 sizes: []int{2, 1},
27 },
28 {
29 name: "a batch ends after two minutes",
30 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},
31 sizes: []int{7, 1},
32 },
33 }
34
35 for _, tc := range testCases {
36 t.Run(tc.name, func(t *testing.T) {
37 batches := whatsappTestBatches(t, filepath.Join(t.TempDir(), whatsappStateFileName))
38 start := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
39 for index, offset := range tc.times {
40 _, err := batches.Add(whatsappTestChat, WhatsAppMessage{
41 ID: string(rune('a' + index)),
42 Time: start.Add(offset),
43 Text: "hi",
44 }, time.Now())
45 require.NoError(t, err)
46 }
47
48 require.NoError(t, batches.CloseAll())
49 var sizes []int
50 for _, batch := range batches.Closed() {
51 sizes = append(sizes, len(batch.Messages))
52 }
53
54 require.Equal(t, tc.sizes, sizes)
55 })
56 }
57 }
58
59 func TestWhatsAppBatchClosesAtFiftyMessages(t *testing.T) {
60 batches := whatsappTestBatches(t, filepath.Join(t.TempDir(), whatsappStateFileName))
61 start := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
62 for index := range whatsappBatchMessages {
63 closed, err := batches.Add(whatsappTestChat, WhatsAppMessage{
64 ID: time.Duration(index).String(),
65 Time: start,
66 Text: "hi",
67 }, time.Now())
68 require.NoError(t, err)
69 require.Equal(t, index == whatsappBatchMessages-1, closed)
70 }
71
72 require.Len(t, batches.Closed(), 1)
73 }
74
75 func TestWhatsAppBatchClosesInRealTime(t *testing.T) {
76 batches := whatsappTestBatches(t, filepath.Join(t.TempDir(), whatsappStateFileName))
77 arrival := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
78 for index, offset := range []time.Duration{0, 15 * time.Second, 30 * time.Second} {
79 _, err := batches.Add(whatsappTestChat, WhatsAppMessage{
80 ID: string(rune('a' + index)),
81 Time: arrival,
82 Text: "hi",
83 }, arrival.Add(offset))
84 require.NoError(t, err)
85 }
86
87 wait, open := batches.Wait(arrival.Add(40 * time.Second))
88 require.True(t, open)
89 require.Equal(t, 10*time.Second, wait)
90
91 closed, err := batches.CloseDue(arrival.Add(49 * time.Second))
92 require.NoError(t, err)
93 require.False(t, closed)
94
95 closed, err = batches.CloseDue(arrival.Add(50 * time.Second))
96 require.NoError(t, err)
97 require.True(t, closed)
98
99 _, open = batches.Wait(arrival.Add(50 * time.Second))
100 require.False(t, open)
101 }
102
103 func TestWhatsAppBatchesSurviveARestart(t *testing.T) {
104 path := filepath.Join(t.TempDir(), whatsappDirName, whatsappStateFileName)
105 batches := whatsappTestBatches(t, path)
106 for _, id := range []string{"a", "a", "b"} {
107 _, err := batches.Add(whatsappTestChat, WhatsAppMessage{
108 ID: id,
109 Time: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC),
110 Text: "hi " + id,
111 }, time.Now())
112 require.NoError(t, err)
113 }
114
115 info, err := os.Stat(path)
116 require.NoError(t, err)
117 require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
118
119 again := whatsappTestBatches(t, path)
120 require.NoError(t, again.CloseAll())
121 closed := again.Closed()
122 require.Len(t, closed, 1)
123 require.Equal(t, []string{"hi a", "hi b"}, []string{closed[0].Messages[0].Text, closed[0].Messages[1].Text})
124
125 require.NoError(t, again.Remove(closed[0].Number))
126 require.Empty(t, whatsappTestBatches(t, path).Closed())
127 }
128
129 func TestWhatsAppBatchesRefuseABrokenStateFile(t *testing.T) {
130 path := filepath.Join(t.TempDir(), whatsappStateFileName)
131 require.NoError(t, os.WriteFile(path, []byte("{"), 0o600))
132
133 _, err := openWhatsAppBatches(path, whatsappBatchQuiet, whatsappBatchWindow)
134 require.ErrorContains(t, err, path)
135 }
136
137 func TestWhatsAppBatchText(t *testing.T) {
138 at := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
139 testCases := []struct {
140 name string
141 batch WhatsAppBatch
142 text string
143 }{
144 {
145 name: "a private chat with a message of another person starts a turn",
146 batch: WhatsAppBatch{
147 Chat: whatsappTestChat,
148 Messages: []WhatsAppMessage{
149 {
150 Sender: "Ana Souza",
151 Time: at,
152 Text: "Are you at home?",
153 },
154 {
155 Time: at.Add(3 * time.Minute),
156 Text: "Yes",
157 Mine: true,
158 },
159 },
160 },
161 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,
162 },
163 {
164 name: "a private chat with messages of the user only starts no turn",
165 batch: WhatsAppBatch{
166 Chat: whatsappTestChat,
167 Messages: []WhatsAppMessage{
168 {
169 Time: at,
170 Text: "On my way.",
171 Mine: true,
172 },
173 },
174 },
175 text: "[WhatsApp] Ana Souza, private chat 15550002222@s.whatsapp.net, Thursday 2026-01-01\n12:00 You: On my way.",
176 },
177 {
178 name: "a group with no mention of the user starts no turn",
179 batch: WhatsAppBatch{
180 Chat: whatsappTestGroup,
181 Messages: []WhatsAppMessage{
182 {
183 Sender: "Bruno Lima",
184 Time: at,
185 Text: "Who brings the cake?",
186 },
187 },
188 },
189 text: "[WhatsApp] Family, group 120363000000000001@g.us, Thursday 2026-01-01\n12:00 Bruno Lima: Who brings the cake?",
190 },
191 {
192 name: "a group with a mention of the user starts a turn",
193 batch: WhatsAppBatch{
194 Chat: whatsappTestGroup,
195 Messages: []WhatsAppMessage{
196 {
197 Sender: "Bruno Lima",
198 Time: at,
199 Text: "@you can you bring the cake?",
200 Mention: true,
201 },
202 },
203 },
204 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,
205 },
206 {
207 name: "a group with a reply to the user starts a turn",
208 batch: WhatsAppBatch{
209 Chat: whatsappTestGroup,
210 Messages: []WhatsAppMessage{
211 {
212 Sender: "Bruno Lima",
213 Time: at,
214 Text: "Thanks!",
215 Reply: true,
216 },
217 },
218 },
219 text: "[WhatsApp] Family, group 120363000000000001@g.us, Thursday 2026-01-01\n12:00 Bruno Lima (replies to you): Thanks!\n" + whatsappBatchEmptyReply,
220 },
221 }
222
223 for _, tc := range testCases {
224 t.Run(tc.name, func(t *testing.T) {
225 require.Equal(t, tc.text, tc.batch.Text(time.UTC))
226 })
227 }
228 }
229
230 // whatsappTestBatches opens the batches of the state file at path with the
231 // times of the plan.
232 func whatsappTestBatches(t *testing.T, path string) *WhatsAppBatches {
233 t.Helper()
234
235 batches, err := openWhatsAppBatches(path, whatsappBatchQuiet, whatsappBatchWindow)
236 require.NoError(t, err)
237
238 return batches
239 }