Files
SnowLuma/packages/core/tests/history-sync-api.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

226 lines
7.3 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import type {
SsoReadedReportReq,
SsoReadedReportResp,
} from '@snowluma/proto-defs/oidb-actions/base';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
const { fetchGroupMessageRange, fetchC2cMessageRange, fetchC2cRoamMessagePage } = vi.hoisted(
() => ({
fetchGroupMessageRange: vi.fn(),
fetchC2cMessageRange: vi.fn(),
fetchC2cRoamMessagePage: vi.fn(),
}),
);
vi.mock('@snowluma/protocol/msg-push', () => ({
fetchGroupMessageRange,
fetchC2cMessageRange,
fetchC2cRoamMessagePage,
}));
vi.mock('../src/bridge/apis/history-request-gate', () => ({
HistoryRequestGate: class {
run<T>(_gap: number, operation: () => Promise<T>): Promise<T> {
return operation();
}
},
}));
import { LOGIN_HISTORY_SYNC_PROTOCOL_LIMITS, MessageApi } from '../src/bridge/apis/message';
import { mockBridge } from './actions/_helpers';
function readReportResult(response: SsoReadedReportResp) {
return {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.from(protobuf_encode<SsoReadedReportResp>(response)),
};
}
describe('MessageApi login history sync seam', () => {
beforeEach(() => {
fetchGroupMessageRange.mockReset();
fetchC2cMessageRange.mockReset();
fetchC2cRoamMessagePage.mockReset();
});
it('probes server cursors once without sending a read confirmation', async () => {
const sendRawPacket = vi.fn(async (_serviceCmd: string, _body: Uint8Array) =>
readReportResult({
groupList: [
{
groupUin: 12345n,
readSeq: 90n,
latestSeq: 120n,
},
],
c2cList: [
{
targetUin: 54321n,
uid: 'u_friend',
readSeq: 70n,
latestSeq: 80n,
lastMsgTime: 1_700_000_000n,
},
],
}),
);
const bridge = mockBridge({ sendRawPacket });
const state = await new MessageApi(bridge as any).probeHistorySyncState(
[12345],
[{ userId: 54321, uid: 'u_friend' }],
);
expect(state).toEqual({
groups: [{ groupId: 12345, readSeq: 90, latestSeq: 120 }],
privateUsers: [
{
userId: 54321,
uid: 'u_friend',
readSeq: 70,
latestSeq: 80,
lastMsgTime: 1_700_000_000,
},
],
});
expect(sendRawPacket).toHaveBeenCalledOnce();
expect(sendRawPacket.mock.calls[0]![0]).toBe('trpc.msg.msg_svc.MsgService.SsoReadedReport');
const request = protobuf_decode<SsoReadedReportReq>(sendRawPacket.mock.calls[0]![1]);
expect(request).toEqual({
groupList: [{ groupUin: 12345n, lastReadSeq: null }],
c2cList: [{ uid: 'u_friend', lastReadTime: null, lastReadSeq: null }],
});
});
it('rejects an oversized probe before sending a packet', async () => {
const bridge = mockBridge();
const groups = Array.from(
{ length: LOGIN_HISTORY_SYNC_PROTOCOL_LIMITS.maxTargetsPerKind + 1 },
(_, index) => 10_000 + index,
);
await expect(
new MessageApi(bridge as any).probeHistorySyncState(groups, []),
).rejects.toThrow(/at most 100 group targets/);
expect(bridge.sendRawPacket).not.toHaveBeenCalled();
});
it('keeps explicit read-report failures observable', async () => {
const sendRawPacket = vi.fn(async () =>
readReportResult({
groupList: [
{
resultCode: 42,
errorMessage: 'probe rejected',
groupUin: 12345n,
},
],
}),
);
const bridge = mockBridge({ sendRawPacket });
await expect(
new MessageApi(bridge as any).probeHistorySyncState([12345], []),
).rejects.toThrow('probe rejected');
expect(sendRawPacket).toHaveBeenCalledOnce();
});
it('fetches at most one bounded group history page', async () => {
fetchGroupMessageRange.mockResolvedValue([
{ kind: 'group_message', msgSeq: 101 },
{ kind: 'group_message', msgSeq: 102 },
]);
const bridge = mockBridge();
const result = await new MessageApi(bridge as any).getGroupHistorySyncPage(
12345,
101,
120,
10001,
);
expect(fetchGroupMessageRange).toHaveBeenCalledOnce();
expect(fetchGroupMessageRange).toHaveBeenCalledWith(
bridge,
bridge.identity,
10001,
12345,
101,
120,
);
expect(result.map((message: { msgSeq: number }) => message.msgSeq)).toEqual([101, 102]);
await expect(
new MessageApi(bridge as any).getGroupHistorySyncPage(12345, 1, 21, 10001),
).rejects.toThrow(/at most 20 sequence slots/);
expect(fetchGroupMessageRange).toHaveBeenCalledOnce();
});
it('fetches at most one bounded private roam page', async () => {
fetchC2cRoamMessagePage.mockResolvedValue({
messages: [
{ kind: 'friend_message', ntMsgSeq: 10, time: 100 },
{ kind: 'friend_message', ntMsgSeq: 11, time: 101 },
],
cursor: { time: 99, random: 1 },
});
const bridge = mockBridge();
const result = await new MessageApi(bridge as any).getC2cLatestHistorySyncPage(
'u_friend',
20,
10001,
1_700_000_001,
);
expect(fetchC2cRoamMessagePage).toHaveBeenCalledOnce();
expect(fetchC2cRoamMessagePage).toHaveBeenCalledWith(
bridge,
bridge.identity,
10001,
'u_friend',
1_700_000_001,
20,
0,
);
expect(result).toHaveLength(2);
await expect(
new MessageApi(bridge as any).getC2cLatestHistorySyncPage('u_friend', 21, 10001),
).rejects.toThrow(/at most 20 messages/);
expect(fetchC2cRoamMessagePage).toHaveBeenCalledOnce();
});
it('fetches one bounded private sequence page without pagination', async () => {
fetchC2cMessageRange.mockResolvedValue([
{ kind: 'friend_message', ntMsgSeq: 201 },
{ kind: 'friend_message', ntMsgSeq: 202 },
]);
const bridge = mockBridge();
const result = await new MessageApi(bridge as any).getC2cHistorySyncPage(
'u_friend',
201,
220,
10001,
);
expect(fetchC2cMessageRange).toHaveBeenCalledOnce();
expect(fetchC2cMessageRange).toHaveBeenCalledWith(
bridge,
bridge.identity,
10001,
'u_friend',
201,
220,
);
expect(result.map((message) => message.ntMsgSeq ?? 0)).toEqual([201, 202]);
});
});