Files
SnowLuma/packages/protocol/tests/format.test.ts
T
TanTan fa9c73363d
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
SnowLuma v2.4.2 source snapshot
2026-11-13 09:02:00 +08:00

797 lines
29 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { JsonObject, JsonValue } from '@snowluma/common/json';
import type { QQEventVariant } from '@snowluma/protocol/events';
import {
formatEvent,
formatGroup,
formatMessageSegments,
formatReply,
formatUser,
type ReplyEventLookup,
} from '@snowluma/protocol/format';
import type { IdentityService } from '@snowluma/protocol/identity-service';
function stubIdentity(
opts: {
groups?: Record<number, { groupName?: string }>;
members?: Record<string, { card?: string; nickname?: string }>;
friends?: Record<number, { remark?: string; nickname?: string }>;
} = {},
): IdentityService {
return {
findGroup: (groupId: number) => opts.groups?.[groupId] ?? null,
findGroupMember: (groupId: number, uin: number) =>
opts.members?.[`${groupId}:${uin}`] ?? null,
findFriend: (uin: number) => opts.friends?.[uin] ?? null,
} as IdentityService;
}
function throwingIdentity(): IdentityService {
return {
findGroup: () => {
throw new Error('identity unavailable');
},
findGroupMember: () => {
throw new Error('identity unavailable');
},
findFriend: () => {
throw new Error('identity unavailable');
},
} as IdentityService;
}
function stubStore(events: Record<number, JsonObject>): ReplyEventLookup {
return {
findEvent: (messageId: number) => events[messageId] ?? null,
};
}
describe('formatGroup', () => {
it('renders [name(id)] from a cached groupName', () => {
const identity = stubIdentity({ groups: { 778899: { groupName: '雪夜茶话' } } });
expect(formatGroup(identity, 778899)).toBe('[雪夜茶话(778899)]');
});
it('trims groupName before wrapping', () => {
const identity = stubIdentity({ groups: { 778899: { groupName: ' 雪夜茶话 ' } } });
expect(formatGroup(identity, 778899)).toBe('[雪夜茶话(778899)]');
});
it('falls back to the bare id when groupName is blank after trim', () => {
const identity = stubIdentity({ groups: { 778899: { groupName: ' ' } } });
expect(formatGroup(identity, 778899)).toBe('778899');
});
it('falls back to the bare id on cache miss', () => {
expect(formatGroup(stubIdentity(), 778899)).toBe('778899');
});
it('maps 0 and other non-positive ids without a roster lookup', () => {
const identity = stubIdentity({ groups: { 0: { groupName: 'ghost' } } });
expect(formatGroup(identity, 0)).toBe('0');
expect(formatGroup(identity, -12)).toBe('-12');
expect(formatGroup(identity, Number.NaN)).toBe('0');
});
it('falls back to the bare id when findGroup throws', () => {
expect(formatGroup(throwingIdentity(), 778899)).toBe('778899');
});
});
describe('formatUser', () => {
it('prefers a group-member card over nickname', () => {
const identity = stubIdentity({
members: { '778899:20001': { card: '冬至', nickname: '小满' } },
});
expect(formatUser(identity, 778899, 20001)).toBe('[冬至(20001)]');
});
it('uses the member nickname when card is empty after trim', () => {
const identity = stubIdentity({
members: { '778899:20001': { card: ' ', nickname: '小满' } },
});
expect(formatUser(identity, 778899, 20001)).toBe('[小满(20001)]');
});
it('skips the member roster when groupId is missing or non-positive', () => {
const identity = stubIdentity({
members: { '778899:20001': { card: '冬至' } },
friends: { 20001: { remark: '谷雨', nickname: '小满' } },
});
expect(formatUser(identity, undefined, 20001)).toBe('[谷雨(20001)]');
expect(formatUser(identity, 0, 20001)).toBe('[谷雨(20001)]');
expect(formatUser(identity, -3, 20001)).toBe('[谷雨(20001)]');
});
it('falls through to the friend nickname when remark is blank', () => {
const identity = stubIdentity({
friends: { 30001: { remark: ' ', nickname: '惊蛰' } },
});
expect(formatUser(identity, undefined, 30001)).toBe('[惊蛰(30001)]');
});
it('uses a friend after a group-member miss', () => {
const identity = stubIdentity({
friends: { 20002: { remark: '白露' } },
});
expect(formatUser(identity, 778899, 20002)).toBe('[白露(20002)]');
});
it('returns the bare uin when nothing is cached', () => {
expect(formatUser(stubIdentity(), 778899, 20001)).toBe('20001');
});
it('returns the uid when uin is missing or non-positive', () => {
expect(formatUser(stubIdentity(), 778899, 0, 'u_winter')).toBe('u_winter');
expect(formatUser(stubIdentity(), 778899, -8, 'u_neg')).toBe('u_neg');
});
it('returns 0 when both uin and uid are missing', () => {
expect(formatUser(stubIdentity(), undefined, 0)).toBe('0');
expect(formatUser(stubIdentity(), undefined, -8)).toBe('0');
});
it('returns the bare uin when identity lookups throw', () => {
expect(formatUser(throwingIdentity(), 778899, 20001)).toBe('20001');
expect(formatUser(throwingIdentity(), undefined, 20001)).toBe('20001');
});
});
describe('formatMessageSegments', () => {
it('renders a raw string and truncates past 50 characters', () => {
expect(formatMessageSegments('清明')).toBe('清明');
expect(formatMessageSegments('雨'.repeat(51))).toBe(`${'雨'.repeat(50)}...`);
});
it('returns [空消息] for an empty string or a non-array payload', () => {
expect(formatMessageSegments('')).toBe('[空消息]');
expect(formatMessageSegments([])).toBe('[空消息]');
expect(formatMessageSegments(null)).toBe('[空消息]');
expect(formatMessageSegments(42)).toBe('[空消息]');
expect(formatMessageSegments({ type: 'text' })).toBe('[空消息]');
});
it('skips non-object segments and still joins the rest', () => {
expect(
formatMessageSegments([
null,
7,
'skip',
['nested'],
{ type: 'text', data: { text: '立夏' } },
{ type: 'text', data: { text: '小暑' } },
]),
).toBe('立夏 小暑');
});
it('treats a missing or non-object data field as empty segment data', () => {
expect(formatMessageSegments([{ type: 'text' }])).toBe('[空消息]');
expect(formatMessageSegments([{ type: 'at', data: null }])).toBe('@');
expect(formatMessageSegments([{ type: 'at', data: ['all'] }])).toBe('@');
expect(formatMessageSegments([{ type: 'image', data: 'pic' }])).toBe('[图片]');
});
it('renders each known segment type with its preview token', () => {
expect(
formatMessageSegments([
{ type: 'text', data: { text: 'hi' } },
{ type: 'image', data: {} },
{ type: 'image', data: { emoji_id: 'e1', summary: '捂脸' } },
{ type: 'image', data: { emoji_id: 'e2' } },
{ type: 'face', data: { id: 1 } },
{ type: 'mface', data: { text: '旺柴' } },
{ type: 'mface', data: {} },
{ type: 'at', data: { qq: 20001 } },
{ type: 'at', data: { qq: 'all' } },
{ type: 'at', data: {} },
{ type: 'reply', data: { id: 4100 } },
{ type: 'reply', data: {} },
{ type: 'record', data: {} },
{ type: 'video', data: {} },
{ type: 'file', data: { name: 'notes.txt' } },
{ type: 'file', data: { file: 'old.bin' } },
{ type: 'file', data: {} },
{ type: 'xml', data: {} },
{ type: 'markdown', data: {} },
{ type: 'inline_keyboard', data: {} },
{ type: 'forward', data: {} },
{ type: 'poke', data: {} },
{ type: 'flash_file', data: { title: '图纸.dwg' } },
{ type: 'flash_file', data: {} },
{ type: 'mystery', data: {} },
{},
]),
).toBe(
[
'hi',
'[图片]',
'[捂脸]',
'[表情]',
'[表情]',
'[旺柴]',
'[表情]',
'@20001',
'@全体成员',
'@',
'[回复:4100]',
'[回复:]',
'[语音]',
'[视频]',
'[文件:notes.txt]',
'[文件:old.bin]',
'[文件]',
'[XML]',
'[Markdown]',
'[交互按钮]',
'[聊天记录]',
'[窗口抖动]',
'[闪传文件:图纸.dwg]',
'[闪传文件]',
'[mystery]',
'[]',
].join(' '),
);
});
it('truncates file names and flash-file titles to 20 characters', () => {
const longName = 'abcdefghijABCDEFGHIJxyz';
expect(formatMessageSegments([{ type: 'file', data: { name: longName } }])).toBe(
'[文件:abcdefghijABCDEFGHIJ...]',
);
expect(formatMessageSegments([{ type: 'flash_file', data: { title: longName } }])).toBe(
'[闪传文件:abcdefghijABCDEFGHIJ...]',
);
});
it('treats an empty file name as absent even if file is also empty', () => {
expect(formatMessageSegments([{ type: 'file', data: { name: '' } }])).toBe('[文件]');
});
it('extracts json card titles from the documented ARK locations in order', () => {
const ranked = JSON.stringify({
meta: {
detail_1: { title: '小程序名' },
detail: { title: '详情名' },
news: { title: '新闻名' },
},
prompt: '提示文案',
});
expect(formatMessageSegments([{ type: 'json', data: { data: ranked } }])).toBe(
'[卡片:小程序名]',
);
expect(
formatMessageSegments([
{
type: 'json',
data: { data: JSON.stringify({ meta: { detail: { title: '图文分享' } } }) },
},
]),
).toBe('[卡片:图文分享]');
expect(
formatMessageSegments([
{
type: 'json',
data: { data: JSON.stringify({ meta: { news: { title: '头条' } } }) },
},
]),
).toBe('[卡片:头条]');
expect(
formatMessageSegments([
{
type: 'json',
data: { data: JSON.stringify({ meta: { mannonce: { title: '群公告' } } }) },
},
]),
).toBe('[卡片:群公告]');
expect(
formatMessageSegments([
{
type: 'json',
data: { data: JSON.stringify({ meta: { music: { title: '夜曲' } } }) },
},
]),
).toBe('[卡片:夜曲]');
expect(
formatMessageSegments([
{
type: 'json',
data: { data: JSON.stringify({ meta: { video: { title: '短片' } } }) },
},
]),
).toBe('[卡片:短片]');
});
it('uses the single remaining meta section title when the key is unknown', () => {
expect(
formatMessageSegments([
{
type: 'json',
data: {
data: JSON.stringify({ meta: { share_card: { title: '自定义卡片' } } }),
},
},
]),
).toBe('[卡片:自定义卡片]');
});
it('falls back to prompt, then desc, when meta has no usable title', () => {
expect(
formatMessageSegments([
{
type: 'json',
data: {
data: JSON.stringify({
meta: { left: { title: '' }, right: { title: ' ' } },
prompt: ' 请升级客户端 ',
}),
},
},
]),
).toBe('[卡片:请升级客户端]');
expect(
formatMessageSegments([
{
type: 'json',
data: { data: JSON.stringify({ desc: ' 简介文案 ' }) },
},
]),
).toBe('[卡片:简介文案]');
});
it('truncates card titles to 30 characters', () => {
const prompt = '卡'.repeat(31);
expect(
formatMessageSegments([
{
type: 'json',
data: { data: JSON.stringify({ prompt }) },
},
]),
).toBe(`[卡片:${'卡'.repeat(30)}...]`);
});
it('falls back to [JSON] when the payload has no extractable title', () => {
expect(formatMessageSegments([{ type: 'json', data: {} }])).toBe('[JSON]');
expect(formatMessageSegments([{ type: 'json', data: { data: 1 } }])).toBe('[JSON]');
expect(formatMessageSegments([{ type: 'json', data: { data: '' } }])).toBe('[JSON]');
expect(formatMessageSegments([{ type: 'json', data: { data: '{broken' } }])).toBe('[JSON]');
expect(formatMessageSegments([{ type: 'json', data: { data: 'null' } }])).toBe('[JSON]');
expect(formatMessageSegments([{ type: 'json', data: { data: '12' } }])).toBe('[JSON]');
expect(formatMessageSegments([{ type: 'json', data: { data: '[1]' } }])).toBe('[JSON]');
expect(
formatMessageSegments([
{
type: 'json',
data: { data: JSON.stringify({ meta: ['x'], prompt: '' }) },
},
]),
).toBe('[JSON]');
expect(
formatMessageSegments([
{
type: 'json',
data: {
data: JSON.stringify({ meta: { a: { title: 9 }, b: { title: true } } }),
},
},
]),
).toBe('[JSON]');
expect(
formatMessageSegments([
{
type: 'json',
data: { data: JSON.stringify({ meta: { only: ['not-object'] } }) },
},
]),
).toBe('[JSON]');
expect(
formatMessageSegments([
{
type: 'json',
data: { data: JSON.stringify({ foo: 1 }) },
},
]),
).toBe('[JSON]');
});
it('returns [消息渲染异常] when walking the segment list throws', () => {
const broken = new Proxy([] as JsonValue[], {
get(target, prop, receiver) {
if (prop === Symbol.iterator) {
throw new Error('segment exploded');
}
return Reflect.get(target, prop, receiver);
},
});
expect(formatMessageSegments(broken)).toBe('[消息渲染异常]');
});
});
describe('formatReply', () => {
const identity = stubIdentity({
groups: { 778899: { groupName: '雪夜茶话' } },
members: { '778899:20001': { card: '冬至' } },
});
it('renders [回复:0] when the reply id is missing', async () => {
expect(await formatReply(stubStore({}), identity, 0)).toBe('[回复:0]');
expect(await formatReply(stubStore({}), identity, Number.NaN)).toBe('[回复:0]');
});
it('renders [回复:<id>] when the store has no matching event', async () => {
expect(await formatReply(stubStore({}), identity, 4100)).toBe('[回复:4100]');
});
it('resolves a group reply from the identity roster', async () => {
const store = stubStore({
4100: {
message_type: 'group',
group_id: 778899,
user_id: 20001,
message: [{ type: 'text', data: { text: '霜降好' } }],
},
});
expect(await formatReply(store, identity, 4100)).toBe('[回复 [冬至(20001)]: 霜降好]');
});
it('coerces string user_id / group_id and uses the stored sender card on cache miss', async () => {
const store = stubStore({
4101: {
message_type: 'group',
group_id: '778899',
user_id: '20003',
sender: { card: '处暑', nickname: 'ignored' },
message: '晚风',
},
});
expect(await formatReply(store, stubIdentity(), 4101)).toBe('[回复 [处暑(20003)]: 晚风]');
});
it('does not treat private messages as group and ignores a whitespace-only card', async () => {
const store = stubStore({
4102: {
message_type: 'private',
group_id: 778899,
user_id: 20001,
sender: { card: ' ', nickname: '寒露' },
message: [{ type: 'text', data: { text: '私聊一句' } }],
},
});
// Fallback is `(card || nickname).trim()`: a whitespace card is truthy
// before trim, so the nickname is not used.
expect(await formatReply(store, stubIdentity(), 4102)).toBe('[回复 20001: 私聊一句]');
});
it('uses sender.nickname when card is absent', async () => {
const store = stubStore({
4102: {
message_type: 'private',
user_id: 20001,
sender: { nickname: '寒露' },
message: [{ type: 'text', data: { text: '私聊一句' } }],
},
});
expect(await formatReply(store, stubIdentity(), 4102)).toBe(
'[回复 [寒露(20001)]: 私聊一句]',
);
});
it('keeps the bare uin when sender is not a usable object', async () => {
const noSender = stubStore({
4103: {
message_type: 'private',
user_id: 20004,
sender: null,
message: 'x',
},
});
expect(await formatReply(noSender, stubIdentity(), 4103)).toBe('[回复 20004: x]');
const arraySender = stubStore({
4104: {
message_type: 'private',
user_id: 20004,
sender: ['寒露'],
message: 'y',
},
});
expect(await formatReply(arraySender, stubIdentity(), 4104)).toBe('[回复 20004: y]');
});
it('maps non-numeric user_id to 0 and skips the sender-name fallback', async () => {
const store = stubStore({
4105: {
message_type: 'private',
user_id: 'not-a-uin',
sender: { nickname: '不该出现' },
message: 'z',
},
});
expect(await formatReply(store, stubIdentity(), 4105)).toBe('[回复 0: z]');
});
it('truncates the body preview to 30 characters after segment rendering', async () => {
const store = stubStore({
4106: {
message_type: 'private',
user_id: 20004,
message: [{ type: 'text', data: { text: '正'.repeat(80) } }],
},
});
expect(await formatReply(store, stubIdentity(), 4106)).toBe(
`[回复 20004: ${'正'.repeat(30)}...]`,
);
});
it('renders referenced reply segments as tokens instead of recursing', async () => {
const store = stubStore({
4107: {
message_type: 'private',
user_id: 20004,
message: [
{ type: 'reply', data: { id: 1 } },
{ type: 'text', data: { text: '外层' } },
],
},
});
expect(await formatReply(store, stubIdentity(), 4107)).toBe('[回复 20004: [回复:1] 外层]');
});
it('returns [回复:<id>] when findEvent throws', async () => {
const store: ReplyEventLookup = {
findEvent: () => {
throw new Error('store unavailable');
},
};
expect(await formatReply(store, identity, 4100)).toBe('[回复:4100]');
});
});
describe('formatEvent', () => {
const identity = stubIdentity({
groups: { 778899: { groupName: '雪夜茶话' } },
members: {
'778899:20001': { card: '冬至' },
'778899:20002': { card: '小满' },
'778899:20003': { nickname: '谷雨' },
},
friends: {
30001: { nickname: '惊蛰' },
30002: { remark: '白露' },
},
});
it('returns null for kinds that have dedicated message-log paths', () => {
expect(formatEvent(identity, { kind: 'group_message' } as QQEventVariant)).toBeNull();
expect(formatEvent(identity, { kind: 'friend_message' } as QQEventVariant)).toBeNull();
expect(formatEvent(identity, { kind: 'temp_message' } as QQEventVariant)).toBeNull();
expect(formatEvent(identity, { kind: 'group_file_upload' } as QQEventVariant)).toBeNull();
expect(formatEvent(identity, { kind: 'friend_add' } as QQEventVariant)).toBeNull();
});
it('returns null for kinds with no notice-line renderer', () => {
expect(formatEvent(identity, { kind: 'bot_offline' } as QQEventVariant)).toBeNull();
expect(formatEvent(identity, { kind: 'ptt_trans_result' } as QQEventVariant)).toBeNull();
expect(formatEvent(identity, { kind: 'group_card_change' } as QQEventVariant)).toBeNull();
expect(formatEvent(identity, { kind: 'friend_input_status' } as QQEventVariant)).toBeNull();
});
it('renders group_recall', () => {
expect(
formatEvent(identity, {
kind: 'group_recall',
groupId: 778899,
authorUin: 20001,
operatorUin: 20002,
} as QQEventVariant),
).toBe('群撤回 [雪夜茶话(778899)] | [冬至(20001)] 被 [小满(20002)] 撤回');
});
it('renders friend_recall', () => {
expect(
formatEvent(identity, {
kind: 'friend_recall',
userUin: 30001,
} as QQEventVariant),
).toBe('私聊撤回 [惊蛰(30001)] 撤回了消息');
});
it('renders group_member_join including a uid-only joiner', () => {
expect(
formatEvent(identity, {
kind: 'group_member_join',
groupId: 778899,
userUin: 20003,
userUid: 'u_guyu',
} as QQEventVariant),
).toBe('入群 [谷雨(20003)] 加入 [雪夜茶话(778899)]');
expect(
formatEvent(identity, {
kind: 'group_member_join',
groupId: 778899,
userUin: 0,
userUid: 'u_only',
} as QQEventVariant),
).toBe('入群 u_only 加入 [雪夜茶话(778899)]');
});
it('renders group_member_leave for leave, kick, and disband', () => {
expect(
formatEvent(identity, {
kind: 'group_member_leave',
groupId: 778899,
userUin: 20001,
leaveType: 'leave',
} as QQEventVariant),
).toBe('退群 [冬至(20001)] 退出 [雪夜茶话(778899)]');
expect(
formatEvent(identity, {
kind: 'group_member_leave',
groupId: 778899,
userUin: 20001,
leaveType: 'kick',
} as QQEventVariant),
).toBe('退群 [冬至(20001)] 被踢出 [雪夜茶话(778899)]');
expect(
formatEvent(identity, {
kind: 'group_member_leave',
groupId: 778899,
userUin: 20001,
leaveType: 'disband',
} as QQEventVariant),
).toBe('退群 [冬至(20001)] 随群解散 [雪夜茶话(778899)]');
});
it('renders group_mute with the duration in seconds', () => {
expect(
formatEvent(identity, {
kind: 'group_mute',
groupId: 778899,
userUin: 20001,
duration: 600,
} as QQEventVariant),
).toBe('禁言 [雪夜茶话(778899)] | [冬至(20001)] 600秒');
});
it('renders group_admin set and unset', () => {
expect(
formatEvent(identity, {
kind: 'group_admin',
groupId: 778899,
userUin: 20002,
set: true,
} as QQEventVariant),
).toBe('管理 [雪夜茶话(778899)] | [小满(20002)] +管理员');
expect(
formatEvent(identity, {
kind: 'group_admin',
groupId: 778899,
userUin: 20002,
set: false,
} as QQEventVariant),
).toBe('管理 [雪夜茶话(778899)] | [小满(20002)] -管理员');
});
it('renders friend_poke and group_poke', () => {
expect(
formatEvent(identity, {
kind: 'friend_poke',
peerUin: 30001,
senderUin: 30002,
targetUin: 30001,
} as QQEventVariant),
).toBe('私聊戳 [惊蛰(30001)] | [白露(30002)] -> [惊蛰(30001)]');
expect(
formatEvent(identity, {
kind: 'group_poke',
groupId: 778899,
userUin: 20001,
targetUin: 20002,
} as QQEventVariant),
).toBe('群戳 [雪夜茶话(778899)] | [冬至(20001)] -> [小满(20002)]');
});
it('renders friend_request and group_invite', () => {
expect(
formatEvent(identity, {
kind: 'friend_request',
fromUin: 30002,
message: '加个好友',
} as QQEventVariant),
).toBe('好友请求 [白露(30002)]: 加个好友');
expect(
formatEvent(identity, {
kind: 'group_invite',
fromUin: 30001,
groupId: 778899,
} as QQEventVariant),
).toBe('群邀请 [惊蛰(30001)] -> [雪夜茶话(778899)]');
expect(
formatEvent(identity, {
kind: 'group_invite',
fromUin: 30001,
invitedUin: 30002,
groupId: 778899,
} as QQEventVariant),
).toBe('群邀请 [惊蛰(30001)] 邀 [白露(30002)] -> [雪夜茶话(778899)]');
});
it('renders group_essence set and unset', () => {
expect(
formatEvent(identity, {
kind: 'group_essence',
groupId: 778899,
set: true,
} as QQEventVariant),
).toBe('精华 [雪夜茶话(778899)] | +精华');
expect(
formatEvent(identity, {
kind: 'group_essence',
groupId: 778899,
set: false,
} as QQEventVariant),
).toBe('精华 [雪夜茶话(778899)] | -精华');
});
it('renders group_msg_emoji_like add and remove', () => {
expect(
formatEvent(identity, {
kind: 'group_msg_emoji_like',
groupId: 778899,
operatorUin: 20001,
operatorUid: 'u_dongzhi',
emojiId: '76',
msgSeq: 8801,
isAdd: true,
} as QQEventVariant),
).toBe('表情回应 [雪夜茶话(778899)] | [冬至(20001)] +[76] msgSeq=8801');
expect(
formatEvent(identity, {
kind: 'group_msg_emoji_like',
groupId: 778899,
operatorUin: 0,
operatorUid: 'u_dongzhi',
emojiId: 'ok',
msgSeq: 8802,
isAdd: false,
} as QQEventVariant),
).toBe('表情回应 [雪夜茶话(778899)] | u_dongzhi -[ok] msgSeq=8802');
});
it('degrades to numeric ids when the roster is empty', () => {
expect(
formatEvent(stubIdentity(), {
kind: 'group_recall',
groupId: 778899,
authorUin: 20001,
operatorUin: 20002,
} as QQEventVariant),
).toBe('群撤回 778899 | 20001 被 20002 撤回');
});
it('returns null when reading the event kind throws', () => {
const broken = {
get kind() {
throw new Error('event exploded');
},
} as unknown as QQEventVariant;
expect(formatEvent(identity, broken)).toBeNull();
});
});