@@ -48,9 +48,9 @@ describe('chatHub', () => {
4848
4949 describe ( 'getConversations' , ( ) => {
5050 it ( 'should list empty conversations' , async ( ) => {
51- const conversations = await chatHubService . getConversations ( member . id ) ;
51+ const conversations = await chatHubService . getConversations ( member . id , 20 ) ;
5252 expect ( conversations ) . toBeDefined ( ) ;
53- expect ( conversations ) . toHaveLength ( 0 ) ;
53+ expect ( conversations . data ) . toHaveLength ( 0 ) ;
5454 } ) ;
5555
5656 it ( "should list user's own conversations in expected order" , async ( ) => {
@@ -83,11 +83,182 @@ describe('chatHub', () => {
8383 tools : [ ] ,
8484 } ) ;
8585
86- const conversations = await chatHubService . getConversations ( member . id ) ;
87- expect ( conversations ) . toHaveLength ( 3 ) ;
88- expect ( conversations [ 0 ] . id ) . toBe ( session1 . id ) ;
89- expect ( conversations [ 1 ] . id ) . toBe ( session2 . id ) ;
90- expect ( conversations [ 2 ] . id ) . toBe ( session3 . id ) ;
86+ const conversations = await chatHubService . getConversations ( member . id , 20 ) ;
87+ expect ( conversations . data ) . toHaveLength ( 3 ) ;
88+ expect ( conversations . data [ 0 ] . id ) . toBe ( session1 . id ) ;
89+ expect ( conversations . data [ 1 ] . id ) . toBe ( session2 . id ) ;
90+ expect ( conversations . data [ 2 ] . id ) . toBe ( session3 . id ) ;
91+ } ) ;
92+
93+ describe ( 'pagination' , ( ) => {
94+ it ( 'should return hasMore=false and nextCursor=null when all sessions fit in one page' , async ( ) => {
95+ await sessionsRepository . createChatSession ( {
96+ id : crypto . randomUUID ( ) ,
97+ ownerId : member . id ,
98+ title : 'session 1' ,
99+ lastMessageAt : new Date ( '2025-01-01T00:00:00Z' ) ,
100+ tools : [ ] ,
101+ } ) ;
102+
103+ const conversations = await chatHubService . getConversations ( member . id , 10 ) ;
104+
105+ expect ( conversations . data ) . toHaveLength ( 1 ) ;
106+ expect ( conversations . hasMore ) . toBe ( false ) ;
107+ expect ( conversations . nextCursor ) . toBeNull ( ) ;
108+ } ) ;
109+
110+ it ( 'should fetch next page using cursor' , async ( ) => {
111+ const session1 = await sessionsRepository . createChatSession ( {
112+ id : crypto . randomUUID ( ) ,
113+ ownerId : member . id ,
114+ title : 'session 1' ,
115+ lastMessageAt : new Date ( '2025-01-05T00:00:00Z' ) ,
116+ tools : [ ] ,
117+ } ) ;
118+
119+ const session2 = await sessionsRepository . createChatSession ( {
120+ id : crypto . randomUUID ( ) ,
121+ ownerId : member . id ,
122+ title : 'session 2' ,
123+ lastMessageAt : new Date ( '2025-01-04T00:00:00Z' ) ,
124+ tools : [ ] ,
125+ } ) ;
126+
127+ const session3 = await sessionsRepository . createChatSession ( {
128+ id : crypto . randomUUID ( ) ,
129+ ownerId : member . id ,
130+ title : 'session 3' ,
131+ lastMessageAt : new Date ( '2025-01-03T00:00:00Z' ) ,
132+ tools : [ ] ,
133+ } ) ;
134+
135+ const session4 = await sessionsRepository . createChatSession ( {
136+ id : crypto . randomUUID ( ) ,
137+ ownerId : member . id ,
138+ title : 'session 4' ,
139+ lastMessageAt : new Date ( '2025-01-02T00:00:00Z' ) ,
140+ tools : [ ] ,
141+ } ) ;
142+
143+ // First page
144+ const page1 = await chatHubService . getConversations ( member . id , 2 ) ;
145+ expect ( page1 . data ) . toHaveLength ( 2 ) ;
146+ expect ( page1 . data [ 0 ] . id ) . toBe ( session1 . id ) ;
147+ expect ( page1 . data [ 1 ] . id ) . toBe ( session2 . id ) ;
148+ expect ( page1 . hasMore ) . toBe ( true ) ;
149+ expect ( page1 . nextCursor ) . toBe ( session2 . id ) ;
150+
151+ // Second page using cursor
152+ const page2 = await chatHubService . getConversations ( member . id , 2 , page1 . nextCursor ! ) ;
153+ expect ( page2 . data ) . toHaveLength ( 2 ) ;
154+ expect ( page2 . data [ 0 ] . id ) . toBe ( session3 . id ) ;
155+ expect ( page2 . data [ 1 ] . id ) . toBe ( session4 . id ) ;
156+ expect ( page2 . hasMore ) . toBe ( false ) ;
157+ expect ( page2 . nextCursor ) . toBeNull ( ) ;
158+ } ) ;
159+
160+ it ( 'should handle sessions with same lastMessageAt using id for ordering' , async ( ) => {
161+ const sameDate = new Date ( '2025-01-01T00:00:00Z' ) ;
162+
163+ const session1 = await sessionsRepository . createChatSession ( {
164+ id : '00000000-0000-0000-0000-000000000001' ,
165+ ownerId : member . id ,
166+ title : 'Session 1' ,
167+ lastMessageAt : sameDate ,
168+ tools : [ ] ,
169+ } ) ;
170+
171+ const session2 = await sessionsRepository . createChatSession ( {
172+ id : '00000000-0000-0000-0000-000000000002' ,
173+ ownerId : member . id ,
174+ title : 'Session 2' ,
175+ lastMessageAt : sameDate ,
176+ tools : [ ] ,
177+ } ) ;
178+
179+ const session3 = await sessionsRepository . createChatSession ( {
180+ id : '00000000-0000-0000-0000-000000000003' ,
181+ ownerId : member . id ,
182+ title : 'Session 3' ,
183+ lastMessageAt : sameDate ,
184+ tools : [ ] ,
185+ } ) ;
186+
187+ // Fetch first page
188+ const page1 = await chatHubService . getConversations ( member . id , 2 ) ;
189+ expect ( page1 . data ) . toHaveLength ( 2 ) ;
190+ expect ( page1 . data [ 0 ] . id ) . toBe ( session1 . id ) ;
191+ expect ( page1 . data [ 1 ] . id ) . toBe ( session2 . id ) ;
192+ expect ( page1 . hasMore ) . toBe ( true ) ;
193+
194+ // Fetch second page
195+ const page2 = await chatHubService . getConversations ( member . id , 2 , page1 . nextCursor ! ) ;
196+ expect ( page2 . data ) . toHaveLength ( 1 ) ;
197+ expect ( page2 . data [ 0 ] . id ) . toBe ( session3 . id ) ;
198+ expect ( page2 . hasMore ) . toBe ( false ) ;
199+ } ) ;
200+
201+ it ( 'should throw error when cursor session does not exist' , async ( ) => {
202+ await sessionsRepository . createChatSession ( {
203+ id : crypto . randomUUID ( ) ,
204+ ownerId : member . id ,
205+ title : 'session 1' ,
206+ lastMessageAt : new Date ( '2025-01-01T00:00:00Z' ) ,
207+ tools : [ ] ,
208+ } ) ;
209+
210+ const nonExistentCursor = '00000000-0000-0000-0000-000000000000' ;
211+
212+ await expect (
213+ chatHubService . getConversations ( member . id , 10 , nonExistentCursor ) ,
214+ ) . rejects . toThrow ( 'Cursor session not found' ) ;
215+ } ) ;
216+
217+ it ( 'should throw error when cursor session belongs to different user' , async ( ) => {
218+ await sessionsRepository . createChatSession ( {
219+ id : crypto . randomUUID ( ) ,
220+ ownerId : member . id ,
221+ title : 'Member Session' ,
222+ lastMessageAt : new Date ( '2025-01-02T00:00:00Z' ) ,
223+ tools : [ ] ,
224+ } ) ;
225+
226+ const adminSession = await sessionsRepository . createChatSession ( {
227+ id : crypto . randomUUID ( ) ,
228+ ownerId : admin . id ,
229+ title : 'Admin Session' ,
230+ lastMessageAt : new Date ( '2025-01-01T00:00:00Z' ) ,
231+ tools : [ ] ,
232+ } ) ;
233+
234+ await expect (
235+ chatHubService . getConversations ( member . id , 10 , adminSession . id ) ,
236+ ) . rejects . toThrow ( 'Cursor session not found' ) ;
237+ } ) ;
238+
239+ it ( 'should handle sessions with null lastMessageAt' , async ( ) => {
240+ const session1 = await sessionsRepository . createChatSession ( {
241+ id : crypto . randomUUID ( ) ,
242+ ownerId : member . id ,
243+ title : 'Session with date' ,
244+ lastMessageAt : new Date ( '2025-01-01T00:00:00Z' ) ,
245+ tools : [ ] ,
246+ } ) ;
247+
248+ const session2 = await sessionsRepository . createChatSession ( {
249+ id : crypto . randomUUID ( ) ,
250+ ownerId : member . id ,
251+ title : 'Session without date' ,
252+ lastMessageAt : null ,
253+ tools : [ ] ,
254+ } ) ;
255+
256+ const conversations = await chatHubService . getConversations ( member . id , 10 ) ;
257+
258+ expect ( conversations . data ) . toHaveLength ( 2 ) ;
259+ expect ( conversations . data [ 0 ] . id ) . toBe ( session1 . id ) ;
260+ expect ( conversations . data [ 1 ] . id ) . toBe ( session2 . id ) ;
261+ } ) ;
91262 } ) ;
92263 } ) ;
93264
0 commit comments