Files
SnowLuma/packages/onebot/tests/backfill-reply-target.test.ts
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

351 lines
12 KiB
TypeScript

// When an incoming message quotes one SnowLuma's store doesn't have, the
// receive pipeline fetches just that message from the server (group →
// getGroupMessageBySeq, c2c → timestamp roam lookup, both rate-limited) and
// persists it under the SAME id the reply resolves to, so a consumer's get_msg
// on the quote hits. These tests cover the gating + keying of that back-fill.
import { describe, expect, it, vi } from 'vitest';
import { backfillReplyTarget } from '../src/modules/message-actions';
import {
GROUP_MESSAGE_EVENT,
PRIVATE_NT_MESSAGE_EVENT,
hashMessageIdInt32,
} from '../src/message-id';
import { createMessageIdResolver } from '../src/message-id-resolver';
const SELF = 10001;
const converterCtx = {
selfId: SELF,
imageUrlResolver: null,
mediaUrlResolver: null,
messageIdResolver: createMessageIdResolver({ resolvePrivateReplyMessageId: async () => null }),
mediaSegmentSink: null,
} as any;
function fakeStore(has = false) {
return {
findEvent: vi.fn(() => (has ? ({ message_id: 1 } as any) : null)),
storeMeta: vi.fn(),
storeEvent: vi.fn(),
findMeta: vi.fn(async () => null),
resolvePrivateReplyMessageId: vi.fn(async () => null),
storeHistoryEvent: vi.fn(async () => true),
};
}
function groupEvent(replySeq: number | null) {
const elements: any[] = replySeq != null ? [{ type: 'reply', replySeq }] : [];
elements.push({ type: 'text', text: '同意' });
return {
kind: 'group_message',
groupName: '',
time: 1,
selfUin: SELF,
groupId: 700,
senderUin: 900,
senderNick: '',
senderCard: '',
senderRole: 'member',
msgSeq: 999,
msgId: 2,
elements,
} as any;
}
function fetchedGroupMessage(msgSeq: number) {
return {
kind: 'group_message',
groupName: '',
time: 1700000000,
selfUin: SELF,
groupId: 700,
senderUin: 800,
senderNick: 'orig',
senderCard: '',
senderRole: 'member',
msgSeq,
msgId: 1,
elements: [{ type: 'text', text: 'quoted' }],
} as any;
}
function fetchedFriendMessage(msgSeq: number, senderUin: number) {
return {
kind: 'friend_message',
time: 1700000000,
selfUin: SELF,
senderUin,
peerUin: senderUin === SELF ? 448671521 : senderUin,
senderNick: 'orig',
msgSeq,
ntMsgSeq: msgSeq + 1000,
clientSeq: msgSeq,
sequenceAuthoritative: true,
msgId: 1,
elements: [{ type: 'text', text: 'quoted' }],
} as any;
}
describe('backfillReplyTarget', () => {
it('group: fetches the quoted message and stores it under the resolved reply id', async () => {
const store = fakeStore(false);
const getGroupMessageBySeq = vi.fn(async () => fetchedGroupMessage(123));
const ref = {
selfId: SELF,
converterCtx,
messageStore: store,
bridge: { apis: { message: { getGroupMessageBySeq } }, resolveUserUid: vi.fn() },
} as any;
await backfillReplyTarget(ref, groupEvent(123));
expect(getGroupMessageBySeq).toHaveBeenCalledWith(700, 123, SELF);
const targetId = hashMessageIdInt32(123, 700, GROUP_MESSAGE_EVENT);
expect(store.storeEvent).toHaveBeenCalledWith(
targetId,
true,
700,
123,
GROUP_MESSAGE_EVENT,
expect.objectContaining({ message_id: targetId, message_type: 'group' }),
);
});
it('c2c: resolves the friend uid, fetches, and stores under the peer session', async () => {
const store = fakeStore(false);
const getC2cLatestHistory = vi.fn(async () => [fetchedFriendMessage(456, 448671521)]);
const resolveUserUid = vi.fn(async () => 'u_friend');
const ref = {
selfId: SELF,
converterCtx,
messageStore: store,
bridge: { apis: { message: { getC2cLatestHistory } }, resolveUserUid },
} as any;
const event = {
kind: 'friend_message',
time: 1700000100,
selfUin: SELF,
senderUin: 448671521,
senderNick: '',
msgSeq: 999,
msgId: 2,
elements: [
{ type: 'reply', replySeq: 456 },
{ type: 'text', text: '同意' },
],
} as any;
await backfillReplyTarget(ref, event);
expect(resolveUserUid).toHaveBeenCalledWith(448671521);
expect(getC2cLatestHistory).toHaveBeenCalledWith('u_friend', 20, SELF, 1700000101);
const targetId = hashMessageIdInt32(1456, 448671521, PRIVATE_NT_MESSAGE_EVENT);
expect(store.storeHistoryEvent).toHaveBeenCalledWith(
targetId,
false,
448671521,
1456,
PRIVATE_NT_MESSAGE_EVENT,
expect.objectContaining({ message_id: targetId, message_type: 'private' }),
{
sequenceAuthoritative: true,
meta: expect.objectContaining({
sequence: 1456,
clientSequence: 456,
privateDirection: 'incoming',
sequenceAuthoritative: true,
}),
},
);
});
it('c2c: a bot self-sent quoted message is keyed under the peer (not self)', async () => {
// The fetched message was sent by us (senderUin = self) — it converts with
// user_id=self, but its canonical id still belongs to the peer conversation.
const store = fakeStore(false);
const getC2cLatestHistory = vi.fn(async () => [fetchedFriendMessage(456, SELF)]);
const ref = {
selfId: SELF,
converterCtx,
messageStore: store,
bridge: {
apis: { message: { getC2cLatestHistory } },
resolveUserUid: vi.fn(async () => 'u_friend'),
},
} as any;
const event = {
kind: 'friend_message',
time: 1700000100,
selfUin: SELF,
senderUin: 448671521,
senderNick: '',
msgSeq: 999,
msgId: 2,
elements: [{ type: 'reply', replySeq: 456, replySenderUin: SELF }],
} as any;
await backfillReplyTarget(ref, event);
const targetId = hashMessageIdInt32(1456, 448671521, PRIVATE_NT_MESSAGE_EVENT);
expect(store.storeHistoryEvent).toHaveBeenCalledWith(
targetId,
false,
448671521,
1456,
PRIVATE_NT_MESSAGE_EVENT,
expect.objectContaining({ message_id: targetId }),
{
sequenceAuthoritative: true,
meta: expect.objectContaining({ privateDirection: 'outgoing' }),
},
);
});
it('c2c: a self-sent reply echo backfills under the conversation peer', async () => {
const peerUin = 448671521;
const store = fakeStore(false);
const getC2cLatestHistory = vi.fn(async () => [fetchedFriendMessage(456, peerUin)]);
const ref = {
selfId: SELF,
converterCtx,
messageStore: store,
bridge: {
apis: { message: { getC2cLatestHistory } },
resolveUserUid: vi.fn(async () => 'u_friend'),
},
} as any;
const event = {
kind: 'friend_message',
time: 1700000100,
selfUin: SELF,
senderUin: SELF,
peerUin,
senderNick: '',
msgSeq: 999,
msgId: 2,
elements: [{ type: 'reply', replySeq: 456 }],
} as any;
await backfillReplyTarget(ref, event);
const targetId = hashMessageIdInt32(1456, peerUin, PRIVATE_NT_MESSAGE_EVENT);
expect(store.storeHistoryEvent).toHaveBeenCalledWith(
targetId,
false,
peerUin,
1456,
PRIVATE_NT_MESSAGE_EVENT,
expect.objectContaining({ message_id: targetId, message_type: 'private' }),
{
sequenceAuthoritative: true,
meta: expect.objectContaining({ privateDirection: 'incoming' }),
},
);
});
it('no-op when the quoted message is already stored (no fetch)', async () => {
const store = fakeStore(true); // findEvent hits
const getGroupMessageBySeq = vi.fn();
const ref = {
selfId: SELF,
converterCtx,
messageStore: store,
bridge: { apis: { message: { getGroupMessageBySeq } }, resolveUserUid: vi.fn() },
} as any;
await backfillReplyTarget(ref, groupEvent(123));
expect(getGroupMessageBySeq).not.toHaveBeenCalled();
expect(store.storeEvent).not.toHaveBeenCalled();
});
it('no-op when the message has no reply element', async () => {
const store = fakeStore(false);
const getGroupMessageBySeq = vi.fn();
const ref = {
selfId: SELF,
converterCtx,
messageStore: store,
bridge: { apis: { message: { getGroupMessageBySeq } }, resolveUserUid: vi.fn() },
} as any;
await backfillReplyTarget(ref, groupEvent(null));
expect(getGroupMessageBySeq).not.toHaveBeenCalled();
expect(store.storeEvent).not.toHaveBeenCalled();
});
it('reconstructs from replyElements when the server has nothing (tier 2)', async () => {
const store = fakeStore(false);
const getGroupMessageBySeq = vi.fn(async () => null);
const ref = {
selfId: SELF,
converterCtx,
messageStore: store,
bridge: { apis: { message: { getGroupMessageBySeq } }, resolveUserUid: vi.fn() },
} as any;
const event = {
kind: 'group_message',
groupName: '',
time: 1,
selfUin: SELF,
groupId: 700,
senderUin: 900,
senderNick: '',
senderCard: '',
senderRole: 'member',
msgSeq: 999,
msgId: 2,
elements: [
{
type: 'reply',
replySeq: 123,
replySenderUin: 800,
replyElements: [{ type: 'text', text: 'quoted' }],
},
],
} as any;
await backfillReplyTarget(ref, event);
const targetId = hashMessageIdInt32(123, 700, GROUP_MESSAGE_EVENT);
expect(store.storeEvent).toHaveBeenCalledOnce();
expect(store.storeEvent).toHaveBeenCalledWith(
targetId,
true,
700,
123,
GROUP_MESSAGE_EVENT,
expect.objectContaining({ message_id: targetId, message_type: 'group', user_id: 800 }),
{ sequenceAuthoritative: false },
);
});
it('stores a [引用消息] placeholder when server fetch and replyElements both miss (tier 3)', async () => {
const store = fakeStore(false);
const getGroupMessageBySeq = vi.fn(async () => null);
const ref = {
selfId: SELF,
converterCtx,
messageStore: store,
bridge: { apis: { message: { getGroupMessageBySeq } }, resolveUserUid: vi.fn() },
} as any;
await backfillReplyTarget(ref, groupEvent(123));
expect(getGroupMessageBySeq).toHaveBeenCalledOnce();
const targetId = hashMessageIdInt32(123, 700, GROUP_MESSAGE_EVENT);
expect(store.storeEvent).toHaveBeenCalledOnce();
expect(store.storeEvent).toHaveBeenCalledWith(
targetId,
true,
700,
123,
GROUP_MESSAGE_EVENT,
expect.objectContaining({ message_id: targetId, message_type: 'group' }),
{ sequenceAuthoritative: false },
);
});
});