test(message): cover slack and feishu media fallbacks
This commit is contained in:
parent
949ac83847
commit
ebfe2b8ced
4 changed files with 139 additions and 8 deletions
|
|
@ -52,6 +52,8 @@ type FeishuChannel struct {
|
||||||
|
|
||||||
progress *channels.ToolFeedbackAnimator
|
progress *channels.ToolFeedbackAnimator
|
||||||
deleteMessageFn func(context.Context, string, string) error
|
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 {
|
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...),
|
client: lark.NewClient(cfg.AppID, cfg.AppSecret.String(), opts...),
|
||||||
}
|
}
|
||||||
ch.deleteMessageFn = ch.deleteMessageAPI
|
ch.deleteMessageFn = ch.deleteMessageAPI
|
||||||
|
ch.sendMediaPartFn = ch.sendMediaPart
|
||||||
|
ch.sendTextFn = ch.sendText
|
||||||
ch.progress = channels.NewToolFeedbackAnimator(ch.EditMessage)
|
ch.progress = channels.NewToolFeedbackAnimator(ch.EditMessage)
|
||||||
ch.SetOwner(ch)
|
ch.SetOwner(ch)
|
||||||
return ch, nil
|
return ch, nil
|
||||||
|
|
@ -500,13 +504,13 @@ func (c *FeishuChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMess
|
||||||
caption := firstMediaCaption(msg.Parts)
|
caption := firstMediaCaption(msg.Parts)
|
||||||
sentAny := false
|
sentAny := false
|
||||||
for _, part := range msg.Parts {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
sentAny = true
|
sentAny = true
|
||||||
}
|
}
|
||||||
if sentAny && caption != "" {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ import (
|
||||||
|
|
||||||
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
|
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/channels"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/media"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestExtractContent(t *testing.T) {
|
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) {
|
func TestFinalizeTrackedToolFeedbackMessage_StopsTrackingBeforeEdit(t *testing.T) {
|
||||||
ch := &FeishuChannel{
|
ch := &FeishuChannel{
|
||||||
progress: channels.NewToolFeedbackAnimator(nil),
|
progress: channels.NewToolFeedbackAnimator(nil),
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ type SlackChannel struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
pendingAcks sync.Map
|
pendingAcks sync.Map
|
||||||
|
uploadFileFn func(context.Context, slack.UploadFileV2Parameters) error
|
||||||
|
postTextFn func(context.Context, string, string, string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type slackMessageRef struct {
|
type slackMessageRef struct {
|
||||||
|
|
@ -63,6 +65,18 @@ func NewSlackChannel(
|
||||||
config: cfg,
|
config: cfg,
|
||||||
api: api,
|
api: api,
|
||||||
socketClient: socketClient,
|
socketClient: socketClient,
|
||||||
|
uploadFileFn: func(ctx context.Context, params slack.UploadFileV2Parameters) error {
|
||||||
|
_, err := api.UploadFileV2Context(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
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,7 +207,7 @@ func (c *SlackChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessa
|
||||||
title = filename
|
title = filename
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = c.api.UploadFileV2Context(ctx, slack.UploadFileV2Parameters{
|
err = c.uploadFileFn(ctx, slack.UploadFileV2Parameters{
|
||||||
Channel: channelID,
|
Channel: channelID,
|
||||||
ThreadTimestamp: threadTS,
|
ThreadTimestamp: threadTS,
|
||||||
File: localPath,
|
File: localPath,
|
||||||
|
|
@ -211,11 +225,7 @@ func (c *SlackChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessa
|
||||||
}
|
}
|
||||||
|
|
||||||
if sentAny && caption != "" {
|
if sentAny && caption != "" {
|
||||||
opts := []slack.MsgOption{slack.MsgOptionText(caption, false)}
|
if err := c.postTextFn(ctx, channelID, threadTS, caption); err != nil {
|
||||||
if threadTS != "" {
|
|
||||||
opts = append(opts, slack.MsgOptionTS(threadTS))
|
|
||||||
}
|
|
||||||
if _, _, err := c.api.PostMessageContext(ctx, channelID, opts...); err != nil {
|
|
||||||
return nil, fmt.Errorf("slack send media caption fallback: %w", channels.ErrTemporary)
|
return nil, fmt.Errorf("slack send media caption fallback: %w", channels.ErrTemporary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,17 @@
|
||||||
package slack
|
package slack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
slacksdk "github.com/slack-go/slack"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/channels"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/media"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseSlackChatID(t *testing.T) {
|
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.UploadFileV2Parameters) 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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue