X-Git-Url: http://git.giacomolli.uk/onechat/blobdiff_plain/2e2524881dfe8f6c5cabfd9d342d5d3492953bd4..refs/heads/master:/whatsapp_test.go?ds=inline diff --git a/whatsapp_test.go b/whatsapp_test.go index 2bdb4cc..96ec2df 100644 --- a/whatsapp_test.go +++ b/whatsapp_test.go @@ -1,14 +1,13 @@ package main import ( - "crypto/sha256" - "encoding/hex" + "context" "errors" - "net/http" "os" "path/filepath" "strings" "testing" + "testing/synctest" "time" "github.com/stretchr/testify/require" @@ -20,6 +19,7 @@ import ( func TestWhatsAppStatusFollowsTheConnection(t *testing.T) { dir := t.TempDir() whatsappTestSession(t, dir, true) + clock := newTestClock() whatsapp, fake, _ := whatsappStartTest(t, dir, clock) daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) @@ -31,21 +31,293 @@ func TestWhatsAppStatusFollowsTheConnection(t *testing.T) { require.Equal(t, "WhatsApp is linked and connected since 2026-01-01 12:00.", whatsapp.Status()) clock.now = clock.now.Add(20 * time.Minute) + require.True(t, fake.Emit(&events.Disconnected{})) + clock.now = clock.now.Add(time.Minute) + require.True(t, fake.Emit(&events.Disconnected{})) 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()) } func TestWhatsAppStatusOfAFirstConnectionThatFailed(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + test := whatsappRetryTest(t) + test.fake.connectErr = errors.New("network unavailable") + test.start(t) + synctest.Wait() + + require.Equal(t, "WhatsApp is linked and disconnected since "+time.Now().Format(whatsappTimeLayout)+".", test.whatsapp.Status()) + require.Equal(t, "<4>first connect failed, retrying: network unavailable\n", test.logs.String()) + }) +} + +func TestWhatsAppRetriesTheFirstConnectionWithCappedBackoff(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + test := whatsappRetryTest(t) + for range 10 { + test.fake.connectErrors = append(test.fake.connectErrors, errors.New("network unavailable")) + } + + test.start(t) + synctest.Wait() + require.Len(t, test.fake.Connections(), 1) + require.True(t, test.fake.Emit(&events.Disconnected{})) + require.True(t, test.fake.Emit(&events.Disconnected{})) + + delays := []time.Duration{2 * time.Second, 4 * time.Second, 8 * time.Second, 16 * time.Second, 32 * time.Second, 64 * time.Second, 128 * time.Second, 256 * time.Second, 5 * time.Minute, 5 * time.Minute} + + var outage time.Duration + + for index, delay := range delays { + time.Sleep(delay - time.Nanosecond) + synctest.Wait() + require.Len(t, test.fake.Connections(), index+1) + time.Sleep(time.Nanosecond) + synctest.Wait() + require.Len(t, test.fake.Connections(), index+2) + + outage += delay + } + + times := test.fake.ConnectionTimes() + for index, delay := range delays { + require.Equal(t, delay, times[index+1].Sub(times[index])) + } + + require.True(t, test.fake.Emit(&events.Connected{})) + require.True(t, test.fake.Emit(&events.OfflineSyncCompleted{})) + synctest.Wait() + time.Sleep(10 * time.Minute) + synctest.Wait() + require.Len(t, test.fake.Connections(), 11) + require.Equal(t, "<4>first connect failed, retrying: network unavailable\n<6>connected again after="+durationText(outage)+"\n", test.logs.String()) + }) +} + +func TestWhatsAppFirstConnectionRetriesStopWhenContextEnds(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + test := whatsappRetryTest(t) + test.fake.connectErr = errors.New("network unavailable") + test.start(t) + synctest.Wait() + time.Sleep(2 * time.Second) + synctest.Wait() + require.Len(t, test.fake.Connections(), 2) + + test.cancel() + require.NoError(t, <-test.done) + time.Sleep(10 * time.Minute) + synctest.Wait() + require.Len(t, test.fake.Connections(), 2) + }) +} + +func TestWhatsAppLinkedPairRequestKeepsFirstConnectionRetries(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + test := whatsappRetryTest(t) + test.fake.connectErr = errors.New("network unavailable") + test.start(t) + synctest.Wait() + + answers := make(chan string, 1) + test.whatsapp.Command("pair 15550001111", func(text string) { + answers <- text + }) + require.Equal(t, "WhatsApp is linked already. Run /whatsapp unlink first.", <-answers) + synctest.Wait() + test.fake.mutex.Lock() + + test.fake.connectErr = nil + test.fake.mutex.Unlock() + time.Sleep(whatsappRetryDelay) + synctest.Wait() + require.Len(t, test.fake.Connections(), 2) + require.True(t, test.fake.Emit(&events.Connected{})) + require.True(t, test.fake.Emit(&events.OfflineSyncCompleted{})) + require.Contains(t, test.whatsapp.Status(), "linked and connected since") + time.Sleep(whatsappRetryLimit) + synctest.Wait() + require.Len(t, test.fake.Connections(), 2) + require.Equal(t, "<4>first connect failed, retrying: network unavailable\n<6>connected again after=2s\n", test.logs.String()) + }) +} + +func TestWhatsAppFirstConnectionRetriesStopForLinkChanges(t *testing.T) { + testCases := []struct { + name string + stop func(test *WhatsAppRetryTest) + }{ + { + name: "unlink", + stop: func(test *WhatsAppRetryTest) { + test.whatsapp.Command("unlink", func(_ string) { + }) + }, + }, + { + name: "logout", + stop: func(test *WhatsAppRetryTest) { + test.fake.Emit(&events.LoggedOut{}) + }, + }, + { + name: "permanent disconnect", + stop: func(test *WhatsAppRetryTest) { + test.fake.Emit(&events.StreamReplaced{}) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + test := whatsappRetryTest(t) + test.fake.connectErr = errors.New("network unavailable") + test.start(t) + synctest.Wait() + tc.stop(test) + synctest.Wait() + time.Sleep(10 * time.Minute) + synctest.Wait() + require.Len(t, test.fake.Connections(), 1) + }) + }) + } +} + +func TestWhatsAppPairsAfterFirstConnectionRetriesStop(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + test := whatsappRetryTest(t) + test.fake.connectErr = errors.New("network unavailable") + test.start(t) + synctest.Wait() + test.whatsapp.Command("unlink", func(_ string) { + }) + synctest.Wait() + require.False(t, test.whatsapp.isLinked()) + + test.fake.mutex.Lock() + + test.fake.connectErr = nil + test.fake.mutex.Unlock() + test.whatsapp.Command("pair 15550001111", func(_ string) { + }) + + items := <-test.fake.pairings + items <- whatsmeow.QRChannelItem{Event: whatsmeow.QRChannelEventCode} + + synctest.Wait() + require.True(t, test.fake.Emit(&events.PairSuccess{ + ID: types.NewADJID(whatsappTestNumber, 0, 1), + LID: types.NewADJID(whatsappTestLID, 0, 1), + })) + + items <- whatsmeow.QRChannelSuccess + + synctest.Wait() + require.True(t, test.whatsapp.isLinked()) + time.Sleep(10 * time.Minute) + synctest.Wait() + require.Len(t, test.fake.Connections(), 2) + require.Equal(t, []string{whatsappTestNumber}, test.fake.Phones()) + }) +} + +func TestWhatsAppInboxFailureStopsFirstConnectionRetries(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + test := whatsappRetryTest(t) + test.fake.connectErr = errors.New("network unavailable") + test.start(t) + synctest.Wait() + + failure := errors.New("state write failed") + test.whatsapp.inbox.failures <- failure + + require.ErrorIs(t, <-test.done, failure) + time.Sleep(10 * time.Minute) + synctest.Wait() + require.Len(t, test.fake.Connections(), 1) + }) +} + +func TestWhatsAppLongFirstConnectionOutageGivesOneNotice(t *testing.T) { + synctest.Test(t, func(t *testing.T) { + test := whatsappRetryTest(t) + test.fake.connectErr = errors.New("network unavailable") + test.start(t) + synctest.Wait() + + since := time.Now().Format(whatsappTimeLayout) + + time.Sleep(whatsappNoticeDelay) + synctest.Wait() + require.Equal(t, 1, test.events.Count("notice")) + require.Equal(t, "WhatsApp is disconnected since "+since+". OneChat tries again.", test.events.Last("notice")) + + time.Sleep(whatsappNoticeDelay) + synctest.Wait() + require.Equal(t, 1, test.events.Count("notice")) + test.fake.mutex.Lock() + + test.fake.connectErr = nil + test.fake.mutex.Unlock() + time.Sleep(whatsappRetryLimit) + synctest.Wait() + require.True(t, test.fake.Emit(&events.Connected{})) + require.True(t, test.fake.Emit(&events.OfflineSyncCompleted{})) + synctest.Wait() + require.Equal(t, 2, test.events.Count("notice")) + require.Equal(t, "WhatsApp is connected again.", test.events.Last("notice")) + }) +} + +func TestWhatsAppStartingRetriesDoNotDuplicateLibraryRetries(t *testing.T) { dir := t.TempDir() whatsappTestSession(t, dir, true) - whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock()) + + whatsapp, _, _ := whatsappStartTest(t, dir, newTestClock()) + client, ok := whatsappNewClient(whatsapp.device, whatsapp.handle).(*whatsmeow.Client) + require.True(t, ok) + require.False(t, client.InitialAutoReconnect) + require.True(t, client.EnableAutoReconnect) + + device := whatsapp.container.NewDevice() + client, ok = whatsappNewClient(device, whatsapp.handle).(*whatsmeow.Client) + require.True(t, ok) + require.True(t, client.InitialAutoReconnect) +} + +func TestWhatsAppLogsEachChangeOfTheConnectionOnce(t *testing.T) { + dir := t.TempDir() + whatsappTestSession(t, dir, true) + + clock := newTestClock() + whatsapp, fake, logs := whatsappStartTest(t, dir, clock) daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) <-fake.connects + require.True(t, fake.Emit(&events.Connected{})) + + clock.now = clock.now.Add(time.Minute) + + require.True(t, fake.Emit(&events.Disconnected{})) + + clock.now = clock.now.Add(time.Minute) + require.True(t, fake.Emit(&events.Disconnected{})) - require.Equal(t, "WhatsApp is linked and disconnected since 2026-01-01 12:00.", whatsapp.Status()) + + clock.now = clock.now.Add(25 * time.Second) + + require.True(t, fake.Emit(&events.Connected{})) + + require.Equal(t, []string{ + "<6>connected", + "<4>disconnected, retrying", + "<6>connected again after=1m25s", + }, strings.Split(strings.TrimSuffix(logs.String(), "\n"), "\n")) + require.NotContains(t, logs.String(), whatsappTestNumber) + require.NotContains(t, logs.String(), whatsappTestLID) } func TestWhatsAppIsNotLinkedWithoutALinkedDevice(t *testing.T) { @@ -55,7 +327,7 @@ func TestWhatsAppIsNotLinkedWithoutALinkedDevice(t *testing.T) { }{ { name: "no session file", - session: func(t *testing.T, dir string) { + session: func(_ *testing.T, _ string) { }, }, { @@ -70,6 +342,7 @@ func TestWhatsAppIsNotLinkedWithoutALinkedDevice(t *testing.T) { t.Run(tc.name, func(t *testing.T) { dir := t.TempDir() tc.session(t, dir) + whatsapp, _, _ := whatsappStartTest(t, dir, newTestClock()) require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status()) }) @@ -115,7 +388,7 @@ func TestWhatsAppCommandsAnswerOnlyTheClientThatAsked(t *testing.T) { func TestWhatsAppPairsWithACode(t *testing.T) { dir := t.TempDir() - whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock()) + whatsapp, fake, logs := whatsappStartTest(t, dir, newTestClock()) server := newModelFake(t) socket, memory := daemonStartTestWithWhatsApp(t, server, nil, whatsapp) asking := terminalStartTestClient(t, socket) @@ -124,6 +397,7 @@ func TestWhatsAppPairsWithACode(t *testing.T) { other.WaitFor(t, "[connected to OneChat]") asking.Type("/whatsapp pair +1 555-000-1111") + items := <-fake.pairings items <- whatsmeow.QRChannelItem{ Event: whatsmeow.QRChannelEventCode, @@ -134,7 +408,9 @@ func TestWhatsAppPairsWithACode(t *testing.T) { ID: types.NewADJID(whatsappTestNumber, 0, 1), LID: types.NewADJID(whatsappTestLID, 0, 1), })) + items <- whatsmeow.QRChannelSuccess + asking.WaitFor(t, "WhatsApp is linked to +15550001111.") // The library connects again after the link with the context of the @@ -150,6 +426,7 @@ func TestWhatsAppPairsWithACode(t *testing.T) { require.Equal(t, "[connected to OneChat]\n", other.Output()) require.Zero(t, server.Calls()) require.Equal(t, []string{"WhatsApp is linked."}, whatsappTestLogTexts(t, memory)) + require.Equal(t, "<6>linked\n", logs.String()) info, err := os.Stat(filepath.Join(dir, whatsappDirName)) require.NoError(t, err) @@ -239,6 +516,7 @@ func TestWhatsAppNewPairingEndsTheOneBefore(t *testing.T) { client.Type("/whatsapp pair 15550001111") <-fake.pairings client.Type("/whatsapp pair 15550002222") + items := <-fake.pairings items <- whatsmeow.QRChannelItem{ Event: whatsmeow.QRChannelEventCode, @@ -252,6 +530,7 @@ func TestWhatsAppNewPairingEndsTheOneBefore(t *testing.T) { func TestWhatsAppPairWhenLinked(t *testing.T) { dir := t.TempDir() whatsappTestSession(t, dir, true) + whatsapp, _, _ := whatsappStartTest(t, dir, newTestClock()) socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) client := terminalStartTestClient(t, socket) @@ -281,15 +560,18 @@ func TestWhatsAppUnlink(t *testing.T) { t.Run(tc.name, func(t *testing.T) { dir := t.TempDir() whatsappTestSession(t, dir, true) - whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock()) + + whatsapp, fake, logs := whatsappStartTest(t, dir, newTestClock()) fake.logoutErr = tc.logoutErr socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) client := terminalStartTestClient(t, socket) + <-fake.connects client.Type("/whatsapp unlink") client.WaitFor(t, tc.answer) require.Equal(t, 1, fake.Logouts()) + require.Equal(t, "<6>unlinked\n", logs.String()) require.NoFileExists(t, filepath.Join(dir, whatsappDirName, whatsappSessionFileName)) require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status()) require.Equal(t, []string{"WhatsApp is unlinked."}, whatsappTestLogTexts(t, memory)) @@ -303,7 +585,8 @@ func TestWhatsAppUnlink(t *testing.T) { func TestWhatsAppLogoutFromThePhone(t *testing.T) { dir := t.TempDir() whatsappTestSession(t, dir, true) - whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock()) + + whatsapp, fake, logs := whatsappStartTest(t, dir, newTestClock()) telegram := newTelegramFake(t) bot, _ := telegramStartTestBot(t, telegram) socket, memory := daemonStartTestWithWhatsApp(t, newModelFake(t), bot, whatsapp) @@ -312,9 +595,11 @@ func TestWhatsAppLogoutFromThePhone(t *testing.T) { <-fake.connects require.True(t, fake.Emit(&events.LoggedOut{})) + notice := "WhatsApp was unlinked from the phone. Run /whatsapp pair PHONE to link it again." client.WaitFor(t, notice) telegramWaitForSent(t, telegram, notice) + require.Equal(t, "<3>logged out by the phone, run /whatsapp pair PHONE\n", logs.String()) require.NoFileExists(t, filepath.Join(dir, whatsappDirName, whatsappSessionFileName)) require.Equal(t, "WhatsApp is not linked. Run /whatsapp pair PHONE to link it.", whatsapp.Status()) require.Equal(t, []string{"WhatsApp was unlinked from the phone."}, whatsappTestLogTexts(t, memory)) @@ -323,6 +608,7 @@ func TestWhatsAppLogoutFromThePhone(t *testing.T) { func TestWhatsAppLongDisconnectionGivesOneNotice(t *testing.T) { dir := t.TempDir() whatsappTestSession(t, dir, true) + clock := newTestClock() whatsapp, fake, _ := whatsappStartTest(t, dir, clock) whatsapp.noticeDelay = 300 * time.Millisecond @@ -338,6 +624,7 @@ func TestWhatsAppLongDisconnectionGivesOneNotice(t *testing.T) { require.Equal(t, "[connected to OneChat]\n", client.Output()) clock.now = clock.now.Add(time.Hour) + require.True(t, fake.Emit(&events.Disconnected{})) require.True(t, fake.Emit(&events.Disconnected{})) client.WaitFor(t, "WhatsApp is disconnected since 2026-01-01 13:00. OneChat tries again.\n") @@ -352,7 +639,8 @@ func TestWhatsAppLongDisconnectionGivesOneNotice(t *testing.T) { func TestWhatsAppDisconnectionWithNoRetryGivesANoticeAtOnce(t *testing.T) { dir := t.TempDir() whatsappTestSession(t, dir, true) - whatsapp, fake, _ := whatsappStartTest(t, dir, newTestClock()) + + whatsapp, fake, logs := whatsappStartTest(t, dir, newTestClock()) socket, _ := daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) client := terminalStartTestClient(t, socket) client.WaitFor(t, "[connected to OneChat]") @@ -360,20 +648,23 @@ func TestWhatsAppDisconnectionWithNoRetryGivesANoticeAtOnce(t *testing.T) { require.True(t, fake.Emit(&events.StreamReplaced{})) client.WaitFor(t, "WhatsApp is disconnected and does not connect again by itself: stream replaced. Restart the daemon to try again.") + require.Equal(t, "<3>disconnected for good, restart to retry: stream replaced\n", logs.String()) } func TestWhatsAppLogsNoOwnNumber(t *testing.T) { dir := t.TempDir() whatsappTestSession(t, dir, true) + whatsapp, fake, logs := whatsappStartTest(t, dir, newTestClock()) fake.connectErr = errors.New("no route for " + whatsappTestNumber + "@s.whatsapp.net and " + whatsappTestLID + "@lid") + daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) <-fake.connects require.Eventually(t, func() bool { return logs.String() != "" }, 10*time.Second, time.Millisecond) - require.Equal(t, "connect: no route for OWN@s.whatsapp.net and OWN@lid\n", logs.String()) + require.Equal(t, "<4>first connect failed, retrying: no route for OWN@s.whatsapp.net and OWN@lid\n", logs.String()) } func TestWhatsAppPhone(t *testing.T) { @@ -465,193 +756,61 @@ func TestSubagentHasNoWhatsAppTools(t *testing.T) { require.Equal(t, `error: unknown tool "whatsapp_send"`, box.run(t.Context(), "whatsapp_send", `{"id":"a","chat":"15550002222@s.whatsapp.net","text":"hi"}`)) } -func TestWhatsAppSendSendsOnceForEachID(t *testing.T) { - whatsapp, fake := whatsappStartTestLinked(t) - whatsappTestContacts(t, whatsapp) - daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) - whatsappTestConnect(t, fake) - - answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.") - require.NoError(t, err) - require.Equal(t, "Sent to Ana Souza at 12:05.", answer) - - answer, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "The keys are under the mat.") - require.NoError(t, err) - require.Equal(t, "Sent to Ana Souza at 12:05. This is the answer of the first call with this id: nothing was sent again.", answer) - - _, err = whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "Another text.") - require.EqualError(t, err, "the id keys belongs to another message: give a new id for each message") - - require.Equal(t, []WhatsAppFakeSend{ - { - to: whatsappTestAna.String(), - text: "The keys are under the mat.", - id: "MSG1", - }, - }, fake.Sent()) - - again, err := openWhatsAppState(filepath.Join(whatsapp.dir, whatsappStateFileName), whatsappBatchQuiet, whatsappBatchWindow) - require.NoError(t, err) - - record, found := again.Send("keys") - require.True(t, found) - require.Equal(t, whatsappSent, record.State) - require.Equal(t, "MSG1", record.MessageID) +// WhatsAppRetryTest runs WhatsApp without a model or network client call. All +// timers and channels belong to its synctest bubble, so backoff uses fake time. +type WhatsAppRetryTest struct { + whatsapp *WhatsApp + fake *WhatsAppFake + logs *SyncBuffer + loop *TurnLoop + events *TurnEventRecorder + cancel context.CancelFunc + done chan error } -func TestWhatsAppSendToAGroup(t *testing.T) { - whatsapp, fake := whatsappStartTestLinked(t) - fake.groups = []*types.GroupInfo{ - { - JID: whatsappTestFamily, - GroupName: types.GroupName{ - Name: "Family", - }, - }, - } +func (r *WhatsAppRetryTest) start(t *testing.T) { + t.Helper() - daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) - whatsappTestConnect(t, fake) + ctx, cancel := context.WithCancel(t.Context()) + r.cancel = cancel + r.done = make(chan error, 1) + go func() { + r.done <- r.whatsapp.Run(ctx, r.loop, r.events.Add) + }() - answer, err := whatsapp.Send(t.Context(), "cake", whatsappTestFamily.String(), "I bring the cake.") - require.NoError(t, err) - require.Equal(t, "Sent to Family at 12:05.", answer) + t.Cleanup(func() { + cancel() + synctest.Wait() + }) } -func TestWhatsAppSendThatFails(t *testing.T) { - whatsapp, fake := whatsappStartTestLinked(t) - fake.sendErr = errors.New("server error for " + whatsappTestNumber) - daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) - whatsappTestConnect(t, fake) +func whatsappRetryTest(t *testing.T) *WhatsAppRetryTest { + t.Helper() - for range 2 { - answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi") - require.NoError(t, err) - require.True(t, strings.HasPrefix(answer, "The send failed: server error for OWN."), answer) - } - - require.Len(t, fake.Sent(), 1) -} - -func TestWhatsAppSendCutByAStop(t *testing.T) { - whatsapp, fake := whatsappStartTestLinked(t) - daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) - whatsappTestConnect(t, fake) - - digest := sha256.Sum256([]byte("hi")) - require.NoError(t, whatsapp.state.PutSend(WhatsAppSend{ - ID: "keys", - Chat: whatsappTestAna.String(), - Digest: hex.EncodeToString(digest[:]), - State: whatsappSending, - })) + dir := t.TempDir() + whatsappTestSession(t, dir, true) - answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi") + whatsapp, fake, logs := whatsappStartTest(t, dir, newTestClock()) + whatsapp.clock = time.Now + agent, err := openAgent(t.TempDir()) require.NoError(t, err) - require.Contains(t, answer, "its result is unknown") - require.Empty(t, fake.Sent()) -} - -func TestWhatsAppSendRefuses(t *testing.T) { - testCases := []struct { - name string - id string - chat string - text string - err string - }{ - { - name: "no id", - id: " ", - chat: whatsappTestAna.String(), - text: "hi", - err: "the id must have 1 to 100 bytes", - }, - { - name: "no text", - id: "a", - chat: whatsappTestAna.String(), - text: "", - err: "the text must have 1 to 65536 bytes", - }, - { - name: "a name for a chat", - id: "a", - chat: "Ana Souza", - text: "hi", - err: `"Ana Souza" is not the ID of a WhatsApp chat: find the ID with whatsapp_find`, - }, - { - name: "a status broadcast", - id: "a", - chat: types.StatusBroadcastJID.String(), - text: "hi", - err: `"status@broadcast" is not a private chat or a group`, - }, - { - name: "the chat of the user", - id: "a", - chat: whatsappTestOwn.String(), - text: "hi", - err: "this is the chat of the user with itself: send to another chat", - }, - } - whatsapp, fake := whatsappStartTestLinked(t) - daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) - whatsappTestConnect(t, fake) - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - _, err := whatsapp.Send(t.Context(), tc.id, tc.chat, tc.text) - require.EqualError(t, err, tc.err) - }) - } - - require.Empty(t, fake.Sent()) -} - -func TestWhatsAppSendWhileDisconnected(t *testing.T) { - whatsapp, fake := whatsappStartTestLinked(t) - daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp) - whatsappTestConnect(t, fake) - require.True(t, fake.Emit(&events.Disconnected{})) - - _, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi") - require.EqualError(t, err, "WhatsApp is disconnected, so nothing was sent: try again later") - - require.True(t, fake.Emit(&events.Connected{})) - answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi") + memory, err := openMemory(t.Context(), agent.dir, time.Now, newFakeSummarizer(), discardLog()) require.NoError(t, err) - require.Equal(t, "Sent to +15550002222 at 12:05.", answer) -} - -func TestWhatsAppSendWithoutALink(t *testing.T) { - whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock()) + t.Cleanup(func() { + require.NoError(t, memory.Close()) + }) - answer, err := whatsapp.Send(t.Context(), "keys", whatsappTestAna.String(), "hi") + events := &TurnEventRecorder{} + client := newModelClient("https://example.com", ModelAPIKey(modelTestKey), agent.CacheKey(), filepath.Join(agent.dir, "usage.jsonl")) + loop, err := newTurnLoop(agent, memory, newTestSubagents(t), whatsapp, newTestSchedules(t, time.Now), client, time.Now, discardLog(), events.Add) require.NoError(t, err) - require.Equal(t, whatsappNotLinked, answer) -} - -func TestAgentSendsAWhatsAppMessage(t *testing.T) { - whatsapp, fake := whatsappStartTestLinked(t) - whatsappTestContacts(t, whatsapp) - server := newModelFake(t) - server.reply = func(w http.ResponseWriter, call ModelFakeCall) { - if call.number == 0 { - modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "whatsapp_send", `{"id":"keys","chat":"15550002222@s.whatsapp.net","text":"The keys are under the mat."}`)}, 100, 0, 5)) - return - } - modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Sent.")}, 100, 0, 5)) + return &WhatsAppRetryTest{ + whatsapp: whatsapp, + fake: fake, + logs: logs, + loop: loop, + events: events, } - - socket, _ := daemonStartTestWithWhatsApp(t, server, nil, whatsapp) - client := terminalStartTestClient(t, socket) - whatsappTestConnect(t, fake) - - client.Type("Tell Ana that the keys are under the mat.") - client.WaitFor(t, "[result] Sent to Ana Souza at 12:05.\nSent.\n[done]") - require.Len(t, fake.Sent(), 1) }