package integrationtools import ( "context" "errors" "testing" "github.com/sipeed/picoclaw/pkg/session" ) func TestMessageTool_Execute_Success(t *testing.T) { tool := NewMessageTool() var sentChannel, sentChatID, sentContent string tool.SetSendCallback(func(ctx context.Context, channel, chatID, content, replyToMessageID string) error { sentChannel = channel sentChatID = chatID sentContent = content if ToolAgentID(ctx) != "" || ToolSessionKey(ctx) != "" || ToolSessionScope(ctx) != nil { t.Fatalf("expected empty turn metadata in basic context, got agent=%q session=%q scope=%+v", ToolAgentID(ctx), ToolSessionKey(ctx), ToolSessionScope(ctx)) } return nil }) ctx := WithToolContext(context.Background(), "test-channel", "test-chat-id") args := map[string]any{ "content": "Hello, world!", } result := tool.Execute(ctx, args) // Verify message was sent with correct parameters if sentChannel != "test-channel" { t.Errorf("Expected channel 'test-channel', got '%s'", sentChannel) } if sentChatID != "test-chat-id" { t.Errorf("Expected chatID 'test-chat-id', got '%s'", sentChatID) } if sentContent != "Hello, world!" { t.Errorf("Expected content 'Hello, world!', got '%s'", sentContent) } // Verify ToolResult meets US-011 criteria: // - Send success returns SilentResult (Silent=true) if !result.Silent { t.Error("Expected Silent=true for successful send") } // - ForLLM contains send status description if result.ForLLM != "Message sent to test-channel:test-chat-id" { t.Errorf("Expected ForLLM 'Message sent to test-channel:test-chat-id', got '%s'", result.ForLLM) } // - ForUser is empty (user already received message directly) if result.ForUser != "" { t.Errorf("Expected ForUser to be empty, got '%s'", result.ForUser) } // - IsError should be false if result.IsError { t.Error("Expected IsError=false for successful send") } } func TestMessageTool_Execute_WithCustomChannel(t *testing.T) { tool := NewMessageTool() var sentChannel, sentChatID string tool.SetSendCallback(func(ctx context.Context, channel, chatID, content, replyToMessageID string) error { sentChannel = channel sentChatID = chatID return nil }) ctx := WithToolContext(context.Background(), "default-channel", "default-chat-id") args := map[string]any{ "content": "Test message", "channel": "custom-channel", "chat_id": "custom-chat-id", } result := tool.Execute(ctx, args) // Verify custom channel/chatID were used instead of defaults if sentChannel != "custom-channel" { t.Errorf("Expected channel 'custom-channel', got '%s'", sentChannel) } if sentChatID != "custom-chat-id" { t.Errorf("Expected chatID 'custom-chat-id', got '%s'", sentChatID) } if !result.Silent { t.Error("Expected Silent=true") } if result.ForLLM != "Message sent to custom-channel:custom-chat-id" { t.Errorf("Expected ForLLM 'Message sent to custom-channel:custom-chat-id', got '%s'", result.ForLLM) } } func TestMessageTool_Execute_SendFailure(t *testing.T) { tool := NewMessageTool() sendErr := errors.New("network error") tool.SetSendCallback(func(ctx context.Context, channel, chatID, content, replyToMessageID string) error { return sendErr }) ctx := WithToolContext(context.Background(), "test-channel", "test-chat-id") args := map[string]any{ "content": "Test message", } result := tool.Execute(ctx, args) // Verify ToolResult for send failure: // - Send failure returns ErrorResult (IsError=true) if !result.IsError { t.Error("Expected IsError=true for failed send") } // - ForLLM contains error description expectedErrMsg := "sending message: network error" if result.ForLLM != expectedErrMsg { t.Errorf("Expected ForLLM '%s', got '%s'", expectedErrMsg, result.ForLLM) } // - Err field should contain original error if result.Err == nil { t.Error("Expected Err to be set") } if result.Err != sendErr { t.Errorf("Expected Err to be sendErr, got %v", result.Err) } } func TestMessageTool_Execute_MissingContent(t *testing.T) { tool := NewMessageTool() ctx := WithToolContext(context.Background(), "test-channel", "test-chat-id") args := map[string]any{} // content missing result := tool.Execute(ctx, args) // Verify error result for missing content if !result.IsError { t.Error("Expected IsError=true for missing content") } if result.ForLLM != "content is required" { t.Errorf("Expected ForLLM 'content is required', got '%s'", result.ForLLM) } } func TestMessageTool_Execute_NoTargetChannel(t *testing.T) { tool := NewMessageTool() // No WithToolContext — channel/chatID are empty tool.SetSendCallback(func(ctx context.Context, channel, chatID, content, replyToMessageID string) error { return nil }) ctx := context.Background() args := map[string]any{ "content": "Test message", } result := tool.Execute(ctx, args) // Verify error when no target channel specified if !result.IsError { t.Error("Expected IsError=true when no target channel") } if result.ForLLM != "No target channel/chat specified" { t.Errorf("Expected ForLLM 'No target channel/chat specified', got '%s'", result.ForLLM) } } func TestMessageTool_Execute_NotConfigured(t *testing.T) { tool := NewMessageTool() // No SetSendCallback called ctx := WithToolContext(context.Background(), "test-channel", "test-chat-id") args := map[string]any{ "content": "Test message", } result := tool.Execute(ctx, args) // Verify error when send callback not configured if !result.IsError { t.Error("Expected IsError=true when send callback not configured") } if result.ForLLM != "Message sending not configured" { t.Errorf("Expected ForLLM 'Message sending not configured', got '%s'", result.ForLLM) } } func TestMessageTool_Name(t *testing.T) { tool := NewMessageTool() if tool.Name() != "message" { t.Errorf("Expected name 'message', got '%s'", tool.Name()) } } func TestMessageTool_Description(t *testing.T) { tool := NewMessageTool() desc := tool.Description() if desc == "" { t.Error("Description should not be empty") } } func TestMessageTool_Parameters(t *testing.T) { tool := NewMessageTool() params := tool.Parameters() // Verify parameters structure typ, ok := params["type"].(string) if !ok || typ != "object" { t.Error("Expected type 'object'") } props, ok := params["properties"].(map[string]any) if !ok { t.Fatal("Expected properties to be a map") } // Check required properties required, ok := params["required"].([]string) if !ok || len(required) != 1 || required[0] != "content" { t.Error("Expected 'content' to be required") } // Check content property contentProp, ok := props["content"].(map[string]any) if !ok { t.Error("Expected 'content' property") } if contentProp["type"] != "string" { t.Error("Expected content type to be 'string'") } // Check channel property (optional) channelProp, ok := props["channel"].(map[string]any) if !ok { t.Error("Expected 'channel' property") } if channelProp["type"] != "string" { t.Error("Expected channel type to be 'string'") } // Check chat_id property (optional) chatIDProp, ok := props["chat_id"].(map[string]any) if !ok { t.Error("Expected 'chat_id' property") } if chatIDProp["type"] != "string" { t.Error("Expected chat_id type to be 'string'") } // Check reply_to_message_id property (optional) replyToProp, ok := props["reply_to_message_id"].(map[string]any) if !ok { t.Error("Expected 'reply_to_message_id' property") } if replyToProp["type"] != "string" { t.Error("Expected reply_to_message_id type to be 'string'") } } func TestMessageTool_Execute_WithReplyToMessageID(t *testing.T) { tool := NewMessageTool() var sentReplyTo string tool.SetSendCallback(func(ctx context.Context, channel, chatID, content, replyToMessageID string) error { sentReplyTo = replyToMessageID return nil }) ctx := WithToolContext(context.Background(), "test-channel", "test-chat-id") args := map[string]any{ "content": "Reply test", "reply_to_message_id": "msg-123", } result := tool.Execute(ctx, args) if result.IsError { t.Fatalf("expected success, got error: %s", result.ForLLM) } if sentReplyTo != "msg-123" { t.Fatalf("expected reply_to_message_id msg-123, got %q", sentReplyTo) } } func TestMessageTool_Execute_PropagatesTurnSessionMetadata(t *testing.T) { tool := NewMessageTool() var gotAgentID, gotSessionKey string var gotScope *session.SessionScope tool.SetSendCallback(func(ctx context.Context, channel, chatID, content, replyToMessageID string) error { gotAgentID = ToolAgentID(ctx) gotSessionKey = ToolSessionKey(ctx) gotScope = ToolSessionScope(ctx) return nil }) ctx := WithToolContext(context.Background(), "test-channel", "test-chat-id") ctx = WithToolSessionContext(ctx, "main", "sk_v1_tool", &session.SessionScope{ Version: session.ScopeVersionV1, AgentID: "main", Channel: "telegram", Dimensions: []string{"chat"}, Values: map[string]string{ "chat": "direct:test-chat-id", }, }) result := tool.Execute(ctx, map[string]any{"content": "Hello, world!"}) if result.IsError { t.Fatalf("expected success, got error: %s", result.ForLLM) } if gotAgentID != "main" { t.Fatalf("ToolAgentID() = %q, want main", gotAgentID) } if gotSessionKey != "sk_v1_tool" { t.Fatalf("ToolSessionKey() = %q, want sk_v1_tool", gotSessionKey) } if gotScope == nil || gotScope.Values["chat"] != "direct:test-chat-id" { t.Fatalf("ToolSessionScope() = %+v, want chat scope", gotScope) } }