]> git.giacomolli.uk - onechat/commitdiff
Move whatsapp_find to its own file
authorAnderson A. Giacomolli <redacted>
Sun, 8 Nov 2026 21:39:43 +0000 (18:39 -0300)
committerAnderson A. Giacomolli <redacted>
Sun, 8 Nov 2026 21:39:43 +0000 (18:39 -0300)
The search of whatsapp_find is now the function whatsappFind in
whatsapp_find.go, with its matches, its fold of the names, and its
answer. It owns no state: it gets a WhatsAppLink for the device, the
client, and the own IDs, and the WhatsAppGroups for the names of the
groups. WhatsApp.Find stays the entry point of the tool and gives the
current link. The tests of the tool move to whatsapp_find_test.go. The
behavior does not change.

Issue: 6f69ca1

whatsapp.go
whatsapp_fake_helper_test.go
whatsapp_find.go [new file with mode: 0644]
whatsapp_find_test.go [new file with mode: 0644]
whatsapp_test.go

index c5af946027b9c4341feadf848ac792f26d3123b3..5b0730013703fc0966b69ce918f720a05ab48205 100644 (file)
@@ -1,7 +1,6 @@
 package main
 
 import (
 package main
 
 import (
-       "cmp"
        "context"
        "crypto/sha256"
        "encoding/hex"
        "context"
        "crypto/sha256"
        "encoding/hex"
@@ -15,7 +14,6 @@ import (
        "strings"
        "sync"
        "time"
        "strings"
        "sync"
        "time"
-       "unicode"
 
        "go.mau.fi/whatsmeow"
        "go.mau.fi/whatsmeow/proto/waE2E"
 
        "go.mau.fi/whatsmeow"
        "go.mau.fi/whatsmeow/proto/waE2E"
@@ -24,9 +22,6 @@ import (
        "go.mau.fi/whatsmeow/types"
        "go.mau.fi/whatsmeow/types/events"
        waLog "go.mau.fi/whatsmeow/util/log"
        "go.mau.fi/whatsmeow/types"
        "go.mau.fi/whatsmeow/types/events"
        waLog "go.mau.fi/whatsmeow/util/log"
-       "golang.org/x/text/runes"
-       "golang.org/x/text/transform"
-       "golang.org/x/text/unicode/norm"
 
        // The session store of the library opens SQLite with this pure Go
        // driver.
 
        // The session store of the library opens SQLite with this pure Go
        // driver.
@@ -80,9 +75,6 @@ const (
        // session database or of the groups from WhatsApp.
        whatsappLookupTime = 10 * time.Second
 
        // session database or of the groups from WhatsApp.
        whatsappLookupTime = 10 * time.Second
 
-       // whatsappFindMatches is the most matches of whatsapp_find.
-       whatsappFindMatches = 20
-
        // whatsappNotLinked is the answer of a WhatsApp tool without a link.
        whatsappNotLinked = "WhatsApp is not linked. Tell the user to run /whatsapp pair PHONE in the terminal or in Telegram."
 
        // whatsappNotLinked is the answer of a WhatsApp tool without a link.
        whatsappNotLinked = "WhatsApp is not linked. Tell the user to run /whatsapp pair PHONE in the terminal or in Telegram."
 
@@ -99,10 +91,6 @@ const (
        whatsappSending = "sending"
        whatsappSent    = "sent"
        whatsappFailed  = "failed"
        whatsappSending = "sending"
        whatsappSent    = "sent"
        whatsappFailed  = "failed"
-
-       // whatsappNamesNotReady is the answer of whatsapp_find before the names
-       // arrive from the phone.
-       whatsappNamesNotReady = "The names of the contacts and groups have not arrived from the phone yet. They come some minutes after the link. Try again later."
 )
 
 // WhatsAppClient is the part of the WhatsApp client that the daemon uses. The
 )
 
 // WhatsAppClient is the part of the WhatsApp client that the daemon uses. The
@@ -363,6 +351,11 @@ func (w *WhatsApp) Close() error {
        return err
 }
 
        return err
 }
 
+// Find returns the answer of whatsapp_find for name, as whatsappFind says.
+func (w *WhatsApp) Find(ctx context.Context, name string) (string, error) {
+       return whatsappFind(ctx, w.link(), w.groups, name)
+}
+
 func (w *WhatsApp) sessionPath() string {
        return filepath.Join(w.dir, whatsappSessionFileName)
 }
 func (w *WhatsApp) sessionPath() string {
        return filepath.Join(w.dir, whatsappSessionFileName)
 }
@@ -522,80 +515,6 @@ func (w *WhatsApp) unlink(ctx context.Context, l *TurnLoop, answer func(text str
        return nil
 }
 
        return nil
 }
 
-// Find returns the contacts and the groups whose name contains name, with no
-// regard to case and accents, as lines "ID | name | contact" or "ID | name |
-// group", sorted by name. It gives at most whatsappFindMatches lines and the
-// number of the others. The contacts come from the session database, and the
-// groups from WhatsApp, read again after whatsappGroupsAge. When the groups
-// cannot be read and none are known, the answer says so. Before the names
-// arrive from the phone, it says that they are not ready.
-func (w *WhatsApp) Find(ctx context.Context, name string) (string, error) {
-       w.mutex.Lock()
-       linked := w.linked
-       device := w.device
-       own := slices.Clone(w.own)
-       w.mutex.Unlock()
-
-       stale := w.groups.stale()
-
-       if !linked {
-               return whatsappNotLinked, nil
-       }
-
-       query := whatsappFold(name)
-       if query == "" {
-               return "", errors.New("name is empty: give a part of the name of the contact or the group")
-       }
-
-       contacts, err := device.Contacts.GetAllContacts(ctx)
-       if err != nil {
-               return "", fmt.Errorf("read the contacts: %s", w.redact(err.Error()))
-       }
-
-       read := !stale || w.groups.read(ctx, w.link())
-       groups := w.groups.all()
-
-       if len(contacts) == 0 && len(groups) == 0 {
-               if !read {
-                       return "The names are not known yet, and WhatsApp is disconnected, so the groups cannot be read. Try again later.", nil
-               }
-
-               return whatsappNamesNotReady, nil
-       }
-
-       var matches []WhatsAppMatch
-       for jid, contact := range contacts {
-               names := []string{contact.FullName, contact.FirstName, contact.BusinessName, contact.PushName}
-               shown := slices.IndexFunc(names, func(name string) bool {
-                       return name != ""
-               })
-
-               if slices.Contains(own, jid.User) || shown < 0 || !slices.ContainsFunc(names, func(name string) bool {
-                       return strings.Contains(whatsappFold(name), query)
-               }) {
-                       continue
-               }
-
-               matches = append(matches, WhatsAppMatch{
-                       id:   jid.ToNonAD().String(),
-                       name: names[shown],
-                       kind: "contact",
-               })
-       }
-
-       for id, group := range groups {
-               if strings.Contains(whatsappFold(group), query) {
-                       matches = append(matches, WhatsAppMatch{
-                               id:   id,
-                               name: group,
-                               kind: "group",
-                       })
-               }
-       }
-
-       return whatsappFindAnswer(name, matches, read || len(groups) > 0), nil
-}
-
 // removeSession ends the connection, closes the session database, and deletes
 // its files.
 func (w *WhatsApp) removeSession() error {
 // removeSession ends the connection, closes the session database, and deletes
 // its files.
 func (w *WhatsApp) removeSession() error {
@@ -973,13 +892,6 @@ func (w *WhatsApp) Send(ctx context.Context, id, chat, text string) (string, err
        return record.Answer, nil
 }
 
        return record.Answer, nil
 }
 
-// WhatsAppMatch is one contact or group of Find.
-type WhatsAppMatch struct {
-       id   string
-       name string
-       kind string
-}
-
 // WhatsAppLink is a copy of the link of the device at one time, for the parts
 // of WhatsApp that do not own it: the groups, the batches, find, and send.
 type WhatsAppLink struct {
 // WhatsAppLink is a copy of the link of the device at one time, for the parts
 // of WhatsApp that do not own it: the groups, the batches, find, and send.
 type WhatsAppLink struct {
@@ -1055,44 +967,6 @@ func whatsappContactName(ctx context.Context, device *store.Device, jid types.JI
        }
 }
 
        }
 }
 
-// whatsappFold returns text in lower case with no accents and no space at its
-// ends, so that names match with no regard to case and accents.
-func whatsappFold(text string) string {
-       folded, _, err := transform.String(transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC), text)
-       if err != nil {
-               folded = text
-       }
-
-       return strings.ToLower(strings.TrimSpace(folded))
-}
-
-// whatsappFindAnswer returns the answer of Find for name with matches. Without
-// groups, the answer says that it has no groups.
-func whatsappFindAnswer(name string, matches []WhatsAppMatch, groups bool) string {
-       slices.SortFunc(matches, func(a, b WhatsAppMatch) int {
-               return cmp.Or(strings.Compare(whatsappFold(a.name), whatsappFold(b.name)), strings.Compare(a.id, b.id))
-       })
-
-       var lines []string
-       for _, match := range matches[:min(len(matches), whatsappFindMatches)] {
-               lines = append(lines, match.id+" | "+match.name+" | "+match.kind)
-       }
-
-       if len(matches) == 0 {
-               lines = append(lines, "No contact or group has a name with \""+name+"\".")
-       }
-
-       if len(matches) > whatsappFindMatches {
-               lines = append(lines, fmt.Sprintf("%d more: give a longer part of the name.", len(matches)-whatsappFindMatches))
-       }
-
-       if !groups {
-               lines = append(lines, "The groups are not in this answer, because WhatsApp is disconnected.")
-       }
-
-       return strings.Join(lines, "\n")
-}
-
 // whatsappSendCheck returns the JID of chat, and an error when id, chat, or
 // text cannot go to a send. The chat is a private chat or a group, and not the
 // chat of the user with itself.
 // whatsappSendCheck returns the JID of chat, and an error when id, chat, or
 // text cannot go to a send. The chat is a private chat or a group, and not the
 // chat of the user with itself.
index 3d7d117c0337967156eb096767ee15b06a27166f..ee5321ed4ab837ad9215d6fb9399e9c46c72452c 100644 (file)
@@ -352,3 +352,16 @@ func whatsappTestConnect(t *testing.T, fake *WhatsAppFake) {
        require.True(t, fake.Emit(&events.Connected{}))
        require.True(t, fake.Emit(&events.OfflineSyncCompleted{}))
 }
        require.True(t, fake.Emit(&events.Connected{}))
        require.True(t, fake.Emit(&events.OfflineSyncCompleted{}))
 }
+
+// whatsappTestContacts puts the contacts of the tests into the session of
+// whatsapp: Ana Souza, Joao Silva with an accent, a business, and the user.
+func whatsappTestContacts(t *testing.T, whatsapp *WhatsApp) {
+       t.Helper()
+
+       contacts := whatsapp.device.Contacts
+       require.NoError(t, contacts.PutContactName(t.Context(), whatsappTestAna, "Ana", "Ana Souza"))
+       require.NoError(t, contacts.PutContactName(t.Context(), types.NewJID("15550004444", types.DefaultUserServer), "João", "João Silva"))
+       _, _, err := contacts.PutBusinessName(t.Context(), types.NewJID("15550005555", types.DefaultUserServer), "Corner Bakery")
+       require.NoError(t, err)
+       require.NoError(t, contacts.PutContactName(t.Context(), whatsappTestOwn, "Me", "Me Souza"))
+}
diff --git a/whatsapp_find.go b/whatsapp_find.go
new file mode 100644 (file)
index 0000000..545e01c
--- /dev/null
@@ -0,0 +1,139 @@
+package main
+
+import (
+       "cmp"
+       "context"
+       "errors"
+       "fmt"
+       "slices"
+       "strings"
+       "unicode"
+
+       "golang.org/x/text/runes"
+       "golang.org/x/text/transform"
+       "golang.org/x/text/unicode/norm"
+)
+
+const (
+       // whatsappFindMatches is the most matches of whatsapp_find.
+       whatsappFindMatches = 20
+
+       // whatsappNamesNotReady is the answer of whatsapp_find before the names
+       // arrive from the phone.
+       whatsappNamesNotReady = "The names of the contacts and groups have not arrived from the phone yet. They come some minutes after the link. Try again later."
+)
+
+// whatsappFind returns the contacts and the groups whose name contains name,
+// with no regard to case and accents, as lines "ID | name | contact" or "ID |
+// name | group", sorted by name. It gives at most whatsappFindMatches lines and
+// the number of the others. The contacts come from the session database, and
+// the groups from WhatsApp, read again after whatsappGroupsAge. When the groups
+// cannot be read and none are known, the answer says so. Before the names
+// arrive from the phone, it says that they are not ready. link gives the
+// device, the client to read the groups, and the own IDs, which are not in the
+// answer.
+func whatsappFind(ctx context.Context, link WhatsAppLink, groups *WhatsAppGroups, name string) (string, error) {
+       stale := groups.stale()
+
+       if !link.linked {
+               return whatsappNotLinked, nil
+       }
+
+       query := whatsappFold(name)
+       if query == "" {
+               return "", errors.New("name is empty: give a part of the name of the contact or the group")
+       }
+
+       contacts, err := link.device.Contacts.GetAllContacts(ctx)
+       if err != nil {
+               return "", fmt.Errorf("read the contacts: %s", whatsappRedact(link.own, err.Error()))
+       }
+
+       read := !stale || groups.read(ctx, link)
+       names := groups.all()
+
+       if len(contacts) == 0 && len(names) == 0 {
+               if !read {
+                       return "The names are not known yet, and WhatsApp is disconnected, so the groups cannot be read. Try again later.", nil
+               }
+
+               return whatsappNamesNotReady, nil
+       }
+
+       var matches []WhatsAppMatch
+       for jid, contact := range contacts {
+               names := []string{contact.FullName, contact.FirstName, contact.BusinessName, contact.PushName}
+               shown := slices.IndexFunc(names, func(name string) bool {
+                       return name != ""
+               })
+
+               if slices.Contains(link.own, jid.User) || shown < 0 || !slices.ContainsFunc(names, func(name string) bool {
+                       return strings.Contains(whatsappFold(name), query)
+               }) {
+                       continue
+               }
+
+               matches = append(matches, WhatsAppMatch{
+                       id:   jid.ToNonAD().String(),
+                       name: names[shown],
+                       kind: "contact",
+               })
+       }
+
+       for id, group := range names {
+               if strings.Contains(whatsappFold(group), query) {
+                       matches = append(matches, WhatsAppMatch{
+                               id:   id,
+                               name: group,
+                               kind: "group",
+                       })
+               }
+       }
+
+       return whatsappFindAnswer(name, matches, read || len(names) > 0), nil
+}
+
+// WhatsAppMatch is one contact or group of whatsappFind.
+type WhatsAppMatch struct {
+       id   string
+       name string
+       kind string
+}
+
+// whatsappFold returns text in lower case with no accents and no space at its
+// ends, so that names match with no regard to case and accents.
+func whatsappFold(text string) string {
+       folded, _, err := transform.String(transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC), text)
+       if err != nil {
+               folded = text
+       }
+
+       return strings.ToLower(strings.TrimSpace(folded))
+}
+
+// whatsappFindAnswer returns the answer of whatsappFind for name with matches. Without
+// groups, the answer says that it has no groups.
+func whatsappFindAnswer(name string, matches []WhatsAppMatch, groups bool) string {
+       slices.SortFunc(matches, func(a, b WhatsAppMatch) int {
+               return cmp.Or(strings.Compare(whatsappFold(a.name), whatsappFold(b.name)), strings.Compare(a.id, b.id))
+       })
+
+       var lines []string
+       for _, match := range matches[:min(len(matches), whatsappFindMatches)] {
+               lines = append(lines, match.id+" | "+match.name+" | "+match.kind)
+       }
+
+       if len(matches) == 0 {
+               lines = append(lines, "No contact or group has a name with \""+name+"\".")
+       }
+
+       if len(matches) > whatsappFindMatches {
+               lines = append(lines, fmt.Sprintf("%d more: give a longer part of the name.", len(matches)-whatsappFindMatches))
+       }
+
+       if !groups {
+               lines = append(lines, "The groups are not in this answer, because WhatsApp is disconnected.")
+       }
+
+       return strings.Join(lines, "\n")
+}
diff --git a/whatsapp_find_test.go b/whatsapp_find_test.go
new file mode 100644 (file)
index 0000000..30f84ff
--- /dev/null
@@ -0,0 +1,118 @@
+package main
+
+import (
+       "fmt"
+       "net/http"
+       "strings"
+       "testing"
+
+       "github.com/stretchr/testify/require"
+       "go.mau.fi/whatsmeow/types"
+)
+
+func TestWhatsAppFindMatchesNamesWithNoRegardToCaseAndAccents(t *testing.T) {
+       whatsapp, fake := whatsappStartTestLinked(t)
+       fake.groups = []*types.GroupInfo{
+               {
+                       JID: whatsappTestFamily,
+                       GroupName: types.GroupName{
+                               Name: "Família Souza",
+                       },
+               },
+       }
+
+       whatsappTestContacts(t, whatsapp)
+       daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
+       whatsappTestConnect(t, fake)
+
+       testCases := []struct {
+               name   string
+               query  string
+               answer string
+       }{
+               {
+                       name:   "no accent finds an accent",
+                       query:  "joao",
+                       answer: "15550004444@s.whatsapp.net | João Silva | contact",
+               },
+               {
+                       name:   "a part of a name finds contacts and groups",
+                       query:  "SOUZA",
+                       answer: "15550002222@s.whatsapp.net | Ana Souza | contact\n120363000000000001@g.us | Família Souza | group",
+               },
+               {
+                       name:   "a business name",
+                       query:  "bakery",
+                       answer: "15550005555@s.whatsapp.net | Corner Bakery | contact",
+               },
+               {
+                       name:   "no match",
+                       query:  "Zed",
+                       answer: "No contact or group has a name with \"Zed\".",
+               },
+       }
+
+       for _, tc := range testCases {
+               t.Run(tc.name, func(t *testing.T) {
+                       answer, err := whatsapp.Find(t.Context(), tc.query)
+                       require.NoError(t, err)
+                       require.Equal(t, tc.answer, answer)
+               })
+       }
+}
+
+func TestWhatsAppFindGivesAtMostTwentyMatches(t *testing.T) {
+       whatsapp, _ := whatsappStartTestLinked(t)
+       for index := range 25 {
+               jid := types.NewJID(fmt.Sprintf("1555001%04d", index), types.DefaultUserServer)
+               require.NoError(t, whatsapp.device.Contacts.PutContactName(t.Context(), jid, "", fmt.Sprintf("Member %02d", index)))
+       }
+
+       answer, err := whatsapp.Find(t.Context(), "member")
+       require.NoError(t, err)
+
+       lines := strings.Split(answer, "\n")
+       require.Len(t, lines, 22)
+       require.Equal(t, "15550010000@s.whatsapp.net | Member 00 | contact", lines[0])
+       require.Equal(t, "5 more: give a longer part of the name.", lines[20])
+       require.Equal(t, "The groups are not in this answer, because WhatsApp is disconnected.", lines[21])
+}
+
+func TestWhatsAppFindBeforeTheNamesArrive(t *testing.T) {
+       whatsapp, fake := whatsappStartTestLinked(t)
+       daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
+       whatsappTestConnect(t, fake)
+
+       answer, err := whatsapp.Find(t.Context(), "ana")
+       require.NoError(t, err)
+       require.Equal(t, whatsappNamesNotReady, answer)
+}
+
+func TestWhatsAppFindWithoutALink(t *testing.T) {
+       whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
+
+       answer, err := whatsapp.Find(t.Context(), "ana")
+       require.NoError(t, err)
+       require.Equal(t, whatsappNotLinked, answer)
+}
+
+func TestAgentFindsAWhatsAppContact(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_find", `{"name":"ana"}`)}, 100, 0, 5))
+                       return
+               }
+
+               modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Found Ana.")}, 100, 0, 5))
+       }
+
+       socket, _ := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
+       client := terminalStartTestClient(t, socket)
+       whatsappTestConnect(t, fake)
+
+       client.Type("Find Ana on WhatsApp.")
+       client.WaitFor(t, "[result] 15550002222@s.whatsapp.net | Ana Souza | contact\nFound Ana.\n[done]")
+}
index 3d79def16b0bdea77d4be979bb2c9b9038d3b92d..2bdb4cc349e77f8f1fa901f98c11c3641fc4b611 100644 (file)
@@ -4,7 +4,6 @@ import (
        "crypto/sha256"
        "encoding/hex"
        "errors"
        "crypto/sha256"
        "encoding/hex"
        "errors"
-       "fmt"
        "net/http"
        "os"
        "path/filepath"
        "net/http"
        "os"
        "path/filepath"
@@ -460,132 +459,12 @@ func TestWhatsAppCommandArgs(t *testing.T) {
        }
 }
 
        }
 }
 
