feat: add file attachment support to message tool
This commit is contained in:
parent
59fd391248
commit
45d2edef5c
7 changed files with 314 additions and 13 deletions
|
|
@ -107,11 +107,12 @@ func registerSharedTools(cfg *config.Config, msgBus *bus.MessageBus, registry *A
|
||||||
|
|
||||||
// Message tool
|
// Message tool
|
||||||
messageTool := tools.NewMessageTool()
|
messageTool := tools.NewMessageTool()
|
||||||
messageTool.SetSendCallback(func(channel, chatID, content string) error {
|
messageTool.SetSendCallback(func(channel, chatID, content string, attachments []bus.Attachment) error {
|
||||||
msgBus.PublishOutbound(bus.OutboundMessage{
|
msgBus.PublishOutbound(bus.OutboundMessage{
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
ChatID: chatID,
|
ChatID: chatID,
|
||||||
Content: content,
|
Content: content,
|
||||||
|
Attachments: attachments,
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,16 @@ type InboundMessage struct {
|
||||||
Metadata map[string]string `json:"metadata,omitempty"`
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Attachment struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
}
|
||||||
|
|
||||||
type OutboundMessage struct {
|
type OutboundMessage struct {
|
||||||
Channel string `json:"channel"`
|
Channel string `json:"channel"`
|
||||||
ChatID string `json:"chat_id"`
|
ChatID string `json:"chat_id"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
Attachments []Attachment `json:"attachments,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageHandler func(InboundMessage) error
|
type MessageHandler func(InboundMessage) error
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,11 @@ func (c *DiscordChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
|
||||||
return fmt.Errorf("channel ID is empty")
|
return fmt.Errorf("channel ID is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there are attachments, send them with the message
|
||||||
|
if len(msg.Attachments) > 0 {
|
||||||
|
return c.sendWithAttachments(ctx, channelID, msg.Content, msg.Attachments)
|
||||||
|
}
|
||||||
|
|
||||||
runes := []rune(msg.Content)
|
runes := []rune(msg.Content)
|
||||||
if len(runes) == 0 {
|
if len(runes) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -116,6 +121,56 @@ func (c *DiscordChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *DiscordChannel) sendWithAttachments(ctx context.Context, channelID, content string, attachments []bus.Attachment) error {
|
||||||
|
sendCtx, cancel := context.WithTimeout(ctx, sendTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
files := make([]*discordgo.File, 0, len(attachments))
|
||||||
|
openedFiles := make([]*os.File, 0, len(attachments))
|
||||||
|
|
||||||
|
// Clean up all opened files when done
|
||||||
|
defer func() {
|
||||||
|
for _, f := range openedFiles {
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, attachment := range attachments {
|
||||||
|
file, err := os.Open(attachment.Path)
|
||||||
|
if err != nil {
|
||||||
|
done <- fmt.Errorf("failed to open attachment %s: %w", attachment.Path, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
openedFiles = append(openedFiles, file)
|
||||||
|
|
||||||
|
files = append(files, &discordgo.File{
|
||||||
|
Name: attachment.Filename,
|
||||||
|
Reader: file,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
messageData := &discordgo.MessageSend{
|
||||||
|
Content: content,
|
||||||
|
Files: files,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := c.session.ChannelMessageSendComplex(channelID, messageData)
|
||||||
|
done <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send discord message with attachments: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case <-sendCtx.Done():
|
||||||
|
return fmt.Errorf("send message timeout: %w", sendCtx.Err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *DiscordChannel) sendChunk(ctx context.Context, channelID, content string) error {
|
func (c *DiscordChannel) sendChunk(ctx context.Context, channelID, content string) error {
|
||||||
// 使用传入的 ctx 进行超时控制
|
// 使用传入的 ctx 进行超时控制
|
||||||
sendCtx, cancel := context.WithTimeout(ctx, sendTimeout)
|
sendCtx, cancel := context.WithTimeout(ctx, sendTimeout)
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,11 @@ func (c *SlackChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
return fmt.Errorf("invalid slack chat ID: %s", msg.ChatID)
|
return fmt.Errorf("invalid slack chat ID: %s", msg.ChatID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there are attachments, send them
|
||||||
|
if len(msg.Attachments) > 0 {
|
||||||
|
return c.sendWithAttachments(ctx, channelID, threadTS, msg.Content, msg.Attachments)
|
||||||
|
}
|
||||||
|
|
||||||
opts := []slack.MsgOption{
|
opts := []slack.MsgOption{
|
||||||
slack.MsgOptionText(msg.Content, false),
|
slack.MsgOptionText(msg.Content, false),
|
||||||
}
|
}
|
||||||
|
|
@ -148,6 +153,49 @@ func (c *SlackChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *SlackChannel) sendWithAttachments(ctx context.Context, channelID, threadTS, content string, attachments []bus.Attachment) error {
|
||||||
|
for _, attachment := range attachments {
|
||||||
|
file, err := os.Open(attachment.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open attachment %s: %w", attachment.Path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := slack.UploadFileV2Parameters{
|
||||||
|
Channel: channelID,
|
||||||
|
Filename: attachment.Filename,
|
||||||
|
Reader: file,
|
||||||
|
InitialComment: content,
|
||||||
|
ThreadTimestamp: threadTS,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = c.api.UploadFileV2Context(ctx, params)
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to upload file %s: %w", attachment.Filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only use content for first attachment to avoid duplicate comments
|
||||||
|
content = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ref, ok := c.pendingAcks.LoadAndDelete(channelID); ok {
|
||||||
|
msgRef := ref.(slackMessageRef)
|
||||||
|
c.api.AddReaction("white_check_mark", slack.ItemRef{
|
||||||
|
Channel: msgRef.ChannelID,
|
||||||
|
Timestamp: msgRef.Timestamp,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.DebugCF("slack", "Message with attachments sent", map[string]interface{}{
|
||||||
|
"channel_id": channelID,
|
||||||
|
"thread_ts": threadTS,
|
||||||
|
"attachment_count": len(attachments),
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *SlackChannel) eventLoop() {
|
func (c *SlackChannel) eventLoop() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,11 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
|
||||||
c.stopThinking.Delete(msg.ChatID)
|
c.stopThinking.Delete(msg.ChatID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there are attachments, send them
|
||||||
|
if len(msg.Attachments) > 0 {
|
||||||
|
return c.sendWithAttachments(ctx, chatID, msg.Content, msg.Attachments)
|
||||||
|
}
|
||||||
|
|
||||||
htmlContent := markdownToTelegramHTML(msg.Content)
|
htmlContent := markdownToTelegramHTML(msg.Content)
|
||||||
|
|
||||||
// Try to edit placeholder
|
// Try to edit placeholder
|
||||||
|
|
@ -186,6 +191,71 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *TelegramChannel) sendWithAttachments(ctx context.Context, chatID int64, content string, attachments []bus.Attachment) error {
|
||||||
|
chatIDStr := fmt.Sprintf("%d", chatID)
|
||||||
|
htmlContent := markdownToTelegramHTML(content)
|
||||||
|
|
||||||
|
// Try to edit placeholder with the message content
|
||||||
|
// This shows the LLM's response as a text message
|
||||||
|
if pID, ok := c.placeholders.Load(chatIDStr); ok {
|
||||||
|
c.placeholders.Delete(chatIDStr)
|
||||||
|
editMsg := tu.EditMessageText(tu.ID(chatID), pID.(int), htmlContent)
|
||||||
|
editMsg.ParseMode = telego.ModeHTML
|
||||||
|
|
||||||
|
if _, err := c.bot.EditMessageText(ctx, editMsg); err != nil {
|
||||||
|
logger.DebugCF("telegram", "Failed to edit placeholder, will send new message", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
// Fallback to sending new message if edit fails
|
||||||
|
tgMsg := tu.Message(tu.ID(chatID), htmlContent)
|
||||||
|
tgMsg.ParseMode = telego.ModeHTML
|
||||||
|
if _, err = c.bot.SendMessage(ctx, tgMsg); err != nil {
|
||||||
|
logger.ErrorCF("telegram", "HTML parse failed, falling back to plain text", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
tgMsg.ParseMode = ""
|
||||||
|
tgMsg.Text = content
|
||||||
|
c.bot.SendMessage(ctx, tgMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No placeholder exists, send as new message
|
||||||
|
tgMsg := tu.Message(tu.ID(chatID), htmlContent)
|
||||||
|
tgMsg.ParseMode = telego.ModeHTML
|
||||||
|
if _, err := c.bot.SendMessage(ctx, tgMsg); err != nil {
|
||||||
|
logger.ErrorCF("telegram", "HTML parse failed, falling back to plain text", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
tgMsg.ParseMode = ""
|
||||||
|
tgMsg.Text = content
|
||||||
|
c.bot.SendMessage(ctx, tgMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now send files as separate messages
|
||||||
|
for _, attachment := range attachments {
|
||||||
|
file, err := os.Open(attachment.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open attachment %s: %w", attachment.Path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
document := tu.Document(
|
||||||
|
tu.ID(chatID),
|
||||||
|
tu.File(file),
|
||||||
|
)
|
||||||
|
document.Caption = attachment.Filename
|
||||||
|
|
||||||
|
if _, err := c.bot.SendDocument(ctx, document); err != nil {
|
||||||
|
file.Close()
|
||||||
|
return fmt.Errorf("failed to send document %s: %w", attachment.Filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Message) error {
|
func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Message) error {
|
||||||
if message == nil {
|
if message == nil {
|
||||||
return fmt.Errorf("message is nil")
|
return fmt.Errorf("message is nil")
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ package tools
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SendCallback func(channel, chatID, content string) error
|
type SendCallback func(channel, chatID, content string, attachments []bus.Attachment) error
|
||||||
|
|
||||||
type MessageTool struct {
|
type MessageTool struct {
|
||||||
sendCallback SendCallback
|
sendCallback SendCallback
|
||||||
|
|
@ -42,6 +44,24 @@ func (t *MessageTool) Parameters() map[string]interface{} {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optional: target chat/user ID",
|
"description": "Optional: target chat/user ID",
|
||||||
},
|
},
|
||||||
|
"attachments": map[string]interface{}{
|
||||||
|
"type": "array",
|
||||||
|
"description": "Optional: files to attach to the message",
|
||||||
|
"items": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"path": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Absolute file path to attach",
|
||||||
|
},
|
||||||
|
"filename": map[string]interface{}{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Filename to use for the attachment",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": []string{"path", "filename"},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"required": []string{"content"},
|
"required": []string{"content"},
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +106,24 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]interface{})
|
||||||
return &ToolResult{ForLLM: "Message sending not configured", IsError: true}
|
return &ToolResult{ForLLM: "Message sending not configured", IsError: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := t.sendCallback(channel, chatID, content); err != nil {
|
// Parse attachments if provided
|
||||||
|
var attachments []bus.Attachment
|
||||||
|
if attachmentsRaw, ok := args["attachments"].([]interface{}); ok {
|
||||||
|
for _, attachRaw := range attachmentsRaw {
|
||||||
|
if attachMap, ok := attachRaw.(map[string]interface{}); ok {
|
||||||
|
path, pathOk := attachMap["path"].(string)
|
||||||
|
filename, filenameOk := attachMap["filename"].(string)
|
||||||
|
if pathOk && filenameOk {
|
||||||
|
attachments = append(attachments, bus.Attachment{
|
||||||
|
Path: path,
|
||||||
|
Filename: filename,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.sendCallback(channel, chatID, content, attachments); err != nil {
|
||||||
return &ToolResult{
|
return &ToolResult{
|
||||||
ForLLM: fmt.Sprintf("sending message: %v", err),
|
ForLLM: fmt.Sprintf("sending message: %v", err),
|
||||||
IsError: true,
|
IsError: true,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMessageTool_Execute_Success(t *testing.T) {
|
func TestMessageTool_Execute_Success(t *testing.T) {
|
||||||
|
|
@ -11,10 +13,12 @@ func TestMessageTool_Execute_Success(t *testing.T) {
|
||||||
tool.SetContext("test-channel", "test-chat-id")
|
tool.SetContext("test-channel", "test-chat-id")
|
||||||
|
|
||||||
var sentChannel, sentChatID, sentContent string
|
var sentChannel, sentChatID, sentContent string
|
||||||
tool.SetSendCallback(func(channel, chatID, content string) error {
|
var sentAttachments []bus.Attachment
|
||||||
|
tool.SetSendCallback(func(channel, chatID, content string, attachments []bus.Attachment) error {
|
||||||
sentChannel = channel
|
sentChannel = channel
|
||||||
sentChatID = chatID
|
sentChatID = chatID
|
||||||
sentContent = content
|
sentContent = content
|
||||||
|
sentAttachments = attachments
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -35,6 +39,9 @@ func TestMessageTool_Execute_Success(t *testing.T) {
|
||||||
if sentContent != "Hello, world!" {
|
if sentContent != "Hello, world!" {
|
||||||
t.Errorf("Expected content 'Hello, world!', got '%s'", sentContent)
|
t.Errorf("Expected content 'Hello, world!', got '%s'", sentContent)
|
||||||
}
|
}
|
||||||
|
if len(sentAttachments) != 0 {
|
||||||
|
t.Errorf("Expected no attachments, got %d", len(sentAttachments))
|
||||||
|
}
|
||||||
|
|
||||||
// Verify ToolResult meets US-011 criteria:
|
// Verify ToolResult meets US-011 criteria:
|
||||||
// - Send success returns SilentResult (Silent=true)
|
// - Send success returns SilentResult (Silent=true)
|
||||||
|
|
@ -63,7 +70,7 @@ func TestMessageTool_Execute_WithCustomChannel(t *testing.T) {
|
||||||
tool.SetContext("default-channel", "default-chat-id")
|
tool.SetContext("default-channel", "default-chat-id")
|
||||||
|
|
||||||
var sentChannel, sentChatID string
|
var sentChannel, sentChatID string
|
||||||
tool.SetSendCallback(func(channel, chatID, content string) error {
|
tool.SetSendCallback(func(channel, chatID, content string, attachments []bus.Attachment) error {
|
||||||
sentChannel = channel
|
sentChannel = channel
|
||||||
sentChatID = chatID
|
sentChatID = chatID
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -99,7 +106,7 @@ func TestMessageTool_Execute_SendFailure(t *testing.T) {
|
||||||
tool.SetContext("test-channel", "test-chat-id")
|
tool.SetContext("test-channel", "test-chat-id")
|
||||||
|
|
||||||
sendErr := errors.New("network error")
|
sendErr := errors.New("network error")
|
||||||
tool.SetSendCallback(func(channel, chatID, content string) error {
|
tool.SetSendCallback(func(channel, chatID, content string, attachments []bus.Attachment) error {
|
||||||
return sendErr
|
return sendErr
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -153,7 +160,7 @@ func TestMessageTool_Execute_NoTargetChannel(t *testing.T) {
|
||||||
tool := NewMessageTool()
|
tool := NewMessageTool()
|
||||||
// No SetContext called, so defaultChannel and defaultChatID are empty
|
// No SetContext called, so defaultChannel and defaultChatID are empty
|
||||||
|
|
||||||
tool.SetSendCallback(func(channel, chatID, content string) error {
|
tool.SetSendCallback(func(channel, chatID, content string, attachments []bus.Attachment) error {
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -256,4 +263,81 @@ func TestMessageTool_Parameters(t *testing.T) {
|
||||||
if chatIDProp["type"] != "string" {
|
if chatIDProp["type"] != "string" {
|
||||||
t.Error("Expected chat_id type to be 'string'")
|
t.Error("Expected chat_id type to be 'string'")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check attachments property (optional)
|
||||||
|
attachmentsProp, ok := props["attachments"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
t.Error("Expected 'attachments' property")
|
||||||
|
}
|
||||||
|
if attachmentsProp["type"] != "array" {
|
||||||
|
t.Error("Expected attachments type to be 'array'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessageTool_Execute_WithAttachments(t *testing.T) {
|
||||||
|
tool := NewMessageTool()
|
||||||
|
tool.SetContext("test-channel", "test-chat-id")
|
||||||
|
|
||||||
|
var sentChannel, sentChatID, sentContent string
|
||||||
|
var sentAttachments []bus.Attachment
|
||||||
|
tool.SetSendCallback(func(channel, chatID, content string, attachments []bus.Attachment) error {
|
||||||
|
sentChannel = channel
|
||||||
|
sentChatID = chatID
|
||||||
|
sentContent = content
|
||||||
|
sentAttachments = attachments
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
args := map[string]interface{}{
|
||||||
|
"content": "Here's your document",
|
||||||
|
"attachments": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"path": "/tmp/test.docx",
|
||||||
|
"filename": "test.docx",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"path": "/tmp/test.pdf",
|
||||||
|
"filename": "test.pdf",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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 != "Here's your document" {
|
||||||
|
t.Errorf("Expected content 'Here's your document', got '%s'", sentContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify attachments were passed correctly
|
||||||
|
if len(sentAttachments) != 2 {
|
||||||
|
t.Fatalf("Expected 2 attachments, got %d", len(sentAttachments))
|
||||||
|
}
|
||||||
|
if sentAttachments[0].Path != "/tmp/test.docx" {
|
||||||
|
t.Errorf("Expected first attachment path '/tmp/test.docx', got '%s'", sentAttachments[0].Path)
|
||||||
|
}
|
||||||
|
if sentAttachments[0].Filename != "test.docx" {
|
||||||
|
t.Errorf("Expected first attachment filename 'test.docx', got '%s'", sentAttachments[0].Filename)
|
||||||
|
}
|
||||||
|
if sentAttachments[1].Path != "/tmp/test.pdf" {
|
||||||
|
t.Errorf("Expected second attachment path '/tmp/test.pdf', got '%s'", sentAttachments[1].Path)
|
||||||
|
}
|
||||||
|
if sentAttachments[1].Filename != "test.pdf" {
|
||||||
|
t.Errorf("Expected second attachment filename 'test.pdf', got '%s'", sentAttachments[1].Filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify successful result
|
||||||
|
if !result.Silent {
|
||||||
|
t.Error("Expected Silent=true for successful send")
|
||||||
|
}
|
||||||
|
if result.IsError {
|
||||||
|
t.Error("Expected IsError=false for successful send")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue