package qq_obv11 import ( "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" ) // TestToUnifiedAddressedToMe 是 M11 的核心验收:**五种情形**(私聊 / @她 / @别人 / // @全体 / 无 @)各断言结构化字段 SelfID + MentionAll + AddressedToMe。判据必须 // 全部来自平台元数据(self_id + at 段的 qq),绝不做正文匹配。 // // 实测背景(DevDocs/audit/QQ群链路诊断-2026-10-13.md):旧判据是「Mentions 非空」, // @ 谁不论 ⇒ 群 672432767 里群员 @2116733327(@ 别人)两次都被回。下面的 // group_mention_other 就是那条反例的回归用例。 func TestToUnifiedAddressedToMe(t *testing.T) { t.Parallel() const selfID = "2116733327" tests := []struct { name string raw string wantSelfID string wantMentionAll bool wantAddressed bool wantMentions []string wantReplyTo string }{ { // 私聊:对端就是发送者,本条天然是对她说的 ⇒ true(与有没有 @ 无关)。 name: "private_no_mention", raw: `{"post_type":"message","message_type":"private","time":1700000000,"self_id":2116733327,"message_id":1,"user_id":10001,"message":[{"type":"text","data":{"text":"在吗"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`, wantSelfID: selfID, wantMentionAll: false, wantAddressed: true, }, { name: "group_mention_me", raw: `{"post_type":"message","message_type":"group","time":1700000001,"self_id":2116733327,"message_id":2,"user_id":10001,"group_id":672432767,"message":[{"type":"at","data":{"qq":"2116733327"}},{"type":"text","data":{"text":" 帮我看看"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`, wantSelfID: selfID, wantMentionAll: false, wantAddressed: true, wantMentions: []string{"2116733327"}, }, { // 反例回归:@ 的是别人 ⇒ 不得判成「在叫她」。 name: "group_mention_other", raw: `{"post_type":"message","message_type":"group","time":1700000002,"self_id":2116733327,"message_id":3,"user_id":10001,"group_id":672432767,"message":[{"type":"at","data":{"qq":"999888777"}},{"type":"text","data":{"text":" 你看这个"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`, wantSelfID: selfID, wantMentionAll: false, wantAddressed: false, wantMentions: []string{"999888777"}, }, { // @全体:design/18 §2 触发① 与 design/26 §2.4 都把它算作指向她, // 用独立的结构化标志 MentionAll 记录,AddressedToMe 同步为 true。 name: "group_mention_all", raw: `{"post_type":"message","message_type":"group","time":1700000003,"self_id":2116733327,"message_id":4,"user_id":10001,"group_id":672432767,"message":[{"type":"at","data":{"qq":"all"}},{"type":"text","data":{"text":" 全体注意"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`, wantSelfID: selfID, wantMentionAll: true, wantAddressed: true, wantMentions: []string{"all"}, }, { name: "group_no_mention", raw: `{"post_type":"message","message_type":"group","time":1700000004,"self_id":2116733327,"message_id":5,"user_id":10001,"group_id":672432767,"message":[{"type":"text","data":{"text":"今天天气不错"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`, wantSelfID: selfID, wantMentionAll: false, wantAddressed: false, }, { // CQ 码形态(老实现)同样接线:@全体在 CQ 里是 qq=all。 name: "cq_group_mention_all", raw: `{"post_type":"message","message_type":"group","time":1700000005,"self_id":2116733327,"message_id":6,"user_id":10001,"group_id":672432767,"message":"[CQ:at,qq=all]全体注意","sender":{"user_id":10001,"nickname":"甲","role":"member"}}`, wantSelfID: selfID, wantMentionAll: true, wantAddressed: true, wantMentions: []string{"all"}, }, { // CQ 码形态 @别人 ⇒ 同样不得判成在叫她。 name: "cq_group_mention_other", raw: `{"post_type":"message","message_type":"group","time":1700000006,"self_id":2116733327,"message_id":7,"user_id":10001,"group_id":672432767,"message":"[CQ:at,qq=999888777]你看","sender":{"user_id":10001,"nickname":"甲","role":"member"}}`, wantSelfID: selfID, wantMentionAll: false, wantAddressed: false, wantMentions: []string{"999888777"}, }, { // 同时 @ 她与别人 ⇒ 仍是「在叫她」。 name: "group_mention_me_and_other", raw: `{"post_type":"message","message_type":"group","time":1700000007,"self_id":2116733327,"message_id":8,"user_id":10001,"group_id":672432767,"message":[{"type":"at","data":{"qq":"999888777"}},{"type":"at","data":{"qq":"2116733327"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`, wantSelfID: selfID, wantMentionAll: false, wantAddressed: true, wantMentions: []string{"999888777", "2116733327"}, }, { // fail-closed:平台没给 self_id(=0)时判据缺失,即使有 @ 也不得判成在叫她; // SelfID 如实留空,绝不编一个 id。 name: "group_self_id_absent_fail_closed", raw: `{"post_type":"message","message_type":"group","time":1700000008,"message_id":9,"user_id":10001,"group_id":672432767,"message":[{"type":"at","data":{"qq":"2116733327"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`, wantSelfID: "", wantMentionAll: false, wantAddressed: false, wantMentions: []string{"2116733327"}, }, { // 「接她的话」的现状:回复段只给出 **被回复消息的 id**,它本身不携带「那条是不是 // 她自己发的」。本轮不据此判定 AddressedToMe(见 M11 报告的可行性结论)。 name: "group_reply_without_mention", raw: `{"post_type":"message","message_type":"group","time":1700000009,"self_id":2116733327,"message_id":10,"user_id":10001,"group_id":672432767,"message":[{"type":"reply","data":{"id":"555","text":"她上一条"}},{"type":"text","data":{"text":"那再问一句"}}],"sender":{"user_id":10001,"nickname":"甲","role":"member"}}`, wantSelfID: selfID, wantMentionAll: false, wantAddressed: false, wantReplyTo: "555", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() ev, err := parseEvent([]byte(tt.raw)) if err != nil { t.Fatalf("parseEvent error: %v", err) } msg := NewAdapter(Config{}).toUnified(ev, []byte(tt.raw)) if msg.SelfID != tt.wantSelfID { t.Errorf("SelfID = %q, want %q", msg.SelfID, tt.wantSelfID) } if msg.MentionAll != tt.wantMentionAll { t.Errorf("MentionAll = %v, want %v", msg.MentionAll, tt.wantMentionAll) } if msg.AddressedToMe != tt.wantAddressed { t.Errorf("AddressedToMe = %v, want %v (Mentions=%+v SelfID=%q)", msg.AddressedToMe, tt.wantAddressed, msg.Mentions, msg.SelfID) } // 既有字段语义与形状保持不变。 if len(msg.Mentions) != len(tt.wantMentions) { t.Fatalf("Mentions = %+v, want %v", msg.Mentions, tt.wantMentions) } for i, want := range tt.wantMentions { if msg.Mentions[i].UserID != want { t.Errorf("Mentions[%d].UserID = %q, want %q", i, msg.Mentions[i].UserID, want) } } if msg.ReplyToID != tt.wantReplyTo { t.Errorf("ReplyToID = %q, want %q", msg.ReplyToID, tt.wantReplyTo) } if tt.wantReplyTo != "" && msg.ReplyToText != "她上一条" { t.Errorf("ReplyToText = %q, want 她上一条", msg.ReplyToText) } }) } } // TestApplyAddressingUnwiredSelfID 钉住「判据缺失 ⇒ fail-closed」这条:SelfID 为空时 // 群聊一律 false,绝不因为 Mentions 非空就放行(那正是旧判据的错法)。 func TestApplyAddressingUnwiredSelfID(t *testing.T) { t.Parallel() msg := adapter.UnifiedMessage{ ChannelType: adapter.ChannelGroup, Mentions: []adapter.Mention{{UserID: "999888777"}}, } applyAddressing(&msg, "", msg.Mentions) if msg.SelfID != "" || msg.MentionAll || msg.AddressedToMe { t.Errorf("unwired self id: SelfID=%q MentionAll=%v AddressedToMe=%v, want all zero", msg.SelfID, msg.MentionAll, msg.AddressedToMe) } }