// Regression for #102: a group invite makes QQ emit a content-less c2c push // (msgType=166/0, body.richText.elems=[]) right after the invite card. That // phantom used to surface to OneBot clients as a confusing "[空消息]". // parseMsgPush must drop a message that decodes to zero elements *when its body // is genuinely empty*, while still keeping (and warning about) a message that // carried content we merely failed to decode. import { getLogLevel, setLogLevel, subscribeLogs, type LogEntry } from '@snowluma/common/logger'; import type { PacketInfo } from '@snowluma/common/protocol-types'; import { protobuf_encode } from '@snowluma/proton'; import type { FileExtra, PushMsg, PushMsgBody } from '@snowluma/proto-defs/message'; import { afterEach, describe, expect, it } from 'vitest'; import type { IdentityService } from '../../src/identity-service'; import { parseMsgPush } from '../../src/msg-push'; const identity = { findFriend: () => undefined } as unknown as IdentityService; const previousLogLevel = getLogLevel(); afterEach(() => { setLogLevel(previousLogLevel); }); 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 }), }; } function traceMessages(entries: LogEntry[]): string[] { return entries.filter((entry) => entry.level === 'trace').map((entry) => entry.message); } describe('parseMsgPush — empty message drop (#102)', () => { it('records an invalid push context before returning no events', () => { const entries: LogEntry[] = []; setLogLevel('trace'); const unsubscribe = subscribeLogs((entry) => entries.push(entry)); try { const pkt = pushPacket({ responseHead: { fromUin: 10001, fromUid: 'u_x' }, }); expect(parseMsgPush(pkt, identity)).toEqual([]); expect(traceMessages(entries)).toContain( 'packet_branch serviceCmd="trpc.msg.olpush.OlPushService.MsgPush" seqId=0 branch=push_context_invalid', ); } finally { unsubscribe(); } }); it('records the precise branch when dropping a genuinely empty message', () => { const entries: LogEntry[] = []; setLogLevel('trace'); const unsubscribe = subscribeLogs((entry) => entries.push(entry)); try { const out = parseMsgPush( pushPacket({ responseHead: { fromUin: 10001, fromUid: 'u_x' }, contentHead: { msgType: 166, subType: 0, sequence: 59962, timestamp: 1781540572, msgId: 1963990184, }, body: { richText: { elems: [] } }, }), identity, ); expect(out).toEqual([]); expect(traceMessages(entries)).toContain( 'packet_branch serviceCmd="trpc.msg.olpush.OlPushService.MsgPush" seqId=0 branch=empty_message msgType=166 subType=0 messageSeq=59962', ); } finally { unsubscribe(); } }); it('keeps a friend message that actually has content', () => { const out = parseMsgPush( pushPacket({ responseHead: { fromUin: 10001, fromUid: 'u_x' }, contentHead: { msgType: 166, subType: 0, sequence: 32743, ntMsgSeq: 63214, timestamp: 1781540572, msgId: 1, }, body: { richText: { elems: [{ text: { str: 'hi' } }] } }, }), identity, ); expect(out).toHaveLength(1); expect(out[0]).toMatchObject({ kind: 'friend_message', senderUid: 'u_x', msgSeq: 32743, ntMsgSeq: 63214, clientSeq: 32743, elements: [{ type: 'text', text: 'hi' }], }); }); it('drops an offline-file download receipt instead of a friend file message (#442)', () => { const extra = protobuf_encode({ file: { fileUuid: 'fid-sent-by-bot', fileName: 'doc.bin', fileSize: 6119608n, subcmd: 1, downloadFlag: 2, fileHash: 'abc', }, }); const out = parseMsgPush( pushPacket({ responseHead: { fromUin: 10001, fromUid: 'u_x' }, contentHead: { msgType: 529, subType: 0, sequence: 1, timestamp: 1, msgId: 1 }, body: { msgContent: extra }, }), identity, ); expect(out).toEqual([]); }); it('keeps a real incoming c2c file (#442)', () => { const extra = protobuf_encode({ file: { fileType: 1, fileUuid: 'fid-from-user', fileName: 'doc.bin', fileSize: 10n, subcmd: 1, }, }); const out = parseMsgPush( pushPacket({ responseHead: { fromUin: 10001, fromUid: 'u_x' }, contentHead: { msgType: 529, subType: 0, sequence: 2, timestamp: 1, msgId: 2 }, body: { msgContent: extra }, }), identity, ); expect(out).toHaveLength(1); expect(out[0]).toMatchObject({ kind: 'friend_message', elements: [{ type: 'file', fileId: 'fid-from-user', fileName: 'doc.bin' }], }); }); it.each(['msgContent', 'richText'] as const)( 'drops a subcommand receipt in %s without a download flag (#477)', (location) => { const entries: LogEntry[] = []; setLogLevel('trace'); const unsubscribe = subscribeLogs((entry) => entries.push(entry)); const file = { fileType: 1, subcmd: 2, fileUuid: 'already-sent', fileName: 'test.json', fileSize: 42n, }; try { const out = parseMsgPush( pushPacket({ responseHead: { fromUin: 10001, fromUid: 'u_x' }, contentHead: { msgType: 529, subType: 4, c2cCmd: 4, sequence: 10, timestamp: 1, msgId: 1, }, body: location === 'msgContent' ? { msgContent: protobuf_encode({ file }) } : { richText: { notOnlineFile: file } }, }), identity, ); expect(out).toEqual([]); expect( traceMessages(entries).some((message) => message.includes('branch=offline_file_receipt'), ), ).toBe(true); } finally { unsubscribe(); } }, ); it('records the precise branch for a C2C control push with content', () => { // 166 + c2c_cmd ∈ {1,73,75,129,131,133,135,192} is a control/system signal // QQ NT routes via OnRecvSysMsg, never a bubble — drop regardless of body. const entries: LogEntry[] = []; setLogLevel('trace'); const unsubscribe = subscribeLogs((entry) => entries.push(entry)); try { const out = parseMsgPush( pushPacket({ responseHead: { fromUin: 10001, fromUid: 'u_x' }, contentHead: { msgType: 166, subType: 0, c2cCmd: 75, sequence: 200, timestamp: 1781540572, msgId: 3, }, body: { richText: { elems: [{ text: { str: 'noise' } }] } }, }), identity, ); expect(out).toEqual([]); expect(traceMessages(entries)).toContain( 'packet_branch serviceCmd="trpc.msg.olpush.OlPushService.MsgPush" seqId=0 branch=c2c_control_push msgType=166 subType=0 c2cCmd=75 messageSeq=200', ); } finally { unsubscribe(); } }); it('keeps a normal 166 message whose c2cCmd is not a control command', () => { const out = parseMsgPush( pushPacket({ responseHead: { fromUin: 10001, fromUid: 'u_x' }, contentHead: { msgType: 166, subType: 0, c2cCmd: 0, sequence: 201, timestamp: 1781540572, msgId: 4, }, body: { richText: { elems: [{ text: { str: 'hi' } }] } }, }), identity, ); expect(out).toHaveLength(1); expect(out[0]).toMatchObject({ kind: 'friend_message', elements: [{ type: 'text', text: 'hi' }], }); }); it('keeps an empty-decoded message whose body still carried content (missing decoder)', () => { // A commonElem service type the rich-body decoder ignores → 0 elements, but // the body was not empty, so the message must survive (and be warned about), // not be silently dropped. const out = parseMsgPush( pushPacket({ responseHead: { fromUin: 10001, fromUid: 'u_x' }, contentHead: { msgType: 166, subType: 0, sequence: 101, timestamp: 1781540572, msgId: 2, }, body: { richText: { elems: [{ commonElem: { serviceType: 999, businessType: 0 } }] }, }, }), identity, ); expect(out).toHaveLength(1); expect(out[0]).toMatchObject({ kind: 'friend_message', elements: [] }); }); });