11package runtime
22
33import (
4+ "strings"
45 "testing"
56
67 "github.com/stretchr/testify/assert"
8+ "github.com/stretchr/testify/require"
79
810 "github.com/docker/docker-agent/pkg/agent"
911 "github.com/docker/docker-agent/pkg/chat"
@@ -96,7 +98,7 @@ func TestExtractMessagesToCompact(t *testing.T) {
9698 sess := session .New (session .WithMessages (tt .messages ))
9799
98100 a := agent .New ("test" , "test prompt" )
99- result := extractMessagesToCompact (sess , a , tt .contextLimit , tt .additionalPrompt )
101+ result , _ := extractMessagesToCompact (sess , a , tt .contextLimit , tt .additionalPrompt )
100102
101103 assert .GreaterOrEqual (t , len (result ), tt .wantConversationMsgCount + 2 )
102104 assert .Equal (t , chat .MessageRoleSystem , result [0 ].Role )
@@ -121,3 +123,169 @@ func TestExtractMessagesToCompact(t *testing.T) {
121123 })
122124 }
123125}
126+
127+ func TestSplitIndexForKeep (t * testing.T ) {
128+ msg := func (role chat.MessageRole , content string ) chat.Message {
129+ return chat.Message {Role : role , Content : content }
130+ }
131+
132+ tests := []struct {
133+ name string
134+ messages []chat.Message
135+ maxTokens int64
136+ wantSplit int // expected split index
137+ }{
138+ {
139+ name : "empty messages" ,
140+ messages : nil ,
141+ maxTokens : 1000 ,
142+ wantSplit : 0 ,
143+ },
144+ {
145+ name : "all messages fit in keep budget - compact everything" ,
146+ messages : []chat.Message {
147+ msg (chat .MessageRoleUser , "short" ),
148+ msg (chat .MessageRoleAssistant , "short" ),
149+ },
150+ maxTokens : 100_000 ,
151+ wantSplit : 2 , // all fit → compact everything
152+ },
153+ {
154+ name : "recent messages kept, older ones compacted" ,
155+ messages : []chat.Message {
156+ msg (chat .MessageRoleUser , strings .Repeat ("a" , 40000 )), // ~10005 tokens
157+ msg (chat .MessageRoleAssistant , strings .Repeat ("b" , 40000 )), // ~10005 tokens
158+ msg (chat .MessageRoleUser , strings .Repeat ("c" , 40000 )), // ~10005 tokens
159+ msg (chat .MessageRoleAssistant , strings .Repeat ("d" , 40000 )), // ~10005 tokens
160+ msg (chat .MessageRoleUser , strings .Repeat ("e" , 40000 )), // ~10005 tokens
161+ msg (chat .MessageRoleAssistant , strings .Repeat ("f" , 40000 )), // ~10005 tokens
162+ },
163+ maxTokens : 20_100 , // enough for exactly 2 messages
164+ wantSplit : 4 , // last 2 messages are kept
165+ },
166+ }
167+
168+ for _ , tt := range tests {
169+ t .Run (tt .name , func (t * testing.T ) {
170+ got := splitIndexForKeep (tt .messages , tt .maxTokens )
171+ assert .Equal (t , tt .wantSplit , got )
172+ })
173+ }
174+ }
175+
176+ func TestExtractMessagesToCompact_KeepsRecentMessages (t * testing.T ) {
177+ // Create a session with many messages, some large enough that the last
178+ // ~20k tokens are kept aside.
179+ var items []session.Item
180+ for range 10 {
181+ items = append (items , session .NewMessageItem (& session.Message {
182+ Message : chat.Message {
183+ Role : chat .MessageRoleUser ,
184+ Content : strings .Repeat ("x" , 20000 ), // ~5k tokens each
185+ },
186+ }), session .NewMessageItem (& session.Message {
187+ Message : chat.Message {
188+ Role : chat .MessageRoleAssistant ,
189+ Content : strings .Repeat ("y" , 20000 ), // ~5k tokens each
190+ },
191+ }))
192+ }
193+
194+ sess := session .New (session .WithMessages (items ))
195+ a := agent .New ("test" , "test prompt" )
196+
197+ result , firstKeptEntry := extractMessagesToCompact (sess , a , 200_000 , "" )
198+
199+ // The kept messages should not appear in the compaction result
200+ // (only system + compacted messages + user prompt).
201+ // Total: 20 messages × ~5k tokens = ~100k tokens.
202+ // Keep budget: 20k tokens → ~4 messages kept.
203+ // So compacted messages should be 20 - 4 = 16.
204+ compactedMsgCount := len (result ) - 2 // minus system and user prompt
205+ assert .Less (t , compactedMsgCount , 20 , "some messages should have been kept aside" )
206+ assert .Positive (t , compactedMsgCount , "some messages should be compacted" )
207+
208+ // firstKeptEntry should point into sess.Messages
209+ assert .Positive (t , firstKeptEntry , "firstKeptEntry should be > 0" )
210+ assert .Less (t , firstKeptEntry , len (sess .Messages ), "firstKeptEntry should be within bounds" )
211+ }
212+
213+ func TestSessionGetMessages_WithFirstKeptEntry (t * testing.T ) {
214+ // Build a session with some messages, then add a summary with FirstKeptEntry.
215+ items := []session.Item {
216+ session .NewMessageItem (& session.Message {
217+ Message : chat.Message {Role : chat .MessageRoleUser , Content : "m1" },
218+ }),
219+ session .NewMessageItem (& session.Message {
220+ Message : chat.Message {Role : chat .MessageRoleAssistant , Content : "m2" },
221+ }),
222+ session .NewMessageItem (& session.Message {
223+ Message : chat.Message {Role : chat .MessageRoleUser , Content : "m3" },
224+ }),
225+ session .NewMessageItem (& session.Message {
226+ Message : chat.Message {Role : chat .MessageRoleAssistant , Content : "m4" },
227+ }),
228+ session .NewMessageItem (& session.Message {
229+ Message : chat.Message {Role : chat .MessageRoleUser , Content : "m5" },
230+ }),
231+ }
232+
233+ // Add summary that says "first kept entry is index 3" (m4).
234+ // So we expect: [system...] + [summary] + [m4, m5]
235+ items = append (items , session.Item {
236+ Summary : "This is a summary of m1-m3" ,
237+ FirstKeptEntry : 3 , // index of m4 in the Messages slice
238+ })
239+
240+ sess := session .New (session .WithMessages (items ))
241+ a := agent .New ("test" , "test instruction" )
242+
243+ messages := sess .GetMessages (a )
244+
245+ // Extract just the non-system messages
246+ var conversationMessages []chat.Message
247+ for _ , msg := range messages {
248+ if msg .Role != chat .MessageRoleSystem {
249+ conversationMessages = append (conversationMessages , msg )
250+ }
251+ }
252+
253+ // Should have: summary (as user message), m4, m5
254+ require .Len (t , conversationMessages , 3 , "expected summary + 2 kept messages" )
255+ assert .Contains (t , conversationMessages [0 ].Content , "Session Summary:" )
256+ assert .Equal (t , "m4" , conversationMessages [1 ].Content )
257+ assert .Equal (t , "m5" , conversationMessages [2 ].Content )
258+ }
259+
260+ func TestSessionGetMessages_SummaryWithoutFirstKeptEntry (t * testing.T ) {
261+ // Backward compatibility: summary without FirstKeptEntry should work as before.
262+ items := []session.Item {
263+ session .NewMessageItem (& session.Message {
264+ Message : chat.Message {Role : chat .MessageRoleUser , Content : "m1" },
265+ }),
266+ session .NewMessageItem (& session.Message {
267+ Message : chat.Message {Role : chat .MessageRoleAssistant , Content : "m2" },
268+ }),
269+ {Summary : "This is a summary" },
270+ session .NewMessageItem (& session.Message {
271+ Message : chat.Message {Role : chat .MessageRoleUser , Content : "m3" },
272+ }),
273+ }
274+
275+ sess := session .New (session .WithMessages (items ))
276+ a := agent .New ("test" , "test instruction" )
277+
278+ messages := sess .GetMessages (a )
279+
280+ var conversationMessages []chat.Message
281+ for _ , msg := range messages {
282+ if msg .Role != chat .MessageRoleSystem {
283+ conversationMessages = append (conversationMessages , msg )
284+ }
285+ }
286+
287+ // Should have: summary + m3 (messages after the summary)
288+ require .Len (t , conversationMessages , 2 )
289+ assert .Contains (t , conversationMessages [0 ].Content , "Session Summary:" )
290+ assert .Equal (t , "m3" , conversationMessages [1 ].Content )
291+ }
0 commit comments