127 lines
4.2 KiB
Go
127 lines
4.2 KiB
Go
package agent
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"ollie/cmd/olliesrv/internal/backend"
|
|
)
|
|
|
|
func TestBuildCompactedHistory_OrphanedToolWalkback(t *testing.T) {
|
|
// Scenario: hotStart would land on a tool message. The walk-back logic
|
|
// should include the preceding assistant message with tool_calls to avoid
|
|
// violating the API constraint that tool messages must follow the assistant
|
|
// message containing the corresponding tool_calls.
|
|
|
|
// Build a history where the naive hotStart (total - hotTailSize) lands on a tool message.
|
|
// hotTailSize = 6, so we need enough messages that slicing at -6 hits a tool message.
|
|
|
|
// We want total - hotTailSize to land on a tool message.
|
|
// hotTailSize = 6, so with 12 messages, hotStart = 6.
|
|
// If messages[6] is a tool message, the walk-back should kick in.
|
|
|
|
messages := []backend.Message{
|
|
{Role: "user", Content: "old message 1"},
|
|
{Role: "assistant", Content: "old response 1"},
|
|
{Role: "user", Content: "old message 2"},
|
|
{Role: "assistant", Content: "old response 2"},
|
|
{Role: "user", Content: "request tool use"},
|
|
{Role: "assistant", Content: "", ToolCalls: []backend.ToolCall{{ID: "tc1", Name: "shell"}}}, // index 5
|
|
{Role: "tool", ToolCallID: "tc1", Content: "command output"}, // index 6 - naive hotStart lands here!
|
|
{Role: "user", Content: "next question"},
|
|
{Role: "assistant", Content: "answer"},
|
|
{Role: "user", Content: "another question"},
|
|
{Role: "assistant", Content: "another answer"},
|
|
{Role: "user", Content: "final question"},
|
|
}
|
|
|
|
// Total = 12, hotTailSize = 6, so naive hotStart = 6
|
|
// messages[6] is a tool message → should walk back to 5.
|
|
|
|
result := buildCompactedHistory("test summary", messages)
|
|
|
|
// Result structure:
|
|
// [0] cold zone (compaction summary)
|
|
// [1] warm zone (if any)
|
|
// [2+] hot zone
|
|
|
|
// The hot zone should start with the assistant message at index 5, not the tool at 6.
|
|
// Find the first non-summary message.
|
|
|
|
var hotStart int
|
|
for i, m := range result {
|
|
if m.Role == "assistant" && len(m.ToolCalls) > 0 {
|
|
hotStart = i
|
|
break
|
|
}
|
|
if m.Role == "tool" {
|
|
t.Errorf("hot zone starts with orphaned tool message at index %d", i)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Verify the assistant with tool_calls is included before the tool message
|
|
if hotStart == 0 {
|
|
// The assistant message was not found, which is fine if the walk-back
|
|
// moved the boundary correctly. Let's just verify no orphaned tool.
|
|
for i, m := range result {
|
|
if m.Role == "tool" {
|
|
// Check that the previous message is assistant with matching tool_calls
|
|
if i == 0 {
|
|
t.Error("tool message at start of result with no preceding assistant")
|
|
return
|
|
}
|
|
prev := result[i-1]
|
|
if prev.Role != "assistant" {
|
|
t.Errorf("tool message at %d not preceded by assistant (got %s)", i, prev.Role)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildCompactedHistory_NoOrphanAtBoundary(t *testing.T) {
|
|
// Simpler test: verify that any tool message in the result has
|
|
// a preceding assistant message with tool_calls.
|
|
|
|
messages := []backend.Message{
|
|
{Role: "user", Content: "m1"},
|
|
{Role: "assistant", Content: "r1"},
|
|
{Role: "user", Content: "m2"},
|
|
{Role: "assistant", Content: "", ToolCalls: []backend.ToolCall{{ID: "t1", Name: "shell"}, {ID: "t2", Name: "file_read"}}},
|
|
{Role: "tool", ToolCallID: "t1", Content: "output1"},
|
|
{Role: "tool", ToolCallID: "t2", Content: "output2"},
|
|
{Role: "user", Content: "m3"},
|
|
{Role: "assistant", Content: "r3"},
|
|
{Role: "user", Content: "m4"},
|
|
{Role: "assistant", Content: "r4"},
|
|
{Role: "user", Content: "m5"},
|
|
{Role: "assistant", Content: "r5"},
|
|
}
|
|
|
|
result := buildCompactedHistory("summary", messages)
|
|
|
|
// Scan for tool messages and verify they have proper context
|
|
for i, m := range result {
|
|
if m.Role == "tool" {
|
|
if i == 0 {
|
|
t.Fatal("tool message at index 0 with no preceding message")
|
|
}
|
|
// Walk backward to find assistant with tool_calls
|
|
found := false
|
|
for j := i - 1; j >= 0; j-- {
|
|
if result[j].Role == "assistant" && len(result[j].ToolCalls) > 0 {
|
|
found = true
|
|
break
|
|
}
|
|
if result[j].Role == "user" {
|
|
// Hit a user message without finding assistant+tool_calls
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("tool message at index %d has no preceding assistant with tool_calls", i)
|
|
}
|
|
}
|
|
}
|
|
}
|