import { describe, expect, it, vi } from 'vitest'; import { createLogger, getLogLevel, runWithRequestId, setLogLevel, subscribeLogs, type LogEntry, } from '@snowluma/common/logger'; import type { BridgeInterface } from '../../core/src/bridge/bridge-interface'; import { ApiHandler } from '../src/api-handler'; import { OneBotInstance } from '../src/instance'; import { buildApiContext, type OneBotInstanceContext } from '../src/instance-context'; import { hashMessageIdInt32, PRIVATE_NT_MESSAGE_EVENT } from '../src/message-id'; import { TempSessionStore } from '../src/temp-session-store'; import type { JsonObject, OneBotConfig } from '../src/types'; const SELF_ID = 10001; const PEER_ID = 20002; const RECEIPT = { messageId: 123456, sequence: 87, clientSequence: 9, random: 123456, timestamp: 1_700_000_000, }; type Receipt = typeof RECEIPT; function makeContext(receiptOrReceipts: Receipt | Receipt[] = RECEIPT): { ctx: OneBotInstanceContext; dispatchEvent: ReturnType void>>; events: Map; } { const receipts = Array.isArray(receiptOrReceipts) ? receiptOrReceipts : [receiptOrReceipts]; let receiptIndex = 0; const nextReceipt = () => { const receipt = receipts[receiptIndex++]; if (!receipt) { throw new Error('unexpected extra private send'); } return receipt; }; const events = new Map(); const dispatchEvent = vi.fn<(event: JsonObject) => void>(); const bridge = { identity: { nickname: 'SnowLuma' }, apis: { message: { sendPrivate: vi.fn(async () => nextReceipt()), sendGroupTempMessage: vi.fn(async () => nextReceipt()), sendC2cFile: vi.fn(async () => nextReceipt()), }, groupFile: { uploadPrivate: vi.fn(async () => ({ fileId: 'uploaded-file-uuid', fileHash: 'uploaded-file-hash', })), }, forward: { upload: vi.fn(async () => 'forward-res-id'), }, }, resolveUserUid: vi.fn(async () => 'u_peer'), recallUploadedFile: vi.fn((fileId: string) => fileId === 'uploaded-file-uuid' ? { fileId, scope: 'private', userId: PEER_ID, fileName: 'inline.txt', fileSize: 3, fileMd5: new Uint8Array(16), fileSha1: new Uint8Array(20), fileHash: 'uploaded-file-hash', rememberedAt: Date.now(), } : undefined, ), } as unknown as BridgeInterface; const ctx = { uin: String(SELF_ID), selfId: SELF_ID, bridge, messageStore: { findMedia: () => null, findEvent: (messageId: number) => events.get(messageId) ?? null, findMeta: () => null, resolveReplySequence: () => null, storeEvent: ( messageId: number, _isGroup: boolean, _sessionId: number, _sequence: number, _eventName: string, event: JsonObject, ) => { events.set(messageId, event); return true; }, }, mediaStore: {}, reactionStore: {}, tempSessions: new TempSessionStore(), converterCtx: { selfId: SELF_ID, imageUrlResolver: null, mediaUrlResolver: null, messageIdResolver: null, mediaSegmentSink: null, }, config: { networks: { httpServers: [], httpClients: [], wsServers: [], wsClients: [] }, } as OneBotConfig, cacheMessageMeta: vi.fn(), dispatchEvent, } as unknown as OneBotInstanceContext; return { ctx, dispatchEvent, events }; } interface InstanceHarness { dispatch(event: JsonObject, source: 'send' | 'bridge'): Promise; emitEvent: ReturnType Promise>>; stored: Map; } function makeInstanceHarness( options: { statusCommand?: { enabled: boolean; swallow: boolean; cooldownSeconds: number; trigger: string; }; logger?: ReturnType; } = {}, ): InstanceHarness { const stored = new Map(); const emitEvent = vi.fn(async (_event: JsonObject) => {}); const instance = Object.create(OneBotInstance.prototype) as OneBotInstance; Object.assign(instance as unknown as Record, { bridge: { identity: {} }, ctx: { config: { statusCommand: options.statusCommand ?? { enabled: false, swallow: false, cooldownSeconds: 5, trigger: '#sl', }, }, }, log: options.logger ?? { success: vi.fn(), trace: vi.fn(), warn: vi.fn() }, messageStore: { findMedia: () => null, findEvent: (messageId: number) => stored.get(messageId) ?? null, storeEvent: ( messageId: number, _isGroup: boolean, _sessionId: number, _sequence: number, _eventName: string, event: JsonObject, ) => { stored.set(messageId, event); return true; }, }, networkManager: { emitEvent }, acceptingActions: true, uin: String(SELF_ID), statusCommandCooldown: new Map([['p:20002', Date.now()]]), recentSelfSentEvents: new Map(), }); const dispatch = ( instance as unknown as { dispatchEvent(event: JsonObject, source: 'send' | 'bridge'): Promise; } ).dispatchEvent.bind(instance); return { dispatch, emitEvent, stored }; } function privateSentEvent(overrides: JsonObject = {}): JsonObject { return { time: RECEIPT.timestamp, self_id: SELF_ID, post_type: 'message_sent', message_type: 'private', sub_type: 'friend', message_id: 987654, message_seq: RECEIPT.clientSequence, user_id: SELF_ID, target_id: PEER_ID, message: [{ type: 'text', data: { text: 'hello' } }], raw_message: 'hello', sender: { user_id: SELF_ID, nickname: 'SnowLuma' }, ...overrides, }; } function groupSentEvent(overrides: JsonObject = {}): JsonObject { return { ...privateSentEvent(), message_type: 'group', sub_type: 'normal', message_id: -7654321, message_seq: RECEIPT.sequence, group_id: 30003, target_id: undefined, ...overrides, }; } describe('OneBot self-sent events', () => { it('reports a client private send once across repeated confirmations and pushes', async () => { const { dispatch, emitEvent, stored } = makeInstanceHarness(); const event = privateSentEvent({ message_id: -7654321 }); await Promise.all([dispatch(event, 'bridge'), dispatch({ ...event }, 'bridge')]); await dispatch({ ...event, raw_message: 'canonical content' }, 'bridge'); expect(emitEvent).toHaveBeenCalledExactlyOnceWith(event); expect(stored.get(-7654321)?.raw_message).toBe('canonical content'); }); it('keeps private action deduplication for more than one echo', async () => { const { dispatch, emitEvent } = makeInstanceHarness(); const event = privateSentEvent(); await dispatch(event, 'send'); await dispatch({ ...event }, 'bridge'); await dispatch({ ...event }, 'bridge'); expect(emitEvent).toHaveBeenCalledOnce(); }); it('distinguishes private sends by identity and peer even when text is identical', async () => { const { dispatch, emitEvent } = makeInstanceHarness(); const event = privateSentEvent(); await dispatch(event, 'bridge'); await dispatch({ ...event, message_seq: RECEIPT.clientSequence + 1 }, 'bridge'); await dispatch({ ...event, target_id: PEER_ID + 1 }, 'bridge'); await dispatch({ ...event, message_id: -7654321 }, 'bridge'); expect(emitEvent).toHaveBeenCalledTimes(4); }); it('keeps private deduplication isolated and expires it without deleting history', async () => { vi.useFakeTimers(); try { const first = makeInstanceHarness(); const second = makeInstanceHarness(); const event = privateSentEvent(); await first.dispatch(event, 'bridge'); await second.dispatch( { ...event, self_id: SELF_ID + 1, user_id: SELF_ID + 1 }, 'bridge', ); expect(second.emitEvent).toHaveBeenCalledOnce(); await first.dispatch(event, 'bridge'); expect(first.emitEvent).toHaveBeenCalledOnce(); vi.advanceTimersByTime(120_000); await first.dispatch(event, 'bridge'); expect(first.emitEvent).toHaveBeenCalledTimes(2); expect(first.stored.has(987654)).toBe(true); } finally { vi.useRealTimers(); } }); it('reports a client group send once across repeated server pushes', async () => { const { dispatch, emitEvent, stored } = makeInstanceHarness(); const event = groupSentEvent(); await dispatch(event, 'bridge'); await dispatch({ ...event, raw_message: 'canonical echo' }, 'bridge'); await dispatch({ ...event, raw_message: 'latest canonical echo' }, 'bridge'); expect(emitEvent).toHaveBeenCalledOnce(); expect(emitEvent).toHaveBeenCalledWith(event); expect(stored.get(-7654321)?.raw_message).toBe('latest canonical echo'); }); it('reports concurrent copies of a client group send only once', async () => { const { dispatch, emitEvent } = makeInstanceHarness(); const event = groupSentEvent(); await Promise.all([ dispatch({ ...event }, 'bridge'), dispatch({ ...event }, 'bridge'), dispatch({ ...event }, 'bridge'), ]); expect(emitEvent).toHaveBeenCalledOnce(); }); it('keeps group sends with identical text but different message identities', async () => { const { dispatch, emitEvent } = makeInstanceHarness(); const event = groupSentEvent(); await dispatch(event, 'bridge'); await dispatch({ ...event, message_seq: RECEIPT.sequence + 1 }, 'bridge'); await dispatch({ ...event, group_id: 30004 }, 'bridge'); await dispatch({ ...event, message_id: -7654322 }, 'bridge'); expect(emitEvent).toHaveBeenCalledTimes(4); }); it('keeps group deduplication isolated between account instances', async () => { const first = makeInstanceHarness(); const second = makeInstanceHarness(); await first.dispatch(groupSentEvent(), 'bridge'); await second.dispatch( groupSentEvent({ self_id: SELF_ID + 1, user_id: SELF_ID + 1 }), 'bridge', ); expect(first.emitEvent).toHaveBeenCalledOnce(); expect(second.emitEvent).toHaveBeenCalledOnce(); }); it('expires group self-send identities without treating stored history as already reported', async () => { vi.useFakeTimers(); try { const { dispatch, emitEvent } = makeInstanceHarness(); const event = groupSentEvent(); await dispatch(event, 'bridge'); await dispatch({ ...event }, 'bridge'); expect(emitEvent).toHaveBeenCalledOnce(); vi.advanceTimersByTime(120_000); await dispatch({ ...event }, 'bridge'); expect(emitEvent).toHaveBeenCalledTimes(2); } finally { vi.useRealTimers(); } }); it('keeps the timestamp bound when a group sequence and hash are reused', async () => { const { dispatch, emitEvent } = makeInstanceHarness(); const event = groupSentEvent(); await dispatch(event, 'bridge'); await dispatch({ ...event, time: RECEIPT.timestamp + 121 }, 'bridge'); expect(emitEvent).toHaveBeenCalledTimes(2); }); it('does not publish an event suppressed by the persistence recall check', async () => { const { dispatch, emitEvent } = makeInstanceHarness(); // The event's cache operation is the final authority after asynchronous // conversion: a recall may have committed while media was resolving. const event = privateSentEvent(); const original = OneBotInstance.prototype; const cache = vi .spyOn( original as unknown as { cacheMessageEvent(event: JsonObject): Promise; }, 'cacheMessageEvent', ) .mockResolvedValueOnce(false); try { await dispatch(event, 'bridge'); expect(emitEvent).not.toHaveBeenCalled(); } finally { cache.mockRestore(); } }); it('reports upload_private_file as a private self-sent message', async () => { const { ctx, dispatchEvent } = makeContext(); const handler = new ApiHandler(buildApiContext(ctx)); const response = await handler.handle('upload_private_file', { user_id: PEER_ID, file: 'base64://AQID', name: 'inline.txt', }); expect(response).toMatchObject({ status: 'ok', retcode: 0, data: { file_id: 'uploaded-file-uuid' }, }); expect(ctx.bridge.apis.groupFile.uploadPrivate).toHaveBeenCalledWith( PEER_ID, 'base64://AQID', 'inline.txt', true, false, ); expect(ctx.bridge.apis.message.sendC2cFile).toHaveBeenCalledOnce(); expect(dispatchEvent).toHaveBeenCalledOnce(); expect(dispatchEvent).toHaveBeenCalledWith( expect.objectContaining({ post_type: 'message_sent', message_type: 'private', self_id: SELF_ID, user_id: SELF_ID, target_id: PEER_ID, message_seq: RECEIPT.clientSequence, message: [ { type: 'file', data: expect.objectContaining({ file_id: 'uploaded-file-uuid', file: 'inline.txt', file_size: 3, }), }, ], }), 'send', ); }); it('fails upload_private_file when publishing the private file fails', async () => { const { ctx, dispatchEvent } = makeContext(); vi.mocked(ctx.bridge.apis.message.sendC2cFile).mockRejectedValueOnce( new Error('private file publication rejected'), ); const handler = new ApiHandler(buildApiContext(ctx)); const response = await handler.handle('upload_private_file', { user_id: PEER_ID, file: 'base64://AQID', name: 'inline.txt', }); expect(response).toMatchObject({ status: 'failed', retcode: 100, wording: 'private file publication rejected', }); expect(dispatchEvent).not.toHaveBeenCalled(); }); it('does not report upload_private_file when upload_file is disabled', async () => { const { ctx, dispatchEvent } = makeContext(); const handler = new ApiHandler(buildApiContext(ctx)); const response = await handler.handle('upload_private_file', { user_id: PEER_ID, file: 'base64://AQID', name: 'inline.txt', upload_file: false, }); expect(response).toMatchObject({ status: 'ok', retcode: 0, data: { file_id: 'uploaded-file-uuid' }, }); expect(ctx.bridge.apis.message.sendC2cFile).not.toHaveBeenCalled(); expect(dispatchEvent).not.toHaveBeenCalled(); }); it('reports a private message sent through an action', async () => { const { ctx, dispatchEvent } = makeContext(); const api = buildApiContext(ctx); const result = await api.sendPrivateMessage( PEER_ID, [{ type: 'text', data: { text: 'hello' } }], false, ); expect(result.messageId).not.toBe(0); expect(dispatchEvent).toHaveBeenCalledOnce(); expect(dispatchEvent).toHaveBeenCalledWith( expect.objectContaining({ post_type: 'message_sent', message_type: 'private', sub_type: 'friend', self_id: SELF_ID, user_id: SELF_ID, target_id: PEER_ID, message_seq: RECEIPT.clientSequence, }), 'send', ); }); it('reports storage failure without failing or repeating an already successful send', async () => { const { ctx, dispatchEvent, events } = makeContext(); vi.spyOn(ctx.messageStore, 'storeEvent').mockImplementation(() => { throw new Error('disk full'); }); const handler = new ApiHandler(buildApiContext(ctx)); const entries: LogEntry[] = []; const unsubscribe = subscribeLogs((entry) => entries.push(entry)); try { const response = await handler.handle('send_private_msg', { user_id: PEER_ID, message: 'hello', }); expect(response).toMatchObject({ status: 'ok', retcode: 0 }); expect(ctx.bridge.apis.message.sendPrivate).toHaveBeenCalledOnce(); expect(events.size).toBe(0); expect(dispatchEvent).not.toHaveBeenCalled(); expect(entries).toContainEqual( expect.objectContaining({ level: 'warn', message: expect.stringMatching(/failed to cache self-sent message.*disk full/), }), ); } finally { unsubscribe(); } }); it('[#288] reports a private forward built through an action', async () => { const { ctx, dispatchEvent } = makeContext(); const api = buildApiContext(ctx); const result = await api.sendPrivateForwardMsg(PEER_ID, [ { type: 'node', data: { user_id: SELF_ID, nickname: 'SnowLuma', content: [{ type: 'text', data: { text: 'forwarded' } }], }, }, ]); expect(result.messageId).not.toBe(0); expect(dispatchEvent).toHaveBeenCalledOnce(); expect(dispatchEvent).toHaveBeenCalledWith( expect.objectContaining({ post_type: 'message_sent', message_type: 'private', self_id: SELF_ID, user_id: SELF_ID, target_id: PEER_ID, message_seq: RECEIPT.clientSequence, message: [ expect.objectContaining({ type: 'forward', }), ], }), 'send', ); }); it('[#288] reports a cached message forwarded to a friend through an action', async () => { const { ctx, dispatchEvent, events } = makeContext(); const sourceMessageId = 55; events.set(sourceMessageId, { message_id: sourceMessageId, message_type: 'private', user_id: PEER_ID, message: [{ type: 'text', data: { text: 'forward me' } }], }); const api = buildApiContext(ctx); const result = await api.forwardSingleMsg(sourceMessageId, { userId: PEER_ID }); expect(result.messageId).not.toBe(0); expect(dispatchEvent).toHaveBeenCalledOnce(); expect(dispatchEvent).toHaveBeenCalledWith( expect.objectContaining({ post_type: 'message_sent', message_type: 'private', self_id: SELF_ID, user_id: SELF_ID, target_id: PEER_ID, message_seq: RECEIPT.clientSequence, message: [{ type: 'text', data: { text: 'forward me' } }], }), 'send', ); }); it('[#288] does not report a private forward without a reliable sequence', async () => { const { ctx, dispatchEvent, events } = makeContext({ ...RECEIPT, sequence: 0 }); const sourceMessageId = 56; events.set(sourceMessageId, { message_id: sourceMessageId, message_type: 'private', user_id: PEER_ID, message: [{ type: 'text', data: { text: 'forward me' } }], }); const api = buildApiContext(ctx); await api.forwardSingleMsg(sourceMessageId, { userId: PEER_ID }); expect(dispatchEvent).not.toHaveBeenCalled(); }); it('reports a private file sent through an action', async () => { const { ctx, dispatchEvent } = makeContext(); const api = buildApiContext(ctx); const result = await api.sendPrivateMessage( PEER_ID, [ { type: 'file', data: { file_id: 'file-uuid', name: 'report.txt', size: 128, md5: '00112233445566778899aabbccddeeff', }, }, ], false, ); expect(result.messageId).not.toBe(0); expect(dispatchEvent).toHaveBeenCalledOnce(); expect(dispatchEvent).toHaveBeenCalledWith( expect.objectContaining({ post_type: 'message_sent', message_type: 'private', self_id: SELF_ID, user_id: SELF_ID, target_id: PEER_ID, message: [ expect.objectContaining({ type: 'file', data: expect.objectContaining({ file_id: 'file-uuid', file: 'report.txt', file_size: 128, }), }), ], }), 'send', ); }); it('reports a private file uploaded and sent through an action', async () => { const { ctx, dispatchEvent } = makeContext(); const api = buildApiContext(ctx); await api.sendPrivateMessage( PEER_ID, [ { type: 'file', data: { file: 'base64://AQID', name: 'inline.txt', }, }, ], false, ); expect(dispatchEvent).toHaveBeenCalledOnce(); expect(dispatchEvent).toHaveBeenCalledWith( expect.objectContaining({ post_type: 'message_sent', message_type: 'private', target_id: PEER_ID, message: [ expect.objectContaining({ type: 'file', data: expect.objectContaining({ file: 'inline.txt', url: 'base64://AQID', }), }), ], }), 'send', ); }); it('reports each QQ message produced by a mixed private action', async () => { const fileReceipt: Receipt = { messageId: 654321, sequence: 88, clientSequence: 10, random: 654321, timestamp: RECEIPT.timestamp + 1, }; const { ctx, dispatchEvent } = makeContext([RECEIPT, fileReceipt]); const api = buildApiContext(ctx); const result = await api.sendPrivateMessage( PEER_ID, [ { type: 'text', data: { text: 'attachment:' } }, { type: 'file', data: { file_id: 'mixed-file-uuid', name: 'mixed.txt', size: 64, md5: '00112233445566778899aabbccddeeff', }, }, ], false, ); expect(dispatchEvent).toHaveBeenCalledTimes(2); expect(dispatchEvent.mock.calls[0]?.[0]).toMatchObject({ post_type: 'message_sent', message_seq: RECEIPT.clientSequence, message: [{ type: 'text', data: { text: 'attachment:' } }], }); expect(dispatchEvent.mock.calls[1]?.[0]).toMatchObject({ post_type: 'message_sent', message_seq: fileReceipt.clientSequence, message: [ { type: 'file', data: expect.objectContaining({ file_id: 'mixed-file-uuid' }), }, ], }); expect(result.messageId).toBe( hashMessageIdInt32(fileReceipt.sequence, PEER_ID, PRIVATE_NT_MESSAGE_EVENT), ); }); it('reports each file in a multi-file private action separately', async () => { const secondReceipt: Receipt = { messageId: 654321, sequence: 88, clientSequence: 10, random: 654321, timestamp: RECEIPT.timestamp + 1, }; const { ctx, dispatchEvent } = makeContext([RECEIPT, secondReceipt]); const api = buildApiContext(ctx); await api.sendPrivateMessage( PEER_ID, [ { type: 'file', data: { file_id: 'first-file', name: 'first.txt', size: 1, md5: '00112233445566778899aabbccddeeff', }, }, { type: 'file', data: { file_id: 'second-file', name: 'second.txt', size: 2, md5: 'ffeeddccbbaa00998877665544332211', }, }, ], false, ); expect(dispatchEvent).toHaveBeenCalledTimes(2); expect( dispatchEvent.mock.calls.map( ([event]) => ((event.message as JsonObject[])[0]?.data as JsonObject)?.file_id, ), ).toEqual(['first-file', 'second-file']); }); it('does not report a later QQ echo after the action event', async () => { const { dispatch, emitEvent, stored } = makeInstanceHarness(); const sentEvent = privateSentEvent(); const qqEcho: JsonObject = { ...sentEvent, raw_message: 'canonical echo', sender: { user_id: SELF_ID, nickname: 'Server profile' }, }; await dispatch(sentEvent, 'send'); await dispatch(qqEcho, 'bridge'); expect(emitEvent).toHaveBeenCalledOnce(); expect(stored.get(987654)?.raw_message).toBe('canonical echo'); }); it('records one truthful handoff terminal for reporting, duplicate suppression, and status suppression', async () => { const previousLevel = getLogLevel(); const entries: LogEntry[] = []; const unsubscribe = subscribeLogs((entry) => entries.push(entry)); setLogLevel('trace'); try { const logger = createLogger('Event').child({ uin: SELF_ID }); const reporting = makeInstanceHarness({ logger }); await runWithRequestId(6001, () => reporting.dispatch(privateSentEvent(), 'send')); expect(reporting.emitEvent).toHaveBeenCalledOnce(); const duplicate = makeInstanceHarness({ logger }); await runWithRequestId(6002, async () => { await duplicate.dispatch(privateSentEvent(), 'send'); await duplicate.dispatch(privateSentEvent({ raw_message: 'canonical' }), 'bridge'); }); expect(duplicate.emitEvent).toHaveBeenCalledOnce(); const status = makeInstanceHarness({ logger, statusCommand: { enabled: true, swallow: true, cooldownSeconds: 5, trigger: '#sl' }, }); await runWithRequestId(6003, () => status.dispatch( privateSentEvent({ post_type: 'message', user_id: PEER_ID, target_id: undefined, raw_message: '#sl', message: [{ type: 'text', data: { text: '#sl' } }], }), 'bridge', ), ); expect(status.emitEvent).not.toHaveBeenCalled(); const terminals = entries.filter( (entry) => entry.scope === 'Event' && entry.level === 'trace' && entry.message.startsWith('event_terminal'), ); expect(terminals.filter((entry) => entry.req === 6001)).toHaveLength(1); expect(terminals.find((entry) => entry.req === 6001)?.message).toContain( 'outcome=reporting_started', ); expect(terminals.filter((entry) => entry.req === 6002)).toHaveLength(2); expect( terminals.find( (entry) => entry.req === 6002 && entry.message.includes('reason=duplicate_self_echo'), )?.message, ).toContain('outcome=suppressed'); expect(terminals.filter((entry) => entry.req === 6003)).toHaveLength(1); expect(terminals[terminals.length - 1]?.message).toContain( 'outcome=suppressed reason=status_command', ); } finally { unsubscribe(); setLogLevel(previousLevel); } }); it('does not dispatch an event for a receipt without a reliable sequence', async () => { const { ctx, dispatchEvent } = makeContext({ ...RECEIPT, sequence: 0 }); const api = buildApiContext(ctx); await api.sendPrivateMessage(PEER_ID, [{ type: 'text', data: { text: 'hello' } }], false); expect(dispatchEvent).not.toHaveBeenCalled(); }); it('keeps an earlier QQ echo and does not report the later action event', async () => { const { ctx, dispatchEvent, events } = makeContext(); const messageId = hashMessageIdInt32(RECEIPT.sequence, PEER_ID, PRIVATE_NT_MESSAGE_EVENT); const qqEcho: JsonObject = { time: RECEIPT.timestamp, self_id: SELF_ID, post_type: 'message_sent', message_type: 'private', sub_type: 'friend', message_id: messageId, message_seq: RECEIPT.clientSequence, user_id: SELF_ID, target_id: PEER_ID, message: [{ type: 'text', data: { text: 'hello' } }], raw_message: 'canonical echo', sender: { user_id: SELF_ID, nickname: 'Server profile' }, }; events.set(messageId, qqEcho); const api = buildApiContext(ctx); const result = await api.sendPrivateMessage( PEER_ID, [{ type: 'text', data: { text: 'hello' } }], false, ); expect(result.messageId).toBe(messageId); expect(dispatchEvent).not.toHaveBeenCalled(); expect(events.get(messageId)).toBe(qqEcho); }); it('does not invent a friend event for a group temporary-session send', async () => { const groupId = 30003; const { ctx, dispatchEvent } = makeContext(); ctx.tempSessions.record(PEER_ID, groupId); const api = buildApiContext(ctx); await api.sendPrivateMessage( PEER_ID, [{ type: 'text', data: { text: 'hello' } }], false, groupId, ); expect(dispatchEvent).not.toHaveBeenCalled(); }); it('does not suppress a bridge echo that was cached but never reported', async () => { const { dispatch, emitEvent, stored } = makeInstanceHarness(); const cached: JsonObject = { time: RECEIPT.timestamp, self_id: SELF_ID, post_type: 'message_sent', message_type: 'group', sub_type: 'normal', message_id: 7654321, message_seq: RECEIPT.sequence, group_id: 30003, user_id: SELF_ID, message: [], raw_message: '', sender: { user_id: SELF_ID }, }; stored.set(7654321, cached); await dispatch({ ...cached, raw_message: 'canonical echo' }, 'bridge'); expect(emitEvent).toHaveBeenCalledOnce(); expect(stored.get(7654321)?.raw_message).toBe('canonical echo'); }); it('does not suppress a hash collision from another conversation', async () => { const { dispatch, emitEvent } = makeInstanceHarness(); const sentEvent = privateSentEvent(); await dispatch(sentEvent, 'send'); await dispatch({ ...sentEvent, target_id: PEER_ID + 1 }, 'bridge'); expect(emitEvent).toHaveBeenCalledTimes(2); }); });