test(message): cover slack and feishu media fallbacks

This commit is contained in:
Anton Bogdanovich
2026-05-11 16:45:01 -07:00
parent c05e5e29c6
commit 1bf0d898de
4 changed files with 139 additions and 8 deletions
+6 -2
View File
@@ -52,6 +52,8 @@ type FeishuChannel struct {
progress *channels.ToolFeedbackAnimator
deleteMessageFn func(context.Context, string, string) error
sendMediaPartFn func(context.Context, string, bus.MediaPart, media.MediaStore) error
sendTextFn func(context.Context, string, string) (string, error)
}
type cachedMessage struct {
@@ -78,6 +80,8 @@ func NewFeishuChannel(bc *config.Channel, cfg *config.FeishuSettings, bus *bus.M
client: lark.NewClient(cfg.AppID, cfg.AppSecret.String(), opts...),
}
ch.deleteMessageFn = ch.deleteMessageAPI
ch.sendMediaPartFn = ch.sendMediaPart
ch.sendTextFn = ch.sendText
ch.progress = channels.NewToolFeedbackAnimator(ch.EditMessage)
ch.SetOwner(ch)
return ch, nil
@@ -500,13 +504,13 @@ func (c *FeishuChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMess
caption := firstMediaCaption(msg.Parts)
sentAny := false
for _, part := range msg.Parts {
if err := c.sendMediaPart(ctx, msg.ChatID, part, store); err != nil {
if err := c.sendMediaPartFn(ctx, msg.ChatID, part, store); err != nil {
return nil, err
}
sentAny = true
}
if sentAny && caption != "" {
if _, err := c.sendText(ctx, msg.ChatID, caption); err != nil {
if _, err := c.sendTextFn(ctx, msg.ChatID, caption); err != nil {
return nil, err
}
}
+39
View File
@@ -9,7 +9,9 @@ import (
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels"
"github.com/sipeed/picoclaw/pkg/media"
)
func TestExtractContent(t *testing.T) {
@@ -319,6 +321,43 @@ func TestFinalizeTrackedToolFeedbackMessage_ClearAfterSuccessfulEdit(t *testing.
}
}
func TestSendMedia_SendsCaptionFallbackAfterMedia(t *testing.T) {
ch := &FeishuChannel{
BaseChannel: channels.NewBaseChannel("feishu", nil, nil, nil),
progress: channels.NewToolFeedbackAnimator(nil),
}
ch.SetRunning(true)
ch.SetMediaStore(media.NewFileMediaStore())
var mediaOrder []string
var textCalls []string
ch.sendMediaPartFn = func(ctx context.Context, chatID string, part bus.MediaPart, store media.MediaStore) error {
mediaOrder = append(mediaOrder, part.Type)
return nil
}
ch.sendTextFn = func(ctx context.Context, chatID, text string) (string, error) {
textCalls = append(textCalls, chatID+"|"+text)
return "msg-1", nil
}
_, err := ch.SendMedia(context.Background(), bus.OutboundMediaMessage{
ChatID: "oc_123",
Parts: []bus.MediaPart{
{Type: "image", Caption: "shared caption"},
{Type: "file"},
},
})
if err != nil {
t.Fatalf("SendMedia() error = %v", err)
}
if len(mediaOrder) != 2 {
t.Fatalf("media sends = %v, want 2 sends", mediaOrder)
}
if len(textCalls) != 1 || textCalls[0] != "oc_123|shared caption" {
t.Fatalf("textCalls = %v, want [oc_123|shared caption]", textCalls)
}
}
func TestFinalizeTrackedToolFeedbackMessage_StopsTrackingBeforeEdit(t *testing.T) {
ch := &FeishuChannel{
progress: channels.NewToolFeedbackAnimator(nil),
+16 -6
View File
@@ -29,6 +29,8 @@ type SlackChannel struct {
ctx context.Context
cancel context.CancelFunc
pendingAcks sync.Map
uploadFileFn func(context.Context, slack.UploadFileParameters) error
postTextFn func(context.Context, string, string, string) error
}
type slackMessageRef struct {
@@ -63,6 +65,18 @@ func NewSlackChannel(
config: cfg,
api: api,
socketClient: socketClient,
uploadFileFn: func(ctx context.Context, params slack.UploadFileParameters) error {
_, err := api.UploadFileContext(ctx, params)
return err
},
postTextFn: func(ctx context.Context, channelID, threadTS, text string) error {
opts := []slack.MsgOption{slack.MsgOptionText(text, false)}
if threadTS != "" {
opts = append(opts, slack.MsgOptionTS(threadTS))
}
_, _, err := api.PostMessageContext(ctx, channelID, opts...)
return err
},
}, nil
}
@@ -193,7 +207,7 @@ func (c *SlackChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessa
title = filename
}
_, err = c.api.UploadFileContext(ctx, slack.UploadFileParameters{
err = c.uploadFileFn(ctx, slack.UploadFileParameters{
Channel: channelID,
ThreadTimestamp: threadTS,
File: localPath,
@@ -211,11 +225,7 @@ func (c *SlackChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessa
}
if sentAny && caption != "" {
opts := []slack.MsgOption{slack.MsgOptionText(caption, false)}
if threadTS != "" {
opts = append(opts, slack.MsgOptionTS(threadTS))
}
if _, _, err := c.api.PostMessageContext(ctx, channelID, opts...); err != nil {
if err := c.postTextFn(ctx, channelID, threadTS, caption); err != nil {
return nil, fmt.Errorf("slack send media caption fallback: %w", channels.ErrTemporary)
}
}
+78
View File
@@ -1,10 +1,17 @@
package slack
import (
"context"
"os"
"path/filepath"
"testing"
slacksdk "github.com/slack-go/slack"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/media"
)
func TestParseSlackChatID(t *testing.T) {
@@ -184,3 +191,74 @@ func TestSlackChannelIsAllowed(t *testing.T) {
}
})
}
func TestSendMedia_SendsCaptionFallbackAfterUploads(t *testing.T) {
ch := &SlackChannel{
BaseChannel: channels.NewBaseChannel("slack", nil, nil, nil),
}
ch.SetRunning(true)
store := media.NewFileMediaStore()
ch.SetMediaStore(store)
tmpDir := t.TempDir()
localPath := filepath.Join(tmpDir, "report.txt")
if err := os.WriteFile(localPath, []byte("attachment body"), 0o600); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
ref, err := store.Store(localPath, media.MediaMeta{
Filename: "report.txt",
ContentType: "text/plain",
}, "test-scope")
if err != nil {
t.Fatalf("Store() error = %v", err)
}
var uploaded []slackUploadRecord
var posted []string
ch.uploadFileFn = func(ctx context.Context, params slacksdk.UploadFileParameters) error {
uploaded = append(uploaded, slackUploadRecord{
Channel: params.Channel,
Thread: params.ThreadTimestamp,
File: params.File,
Name: params.Filename,
Title: params.Title,
})
return nil
}
ch.postTextFn = func(ctx context.Context, channelID, threadTS, text string) error {
posted = append(posted, channelID+"|"+threadTS+"|"+text)
return nil
}
_, err = ch.SendMedia(context.Background(), bus.OutboundMediaMessage{
ChatID: "C123456/1234567890.123456",
Parts: []bus.MediaPart{{
Ref: ref,
Type: "file",
Filename: "report.txt",
ContentType: "text/plain",
Caption: "shared caption",
}},
})
if err != nil {
t.Fatalf("SendMedia() error = %v", err)
}
if len(uploaded) != 1 {
t.Fatalf("uploads = %v, want 1 upload", uploaded)
}
if uploaded[0].Title != "shared caption" {
t.Fatalf("upload title = %q, want shared caption", uploaded[0].Title)
}
if len(posted) != 1 || posted[0] != "C123456|1234567890.123456|shared caption" {
t.Fatalf("posted = %v, want fallback text in same thread", posted)
}
}
type slackUploadRecord struct {
Channel string
Thread string
File string
Name string
Title string
}