Files
AskaEth 093f38d986
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/services/channelbridge(纯替换)
- 只翻注释/文档字符串:剥掉注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态)
- 术语对齐既有译法(owner→主人、persona→角色、engage→参与度判定、quiet hours→安静时段、backoff→退避)
- 复核:go build 绿 + channelbridge 包测试绿;gofmt 干净
2026-10-04 13:33:06 +08:00

172 lines
6.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}
})
}