mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 12:14:40 +00:00
* fix(feishu): require exact bot_open_id match for mention detection - Previously, when bot_open_id was empty, ALL mentions were treated as bot mentions - This caused multiple agents in same group to all respond to any mention - Now requires bot_open_id to be set AND match exactly for mentionedBot=true - Fixes issue where CPPAI PM would respond even when other agent was mentioned * fix(feishu): gate group pairing by target bot --------- Co-authored-by: ntduc <ntduc@cpp.ai.vn>
311 lines
8.8 KiB
Go
311 lines
8.8 KiB
Go
package feishu
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// --- parseMessageContent ---
|
|
|
|
func TestParseMessageContent_Text(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
raw string
|
|
want string
|
|
}{
|
|
{"normal text", `{"text":"hello world"}`, "hello world"},
|
|
{"empty raw", "", ""},
|
|
{"invalid json falls back", "not-json", "not-json"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := parseMessageContent(tc.raw, "text")
|
|
if got != tc.want {
|
|
t.Errorf("got %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseMessageContent_Image(t *testing.T) {
|
|
got := parseMessageContent(`{"image_key":"img_123"}`, "image")
|
|
if got != "[image]" {
|
|
t.Errorf("got %q, want \"[image]\"", got)
|
|
}
|
|
}
|
|
|
|
func TestParseMessageContent_File(t *testing.T) {
|
|
got := parseMessageContent(`{"file_key":"f1","file_name":"report.pdf"}`, "file")
|
|
if got != "[file: report.pdf]" {
|
|
t.Errorf("got %q, want \"[file: report.pdf]\"", got)
|
|
}
|
|
}
|
|
|
|
func TestParseMessageContent_File_NoName(t *testing.T) {
|
|
got := parseMessageContent("not-json", "file")
|
|
if got != "[file]" {
|
|
t.Errorf("got %q, want \"[file]\"", got)
|
|
}
|
|
}
|
|
|
|
func TestParseMessageContent_UnknownType(t *testing.T) {
|
|
got := parseMessageContent(`{}`, "sticker")
|
|
if got != "[sticker message]" {
|
|
t.Errorf("got %q, want \"[sticker message]\"", got)
|
|
}
|
|
}
|
|
|
|
// --- parsePostContent ---
|
|
|
|
func TestParsePostContent_FlatFormat(t *testing.T) {
|
|
// Format 2: flat, "content" at top level
|
|
raw := `{"content":[[{"tag":"text","text":"hello"},{"tag":"text","text":" world"}]]}`
|
|
got := parsePostContent(raw)
|
|
if got != "hello world" {
|
|
t.Errorf("got %q, want %q", got, "hello world")
|
|
}
|
|
}
|
|
|
|
func TestParsePostContent_LanguageWrapped_ZhCN(t *testing.T) {
|
|
raw := `{"zh_cn":{"title":"Title","content":[[{"tag":"text","text":"你好"}]]}}`
|
|
got := parsePostContent(raw)
|
|
if got != "你好" {
|
|
t.Errorf("got %q, want %q", got, "你好")
|
|
}
|
|
}
|
|
|
|
func TestParsePostContent_LanguageWrapped_EnUS(t *testing.T) {
|
|
raw := `{"en_us":{"content":[[{"tag":"md","text":"**bold**"}]]}}`
|
|
got := parsePostContent(raw)
|
|
if got != "**bold**" {
|
|
t.Errorf("got %q, want %q", got, "**bold**")
|
|
}
|
|
}
|
|
|
|
func TestParsePostContent_MultiParagraph(t *testing.T) {
|
|
raw := `{"content":[[{"tag":"text","text":"line1"}],[{"tag":"text","text":"line2"}]]}`
|
|
got := parsePostContent(raw)
|
|
if got != "line1\nline2" {
|
|
t.Errorf("got %q, want %q", got, "line1\nline2")
|
|
}
|
|
}
|
|
|
|
func TestParsePostContent_AtTag(t *testing.T) {
|
|
raw := `{"content":[[{"tag":"at","user_name":"Alice"},{"tag":"text","text":" hi"}]]}`
|
|
got := parsePostContent(raw)
|
|
if got != "@Alice hi" {
|
|
t.Errorf("got %q, want %q", got, "@Alice hi")
|
|
}
|
|
}
|
|
|
|
func TestParsePostContent_LinkTag(t *testing.T) {
|
|
raw := `{"content":[[{"tag":"a","href":"https://example.com","text":"click"}]]}`
|
|
got := parsePostContent(raw)
|
|
if got != "[click](https://example.com)" {
|
|
t.Errorf("got %q, want %q", got, "[click](https://example.com)")
|
|
}
|
|
}
|
|
|
|
func TestParsePostContent_LinkTag_NoText(t *testing.T) {
|
|
raw := `{"content":[[{"tag":"a","href":"https://example.com"}]]}`
|
|
got := parsePostContent(raw)
|
|
if got != "https://example.com" {
|
|
t.Errorf("got %q, want %q", got, "https://example.com")
|
|
}
|
|
}
|
|
|
|
func TestParsePostContent_ImageTag(t *testing.T) {
|
|
raw := `{"content":[[{"tag":"img","image_key":"img_abc"}]]}`
|
|
got := parsePostContent(raw)
|
|
if got != "[image]" {
|
|
t.Errorf("got %q, want %q", got, "[image]")
|
|
}
|
|
}
|
|
|
|
func TestParsePostContent_InvalidJSON(t *testing.T) {
|
|
// Falls back to raw content
|
|
raw := "not-json"
|
|
got := parsePostContent(raw)
|
|
if got != raw {
|
|
t.Errorf("got %q, want %q", got, raw)
|
|
}
|
|
}
|
|
|
|
// --- resolvePostElements ---
|
|
|
|
func TestResolvePostElements_FirstMapValueFallback(t *testing.T) {
|
|
// No zh_cn/en_us key — uses first map value as language wrapper
|
|
raw := `{"custom_lang":{"content":[[{"tag":"text","text":"fallback"}]]}}`
|
|
elems := resolvePostElements(raw)
|
|
if len(elems) == 0 {
|
|
t.Fatal("expected elements, got none")
|
|
}
|
|
}
|
|
|
|
func TestResolvePostElements_NoContent(t *testing.T) {
|
|
raw := `{"zh_cn":{"title":"only title"}}`
|
|
elems := resolvePostElements(raw)
|
|
if elems != nil {
|
|
t.Errorf("expected nil, got %v", elems)
|
|
}
|
|
}
|
|
|
|
// --- extractPostImageKeys ---
|
|
|
|
func TestExtractPostImageKeys_Basic(t *testing.T) {
|
|
raw := `{"content":[[{"tag":"img","image_key":"img_001"},{"tag":"text","text":"caption"}],[{"tag":"img","image_key":"img_002"}]]}`
|
|
keys := extractPostImageKeys(raw)
|
|
if len(keys) != 2 {
|
|
t.Fatalf("expected 2 keys, got %d: %v", len(keys), keys)
|
|
}
|
|
if keys[0] != "img_001" || keys[1] != "img_002" {
|
|
t.Errorf("keys: got %v", keys)
|
|
}
|
|
}
|
|
|
|
func TestExtractPostImageKeys_Dedup(t *testing.T) {
|
|
raw := `{"content":[[{"tag":"img","image_key":"img_dup"}],[{"tag":"img","image_key":"img_dup"}]]}`
|
|
keys := extractPostImageKeys(raw)
|
|
if len(keys) != 1 {
|
|
t.Errorf("expected 1 deduped key, got %d: %v", len(keys), keys)
|
|
}
|
|
}
|
|
|
|
func TestExtractPostImageKeys_NoImages(t *testing.T) {
|
|
raw := `{"content":[[{"tag":"text","text":"no images"}]]}`
|
|
keys := extractPostImageKeys(raw)
|
|
if keys != nil {
|
|
t.Errorf("expected nil, got %v", keys)
|
|
}
|
|
}
|
|
|
|
// --- resolveMentions ---
|
|
|
|
func TestResolveMentions_BotMentionStripped(t *testing.T) {
|
|
text := "@_user_1 hello"
|
|
mentions := []mentionInfo{{Key: "@_user_1", OpenID: "ou_bot123", Name: "Bot"}}
|
|
got := resolveMentions(text, mentions, "ou_bot123")
|
|
if got != "hello" {
|
|
t.Errorf("got %q, want %q", got, "hello")
|
|
}
|
|
}
|
|
|
|
func TestResolveMentions_UserMentionReplaced(t *testing.T) {
|
|
text := "hey @_user_1 how are you"
|
|
mentions := []mentionInfo{{Key: "@_user_1", OpenID: "ou_user999", Name: "Alice"}}
|
|
got := resolveMentions(text, mentions, "ou_bot123")
|
|
if got != "hey @Alice how are you" {
|
|
t.Errorf("got %q, want %q", got, "hey @Alice how are you")
|
|
}
|
|
}
|
|
|
|
func TestResolveMentions_EmptyKey_Skipped(t *testing.T) {
|
|
text := "hello"
|
|
mentions := []mentionInfo{{Key: "", OpenID: "ou_abc", Name: "Bob"}}
|
|
got := resolveMentions(text, mentions, "")
|
|
if got != "hello" {
|
|
t.Errorf("got %q, want %q", got, "hello")
|
|
}
|
|
}
|
|
|
|
func TestResolveMentions_NoBotID_AllMentionsReplacedWithName(t *testing.T) {
|
|
text := "@_user_1 and @_user_2"
|
|
mentions := []mentionInfo{
|
|
{Key: "@_user_1", OpenID: "ou_A", Name: "Alice"},
|
|
{Key: "@_user_2", OpenID: "ou_B", Name: "Bob"},
|
|
}
|
|
got := resolveMentions(text, mentions, "")
|
|
// botOpenID is empty: no mention is treated as this bot, so all user
|
|
// mentions are preserved as readable names.
|
|
if got != "@Alice and @Bob" {
|
|
t.Errorf("got %q, want %q", got, "@Alice and @Bob")
|
|
}
|
|
}
|
|
|
|
// --- parseMessageEvent (via Channel) ---
|
|
|
|
func TestParseMessageEvent_TextMessage(t *testing.T) {
|
|
ch := &Channel{}
|
|
ev := &MessageEvent{}
|
|
ev.Event.Message.MessageID = "om_abc"
|
|
ev.Event.Message.ChatID = "oc_chat_1"
|
|
ev.Event.Message.ChatType = "p2p"
|
|
ev.Event.Message.MessageType = "text"
|
|
ev.Event.Message.Content = `{"text":"hello"}`
|
|
ev.Event.Sender.SenderID.OpenID = "ou_sender1"
|
|
|
|
mc := ch.parseMessageEvent(ev)
|
|
if mc == nil {
|
|
t.Fatal("got nil messageContext")
|
|
}
|
|
if mc.Content != "hello" {
|
|
t.Errorf("content: got %q, want %q", mc.Content, "hello")
|
|
}
|
|
if mc.ChatID != "oc_chat_1" {
|
|
t.Errorf("ChatID: got %q", mc.ChatID)
|
|
}
|
|
if mc.SenderID != "ou_sender1" {
|
|
t.Errorf("SenderID: got %q", mc.SenderID)
|
|
}
|
|
if mc.MentionedBot {
|
|
t.Error("MentionedBot should be false for text with no mentions")
|
|
}
|
|
}
|
|
|
|
func TestParseMessageEvent_BotMentioned(t *testing.T) {
|
|
ch := &Channel{botOpenID: "ou_bot_xyz"}
|
|
ev := &MessageEvent{}
|
|
ev.Event.Message.MessageType = "text"
|
|
ev.Event.Message.Content = `{"text":"@_user_1 please help"}`
|
|
ev.Event.Message.Mentions = []EventMention{
|
|
{Key: "@_user_1", ID: struct {
|
|
OpenID string `json:"open_id"`
|
|
UserID string `json:"user_id"`
|
|
UnionID string `json:"union_id"`
|
|
}{OpenID: "ou_bot_xyz"}, Name: "MyBot"},
|
|
}
|
|
|
|
mc := ch.parseMessageEvent(ev)
|
|
if !mc.MentionedBot {
|
|
t.Error("MentionedBot should be true when bot's open_id is mentioned")
|
|
}
|
|
// Bot mention key should be stripped from content
|
|
if mc.Content == "@_user_1 please help" {
|
|
t.Error("bot mention key should be stripped from content")
|
|
}
|
|
}
|
|
|
|
func TestParseMessageEvent_UnknownBotIDDoesNotTreatAnyMentionAsBot(t *testing.T) {
|
|
ch := &Channel{}
|
|
ev := &MessageEvent{}
|
|
ev.Event.Message.MessageType = "text"
|
|
ev.Event.Message.Content = `{"text":"@_user_1 please help"}`
|
|
ev.Event.Message.Mentions = []EventMention{
|
|
{Key: "@_user_1", ID: struct {
|
|
OpenID string `json:"open_id"`
|
|
UserID string `json:"user_id"`
|
|
UnionID string `json:"union_id"`
|
|
}{OpenID: "ou_other_bot"}, Name: "OtherBot"},
|
|
}
|
|
|
|
mc := ch.parseMessageEvent(ev)
|
|
if mc.MentionedBot {
|
|
t.Fatal("MentionedBot must be false when bot open_id is unknown")
|
|
}
|
|
if mc.Content != "@OtherBot please help" {
|
|
t.Fatalf("content = %q, want other mention preserved", mc.Content)
|
|
}
|
|
}
|
|
|
|
func TestParseMessageEvent_NoSender(t *testing.T) {
|
|
ch := &Channel{}
|
|
ev := &MessageEvent{}
|
|
ev.Event.Message.MessageType = "text"
|
|
ev.Event.Message.Content = `{"text":"hi"}`
|
|
// Sender fields are zero-value
|
|
|
|
mc := ch.parseMessageEvent(ev)
|
|
if mc.SenderID != "" {
|
|
t.Errorf("SenderID: got %q, want empty", mc.SenderID)
|
|
}
|
|
}
|