// Regression for #137: inviting an official robot (e.g. 2854207029) into a // group makes QQ push the `group_member_increase` notice TWICE, so SnowLuma // reported two `notice.group_increase` while a normal member produced one. // QQ NT dedups system messages in `sys_msg_mgr.cc::ProcessRecvSysMsg` by a // global key whose per-message discriminators are msg_seq (contentHead field 5 // = head.sequence) and random (contentHead field 4 = head.msgId); kernel-based // bots see events only after that dedup. SnowLuma reads the raw OlPush, so // parseMsgPush must replicate the drop for routes in QQ NT's static system- // message table, but only when given a dedup tracker (the live path). Ordinary // chat routes remain outside that table even when their outer msgType matches. import { getLogLevel, setLogLevel, subscribeLogs, type LogEntry } from '@snowluma/common/logger'; import { describe, expect, it } from 'vitest'; import type { PacketInfo } from '@snowluma/common/protocol-types'; import { protobuf_encode } from '@snowluma/proton'; import type { PushMsg, PushMsgBody } from '@snowluma/proto-defs/message'; import type { GroupChange, OnlineDeviceNotify } from '@snowluma/proto-defs/notify'; import { IdentityService } from '../../src/identity-service'; import { deriveSysMsgDedupIdentity, parseMsgPush, SysMsgDedup, type SysMsgDedupIdentity, } from '../../src/msg-push'; const identity = IdentityService.memory('2000000001'); function pushPacket(message: PushMsgBody): PacketInfo { return { pid: 0, uin: '2000000001', serviceCmd: 'trpc.msg.olpush.OlPushService.MsgPush', seqId: 0, retCode: 0, fromClient: false, body: protobuf_encode({ message }), }; } // A type-33 group member-increase push (the #137 shape). `memberUid` is a // numeric string so Identity.findUinByUid returns it without a map lookup. function memberIncreasePush(opts: { groupId: number; memberUin: number; sequence: number; msgId: number; }): PacketInfo { return pushPacket({ responseHead: { fromUin: opts.groupId, fromUid: '' }, contentHead: { msgType: 33, subType: 0, sequence: opts.sequence, timestamp: 1781540572, msgId: opts.msgId, }, body: { msgContent: protobuf_encode({ groupUin: opts.groupId, memberUid: String(opts.memberUin), }), }, }); } function c2cPacket(msgType: number, subType: number): PacketInfo { return pushPacket({ responseHead: { fromUin: 10001, fromUid: 'u_x' }, contentHead: { msgType, subType, sequence: 900, timestamp: 1781540572, msgId: 7 }, body: { richText: { elems: [{ text: { str: 'hi' } }] } }, }); } function onlineDevicesPush(deviceName: string): PacketInfo { return pushPacket({ responseHead: { fromUin: 2000000001, fromUid: '' }, contentHead: { msgType: 528, subType: 349, sequence: 800, timestamp: 1781540572, msgId: 555, }, body: { msgContent: protobuf_encode({ devices: [ { appId: 537242075, instanceId: 202, clientType: 1, platform: 3, deviceName }, ], }), }, }); } function dedupIdentity(overrides: Partial = {}): SysMsgDedupIdentity { return { peerUid: '700', chatType: 2, sequence: 500, random: 12345, ...overrides, }; } function deriveIdentity(opts: { msgType: number; subType: number; sequence?: number; msgId?: number; fromUin?: number; fromUid?: string; }) { return deriveSysMsgDedupIdentity({ head: { msgType: opts.msgType, subType: opts.subType, c2cCmd: 0, sequence: opts.sequence ?? 500, ntMsgSeq: 0, timestamp: 1781540572, msgId: opts.msgId ?? 12345, }, fromUin: opts.fromUin ?? 700, fromUid: opts.fromUid ?? 'u_peer', }); } describe('deriveSysMsgDedupIdentity', () => { it.each([ [528, 290], [166, 75], [166, 129], [166, 131], [166, 133], [166, 135], [167, 133], ])('maps native C2C route %d/%d to fromUid and chatType 1', (msgType, subType) => { expect(deriveIdentity({ msgType, subType, fromUin: 700, fromUid: 'u_peer' })).toEqual({ peerUid: 'u_peer', chatType: 1, sequence: 500, random: 12345, }); }); it.each([ [732, 12], [732, 16], [732, 20], [33, 0], [34, 0], [85, 0], ])('maps native group route %d/%d to decimal fromUin and chatType 2', (msgType, subType) => { expect(deriveIdentity({ msgType, subType, fromUin: 700, fromUid: 'ignored' })).toEqual({ peerUid: '700', chatType: 2, sequence: 500, random: 12345, }); }); it('[#266] fails open for an outer package pair absent from QQ NT’s static table', () => { expect(deriveIdentity({ msgType: 528, subType: 99 })).toBeNull(); }); it('fails open when the native sequence, random, or route-specific peer is missing', () => { expect(deriveIdentity({ msgType: 33, subType: 0, sequence: 0 })).toBeNull(); expect(deriveIdentity({ msgType: 33, subType: 0, msgId: 0 })).toBeNull(); expect(deriveIdentity({ msgType: 33, subType: 0, fromUin: 0 })).toBeNull(); expect(deriveIdentity({ msgType: 528, subType: 290, fromUid: '' })).toBeNull(); }); }); describe('SysMsgDedup', () => { it('flags the second push with the same native global key', () => { const d = new SysMsgDedup(); expect(d.seenDuplicate(dedupIdentity())).toBe(false); expect(d.seenDuplicate(dedupIdentity())).toBe(true); }); it('treats a different sequence, random, peer, or chat type as distinct', () => { const d = new SysMsgDedup(); expect(d.seenDuplicate(dedupIdentity())).toBe(false); expect(d.seenDuplicate(dedupIdentity({ sequence: 501 }))).toBe(false); expect(d.seenDuplicate(dedupIdentity({ random: 99999 }))).toBe(false); expect(d.seenDuplicate(dedupIdentity({ peerUid: '701' }))).toBe(false); expect(d.seenDuplicate(dedupIdentity({ chatType: 1 }))).toBe(false); }); it('[#266] dedups different listed outer routes that derive the same native identity', () => { const d = new SysMsgDedup(); const first = deriveIdentity({ msgType: 166, subType: 133 }); const alternateRoute = deriveIdentity({ msgType: 167, subType: 133 }); expect(first).not.toBeNull(); expect(alternateRoute).toEqual(first); expect(d.seenDuplicate(first!)).toBe(false); expect(d.seenDuplicate(alternateRoute!)).toBe(true); }); it('[#266] does not record an unlisted outer route', () => { const d = new SysMsgDedup(); const unlisted = deriveIdentity({ msgType: 528, subType: 99 }); const listed = deriveIdentity({ msgType: 528, subType: 290 }); expect(unlisted).toBeNull(); expect(listed).not.toBeNull(); expect(d.seenDuplicate(listed!)).toBe(false); }); it('never dedups a manually supplied identity without a server discriminator or peer', () => { const d = new SysMsgDedup(); expect(d.seenDuplicate(dedupIdentity({ sequence: 0 }))).toBe(false); expect(d.seenDuplicate(dedupIdentity({ sequence: 0 }))).toBe(false); expect(d.seenDuplicate(dedupIdentity({ random: 0 }))).toBe(false); expect(d.seenDuplicate(dedupIdentity({ peerUid: '' }))).toBe(false); }); it('evicts the oldest key once capacity is exceeded (bounded memory)', () => { const d = new SysMsgDedup(2); expect(d.seenDuplicate(dedupIdentity({ sequence: 1, random: 1 }))).toBe(false); expect(d.seenDuplicate(dedupIdentity({ sequence: 2, random: 1 }))).toBe(false); expect(d.seenDuplicate(dedupIdentity({ sequence: 3, random: 1 }))).toBe(false); expect(d.seenDuplicate(dedupIdentity({ sequence: 1, random: 1 }))).toBe(false); expect(d.seenDuplicate(dedupIdentity({ sequence: 3, random: 1 }))).toBe(true); }); }); describe('parseMsgPush — system-push dedup (#137)', () => { it('drops the second identical official-robot member-increase push', () => { const dedup = new SysMsgDedup(); const push = () => memberIncreasePush({ groupId: 700, memberUin: 2854207029, sequence: 800, msgId: 555 }); const first = parseMsgPush(push(), identity, dedup); expect(first).toHaveLength(1); expect(first[0]).toMatchObject({ kind: 'group_member_join', groupId: 700, userUin: 2854207029, }); const second = parseMsgPush(push(), identity, dedup); expect(second).toEqual([]); }); it('traces the actual duplicate branch before returning zero events', () => { const previousLevel = getLogLevel(); const entries: LogEntry[] = []; setLogLevel('trace'); const unsubscribe = subscribeLogs((entry) => entries.push(entry)); try { const dedup = new SysMsgDedup(); const push = () => memberIncreasePush({ groupId: 700, memberUin: 2854207029, sequence: 800, msgId: 555, }); expect(parseMsgPush(push(), identity, dedup)).toHaveLength(1); expect(parseMsgPush(push(), identity, dedup)).toEqual([]); expect( entries.filter((entry) => entry.scope === 'MsgPush' && entry.level === 'trace'), ).toEqual([ expect.objectContaining({ message: 'packet_branch serviceCmd="trpc.msg.olpush.OlPushService.MsgPush" seqId=0 branch=duplicate_system_push peer="700" chatType=2 messageSeq=800 messageRandom=555', }), ]); } finally { unsubscribe(); setLogLevel(previousLevel); } }); it('keeps both pushes when no dedup tracker is supplied (forward re-parse path)', () => { const push = () => memberIncreasePush({ groupId: 700, memberUin: 2854207029, sequence: 800, msgId: 555 }); expect(parseMsgPush(push(), identity)).toHaveLength(1); expect(parseMsgPush(push(), identity)).toHaveLength(1); }); it('keeps distinct member-increase events (different members → different seq/msgId)', () => { const dedup = new SysMsgDedup(); const a = parseMsgPush( memberIncreasePush({ groupId: 700, memberUin: 111, sequence: 800, msgId: 555 }), identity, dedup, ); const b = parseMsgPush( memberIncreasePush({ groupId: 700, memberUin: 222, sequence: 801, msgId: 556 }), identity, dedup, ); expect(a).toHaveLength(1); expect(b).toHaveLength(1); expect(b[0]).toMatchObject({ userUin: 222 }); }); it('[#266] dedups a listed C2C route before its outer type is decoded as a chat message', () => { const dedup = new SysMsgDedup(); expect(parseMsgPush(c2cPacket(166, 133), identity, dedup)).toHaveLength(1); expect(parseMsgPush(c2cPacket(166, 133), identity, dedup)).toEqual([]); }); it('[#266] records a listed undecoded route before a matching listed route is dispatched', () => { const dedup = new SysMsgDedup(); expect(parseMsgPush(c2cPacket(167, 133), identity, dedup)).toEqual([]); expect(parseMsgPush(c2cPacket(166, 133), identity, dedup)).toEqual([]); }); it('does not dedup ordinary chat messages outside the native system-message table', () => { const dedup = new SysMsgDedup(); expect(parseMsgPush(c2cPacket(166, 0), identity, dedup)).toHaveLength(1); expect(parseMsgPush(c2cPacket(166, 0), identity, dedup)).toHaveLength(1); }); it('does not dedup replacement state snapshots with the same server identity', () => { const dedup = new SysMsgDedup(); expect(parseMsgPush(onlineDevicesPush('DESKTOP-A'), identity, dedup)).toMatchObject([ { kind: 'online_devices_changed', devices: [{ deviceName: 'DESKTOP-A' }] }, ]); expect(parseMsgPush(onlineDevicesPush('DESKTOP-B'), identity, dedup)).toMatchObject([ { kind: 'online_devices_changed', devices: [{ deviceName: 'DESKTOP-B' }] }, ]); }); });