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 = 50 * 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(100 * 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(100 * 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]")
489 whatsappTestConnect(t, fake)
491 at := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
492 require.True(t, fake.Emit(whatsappTestMessage(whatsappTestAna, whatsappTestAna, "m1", "Are you at home?", at)))
493 require.True(t, fake.Emit(whatsappTestMessage(whatsappTestAna, whatsappTestOwn, "m2", "Yes", at)))
495 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
496 client.WaitFor(t, text+"\n[working]\n[done]\n")
497 require.Equal(t, 1, server.Calls())
498 require.Contains(t, string(server.Call(0).raw), "Are you at home?")
499 require.Equal(t, []string{text}, whatsappTestLogTexts(t, memory))
500 require.Empty(t, telegram.Sent())
501 require.Eventually(t, func() bool {
502 return len(whatsapp.batches.Closed()) == 0
503 }, 10*time.Second, time.Millisecond)
506 func TestWhatsAppGroupTurns(t *testing.T) {
507 testCases := []struct {
509 message func(event *events.Message)
515 message: func(event *events.Message) {
517 line: "12:00 Bruno Lima: Who brings the cake?",
521 message: func(event *events.Message) {
522 event.Message = &waE2E.Message{
523 ExtendedTextMessage: &waE2E.ExtendedTextMessage{
524 Text: new("@" + whatsappTestLID + " can you bring the cake? Call " + whatsappTestNumber + "."),
525 ContextInfo: &waE2E.ContextInfo{
526 MentionedJID: []string{whatsappTestLID + "@lid"},
531 line: "12:00 Bruno Lima (mentions you): @you can you bring the cake? Call OWN.",
536 message: func(event *events.Message) {
537 event.Message = &waE2E.Message{
538 ExtendedTextMessage: &waE2E.ExtendedTextMessage{
539 Text: new("Who brings the cake?"),
540 ContextInfo: &waE2E.ContextInfo{
541 Participant: new(whatsappTestOwn.String()),
546 line: "12:00 Bruno Lima (replies to you): Who brings the cake?",
551 for _, tc := range testCases {
552 t.Run(tc.name, func(t *testing.T) {
553 whatsapp, fake := whatsappStartTestLinked(t)
554 fake.groups = []*types.GroupInfo{
556 JID: whatsappTestFamily,
557 GroupName: types.GroupName{
563 server := newModelFake(t)
564 socket, memory := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
565 client := terminalStartTestClient(t, socket)
566 client.WaitFor(t, "[connected to OneChat]")
567 whatsappTestConnect(t, fake)
569 event := whatsappTestMessage(whatsappTestFamily, whatsappTestBruno, "m1", "Who brings the cake?", time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))
570 event.Info.PushName = "Bruno Lima"
572 require.True(t, fake.Emit(event))
574 text := "[WhatsApp] Family, group 120363000000000001@g.us, Thursday 2026-01-01\n" + tc.line
576 text += "\n" + whatsappBatchEmptyReply
577 client.WaitFor(t, "[done]")
578 require.Equal(t, 1, server.Calls())
580 client.WaitFor(t, text)
581 time.Sleep(100 * time.Millisecond)
582 require.Zero(t, server.Calls())
585 require.Equal(t, text, whatsappTestLogTexts(t, memory)[0])
590 func TestWhatsAppTakesOnlyText(t *testing.T) {
591 at := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
592 testCases := []struct {
594 change func(event *events.Message)
598 change: func(event *events.Message) {
604 change: func(event *events.Message) {
605 event.SourceWebMsg = &waWeb.WebMessageInfo{}
609 name: "a view-once message",
610 change: func(event *events.Message) {
611 event.IsViewOnce = true
615 name: "a disappearing message",
616 change: func(event *events.Message) {
617 event.IsEphemeral = true
621 name: "the chat of the user with itself",
622 change: func(event *events.Message) {
623 event.Info.Chat = whatsappTestOwn
627 name: "a status broadcast",
628 change: func(event *events.Message) {
629 event.Info.Chat = types.StatusBroadcastJID
633 name: "a photo with a caption",
634 change: func(event *events.Message) {
635 event.Message = &waE2E.Message{
636 ImageMessage: &waE2E.ImageMessage{
637 Caption: new("look"),
643 name: "a text of spaces",
644 change: func(event *events.Message) {
645 event.Message.Conversation = new(" ")
650 for _, tc := range testCases {
651 t.Run(tc.name, func(t *testing.T) {
652 whatsapp, _ := whatsappStartTestLinked(t)
653 event := whatsappTestMessage(whatsappTestAna, whatsappTestAna, "m1", "hi", at)
655 require.True(t, whatsapp.handle(event))
656 require.NoError(t, whatsapp.batches.CloseAll())
657 require.Empty(t, whatsapp.batches.Closed())
662 // whatsappStartTestLinked opens a WhatsApp with a linked device in a temporary
663 // directory, with a quiet time of 50 ms for its batches.
664 func whatsappStartTestLinked(t *testing.T) (*WhatsApp, *WhatsAppFake) {
668 whatsappTestSession(t, dir, true)
669 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
670 whatsapp.batches.quiet = 50 * time.Millisecond
672 return whatsapp, fake
675 func TestWhatsAppCatchupGivesOneTurn(t *testing.T) {
676 whatsapp, fake := whatsappStartTestLinked(t)
677 server := newModelFake(t)
678 socket, memory := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
679 client := terminalStartTestClient(t, socket)
680 client.WaitFor(t, "[connected to OneChat]")
683 require.True(t, fake.Emit(&events.Connected{}))
684 at := time.Date(2026, 1, 1, 8, 0, 0, 0, time.UTC)
685 for index, event := range []*events.Message{
686 whatsappTestMessage(whatsappTestAna, whatsappTestAna, "m1", "Good morning", at),
687 whatsappTestMessage(whatsappTestBruno, whatsappTestBruno, "m2", "Call me", at.Add(time.Hour)),
688 whatsappTestMessage(whatsappTestAna, whatsappTestAna, "m3", "Are you there?", at.Add(2*time.Hour)),
690 event.Info.PushName = []string{"Ana", "Bruno", "Ana"}[index]
691 require.True(t, fake.Emit(event))
694 time.Sleep(100 * time.Millisecond)
695 require.Zero(t, memory.Len())
697 require.True(t, fake.Emit(&events.OfflineSyncCompleted{}))
698 client.WaitFor(t, "[done]")
699 require.Equal(t, []string{
700 "[WhatsApp] Ana, private chat 15550002222@s.whatsapp.net, Thursday 2026-01-01\n08:00 Ana: Good morning\n" + whatsappBatchEmptyReply,
701 "[WhatsApp] Bruno, private chat 15550003333@s.whatsapp.net, Thursday 2026-01-01\n09:00 Bruno: Call me\n" + whatsappBatchEmptyReply,
702 "[WhatsApp] Ana, private chat 15550002222@s.whatsapp.net, Thursday 2026-01-01\n10:00 Ana: Are you there?\n" + whatsappBatchEmptyReply,
703 }, whatsappTestLogTexts(t, memory)[:3])
705 time.Sleep(100 * time.Millisecond)
706 require.Equal(t, 1, server.Calls())
707 for _, text := range []string{"Good morning", "Call me", "Are you there?"} {
708 require.Contains(t, string(server.Call(0).raw), text)
712 func TestWhatsAppCatchupEndsAfterItsLimit(t *testing.T) {
713 whatsapp, fake := whatsappStartTestLinked(t)
714 whatsapp.catchupLimit = 100 * time.Millisecond
715 socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
716 client := terminalStartTestClient(t, socket)
717 client.WaitFor(t, "[connected to OneChat]")
720 require.True(t, fake.Emit(&events.Connected{}))
721 require.True(t, fake.Emit(whatsappTestMessage(whatsappTestAna, whatsappTestOwn, "m1", "On my way.", time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC))))
722 client.WaitFor(t, "12:00 You: On my way.")
723 require.Len(t, whatsappTestLogTexts(t, memory), 1)
726 func TestWhatsAppBatchesOfTheStateFileWaitForTheCatchup(t *testing.T) {
728 whatsappTestSession(t, dir, true)
729 before, _, _ := whatsappStartTest(t, dir, newTestClock())
730 _, err := before.batches.Add(whatsappTestChat, WhatsAppMessage{
733 Time: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC),
734 Text: "Did you see my message?",
735 }, time.Now().Add(-time.Hour))
736 require.NoError(t, err)
738 whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock())
739 socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
740 client := terminalStartTestClient(t, socket)
741 client.WaitFor(t, "[connected to OneChat]")
744 time.Sleep(100 * time.Millisecond)
745 require.Zero(t, memory.Len())
747 require.True(t, fake.Emit(&events.Connected{}))
748 require.True(t, fake.Emit(&events.OfflineSyncCompleted{}))
749 client.WaitFor(t, "12:00 Ana Souza: Did you see my message?")
750 client.WaitFor(t, "[done]")
753 // whatsappTestConnect waits for the connection of the daemon and gives it the
754 // events of a connection with an empty catch-up.
755 func whatsappTestConnect(t *testing.T, fake *WhatsAppFake) {
759 require.True(t, fake.Emit(&events.Connected{}))
760 require.True(t, fake.Emit(&events.OfflineSyncCompleted{}))
763 func TestWhatsAppFindMatchesNamesWithNoRegardToCaseAndAccents(t *testing.T) {
764 whatsapp, fake := whatsappStartTestLinked(t)
765 fake.groups = []*types.GroupInfo{
767 JID: whatsappTestFamily,
768 GroupName: types.GroupName{
769 Name: "Família Souza",
774 whatsappTestContacts(t, whatsapp)
775 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
776 whatsappTestConnect(t, fake)
778 testCases := []struct {
784 name: "no accent finds an accent",
786 answer: "15550004444@s.whatsapp.net | João Silva | contact",
789 name: "a part of a name finds contacts and groups",
791 answer: "15550002222@s.whatsapp.net | Ana Souza | contact\n120363000000000001@g.us | Família Souza | group",
794 name: "a business name",
796 answer: "15550005555@s.whatsapp.net | Corner Bakery | contact",
801 answer: "No contact or group has a name with \"Zed\".",
805 for _, tc := range testCases {
806 t.Run(tc.name, func(t *testing.T) {
807 answer, err := whatsapp.Find(t.Context(), tc.query)
808 require.NoError(t, err)
809 require.Equal(t, tc.answer, answer)
814 func TestWhatsAppFindGivesAtMostTwentyMatches(t *testing.T) {
815 whatsapp, _ := whatsappStartTestLinked(t)
816 for index := range 25 {
817 jid := types.NewJID(fmt.Sprintf("1555001%04d", index), types.DefaultUserServer)
818 require.NoError(t, whatsapp.device.Contacts.PutContactName(t.Context(), jid, "", fmt.Sprintf("Member %02d", index)))
821 answer, err := whatsapp.Find(t.Context(), "member")
822 require.NoError(t, err)
824 lines := strings.Split(answer, "\n")
825 require.Len(t, lines, 22)
826 require.Equal(t, "15550010000@s.whatsapp.net | Member 00 | contact", lines[0])
827 require.Equal(t, "5 more: give a longer part of the name.", lines[20])
828 require.Equal(t, "The groups are not in this answer, because WhatsApp is disconnected.", lines[21])
831 func TestWhatsAppFindBeforeTheNamesArrive(t *testing.T) {
832 whatsapp, fake := whatsappStartTestLinked(t)
833 daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
834 whatsappTestConnect(t, fake)
836 answer, err := whatsapp.Find(t.Context(), "ana")
837 require.NoError(t, err)
838 require.Equal(t, whatsappNamesNotReady, answer)
841 func TestWhatsAppFindWithoutALink(t *testing.T) {
842 whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
844 answer, err := whatsapp.Find(t.Context(), "ana")
845 require.NoError(t, err)
846 require.Equal(t, whatsappNotLinked, answer)
849 func TestAgentFindsAWhatsAppContact(t *testing.T) {
850 whatsapp, fake := whatsappStartTestLinked(t)
851 whatsappTestContacts(t, whatsapp)
852 server := newModelFake(t)
853 server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
854 if call.number == 0 {
855 modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "whatsapp_find", `{"name":"ana"}`)}, 100, 0, 5))
859 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Found Ana.")}, 100, 0, 5))
862 socket, _ := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
863 client := terminalStartTestClient(t, socket)
864 whatsappTestConnect(t, fake)
866 client.Type("Find Ana on WhatsApp.")
867 client.WaitFor(t, "[result] 15550002222@s.whatsapp.net | Ana Souza | contact\nFound Ana.\n[done]")
870 func TestSubagentHasNoWhatsAppTools(t *testing.T) {
872 require.Equal(t, `error: unknown tool "whatsapp_find"`, box.run(t.Context(), "whatsapp_find", `{"name":"ana"}`))
875 // whatsappTestContacts puts the contacts of the tests into the session of
876 // whatsapp: Ana Souza, Joao Silva with an accent, a business, and the user.
877 func whatsappTestContacts(t *testing.T, whatsapp *WhatsApp) {
880 contacts := whatsapp.device.Contacts
881 require.NoError(t, contacts.PutContactName(t.Context(), whatsappTestAna, "Ana", "Ana Souza"))
882 require.NoError(t, contacts.PutContactName(t.Context(), types.NewJID("15550004444", types.DefaultUserServer), "João", "João Silva"))
883 _, _, err := contacts.PutBusinessName(t.Context(), types.NewJID("15550005555", types.DefaultUserServer), "Corner Bakery")
884 require.NoError(t, err)
885 require.NoError(t, contacts.PutContactName(t.Context(), whatsappTestOwn, "Me", "Me Souza"))