test: add tool result truncation tests
Cover 5 scenarios: oversized result truncated with footer, small result untouched, non-tool messages preserved, newline boundary preference, and multiple tool results in single session.
This commit is contained in:
parent
d83280739c
commit
372dfa031f
1 changed files with 308 additions and 0 deletions
|
|
@ -628,3 +628,311 @@ func TestAgentLoop_ContextExhaustionRetry(t *testing.T) {
|
|||
t.Errorf("Expected history to be compressed (len < 8), got %d", len(finalHistory))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTruncateToolResults_OversizedResultTruncated verifies that tool results
|
||||
// exceeding maxToolResultChars are truncated with a warning footer.
|
||||
func TestTruncateToolResults_OversizedResultTruncated(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
cfg := &config.Config{
|
||||
Agents: config.AgentsConfig{
|
||||
Defaults: config.AgentDefaults{
|
||||
Workspace: tmpDir,
|
||||
Model: "test-model",
|
||||
MaxTokens: 4096,
|
||||
MaxToolIterations: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
msgBus := bus.NewMessageBus()
|
||||
provider := &mockProvider{}
|
||||
al := NewAgentLoop(cfg, msgBus, provider)
|
||||
|
||||
agent := al.registry.GetDefaultAgent()
|
||||
if agent == nil {
|
||||
t.Fatal("No default agent found")
|
||||
}
|
||||
|
||||
// Create a large tool result (> maxToolResultChars)
|
||||
bigContent := ""
|
||||
for i := 0; i < maxToolResultChars+1000; i++ {
|
||||
bigContent += "x"
|
||||
}
|
||||
|
||||
sessionKey := "test-truncate"
|
||||
agent.Sessions.GetOrCreate(sessionKey) // ensure session exists
|
||||
history := []providers.Message{
|
||||
{Role: "system", Content: "You are a helper."},
|
||||
{Role: "user", Content: "Do something"},
|
||||
{Role: "assistant", Content: "Calling tool..."},
|
||||
{Role: "tool", Content: bigContent, ToolCallID: "call-1"},
|
||||
{Role: "assistant", Content: "Here is the result."},
|
||||
}
|
||||
agent.Sessions.SetHistory(sessionKey, history)
|
||||
|
||||
al.truncateToolResults(agent, sessionKey)
|
||||
|
||||
updated := agent.Sessions.GetHistory(sessionKey)
|
||||
toolMsg := updated[3]
|
||||
if len(toolMsg.Content) >= len(bigContent) {
|
||||
t.Errorf("Expected tool result to be truncated, got length %d", len(toolMsg.Content))
|
||||
}
|
||||
if toolMsg.ToolCallID != "call-1" {
|
||||
t.Errorf("ToolCallID should be preserved, got %q", toolMsg.ToolCallID)
|
||||
}
|
||||
if !containsString(toolMsg.Content, "[... content truncated") {
|
||||
t.Error("Expected truncation footer in tool result")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTruncateToolResults_SmallResultUntouched verifies that tool results
|
||||
// under the threshold are not modified.
|
||||
func TestTruncateToolResults_SmallResultUntouched(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
cfg := &config.Config{
|
||||
Agents: config.AgentsConfig{
|
||||
Defaults: config.AgentDefaults{
|
||||
Workspace: tmpDir,
|
||||
Model: "test-model",
|
||||
MaxTokens: 4096,
|
||||
MaxToolIterations: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
msgBus := bus.NewMessageBus()
|
||||
provider := &mockProvider{}
|
||||
al := NewAgentLoop(cfg, msgBus, provider)
|
||||
|
||||
agent := al.registry.GetDefaultAgent()
|
||||
if agent == nil {
|
||||
t.Fatal("No default agent found")
|
||||
}
|
||||
|
||||
originalContent := "Short tool result"
|
||||
sessionKey := "test-no-truncate"
|
||||
agent.Sessions.GetOrCreate(sessionKey)
|
||||
history := []providers.Message{
|
||||
{Role: "system", Content: "You are a helper."},
|
||||
{Role: "user", Content: "Do something"},
|
||||
{Role: "tool", Content: originalContent, ToolCallID: "call-1"},
|
||||
{Role: "assistant", Content: "Done."},
|
||||
}
|
||||
agent.Sessions.SetHistory(sessionKey, history)
|
||||
|
||||
al.truncateToolResults(agent, sessionKey)
|
||||
|
||||
updated := agent.Sessions.GetHistory(sessionKey)
|
||||
if updated[2].Content != originalContent {
|
||||
t.Errorf("Small tool result should not be modified, got %q", updated[2].Content)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTruncateToolResults_NonToolMessagesUntouched verifies that user and
|
||||
// assistant messages are never truncated, even if large.
|
||||
func TestTruncateToolResults_NonToolMessagesUntouched(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
cfg := &config.Config{
|
||||
Agents: config.AgentsConfig{
|
||||
Defaults: config.AgentDefaults{
|
||||
Workspace: tmpDir,
|
||||
Model: "test-model",
|
||||
MaxTokens: 4096,
|
||||
MaxToolIterations: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
msgBus := bus.NewMessageBus()
|
||||
provider := &mockProvider{}
|
||||
al := NewAgentLoop(cfg, msgBus, provider)
|
||||
|
||||
agent := al.registry.GetDefaultAgent()
|
||||
if agent == nil {
|
||||
t.Fatal("No default agent found")
|
||||
}
|
||||
|
||||
// Create large user & assistant messages
|
||||
bigContent := ""
|
||||
for i := 0; i < maxToolResultChars+500; i++ {
|
||||
bigContent += "y"
|
||||
}
|
||||
|
||||
sessionKey := "test-non-tool"
|
||||
agent.Sessions.GetOrCreate(sessionKey)
|
||||
history := []providers.Message{
|
||||
{Role: "system", Content: "Prompt"},
|
||||
{Role: "user", Content: bigContent},
|
||||
{Role: "assistant", Content: bigContent},
|
||||
}
|
||||
agent.Sessions.SetHistory(sessionKey, history)
|
||||
|
||||
al.truncateToolResults(agent, sessionKey)
|
||||
|
||||
updated := agent.Sessions.GetHistory(sessionKey)
|
||||
if updated[1].Content != bigContent {
|
||||
t.Error("User message should not be truncated")
|
||||
}
|
||||
if updated[2].Content != bigContent {
|
||||
t.Error("Assistant message should not be truncated")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTruncateToolResults_NewlineBoundary verifies truncation prefers
|
||||
// newline boundaries for cleaner output.
|
||||
func TestTruncateToolResults_NewlineBoundary(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
cfg := &config.Config{
|
||||
Agents: config.AgentsConfig{
|
||||
Defaults: config.AgentDefaults{
|
||||
Workspace: tmpDir,
|
||||
Model: "test-model",
|
||||
MaxTokens: 4096,
|
||||
MaxToolIterations: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
msgBus := bus.NewMessageBus()
|
||||
provider := &mockProvider{}
|
||||
al := NewAgentLoop(cfg, msgBus, provider)
|
||||
|
||||
agent := al.registry.GetDefaultAgent()
|
||||
if agent == nil {
|
||||
t.Fatal("No default agent found")
|
||||
}
|
||||
|
||||
// Build content with a newline near the cut point.
|
||||
// Place a newline 50 chars before maxToolResultChars.
|
||||
content := ""
|
||||
for i := 0; i < maxToolResultChars-50; i++ {
|
||||
content += "a"
|
||||
}
|
||||
content += "\n" // newline at maxToolResultChars - 49
|
||||
for i := 0; i < 1500; i++ {
|
||||
content += "b"
|
||||
}
|
||||
|
||||
sessionKey := "test-newline"
|
||||
agent.Sessions.GetOrCreate(sessionKey)
|
||||
history := []providers.Message{
|
||||
{Role: "system", Content: "Prompt"},
|
||||
{Role: "tool", Content: content, ToolCallID: "call-1"},
|
||||
}
|
||||
agent.Sessions.SetHistory(sessionKey, history)
|
||||
|
||||
al.truncateToolResults(agent, sessionKey)
|
||||
|
||||
updated := agent.Sessions.GetHistory(sessionKey)
|
||||
toolContent := updated[1].Content
|
||||
|
||||
// The truncation should have cut at the newline, not at maxToolResultChars
|
||||
// The content before the footer should end at the newline boundary
|
||||
if !containsString(toolContent, "[... content truncated") {
|
||||
t.Error("Expected truncation footer")
|
||||
}
|
||||
// The truncated content should be shorter than maxToolResultChars + footer
|
||||
if len([]rune(toolContent)) > maxToolResultChars+100 {
|
||||
t.Error("Content should be truncated near the newline boundary")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTruncateToolResults_MultipleToolResults verifies that all oversized tool
|
||||
// results in a session are truncated.
|
||||
func TestTruncateToolResults_MultipleToolResults(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "agent-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
cfg := &config.Config{
|
||||
Agents: config.AgentsConfig{
|
||||
Defaults: config.AgentDefaults{
|
||||
Workspace: tmpDir,
|
||||
Model: "test-model",
|
||||
MaxTokens: 4096,
|
||||
MaxToolIterations: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
msgBus := bus.NewMessageBus()
|
||||
provider := &mockProvider{}
|
||||
al := NewAgentLoop(cfg, msgBus, provider)
|
||||
|
||||
agent := al.registry.GetDefaultAgent()
|
||||
if agent == nil {
|
||||
t.Fatal("No default agent found")
|
||||
}
|
||||
|
||||
bigContent := ""
|
||||
for i := 0; i < maxToolResultChars+2000; i++ {
|
||||
bigContent += "z"
|
||||
}
|
||||
|
||||
sessionKey := "test-multi-tool"
|
||||
agent.Sessions.GetOrCreate(sessionKey)
|
||||
history := []providers.Message{
|
||||
{Role: "system", Content: "Prompt"},
|
||||
{Role: "user", Content: "Step 1"},
|
||||
{Role: "tool", Content: bigContent, ToolCallID: "call-1"},
|
||||
{Role: "assistant", Content: "Got it"},
|
||||
{Role: "user", Content: "Step 2"},
|
||||
{Role: "tool", Content: bigContent, ToolCallID: "call-2"},
|
||||
{Role: "tool", Content: "small result", ToolCallID: "call-3"},
|
||||
{Role: "assistant", Content: "Done"},
|
||||
}
|
||||
agent.Sessions.SetHistory(sessionKey, history)
|
||||
|
||||
al.truncateToolResults(agent, sessionKey)
|
||||
|
||||
updated := agent.Sessions.GetHistory(sessionKey)
|
||||
|
||||
// call-1 should be truncated
|
||||
if !containsString(updated[2].Content, "[... content truncated") {
|
||||
t.Error("First oversized tool result should be truncated")
|
||||
}
|
||||
// call-2 should be truncated
|
||||
if !containsString(updated[5].Content, "[... content truncated") {
|
||||
t.Error("Second oversized tool result should be truncated")
|
||||
}
|
||||
// call-3 should be untouched
|
||||
if updated[6].Content != "small result" {
|
||||
t.Error("Small tool result should not be modified")
|
||||
}
|
||||
}
|
||||
|
||||
// containsString is a simple helper to check substring presence.
|
||||
func containsString(s, substr string) bool {
|
||||
return len(s) >= len(substr) && searchSubstring(s, substr)
|
||||
}
|
||||
|
||||
func searchSubstring(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue