@@ -159,31 +159,16 @@ describe('trimMessagesToFitTokenLimit', () => {
159159 maxTotalTokens ,
160160 )
161161
162- // Verify the first message was dropped
163- expect ( result ) . toHaveLength ( testMessages . length - 1 )
164-
165- // Regular messages should be unchanged
166- expect ( result [ 0 ] . content ) . toBe ( testMessages [ 1 ] . content )
167- expect ( result [ 6 ] . content ) . toEqual ( testMessages [ 7 ] . content )
168-
169- // 0th and second terminal outputs should be simplified
170- expect ( result [ 1 ] . role ) . toEqual ( testMessages [ 2 ] . role )
171- expect ( Array . isArray ( result [ 1 ] . content ) ) . toBe ( true )
172- expect ( ( result [ 1 ] . content [ 0 ] as any ) . text ) . toContain (
173- '<result>[Output omitted]</result>' ,
174- )
175- expect ( ( result [ 1 ] . content [ 1 ] as any ) . text ) . toBe (
176- ( testMessages [ 2 ] . content [ 1 ] as any ) . text ,
177- )
178-
179- expect ( result [ 2 ] . role ) . toEqual ( testMessages [ 3 ] . role )
180- expect ( result [ 2 ] . content ) . toContain ( '<result>[Output omitted]</result>' )
162+ // Should have replacement message for omitted content
163+ expect ( result . length ) . toBeGreaterThan ( 0 )
181164
182- // Terminal outputs 3-7 should be preserved exactly
183- expect ( result [ 3 ] . content ) . toBe ( testMessages [ 4 ] . content )
184- expect ( result [ 4 ] . content ) . toEqual ( testMessages [ 5 ] . content )
185- expect ( result [ 5 ] . content ) . toBe ( testMessages [ 6 ] . content )
186- expect ( result [ 6 ] . content ) . toBe ( testMessages [ 7 ] . content )
165+ // Should contain a replacement message for omitted content
166+ const hasReplacementMessage = result . some (
167+ ( msg ) =>
168+ typeof msg . content === 'string' &&
169+ msg . content . includes ( 'Previous message(s) omitted due to length' ) ,
170+ )
171+ expect ( hasReplacementMessage ) . toBe ( true )
187172
188173 // Verify total tokens are under limit
189174 const finalTokens = tokenCounter . countTokensJson ( result )
@@ -199,31 +184,16 @@ describe('trimMessagesToFitTokenLimit', () => {
199184 maxTotalTokens ,
200185 )
201186
202- // Verify the first message was dropped
203- expect ( result ) . toHaveLength ( testMessages . length - 1 )
204-
205- // Regular messages should be unchanged
206- expect ( result [ 0 ] . content ) . toBe ( testMessages [ 1 ] . content )
207- expect ( result [ 6 ] . content ) . toEqual ( testMessages [ 7 ] . content )
187+ // Should have replacement message for omitted content
188+ expect ( result . length ) . toBeGreaterThan ( 0 )
208189
209- // 0th and second terminal outputs should be simplified
210- expect ( result [ 1 ] . role ) . toEqual ( testMessages [ 2 ] . role )
211- expect ( Array . isArray ( result [ 1 ] . content ) ) . toBe ( true )
212- expect ( ( result [ 1 ] . content [ 0 ] as any ) . text ) . toContain (
213- '<result>[Output omitted]</result>' ,
214- )
215- expect ( ( result [ 1 ] . content [ 1 ] as any ) . text ) . toBe (
216- ( testMessages [ 2 ] . content [ 1 ] as any ) . text ,
190+ // Should contain a replacement message for omitted content
191+ const hasReplacementMessage = result . some (
192+ ( msg ) =>
193+ typeof msg . content === 'string' &&
194+ msg . content . includes ( 'Previous message(s) omitted due to length' ) ,
217195 )
218-
219- expect ( result [ 2 ] . role ) . toEqual ( testMessages [ 3 ] . role )
220- expect ( result [ 2 ] . content ) . toContain ( '<result>[Output omitted]</result>' )
221-
222- // Terminal outputs 3-7 should be preserved exactly
223- expect ( result [ 3 ] . content ) . toBe ( testMessages [ 4 ] . content )
224- expect ( result [ 4 ] . content ) . toEqual ( testMessages [ 5 ] . content )
225- expect ( result [ 5 ] . content ) . toBe ( testMessages [ 6 ] . content )
226- expect ( result [ 6 ] . content ) . toBe ( testMessages [ 7 ] . content )
196+ expect ( hasReplacementMessage ) . toBe ( true )
227197
228198 // Verify total tokens are under limit
229199 const finalTokens = tokenCounter . countTokensJson ( result )
@@ -258,4 +228,142 @@ describe('trimMessagesToFitTokenLimit', () => {
258228
259229 expect ( result ) . toEqual ( [ ] )
260230 } )
231+
232+ describe ( 'keepDuringTruncation functionality' , ( ) => {
233+ it ( 'preserves messages marked with keepDuringTruncation=true' , ( ) => {
234+ const messages = [
235+ { role : 'user' , content : 'A' . repeat ( 500 ) } , // Large message to force truncation
236+ { role : 'user' , content : 'B' . repeat ( 500 ) } , // Large message to force truncation
237+ {
238+ role : 'user' ,
239+ content : 'Message 3 - keep me!' ,
240+ keepDuringTruncation : true ,
241+ } ,
242+ { role : 'assistant' , content : 'C' . repeat ( 500 ) } , // Large message to force truncation
243+ {
244+ role : 'user' ,
245+ content : 'Message 5 - keep me too!' ,
246+ keepDuringTruncation : true ,
247+ } ,
248+ ] as CodebuffMessage [ ]
249+
250+ const result = trimMessagesToFitTokenLimit ( messages , 0 , 1000 )
251+
252+ // Should contain the kept messages
253+ const keptMessages = result . filter (
254+ ( msg ) =>
255+ typeof msg . content === 'string' &&
256+ ( msg . content . includes ( 'keep me!' ) ||
257+ msg . content . includes ( 'keep me too!' ) ) ,
258+ )
259+ expect ( keptMessages ) . toHaveLength ( 2 )
260+
261+ // Should have replacement message for omitted content
262+ const hasReplacementMessage = result . some (
263+ ( msg ) =>
264+ typeof msg . content === 'string' &&
265+ msg . content . includes ( 'Previous message(s) omitted due to length' ) ,
266+ )
267+ expect ( hasReplacementMessage ) . toBe ( true )
268+ } )
269+
270+ it ( 'does not add replacement message when no messages are removed' , ( ) => {
271+ const messages = [
272+ { role : 'user' , content : 'Short message 1' } ,
273+ {
274+ role : 'user' ,
275+ content : 'Short message 2' ,
276+ keepDuringTruncation : true ,
277+ } ,
278+ ] as CodebuffMessage [ ]
279+
280+ const result = trimMessagesToFitTokenLimit ( messages , 0 , 10000 )
281+
282+ // Should be unchanged when under token limit
283+ expect ( result ) . toHaveLength ( 2 )
284+ expect ( result [ 0 ] . content ) . toBe ( 'Short message 1' )
285+ expect ( result [ 1 ] . content ) . toBe ( 'Short message 2' )
286+ } )
287+
288+ it ( 'handles consecutive replacement messages correctly' , ( ) => {
289+ const messages = [
290+ { role : 'user' , content : 'A' . repeat ( 1000 ) } , // Large message to be removed
291+ { role : 'user' , content : 'B' . repeat ( 1000 ) } , // Large message to be removed
292+ { role : 'user' , content : 'C' . repeat ( 1000 ) } , // Large message to be removed
293+ { role : 'user' , content : 'Keep this' , keepDuringTruncation : true } ,
294+ ] as CodebuffMessage [ ]
295+
296+ const result = trimMessagesToFitTokenLimit ( messages , 0 , 1000 )
297+
298+ // Should only have one replacement message for consecutive removals
299+ const replacementMessages = result . filter (
300+ ( msg ) =>
301+ typeof msg . content === 'string' &&
302+ msg . content . includes ( 'Previous message(s) omitted due to length' ) ,
303+ )
304+ expect ( replacementMessages ) . toHaveLength ( 1 )
305+
306+ // Should keep the marked message
307+ const keptMessage = result . find (
308+ ( msg ) =>
309+ typeof msg . content === 'string' && msg . content . includes ( 'Keep this' ) ,
310+ )
311+ expect ( keptMessage ) . toBeDefined ( )
312+ } )
313+
314+ it ( 'calculates token removal correctly with keepDuringTruncation' , ( ) => {
315+ const messages = [
316+ { role : 'user' , content : 'A' . repeat ( 500 ) } , // Will be removed
317+ { role : 'user' , content : 'B' . repeat ( 500 ) } , // Will be removed
318+ {
319+ role : 'user' ,
320+ content : 'Keep this short message' ,
321+ keepDuringTruncation : true ,
322+ } ,
323+ { role : 'user' , content : 'C' . repeat ( 100 ) } , // Might be kept
324+ ] as CodebuffMessage [ ]
325+
326+ const result = trimMessagesToFitTokenLimit ( messages , 0 , 2000 )
327+
328+ // Should preserve the keepDuringTruncation message
329+ const keptMessage = result . find (
330+ ( msg ) =>
331+ typeof msg . content === 'string' &&
332+ msg . content . includes ( 'Keep this short message' ) ,
333+ )
334+ expect ( keptMessage ) . toBeDefined ( )
335+
336+ // Total tokens should be under limit
337+ const finalTokens = tokenCounter . countTokensJson ( result )
338+ expect ( finalTokens ) . toBeLessThan ( 2000 )
339+ } )
340+
341+ it ( 'handles mixed keepDuringTruncation and regular messages' , ( ) => {
342+ const messages = [
343+ { role : 'user' , content : 'A' . repeat ( 800 ) } , // Large message to force truncation
344+ { role : 'user' , content : 'Keep 1' , keepDuringTruncation : true } ,
345+ { role : 'user' , content : 'B' . repeat ( 800 ) } , // Large message to force truncation
346+ { role : 'user' , content : 'Keep 2' , keepDuringTruncation : true } ,
347+ { role : 'user' , content : 'C' . repeat ( 800 ) } , // Large message to force truncation
348+ ] as CodebuffMessage [ ]
349+
350+ const result = trimMessagesToFitTokenLimit ( messages , 0 , 500 )
351+
352+ // Should keep both marked messages
353+ const keptMessages = result . filter (
354+ ( msg ) =>
355+ typeof msg . content === 'string' &&
356+ ( msg . content . includes ( 'Keep 1' ) || msg . content . includes ( 'Keep 2' ) ) ,
357+ )
358+ expect ( keptMessages ) . toHaveLength ( 2 )
359+
360+ // Should have replacement messages for removed content
361+ const replacementMessages = result . filter (
362+ ( msg ) =>
363+ typeof msg . content === 'string' &&
364+ msg . content . includes ( 'Previous message(s) omitted due to length' ) ,
365+ )
366+ expect ( replacementMessages . length ) . toBeGreaterThan ( 0 )
367+ } )
368+ } )
261369} )
0 commit comments