-func TestWhatsAppFindMatchesNamesWithNoRegardToCaseAndAccents(t *testing.T) {
-       whatsapp, fake := whatsappStartTestLinked(t)
-       fake.groups = []*types.GroupInfo{
-               {
-                       JID: whatsappTestFamily,
-                       GroupName: types.GroupName{
-                               Name: "Família Souza",
-                       },
-               },
-       }
-
-       whatsappTestContacts(t, whatsapp)
-       daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
-       whatsappTestConnect(t, fake)
-
-       testCases := []struct {
-               name   string
-               query  string
-               answer string
-       }{
-               {
-                       name:   "no accent finds an accent",
-                       query:  "joao",
-                       answer: "15550004444@s.whatsapp.net | João Silva | contact",
-               },
-               {
-                       name:   "a part of a name finds contacts and groups",
-                       query:  "SOUZA",
-                       answer: "15550002222@s.whatsapp.net | Ana Souza | contact\n120363000000000001@g.us | Família Souza | group",
-               },
-               {
-                       name:   "a business name",
-                       query:  "bakery",
-                       answer: "15550005555@s.whatsapp.net | Corner Bakery | contact",
-               },
-               {
-                       name:   "no match",
-                       query:  "Zed",
-                       answer: "No contact or group has a name with \"Zed\".",
-               },
-       }
-
-       for _, tc := range testCases {
-               t.Run(tc.name, func(t *testing.T) {
-                       answer, err := whatsapp.Find(t.Context(), tc.query)
-                       require.NoError(t, err)
-                       require.Equal(t, tc.answer, answer)
-               })
-       }
-}
-
-func TestWhatsAppFindGivesAtMostTwentyMatches(t *testing.T) {
-       whatsapp, _ := whatsappStartTestLinked(t)
-       for index := range 25 {
-               jid := types.NewJID(fmt.Sprintf("1555001%04d", index), types.DefaultUserServer)
-               require.NoError(t, whatsapp.device.Contacts.PutContactName(t.Context(), jid, "", fmt.Sprintf("Member %02d", index)))
-       }
-
-       answer, err := whatsapp.Find(t.Context(), "member")
-       require.NoError(t, err)
-
-       lines := strings.Split(answer, "\n")
-       require.Len(t, lines, 22)
-       require.Equal(t, "15550010000@s.whatsapp.net | Member 00 | contact", lines[0])
-       require.Equal(t, "5 more: give a longer part of the name.", lines[20])
-       require.Equal(t, "The groups are not in this answer, because WhatsApp is disconnected.", lines[21])
-}
-
-func TestWhatsAppFindBeforeTheNamesArrive(t *testing.T) {
-       whatsapp, fake := whatsappStartTestLinked(t)
-       daemonStartTestWithWhatsApp(t, newModelFake(t), nil, whatsapp)
-       whatsappTestConnect(t, fake)
-
-       answer, err := whatsapp.Find(t.Context(), "ana")
-       require.NoError(t, err)
-       require.Equal(t, whatsappNamesNotReady, answer)
-}
-
-func TestWhatsAppFindWithoutALink(t *testing.T) {
-       whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
-
-       answer, err := whatsapp.Find(t.Context(), "ana")
-       require.NoError(t, err)
-       require.Equal(t, whatsappNotLinked, answer)
-}
-
-func TestAgentFindsAWhatsAppContact(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_find", `{"name":"ana"}`)}, 100, 0, 5))
-                       return
-               }
-
-               modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Found Ana.")}, 100, 0, 5))
-       }
-
-       socket, _ := daemonStartTestWithWhatsApp(t, server, nil, whatsapp)
-       client := terminalStartTestClient(t, socket)
-       whatsappTestConnect(t, fake)
-
-       client.Type("Find Ana on WhatsApp.")
-       client.WaitFor(t, "[result] 15550002222@s.whatsapp.net | Ana Souza | contact\nFound Ana.\n[done]")
-}
-
 func TestSubagentHasNoWhatsAppTools(t *testing.T) {
        box := &Toolbox{}
        require.Equal(t, `error: unknown tool "whatsapp_find"`, box.run(t.Context(), "whatsapp_find", `{"name":"ana"}`))
        require.Equal(t, `error: unknown tool "whatsapp_send"`, box.run(t.Context(), "whatsapp_send", `{"id":"a","chat":"15550002222@s.whatsapp.net","text":"hi"}`))
 }
 
 func TestSubagentHasNoWhatsAppTools(t *testing.T) {
        box := &Toolbox{}
        require.Equal(t, `error: unknown tool "whatsapp_find"`, box.run(t.Context(), "whatsapp_find", `{"name":"ana"}`))
        require.Equal(t, `error: unknown tool "whatsapp_send"`, box.run(t.Context(), "whatsapp_send", `{"id":"a","chat":"15550002222@s.whatsapp.net","text":"hi"}`))
 }
 
