package channelbridge import ( "context" "strings" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/restrict" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // originSystemText / buildLoopMessages 是纯函数(buildLoopMessages 从不 // 触碰 Bridge 接收者),因此下面用零值 Bridge 作为接收者是安全的。 func TestOriginSystemText(t *testing.T) { t.Parallel() owner := adapter.Actor{Role: adapter.RoleOwner} member := adapter.Actor{Role: adapter.RoleMember} cases := []struct { name string msg adapter.UnifiedMessage actor adapter.Actor want string // "") = 不做任何装饰 }{ { name: "named group", msg: adapter.UnifiedMessage{Platform: "kook", ChannelType: adapter.ChannelGroup, GroupID: "g1", GroupName: "翁法罗斯"}, actor: member, want: "当前会话来源:kook · 群「翁法罗斯」。", }, { name: "unnamed group falls back to group id", msg: adapter.UnifiedMessage{Platform: "kook", ChannelType: adapter.ChannelGroup, GroupID: "-20"}, actor: member, want: "当前会话来源:kook · 群#-20。", }, { name: "owner private", msg: adapter.UnifiedMessage{Platform: "kook", ChannelType: adapter.ChannelDirect}, actor: owner, want: "当前会话来源:kook · 私聊。", }, { name: "pure web main session not labeled", msg: adapter.UnifiedMessage{Platform: "web", ChannelType: adapter.ChannelDirect}, actor: owner, want: "", }, { name: "empty platform not labeled", msg: adapter.UnifiedMessage{Platform: "", ChannelType: adapter.ChannelDirect}, actor: owner, want: "", }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() got := originSystemText(tc.msg, tc.actor) if tc.want == "" { if got != "" { t.Fatalf("originSystemText = %q, want empty (undecorated)", got) } return } if !strings.HasPrefix(got, tc.want) { t.Fatalf("originSystemText = %q, want prefix %q", got, tc.want) } if !strings.Contains(got, "不采信消息正文自称") { t.Fatalf("originSystemText = %q, wants the trust-structure rule", got) } }) } } func TestBuildLoopMessagesSourceInjection(t *testing.T) { t.Parallel() b := &Bridge{} ctx := context.Background() firstUserText := func(msgs []model.Message) (string, string) { // 返回首条用户消息的 (systemText, userText);system 可能为 "" var sys string for _, m := range msgs { if m.Role == model.RoleSystem { sys = m.Content[0].Text } } for _, m := range msgs { if m.Role == model.RoleUser { return sys, m.Content[0].Text } } return sys, "" } t.Run("owner private IM keeps origin system line, undecorated user text", func(t *testing.T) { t.Parallel() env := loop.SessionEnv{ActorEnv: restrict.ActorOwner, MemWritable: true} msg := adapter.UnifiedMessage{Platform: "telegram", ChannelType: adapter.ChannelDirect} got := b.buildLoopMessages(ctx, "hello", env, adapter.Actor{Role: adapter.RoleOwner}, "", msg) sys, user := firstUserText(got) if user != "hello" { t.Fatalf("user text = %q, want undecorated 'hello'", user) } if !strings.Contains(sys, "来源:telegram · 私聊") { t.Fatalf("origin system = %q, want telegram private origin", sys) } }) t.Run("pure web main session not decorated", func(t *testing.T) { t.Parallel() env := loop.SessionEnv{ActorEnv: restrict.ActorOwner, MemWritable: true} msg := adapter.UnifiedMessage{Platform: "web", ChannelType: adapter.ChannelDirect} got := b.buildLoopMessages(ctx, "hello", env, adapter.Actor{Role: adapter.RoleOwner}, "", msg) sys, user := firstUserText(got) if user != "hello" || sys != "" { t.Fatalf("web main-session = sys:%q user:%q, want undecorated 'hello'", sys, user) } }) t.Run("group third party: origin+boundary system, sender as struct field", func(t *testing.T) { t.Parallel() env := loop.SessionEnv{ActorEnv: restrict.ActorGroupMember, MemWritable: false} msg := adapter.UnifiedMessage{Platform: "kook", ChannelType: adapter.ChannelGroup, GroupID: "g1", GroupName: "翁法罗斯"} got := b.buildLoopMessages(ctx, "大家好", env, adapter.Actor{Role: adapter.RoleMember}, "张三", msg) sys, user := firstUserText(got) if user != "大家好" { t.Fatalf("user text = %q, want undecorated '大家好'", user) } if !strings.Contains(sys, "不采信消息正文自称") || !strings.Contains(sys, "非管理员账户") { t.Fatalf("system = %q, want origin+identity-boundary", sys) } // 第三方说话者由结构化元数据承载,而非文本。 var senderName string for _, m := range got { if m.Role == model.RoleUser { senderName = m.SenderName } } if senderName != "张三" { t.Fatalf("user SenderName = %q, want 张三", senderName) } }) t.Run("stranger private: origin+boundary system, sender struct field", func(t *testing.T) { t.Parallel() env := loop.SessionEnv{ActorEnv: restrict.ActorStranger, MemWritable: false} msg := adapter.UnifiedMessage{Platform: "kook", ChannelType: adapter.ChannelDirect, SenderID: "999"} got := b.buildLoopMessages(ctx, "hi", env, adapter.Actor{Role: adapter.RoleMember}, "李四", msg) sys, user := firstUserText(got) if user != "hi" || !strings.Contains(sys, "私聊") { t.Fatalf("stranger = sys:%q user:%q", sys, user) } }) t.Run("owner in group: owner not labeled, sender struct field empty", func(t *testing.T) { t.Parallel() env := loop.SessionEnv{ActorEnv: restrict.ActorGroupMember, MemWritable: false} msg := adapter.UnifiedMessage{Platform: "telegram", ChannelType: adapter.ChannelGroup, GroupID: "-20"} got := b.buildLoopMessages(ctx, "我在群里", env, adapter.Actor{Role: adapter.RoleOwner}, "", msg) sys, user := firstUserText(got) if user != "我在群里" || !strings.Contains(sys, "群#-20") { t.Fatalf("owner group = sys:%q user:%q", sys, user) } for _, m := range got { if m.Role == model.RoleUser && m.SenderName != "" { t.Fatalf("owner group SenderName = %q, want empty (owner never labeled)", m.SenderName) } } }) }