mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
441 lines
14 KiB
Go
441 lines
14 KiB
Go
package message
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/memohai/memoh/internal/channel"
|
|
mcpgw "github.com/memohai/memoh/internal/mcp"
|
|
)
|
|
|
|
type fakeSender struct {
|
|
err error
|
|
lastReq channel.SendRequest
|
|
}
|
|
|
|
func (f *fakeSender) Send(ctx context.Context, botID string, channelType channel.ChannelType, req channel.SendRequest) error {
|
|
f.lastReq = req
|
|
return f.err
|
|
}
|
|
|
|
type fakeReactor struct {
|
|
err error
|
|
lastReq channel.ReactRequest
|
|
}
|
|
|
|
func (f *fakeReactor) React(ctx context.Context, botID string, channelType channel.ChannelType, req channel.ReactRequest) error {
|
|
f.lastReq = req
|
|
return f.err
|
|
}
|
|
|
|
type fakeResolver struct {
|
|
ct channel.ChannelType
|
|
err error
|
|
}
|
|
|
|
func (f *fakeResolver) ParseChannelType(raw string) (channel.ChannelType, error) {
|
|
if f.err != nil {
|
|
return "", f.err
|
|
}
|
|
return f.ct, nil
|
|
}
|
|
|
|
// --- send tests ---
|
|
|
|
func TestExecutor_ListTools_NilDeps(t *testing.T) {
|
|
exec := NewExecutor(nil, nil, nil, nil, nil)
|
|
tools, err := exec.ListTools(context.Background(), mcpgw.ToolSessionContext{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(tools) != 0 {
|
|
t.Errorf("expected 0 tools when deps nil, got %d", len(tools))
|
|
}
|
|
}
|
|
|
|
func TestExecutor_ListTools(t *testing.T) {
|
|
sender := &fakeSender{}
|
|
reactor := &fakeReactor{}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
|
exec := NewExecutor(nil, sender, reactor, resolver, nil)
|
|
tools, err := exec.ListTools(context.Background(), mcpgw.ToolSessionContext{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(tools) != 2 {
|
|
t.Fatalf("expected 2 tools, got %d", len(tools))
|
|
}
|
|
if tools[0].Name != toolSend {
|
|
t.Errorf("tool[0] name = %q, want %q", tools[0].Name, toolSend)
|
|
}
|
|
if tools[1].Name != toolReact {
|
|
t.Errorf("tool[1] name = %q, want %q", tools[1].Name, toolReact)
|
|
}
|
|
}
|
|
|
|
func TestExecutor_ListTools_OnlySender(t *testing.T) {
|
|
sender := &fakeSender{}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
|
tools, err := exec.ListTools(context.Background(), mcpgw.ToolSessionContext{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(tools) != 1 {
|
|
t.Fatalf("expected 1 tool, got %d", len(tools))
|
|
}
|
|
if tools[0].Name != toolSend {
|
|
t.Errorf("tool name = %q, want %q", tools[0].Name, toolSend)
|
|
}
|
|
}
|
|
|
|
func TestExecutor_CallTool_NotFound(t *testing.T) {
|
|
sender := &fakeSender{}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
|
_, err := exec.CallTool(context.Background(), mcpgw.ToolSessionContext{BotID: "bot1"}, "other_tool", nil)
|
|
if err != mcpgw.ErrToolNotFound {
|
|
t.Errorf("expected ErrToolNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestExecutor_CallTool_NilDeps(t *testing.T) {
|
|
exec := NewExecutor(nil, nil, nil, nil, nil)
|
|
result, err := exec.CallTool(context.Background(), mcpgw.ToolSessionContext{BotID: "bot1"}, toolSend, map[string]any{
|
|
"platform": "feishu", "target": "t1", "text": "hi",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if isErr, _ := result["isError"].(bool); !isErr {
|
|
t.Error("expected error result when deps nil")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_CallTool_NoBotID(t *testing.T) {
|
|
sender := &fakeSender{}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
|
result, err := exec.CallTool(context.Background(), mcpgw.ToolSessionContext{}, toolSend, map[string]any{
|
|
"platform": "feishu", "target": "t1", "text": "hi",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if isErr, _ := result["isError"].(bool); !isErr {
|
|
t.Error("expected error when bot_id is missing")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_CallTool_BotIDMismatch(t *testing.T) {
|
|
sender := &fakeSender{}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
|
session := mcpgw.ToolSessionContext{BotID: "bot1"}
|
|
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
|
"bot_id": "other", "platform": "feishu", "target": "t1", "text": "hi",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if isErr, _ := result["isError"].(bool); !isErr {
|
|
t.Error("expected error when bot_id mismatch")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_CallTool_NoPlatform(t *testing.T) {
|
|
sender := &fakeSender{}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
|
session := mcpgw.ToolSessionContext{BotID: "bot1"}
|
|
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
|
"target": "t1", "text": "hi",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if isErr, _ := result["isError"].(bool); !isErr {
|
|
t.Error("expected error when platform is missing")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_CallTool_PlatformParseError(t *testing.T) {
|
|
sender := &fakeSender{}
|
|
resolver := &fakeResolver{err: errors.New("unknown platform")}
|
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
|
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "feishu"}
|
|
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
|
"platform": "bad", "target": "t1", "text": "hi",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if isErr, _ := result["isError"].(bool); !isErr {
|
|
t.Error("expected error when platform parse fails")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_CallTool_NoMessage(t *testing.T) {
|
|
sender := &fakeSender{}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
|
session := mcpgw.ToolSessionContext{BotID: "bot1"}
|
|
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
|
"platform": "feishu", "target": "t1",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if isErr, _ := result["isError"].(bool); !isErr {
|
|
t.Error("expected error when message/text is missing")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_CallTool_NoTarget(t *testing.T) {
|
|
sender := &fakeSender{}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
|
session := mcpgw.ToolSessionContext{BotID: "bot1"}
|
|
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
|
"platform": "feishu", "text": "hi",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if isErr, _ := result["isError"].(bool); !isErr {
|
|
t.Error("expected error when target and channel_identity_id are missing")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_CallTool_SendError(t *testing.T) {
|
|
sender := &fakeSender{err: errors.New("send failed")}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
|
session := mcpgw.ToolSessionContext{BotID: "bot1", ReplyTarget: "t1"}
|
|
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
|
"platform": "feishu", "text": "hi",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if isErr, _ := result["isError"].(bool); !isErr {
|
|
t.Error("expected error when Send fails")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_CallTool_Success(t *testing.T) {
|
|
sender := &fakeSender{}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
|
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "feishu", ReplyTarget: "chat1"}
|
|
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
|
"text": "hello",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := mcpgw.PayloadError(result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content, _ := result["structuredContent"].(map[string]any)
|
|
if content == nil {
|
|
t.Fatal("no structuredContent")
|
|
}
|
|
if content["ok"] != true {
|
|
t.Errorf("ok = %v", content["ok"])
|
|
}
|
|
if content["platform"] != "feishu" {
|
|
t.Errorf("platform = %v", content["platform"])
|
|
}
|
|
}
|
|
|
|
func TestExecutor_CallTool_ReplyTo(t *testing.T) {
|
|
sender := &fakeSender{}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("telegram")}
|
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
|
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
|
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
|
"text": "reply text",
|
|
"reply_to": "msg-789",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := mcpgw.PayloadError(result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sender.lastReq.Message.Reply == nil {
|
|
t.Fatal("expected Reply to be set")
|
|
}
|
|
if sender.lastReq.Message.Reply.MessageID != "msg-789" {
|
|
t.Errorf("Reply.MessageID = %q, want %q", sender.lastReq.Message.Reply.MessageID, "msg-789")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_CallTool_NoReplyTo(t *testing.T) {
|
|
sender := &fakeSender{}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("telegram")}
|
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
|
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
|
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
|
"text": "no reply",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := mcpgw.PayloadError(result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if sender.lastReq.Message.Reply != nil {
|
|
t.Error("expected Reply to be nil when reply_to is not provided")
|
|
}
|
|
}
|
|
|
|
// --- react tests ---
|
|
|
|
func TestExecutor_React_NilReactor(t *testing.T) {
|
|
exec := NewExecutor(nil, nil, nil, nil, nil)
|
|
result, err := exec.CallTool(context.Background(), mcpgw.ToolSessionContext{BotID: "bot1"}, toolReact, map[string]any{
|
|
"platform": "telegram", "target": "123", "message_id": "456", "emoji": "👍",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if isErr, _ := result["isError"].(bool); !isErr {
|
|
t.Error("expected error when reactor is nil")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_React_NoMessageID(t *testing.T) {
|
|
reactor := &fakeReactor{}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("telegram")}
|
|
exec := NewExecutor(nil, nil, reactor, resolver, nil)
|
|
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
|
result, err := exec.CallTool(context.Background(), session, toolReact, map[string]any{
|
|
"emoji": "👍",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if isErr, _ := result["isError"].(bool); !isErr {
|
|
t.Error("expected error when message_id is missing")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_React_NoTarget(t *testing.T) {
|
|
reactor := &fakeReactor{}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("telegram")}
|
|
exec := NewExecutor(nil, nil, reactor, resolver, nil)
|
|
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram"}
|
|
result, err := exec.CallTool(context.Background(), session, toolReact, map[string]any{
|
|
"message_id": "456", "emoji": "👍",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if isErr, _ := result["isError"].(bool); !isErr {
|
|
t.Error("expected error when target is missing")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_React_Success(t *testing.T) {
|
|
reactor := &fakeReactor{}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("telegram")}
|
|
exec := NewExecutor(nil, nil, reactor, resolver, nil)
|
|
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
|
result, err := exec.CallTool(context.Background(), session, toolReact, map[string]any{
|
|
"message_id": "456", "emoji": "👍",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := mcpgw.PayloadError(result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content, _ := result["structuredContent"].(map[string]any)
|
|
if content == nil {
|
|
t.Fatal("no structuredContent")
|
|
}
|
|
if content["ok"] != true {
|
|
t.Errorf("ok = %v", content["ok"])
|
|
}
|
|
if content["action"] != "added" {
|
|
t.Errorf("action = %v", content["action"])
|
|
}
|
|
if content["emoji"] != "👍" {
|
|
t.Errorf("emoji = %v", content["emoji"])
|
|
}
|
|
}
|
|
|
|
func TestExecutor_React_Remove(t *testing.T) {
|
|
reactor := &fakeReactor{}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("telegram")}
|
|
exec := NewExecutor(nil, nil, reactor, resolver, nil)
|
|
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
|
result, err := exec.CallTool(context.Background(), session, toolReact, map[string]any{
|
|
"message_id": "456", "remove": true,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := mcpgw.PayloadError(result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
content, _ := result["structuredContent"].(map[string]any)
|
|
if content["action"] != "removed" {
|
|
t.Errorf("action = %v", content["action"])
|
|
}
|
|
if reactor.lastReq.Remove != true {
|
|
t.Error("expected Remove=true in request")
|
|
}
|
|
}
|
|
|
|
func TestExecutor_React_Error(t *testing.T) {
|
|
reactor := &fakeReactor{err: errors.New("reaction failed")}
|
|
resolver := &fakeResolver{ct: channel.ChannelType("telegram")}
|
|
exec := NewExecutor(nil, nil, reactor, resolver, nil)
|
|
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
|
result, err := exec.CallTool(context.Background(), session, toolReact, map[string]any{
|
|
"message_id": "456", "emoji": "👍",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if isErr, _ := result["isError"].(bool); !isErr {
|
|
t.Error("expected error when React fails")
|
|
}
|
|
}
|
|
|
|
// --- parseOutboundMessage tests ---
|
|
|
|
func TestParseOutboundMessage(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args map[string]any
|
|
fallback string
|
|
wantEmpty bool
|
|
wantErr bool
|
|
}{
|
|
{"text fallback", map[string]any{}, "hello", false, false},
|
|
{"message string", map[string]any{"message": "msg"}, "", false, false},
|
|
{"message object", map[string]any{"message": map[string]any{"text": "obj"}}, "", false, false},
|
|
{"empty", map[string]any{}, "", true, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
msg, err := parseOutboundMessage(tt.args, tt.fallback)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("parseOutboundMessage() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if tt.wantEmpty && !msg.IsEmpty() {
|
|
t.Error("expected empty message")
|
|
}
|
|
if !tt.wantEmpty && msg.IsEmpty() {
|
|
t.Error("expected non-empty message")
|
|
}
|
|
})
|
|
}
|
|
}
|