mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
337 lines
9.2 KiB
Go
337 lines
9.2 KiB
Go
package qq
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
"github.com/memohai/memoh/internal/channel"
|
|
)
|
|
|
|
func TestEventToInboundMessageC2C(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
msg, ok := eventToInboundMessage(InboundEvent{
|
|
Type: "C2C_MESSAGE_CREATE",
|
|
C2CMessage: &C2CMessageEvent{
|
|
ID: "msg-1",
|
|
Content: "hello",
|
|
Timestamp: "2026-03-06T12:00:00Z",
|
|
Author: C2CAuthor{
|
|
UserOpenID: "user-openid",
|
|
},
|
|
Attachments: []MessageAttachment{{
|
|
ContentType: "image/png",
|
|
URL: "//cdn.qq.com/image.png",
|
|
FileName: "a.png",
|
|
Width: 120,
|
|
Height: 80,
|
|
Size: 2048,
|
|
}},
|
|
},
|
|
}, "bot-1")
|
|
if !ok {
|
|
t.Fatal("expected inbound message")
|
|
}
|
|
if msg.Channel != Type {
|
|
t.Fatalf("unexpected channel: %s", msg.Channel)
|
|
}
|
|
if msg.BotID != "bot-1" {
|
|
t.Fatalf("unexpected bot id: %s", msg.BotID)
|
|
}
|
|
if msg.ReplyTarget != "c2c:user-openid" {
|
|
t.Fatalf("unexpected reply target: %s", msg.ReplyTarget)
|
|
}
|
|
if msg.Conversation.Type != channel.ConversationTypePrivate {
|
|
t.Fatalf("unexpected conversation type: %s", msg.Conversation.Type)
|
|
}
|
|
if msg.Sender.SubjectID != "user-openid" {
|
|
t.Fatalf("unexpected sender subject: %s", msg.Sender.SubjectID)
|
|
}
|
|
if len(msg.Message.Attachments) != 1 {
|
|
t.Fatalf("unexpected attachments: %d", len(msg.Message.Attachments))
|
|
}
|
|
att := msg.Message.Attachments[0]
|
|
if att.Type != channel.AttachmentImage {
|
|
t.Fatalf("unexpected attachment type: %s", att.Type)
|
|
}
|
|
if att.URL != "https://cdn.qq.com/image.png" {
|
|
t.Fatalf("unexpected attachment url: %s", att.URL)
|
|
}
|
|
if mentioned, _ := msg.Metadata["is_mentioned"].(bool); mentioned {
|
|
t.Fatal("direct message should not be marked mentioned")
|
|
}
|
|
}
|
|
|
|
func TestEventToInboundMessageGroupAt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
msg, ok := eventToInboundMessage(InboundEvent{
|
|
Type: "GROUP_AT_MESSAGE_CREATE",
|
|
GroupMessage: &GroupMessageEvent{
|
|
ID: "msg-2",
|
|
Content: "@bot hi",
|
|
Timestamp: "2026-03-06T12:00:00Z",
|
|
GroupOpenID: "group-openid",
|
|
Author: GroupAuthor{
|
|
MemberOpenID: "member-openid",
|
|
},
|
|
},
|
|
}, "bot-2")
|
|
if !ok {
|
|
t.Fatal("expected inbound message")
|
|
}
|
|
if msg.ReplyTarget != "group:group-openid" {
|
|
t.Fatalf("unexpected reply target: %s", msg.ReplyTarget)
|
|
}
|
|
if msg.Conversation.ID != "group-openid" {
|
|
t.Fatalf("unexpected conversation id: %s", msg.Conversation.ID)
|
|
}
|
|
if msg.Conversation.Type != channel.ConversationTypeGroup {
|
|
t.Fatalf("unexpected conversation type: %s", msg.Conversation.Type)
|
|
}
|
|
if msg.Sender.SubjectID != "member-openid" {
|
|
t.Fatalf("unexpected sender subject: %s", msg.Sender.SubjectID)
|
|
}
|
|
if mentioned, _ := msg.Metadata["is_mentioned"].(bool); !mentioned {
|
|
t.Fatal("group at message should be marked mentioned")
|
|
}
|
|
}
|
|
|
|
func TestEventToInboundMessageChannelAt(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
msg, ok := eventToInboundMessage(InboundEvent{
|
|
Type: "AT_MESSAGE_CREATE",
|
|
GuildMessage: &GuildMessageEvent{
|
|
ID: "msg-3",
|
|
Content: "<@bot> hi",
|
|
Timestamp: "2026-03-06T12:00:00Z",
|
|
ChannelID: "channel-1",
|
|
GuildID: "guild-1",
|
|
Author: GuildAuthor{
|
|
ID: "author-1",
|
|
Username: "alice",
|
|
},
|
|
},
|
|
}, "bot-3")
|
|
if !ok {
|
|
t.Fatal("expected inbound message")
|
|
}
|
|
if msg.ReplyTarget != "channel:channel-1" {
|
|
t.Fatalf("unexpected reply target: %s", msg.ReplyTarget)
|
|
}
|
|
if msg.Conversation.Type != channel.ConversationTypeThread {
|
|
t.Fatalf("unexpected conversation type: %s", msg.Conversation.Type)
|
|
}
|
|
if msg.Conversation.ID != "guild-1" {
|
|
t.Fatalf("unexpected conversation id: %s", msg.Conversation.ID)
|
|
}
|
|
if msg.Conversation.ThreadID != "channel-1" {
|
|
t.Fatalf("unexpected thread id: %s", msg.Conversation.ThreadID)
|
|
}
|
|
if msg.Sender.DisplayName != "alice" {
|
|
t.Fatalf("unexpected sender display name: %s", msg.Sender.DisplayName)
|
|
}
|
|
if msg.Sender.Attribute("channel_id") != "channel-1" {
|
|
t.Fatalf("unexpected channel_id attribute: %s", msg.Sender.Attribute("channel_id"))
|
|
}
|
|
if msg.Metadata["guild_id"] != "guild-1" {
|
|
t.Fatalf("unexpected guild_id metadata: %#v", msg.Metadata["guild_id"])
|
|
}
|
|
if mentioned, _ := msg.Metadata["is_mentioned"].(bool); !mentioned {
|
|
t.Fatal("channel at message should be marked mentioned")
|
|
}
|
|
}
|
|
|
|
func TestEventToInboundMessageIgnoresUnsupportedType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if _, ok := eventToInboundMessage(InboundEvent{Type: "READY"}, "bot-1"); ok {
|
|
t.Fatal("unexpected inbound message for READY")
|
|
}
|
|
}
|
|
|
|
func TestEventToInboundMessagePreservesGIFType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
msg, ok := eventToInboundMessage(InboundEvent{
|
|
Type: "C2C_MESSAGE_CREATE",
|
|
C2CMessage: &C2CMessageEvent{
|
|
ID: "msg-gif",
|
|
Content: "gif",
|
|
Timestamp: "2026-03-06T12:00:00Z",
|
|
Author: C2CAuthor{
|
|
UserOpenID: "user-openid",
|
|
},
|
|
Attachments: []MessageAttachment{{
|
|
ContentType: "image/gif",
|
|
URL: "https://cdn.qq.com/animated.gif",
|
|
FileName: "animated.gif",
|
|
}},
|
|
},
|
|
}, "bot-gif")
|
|
if !ok {
|
|
t.Fatal("expected inbound message")
|
|
}
|
|
if len(msg.Message.Attachments) != 1 {
|
|
t.Fatalf("unexpected attachments: %d", len(msg.Message.Attachments))
|
|
}
|
|
if msg.Message.Attachments[0].Type != channel.AttachmentGIF {
|
|
t.Fatalf("unexpected attachment type: %s", msg.Message.Attachments[0].Type)
|
|
}
|
|
}
|
|
|
|
func TestAdjustSessionAfterInvalidKeepsIntentLevel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := NewQQAdapter(nil)
|
|
session := sessionState{
|
|
SessionID: "session-1",
|
|
LastSeq: 42,
|
|
IntentLevel: 0,
|
|
}
|
|
|
|
adapter.adjustSessionAfterInvalid("cfg-1", &session)
|
|
|
|
if session.SessionID != "" {
|
|
t.Fatalf("unexpected session id: %q", session.SessionID)
|
|
}
|
|
if session.LastSeq != 0 {
|
|
t.Fatalf("unexpected seq: %d", session.LastSeq)
|
|
}
|
|
if session.IntentLevel != 0 {
|
|
t.Fatalf("unexpected intent level: %d", session.IntentLevel)
|
|
}
|
|
|
|
saved := adapter.loadSession("cfg-1")
|
|
if saved.IntentLevel != 0 {
|
|
t.Fatalf("unexpected saved intent level: %d", saved.IntentLevel)
|
|
}
|
|
}
|
|
|
|
func TestStartHeartbeatCancelStopsSessionLoop(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
heartbeat := startHeartbeat(context.Background(), &gatewayWriter{}, time.Hour, func() int { return 0 })
|
|
heartbeat.cancel()
|
|
|
|
select {
|
|
case <-heartbeat.done:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("heartbeat did not stop after session cancel")
|
|
}
|
|
}
|
|
|
|
func TestHandleDispatchMarksHealthySessionForReadyAndResumed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := NewQQAdapter(nil)
|
|
cfg := channel.ChannelConfig{ID: "cfg-healthy", BotID: "bot-healthy"}
|
|
|
|
session := sessionState{}
|
|
healthy, err := adapter.handleDispatch(context.Background(), cfg, func(context.Context, channel.ChannelConfig, channel.InboundMessage) error {
|
|
return nil
|
|
}, "READY", []byte(`{"session_id":"session-1"}`), &session)
|
|
if err != nil {
|
|
t.Fatalf("handle ready: %v", err)
|
|
}
|
|
if !healthy {
|
|
t.Fatal("expected READY to mark session healthy")
|
|
}
|
|
|
|
healthy, err = adapter.handleDispatch(context.Background(), cfg, func(context.Context, channel.ChannelConfig, channel.InboundMessage) error {
|
|
return nil
|
|
}, "RESUMED", []byte(`{}`), &session)
|
|
if err != nil {
|
|
t.Fatalf("handle resumed: %v", err)
|
|
}
|
|
if !healthy {
|
|
t.Fatal("expected RESUMED to mark session healthy")
|
|
}
|
|
}
|
|
|
|
func TestNextReconnectDelayResetsAfterHealthySession(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
backoffs := []time.Duration{time.Second, 2 * time.Second, 5 * time.Second}
|
|
delay, attempt := nextReconnectDelay(backoffs, 2, true)
|
|
|
|
if delay != time.Second {
|
|
t.Fatalf("unexpected delay: %v", delay)
|
|
}
|
|
if attempt != 1 {
|
|
t.Fatalf("unexpected next attempt: %d", attempt)
|
|
}
|
|
}
|
|
|
|
func TestHandleGatewayClose_IntentCodesRequireReconnect(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := NewQQAdapter(nil)
|
|
session := sessionState{
|
|
SessionID: "session-1",
|
|
LastSeq: 42,
|
|
IntentLevel: 0,
|
|
}
|
|
|
|
healthy, err := adapter.handleGatewayClose(
|
|
"cfg-intent",
|
|
&qqClient{},
|
|
&session,
|
|
&websocket.CloseError{Code: 4914},
|
|
true,
|
|
)
|
|
if !healthy {
|
|
t.Fatal("expected healthy flag to be preserved")
|
|
}
|
|
if err == nil {
|
|
t.Fatal("expected reconnect error")
|
|
}
|
|
if !strings.Contains(err.Error(), "intent code 4914") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if session.SessionID != "" || session.LastSeq != 0 {
|
|
t.Fatalf("session should be reset, got id=%q seq=%d", session.SessionID, session.LastSeq)
|
|
}
|
|
if session.IntentLevel != 1 {
|
|
t.Fatalf("expected intent fallback level 1, got %d", session.IntentLevel)
|
|
}
|
|
|
|
saved := adapter.loadSession("cfg-intent")
|
|
if saved.SessionID != "" || saved.LastSeq != 0 {
|
|
t.Fatalf("saved session should be reset, got id=%q seq=%d", saved.SessionID, saved.LastSeq)
|
|
}
|
|
if saved.IntentLevel != session.IntentLevel {
|
|
t.Fatalf("unexpected intent level: %d", saved.IntentLevel)
|
|
}
|
|
}
|
|
|
|
func TestAdjustSessionAfterIntentCloseCapsIntentLevel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
adapter := NewQQAdapter(nil)
|
|
session := sessionState{
|
|
SessionID: "session-2",
|
|
LastSeq: 99,
|
|
IntentLevel: len(qqIntentLevels) - 1,
|
|
}
|
|
|
|
adapter.adjustSessionAfterIntentClose("cfg-intent-cap", &session)
|
|
|
|
if session.SessionID != "" || session.LastSeq != 0 {
|
|
t.Fatalf("session should be reset, got id=%q seq=%d", session.SessionID, session.LastSeq)
|
|
}
|
|
if session.IntentLevel != len(qqIntentLevels)-1 {
|
|
t.Fatalf("expected capped intent level %d, got %d", len(qqIntentLevels)-1, session.IntentLevel)
|
|
}
|
|
|
|
saved := adapter.loadSession("cfg-intent-cap")
|
|
if saved.IntentLevel != len(qqIntentLevels)-1 {
|
|
t.Fatalf("unexpected saved intent level: %d", saved.IntentLevel)
|
|
}
|
|
}
|