-// whatsappTestContacts puts the contacts of the tests into the session of
-// whatsapp: Ana Souza, Joao Silva with an accent, a business, and the user.
-func whatsappTestContacts(t *testing.T, whatsapp *WhatsApp) {
-       t.Helper()
-
-       contacts := whatsapp.device.Contacts
-       require.NoError(t, contacts.PutContactName(t.Context(), whatsappTestAna, "Ana", "Ana Souza"))
-       require.NoError(t, contacts.PutContactName(t.Context(), types.NewJID("15550004444", types.DefaultUserServer), "João", "João Silva"))
-       _, _, err := contacts.PutBusinessName(t.Context(), types.NewJID("15550005555", types.DefaultUserServer), "Corner Bakery")
-       require.NoError(t, err)
-       require.NoError(t, contacts.PutContactName(t.Context(), whatsappTestOwn, "Me", "Me Souza"))
-}
-
 func TestWhatsAppSendSendsOnceForEachID(t *testing.T) {
        whatsapp, fake := whatsappStartTestLinked(t)
        whatsappTestContacts(t, whatsapp)
 func TestWhatsAppSendSendsOnceForEachID(t *testing.T) {
        whatsapp, fake := whatsappStartTestLinked(t)
        whatsappTestContacts(t, whatsapp)