Files
2026-07-13 13:00:08 +08:00

225 lines
6.7 KiB
Go

package bot
import (
"testing"
"time"
)
func TestBuildSessionKey(t *testing.T) {
tests := []struct {
name string
src SessionSource
// DM 同 chat 不同 user 应返回相同 key
wantSame bool
src2 SessionSource
}{
{
name: "dm same chat different user",
src: SessionSource{Platform: PlatformQQ, ChatType: ChatDM, ChatID: "user123", UserID: "a"},
src2: SessionSource{Platform: PlatformQQ, ChatType: ChatDM, ChatID: "user123", UserID: "b"},
wantSame: true,
},
{
name: "dm different chat",
src: SessionSource{Platform: PlatformQQ, ChatType: ChatDM, ChatID: "user123", UserID: "a"},
src2: SessionSource{Platform: PlatformQQ, ChatType: ChatDM, ChatID: "user456", UserID: "a"},
wantSame: false,
},
{
name: "direct same chat different user",
src: SessionSource{Platform: PlatformQQ, ChatType: ChatDirect, ChatID: "guild123", UserID: "a"},
src2: SessionSource{Platform: PlatformQQ, ChatType: ChatDirect, ChatID: "guild123", UserID: "b"},
wantSame: true,
},
{
name: "direct distinct from dm",
src: SessionSource{Platform: PlatformQQ, ChatType: ChatDirect, ChatID: "shared", UserID: "a"},
src2: SessionSource{Platform: PlatformQQ, ChatType: ChatDM, ChatID: "shared", UserID: "a"},
wantSame: false,
},
{
name: "group same chat different user",
src: SessionSource{Platform: PlatformFeishu, ChatType: ChatGroup, ChatID: "group1", UserID: "a"},
src2: SessionSource{Platform: PlatformFeishu, ChatType: ChatGroup, ChatID: "group1", UserID: "b"},
wantSame: false,
},
{
name: "group same user different chat",
src: SessionSource{Platform: PlatformFeishu, ChatType: ChatGroup, ChatID: "group1", UserID: "a"},
src2: SessionSource{Platform: PlatformFeishu, ChatType: ChatGroup, ChatID: "group2", UserID: "a"},
wantSame: false,
},
{
name: "thread shared",
src: SessionSource{Platform: PlatformQQ, ChatType: ChatThread, ChatID: "ch1", ThreadID: "th1", UserID: "a"},
src2: SessionSource{Platform: PlatformQQ, ChatType: ChatThread, ChatID: "ch1", ThreadID: "th1", UserID: "b"},
wantSame: true,
},
{
name: "different platform same ids",
src: SessionSource{Platform: PlatformQQ, ChatType: ChatDM, ChatID: "123", UserID: "u1"},
src2: SessionSource{Platform: PlatformFeishu, ChatType: ChatDM, ChatID: "123", UserID: "u1"},
wantSame: false,
},
{
name: "same platform different connection",
src: SessionSource{Platform: PlatformFeishu, ConnectionID: "feishu-feishu", ChatType: ChatDM, ChatID: "123", UserID: "u1"},
src2: SessionSource{Platform: PlatformFeishu, ConnectionID: "feishu-lark", ChatType: ChatDM, ChatID: "123", UserID: "u1"},
wantSame: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
k1 := BuildSessionKey(tt.src)
k2 := BuildSessionKey(tt.src2)
if tt.wantSame && k1 != k2 {
t.Errorf("want same key, got %s != %s", k1, k2)
}
if !tt.wantSame && k1 == k2 {
t.Errorf("want different keys, got %s == %s", k1, k2)
}
})
}
}
func TestIsSlashBypass(t *testing.T) {
tests := []struct {
text string
bypass bool
}{
{"/stop", true},
{"/stop extra args", true},
{"/new", true},
{"/reset", true},
{"/approve", true},
{"/deny", true},
{"/yolo", true},
{"/yolo on", true},
{"/mode yolo", true},
{"/status", true},
{"/help", true},
{"hello", false},
{"/unknown", false},
{"", false},
{" /stop", false}, // leading space means not a slash command
}
for _, tt := range tests {
got := IsSlashBypass(tt.text)
if got != tt.bypass {
t.Errorf("IsSlashBypass(%q) = %v, want %v", tt.text, got, tt.bypass)
}
}
}
func TestSessionManager_TryAcquire(t *testing.T) {
sm := NewSessionManager(100 * time.Millisecond)
msg := InboundMessage{Text: "hello", Platform: PlatformQQ, ChatType: ChatDM, ChatID: "c1", UserID: "u1"}
key := BuildSessionKey(msg.Session())
// 第一次获取成功
acquired, merged := sm.TryAcquire(key, msg)
if !acquired || merged {
t.Error("first acquire should succeed")
}
// 第二次获取应该排队
acquired, merged = sm.TryAcquire(key, InboundMessage{Text: "world"})
if acquired || !merged {
t.Error("second acquire should merge into queue")
}
// slash bypass 命令应绕过
acquired, merged = sm.TryAcquire(key, InboundMessage{Text: "/stop"})
if !acquired || merged {
t.Error("slash bypass should acquire immediately")
}
// 第一次 Release 返回排队消息
next := sm.Release(key)
if next == nil {
t.Fatal("expected queued message after first release")
}
if next.Text != "world" {
t.Errorf("merged text = %q, want %q", next.Text, "world")
}
}
func TestSessionManager_Debounce(t *testing.T) {
sm := NewSessionManager(200 * time.Millisecond)
msg := InboundMessage{Text: "first", Platform: PlatformQQ, ChatType: ChatDM, ChatID: "c1", UserID: "u1"}
key := BuildSessionKey(msg.Session())
acquired, _ := sm.TryAcquire(key, msg)
if !acquired {
t.Fatal("first acquire should succeed")
}
// 同 session 消息应合并
sm.TryAcquire(key, InboundMessage{Text: "second"})
// 在 debounce 窗口内发第三条
sm.TryAcquire(key, InboundMessage{Text: "third"})
next := sm.Release(key)
if next == nil {
t.Fatal("expected queued message after release")
}
// "second" 和 "third" 合并在队列里("first" 已作为 active 被处理)
if next.Text != "second\nthird" {
t.Errorf("merged = %q, want %q", next.Text, "second\nthird")
}
}
func TestSessionManager_ForceRelease(t *testing.T) {
sm := NewSessionManager(100 * time.Millisecond)
msg := InboundMessage{Text: "test", Platform: PlatformQQ, ChatType: ChatDM, ChatID: "c1", UserID: "u1"}
key := BuildSessionKey(msg.Session())
sm.TryAcquire(key, msg)
if !sm.IsActive(key) {
t.Error("should be active")
}
sm.ForceRelease(key)
if sm.IsActive(key) {
t.Error("should not be active after force release")
}
}
func TestHashID(t *testing.T) {
h1 := hashID("user_12345")
h2 := hashID("user_12345")
h3 := hashID("user_67890")
if h1 != h2 {
t.Error("same input should produce same hash")
}
if h1 == h3 {
t.Error("different inputs should produce different hashes")
}
if hashID("") != "" {
t.Error("empty input should produce empty hash")
}
}
func TestInboundMessage_Session(t *testing.T) {
msg := InboundMessage{
Platform: PlatformQQ,
ConnectionID: "qq-main",
Domain: "qq",
ChatType: ChatDM,
ChatID: "chat1",
UserID: "user1",
ThreadID: "thread1",
}
src := msg.Session()
if src.Platform != PlatformQQ || src.ConnectionID != "qq-main" || src.Domain != "qq" || src.ChatType != ChatDM || src.ChatID != "chat1" || src.UserID != "user1" || src.ThreadID != "thread1" {
t.Error("Session() should copy all fields")
}
}