ollie/cmd/olliesrv/internal/agent/history_test.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)
}
}
}
}