github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/uithreadloader_test.go (about)

     1  package chat
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/keybase/client/go/chat/storage"
    10  	"github.com/keybase/client/go/chat/types"
    11  	"github.com/keybase/client/go/kbtest"
    12  	"github.com/keybase/client/go/protocol/chat1"
    13  	"github.com/keybase/client/go/protocol/gregor1"
    14  	"github.com/keybase/client/go/protocol/keybase1"
    15  	"github.com/keybase/clockwork"
    16  	"github.com/keybase/go-codec/codec"
    17  	"github.com/stretchr/testify/require"
    18  )
    19  
    20  func TestUIThreadLoaderGrouper(t *testing.T) {
    21  	useRemoteMock = false
    22  	defer func() { useRemoteMock = true }()
    23  	ctc := makeChatTestContext(t, "TestUIThreadLoaderGrouper", 6)
    24  	defer ctc.cleanup()
    25  
    26  	timeout := 2 * time.Second
    27  	users := ctc.users()
    28  	chatUI := kbtest.NewChatUI()
    29  	tc := ctc.world.Tcs[users[0].Username]
    30  	ctx := ctc.as(t, users[0]).startCtx
    31  	uid := gregor1.UID(users[0].GetUID().ToBytes())
    32  	listener0 := newServerChatListener()
    33  	ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
    34  	listener1 := newServerChatListener()
    35  	ctc.as(t, users[1]).h.G().NotifyRouter.AddListener(listener1)
    36  	baseconv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
    37  		chat1.ConversationMembersType_TEAM, users[1:]...)
    38  	topicName := "MKMK"
    39  	convFull, err := ctc.as(t, users[0]).chatLocalHandler().NewConversationLocal(ctx,
    40  		chat1.NewConversationLocalArg{
    41  			TlfName:       baseconv.TlfName,
    42  			TopicName:     &topicName,
    43  			TopicType:     chat1.TopicType_CHAT,
    44  			TlfVisibility: keybase1.TLFVisibility_PRIVATE,
    45  			MembersType:   chat1.ConversationMembersType_TEAM,
    46  		})
    47  	require.NoError(t, err)
    48  	consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
    49  	consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
    50  	consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
    51  	conv := convFull.Conv.Info
    52  
    53  	err = ctc.as(t, users[0]).chatLocalHandler().BulkAddToConv(ctx,
    54  		chat1.BulkAddToConvArg{
    55  			Usernames: []string{"foo", "bar", "baz"},
    56  			ConvID:    conv.Id,
    57  		})
    58  	require.NoError(t, err)
    59  	consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
    60  
    61  	err = ctc.as(t, users[0]).chatLocalHandler().BulkAddToConv(ctx,
    62  		chat1.BulkAddToConvArg{
    63  			Usernames: []string{users[3].Username},
    64  			ConvID:    conv.Id,
    65  		})
    66  	require.NoError(t, err)
    67  	consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
    68  
    69  	err = ctc.as(t, users[0]).chatLocalHandler().BulkAddToConv(ctx,
    70  		chat1.BulkAddToConvArg{
    71  			Usernames: []string{users[4].Username},
    72  			ConvID:    conv.Id,
    73  		})
    74  	require.NoError(t, err)
    75  	lastBulkAdd := consumeNewMsgRemote(t, listener0, chat1.MessageType_SYSTEM)
    76  
    77  	_, err = ctc.as(t, users[1]).chatLocalHandler().JoinConversationByIDLocal(ctx, conv.Id)
    78  	require.NoError(t, err)
    79  	consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
    80  	_, err = ctc.as(t, users[2]).chatLocalHandler().JoinConversationByIDLocal(ctx, conv.Id)
    81  	require.NoError(t, err)
    82  	consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
    83  	_, err = ctc.as(t, users[2]).chatLocalHandler().LeaveConversationLocal(ctx, conv.Id)
    84  	require.NoError(t, err)
    85  	consumeNewMsgRemote(t, listener0, chat1.MessageType_LEAVE)
    86  	mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{
    87  		Body: "HI",
    88  	}))
    89  	consumeNewMsgRemote(t, listener0, chat1.MessageType_TEXT)
    90  	_, err = ctc.as(t, users[2]).chatLocalHandler().JoinConversationByIDLocal(ctx, conv.Id)
    91  	require.NoError(t, err)
    92  	consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
    93  	_, err = ctc.as(t, users[1]).chatLocalHandler().LeaveConversationLocal(ctx, conv.Id)
    94  	require.NoError(t, err)
    95  	lastLeave := consumeNewMsgRemote(t, listener0, chat1.MessageType_LEAVE)
    96  	consumeLeaveConv(t, listener1)
    97  
    98  	_, err = ctc.as(t, users[5]).chatLocalHandler().JoinConversationByIDLocal(ctx, conv.Id)
    99  	require.NoError(t, err)
   100  	consumeNewMsgRemote(t, listener0, chat1.MessageType_JOIN)
   101  	_, err = ctc.as(t, users[5]).chatLocalHandler().LeaveConversationLocal(ctx, conv.Id)
   102  	require.NoError(t, err)
   103  	consumeNewMsgRemote(t, listener0, chat1.MessageType_LEAVE)
   104  
   105  	require.NoError(t, tc.Context().ConvSource.Clear(ctx, conv.Id, uid, nil))
   106  	_, err = tc.Context().ConvSource.GetMessages(ctx, convFull.Conv.GetConvID(), uid,
   107  		[]chat1.MessageID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, nil, nil, false)
   108  	require.NoError(t, err)
   109  
   110  	_, err = tc.Context().ParticipantsSource.Get(ctx, uid, conv.Id, types.InboxSourceDataSourceAll)
   111  	require.NoError(t, err)
   112  
   113  	clock := clockwork.NewFakeClock()
   114  	ri := ctc.as(t, users[0]).ri
   115  	uil := NewUIThreadLoader(tc.Context(), func() chat1.RemoteInterface { return ri })
   116  	uil.cachedThreadDelay = nil
   117  	uil.remoteThreadDelay = &timeout
   118  	uil.resolveThreadDelay = &timeout
   119  	uil.validatedDelay = 0
   120  	uil.clock = clock
   121  	cb := make(chan error, 1)
   122  	go func() {
   123  		cb <- uil.LoadNonblock(ctx, chatUI, uid, conv.Id, chat1.GetThreadReason_GENERAL,
   124  			chat1.GetThreadNonblockPgMode_DEFAULT, chat1.GetThreadNonblockCbMode_INCREMENTAL, nil, nil, nil)
   125  	}()
   126  	select {
   127  	case res := <-chatUI.ThreadCb:
   128  		require.False(t, res.Full)
   129  
   130  		require.Equal(t, 9, len(res.Thread.Messages))
   131  
   132  		require.True(t, res.Thread.Messages[0].IsPlaceholder())
   133  		require.True(t, res.Thread.Messages[1].IsPlaceholder())
   134  
   135  		require.Equal(t, chat1.MessageType_JOIN, res.Thread.Messages[2].GetMessageType())
   136  		require.Equal(t, 1, len(res.Thread.Messages[2].Valid().MessageBody.Join().Joiners))
   137  		require.Equal(t, 1, len(res.Thread.Messages[2].Valid().MessageBody.Join().Leavers))
   138  
   139  		require.Equal(t, chat1.MessageType_TEXT, res.Thread.Messages[3].GetMessageType())
   140  
   141  		require.Equal(t, chat1.MessageType_JOIN, res.Thread.Messages[4].GetMessageType())
   142  		require.Zero(t, len(res.Thread.Messages[4].Valid().MessageBody.Join().Joiners))
   143  		require.Equal(t, 1, len(res.Thread.Messages[4].Valid().MessageBody.Join().Leavers))
   144  
   145  		require.Equal(t, chat1.MessageType_JOIN, res.Thread.Messages[5].GetMessageType())
   146  		require.Equal(t, 2, len(res.Thread.Messages[5].Valid().MessageBody.Join().Joiners))
   147  		require.Zero(t, len(res.Thread.Messages[5].Valid().MessageBody.Join().Leavers))
   148  
   149  		require.Equal(t, chat1.MessageType_SYSTEM, res.Thread.Messages[6].GetMessageType())
   150  		bod := res.Thread.Messages[6].Valid().MessageBody.System()
   151  		sysTyp, err := bod.SystemType()
   152  		require.NoError(t, err)
   153  		require.Equal(t, chat1.MessageSystemType_BULKADDTOCONV, sysTyp)
   154  		require.Equal(t, 2, len(bod.Bulkaddtoconv().Usernames))
   155  
   156  		require.Equal(t, chat1.MessageType_JOIN, res.Thread.Messages[7].GetMessageType())
   157  		require.Zero(t, len(res.Thread.Messages[7].Valid().MessageBody.Join().Joiners))
   158  		require.Zero(t, len(res.Thread.Messages[7].Valid().MessageBody.Join().Leavers))
   159  
   160  	case <-time.After(timeout):
   161  		require.Fail(t, "no full cb")
   162  	}
   163  	require.NoError(t, tc.Context().ConvSource.Clear(ctx, conv.Id, uid, nil))
   164  	clock.Advance(5 * time.Second)
   165  	select {
   166  	case res := <-chatUI.ThreadCb:
   167  		require.True(t, res.Full)
   168  		require.Equal(t, 7, len(res.Thread.Messages))
   169  		for _, msg := range res.Thread.Messages {
   170  			switch msg.GetMessageID() {
   171  			case lastLeave.GetMessageID():
   172  				require.True(t, msg.IsPlaceholder())
   173  			case lastBulkAdd.GetMessageID():
   174  				require.Equal(t, chat1.MessageType_SYSTEM, msg.GetMessageType())
   175  			default:
   176  				require.Equal(t, chat1.MessageType_JOIN, msg.GetMessageType())
   177  			}
   178  		}
   179  	case <-time.After(timeout):
   180  		require.Fail(t, "no full cb")
   181  	}
   182  }
   183  
   184  func TestUIThreadLoaderCache(t *testing.T) {
   185  	useRemoteMock = false
   186  	defer func() { useRemoteMock = true }()
   187  	ctc := makeChatTestContext(t, "TestUIThreadLoaderCache", 1)
   188  	defer ctc.cleanup()
   189  
   190  	timeout := 2 * time.Second
   191  	users := ctc.users()
   192  	chatUI := kbtest.NewChatUI()
   193  	tc := ctc.world.Tcs[users[0].Username]
   194  	ctx := ctc.as(t, users[0]).startCtx
   195  	uid := gregor1.UID(users[0].GetUID().ToBytes())
   196  	<-ctc.as(t, users[0]).h.G().ConvLoader.Stop(ctx)
   197  	listener0 := newServerChatListener()
   198  	ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener0)
   199  	conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   200  		chat1.ConversationMembersType_IMPTEAMNATIVE)
   201  	mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{
   202  		Body: "HI",
   203  	}))
   204  	consumeNewMsgRemote(t, listener0, chat1.MessageType_TEXT)
   205  	require.NoError(t, tc.Context().ConvSource.Clear(ctx, conv.Id, uid, nil))
   206  	_, err := tc.Context().ConvSource.PullLocalOnly(ctx, conv.Id, uid, chat1.GetThreadReason_GENERAL, nil, nil, 0)
   207  	require.Error(t, err)
   208  	require.IsType(t, storage.MissError{}, err)
   209  
   210  	clock := clockwork.NewFakeClock()
   211  	ri := ctc.as(t, users[0]).ri
   212  	uil := NewUIThreadLoader(tc.Context(), func() chat1.RemoteInterface { return ri })
   213  	uil.cachedThreadDelay = &timeout
   214  	uil.resolveThreadDelay = &timeout
   215  	uil.validatedDelay = 0
   216  	uil.clock = clock
   217  	cb := make(chan error, 1)
   218  	go func() {
   219  		cb <- uil.LoadNonblock(ctx, chatUI, uid, conv.Id, chat1.GetThreadReason_GENERAL,
   220  			chat1.GetThreadNonblockPgMode_DEFAULT, chat1.GetThreadNonblockCbMode_INCREMENTAL, nil, nil, nil)
   221  	}()
   222  	select {
   223  	case res := <-chatUI.ThreadCb:
   224  		require.True(t, res.Full)
   225  		require.Equal(t, 2, len(res.Thread.Messages))
   226  	case <-time.After(timeout):
   227  		require.Fail(t, "no full cb")
   228  	}
   229  	_, err = tc.Context().ConvSource.PullLocalOnly(ctx, conv.Id, uid, chat1.GetThreadReason_GENERAL, nil, nil, 0)
   230  	require.Error(t, err)
   231  	require.IsType(t, storage.MissError{}, err)
   232  	clock.Advance(10 * time.Second)
   233  	worked := false
   234  	for i := 0; i < 5 && !worked; i++ {
   235  		select {
   236  		case err := <-cb:
   237  			require.NoError(t, err)
   238  			t.Logf("cb received: %d", i)
   239  			worked = true
   240  		case <-time.After(timeout):
   241  			t.Logf("end failed: %d", i)
   242  			clock.Advance(10 * time.Second)
   243  		}
   244  	}
   245  	require.True(t, worked)
   246  	tv, err := tc.Context().ConvSource.PullLocalOnly(ctx, conv.Id, uid, chat1.GetThreadReason_GENERAL, nil, nil, 0)
   247  	require.NoError(t, err)
   248  	require.Equal(t, 2, len(tv.Messages))
   249  }
   250  
   251  func TestUIThreadLoaderDisplayStatus(t *testing.T) {
   252  	useRemoteMock = false
   253  	defer func() { useRemoteMock = true }()
   254  	ctc := makeChatTestContext(t, "TestUIThreadLoaderCache", 1)
   255  	defer ctc.cleanup()
   256  
   257  	timeout := 2 * time.Second
   258  	users := ctc.users()
   259  	chatUI := kbtest.NewChatUI()
   260  	tc := ctc.world.Tcs[users[0].Username]
   261  	ctx := ctc.as(t, users[0]).startCtx
   262  	uid := gregor1.UID(users[0].GetUID().ToBytes())
   263  	conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   264  		chat1.ConversationMembersType_IMPTEAMNATIVE)
   265  	mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{
   266  		Body: "HI",
   267  	}))
   268  
   269  	clock := clockwork.NewFakeClock()
   270  	ri := ctc.as(t, users[0]).ri
   271  	uil := NewUIThreadLoader(tc.Context(), func() chat1.RemoteInterface { return ri })
   272  	rtd := 10 * time.Second
   273  	uil.cachedThreadDelay = nil
   274  	uil.remoteThreadDelay = &rtd
   275  	uil.resolveThreadDelay = &rtd
   276  	uil.validatedDelay = 0
   277  	uil.clock = clock
   278  	cb := make(chan error, 1)
   279  	go func() {
   280  		cb <- uil.LoadNonblock(ctx, chatUI, uid, conv.Id, chat1.GetThreadReason_GENERAL,
   281  			chat1.GetThreadNonblockPgMode_DEFAULT, chat1.GetThreadNonblockCbMode_INCREMENTAL, nil, nil, nil)
   282  	}()
   283  	select {
   284  	case res := <-chatUI.ThreadCb:
   285  		require.False(t, res.Full)
   286  		require.Equal(t, 2, len(res.Thread.Messages))
   287  	case <-time.After(timeout):
   288  		require.Fail(t, "no cache cb")
   289  	}
   290  	clock.Advance(5 * time.Second)
   291  	select {
   292  	case res := <-chatUI.ThreadStatusCb:
   293  		typ, err := res.Typ()
   294  		require.NoError(t, err)
   295  		require.Equal(t, chat1.UIChatThreadStatusTyp_SERVER, typ)
   296  	case <-time.After(timeout):
   297  		require.Fail(t, "no server status")
   298  	}
   299  	clock.Advance(6 * time.Second)
   300  	select {
   301  	case res := <-chatUI.ThreadCb:
   302  		require.True(t, res.Full)
   303  		require.Zero(t, len(res.Thread.Messages))
   304  	case <-time.After(timeout):
   305  		require.Fail(t, "no full cb")
   306  	}
   307  	time.Sleep(time.Second)
   308  	clock.Advance(time.Second)
   309  	select {
   310  	case res := <-chatUI.ThreadStatusCb:
   311  		typ, err := res.Typ()
   312  		require.NoError(t, err)
   313  		require.Equal(t, chat1.UIChatThreadStatusTyp_VALIDATING, typ)
   314  	case <-time.After(timeout):
   315  		require.Fail(t, "no validating status")
   316  	}
   317  	clock.Advance(20 * time.Second)
   318  	select {
   319  	case res := <-chatUI.ThreadStatusCb:
   320  		typ, err := res.Typ()
   321  		require.NoError(t, err)
   322  		require.Equal(t, chat1.UIChatThreadStatusTyp_VALIDATED, typ)
   323  	case <-time.After(timeout):
   324  		require.Fail(t, "no validating status")
   325  	}
   326  	select {
   327  	case err := <-cb:
   328  		require.NoError(t, err)
   329  	case <-time.After(timeout):
   330  		require.Fail(t, "no end")
   331  	}
   332  
   333  	// Too fast for status
   334  	rtd = 1 * time.Second
   335  	uil.remoteThreadDelay = &rtd
   336  	uil.resolveThreadDelay = nil
   337  	go func() {
   338  		cb <- uil.LoadNonblock(ctx, chatUI, uid, conv.Id, chat1.GetThreadReason_GENERAL,
   339  			chat1.GetThreadNonblockPgMode_DEFAULT, chat1.GetThreadNonblockCbMode_INCREMENTAL, nil, nil, nil)
   340  	}()
   341  	select {
   342  	case res := <-chatUI.ThreadCb:
   343  		require.False(t, res.Full)
   344  		require.Equal(t, 2, len(res.Thread.Messages))
   345  	case <-time.After(timeout):
   346  		require.Fail(t, "no cache cb")
   347  	}
   348  	clock.Advance(2 * time.Second)
   349  	select {
   350  	case res := <-chatUI.ThreadCb:
   351  		require.True(t, res.Full)
   352  		require.Zero(t, len(res.Thread.Messages))
   353  	case <-time.After(timeout):
   354  		require.Fail(t, "no full cb")
   355  	}
   356  	select {
   357  	case err := <-cb:
   358  		require.NoError(t, err)
   359  	case <-time.After(timeout):
   360  		require.Fail(t, "no end")
   361  	}
   362  	select {
   363  	case <-chatUI.ThreadStatusCb:
   364  		require.Fail(t, "no status cbs")
   365  	default:
   366  	}
   367  }
   368  
   369  func TestUIThreadLoaderSingleFlight(t *testing.T) {
   370  	useRemoteMock = false
   371  	defer func() { useRemoteMock = true }()
   372  	ctc := makeChatTestContext(t, "TestUIThreadLoaderSingleFlight", 1)
   373  	defer ctc.cleanup()
   374  
   375  	timeout := 20 * time.Second
   376  	users := ctc.users()
   377  	chatUI := kbtest.NewChatUI()
   378  	tc := ctc.world.Tcs[users[0].Username]
   379  	ctx := ctc.as(t, users[0]).startCtx
   380  	uid := gregor1.UID(users[0].GetUID().ToBytes())
   381  	conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   382  		chat1.ConversationMembersType_IMPTEAMNATIVE)
   383  	mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{
   384  		Body: "HI",
   385  	}))
   386  
   387  	clock := clockwork.NewFakeClock()
   388  	ri := ctc.as(t, users[0]).ri
   389  	uil := NewUIThreadLoader(tc.Context(), func() chat1.RemoteInterface { return ri })
   390  	rtd := 1 * time.Second
   391  	uil.cachedThreadDelay = nil
   392  	uil.remoteThreadDelay = &rtd
   393  	uil.resolveThreadDelay = nil
   394  	uil.validatedDelay = 0
   395  	uil.clock = clock
   396  	cb := make(chan error, 1)
   397  	cb2 := make(chan error, 1)
   398  	go func() {
   399  		cb <- uil.LoadNonblock(ctx, chatUI, uid, conv.Id, chat1.GetThreadReason_GENERAL,
   400  			chat1.GetThreadNonblockPgMode_DEFAULT, chat1.GetThreadNonblockCbMode_INCREMENTAL, nil, nil, nil)
   401  	}()
   402  	go func() {
   403  		cb2 <- uil.LoadNonblock(ctx, chatUI, uid, conv.Id, chat1.GetThreadReason_GENERAL,
   404  			chat1.GetThreadNonblockPgMode_DEFAULT, chat1.GetThreadNonblockCbMode_INCREMENTAL, nil, nil, nil)
   405  	}()
   406  	time.Sleep(time.Second)
   407  	errors := 0
   408  	select {
   409  	case res := <-chatUI.ThreadCb:
   410  		require.False(t, res.Full)
   411  		require.Equal(t, 2, len(res.Thread.Messages))
   412  	case <-time.After(timeout):
   413  		require.Fail(t, "no cache cb")
   414  	}
   415  	clock.Advance(2 * time.Second)
   416  	select {
   417  	case res := <-chatUI.ThreadCb:
   418  		require.True(t, res.Full)
   419  		require.Zero(t, len(res.Thread.Messages))
   420  	case <-time.After(timeout):
   421  		require.Fail(t, "no full cb")
   422  	}
   423  	select {
   424  	case err := <-cb:
   425  		if err != nil {
   426  			errors++
   427  		}
   428  	case <-time.After(timeout):
   429  		require.Fail(t, "no end")
   430  	}
   431  	select {
   432  	case err := <-cb2:
   433  		if err != nil {
   434  			errors++
   435  		}
   436  	case <-time.After(timeout):
   437  		require.Fail(t, "no end")
   438  	}
   439  	select {
   440  	case <-chatUI.ThreadStatusCb:
   441  		require.Fail(t, "no status cbs")
   442  	default:
   443  	}
   444  	require.Equal(t, 1, errors)
   445  }
   446  
   447  type testingKnownRemote struct {
   448  	chat1.RemoteInterface
   449  	t *testing.T
   450  }
   451  
   452  func (r *testingKnownRemote) GetMessagesRemote(ctx context.Context, arg chat1.GetMessagesRemoteArg) (res chat1.GetMessagesRemoteRes, err error) {
   453  	require.Fail(r.t, "no remote reqs!")
   454  	return res, err
   455  }
   456  
   457  func TestUIThreadLoaderKnownRemotes(t *testing.T) {
   458  	useRemoteMock = false
   459  	defer func() { useRemoteMock = true }()
   460  	ctc := makeChatTestContext(t, "TestUIThreadLoaderKnownRemotes", 1)
   461  	defer ctc.cleanup()
   462  
   463  	timeout := 2 * time.Second
   464  	users := ctc.users()
   465  	chatUI := kbtest.NewChatUI()
   466  	tc := ctc.world.Tcs[users[0].Username]
   467  	ctx := ctc.as(t, users[0]).startCtx
   468  	uid := gregor1.UID(users[0].GetUID().ToBytes())
   469  	<-ctc.as(t, users[0]).h.G().ConvLoader.Stop(ctx)
   470  	listener := newServerChatListener()
   471  	ctc.as(t, users[0]).h.G().NotifyRouter.AddListener(listener)
   472  	conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT,
   473  		chat1.ConversationMembersType_IMPTEAMNATIVE)
   474  	msgID1 := mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{
   475  		Body: "HI",
   476  	}))
   477  	msgID2 := mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{
   478  		Body: "HI2",
   479  	}))
   480  	consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
   481  	consumeNewMsgRemote(t, listener, chat1.MessageType_TEXT)
   482  
   483  	require.NoError(t, tc.Context().ConvSource.Clear(ctx, conv.Id, uid, nil))
   484  	_, err := ctc.as(t, users[0]).chatLocalHandler().GetMessagesLocal(ctx, chat1.GetMessagesLocalArg{
   485  		ConversationID: conv.Id,
   486  		MessageIDs:     []chat1.MessageID{1, msgID1},
   487  	})
   488  	require.NoError(t, err)
   489  
   490  	msgBoxed := chat1.MessageBoxed{
   491  		ServerHeader: &chat1.MessageServerHeader{
   492  			MessageID: msgID2,
   493  		},
   494  	}
   495  	var dat []byte
   496  	mh := codec.MsgpackHandle{WriteExt: true}
   497  	require.NoError(t, codec.NewEncoderBytes(&dat, &mh).Encode(msgBoxed))
   498  	knownRemote := base64.StdEncoding.EncodeToString(dat)
   499  	cb := make(chan error, 1)
   500  	ri := ctc.as(t, users[0]).ri
   501  	testingRemote := &testingKnownRemote{
   502  		RemoteInterface: ri,
   503  		t:               t,
   504  	}
   505  	clock := clockwork.NewFakeClock()
   506  
   507  	uil := NewUIThreadLoader(tc.Context(), func() chat1.RemoteInterface { return testingRemote })
   508  	uil.remoteThreadDelay = &timeout
   509  	uil.cachedThreadDelay = nil
   510  	uil.validatedDelay = 0
   511  	uil.clock = clock
   512  	go func() {
   513  		cb <- uil.LoadNonblock(ctx, chatUI, uid, conv.Id, chat1.GetThreadReason_GENERAL,
   514  			chat1.GetThreadNonblockPgMode_DEFAULT, chat1.GetThreadNonblockCbMode_FULL,
   515  			[]string{knownRemote}, nil, nil)
   516  	}()
   517  	numNonPlace := func(msgs []chat1.UIMessage) (res int) {
   518  		for _, msg := range msgs {
   519  			if !msg.IsPlaceholder() {
   520  				res++
   521  			}
   522  		}
   523  		return res
   524  	}
   525  	select {
   526  	case res := <-chatUI.ThreadCb:
   527  		require.False(t, res.Full)
   528  		require.Equal(t, 2, numNonPlace(res.Thread.Messages))
   529  	case <-time.After(timeout):
   530  		require.Fail(t, "no cache cb")
   531  	}
   532  	clock.Advance(10 * time.Second)
   533  	select {
   534  	case res := <-chatUI.ThreadCb:
   535  		require.True(t, res.Full)
   536  		require.Equal(t, 3, numNonPlace(res.Thread.Messages))
   537  	case <-time.After(timeout):
   538  		require.Fail(t, "no cache cb")
   539  	}
   540  	select {
   541  	case err = <-cb:
   542  		require.NoError(t, err)
   543  	case <-time.After(timeout):
   544  		require.Fail(t, "no end cb")
   545  	}
   546  }