diff --git a/pkg/agent/hooks.go b/pkg/agent/hooks.go index 687e54532..6ef796543 100644 --- a/pkg/agent/hooks.go +++ b/pkg/agent/hooks.go @@ -788,7 +788,7 @@ func cloneLLMResponse(resp *providers.LLMResponse) *providers.LLMResponse { func cloneStringAnyMap(src map[string]any) map[string]any { if len(src) == 0 { - return nil + return map[string]any{} } cloned := make(map[string]any, len(src)) diff --git a/pkg/agent/hooks_test.go b/pkg/agent/hooks_test.go index 1cfa341a7..e479fbff9 100644 --- a/pkg/agent/hooks_test.go +++ b/pkg/agent/hooks_test.go @@ -1168,6 +1168,56 @@ func TestAgentLoop_HookRespond_SteeringSkipsRemaining(t *testing.T) { } } +func TestCloneStringAnyMap_EmptyMapReturnsNonNil(t *testing.T) { + tests := []struct { + name string + input map[string]any + wantNil bool + wantLen int + }{ + { + name: "nil input returns empty map", + input: nil, + wantNil: false, + wantLen: 0, + }, + { + name: "empty map returns empty map", + input: map[string]any{}, + wantNil: false, + wantLen: 0, + }, + { + name: "populated map is cloned", + input: map[string]any{"key": "value"}, + wantNil: false, + wantLen: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := cloneStringAnyMap(tt.input) + if result == nil { + t.Fatal("cloneStringAnyMap returned nil — MCP tool calls " + + "with no arguments would send null instead of {}") + } + if len(result) != tt.wantLen { + t.Fatalf("expected len %d, got %d", tt.wantLen, len(result)) + } + }) + } + + t.Run("clone does not share underlying map", func(t *testing.T) { + src := map[string]any{"a": 1} + cloned := cloneStringAnyMap(src) + cloned["b"] = 2 + if _, ok := src["b"]; ok { + t.Fatal("modifying clone should not affect source") + } + }) +} + func filterEvents(events []Event, kind EventKind) []Event { var result []Event for _, evt := range events {