github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/search_test.go (about)

     1  package chat
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/keybase/client/go/chat/globals"
    10  	"github.com/keybase/client/go/chat/search"
    11  	"github.com/keybase/client/go/kbtest"
    12  	"github.com/keybase/client/go/libkb"
    13  	"github.com/keybase/client/go/protocol/chat1"
    14  	"github.com/keybase/client/go/protocol/gregor1"
    15  	"github.com/keybase/client/go/protocol/keybase1"
    16  	"github.com/keybase/client/go/protocol/stellar1"
    17  	"github.com/stretchr/testify/require"
    18  )
    19  
    20  func TestChatSearchConvRegexp(t *testing.T) {
    21  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
    22  
    23  		// Only test against IMPTEAMNATIVE. There is a bug in ChatRemoteMock
    24  		// with using Pagination Next/Prev and we don't need to triple test
    25  		// here.
    26  		switch mt {
    27  		case chat1.ConversationMembersType_IMPTEAMNATIVE:
    28  		default:
    29  			return
    30  		}
    31  
    32  		ctc := makeChatTestContext(t, "SearchRegexp", 2)
    33  		defer ctc.cleanup()
    34  		users := ctc.users()
    35  		u1 := users[0]
    36  		u2 := users[1]
    37  
    38  		conv := mustCreateConversationForTest(t, ctc, u1, chat1.TopicType_CHAT,
    39  			mt, ctc.as(t, u2).user())
    40  		convID := conv.Id
    41  
    42  		tc1 := ctc.as(t, u1)
    43  		tc2 := ctc.as(t, u2)
    44  
    45  		chatUI := kbtest.NewChatUI()
    46  		tc1.h.mockChatUI = chatUI
    47  
    48  		listener1 := newServerChatListener()
    49  		tc1.h.G().NotifyRouter.AddListener(listener1)
    50  		listener2 := newServerChatListener()
    51  		tc2.h.G().NotifyRouter.AddListener(listener2)
    52  
    53  		sendMessage := func(msgBody chat1.MessageBody, user *kbtest.FakeUser) chat1.MessageID {
    54  			msgID := mustPostLocalForTest(t, ctc, user, conv, msgBody)
    55  			typ, err := msgBody.MessageType()
    56  			require.NoError(t, err)
    57  			consumeNewMsgRemote(t, listener1, typ)
    58  			consumeNewMsgRemote(t, listener2, typ)
    59  			return msgID
    60  		}
    61  
    62  		verifyHit := func(beforeMsgIDs []chat1.MessageID, hitMessageID chat1.MessageID, afterMsgIDs []chat1.MessageID,
    63  			matches []chat1.ChatSearchMatch, searchHit chat1.ChatSearchHit) {
    64  			_verifyHit := func(searchHit chat1.ChatSearchHit) {
    65  				if beforeMsgIDs == nil {
    66  					require.Nil(t, searchHit.BeforeMessages)
    67  				} else {
    68  					require.Equal(t, len(beforeMsgIDs), len(searchHit.BeforeMessages))
    69  					for i, msgID := range beforeMsgIDs {
    70  						msg := searchHit.BeforeMessages[i]
    71  						t.Logf("msg: %v", msg.Valid())
    72  						require.True(t, msg.IsValid())
    73  						require.Equal(t, msgID, msg.GetMessageID())
    74  					}
    75  				}
    76  				require.EqualValues(t, hitMessageID, searchHit.HitMessage.Valid().MessageID)
    77  				require.Equal(t, matches, searchHit.Matches)
    78  
    79  				if afterMsgIDs == nil {
    80  					require.Nil(t, searchHit.AfterMessages)
    81  				} else {
    82  					require.Equal(t, len(afterMsgIDs), len(searchHit.AfterMessages))
    83  					for i, msgID := range afterMsgIDs {
    84  						msg := searchHit.AfterMessages[i]
    85  						require.True(t, msg.IsValid())
    86  						require.Equal(t, msgID, msg.GetMessageID())
    87  					}
    88  				}
    89  
    90  			}
    91  			_verifyHit(searchHit)
    92  			select {
    93  			case searchHitRes := <-chatUI.SearchHitCb:
    94  				_verifyHit(searchHitRes.SearchHit)
    95  			case <-time.After(20 * time.Second):
    96  				require.Fail(t, "no search result received")
    97  			}
    98  		}
    99  		verifySearchDone := func(numHits int) {
   100  			select {
   101  			case searchDone := <-chatUI.SearchDoneCb:
   102  				require.Equal(t, numHits, searchDone.NumHits)
   103  			case <-time.After(20 * time.Second):
   104  				require.Fail(t, "no search result received")
   105  			}
   106  		}
   107  
   108  		runSearch := func(query string, isRegex bool, opts chat1.SearchOpts) chat1.SearchRegexpRes {
   109  			opts.IsRegex = isRegex
   110  			res, err := tc1.chatLocalHandler().SearchRegexp(tc1.startCtx, chat1.SearchRegexpArg{
   111  				ConvID: convID,
   112  				Query:  query,
   113  				Opts:   opts,
   114  			})
   115  			require.NoError(t, err)
   116  			t.Logf("query: %v, searchRes: %+v", query, res)
   117  			return res
   118  		}
   119  
   120  		isRegex := false
   121  		opts := chat1.SearchOpts{
   122  			MaxHits:       5,
   123  			BeforeContext: 2,
   124  			AfterContext:  2,
   125  			MaxMessages:   1000,
   126  		}
   127  
   128  		// Test basic equality match
   129  		query := "hi"
   130  		msgBody := "hi @here"
   131  		msgID1 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{
   132  			Body: msgBody,
   133  		}), u1)
   134  		searchMatch := chat1.ChatSearchMatch{
   135  			StartIndex: 0,
   136  			EndIndex:   2,
   137  			Match:      query,
   138  		}
   139  		res := runSearch(query, isRegex, opts)
   140  		require.Equal(t, 1, len(res.Hits))
   141  		verifyHit(nil, msgID1, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0])
   142  		verifySearchDone(1)
   143  
   144  		// Test basic no results
   145  		query = "hey"
   146  		res = runSearch(query, isRegex, opts)
   147  		require.Equal(t, 0, len(res.Hits))
   148  		verifySearchDone(0)
   149  
   150  		// Test maxHits
   151  		opts.MaxHits = 1
   152  		query = "hi"
   153  		msgBody = "hi there"
   154  		msgID2 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{
   155  			Body: msgBody,
   156  		}), u1)
   157  		res = runSearch(query, isRegex, opts)
   158  		require.Equal(t, 1, len(res.Hits))
   159  		verifyHit([]chat1.MessageID{msgID1}, msgID2, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0])
   160  		verifySearchDone(1)
   161  
   162  		opts.MaxHits = 5
   163  		res = runSearch(query, isRegex, opts)
   164  		require.Equal(t, 2, len(res.Hits))
   165  		verifyHit([]chat1.MessageID{msgID1}, msgID2, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0])
   166  		verifyHit(nil, msgID1, []chat1.MessageID{msgID2}, []chat1.ChatSearchMatch{searchMatch}, res.Hits[1])
   167  		verifySearchDone(2)
   168  
   169  		msgID3 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{
   170  			Body: msgBody,
   171  		}), u1)
   172  		res = runSearch(query, isRegex, opts)
   173  		require.Equal(t, 3, len(res.Hits))
   174  		verifyHit([]chat1.MessageID{msgID1, msgID2}, msgID3, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0])
   175  		verifyHit([]chat1.MessageID{msgID1}, msgID2, []chat1.MessageID{msgID3}, []chat1.ChatSearchMatch{searchMatch}, res.Hits[1])
   176  		verifyHit(nil, msgID1, []chat1.MessageID{msgID2, msgID3}, []chat1.ChatSearchMatch{searchMatch}, res.Hits[2])
   177  		verifySearchDone(3)
   178  
   179  		// test sentBy
   180  		// invalid username
   181  		opts.SentBy = u1.Username + "foo"
   182  		res = runSearch(query, isRegex, opts)
   183  		require.Zero(t, len(res.Hits))
   184  		verifySearchDone(0)
   185  
   186  		// send from user2 and make sure we can filter, @mention user1 to test
   187  		// SentTo later.
   188  		opts.SentBy = u2.Username
   189  		msgBody = fmt.Sprintf("hi @%s", u1.Username)
   190  		msgID4 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{
   191  			Body: msgBody,
   192  		}), u2)
   193  		res = runSearch(query, isRegex, opts)
   194  		require.Equal(t, 1, len(res.Hits))
   195  		verifyHit([]chat1.MessageID{msgID2, msgID3}, msgID4, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0])
   196  		verifySearchDone(1)
   197  		opts.SentBy = ""
   198  
   199  		// test sentTo
   200  		// invalid username
   201  		opts.SentTo = u1.Username + "foo"
   202  		res = runSearch(query, isRegex, opts)
   203  		require.Zero(t, len(res.Hits))
   204  		verifySearchDone(0)
   205  
   206  		opts.SentTo = u1.Username
   207  		res = runSearch(query, isRegex, opts)
   208  		require.Equal(t, 2, len(res.Hits))
   209  		verifyHit([]chat1.MessageID{msgID2, msgID3}, msgID4, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0])
   210  		verifyHit(nil, msgID1, []chat1.MessageID{msgID2, msgID3}, []chat1.ChatSearchMatch{searchMatch}, res.Hits[1])
   211  		verifySearchDone(2)
   212  		opts.SentTo = ""
   213  
   214  		// test sentBefore/sentAfter
   215  		msgRes, err := tc1.chatLocalHandler().GetMessagesLocal(tc1.startCtx, chat1.GetMessagesLocalArg{
   216  			ConversationID: convID,
   217  			MessageIDs:     []chat1.MessageID{msgID1, msgID4},
   218  		})
   219  		require.NoError(t, err)
   220  		require.Equal(t, 2, len(msgRes.Messages))
   221  		msg1 := msgRes.Messages[0]
   222  		msg4 := msgRes.Messages[1]
   223  
   224  		// nothing sent after msg4
   225  		opts.SentAfter = msg4.Ctime() + 500
   226  		res = runSearch(query, isRegex, opts)
   227  		require.Zero(t, len(res.Hits))
   228  
   229  		opts.SentAfter = msg1.Ctime()
   230  		res = runSearch(query, isRegex, opts)
   231  		require.Equal(t, 4, len(res.Hits))
   232  
   233  		// nothing sent before msg1
   234  		opts.SentAfter = 0
   235  		opts.SentBefore = msg1.Ctime() - 500
   236  		res = runSearch(query, isRegex, opts)
   237  		require.Zero(t, len(res.Hits))
   238  
   239  		opts.SentBefore = msg4.Ctime()
   240  		res = runSearch(query, isRegex, opts)
   241  		require.Equal(t, 4, len(res.Hits))
   242  
   243  		opts.SentBefore = 0
   244  
   245  		// drain the cbs, 8 hits and 4 dones
   246  		timeout := 20 * time.Second
   247  		for i := 0; i < 8+4; i++ {
   248  			select {
   249  			case <-chatUI.SearchHitCb:
   250  			case <-chatUI.SearchDoneCb:
   251  			case <-time.After(timeout):
   252  				require.Fail(t, "no search result received")
   253  			}
   254  		}
   255  
   256  		query = "edited"
   257  		msgBody = "edited"
   258  		searchMatch = chat1.ChatSearchMatch{
   259  			StartIndex: 0,
   260  			EndIndex:   len(msgBody),
   261  			Match:      msgBody,
   262  		}
   263  		mustEditMsg(tc2.startCtx, t, ctc, u2, conv, msgID4)
   264  		consumeNewMsgRemote(t, listener1, chat1.MessageType_EDIT)
   265  		consumeNewMsgRemote(t, listener2, chat1.MessageType_EDIT)
   266  
   267  		res = runSearch(query, isRegex, opts)
   268  		require.Equal(t, 1, len(res.Hits))
   269  		verifyHit([]chat1.MessageID{msgID2, msgID3}, msgID4, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0])
   270  		verifySearchDone(1)
   271  
   272  		// Test delete
   273  		mustDeleteMsg(tc2.startCtx, t, ctc, u2, conv, msgID4)
   274  		consumeNewMsgRemote(t, listener1, chat1.MessageType_DELETE)
   275  		consumeNewMsgRemote(t, listener2, chat1.MessageType_DELETE)
   276  		res = runSearch(query, isRegex, opts)
   277  		require.Equal(t, 0, len(res.Hits))
   278  		verifySearchDone(0)
   279  
   280  		// Test request payment
   281  		query = "payment :moneybag:"
   282  		msgBody = "payment :moneybag:"
   283  		searchMatch = chat1.ChatSearchMatch{
   284  			StartIndex: 0,
   285  			EndIndex:   len(msgBody),
   286  			Match:      msgBody,
   287  		}
   288  		msgID7 := sendMessage(chat1.NewMessageBodyWithRequestpayment(chat1.MessageRequestPayment{
   289  			RequestID: stellar1.KeybaseRequestID("dummy id"),
   290  			Note:      msgBody,
   291  		}), u1)
   292  		res = runSearch(query, isRegex, opts)
   293  		require.Equal(t, 1, len(res.Hits))
   294  		verifyHit([]chat1.MessageID{msgID2, msgID3}, msgID7, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0])
   295  		verifySearchDone(1)
   296  
   297  		// Test regex functionality
   298  		isRegex = true
   299  
   300  		// Test utf8
   301  		msgBody = `约书亚和约翰屌爆了`
   302  		query = `约.*`
   303  		searchMatch = chat1.ChatSearchMatch{
   304  			StartIndex: 0,
   305  			EndIndex:   len(msgBody),
   306  			Match:      msgBody,
   307  		}
   308  		msgID8 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{
   309  			Body: msgBody,
   310  		}), u1)
   311  		res = runSearch(query, isRegex, opts)
   312  		require.Equal(t, 1, len(res.Hits))
   313  		verifyHit([]chat1.MessageID{msgID3, msgID7}, msgID8, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0])
   314  		verifySearchDone(1)
   315  
   316  		msgBody = "hihihi"
   317  		query = "hi"
   318  		matches := []chat1.ChatSearchMatch{}
   319  		startIndex := 0
   320  		for i := 0; i < 3; i++ {
   321  			matches = append(matches, chat1.ChatSearchMatch{
   322  				StartIndex: startIndex,
   323  				EndIndex:   startIndex + 2,
   324  				Match:      query,
   325  			})
   326  			startIndex += 2
   327  		}
   328  
   329  		opts.MaxHits = 1
   330  		msgID9 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{
   331  			Body: msgBody,
   332  		}), u1)
   333  		res = runSearch(query, isRegex, opts)
   334  		require.Equal(t, 1, len(res.Hits))
   335  		verifyHit([]chat1.MessageID{msgID7, msgID8}, msgID9, nil, matches, res.Hits[0])
   336  		verifySearchDone(1)
   337  
   338  		query = "h.*"
   339  		lowercase := "abcdefghijklmnopqrstuvwxyz"
   340  		for _, char := range lowercase {
   341  			sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{
   342  				Body: "h." + string(char),
   343  			}), u1)
   344  		}
   345  		opts.MaxHits = len(lowercase)
   346  		res = runSearch(query, isRegex, opts)
   347  		require.Equal(t, opts.MaxHits, len(res.Hits))
   348  		verifySearchDone(opts.MaxHits)
   349  
   350  		// Test maxMessages
   351  		opts.MaxMessages = 2
   352  		res = runSearch(query, isRegex, opts)
   353  		require.Equal(t, opts.MaxMessages, len(res.Hits))
   354  		verifySearchDone(opts.MaxMessages)
   355  
   356  		query = `[A-Z]*`
   357  		res = runSearch(query, isRegex, opts)
   358  		require.Equal(t, 0, len(res.Hits))
   359  		verifySearchDone(0)
   360  
   361  		// Test invalid regex
   362  		_, err = tc1.chatLocalHandler().SearchRegexp(tc1.startCtx, chat1.SearchRegexpArg{
   363  			ConvID: convID,
   364  			Query:  "(",
   365  			Opts: chat1.SearchOpts{
   366  				IsRegex: true,
   367  			},
   368  		})
   369  		require.Error(t, err)
   370  	})
   371  }
   372  
   373  func TestChatSearchRemoveMsg(t *testing.T) {
   374  	useRemoteMock = false
   375  	defer func() { useRemoteMock = true }()
   376  	ctc := makeChatTestContext(t, "TestChatSearchRemoveMsg", 2)
   377  	defer ctc.cleanup()
   378  
   379  	users := ctc.users()
   380  	ctx := ctc.as(t, users[0]).startCtx
   381  	tc := ctc.world.Tcs[users[0].Username]
   382  	chatUI := kbtest.NewChatUI()
   383  	uid := gregor1.UID(users[0].GetUID().ToBytes())
   384  	ctc.as(t, users[0]).h.mockChatUI = chatUI
   385  	conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   386  		chat1.ConversationMembersType_IMPTEAMNATIVE)
   387  	conv1 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   388  		chat1.ConversationMembersType_IMPTEAMNATIVE, users[1])
   389  
   390  	msgID0 := mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{
   391  		Body: "MIKEMAXIM",
   392  	}))
   393  	msgID1 := mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{
   394  		Body: "MIKEMAXIM",
   395  	}))
   396  	msgID2 := mustPostLocalForTest(t, ctc, users[0], conv1, chat1.NewMessageBodyWithText(chat1.MessageText{
   397  		Body: "MIKEMAXIM",
   398  	}))
   399  	mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{
   400  		Body: "CRICKETS",
   401  	}))
   402  	res, err := ctc.as(t, users[0]).chatLocalHandler().SearchInbox(ctx, chat1.SearchInboxArg{
   403  		Query: "MIKEM",
   404  		Opts: chat1.SearchOpts{
   405  			MaxConvsHit: 5,
   406  			MaxHits:     5,
   407  		},
   408  	})
   409  	require.NoError(t, err)
   410  	require.NotNil(t, res.Res)
   411  	require.Equal(t, 2, len(res.Res.Hits))
   412  	if res.Res.Hits[0].ConvID.Eq(conv.Id) {
   413  		require.Equal(t, 2, len(res.Res.Hits[0].Hits))
   414  		require.Equal(t, 1, len(res.Res.Hits[1].Hits))
   415  	} else {
   416  		require.Equal(t, 1, len(res.Res.Hits[0].Hits))
   417  		require.Equal(t, 2, len(res.Res.Hits[1].Hits))
   418  	}
   419  
   420  	mustDeleteMsg(ctx, t, ctc, users[0], conv1, msgID2)
   421  
   422  	res, err = ctc.as(t, users[0]).chatLocalHandler().SearchInbox(ctx, chat1.SearchInboxArg{
   423  		Query: "MIKEM",
   424  		Opts: chat1.SearchOpts{
   425  			MaxConvsHit: 5,
   426  			MaxHits:     5,
   427  		},
   428  	})
   429  	require.NoError(t, err)
   430  	require.NotNil(t, res.Res)
   431  	require.Equal(t, 1, len(res.Res.Hits))
   432  	require.Equal(t, 2, len(res.Res.Hits[0].Hits))
   433  
   434  	mustDeleteMsg(ctx, t, ctc, users[0], conv, msgID0)
   435  	mustDeleteMsg(ctx, t, ctc, users[0], conv, msgID1)
   436  
   437  	hres, err := tc.ChatG.Indexer.(*search.Indexer).GetStoreHits(ctx, uid, conv.Id, "MIKEM")
   438  	require.NoError(t, err)
   439  	require.Zero(t, len(hres))
   440  }
   441  
   442  func TestChatSearchInbox(t *testing.T) {
   443  	runWithMemberTypes(t, func(mt chat1.ConversationMembersType) {
   444  
   445  		// Only test against IMPTEAMNATIVE. There is a bug in ChatRemoteMock
   446  		// with using Pagination Next/Prev and we don't need to triple test
   447  		// here.
   448  		switch mt {
   449  		case chat1.ConversationMembersType_IMPTEAMNATIVE:
   450  		default:
   451  			return
   452  		}
   453  
   454  		ctx := context.TODO()
   455  		ctc := makeChatTestContext(t, "SearchInbox", 2)
   456  		defer ctc.cleanup()
   457  		users := ctc.users()
   458  		u1 := users[0]
   459  		u2 := users[1]
   460  
   461  		tc1 := ctc.as(t, u1)
   462  		tc2 := ctc.as(t, u2)
   463  		g1 := ctc.world.Tcs[u1.Username].Context()
   464  		g2 := ctc.world.Tcs[u2.Username].Context()
   465  		uid1 := u1.User.GetUID().ToBytes()
   466  		uid2 := u2.User.GetUID().ToBytes()
   467  
   468  		chatUI := kbtest.NewChatUI()
   469  		tc1.h.mockChatUI = chatUI
   470  
   471  		listener1 := newServerChatListener()
   472  		tc1.h.G().NotifyRouter.AddListener(listener1)
   473  		listener2 := newServerChatListener()
   474  		tc2.h.G().NotifyRouter.AddListener(listener2)
   475  
   476  		// Create our own Indexer instances so we have access to non-interface methods
   477  		indexer1 := search.NewIndexer(g1)
   478  		consumeCh1 := make(chan chat1.ConversationID, 100)
   479  		reindexCh1 := make(chan chat1.ConversationID, 100)
   480  		indexer1.SetConsumeCh(consumeCh1)
   481  		indexer1.SetReindexCh(reindexCh1)
   482  		indexer1.SetStartSyncDelay(0)
   483  		indexer1.SetUID(uid1)
   484  		indexer1.SetFlushDelay(100 * time.Millisecond)
   485  		indexer1.StartFlushLoop()
   486  		indexer1.StartStorageLoop()
   487  		// Stop the original
   488  		select {
   489  		case <-g1.Indexer.Stop(ctx):
   490  		case <-time.After(20 * time.Second):
   491  			require.Fail(t, "g1 Indexer did not stop")
   492  		}
   493  		g1.Indexer = indexer1
   494  
   495  		indexer2 := search.NewIndexer(g2)
   496  		consumeCh2 := make(chan chat1.ConversationID, 100)
   497  		reindexCh2 := make(chan chat1.ConversationID, 100)
   498  		indexer2.SetConsumeCh(consumeCh2)
   499  		indexer2.SetReindexCh(reindexCh2)
   500  		indexer2.SetStartSyncDelay(0)
   501  		indexer2.SetUID(uid2)
   502  		indexer2.SetFlushDelay(10 * time.Millisecond)
   503  		indexer2.StartFlushLoop()
   504  		indexer2.StartStorageLoop()
   505  		// Stop the original
   506  		select {
   507  		case <-g2.Indexer.Stop(ctx):
   508  		case <-time.After(20 * time.Second):
   509  			require.Fail(t, "g2 Indexer did not stop")
   510  		}
   511  		g2.Indexer = indexer2
   512  
   513  		conv := mustCreateConversationForTest(t, ctc, u1, chat1.TopicType_CHAT,
   514  			mt, ctc.as(t, u2).user())
   515  		convID := conv.Id
   516  
   517  		// verify zero messages case
   518  		fi, err := indexer1.FullyIndexed(ctx, convID)
   519  		require.NoError(t, err)
   520  		require.True(t, fi)
   521  		pi, err := indexer1.PercentIndexed(ctx, convID)
   522  		require.NoError(t, err)
   523  		require.Equal(t, 100, pi)
   524  
   525  		fi, err = indexer2.FullyIndexed(ctx, convID)
   526  		require.NoError(t, err)
   527  		require.True(t, fi)
   528  		pi, err = indexer2.PercentIndexed(ctx, convID)
   529  		require.NoError(t, err)
   530  		require.Equal(t, 100, pi)
   531  
   532  		sendMessage := func(msgBody chat1.MessageBody, user *kbtest.FakeUser) chat1.MessageID {
   533  			msgID := mustPostLocalForTest(t, ctc, user, conv, msgBody)
   534  			typ, err := msgBody.MessageType()
   535  			require.NoError(t, err)
   536  			consumeNewMsgRemote(t, listener1, typ)
   537  			consumeNewMsgRemote(t, listener2, typ)
   538  			return msgID
   539  		}
   540  
   541  		verifyHit := func(convID chat1.ConversationID, beforeMsgIDs []chat1.MessageID, hitMessageID chat1.MessageID,
   542  			afterMsgIDs []chat1.MessageID, matches []chat1.ChatSearchMatch, searchHit chat1.ChatSearchHit) {
   543  			if beforeMsgIDs == nil {
   544  				require.Nil(t, searchHit.BeforeMessages)
   545  			} else {
   546  				require.Equal(t, len(beforeMsgIDs), len(searchHit.BeforeMessages))
   547  				for i, msgID := range beforeMsgIDs {
   548  					msg := searchHit.BeforeMessages[i]
   549  					require.True(t, msg.IsValid())
   550  					require.Equal(t, msgID, msg.GetMessageID())
   551  				}
   552  			}
   553  			require.EqualValues(t, hitMessageID, searchHit.HitMessage.Valid().MessageID)
   554  			require.Equal(t, matches, searchHit.Matches)
   555  
   556  			if afterMsgIDs == nil {
   557  				require.Nil(t, searchHit.AfterMessages)
   558  			} else {
   559  				require.Equal(t, len(afterMsgIDs), len(searchHit.AfterMessages))
   560  				for i, msgID := range afterMsgIDs {
   561  					msg := searchHit.AfterMessages[i]
   562  					require.True(t, msg.IsValid())
   563  					require.Equal(t, msgID, msg.GetMessageID())
   564  				}
   565  			}
   566  		}
   567  		verifySearchDone := func(numHits int, delegated bool) {
   568  			select {
   569  			case <-chatUI.InboxSearchConvHitsCb:
   570  			case <-time.After(20 * time.Second):
   571  				require.Fail(t, "no name hits")
   572  			}
   573  			select {
   574  			case searchDone := <-chatUI.InboxSearchDoneCb:
   575  				require.Equal(t, numHits, searchDone.Res.NumHits)
   576  				numConvs := 1
   577  				if numHits == 0 {
   578  					numConvs = 0
   579  				}
   580  				require.Equal(t, numConvs, searchDone.Res.NumConvs)
   581  				if delegated {
   582  					require.True(t, searchDone.Res.Delegated)
   583  				} else {
   584  					require.Equal(t, 100, searchDone.Res.PercentIndexed)
   585  				}
   586  			case <-time.After(20 * time.Second):
   587  				require.Fail(t, "no search result received")
   588  			}
   589  		}
   590  
   591  		verifyIndexConsumption := func(ch chan chat1.ConversationID) {
   592  			select {
   593  			case id := <-ch:
   594  				require.Equal(t, convID, id)
   595  			case <-time.After(5 * time.Second):
   596  				require.Fail(t, "indexer didn't consume")
   597  			}
   598  		}
   599  
   600  		verifyIndexNoConsumption := func(ch chan chat1.ConversationID) {
   601  			select {
   602  			case <-ch:
   603  				require.Fail(t, "indexer reindexed")
   604  			default:
   605  			}
   606  		}
   607  
   608  		verifyIndex := func() {
   609  			t.Logf("verify user 1 index")
   610  			verifyIndexConsumption(consumeCh1)
   611  			t.Logf("verify user 2 index")
   612  			verifyIndexConsumption(consumeCh2)
   613  		}
   614  
   615  		runSearch := func(query string, opts chat1.SearchOpts, expectedReindex bool) *chat1.ChatSearchInboxResults {
   616  			res, err := tc1.chatLocalHandler().SearchInbox(tc1.startCtx, chat1.SearchInboxArg{
   617  				Query: query,
   618  				Opts:  opts,
   619  			})
   620  			require.NoError(t, err)
   621  			t.Logf("query: %v, searchRes: %+v", query, res)
   622  			if expectedReindex {
   623  				verifyIndexConsumption(reindexCh1)
   624  			} else {
   625  				verifyIndexNoConsumption(reindexCh1)
   626  			}
   627  			return res.Res
   628  		}
   629  
   630  		opts := chat1.SearchOpts{
   631  			MaxHits:       5,
   632  			BeforeContext: 2,
   633  			AfterContext:  2,
   634  			MaxMessages:   1000,
   635  			MaxNameConvs:  1,
   636  		}
   637  
   638  		// Test basic equality match
   639  		msgBody := "hello, byE"
   640  		msgID1 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{
   641  			Body: msgBody,
   642  		}), u1)
   643  
   644  		queries := []string{"hello", "hello, ByE"}
   645  		matches := []chat1.ChatSearchMatch{
   646  			{
   647  				StartIndex: 0,
   648  				EndIndex:   5,
   649  				Match:      "hello",
   650  			},
   651  			{
   652  				StartIndex: 0,
   653  				EndIndex:   10,
   654  				Match:      "hello, byE",
   655  			},
   656  		}
   657  		for i, query := range queries {
   658  			res := runSearch(query, opts, false /* expectedReindex */)
   659  			require.Equal(t, 1, len(res.Hits))
   660  			convHit := res.Hits[0]
   661  			require.Equal(t, convID, convHit.ConvID)
   662  			require.Equal(t, 1, len(convHit.Hits))
   663  			verifyHit(convID, nil, msgID1, nil, []chat1.ChatSearchMatch{matches[i]}, convHit.Hits[0])
   664  			verifySearchDone(1, false)
   665  		}
   666  
   667  		// We get a hit but without any highlighting highlighting fails
   668  		query := "hell bye"
   669  		res := runSearch(query, opts, false /* expectedReindex */)
   670  		require.Equal(t, 1, len(res.Hits))
   671  		convHit := res.Hits[0]
   672  		verifyHit(convID, nil, msgID1, nil, nil, convHit.Hits[0])
   673  		verifySearchDone(1, false)
   674  
   675  		// Test basic no results
   676  		query = "hey"
   677  		res = runSearch(query, opts, false /* expectedReindex*/)
   678  		require.Equal(t, 0, len(res.Hits))
   679  		verifySearchDone(0, false)
   680  
   681  		// Test maxHits
   682  		opts.MaxHits = 1
   683  		query = "hello"
   684  		searchMatch := chat1.ChatSearchMatch{
   685  			StartIndex: 0,
   686  			EndIndex:   len(query),
   687  			Match:      query,
   688  		}
   689  		msgID2 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{
   690  			Body: msgBody,
   691  		}), u1)
   692  		verifyIndex()
   693  
   694  		res = runSearch(query, opts, false /* expectedReindex*/)
   695  		require.Equal(t, 1, len(res.Hits))
   696  		convHit = res.Hits[0]
   697  		require.Equal(t, convID, convHit.ConvID)
   698  		require.Equal(t, 1, len(convHit.Hits))
   699  		verifyHit(convID, []chat1.MessageID{msgID1}, msgID2, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0])
   700  		verifySearchDone(1, false)
   701  
   702  		opts.MaxHits = 5
   703  		res = runSearch(query, opts, false /* expectedReindex*/)
   704  		require.Equal(t, 1, len(res.Hits))
   705  		convHit = res.Hits[0]
   706  		require.Equal(t, convID, convHit.ConvID)
   707  		require.Equal(t, 2, len(convHit.Hits))
   708  		verifyHit(convID, []chat1.MessageID{msgID1}, msgID2, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0])
   709  		verifyHit(convID, nil, msgID1, []chat1.MessageID{msgID2}, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[1])
   710  		verifySearchDone(2, false)
   711  
   712  		msgID3 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{
   713  			Body: msgBody,
   714  		}), u1)
   715  
   716  		verifyIndex()
   717  
   718  		res = runSearch(query, opts, false /* expectedReindex*/)
   719  		require.Equal(t, 1, len(res.Hits))
   720  		convHit = res.Hits[0]
   721  		require.Equal(t, convID, convHit.ConvID)
   722  		require.Equal(t, 3, len(convHit.Hits))
   723  		verifyHit(convID, []chat1.MessageID{msgID1, msgID2}, msgID3, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0])
   724  		verifyHit(convID, []chat1.MessageID{msgID1}, msgID2, []chat1.MessageID{msgID3}, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[1])
   725  		verifyHit(convID, nil, msgID1, []chat1.MessageID{msgID2, msgID3}, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[2])
   726  		verifySearchDone(3, false)
   727  
   728  		// test sentBy
   729  		// invalid username
   730  		opts.SentBy = u1.Username + "foo"
   731  		res = runSearch(query, opts, false /* expectedReindex*/)
   732  		require.Zero(t, len(res.Hits))
   733  		verifySearchDone(0, false)
   734  
   735  		// send from user2 and make sure we can filter
   736  		opts.SentBy = u2.Username
   737  		msgBody = "hello"
   738  		query = "hello"
   739  		msgID4 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{
   740  			Body: msgBody,
   741  		}), u2)
   742  		verifyIndex()
   743  
   744  		res = runSearch(query, opts, false /* expectedReindex*/)
   745  		require.Equal(t, 1, len(res.Hits))
   746  		convHit = res.Hits[0]
   747  		require.Equal(t, convID, convHit.ConvID)
   748  		require.Equal(t, 1, len(convHit.Hits))
   749  		verifyHit(convID, []chat1.MessageID{msgID2, msgID3}, msgID4, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0])
   750  		verifySearchDone(1, false)
   751  		opts.SentBy = ""
   752  
   753  		// test sentBefore/sentAfter
   754  		msgRes, err := tc1.chatLocalHandler().GetMessagesLocal(tc1.startCtx, chat1.GetMessagesLocalArg{
   755  			ConversationID: convID,
   756  			MessageIDs:     []chat1.MessageID{msgID1, msgID4},
   757  		})
   758  		require.NoError(t, err)
   759  		require.Equal(t, 2, len(msgRes.Messages))
   760  		msg1 := msgRes.Messages[0]
   761  		msg4 := msgRes.Messages[1]
   762  
   763  		// nothing sent after msg4
   764  		opts.SentAfter = msg4.Ctime() + 500
   765  		res = runSearch(query, opts, false /* expectedReindex*/)
   766  		require.Zero(t, len(res.Hits))
   767  		verifySearchDone(0, false)
   768  
   769  		opts.SentAfter = msg1.Ctime()
   770  		res = runSearch(query, opts, false /* expectedReindex*/)
   771  		require.Equal(t, 1, len(res.Hits))
   772  		require.Equal(t, 4, len(res.Hits[0].Hits))
   773  		verifySearchDone(4, false)
   774  
   775  		// nothing sent before msg1
   776  		opts.SentAfter = 0
   777  		opts.SentBefore = msg1.Ctime() - 500
   778  		res = runSearch(query, opts, false /* expectedReindex*/)
   779  		require.Zero(t, len(res.Hits))
   780  		verifySearchDone(0, false)
   781  
   782  		opts.SentBefore = msg4.Ctime()
   783  		res = runSearch(query, opts, false /* expectedReindex*/)
   784  		require.Equal(t, 1, len(res.Hits))
   785  		require.Equal(t, 4, len(res.Hits[0].Hits))
   786  		verifySearchDone(4, false)
   787  		opts.SentBefore = 0
   788  
   789  		// Test edit
   790  		query = "edited"
   791  		msgBody = "edited"
   792  		searchMatch = chat1.ChatSearchMatch{
   793  			StartIndex: 0,
   794  			EndIndex:   len(msgBody),
   795  			Match:      msgBody,
   796  		}
   797  		mustEditMsg(tc2.startCtx, t, ctc, u2, conv, msgID4)
   798  		consumeNewMsgRemote(t, listener1, chat1.MessageType_EDIT)
   799  		consumeNewMsgRemote(t, listener2, chat1.MessageType_EDIT)
   800  		verifyIndex()
   801  
   802  		res = runSearch(query, opts, false /* expectedReindex*/)
   803  		t.Logf("%+v", res)
   804  		require.Equal(t, 1, len(res.Hits))
   805  		convHit = res.Hits[0]
   806  		require.Equal(t, convID, convHit.ConvID)
   807  		require.Equal(t, 1, len(convHit.Hits))
   808  		verifyHit(convID, []chat1.MessageID{msgID2, msgID3}, msgID4, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0])
   809  		verifySearchDone(1, false)
   810  
   811  		// Test delete
   812  		mustDeleteMsg(tc2.startCtx, t, ctc, u2, conv, msgID4)
   813  		consumeNewMsgRemote(t, listener1, chat1.MessageType_DELETE)
   814  		consumeNewMsgRemote(t, listener2, chat1.MessageType_DELETE)
   815  		verifyIndex()
   816  
   817  		res = runSearch(query, opts, false /* expectedReindex*/)
   818  		require.Equal(t, 0, len(res.Hits))
   819  		verifySearchDone(0, false)
   820  
   821  		// Test request payment
   822  		query = "payment :moneybag:"
   823  		msgBody = "payment :moneybag:"
   824  		searchMatch = chat1.ChatSearchMatch{
   825  			StartIndex: 0,
   826  			EndIndex:   len(msgBody),
   827  			Match:      msgBody,
   828  		}
   829  		msgID7 := sendMessage(chat1.NewMessageBodyWithRequestpayment(chat1.MessageRequestPayment{
   830  			RequestID: stellar1.KeybaseRequestID("dummy id"),
   831  			Note:      msgBody,
   832  		}), u1)
   833  		verifyIndex()
   834  
   835  		res = runSearch(query, opts, false /* expectedReindex*/)
   836  		require.Equal(t, 1, len(res.Hits))
   837  		convHit = res.Hits[0]
   838  		require.Equal(t, convID, convHit.ConvID)
   839  		require.Equal(t, 1, len(convHit.Hits))
   840  		verifyHit(convID, []chat1.MessageID{msgID2, msgID3}, msgID7, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0])
   841  		verifySearchDone(1, false)
   842  
   843  		// Test utf8
   844  		msgBody = `约书亚和约翰屌爆了`
   845  		query = `约书亚和约翰屌爆了`
   846  		searchMatch = chat1.ChatSearchMatch{
   847  			StartIndex: 0,
   848  			EndIndex:   len(msgBody),
   849  			Match:      msgBody,
   850  		}
   851  		msgID8 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{
   852  			Body: msgBody,
   853  		}), u1)
   854  		// NOTE other prefixes are cut off since they exceed the max length
   855  		verifyIndex()
   856  		res = runSearch(query, opts, false /* expectedReindex*/)
   857  		require.Equal(t, 1, len(res.Hits))
   858  		convHit = res.Hits[0]
   859  		require.Equal(t, convID, convHit.ConvID)
   860  		require.Equal(t, 1, len(convHit.Hits))
   861  		verifyHit(convID, []chat1.MessageID{msgID3, msgID7}, msgID8, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0])
   862  		verifySearchDone(1, false)
   863  
   864  		// DB nuke, ensure that we reindex after the search
   865  		_, err = g1.LocalChatDb.Nuke()
   866  		require.NoError(t, indexer1.OnDbNuke(libkb.NewMetaContext(context.TODO(), g1.ExternalG())))
   867  		require.NoError(t, err)
   868  		opts.ReindexMode = chat1.ReIndexingMode_PRESEARCH_SYNC // force reindex so we're fully up to date.
   869  		res = runSearch(query, opts, true /* expectedReindex*/)
   870  		require.Equal(t, 1, len(res.Hits))
   871  		convHit = res.Hits[0]
   872  		require.Equal(t, convID, convHit.ConvID)
   873  		require.Equal(t, 1, len(convHit.Hits))
   874  		verifyHit(convID, []chat1.MessageID{msgID3, msgID7}, msgID8, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0])
   875  		verifySearchDone(1, false)
   876  		verifyIndex()
   877  
   878  		// since our index is full, we shouldn't fire off any calls to get messages
   879  		runSearch(query, opts, false /* expectedReindex*/)
   880  		verifySearchDone(1, false)
   881  
   882  		// Verify POSTSEARCH_SYNC
   883  		ictx := globals.CtxAddIdentifyMode(ctx, keybase1.TLFIdentifyBehavior_CHAT_SKIP, nil)
   884  		_, err = g1.LocalChatDb.Nuke()
   885  		require.NoError(t, err)
   886  		require.NoError(t, indexer1.OnDbNuke(libkb.NewMetaContext(context.TODO(), g1.ExternalG())))
   887  		err = indexer1.SelectiveSync(ictx)
   888  		require.NoError(t, err)
   889  		opts.ReindexMode = chat1.ReIndexingMode_POSTSEARCH_SYNC
   890  		res = runSearch(query, opts, true /* expectedReindex*/)
   891  		require.Equal(t, 1, len(res.Hits))
   892  		convHit = res.Hits[0]
   893  		require.Equal(t, convID, convHit.ConvID)
   894  		require.Equal(t, 1, len(convHit.Hits))
   895  		verifyHit(convID, []chat1.MessageID{msgID3, msgID7}, msgID8, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0])
   896  		verifySearchDone(1, false)
   897  		verifyIndex()
   898  
   899  		// since our index is full, we shouldn't fire off any calls to get messages
   900  		runSearch(query, opts, false /* expectedReindex*/)
   901  		verifySearchDone(1, false)
   902  
   903  		// Test prefix searching
   904  		query = "pay"
   905  		searchMatch = chat1.ChatSearchMatch{
   906  			StartIndex: 0,
   907  			EndIndex:   3,
   908  			Match:      "pay",
   909  		}
   910  		res = runSearch(query, opts, false /* expectedReindex*/)
   911  		require.Equal(t, 1, len(res.Hits))
   912  		convHit = res.Hits[0]
   913  		require.Equal(t, convID, convHit.ConvID)
   914  		require.Equal(t, 1, len(convHit.Hits))
   915  		verifyHit(convID, []chat1.MessageID{msgID2, msgID3}, msgID7, []chat1.MessageID{msgID8}, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0])
   916  		verifySearchDone(1, false)
   917  
   918  		query = "payments"
   919  		res = runSearch(query, opts, false /* expectedReindex*/)
   920  		require.Equal(t, 0, len(res.Hits))
   921  		verifySearchDone(0, false)
   922  
   923  		// Test deletehistory
   924  		mustDeleteHistory(tc2.startCtx, t, ctc, u2, conv, msgID8+1)
   925  		consumeNewMsgRemote(t, listener1, chat1.MessageType_DELETEHISTORY)
   926  		consumeNewMsgRemote(t, listener2, chat1.MessageType_DELETEHISTORY)
   927  		verifyIndex()
   928  
   929  		// test sentTo
   930  		msgBody = "hello @" + u1.Username
   931  		query = "hello"
   932  		searchMatch = chat1.ChatSearchMatch{
   933  			StartIndex: 0,
   934  			EndIndex:   5,
   935  			Match:      "hello",
   936  		}
   937  		msgID10 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{
   938  			Body: msgBody,
   939  		}), u2)
   940  
   941  		// invalid username
   942  		opts.SentTo = u1.Username + "foo"
   943  		res = runSearch(query, opts, false /* expectedReindex*/)
   944  		require.Zero(t, len(res.Hits))
   945  		verifySearchDone(0, false)
   946  
   947  		opts.SentTo = u1.Username
   948  		res = runSearch(query, opts, false /* expectedReindex*/)
   949  		require.Equal(t, 1, len(res.Hits))
   950  		convHit = res.Hits[0]
   951  		require.Equal(t, convID, convHit.ConvID)
   952  		require.Equal(t, 1, len(convHit.Hits))
   953  		verifyHit(convID, []chat1.MessageID{}, msgID10, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0])
   954  		verifySearchDone(1, false)
   955  		opts.SentTo = ""
   956  
   957  		// Test canceling sync loop
   958  		syncLoopCh := make(chan struct{})
   959  		indexer1.SetSyncLoopCh(syncLoopCh)
   960  		indexer1.StartSyncLoop()
   961  		waitForFail := func() bool {
   962  			for i := 0; i < 5; i++ {
   963  				indexer1.CancelSync(ctx)
   964  				select {
   965  				case <-time.After(2 * time.Second):
   966  				case <-syncLoopCh:
   967  					return true
   968  				}
   969  			}
   970  			return false
   971  		}
   972  		require.True(t, waitForFail())
   973  		indexer1.PokeSync(ctx)
   974  		require.True(t, waitForFail())
   975  
   976  		// test search delegation with a specific conv
   977  		// delegate on queries shorter than search.MinTokenLength
   978  		opts.ConvID = &convID
   979  		// delegate if a single conv is not fully indexed
   980  		query = "hello"
   981  		_, err = g1.LocalChatDb.Nuke()
   982  		require.NoError(t, err)
   983  		require.NoError(t, indexer1.OnDbNuke(libkb.NewMetaContext(context.TODO(), g1.ExternalG())))
   984  		res = runSearch(query, opts, false /* expectedReindex*/)
   985  		require.Equal(t, 1, len(res.Hits))
   986  		convHit = res.Hits[0]
   987  		require.Equal(t, convID, convHit.ConvID)
   988  		require.Equal(t, 1, len(convHit.Hits))
   989  		verifyHit(convID, []chat1.MessageID{}, msgID10, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0])
   990  		verifySearchDone(1, true)
   991  
   992  		// delegate on regexp searches
   993  		query = "/hello/"
   994  		res = runSearch(query, opts, false /* expectedReindex*/)
   995  		require.Equal(t, 1, len(res.Hits))
   996  		convHit = res.Hits[0]
   997  		require.Equal(t, convID, convHit.ConvID)
   998  		require.Equal(t, 1, len(convHit.Hits))
   999  		verifyHit(convID, []chat1.MessageID{}, msgID10, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0])
  1000  		verifySearchDone(1, true)
  1001  
  1002  		query = "hi"
  1003  		searchMatch = chat1.ChatSearchMatch{
  1004  			StartIndex: 0,
  1005  			EndIndex:   2,
  1006  			Match:      "hi",
  1007  		}
  1008  		msgID11 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{
  1009  			Body: query,
  1010  		}), u1)
  1011  
  1012  		res = runSearch(query, opts, false /* expectedReindex*/)
  1013  		require.Equal(t, 1, len(res.Hits))
  1014  		convHit = res.Hits[0]
  1015  		require.Equal(t, convID, convHit.ConvID)
  1016  		require.Equal(t, 1, len(convHit.Hits))
  1017  		verifyHit(convID, []chat1.MessageID{msgID10}, msgID11, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0])
  1018  		verifySearchDone(1, true)
  1019  
  1020  		err = indexer1.Clear(ctx, uid1, convID)
  1021  		require.NoError(t, err)
  1022  		pi, err = indexer1.PercentIndexed(ctx, convID)
  1023  		require.NoError(t, err)
  1024  		require.Zero(t, pi)
  1025  
  1026  		err = indexer2.Clear(ctx, uid2, convID)
  1027  		require.NoError(t, err)
  1028  		pi, err = indexer2.PercentIndexed(ctx, convID)
  1029  		require.NoError(t, err)
  1030  		require.Zero(t, pi)
  1031  	})
  1032  }