13 "github.com/stretchr/testify/require"
15 "go.mau.fi/whatsmeow/proto/waE2E"
16 "go.mau.fi/whatsmeow/proto/waWeb"
17 "go.mau.fi/whatsmeow/types"
18 "go.mau.fi/whatsmeow/types/events"
21 func TestWhatsAppStatusFollowsTheConnection(t *testing.T) {
23 whatsappTestSession(t, dir, true)
24 clock := newTestClock()
25 whatsapp, fake, _ := whatsappStartTest(t, dir, clock)
26 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
29 require.Equal(t, "WhatsApp is linked and not connected yet.", whatsapp.Status())
31 require.True(t, fake.Emit(&events.Connected{}))
32 require.Equal(t, "WhatsApp is linked and connected since 2026-01-01 12:00.", whatsapp.Status())
34 clock.now = clock.now.Add(20 * time.Minute)
35 require.True(t, fake.Emit(&events.Disconnected{}))
36 clock.now = clock.now.Add(time.Minute)
37 require.True(t, fake.Emit(&events.Disconnected{}))
38 require.Equal(t, "WhatsApp is linked and disconnected since 2026-01-01 12:20. The last connection started at 2026-01-01 12:00.", whatsapp.Status())
41 func TestWhatsAppStatusOfAFirstConnectionThatFailed(t *testing.T) {
43 whatsappTestSession(t, dir, true)
44 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
45 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
48 require.True(t, fake.Emit(&events.Disconnected{}))
49 require.Equal(t, "WhatsApp is linked and disconnected since 2026-01-01 12:00.", whatsapp.Status())
52 func TestWhatsAppIsNotLinkedWithoutALinkedDevice(t *testing.T) {
53 testCases := []struct {
55 session func(t *testing.T, dir string)
58 name: "no session file",
59 session: func(t *testing.T, dir string) {
63 name: "a session of a pairing that did not end",
64 session: func(t *testing.T, dir string) {
65 whatsappTestSession(t, dir, false)
70 for _, tc := range testCases {
71 t.Run(tc.name, func(t *testing.T) {
74 whatsapp, _, _ := whatsappStartTest(t, dir, newTestClock())
75 require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status())
80 func TestWhatsAppRefusesASessionThatOthersCanRead(t *testing.T) {
82 whatsappTestSession(t, dir, true)
83 require.NoError(t, os.Chmod(filepath.Join(dir, whatsappDirName, whatsappSessionFileName), 0o644))
85 _, err := openWhatsApp(t.Context(), dir, time.Now, whatsappDiscardLog(), whatsappNewClient)
86 require.EqualError(t, err, "whatsapp/session.db holds the WhatsApp session, so its mode must be 0600")
89 func TestWhatsAppCommandsAnswerOnlyTheClientThatAsked(t *testing.T) {
90 server := newModelFake(t)
91 telegram := newTelegramFake(t)
92 bot, _ := telegramStartTestBot(t, telegram)
93 socket := daemonStartTestWithBot(t, server, bot)
94 asking := terminalStartTestClient(t, socket)
95 other := terminalStartTestClient(t, socket)
96 asking.WaitFor(t, "[connected to OneChat]")
97 other.WaitFor(t, "[connected to OneChat]")
99 asking.Type("/whatsapp status")
100 asking.WaitFor(t, "WhatsApp is not linked.")
101 asking.Type(" /whatsapp unknown ")
102 asking.WaitFor(t, whatsappUsage)
103 asking.Type("/whatsapp pair 0123")
104 asking.WaitFor(t, "PHONE must be the number of your WhatsApp account")
106 telegram.Add(TelegramUpdate{
108 Message: telegramOwnerMessage("/whatsapp status"),
111 telegramWaitForSent(t, telegram, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.")
112 require.Len(t, telegram.Sent(), 1)
113 require.Equal(t, "[connected to OneChat]\n", other.Output())
114 require.Zero(t, server.Calls())
117 func TestWhatsAppPairsWithACode(t *testing.T) {
119 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
120 server := newModelFake(t)
121 socket, memory := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
122 asking := terminalStartTestClient(t, socket)
123 other := terminalStartTestClient(t, socket)
124 asking.WaitFor(t, "[connected to OneChat]")
125 other.WaitFor(t, "[connected to OneChat]")
127 asking.Type("/whatsapp pair +1 555-000-1111")
128 items := <-fake.pairings
129 items <- whatsmeow.QRChannelItem{
130 Event: whatsmeow.QRChannelEventCode,
133 asking.WaitFor(t, whatsappPairSteps+"\n"+whatsappTestCode+"\n")
134 require.True(t, fake.Emit(&events.PairSuccess{
135 ID: types.NewADJID(whatsappTestNumber, 0, 1),
136 LID: types.NewADJID(whatsappTestLID, 0, 1),
138 items <- whatsmeow.QRChannelSuccess
139 asking.WaitFor(t, "WhatsApp is linked to +15550001111.")
141 require.Equal(t, []string{whatsappTestNumber}, fake.Phones())
142 require.Equal(t, "WhatsApp is linked and not connected yet.", whatsapp.Status())
143 require.Equal(t, "[connected to OneChat]\n", other.Output())
144 require.Zero(t, server.Calls())
145 require.Equal(t, []string{"WhatsApp is linked."}, whatsappTestLogTexts(t, memory))
147 info, err := os.Stat(filepath.Join(dir, whatsappDirName))
148 require.NoError(t, err)
149 require.Equal(t, os.FileMode(0o700), info.Mode().Perm())
151 info, err = os.Stat(filepath.Join(dir, whatsappDirName, whatsappSessionFileName))
152 require.NoError(t, err)
153 require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
156 func TestWhatsAppPairingCodeIsATelegramMessageAlone(t *testing.T) {
157 whatsapp, fake, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
158 telegram := newTelegramFake(t)
159 bot, _ := telegramStartTestBot(t, telegram)
160 daemonStartTestWithWhatsApp(t, newModelFake(t), bot, whatsapp)
162 telegram.Add(TelegramUpdate{
164 Message: telegramOwnerMessage("/whatsapp pair 15550001111"),
167 items := <-fake.pairings
168 items <- whatsmeow.QRChannelItem{
169 Event: whatsmeow.QRChannelEventCode,
172 telegramWaitForSent(t, telegram, whatsappPairSteps, whatsappTestCode)
175 func TestWhatsAppPairingEnds(t *testing.T) {
176 testCases := []struct {
178 end func(items chan whatsmeow.QRChannelItem)
182 name: "the code expires",
183 end: func(items chan whatsmeow.QRChannelItem) {
184 items <- whatsmeow.QRChannelTimeout
186 answer: whatsappPairExpired,
189 name: "the library closes the pairing",
190 end: func(items chan whatsmeow.QRChannelItem) {
193 answer: whatsappPairExpired,
196 name: "the pairing fails",
197 end: func(items chan whatsmeow.QRChannelItem) {
198 items <- whatsmeow.QRChannelItem{
199 Event: whatsmeow.QRChannelEventError,
200 Error: errors.New("bad handshake"),
203 answer: "The pairing failed: bad handshake. Run /whatsapp pair PHONE again.",
207 for _, tc := range testCases {
208 t.Run(tc.name, func(t *testing.T) {
209 whatsapp, fake, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
210 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
211 client := terminalStartTestClient(t, socket)
212 client.Type("/whatsapp pair 15550001111")
214 items := <-fake.pairings
215 items <- whatsmeow.QRChannelItem{
216 Event: whatsmeow.QRChannelEventCode,
219 client.WaitFor(t, whatsappTestCode)
221 client.WaitFor(t, tc.answer)
222 require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status())
227 func TestWhatsAppNewPairingEndsTheOneBefore(t *testing.T) {
228 whatsapp, fake, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
229 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
230 client := terminalStartTestClient(t, socket)
232 client.Type("/whatsapp pair 15550001111")
234 client.Type("/whatsapp pair 15550002222")
235 items := <-fake.pairings
236 items <- whatsmeow.QRChannelItem{
237 Event: whatsmeow.QRChannelEventCode,
240 client.WaitFor(t, whatsappTestCode)
241 require.Equal(t, []string{"15550002222"}, fake.Phones())
242 require.NotContains(t, client.Output(), "did not start the pairing")
245 func TestWhatsAppPairWhenLinked(t *testing.T) {
247 whatsappTestSession(t, dir, true)
248 whatsapp, _, _ := whatsappStartTest(t, dir, newTestClock())
249 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
250 client := terminalStartTestClient(t, socket)
252 client.Type("/whatsapp pair 15550001111")
253 client.WaitFor(t, "WhatsApp is linked already. Run /whatsapp unlink first.")
256 func TestWhatsAppUnlink(t *testing.T) {
257 testCases := []struct {
263 name: "with a logout",
264 answer: "WhatsApp is unlinked.",
267 name: "with no logout",
268 logoutErr: errors.New("not connected"),
269 answer: "WhatsApp is unlinked here, but the phone did not get the logout. In WhatsApp on your phone, open Linked devices and remove the device.",
273 for _, tc := range testCases {
274 t.Run(tc.name, func(t *testing.T) {
276 whatsappTestSession(t, dir, true)
277 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
278 fake.logoutErr = tc.logoutErr
279 socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
280 client := terminalStartTestClient(t, socket)
283 client.Type("/whatsapp unlink")
284 client.WaitFor(t, tc.answer)
285 require.Equal(t, 1, fake.Logouts())
286 require.NoFileExists(t, filepath.Join(dir, whatsappDirName, whatsappSessionFileName))
287 require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status())
288 require.Equal(t, []string{"WhatsApp is unlinked."}, whatsappTestLogTexts(t, memory))
290 client.Type("/whatsapp unlink")
291 client.WaitFor(t, "WhatsApp is not linked.\n")
296 func TestWhatsAppLogoutFromThePhone(t *testing.T) {
298 whatsappTestSession(t, dir, true)
299 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
300 telegram := newTelegramFake(t)
301 bot, _ := telegramStartTestBot(t, telegram)
302 socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), bot, whatsapp)
303 client := terminalStartTestClient(t, socket)
304 client.WaitFor(t, "[connected to OneChat]")
307 require.True(t, fake.Emit(&events.LoggedOut{}))
308 notice := "WhatsApp was unlinked from the phone. Run /whatsapp pair PHONE to link it again."
309 client.WaitFor(t, notice)
310 telegramWaitForSent(t, telegram, notice)
311 require.NoFileExists(t, filepath.Join(dir, whatsappDirName, whatsappSessionFileName))
312 require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status())
313 require.Equal(t, []string{"WhatsApp was unlinked from the phone."}, whatsappTestLogTexts(t, memory))
316 func TestWhatsAppLongDisconnectionGivesOneNotice(t *testing.T) {
318 whatsappTestSession(t, dir, true)
319 clock := newTestClock()
320 whatsapp, fake, _ := whatsappStartTest(t, dir, clock)
321 whatsapp.noticeDelay = 300 * time.Millisecond
322 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
323 client := terminalStartTestClient(t, socket)
324 client.WaitFor(t, "[connected to OneChat]")
327 require.True(t, fake.Emit(&events.Connected{}))
328 require.True(t, fake.Emit(&events.Disconnected{}))
329 require.True(t, fake.Emit(&events.Connected{}))
330 time.Sleep(400 * time.Millisecond)
331 require.Equal(t, "[connected to OneChat]\n", client.Output())
333 clock.now = clock.now.Add(time.Hour)
334 require.True(t, fake.Emit(&events.Disconnected{}))
335 require.True(t, fake.Emit(&events.Disconnected{}))
336 client.WaitFor(t, "WhatsApp is disconnected since 2026-01-01 13:00. OneChat tries again.\n")
337 require.True(t, fake.Emit(&events.Disconnected{}))
338 time.Sleep(400 * time.Millisecond)
340 require.True(t, fake.Emit(&events.Connected{}))
341 client.WaitFor(t, "WhatsApp is connected again.\n")
342 require.Equal(t, 1, strings.Count(client.Output(), "WhatsApp is disconnected since"))
345 func TestWhatsAppDisconnectionWithNoRetryGivesANoticeAtOnce(t *testing.T) {
347 whatsappTestSession(t, dir, true)
348 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
349 socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
350 client := terminalStartTestClient(t, socket)
351 client.WaitFor(t, "[connected to OneChat]")
354 require.True(t, fake.Emit(&events.StreamReplaced{}))
355 client.WaitFor(t, "WhatsApp is disconnected and does not connect again by itself: stream replaced. Restart the daemon to try again.")
358 func TestWhatsAppLogsNoOwnNumber(t *testing.T) {
360 whatsappTestSession(t, dir, true)
361 whatsapp, fake, logs := whatsappStartTest(t, dir, newTestClock())
362 fake.connectErr = errors.New("no route for " + whatsappTestNumber + "@s.whatsapp.net and " + whatsappTestLID + "@lid")
363 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
366 require.Eventually(t, func() bool {
367 return logs.String() != ""
368 }, 10*time.Second, time.Millisecond)
369 require.Equal(t, "connect: no route for OWN@s.whatsapp.net and OWN@lid\n", logs.String())
372 func TestWhatsAppPhone(t *testing.T) {
373 testCases := []struct {
379 name: "a number with a plus and dashes",
380 phone: "+1-555-000-1111",
384 name: "a number with no country code",
385 phone: "0115550001111",
389 name: "a short number",
394 name: "a long number",
395 phone: "1234567890123456",
399 name: "a number with letters",
400 phone: "1555ABC1111",
405 for _, tc := range testCases {
406 t.Run(tc.name, func(t *testing.T) {
407 _, valid := whatsappPhone(tc.phone)
408 require.Equal(t, tc.valid, valid)
413 func TestWhatsAppCommandArgs(t *testing.T) {
414 testCases := []struct {
421 name: "a command with its arguments",
422 line: " /whatsapp pair +1 555 ",
427 name: "the command alone",
433 name: "a word that starts with the command",
434 line: "/whatsappstatus",
439 name: "a message that names the command",
440 line: "what does /whatsapp status do?",
446 for _, tc := range testCases {
447 t.Run(tc.name, func(t *testing.T) {
448 args, found := whatsappCommandArgs(tc.line)
449 require.Equal(t, tc.args, args)
450 require.Equal(t, tc.found, found)
455 // whatsappTestLogTexts returns the texts of the messages in the log of memory,
456 // after the log holds at least one message.
457 func whatsappTestLogTexts(t *testing.T, memory *Memory) []string {
460 require.Eventually(t, func() bool {
461 return memory.Len() > 0
462 }, 10*time.Second, time.Millisecond)
465 for id := range memory.Len() {
466 message, err := memory.Message(id)
467 require.NoError(t, err)
469 texts = append(texts, message.Text)
475 func TestWhatsAppPrivateBatchStartsATurnWithAnEmptyReply(t *testing.T) {
476 whatsapp, fake := whatsappStartTestLinked(t)
477 require.NoError(t, whatsapp.device.Contacts.PutContactName(t.Context(), whatsappTestAna, "Ana", "Ana Souza"))
479 server := newModelFake(t)
480 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
481 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5))
484 telegram := newTelegramFake(t)
485 bot, _ := telegramStartTestBot(t, telegram)
486 socket, memory := daemonStartTestWithWhatsApp(t, server, bot, whatsapp)
487 client := terminalStartTestClient(t, socket)
488 client.WaitFor(t, "[connected to OneChat]")
491 // The catch-up holds the batch open until its end, so the two messages
492 // form one batch also on a slow machine.
493 require.True(t, fake.Emit(&events.Connected{}))
494 at := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
495 require.True(t, fake.Emit(whatsappTestMessage(whatsappTestAna, whatsappTestAna, "m1", "Are you at home?", at)))
496 require.True(t, fake.Emit(whatsappTestMessage(whatsappTestAna, whatsappTestOwn, "m2", "Yes", at)))
497 require.True(t, fake.Emit(&events.OfflineSyncCompleted{}))
499 text := "[WhatsApp] Ana Souza, private chat 15550002222@s.whatsapp.net, Thursday 2026-01-01\n12:00 Ana Souza: Are you at home?\n12:00 You: Yes\n" + whatsappBatchEmptyReply
500 client.WaitFor(t, text+"\n[working]\n[done]\n")
501 require.Equal(t, 1, server.Calls())
502 require.Contains(t, string(server.Call(0).raw), "Are you at home?")
503 require.Equal(t, []string{text}, whatsappTestLogTexts(t, memory))
504 require.Empty(t, telegram.Sent())
505 require.Eventually(t, func() bool {
506 return len(whatsapp.batches.Closed()) == 0
507 }, 10*time.Second, time.Millisecond)
510 func TestWhatsAppGroupTurns(t *testing.T) {
511 testCases := []struct {
513 message func(event *events.Message)
519 message: func(event *events.Message) {
521 line: "12:00 Bruno Lima: Who brings the cake?",
525 message: func(event *events.Message) {
526 event.Message = &waE2E.Message{
527 ExtendedTextMessage: &waE2E.ExtendedTextMessage{
528 Text: new("@" + whatsappTestLID + " can you bring the cake? Call " + whatsappTestNumber + "."),
529 ContextInfo: &waE2E.ContextInfo{
530 MentionedJID: []string{whatsappTestLID + "@lid"},
535 line: "12:00 Bruno Lima (mentions you): @you can you bring the cake? Call OWN.",
540 message: func(event *events.Message) {
541 event.Message = &waE2E.Message{
542 ExtendedTextMessage: &waE2E.ExtendedTextMessage{
543 Text: new("Who brings the cake?"),
544 ContextInfo: &waE2E.ContextInfo{
545 Participant: new(whatsappTestOwn.String()),
550 line: "12:00 Bruno Lima (replies to you): Who brings the cake?",
555 for _, tc := range testCases {
556 t.Run(tc.name, func(t *testing.T) {
557 whatsapp, fake := whatsappStartTestLinked(t)
558 fake.groups = []*types.GroupInfo{
560 JID: whatsappTestFamily,
561 GroupName: types.GroupName{
567 server := newModelFake(t)
568 socket, memory := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
569 client := terminalStartTestClient(t, socket)
570 client.WaitFor(t, "[connected to OneChat]")
571 whatsappTestConnect(t, fake)
573 event := whatsappTestMessage(whatsappTestFamily, whatsappTestBruno, "m1", "Who brings the cake?", time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
574 event.Info.PushName = "Bruno Lima"
576 require.True(t, fake.Emit(event))
578 text := "[WhatsApp] Family, group 120363000000000001@g.us, Thursday 2026-01-01\n" + tc.line
580 text += "\n" + whatsappBatchEmptyReply
581 client.WaitFor(t, "[done]")
582 require.Equal(t, 1, server.Calls())
584 client.WaitFor(t, text)
585 time.Sleep(100 * time.Millisecond)
586 require.Zero(t, server.Calls())
589 require.Equal(t, text, whatsappTestLogTexts(t, memory)[0])
594 func TestWhatsAppTakesOnlyText(t *testing.T) {
595 at := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
596 testCases := []struct {
598 change func(event *events.Message)
602 change: func(event *events.Message) {
608 change: func(event *events.Message) {
609 event.SourceWebMsg = &waWeb.WebMessageInfo{}
613 name: "a view-once message",
614 change: func(event *events.Message) {
615 event.IsViewOnce = true
619 name: "a disappearing message",
620 change: func(event *events.Message) {
621 event.IsEphemeral = true
625 name: "the chat of the user with itself",
626 change: func(event *events.Message) {
627 event.Info.Chat = whatsappTestOwn
631 name: "a status broadcast",
632 change: func(event *events.Message) {
633 event.Info.Chat = types.StatusBroadcastJID
637 name: "a photo with a caption",
638 change: func(event *events.Message) {
639 event.Message = &waE2E.Message{
640 ImageMessage: &waE2E.ImageMessage{
641 Caption: new("look"),
647 name: "a text of spaces",
648 change: func(event *events.Message) {
649 event.Message.Conversation = new(" ")
654 for _, tc := range testCases {
655 t.Run(tc.name, func(t *testing.T) {
656 whatsapp, _ := whatsappStartTestLinked(t)
657 event := whatsappTestMessage(whatsappTestAna, whatsappTestAna, "m1", "hi", at)
659 require.True(t, whatsapp.handle(event))
660 require.NoError(t, whatsapp.batches.CloseAll())
661 require.Empty(t, whatsapp.batches.Closed())
666 // whatsappStartTestLinked opens a WhatsApp with a linked device in a temporary
667 // directory, with a quiet time of 50 ms for its batches.
668 func whatsappStartTestLinked(t *testing.T) (*WhatsApp, *WhatsAppFake) {
672 whatsappTestSession(t, dir, true)
673 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
674 whatsapp.batches.quiet = 50 * time.Millisecond
676 return whatsapp, fake
679 func TestWhatsAppCatchupGivesOneTurn(t *testing.T) {
680 whatsapp, fake := whatsappStartTestLinked(t)
681 server := newModelFake(t)
682 socket, memory := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
683 client := terminalStartTestClient(t, socket)
684 client.WaitFor(t, "[connected to OneChat]")
687 require.True(t, fake.Emit(&events.Connected{}))
688 at := time.Date(2026, 1, 1, 8, 0, 0, 0, time.UTC)
689 for index, event := range []*events.Message{
690 whatsappTestMessage(whatsappTestAna, whatsappTestAna, "m1", "Good morning", at),
691 whatsappTestMessage(whatsappTestBruno, whatsappTestBruno, "m2", "Call me", at.Add(time.Hour)),
692 whatsappTestMessage(whatsappTestAna, whatsappTestAna, "m3", "Are you there?", at.Add(2*time.Hour)),
694 event.Info.PushName = []string{"Ana", "Bruno", "Ana"}[index]
695 require.True(t, fake.Emit(event))
698 time.Sleep(100 * time.Millisecond)
699 require.Zero(t, memory.Len())
701 require.True(t, fake.Emit(&events.OfflineSyncCompleted{}))
702 client.WaitFor(t, "[done]")
703 require.Equal(t, []string{
704 "[WhatsApp] Ana, private chat 15550002222@s.whatsapp.net, Thursday 2026-01-01\n08:00 Ana: Good morning\n" + whatsappBatchEmptyReply,
705 "[WhatsApp] Bruno, private chat 15550003333@s.whatsapp.net, Thursday 2026-01-01\n09:00 Bruno: Call me\n" + whatsappBatchEmptyReply,
706 "[WhatsApp] Ana, private chat 15550002222@s.whatsapp.net, Thursday 2026-01-01\n10:00 Ana: Are you there?\n" + whatsappBatchEmptyReply,
707 }, whatsappTestLogTexts(t, memory)[:3])
709 time.Sleep(100 * time.Millisecond)
710 require.Equal(t, 1, server.Calls())
711 for _, text := range []string{"Good morning", "Call me", "Are you there?"} {
712 require.Contains(t, string(server.Call(0).raw), text)
716 func TestWhatsAppCatchupEndsAfterItsLimit(t *testing.T) {
717 whatsapp, fake := whatsappStartTestLinked(t)
718 whatsapp.catchupLimit = 100 * time.Millisecond
719 socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
720 client := terminalStartTestClient(t, socket)
721 client.WaitFor(t, "[connected to OneChat]")
724 require.True(t, fake.Emit(&events.Connected{}))
725 require.True(t, fake.Emit(whatsappTestMessage(whatsappTestAna, whatsappTestOwn, "m1", "On my way.", time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))))
726 client.WaitFor(t, "12:00 You: On my way.")
727 require.Len(t, whatsappTestLogTexts(t, memory), 1)
730 func TestWhatsAppBatchesOfTheStateFileWaitForTheCatchup(t *testing.T) {
732 whatsappTestSession(t, dir, true)
733 before, _, _ := whatsappStartTest(t, dir, newTestClock())
734 _, err := before.batches.Add(whatsappTestChat, WhatsAppMessage{
737 Time: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC),
738 Text: "Did you see my message?",
739 }, time.Now().Add(-time.Hour))
740 require.NoError(t, err)
742 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
743 socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
744 client := terminalStartTestClient(t, socket)
745 client.WaitFor(t, "[connected to OneChat]")
748 time.Sleep(100 * time.Millisecond)
749 require.Zero(t, memory.Len())
751 require.True(t, fake.Emit(&events.Connected{}))
752 require.True(t, fake.Emit(&events.OfflineSyncCompleted{}))
753 client.WaitFor(t, "12:00 Ana Souza: Did you see my message?")
754 client.WaitFor(t, "[done]")
757 // whatsappTestConnect waits for the connection of the daemon and gives it the
758 // events of a connection with an empty catch-up.
759 func whatsappTestConnect(t *testing.T, fake *WhatsAppFake) {
763 require.True(t, fake.Emit(&events.Connected{}))
764 require.True(t, fake.Emit(&events.OfflineSyncCompleted{}))
767 func TestWhatsAppFindMatchesNamesWithNoRegardToCaseAndAccents(t *testing.T) {
768 whatsapp, fake := whatsappStartTestLinked(t)
769 fake.groups = []*types.GroupInfo{
771 JID: whatsappTestFamily,
772 GroupName: types.GroupName{
773 Name: "Família Souza",
778 whatsappTestContacts(t, whatsapp)
779 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
780 whatsappTestConnect(t, fake)
782 testCases := []struct {
788 name: "no accent finds an accent",
790 answer: "15550004444@s.whatsapp.net | João Silva | contact",
793 name: "a part of a name finds contacts and groups",
795 answer: "15550002222@s.whatsapp.net | Ana Souza | contact\n120363000000000001@g.us | Família Souza | group",
798 name: "a business name",
800 answer: "15550005555@s.whatsapp.net | Corner Bakery | contact",
805 answer: "No contact or group has a name with \"Zed\".",
809 for _, tc := range testCases {
810 t.Run(tc.name, func(t *testing.T) {
811 answer, err := whatsapp.Find(t.Context(), tc.query)
812 require.NoError(t, err)
813 require.Equal(t, tc.answer, answer)
818 func TestWhatsAppFindGivesAtMostTwentyMatches(t *testing.T) {
819 whatsapp, _ := whatsappStartTestLinked(t)
820 for index := range 25 {
821 jid := types.NewJID(fmt.Sprintf("1555001%04d", index), types.DefaultUserServer)
822 require.NoError(t, whatsapp.device.Contacts.PutContactName(t.Context(), jid, "", fmt.Sprintf("Member %02d", index)))
825 answer, err := whatsapp.Find(t.Context(), "member")
826 require.NoError(t, err)
828 lines := strings.Split(answer, "\n")
829 require.Len(t, lines, 22)
830 require.Equal(t, "15550010000@s.whatsapp.net | Member 00 | contact", lines[0])
831 require.Equal(t, "5 more: give a longer part of the name.", lines[20])
832 require.Equal(t, "The groups are not in this answer, because WhatsApp is disconnected.", lines[21])
835 func TestWhatsAppFindBeforeTheNamesArrive(t *testing.T) {
836 whatsapp, fake := whatsappStartTestLinked(t)
837 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
838 whatsappTestConnect(t, fake)
840 answer, err := whatsapp.Find(t.Context(), "ana")
841 require.NoError(t, err)
842 require.Equal(t, whatsappNamesNotReady, answer)
845 func TestWhatsAppFindWithoutALink(t *testing.T) {
846 whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
848 answer, err := whatsapp.Find(t.Context(), "ana")
849 require.NoError(t, err)
850 require.Equal(t, whatsappNotLinked, answer)
853 func TestAgentFindsAWhatsAppContact(t *testing.T) {
854 whatsapp, fake := whatsappStartTestLinked(t)
855 whatsappTestContacts(t, whatsapp)
856 server := newModelFake(t)
857 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
858 if call.number == 0 {
859 modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "whatsapp_find", `{"name":"ana"}`)}, 100, 0, 5))
863 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Found Ana.")}, 100, 0, 5))
866 socket, _ := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
867 client := terminalStartTestClient(t, socket)
868 whatsappTestConnect(t, fake)
870 client.Type("Find Ana on WhatsApp.")
871 client.WaitFor(t, "[result] 15550002222@s.whatsapp.net | Ana Souza | contact\nFound Ana.\n[done]")
874 func TestSubagentHasNoWhatsAppTools(t *testing.T) {
876 require.Equal(t, `error: unknown tool "whatsapp_find"`, box.run(t.Context(), "whatsapp_find", `{"name":"ana"}`))
879 // whatsappTestContacts puts the contacts of the tests into the session of
880 // whatsapp: Ana Souza, Joao Silva with an accent, a business, and the user.
881 func whatsappTestContacts(t *testing.T, whatsapp *WhatsApp) {
884 contacts := whatsapp.device.Contacts
885 require.NoError(t, contacts.PutContactName(t.Context(), whatsappTestAna, "Ana", "Ana Souza"))
886 require.NoError(t, contacts.PutContactName(t.Context(), types.NewJID("15550004444", types.DefaultUserServer), "João", "João Silva"))
887 _, _, err := contacts.PutBusinessName(t.Context(), types.NewJID("15550005555", types.DefaultUserServer), "Corner Bakery")
888 require.NoError(t, err)
889 require.NoError(t, contacts.PutContactName(t.Context(), whatsappTestOwn, "Me", "Me Souza"))