@ -1,34 +1,43 @@
/** biome-ignore-all lint/style/noNonNullAssertion: <tests> */
import { Types } from "@meshtastic/core" ;
import { setAutoFreeze } from "immer" ;
import { beforeEach , describe , expect , it , vi } from "vitest" ;
import {
getConversationId ,
MessageState ,
MessageType ,
useMessageStore ,
} from "./index.ts" ;
import type {
ChannelId ,
ConversationId ,
Message ,
MessageLogMap ,
} from "./types.ts" ;
vi . mock ( "../utils/indexDB.ts" , ( ) = > {
const memoryStorage : Record < string , string > = { } ;
return {
storageWithMapSupport : {
getItem : vi.fn ( async ( name : string ) : Promise < string | null > = > {
return ( await memoryStorage [ name ] ) ? ? null ;
} ) ,
setItem : vi.fn ( async ( name : string , value : string ) : Promise < void > = > {
memoryStorage [ name ] = await value ;
} ) ,
removeItem : vi.fn ( async ( name : string ) : Promise < void > = > {
await delete memoryStorage [ name ] ;
} ) ,
import { getConversationId , MessageState , MessageType } from "./index.ts" ;
import type { ChannelId , Message } from "./types.ts" ;
const idbMem = new Map < string , string > ( ) ;
vi . mock ( "idb-keyval" , ( ) = > ( {
get : vi . fn ( ( key : string ) = > Promise . resolve ( idbMem . get ( key ) ) ) ,
set : vi . fn ( ( key : string , val : string ) = > {
idbMem . set ( key , val ) ;
return Promise . resolve ( ) ;
} ) ,
del : vi.fn ( ( k : string ) = > {
idbMem . delete ( k ) ;
return Promise . resolve ( ) ;
} ) ,
} ) ) ;
async function freshStore ( persist = false ) {
vi . resetModules ( ) ;
// suppress console output from the store during tests (for github actions)
vi . spyOn ( console , "debug" ) . mockImplementation ( ( ) = > { } ) ;
vi . spyOn ( console , "log" ) . mockImplementation ( ( ) = > { } ) ;
vi . spyOn ( console , "info" ) . mockImplementation ( ( ) = > { } ) ;
// Mock feature flag for persistence
vi . doMock ( "@core/services/featureFlags" , ( ) = > ( {
featureFlags : {
get : vi . fn ( ( key : string ) = >
key === "persistMessages" ? persist : false ,
) ,
} ,
} ;
} ) ;
} ) ) ;
const mod = await import ( "./index.ts" ) ;
return mod ;
}
const myNodeNum = 111 ;
const otherNodeNum1 = 222 ;
@ -90,53 +99,56 @@ const broadcastMessage2: Message = {
message : "Broadcast message 2" ,
} ;
describe ( "useMessageStore" , ( ) = > {
const initialState = useMessageStore . getState ( ) ;
describe ( "MessageStore persistence & rehydrate" , ( ) = > {
beforeEach ( ( ) = > {
useMessageStore . setState (
{
. . . initialState ,
messages : {
direct : new Map < ConversationId , MessageLogMap > ( ) ,
broadcast : new Map < ChannelId , MessageLogMap > ( ) ,
} ,
draft : new Map < Types.Destination , string > ( ) ,
} ,
true ,
) ;
idbMem . clear ( ) ;
vi . clearAllMocks ( ) ;
} ) ;
it ( "should have correct initial state" , ( ) = > {
it ( "should have correct initial state" , async ( ) = > {
const { useMessageStore } = await freshStore ( ) ;
const state = useMessageStore . getState ( ) ;
expect ( state . messages . direct ) . toBeInstanceOf ( Map ) ;
expect ( state . messages . direct . size ) . toBe ( 0 ) ;
expect ( state . messages . broadcast ) . toBeInstanceOf ( Map ) ;
expect ( state . messages . broadcast . size ) . toBe ( 0 ) ;
expect ( state . draft ) . toBeInstanceOf ( Map ) ;
expect ( state . draft . size ) . toBe ( 0 ) ;
expect ( state . nodeNum ) . toBe ( 0 ) ;
expect ( state . activeChat ) . toBe ( 0 ) ;
expect ( state . chatType ) . toBe ( MessageType . Broadcast ) ;
const store = state . addMessageStore ( 123 ) ;
expect ( store . messages . direct ) . toBeInstanceOf ( Map ) ;
expect ( store . messages . direct . size ) . toBe ( 0 ) ;
expect ( store . messages . broadcast ) . toBeInstanceOf ( Map ) ;
expect ( store . messages . broadcast . size ) . toBe ( 0 ) ;
expect ( store . drafts ) . toBeInstanceOf ( Map ) ;
expect ( store . drafts . size ) . toBe ( 0 ) ;
expect ( store . myNodeNum ) . toBe ( undefined ) ;
expect ( store . activeChat ) . toBe ( 0 ) ;
expect ( store . chatType ) . toBe ( MessageType . Broadcast ) ;
} ) ;
it ( "should set nodeNum" , ( ) = > {
useMessageStore . getState ( ) . setNodeNum ( myNodeNum ) ;
expect ( useMessageStore . getState ( ) . nodeNum ) . toBe ( myNodeNum ) ;
it ( "should set nodeNum" , async ( ) = > {
const { useMessageStore } = await freshStore ( ) ;
const state = useMessageStore . getState ( ) ;
const db = state . addMessageStore ( 123 ) ;
db . setNodeNum ( myNodeNum ) ;
expect ( useMessageStore . getState ( ) . getMessageStore ( 123 ) ? . myNodeNum ) . toBe (
myNodeNum ,
) ;
} ) ;
describe ( "saveMessage" , ( ) = > {
describe ( "saveMessage" , async ( ) = > {
const { useMessageStore } = await freshStore ( ) ;
const state = useMessageStore . getState ( ) ;
state . addMessageStore ( 123 ) ;
it ( "should save a direct message with correct Map structure" , ( ) = > {
useMessageStore . getState ( ) . saveMessage ( directMessageToOther1 ) ;
const state = useMessageStore . getState ( ) ;
state . getMessageStore ( 123 ) ? . saveMessage ( directMessageToOther1 ) ;
const conversationId = getConversationId (
directMessageToOther1 . from ,
directMessageToOther1 . to ,
) ;
const store = state . getMessageStore ( 123 ) ! ;
// Check if the conversation Map exists
expect ( state . messages . direct . has ( conversationId ) ) . toBe ( true ) ;
const conversationLog = stat e . messages . direct . get ( conversationId ) ;
expect ( stor e . messages . direct . has ( conversationId ) ) . toBe ( true ) ;
const conversationLog = stor e . messages . direct . get ( conversationId ) ;
// Check if the inner Map (MessageLogMap) exists and is a Map
expect ( conversationLog ) . toBeInstanceOf ( Map ) ;
// Check if the message exists within the inner Map
@ -148,12 +160,12 @@ describe("useMessageStore", () => {
} ) ;
it ( "should save a broadcast message with correct Map structure" , ( ) = > {
useMes sageS tor e. getState ( ) . saveMessage ( broadcastMessage1 ) ;
const stat e = useMes sageS tor e. getState ( ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( broadcastMessage1 ) ;
const stor e = st ate . getMessageStore ( 123 ) ! ;
const channelId = broadcastMessage1 . channel ;
expect ( stat e . messages . broadcast . has ( channelId ) ) . toBe ( true ) ;
const channelLog = stat e . messages . broadcast . get ( channelId ) ;
expect ( stor e . messages . broadcast . has ( channelId ) ) . toBe ( true ) ;
const channelLog = stor e . messages . broadcast . get ( channelId ) ;
expect ( channelLog ) . toBeInstanceOf ( Map ) ;
expect ( channelLog ? . has ( broadcastMessage1 . messageId ) ) . toBe ( true ) ;
expect ( channelLog ? . get ( broadcastMessage1 . messageId ) ) . toEqual (
@ -162,46 +174,50 @@ describe("useMessageStore", () => {
} ) ;
it ( "should save multiple messages correctly" , ( ) = > {
useMes sageS tor e. getState ( ) . saveMessage ( directMessageToOther1 ) ;
useMes sageS tor e. getState ( ) . saveMessage ( directMessageFromOther1 ) ;
useMes sageS tor e. getState ( ) . saveMessage ( broadcastMessage1 ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( directMessageToOther1 ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( directMessageFromOther1 ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( broadcastMessage1 ) ;
const stat e = useMes sageS tor e. getState ( ) ;
const stor e = st ate . getMessageStore ( 123 ) ! ;
const convId1 = getConversationId ( myNodeNum , otherNodeNum1 ) ;
expect (
stat e . messages . direct
stor e . messages . direct
. get ( convId1 )
? . get ( directMessageToOther1 . messageId ) ,
) . toEqual ( directMessageToOther1 ) ;
expect (
stat e . messages . direct
stor e . messages . direct
. get ( convId1 )
? . get ( directMessageFromOther1 . messageId ) ,
) . toEqual ( directMessageFromOther1 ) ;
const channelId = broadcastMessage1 . channel ;
expect (
stat e . messages . broadcast
stor e . messages . broadcast
. get ( channelId )
? . get ( broadcastMessage1 . messageId ) ,
) . toEqual ( broadcastMessage1 ) ;
} ) ;
} ) ;
describe ( "getMessages" , ( ) = > {
describe ( "getMessages" , async ( ) = > {
const { useMessageStore } = await freshStore ( ) ;
const state = useMessageStore . getState ( ) ;
state . addMessageStore ( 123 ) ;
beforeEach ( ( ) = > {
useMessageStore . getState ( ) . setNodeNum ( myNodeNum ) ;
useMessageStore . getState ( ) . saveMessage ( directMessageToOther1 ) ;
useMessageStore . getState ( ) . saveMessage ( directMessageFromOther1 ) ;
useMes sageS tor e. getState ( ) . saveMessage ( directMessageToOther2 ) ;
useMes sageS tor e. getState ( ) . saveMessage ( broadcastMessage1 ) ;
useMes sageS tor e. getState ( ) . saveMessage ( broadcastMessage2 ) ;
st ate . getMessageStore ( 123 ) ? . setNodeNum ( myNodeNum ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( directMessageToOther1 ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( directMessageFromOther1 ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( directMessageToOther2 ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( broadcastMessage1 ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( broadcastMessage2 ) ;
} ) ;
it ( "should return broadcast messages for a channel, sorted by date" , ( ) = > {
const messages = useMes sageS tor e. getState ( ) . getMessages ( {
const messages = st ate . getMessageStore ( 123 ) ! . getMessages ( {
type : MessageType . Broadcast ,
channelId : broadcastChannel ,
} ) ;
@ -211,7 +227,7 @@ describe("useMessageStore", () => {
} ) ;
it ( "should return empty array for broadcast if channel has no messages" , ( ) = > {
const messages = useMes sageS tor e. getState ( ) . getMessages ( {
const messages = st ate . getMessageStore ( 123 ) ! . getMessages ( {
type : MessageType . Broadcast ,
channelId : Types.ChannelNumber.Channel1 ,
} ) ;
@ -219,7 +235,7 @@ describe("useMessageStore", () => {
} ) ;
it ( "should return combined direct messages for a specific chat pair, sorted by date" , ( ) = > {
const messages = useMes sageS tor e. getState ( ) . getMessages ( {
const messages = st ate . getMessageStore ( 123 ) ! . getMessages ( {
type : MessageType . Direct ,
nodeA : myNodeNum ,
nodeB : otherNodeNum1 ,
@ -230,7 +246,7 @@ describe("useMessageStore", () => {
} ) ;
it ( "should return only relevant direct messages for a different chat pair" , ( ) = > {
const messages = useMes sageS tor e. getState ( ) . getMessages ( {
const messages = st ate . getMessageStore ( 123 ) ! . getMessages ( {
type : MessageType . Direct ,
nodeA : myNodeNum ,
nodeB : otherNodeNum2 ,
@ -240,7 +256,7 @@ describe("useMessageStore", () => {
} ) ;
it ( "should return empty array for direct chat if no messages exist" , ( ) = > {
const messages = useMes sageS tor e. getState ( ) . getMessages ( {
const messages = st ate . getMessageStore ( 123 ) ! . getMessages ( {
type : MessageType . Direct ,
nodeA : myNodeNum ,
nodeB : 999 ,
@ -249,16 +265,20 @@ describe("useMessageStore", () => {
} ) ;
} ) ;
describe ( "setMessageState" , ( ) = > {
describe ( "setMessageState" , async ( ) = > {
const { useMessageStore } = await freshStore ( ) ;
const state = useMessageStore . getState ( ) ;
state . addMessageStore ( 123 ) ;
beforeEach ( ( ) = > {
useMessageStore . getState ( ) . setNodeNum ( myNodeNum ) ;
useMessageStore . getState ( ) . saveMessage ( directMessageToOther1 ) ;
useMessageStore . getState ( ) . saveMessage ( directMessageFromOther1 ) ;
useMes sageS tor e. getState ( ) . saveMessage ( broadcastMessage1 ) ;
st ate . getMessageStore ( 123 ) ? . setNodeNum ( myNodeNum ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( directMessageToOther1 ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( directMessageFromOther1 ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( broadcastMessage1 ) ;
} ) ;
it ( "should update state for a direct message" , ( ) = > {
useMes sageS tor e. getState ( ) . setMessageState ( {
st ate . getMessageStore ( 123 ) ! . setMessageState ( {
type : MessageType . Direct ,
nodeA : directMessageToOther1.from ,
nodeB : directMessageToOther1.to ,
@ -271,13 +291,14 @@ describe("useMessageStore", () => {
) ;
const message = useMessageStore
. getState ( )
. getMessageStore ( 123 ) !
. messages . direct . get ( conversationId )
? . get ( directMessageToOther1 . messageId ) ;
expect ( message ? . state ) . toBe ( MessageState . Ack ) ;
} ) ;
it ( "should update state for another direct message in the same conversation" , ( ) = > {
useMes sageS tor e. getState ( ) . setMessageState ( {
st ate . getMessageStore ( 123 ) ! . setMessageState ( {
type : MessageType . Direct ,
nodeA : directMessageFromOther1.from ,
nodeB : directMessageFromOther1.to ,
@ -290,13 +311,14 @@ describe("useMessageStore", () => {
) ;
const message = useMessageStore
. getState ( )
. getMessageStore ( 123 ) !
. messages . direct . get ( conversationId )
? . get ( directMessageFromOther1 . messageId ) ;
expect ( message ? . state ) . toBe ( MessageState . Failed ) ;
} ) ;
it ( "should update state for a broadcast message" , ( ) = > {
useMes sageS tor e. getState ( ) . setMessageState ( {
st ate . getMessageStore ( 123 ) ! . setMessageState ( {
type : MessageType . Broadcast ,
channelId : broadcastChannel ,
messageId : broadcastMessage1.messageId ,
@ -304,6 +326,7 @@ describe("useMessageStore", () => {
} ) ;
const message = useMessageStore
. getState ( )
. getMessageStore ( 123 ) !
. messages . broadcast . get ( broadcastChannel )
? . get ( broadcastMessage1 . messageId ) ;
expect ( message ? . state ) . toBe ( MessageState . Ack ) ;
@ -311,7 +334,7 @@ describe("useMessageStore", () => {
it ( "should warn if message is not found (direct)" , ( ) = > {
const warnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) = > { } ) ;
useMes sageS tor e. getState ( ) . setMessageState ( {
st ate . getMessageStore ( 123 ) ! . setMessageState ( {
type : MessageType . Direct ,
nodeA : myNodeNum ,
nodeB : otherNodeNum1 ,
@ -328,7 +351,7 @@ describe("useMessageStore", () => {
it ( "should warn if message is not found (broadcast)" , ( ) = > {
const warnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) = > { } ) ;
useMes sageS tor e. getState ( ) . setMessageState ( {
st ate . getMessageStore ( 123 ) ! . setMessageState ( {
type : MessageType . Broadcast ,
channelId : broadcastChannel ,
messageId : 999 ,
@ -344,7 +367,7 @@ describe("useMessageStore", () => {
it ( "should warn if conversation/channel is not found" , ( ) = > {
const warnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) = > { } ) ;
useMes sageS tor e. getState ( ) . setMessageState ( {
st ate . getMessageStore ( 123 ) ! . setMessageState ( {
type : MessageType . Direct ,
nodeA : myNodeNum ,
nodeB : 998 ,
@ -360,14 +383,18 @@ describe("useMessageStore", () => {
} ) ;
} ) ;
describe ( "clearMessageByMessageId" , ( ) = > {
describe ( "clearMessageByMessageId" , async ( ) = > {
const { useMessageStore } = await freshStore ( ) ;
const state = useMessageStore . getState ( ) ;
state . addMessageStore ( 123 ) ;
const extraDirectMessageId = 1011 ;
beforeEach ( ( ) = > {
useMessageStore . getState ( ) . setNodeNum ( myNodeNum ) ;
useMessageStore . getState ( ) . saveMessage ( directMessageToOther1 ) ;
useMes sageS tor e. getState ( ) . saveMessage ( directMessageFromOther1 ) ;
useMes sageS tor e. getState ( ) . saveMessage ( broadcastMessage1 ) ;
useMes sageS tor e. getState ( ) . saveMessage ( {
st ate . getMessageStore ( 123 ) ? . setNodeNum ( myNodeNum ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( directMessageToOther1 ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( directMessageFromOther1 ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( broadcastMessage1 ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( {
. . . directMessageToOther1 ,
messageId : extraDirectMessageId ,
date : Date.now ( ) + 50 ,
@ -380,21 +407,21 @@ describe("useMessageStore", () => {
const nodeB = directMessageToOther1 . to ;
const conversationId = getConversationId ( nodeA , nodeB ) ;
useMes sageS tor e. getState ( ) . clearMessageByMessageId ( {
st ate . getMessageStore ( 123 ) ? . clearMessageByMessageId ( {
type : MessageType . Direct ,
nodeA : nodeA ,
nodeB : nodeB ,
messageId : messageIdToDelete ,
} ) ;
const stat e = useMessageStore . getState ( ) ;
const conversationLog = stat e . messages . direct . get ( conversationId ) ;
const stor e = useMessageStore . getState ( ) . getMessageStore ( 123 ) ! ;
const conversationLog = stor e . messages . direct . get ( conversationId ) ;
expect ( conversationLog ? . has ( messageIdToDelete ) ) . toBe ( false ) ;
expect ( conversationLog ? . has ( extraDirectMessageId ) ) . toBe ( true ) ;
expect ( conversationLog ? . has ( directMessageFromOther1 . messageId ) ) . toBe (
true ,
) ;
expect ( stat e . messages . direct . has ( conversationId ) ) . toBe ( true ) ;
expect ( stor e . messages . direct . has ( conversationId ) ) . toBe ( true ) ;
} ) ;
it ( "should delete another specific direct message" , ( ) = > {
@ -403,15 +430,15 @@ describe("useMessageStore", () => {
const nodeB = directMessageFromOther1 . to ;
const conversationId = getConversationId ( nodeA , nodeB ) ;
useMes sageS tor e. getState ( ) . clearMessageByMessageId ( {
st ate . getMessageStore ( 123 ) ? . clearMessageByMessageId ( {
type : MessageType . Direct ,
nodeA : nodeA ,
nodeB : nodeB ,
messageId : messageIdToDelete ,
} ) ;
const stat e = useMessageStore . getState ( ) ;
const conversationLog = stat e . messages . direct . get ( conversationId ) ;
const stor e = useMessageStore . getState ( ) . getMessageStore ( 123 ) ! ;
const conversationLog = stor e . messages . direct . get ( conversationId ) ;
expect ( conversationLog ? . has ( messageIdToDelete ) ) . toBe ( false ) ;
expect ( conversationLog ? . has ( directMessageToOther1 . messageId ) ) . toBe ( true ) ;
expect ( conversationLog ? . has ( extraDirectMessageId ) ) . toBe ( true ) ;
@ -421,15 +448,15 @@ describe("useMessageStore", () => {
const messageIdToDelete = broadcastMessage1 . messageId ;
const channelId = broadcastMessage1 . channel ;
useMes sageS tor e. getState ( ) . clearMessageByMessageId ( {
st ate . getMessageStore ( 123 ) ? . clearMessageByMessageId ( {
type : MessageType . Broadcast ,
channelId : channelId ,
messageId : messageIdToDelete ,
} ) ;
const stat e = useMessageStore . getState ( ) ;
const stor e = useMessageStore . getState ( ) . getMessageStore ( 123 ) ! ;
expect (
stat e . messages . broadcast . get ( channelId ) ? . get ( messageIdToDelete ) ,
stor e . messages . broadcast . get ( channelId ) ? . get ( messageIdToDelete ) ,
) . toBeUndefined ( ) ;
} ) ;
@ -440,37 +467,37 @@ describe("useMessageStore", () => {
) ;
const broadcastChanId = broadcastMessage1 . channel ;
useMes sageS tor e. getState ( ) . clearMessageByMessageId ( {
st ate . getMessageStore ( 123 ) ? . clearMessageByMessageId ( {
type : MessageType . Direct ,
nodeA : directMessageToOther1.from ,
nodeB : directMessageToOther1.to ,
messageId : directMessageToOther1.messageId ,
} ) ;
useMes sageS tor e. getState ( ) . clearMessageByMessageId ( {
st ate . getMessageStore ( 123 ) ? . clearMessageByMessageId ( {
type : MessageType . Direct ,
nodeA : directMessageFromOther1.from ,
nodeB : directMessageFromOther1.to ,
messageId : directMessageFromOther1.messageId ,
} ) ;
useMes sageS tor e. getState ( ) . clearMessageByMessageId ( {
st ate . getMessageStore ( 123 ) ? . clearMessageByMessageId ( {
type : MessageType . Direct ,
nodeA : directMessageToOther1.from ,
nodeB : directMessageToOther1.to ,
messageId : extraDirectMessageId ,
} ) ;
expect ( useMessageStore . getState ( ) . messages . direct . has ( directConvId ) ) . toBe (
false ,
) ;
expect (
state . getMessageStore ( 123 ) ? . messages . direct . has ( directConvId ) ,
) . toBe ( false ) ;
useMes sageS tor e. getState ( ) . clearMessageByMessageId ( {
st ate . getMessageStore ( 123 ) ? . clearMessageByMessageId ( {
type : MessageType . Broadcast ,
channelId : broadcastChanId ,
messageId : broadcastMessage1.messageId ,
} ) ;
expect (
useMes sageS tor e. getState ( ) . messages . broadcast . has ( broadcastChanId ) ,
st ate . getMessageStore ( 123 ) ? . messages . broadcast . has ( broadcastChanId ) ,
) . toBe ( false ) ;
} ) ;
@ -479,7 +506,7 @@ describe("useMessageStore", () => {
const conversationId = getConversationId ( myNodeNum , otherNodeNum1 ) ;
expect ( ( ) = > {
useMes sageS tor e. getState ( ) . clearMessageByMessageId ( {
st ate . getMessageStore ( 123 ) ? . clearMessageByMessageId ( {
type : MessageType . Direct ,
nodeA : myNodeNum ,
nodeB : otherNodeNum1 ,
@ -487,8 +514,8 @@ describe("useMessageStore", () => {
} ) ;
} ) . not . toThrow ( ) ;
const stat e = useMessageStore . getState ( ) ;
const conversationLog = stat e . messages . direct . get ( conversationId ) ;
const stor e = useMessageStore . getState ( ) . getMessageStore ( 123 ) ! ;
const conversationLog = stor e . messages . direct . get ( conversationId ) ;
expect ( conversationLog ? . size ) . toBe ( 3 ) ; // 101, 102, 1011
expect ( warnSpy ) . toHaveBeenCalledWith (
expect . stringContaining ( "not found in direct chat" ) ,
@ -500,7 +527,7 @@ describe("useMessageStore", () => {
it ( "should not error when trying to delete from non-existent conversation/channel" , ( ) = > {
const warnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) = > { } ) ;
expect ( ( ) = > {
useMes sageS tor e. getState ( ) . clearMessageByMessageId ( {
st ate . getMessageStore ( 123 ) ? . clearMessageByMessageId ( {
type : MessageType . Direct ,
nodeA : myNodeNum ,
nodeB : 9998 ,
@ -518,63 +545,241 @@ describe("useMessageStore", () => {
} ) ;
} ) ;
describe ( "Drafts" , ( ) = > {
describe ( "Drafts" , async ( ) = > {
const { useMessageStore } = await freshStore ( ) ;
const state = useMessageStore . getState ( ) ;
state . addMessageStore ( 123 ) ;
const draftKeyDirect = otherNodeNum1 ;
const draftKeyBroadcast = broadcastChannel ;
const draftMessage = "This is a draft" ;
it ( "should set and get a draft for direct chat" , ( ) = > {
useMes sageS tor e. getState ( ) . setDraft ( draftKeyDirect , draftMessage ) ;
expect ( useMes sageS tor e. getState ( ) . draft . get ( draftKeyDirect ) ) . toBe (
st ate . getMessageStore ( 123 ) ? . setDraft ( draftKeyDirect , draftMessage ) ;
expect ( st ate . getMessageStore ( 123 ) ? . drafts . get ( draftKeyDirect ) ) . toBe (
draftMessage ,
) ;
expect ( useMes sageS tor e. getState ( ) . getDraft ( draftKeyDirect ) ) . toBe (
expect ( st ate . getMessageStore ( 123 ) ? . getDraft ( draftKeyDirect ) ) . toBe (
draftMessage ,
) ;
} ) ;
it ( "should set and get a draft for broadcast chat" , ( ) = > {
useMes sageS tor e. getState ( ) . setDraft ( draftKeyBroadcast , draftMessage ) ;
expect ( useMes sageS tor e. getState ( ) . draft . get ( draftKeyBroadcast ) ) . toBe (
st ate . getMessageStore ( 123 ) ? . setDraft ( draftKeyBroadcast , draftMessage ) ;
expect ( st ate . getMessageStore ( 123 ) ? . drafts . get ( draftKeyBroadcast ) ) . toBe (
draftMessage ,
) ;
expect ( useMes sageS tor e. getState ( ) . getDraft ( draftKeyBroadcast ) ) . toBe (
expect ( st ate . getMessageStore ( 123 ) ? . getDraft ( draftKeyBroadcast ) ) . toBe (
draftMessage ,
) ;
} ) ;
it ( "should return empty string for non-existent draft" , ( ) = > {
expect ( useMes sageS tor e. getState ( ) . getDraft ( 999 ) ) . toBe ( "" ) ;
expect ( st ate . getMessageStore ( 123 ) ? . getDraft ( 999 ) ) . toBe ( "" ) ;
} ) ;
it ( "should clear a draft" , ( ) = > {
useMessageStore . getState ( ) . setDraft ( draftKeyDirect , draftMessage ) ;
expect ( useMessageStore . getState ( ) . draft . has ( draftKeyDirect ) ) . toBe ( true ) ;
useMessageStore . getState ( ) . clearDraft ( draftKeyDirect ) ;
expect ( useMessageStore . getState ( ) . draft . has ( draftKeyDirect ) ) . toBe ( false ) ;
expect ( useMessageStore . getState ( ) . getDraft ( draftKeyDirect ) ) . toBe ( "" ) ;
state . getMessageStore ( 123 ) ? . setDraft ( draftKeyDirect , draftMessage ) ;
expect ( state . getMessageStore ( 123 ) ? . drafts . has ( draftKeyDirect ) ) . toBe ( true ) ;
state . getMessageStore ( 123 ) ? . clearDraft ( draftKeyDirect ) ;
expect ( state . getMessageStore ( 123 ) ? . drafts . has ( draftKeyDirect ) ) . toBe (
false ,
) ;
expect ( state . getMessageStore ( 123 ) ? . getDraft ( draftKeyDirect ) ) . toBe ( "" ) ;
} ) ;
} ) ;
describe ( "deleteAllMessages" , ( ) = > {
describe ( "deleteAllMessages" , async ( ) = > {
const { useMessageStore } = await freshStore ( ) ;
const state = useMessageStore . getState ( ) ;
state . addMessageStore ( 123 ) ;
it ( "should clear all direct and broadcast messages, leaving empty Maps" , ( ) = > {
useMessageStore . getState ( ) . saveMessage ( directMessageToOther1 ) ;
useMessageStore . getState ( ) . saveMessage ( broadcastMessage1 ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( directMessageToOther1 ) ;
st ate . getMessageStore ( 123 ) ? . saveMessage ( broadcastMessage1 ) ;
expect ( useMes sageS tor e. getState ( ) . messages . direct . size ) . toBeGreaterThan (
expect ( st ate . getMessageStore ( 123 ) ? . messages . direct . size ) . toBeGreaterThan (
0 ,
) ;
expect (
useMes sageS tor e. getState ( ) . messages . broadcast . size ,
st ate . getMessageStore ( 123 ) ? . messages . broadcast . size ,
) . toBeGreaterThan ( 0 ) ;
useMessageStore . getState ( ) . deleteAllMessages ( ) ;
state . getMessageStore ( 123 ) ? . deleteAllMessages ( ) ;
const store = useMessageStore . getState ( ) . getMessageStore ( 123 ) ! ;
expect ( store . messages . direct ) . toBeInstanceOf ( Map ) ;
expect ( store . messages . direct . size ) . toBe ( 0 ) ;
expect ( store . messages . broadcast ) . toBeInstanceOf ( Map ) ;
expect ( store . messages . broadcast . size ) . toBe ( 0 ) ;
} ) ;
} ) ;
describe ( "persistence" , ( ) = > {
it ( "partialize persists data; onRehydrateStorage rebuilds methods (messages + drafts survive)" , async ( ) = > {
{
const { useMessageStore } = await freshStore ( true ) ;
const state = useMessageStore . getState ( ) ;
const store = state . addMessageStore ( 123 ) ;
store . setNodeNum ( 321 ) ;
const convId = getConversationId ( myNodeNum , otherNodeNum1 ) ;
store . saveMessage ( directMessageToOther1 ) ;
store . saveMessage ( broadcastMessage1 ) ;
store . setDraft (
otherNodeNum1 as unknown as Types . Destination ,
"draft-text" ,
) ;
const store2 = state . addMessageStore ( 123 ) ;
expect ( store2 . messages . direct . has ( convId ) ) . toBe ( true ) ;
expect ( store2 . messages . direct . get ( convId ) ? . has ( 101 ) ) . toBe ( true ) ;
expect ( store2 . messages . broadcast . get ( broadcastChannel ) ? . has ( 201 ) ) . toBe (
true ,
) ;
expect (
store2 . getDraft ( otherNodeNum1 as unknown as Types . Destination ) ,
) . toBe ( "draft-text" ) ;
}
{
const { useMessageStore } = await freshStore ( true ) ;
const state = useMessageStore . getState ( ) ;
const store = state . getMessageStore ( 123 ) ! ; // rebuilt instance
expect ( store ) . toBeTruthy ( ) ;
// Methods should work after rehydrate
const directMsgs = store . getMessages ( {
type : MessageType . Direct ,
nodeA : myNodeNum ,
nodeB : otherNodeNum1 ,
} ) ;
expect ( directMsgs . map ( ( m ) = > m . messageId ) ) . toEqual ( [ 101 ] ) ;
const bMsgs = store . getMessages ( {
type : MessageType . Broadcast ,
channelId : broadcastChannel ,
} ) ;
expect ( bMsgs . map ( ( m ) = > m . messageId ) ) . toEqual ( [ 201 ] ) ;
expect (
store . getDraft ( otherNodeNum1 as unknown as Types . Destination ) ,
) . toBe ( "draft-text" ) ;
store . saveMessage ( directMessageFromOther1 ) ;
const after = store . getMessages ( {
type : MessageType . Direct ,
nodeA : myNodeNum ,
nodeB : otherNodeNum1 ,
} ) ;
expect ( after . map ( ( m ) = > m . messageId ) ) . toEqual ( [ 101 , 102 ] ) ;
expect ( after [ 1 ] ? . state ) . toBe ( MessageState . Waiting ) ;
}
} ) ;
it ( "removeMessageStore persists removal across reload" , async ( ) = > {
{
const { useMessageStore } = await freshStore ( true ) ;
const state = useMessageStore . getState ( ) ;
const store = state . addMessageStore ( 99 ) ;
store . setNodeNum ( 42 ) ;
expect ( state . getMessageStore ( 99 ) ) . toBeDefined ( ) ;
state . removeMessageStore ( 99 ) ;
expect ( state . getMessageStore ( 99 ) ) . toBeUndefined ( ) ;
}
{
const { useMessageStore } = await freshStore ( true ) ;
const state = useMessageStore . getState ( ) ;
expect ( state . getMessageStore ( 99 ) ) . toBeUndefined ( ) ; // still gone
}
} ) ;
it ( "rehydrate only rebuilds stores with myNodeNum set (orphans dropped)" , async ( ) = > {
{
const { useMessageStore } = await freshStore ( true ) ;
const state = useMessageStore . getState ( ) ;
// Orphan (no myNodeNum)
const orphan = state . addMessageStore ( 500 ) ;
orphan . saveMessage ( broadcastMessage1 ) ;
// Proper store
const good = state . addMessageStore ( 501 ) ;
good . setNodeNum ( 777 ) ;
good . saveMessage ( broadcastMessage2 ) ;
}
{
const { useMessageStore } = await freshStore ( true ) ;
const state = useMessageStore . getState ( ) ;
expect ( state . getMessageStore ( 500 ) ) . toBeUndefined ( ) ; // orphan dropped
const kept = state . getMessageStore ( 501 ) ;
expect ( kept ) . toBeDefined ( ) ;
expect ( kept ? . messages . broadcast . get ( broadcastChannel ) ? . has ( 202 ) ) . toBe (
true ,
) ;
}
} ) ;
it ( "evicts the earliest-added message store when exceeding cap of 10" , async ( ) = > {
const { useMessageStore } = await freshStore ( ) ;
const state = useMessageStore . getState ( ) ;
expect ( state . messages . direct ) . toBeInstanceOf ( Map ) ;
expect ( state . messages . direct . size ) . toBe ( 0 ) ;
expect ( state . messages . broadcast ) . toBeInstanceOf ( Map ) ;
expect ( state . messages . broadcast . size ) . toBe ( 0 ) ;
for ( let i = 1 ; i <= 10 ; i ++ ) {
state . addMessageStore ( i ) ;
}
// Adding the 11th should evict the earliest (id=1)
state . addMessageStore ( 11 ) ;
expect ( state . getMessageStore ( 1 ) ) . toBeUndefined ( ) ; // evicted
expect ( state . getMessageStore ( 2 ) ) . toBeDefined ( ) ; // still there
expect ( state . getMessageStore ( 11 ) ) . toBeDefined ( ) ; // newest kept
} ) ;
it ( "keeps only the latest 1000 messages in a broadcast channel (oldest trimmed)" , async ( ) = > {
setAutoFreeze ( false ) ; // Disable immer auto-freeze for performance in this test
try {
const { useMessageStore , MessageType , MessageState } =
await freshStore ( ) ;
const state = useMessageStore . getState ( ) ;
const store = state . addMessageStore ( 123 ) ;
const channelId = 0 as number ;
const total = 1005 ;
for ( let i = 1 ; i <= total ; i ++ ) {
store . saveMessage ( {
type : MessageType . Broadcast ,
from : 123 ,
to : 0xffffffff ,
channel : channelId ,
date : Date.now ( ) + i ,
messageId : i ,
state : MessageState.Waiting ,
message : ` m ${ i } ` ,
} ) ;
}
const fresh = useMessageStore . getState ( ) . getMessageStore ( 123 ) ! ;
const log = fresh . messages . broadcast . get ( channelId ) ! ;
expect ( log . size ) . toBe ( 1000 ) ; // capped
for ( let i = 1 ; i <= 5 ; i ++ ) {
expect ( log . has ( i ) ) . toBe ( false ) ; // oldest removed
}
for ( let i = 6 ; i <= 1005 ; i ++ ) {
expect ( log . has ( i ) ) . toBe ( true ) ; // newest kept
}
} finally {
setAutoFreeze ( true ) ; // Restore immer auto-freeze
}
} ) ;
} ) ;
} ) ;