package tools import ( "context" "strings" "testing" "github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/providers" ) // MockLLMProvider is a test implementation of LLMProvider type MockLLMProvider struct{} func (m *MockLLMProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, options map[string]interface{}) (*providers.LLMResponse, error) { // Find the last user message to generate a response for i := len(messages) - 1; i >= 0; i-- { if messages[i].Role == "user" { return &providers.LLMResponse{ Content: "Task completed: " + messages[i].Content, }, nil } } return &providers.LLMResponse{Content: "No task provided"}, nil } func (m *MockLLMProvider) GetDefaultModel() string { return "test-model" } func (m *MockLLMProvider) SupportsTools() bool { return false } func (m *MockLLMProvider) GetContextWindow() int { return 4096 } // TestSubagentTool_Name verifies tool name func TestSubagentTool_Name(t *testing.T) { provider := &MockLLMProvider{} manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil) tool := NewSubagentTool(manager) if tool.Name() != "subagent" { t.Errorf("Expected name 'subagent', got '%s'", tool.Name()) } } // TestSubagentTool_Description verifies tool description func TestSubagentTool_Description(t *testing.T) { provider := &MockLLMProvider{} manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil) tool := NewSubagentTool(manager) desc := tool.Description() if desc == "" { t.Error("Description should not be empty") } if !strings.Contains(desc, "subagent") { t.Errorf("Description should mention 'subagent', got: %s", desc) } } // TestSubagentTool_Parameters verifies tool parameters schema func TestSubagentTool_Parameters(t *testing.T) { provider := &MockLLMProvider{} manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil) tool := NewSubagentTool(manager) params := tool.Parameters() if params == nil { t.Error("Parameters should not be nil") } // Check type if params["type"] != "object" { t.Errorf("Expected type 'object', got: %v", params["type"]) } // Check properties props, ok := params["properties"].(map[string]interface{}) if !ok { t.Fatal("Properties should be a map") } // Verify task parameter task, ok := props["task"].(map[string]interface{}) if !ok { t.Fatal("Task parameter should exist") } if task["type"] != "string" { t.Errorf("Task type should be 'string', got: %v", task["type"]) } // Verify label parameter label, ok := props["label"].(map[string]interface{}) if !ok { t.Fatal("Label parameter should exist") } if label["type"] != "string" { t.Errorf("Label type should be 'string', got: %v", label["type"]) } // Check required fields required, ok := params["required"].([]string) if !ok { t.Fatal("Required should be a string array") } if len(required) != 1 || required[0] != "task" { t.Errorf("Required should be ['task'], got: %v", required) } } // TestSubagentTool_SetContext verifies context setting func TestSubagentTool_SetContext(t *testing.T) { provider := &MockLLMProvider{} manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil) tool := NewSubagentTool(manager) tool.SetContext("test-channel", "test-chat") // Verify context is set (we can't directly access private fields, // but we can verify it doesn't crash) // The actual context usage is tested in Execute tests } // TestSubagentTool_Execute_Success tests successful execution func TestSubagentTool_Execute_Success(t *testing.T) { provider := &MockLLMProvider{} msgBus := bus.NewMessageBus() manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus) tool := NewSubagentTool(manager) tool.SetContext("telegram", "chat-123") ctx := context.Background() args := map[string]interface{}{ "task": "Write a haiku about coding", "label": "haiku-task", } result := tool.Execute(ctx, args) // Verify basic ToolResult structure if result == nil { t.Fatal("Result should not be nil") } // Verify no error if result.IsError { t.Errorf("Expected success, got error: %s", result.ForLLM) } // Verify not async if result.Async { t.Error("SubagentTool should be synchronous, not async") } // Verify not silent if result.Silent { t.Error("SubagentTool should not be silent") } // Verify ForUser contains brief summary (not empty) if result.ForUser == "" { t.Error("ForUser should contain result summary") } if !strings.Contains(result.ForUser, "Task completed") { t.Errorf("ForUser should contain task completion, got: %s", result.ForUser) } // Verify ForLLM contains full details if result.ForLLM == "" { t.Error("ForLLM should contain full details") } if !strings.Contains(result.ForLLM, "haiku-task") { t.Errorf("ForLLM should contain label 'haiku-task', got: %s", result.ForLLM) } if !strings.Contains(result.ForLLM, "Task completed:") { t.Errorf("ForLLM should contain task result, got: %s", result.ForLLM) } } // TestSubagentTool_Execute_NoLabel tests execution without label func TestSubagentTool_Execute_NoLabel(t *testing.T) { provider := &MockLLMProvider{} msgBus := bus.NewMessageBus() manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus) tool := NewSubagentTool(manager) ctx := context.Background() args := map[string]interface{}{ "task": "Test task without label", } result := tool.Execute(ctx, args) if result.IsError { t.Errorf("Expected success without label, got error: %s", result.ForLLM) } // ForLLM should show (unnamed) for missing label if !strings.Contains(result.ForLLM, "(unnamed)") { t.Errorf("ForLLM should show '(unnamed)' for missing label, got: %s", result.ForLLM) } } // TestSubagentTool_Execute_MissingTask tests error handling for missing task func TestSubagentTool_Execute_MissingTask(t *testing.T) { provider := &MockLLMProvider{} manager := NewSubagentManager(provider, "test-model", "/tmp/test", nil) tool := NewSubagentTool(manager) ctx := context.Background() args := map[string]interface{}{ "label": "test", } result := tool.Execute(ctx, args) // Should return error if !result.IsError { t.Error("Expected error for missing task parameter") } // ForLLM should contain error message if !strings.Contains(result.ForLLM, "task is required") { t.Errorf("Error message should mention 'task is required', got: %s", result.ForLLM) } // Err should be set if result.Err == nil { t.Error("Err should be set for validation failure") } } // TestSubagentTool_Execute_NilManager tests error handling for nil manager func TestSubagentTool_Execute_NilManager(t *testing.T) { tool := NewSubagentTool(nil) ctx := context.Background() args := map[string]interface{}{ "task": "test task", } result := tool.Execute(ctx, args) // Should return error if !result.IsError { t.Error("Expected error for nil manager") } if !strings.Contains(result.ForLLM, "Subagent manager not configured") { t.Errorf("Error message should mention manager not configured, got: %s", result.ForLLM) } } // TestSubagentTool_Execute_ContextPassing verifies context is properly used func TestSubagentTool_Execute_ContextPassing(t *testing.T) { provider := &MockLLMProvider{} msgBus := bus.NewMessageBus() manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus) tool := NewSubagentTool(manager) // Set context channel := "test-channel" chatID := "test-chat" tool.SetContext(channel, chatID) ctx := context.Background() args := map[string]interface{}{ "task": "Test context passing", } result := tool.Execute(ctx, args) // Should succeed if result.IsError { t.Errorf("Expected success with context, got error: %s", result.ForLLM) } // The context is used internally; we can't directly test it // but execution success indicates context was handled properly } // TestSubagentTool_ForUserTruncation verifies long content is truncated for user func TestSubagentTool_ForUserTruncation(t *testing.T) { // Create a mock provider that returns very long content provider := &MockLLMProvider{} msgBus := bus.NewMessageBus() manager := NewSubagentManager(provider, "test-model", "/tmp/test", msgBus) tool := NewSubagentTool(manager) ctx := context.Background() // Create a task that will generate long response longTask := strings.Repeat("This is a very long task description. ", 100) args := map[string]interface{}{ "task": longTask, "label": "long-test", } result := tool.Execute(ctx, args) // ForUser should be truncated to 500 chars + "..." maxUserLen := 500 if len(result.ForUser) > maxUserLen+3 { // +3 for "..." t.Errorf("ForUser should be truncated to ~%d chars, got: %d", maxUserLen, len(result.ForUser)) } // ForLLM should have full content if !strings.Contains(result.ForLLM, longTask[:50]) { t.Error("ForLLM should contain reference to original task") } }