114 lines
3.6 KiB
Go
114 lines
3.6 KiB
Go
package agent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"testing"
|
|
|
|
"ollie/backend"
|
|
)
|
|
|
|
type mockBackendForCompact struct {
|
|
name string
|
|
model string
|
|
}
|
|
|
|
func (b *mockBackendForCompact) Name() string { return b.name }
|
|
func (b *mockBackendForCompact) Model() string { return b.model }
|
|
|
|
func TestResolveCompactionModel_ConfigWins(t *testing.T) {
|
|
b := &mockBackendForCompact{name: "anthropic", model: "claude-sonnet-4-5"}
|
|
got := resolveCompactionModel("my-custom-model", b)
|
|
if got != "my-custom-model" {
|
|
t.Errorf("got %q; want my-custom-model", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveCompactionModel_EnvOverridesDefault(t *testing.T) {
|
|
t.Setenv("OLLIE_COMPACTION_MODEL", "env-model")
|
|
b := &mockBackendForCompact{name: "anthropic", model: "claude-sonnet-4-5"}
|
|
got := resolveCompactionModel("", b)
|
|
if got != "env-model" {
|
|
t.Errorf("got %q; want env-model", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveCompactionModel_BackendDefault(t *testing.T) {
|
|
os.Unsetenv("OLLIE_COMPACTION_MODEL")
|
|
tests := []struct {
|
|
backend string
|
|
want string
|
|
}{
|
|
{"anthropic", "claude-3-5-haiku-latest"},
|
|
{"openai", "gpt-4o-mini"},
|
|
{"openrouter", "deepseek-v4-flash"},
|
|
{"gemini", "gemini-2.0-flash"},
|
|
{"ollama", ""},
|
|
}
|
|
for _, tt := range tests {
|
|
b := &mockBackendForCompact{name: tt.backend}
|
|
got := resolveCompactionModel("", b)
|
|
if got != tt.want {
|
|
t.Errorf("backend=%s: got %q; want %q", tt.backend, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildCompactedHistory_OrphanedToolMessage(t *testing.T) {
|
|
// Simulate a history where the hot zone boundary (total - hotTailSize)
|
|
// lands on a tool message whose preceding assistant+tool_calls is outside
|
|
// the hot zone. Without the fix, this produces an orphaned tool message
|
|
// that causes OpenAI 400 errors.
|
|
var msgs []backend.Message
|
|
|
|
// Pad with enough messages so the boundary falls on the tool message.
|
|
// We need total - hotTailSize to land on the tool result.
|
|
// hotTailSize = 8, so we need the tool msg at index total-8.
|
|
// Build: 10 user/assistant pairs (20 msgs), then assistant+tool_calls, tool result, then 7 more messages.
|
|
for i := range 10 {
|
|
msgs = append(msgs,
|
|
backend.Message{Role: "user", Content: "q" + string(rune('0'+i))},
|
|
backend.Message{Role: "assistant", Content: "a" + string(rune('0'+i))},
|
|
)
|
|
}
|
|
// assistant with tool_calls at index 20
|
|
msgs = append(msgs, backend.Message{
|
|
Role: "assistant",
|
|
ToolCalls: []backend.ToolCall{{ID: "call_orphan", Name: "test_tool", Arguments: json.RawMessage(`{}`)}},
|
|
})
|
|
// tool result at index 21 — this is where hotStart would land without the fix
|
|
msgs = append(msgs, backend.Message{
|
|
Role: "tool",
|
|
Content: "tool output",
|
|
ToolCallID: "call_orphan",
|
|
})
|
|
// 7 more messages to fill the rest of the hot zone (indices 22-28)
|
|
for i := range 3 {
|
|
msgs = append(msgs,
|
|
backend.Message{Role: "user", Content: "follow " + string(rune('0'+i))},
|
|
backend.Message{Role: "assistant", Content: "reply " + string(rune('0'+i))},
|
|
)
|
|
}
|
|
msgs = append(msgs, backend.Message{Role: "user", Content: "final"})
|
|
// total = 29, hotStart = 29 - 8 = 21 (the tool message)
|
|
|
|
ts := TaskState{Objective: "test"}
|
|
result := buildCompactedHistory(ts, msgs)
|
|
|
|
// Verify no tool message appears without a preceding assistant+tool_calls.
|
|
for i, m := range result {
|
|
if m.Role == "tool" {
|
|
if i == 0 {
|
|
t.Fatalf("result[0] is a tool message — no preceding assistant")
|
|
}
|
|
prev := result[i-1]
|
|
if prev.Role != "assistant" && prev.Role != "tool" {
|
|
t.Fatalf("result[%d] is tool but result[%d] is %q (want assistant or tool)", i, i-1, prev.Role)
|
|
}
|
|
if prev.Role == "assistant" && len(prev.ToolCalls) == 0 {
|
|
t.Fatalf("result[%d] is tool but preceding assistant has no tool_calls", i)
|
|
}
|
|
}
|
|
}
|
|
}
|