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 }