@@ -57,6 +57,205 @@ function createThreadStore(initialMessages: ChatMessage[]) {
5757}
5858
5959describe ( "withThreads" , ( ) => {
60+ it ( "preserves explicit topology for messages without app metadata" , ( ) => {
61+ const root = {
62+ id : "root" ,
63+ parts : [ { type : "text" , text : "Root" } ] ,
64+ role : "user" ,
65+ } as ChatMessage ;
66+ const assistant = {
67+ id : "assistant" ,
68+ parts : [ { type : "text" , text : "Response" } ] ,
69+ role : "assistant" ,
70+ } as ChatMessage ;
71+ const store = createThreadStore ( [ ] ) ;
72+
73+ store . getState ( ) . setTreeSnapshot ( {
74+ cursorId : assistant . id ,
75+ nodes : [
76+ { message : root , parentId : null } ,
77+ { message : assistant , parentId : root . id } ,
78+ ] ,
79+ version : 1 ,
80+ } ) ;
81+
82+ assert . deepEqual (
83+ store . getState ( ) . messages . map ( ( message ) => message . id ) ,
84+ [ root . id , assistant . id ]
85+ ) ;
86+ assert . deepEqual (
87+ store
88+ . getState ( )
89+ . treeSnapshot . nodes . map ( ( { message, parentId } ) => [
90+ message . id ,
91+ parentId ,
92+ ] ) ,
93+ [
94+ [ root . id , null ] ,
95+ [ assistant . id , root . id ] ,
96+ ]
97+ ) ;
98+ } ) ;
99+
100+ it ( "treats a null cursor as an empty selected path" , ( ) => {
101+ const root = createMessage ( {
102+ id : "root" ,
103+ role : "user" ,
104+ createdAt : "2024-01-01T00:00:00.000Z" ,
105+ } ) ;
106+ const store = createThreadStore ( [ root ] ) ;
107+
108+ store . getState ( ) . setTreeSnapshot ( {
109+ cursorId : null ,
110+ nodes : [ { message : root , parentId : null } ] ,
111+ version : 1 ,
112+ } ) ;
113+
114+ assert . deepEqual ( store . getState ( ) . messages , [ ] ) ;
115+ assert . deepEqual (
116+ store . getState ( ) . allMessages . map ( ( message ) => message . id ) ,
117+ [ root . id ]
118+ ) ;
119+ } ) ;
120+
121+ it ( "preserves paths deeper than 100 messages" , ( ) => {
122+ const messages = Array . from ( { length : 125 } , ( _ , index ) =>
123+ createMessage ( {
124+ id : `message-${ index } ` ,
125+ role : index % 2 === 0 ? "user" : "assistant" ,
126+ createdAt : new Date ( index * 1000 ) . toISOString ( ) ,
127+ parentMessageId : index === 0 ? null : `message-${ index - 1 } ` ,
128+ } )
129+ ) ;
130+ const store = createThreadStore ( [ ] ) ;
131+
132+ store . getState ( ) . setTreeSnapshot ( {
133+ cursorId : messages . at ( - 1 ) ?. id ?? null ,
134+ nodes : messages . map ( ( message , index ) => ( {
135+ message,
136+ parentId : index === 0 ? null : ( messages [ index - 1 ] ?. id ?? null ) ,
137+ } ) ) ,
138+ version : 1 ,
139+ } ) ;
140+
141+ assert . equal ( store . getState ( ) . messages . length , messages . length ) ;
142+ assert . equal ( store . getState ( ) . messages . at ( 0 ) ?. id , messages . at ( 0 ) ?. id ) ;
143+ } ) ;
144+
145+ it ( "keeps known root parents when metadata points outside the tree" , ( ) => {
146+ const rootA = createMessage ( {
147+ id : "root-a" ,
148+ role : "user" ,
149+ createdAt : "2024-01-01T00:00:00.000Z" ,
150+ parentMessageId : "missing-parent" ,
151+ } ) ;
152+ const rootB = createMessage ( {
153+ id : "root-b" ,
154+ role : "user" ,
155+ createdAt : "2024-01-01T00:00:01.000Z" ,
156+ parentMessageId : "missing-parent" ,
157+ } ) ;
158+ const store = createThreadStore ( [ ] ) ;
159+
160+ store . getState ( ) . setTreeSnapshot ( {
161+ cursorId : rootB . id ,
162+ nodes : [
163+ { message : rootA , parentId : null } ,
164+ { message : rootB , parentId : null } ,
165+ ] ,
166+ version : 1 ,
167+ } ) ;
168+
169+ assert . deepEqual (
170+ store
171+ . getState ( )
172+ . getMessageSiblingInfo ( rootB . id )
173+ ?. siblings . map ( ( message ) => message . id ) ,
174+ [ rootA . id , rootB . id ]
175+ ) ;
176+ } ) ;
177+
178+ it ( "hydrates an empty visible path from the server tree" , ( ) => {
179+ const root = createMessage ( {
180+ id : "root" ,
181+ role : "user" ,
182+ createdAt : "2024-01-01T00:00:00.000Z" ,
183+ } ) ;
184+ const assistant = createMessage ( {
185+ id : "assistant" ,
186+ role : "assistant" ,
187+ createdAt : "2024-01-01T00:00:01.000Z" ,
188+ parentMessageId : root . id ,
189+ } ) ;
190+ const store = createThreadStore ( [ ] ) ;
191+
192+ store . getState ( ) . setAllMessages ( [ root , assistant ] ) ;
193+
194+ assert . deepEqual (
195+ store . getState ( ) . messages . map ( ( message ) => message . id ) ,
196+ [ root . id , assistant . id ]
197+ ) ;
198+ assert . equal ( store . getState ( ) . treeSnapshot . cursorId , assistant . id ) ;
199+ } ) ;
200+
201+ it ( "inherits deterministic fallback metadata from the parent" , ( ) => {
202+ const root = createMessage ( {
203+ id : "root" ,
204+ role : "user" ,
205+ createdAt : "2024-01-01T00:00:00.000Z" ,
206+ } ) ;
207+ const assistant = {
208+ id : "assistant" ,
209+ parts : [ ] ,
210+ role : "assistant" ,
211+ } as unknown as ChatMessage ;
212+ const store = createThreadStore ( [ root ] ) ;
213+
214+ store . getState ( ) . setTreeSnapshot ( {
215+ cursorId : assistant . id ,
216+ nodes : [
217+ { message : root , parentId : null } ,
218+ { message : assistant , parentId : root . id } ,
219+ ] ,
220+ version : 1 ,
221+ } ) ;
222+
223+ assert . deepEqual (
224+ store . getState ( ) . messages . at ( - 1 ) ?. metadata . createdAt ,
225+ root . metadata . createdAt
226+ ) ;
227+ } ) ;
228+
229+ it ( "keeps initial messages stable across server synchronization" , ( ) => {
230+ const root = createMessage ( {
231+ id : "root" ,
232+ role : "user" ,
233+ createdAt : "2024-01-01T00:00:00.000Z" ,
234+ } ) ;
235+ const placeholder = createMessage ( {
236+ id : "assistant" ,
237+ role : "assistant" ,
238+ createdAt : "2024-01-01T00:00:01.000Z" ,
239+ parentMessageId : root . id ,
240+ } ) ;
241+ const completed = createMessage ( {
242+ id : placeholder . id ,
243+ role : "assistant" ,
244+ createdAt : "2024-01-01T00:00:01.000Z" ,
245+ parentMessageId : root . id ,
246+ text : "Completed" ,
247+ } ) ;
248+ const store = createThreadStore ( [ root , placeholder ] ) ;
249+
250+ store . getState ( ) . setAllMessages ( [ root , completed ] ) ;
251+
252+ assert . deepEqual ( store . getState ( ) . threadInitialMessages , [
253+ root ,
254+ placeholder ,
255+ ] ) ;
256+ assert . equal ( store . getState ( ) . messages . at ( - 1 ) ?. parts . at ( 0 ) ?. type , "text" ) ;
257+ } ) ;
258+
60259 it ( "describes a parallel group before any assistant message exists" , ( ) => {
61260 const user = createMessage ( {
62261 id : "user-root" ,
0 commit comments