@@ -33,7 +33,8 @@ type TestConversation = {
3333 contactId : string
3434 messages : unknown [ ]
3535 lastActivityAt : Date | null
36- agentLastReadAt ?: Date
36+ agentLastReadAt ?: Date | null
37+ adminRepliedAt ?: Date | null
3738}
3839
3940type TestMessage = {
@@ -42,6 +43,8 @@ type TestMessage = {
4243 conversationId : string
4344 createdAt : Date
4445 messageType : string
46+ senderType ?: string
47+ senderId ?: string | null
4548}
4649
4750const makeConversation = ( id : string , lastActivityAt : Date ) =>
@@ -62,6 +65,22 @@ const makeMessage = (conversationId: string, createdAt: Date) =>
6265 messageType : "incoming" ,
6366 } ) as TestMessage
6467
68+ const makeOutgoingMessage = (
69+ conversationId : string ,
70+ createdAt : Date ,
71+ senderType : "user" | "api" | "bot" | "system" ,
72+ // `received-message` stamps a channel echo senderType "user" with a null
73+ // senderId; `createOutgoing` always carries the acting user's id.
74+ senderId : string | null = senderType === "user" ? "user-1" : null ,
75+ ) =>
76+ ( {
77+ ...makeMessage ( conversationId , createdAt ) ,
78+ id : `msg-${ conversationId } -${ senderType } ` ,
79+ messageType : "outgoing" ,
80+ senderType,
81+ senderId,
82+ } ) as TestMessage
83+
6584const setConversationUrl = ( conversationId : string | null ) => {
6685 window . history . replaceState (
6786 { } ,
@@ -391,6 +410,131 @@ describe("chat store conversation updates", () => {
391410 } )
392411} )
393412
413+ describe ( "chat store handleNewMessage read state" , ( ) => {
414+ const AGENT_LAST_READ_AT = new Date ( "2026-01-01T00:00:00Z" )
415+
416+ const makeUnreadStore = ( activeConversationId : string | null = null ) => {
417+ const store = createChatStore ( )
418+ const conversation = {
419+ ...makeConversation ( "conv-1" , new Date ( "2026-01-01T01:00:00Z" ) ) ,
420+ agentLastReadAt : AGENT_LAST_READ_AT ,
421+ adminRepliedAt : null ,
422+ }
423+ store . setState ( {
424+ conversations : [ conversation ] as never ,
425+ activeConversationId,
426+ } )
427+ return store
428+ }
429+
430+ const readStateOf = ( store : ReturnType < typeof createChatStore > ) => {
431+ const conversation = store
432+ . getState ( )
433+ . conversations . find ( ( c ) => c . id === "conv-1" ) as TestConversation
434+ return {
435+ agentLastReadAt : conversation . agentLastReadAt ,
436+ adminRepliedAt : conversation . adminRepliedAt ,
437+ }
438+ }
439+
440+ beforeEach ( ( ) => {
441+ vi . clearAllMocks ( )
442+ setConversationUrl ( null )
443+ } )
444+
445+ test . each ( [
446+ "bot" ,
447+ "system" ,
448+ ] as const ) ( "a %s outgoing message leaves the conversation unread" , async ( senderType ) => {
449+ const store = makeUnreadStore ( )
450+
451+ await store
452+ . getState ( )
453+ . handleNewMessage (
454+ makeOutgoingMessage (
455+ "conv-1" ,
456+ new Date ( "2026-01-01T02:00:00Z" ) ,
457+ senderType ,
458+ ) as never ,
459+ )
460+
461+ expect ( readStateOf ( store ) ) . toEqual ( {
462+ agentLastReadAt : AGENT_LAST_READ_AT ,
463+ adminRepliedAt : null ,
464+ } )
465+ } )
466+
467+ test . each ( [
468+ "user" ,
469+ "api" ,
470+ ] as const ) ( "a %s outgoing message marks the conversation read and replied" , async ( senderType ) => {
471+ const store = makeUnreadStore ( )
472+
473+ await store
474+ . getState ( )
475+ . handleNewMessage (
476+ makeOutgoingMessage (
477+ "conv-1" ,
478+ new Date ( "2026-01-01T02:00:00Z" ) ,
479+ senderType ,
480+ ) as never ,
481+ )
482+
483+ const { agentLastReadAt, adminRepliedAt } = readStateOf ( store )
484+ expect ( agentLastReadAt ) . not . toEqual ( AGENT_LAST_READ_AT )
485+ expect ( agentLastReadAt ) . toEqual ( adminRepliedAt )
486+ } )
487+
488+ test ( "a channel echo (senderType user, no senderId) leaves the conversation unread" , async ( ) => {
489+ const store = makeUnreadStore ( )
490+
491+ await store
492+ . getState ( )
493+ . handleNewMessage (
494+ makeOutgoingMessage (
495+ "conv-1" ,
496+ new Date ( "2026-01-01T02:00:00Z" ) ,
497+ "user" ,
498+ null ,
499+ ) as never ,
500+ )
501+
502+ expect ( readStateOf ( store ) ) . toEqual ( {
503+ agentLastReadAt : AGENT_LAST_READ_AT ,
504+ adminRepliedAt : null ,
505+ } )
506+ } )
507+
508+ test ( "an incoming message on the open conversation marks it read without an admin reply" , async ( ) => {
509+ const store = makeUnreadStore ( "conv-1" )
510+
511+ await store
512+ . getState ( )
513+ . handleNewMessage (
514+ makeMessage ( "conv-1" , new Date ( "2026-01-01T02:00:00Z" ) ) as never ,
515+ )
516+
517+ const { agentLastReadAt, adminRepliedAt } = readStateOf ( store )
518+ expect ( agentLastReadAt ) . not . toEqual ( AGENT_LAST_READ_AT )
519+ expect ( adminRepliedAt ) . toBeNull ( )
520+ } )
521+
522+ test ( "an incoming message on a background conversation stays unread" , async ( ) => {
523+ const store = makeUnreadStore ( "conv-other" )
524+
525+ await store
526+ . getState ( )
527+ . handleNewMessage (
528+ makeMessage ( "conv-1" , new Date ( "2026-01-01T02:00:00Z" ) ) as never ,
529+ )
530+
531+ expect ( readStateOf ( store ) ) . toEqual ( {
532+ agentLastReadAt : AGENT_LAST_READ_AT ,
533+ adminRepliedAt : null ,
534+ } )
535+ } )
536+ } )
537+
394538describe ( "chat store loadMoreMessages" , ( ) => {
395539 test ( "prepends the older page and advances the message cursor" , async ( ) => {
396540 const store = createChatStore ( )
0 commit comments