SnowLuma v2.4.2 source snapshot
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
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
This commit is contained in:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,345 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CLIENT_MESSAGE_RECEIPT_CMD, type PacketInfo } from '@snowluma/common/protocol-types';
|
||||
import type { PacketSender, SendPacketResult } from '@snowluma/common/packet-sender';
|
||||
import type { SendMessageResponse } from '@snowluma/proto-defs/action';
|
||||
import type { SsoGetC2cMsg, SsoGetC2cMsgResponse } from '@snowluma/proto-defs/get-c2c-msg';
|
||||
import type { PushMsgBody } from '@snowluma/proto-defs/message';
|
||||
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
|
||||
import { IdentityService } from '../../src/identity-service';
|
||||
import {
|
||||
decodeClientMessageReceipt,
|
||||
fetchClientMessage,
|
||||
} from '../../src/msg-push/client-message-receipt';
|
||||
import { SSO_GET_C2C_MSG_CMD } from '../../src/msg-push/fetch-c2c-history';
|
||||
|
||||
const self = '10001';
|
||||
const peer = 'u_test_peer';
|
||||
const time = 1_700_000_000;
|
||||
const identities: IdentityService[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
vi.useRealTimers();
|
||||
await Promise.all(identities.splice(0).map((identity) => identity.close()));
|
||||
});
|
||||
|
||||
function packet(ack: SendMessageResponse = { privateSequence: 87, timestamp1: time }): PacketInfo {
|
||||
const uid = Buffer.from(peer);
|
||||
const header = Buffer.alloc(8);
|
||||
header[0] = 1;
|
||||
header[1] = uid.length;
|
||||
header.writeUInt32LE(9, 4);
|
||||
return {
|
||||
pid: 1234,
|
||||
uin: self,
|
||||
seqId: 42,
|
||||
retCode: 0,
|
||||
fromClient: false,
|
||||
source: 'client-message',
|
||||
serviceCmd: CLIENT_MESSAGE_RECEIPT_CMD,
|
||||
body: Buffer.concat([header, uid, protobuf_encode<SendMessageResponse>(ack)]),
|
||||
};
|
||||
}
|
||||
|
||||
function message(): PushMsgBody {
|
||||
return {
|
||||
responseHead: { fromUin: Number(self), fromUid: 'u_test_self', toUin: 20002, toUid: peer },
|
||||
contentHead: { msgType: 166, sequence: 9, ntMsgSeq: 87, timestamp: time, msgId: 123456 },
|
||||
body: { richText: { elems: [{ text: { str: 'hello' } }, { face: { index: 1 } }] } },
|
||||
};
|
||||
}
|
||||
|
||||
function lookupResponse(response: SsoGetC2cMsgResponse): SendPacketResult {
|
||||
return {
|
||||
success: true,
|
||||
gotResponse: true,
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData: Buffer.from(protobuf_encode<SsoGetC2cMsgResponse>(response)),
|
||||
};
|
||||
}
|
||||
|
||||
function setup(messages = [message()], friendUid = peer) {
|
||||
const identity = IdentityService.memory(self);
|
||||
identities.push(identity);
|
||||
const sendPacket = vi.fn<PacketSender['sendPacket']>(async () => ({
|
||||
success: true,
|
||||
gotResponse: true,
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData: Buffer.from(protobuf_encode<SsoGetC2cMsgResponse>({ friendUid, messages })),
|
||||
}));
|
||||
const receipt = decodeClientMessageReceipt(packet(), self);
|
||||
return {
|
||||
sendPacket,
|
||||
identity,
|
||||
receipt,
|
||||
run: () => fetchClientMessage({ sendPacket }, identity, receipt),
|
||||
};
|
||||
}
|
||||
|
||||
describe('client message receipts', () => {
|
||||
it('decodes a confirmation and retrieves exactly its conversation and sequence', async () => {
|
||||
const ctx = setup();
|
||||
expect(ctx.receipt).toEqual({
|
||||
peerUid: peer,
|
||||
clientSequence: 9,
|
||||
sequence: 87,
|
||||
timestamp: time,
|
||||
});
|
||||
const event = await ctx.run();
|
||||
expect(ctx.sendPacket).toHaveBeenCalledOnce();
|
||||
const [cmd, body] = ctx.sendPacket.mock.calls[0]!;
|
||||
expect(cmd).toBe(SSO_GET_C2C_MSG_CMD);
|
||||
expect(protobuf_decode<SsoGetC2cMsg>(body)).toMatchObject({
|
||||
friendUid: peer,
|
||||
startSequence: 87,
|
||||
endSequence: 87,
|
||||
});
|
||||
expect(event).toMatchObject({
|
||||
kind: 'friend_message',
|
||||
selfUin: 10001,
|
||||
senderUin: 10001,
|
||||
peerUin: 20002,
|
||||
clientSeq: 9,
|
||||
ntMsgSeq: 87,
|
||||
sequenceAuthoritative: true,
|
||||
time,
|
||||
elements: [
|
||||
{ type: 'text', text: 'hello' },
|
||||
{ type: 'face', faceId: 1 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ source: undefined },
|
||||
{ serviceCmd: 'MessageSvc.PbSendMsg' },
|
||||
{ uin: '20002' },
|
||||
{ retCode: -1 },
|
||||
] satisfies Partial<PacketInfo>[])('rejects invalid provenance or status: %j', (patch) => {
|
||||
expect(() => decodeClientMessageReceipt({ ...packet(), ...patch }, self)).toThrow();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ result: 1 },
|
||||
{ reviewResult: 1 },
|
||||
{ serverResult: { code: -1 } },
|
||||
{ privateSequence: 0 },
|
||||
{ timestamp1: 0 },
|
||||
{ groupSequence: 5 },
|
||||
] satisfies SendMessageResponse[])(
|
||||
'rejects an unsuccessful or incomplete confirmation: %j',
|
||||
(patch) => {
|
||||
expect(() =>
|
||||
decodeClientMessageReceipt(
|
||||
packet({ privateSequence: 87, timestamp1: time, ...patch }),
|
||||
self,
|
||||
),
|
||||
).toThrow();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[
|
||||
'version',
|
||||
(body: Buffer) => {
|
||||
body[0] = 2;
|
||||
},
|
||||
],
|
||||
[
|
||||
'reserved',
|
||||
(body: Buffer) => {
|
||||
body[2] = 1;
|
||||
},
|
||||
],
|
||||
[
|
||||
'uid length',
|
||||
(body: Buffer) => {
|
||||
body[1] = 129;
|
||||
},
|
||||
],
|
||||
[
|
||||
'uid',
|
||||
(body: Buffer) => {
|
||||
body[8] = 0xff;
|
||||
},
|
||||
],
|
||||
[
|
||||
'client sequence',
|
||||
(body: Buffer) => {
|
||||
body.writeUInt32LE(0, 4);
|
||||
},
|
||||
],
|
||||
] as const)('rejects a malformed %s', (_name, mutate) => {
|
||||
const pkt = packet();
|
||||
const body = Buffer.from(pkt.body);
|
||||
mutate(body);
|
||||
expect(() => decodeClientMessageReceipt({ ...pkt, body }, self)).toThrow();
|
||||
});
|
||||
|
||||
it('rejects truncated and excessive bodies', () => {
|
||||
for (const body of [
|
||||
new Uint8Array(),
|
||||
packet().body.subarray(0, 8 + peer.length),
|
||||
new Uint8Array(8 + 128 + 65537),
|
||||
]) {
|
||||
expect(() => decodeClientMessageReceipt({ ...packet(), body }, self)).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
'sender',
|
||||
(msg: PushMsgBody) => {
|
||||
msg.responseHead!.fromUin = 20002;
|
||||
},
|
||||
],
|
||||
[
|
||||
'recipient',
|
||||
(msg: PushMsgBody) => {
|
||||
msg.responseHead!.toUid = 'u_other';
|
||||
},
|
||||
],
|
||||
[
|
||||
'missing recipient',
|
||||
(msg: PushMsgBody) => {
|
||||
delete msg.responseHead!.toUin;
|
||||
},
|
||||
],
|
||||
[
|
||||
'client sequence',
|
||||
(msg: PushMsgBody) => {
|
||||
msg.contentHead!.sequence = 10;
|
||||
},
|
||||
],
|
||||
[
|
||||
'server sequence',
|
||||
(msg: PushMsgBody) => {
|
||||
msg.contentHead!.ntMsgSeq = 88;
|
||||
},
|
||||
],
|
||||
[
|
||||
'timestamp',
|
||||
(msg: PushMsgBody) => {
|
||||
msg.contentHead!.timestamp = time + 1;
|
||||
},
|
||||
],
|
||||
[
|
||||
'group',
|
||||
(msg: PushMsgBody) => {
|
||||
msg.responseHead!.grp = { groupCode: 30003 };
|
||||
},
|
||||
],
|
||||
[
|
||||
'type',
|
||||
(msg: PushMsgBody) => {
|
||||
msg.contentHead!.msgType = 82;
|
||||
},
|
||||
],
|
||||
[
|
||||
'empty content',
|
||||
(msg: PushMsgBody) => {
|
||||
msg.body = {};
|
||||
},
|
||||
],
|
||||
[
|
||||
'unknown content',
|
||||
(msg: PushMsgBody) => {
|
||||
msg.body = { richText: { elems: [{ commonElem: { serviceType: 999 } }] } };
|
||||
},
|
||||
],
|
||||
[
|
||||
'control',
|
||||
(msg: PushMsgBody) => {
|
||||
msg.contentHead!.c2cCmd = 1;
|
||||
},
|
||||
],
|
||||
] as const)('rejects an incorrect %s in the lookup', async (_name, mutate) => {
|
||||
const msg = message();
|
||||
mutate(msg);
|
||||
await expect(setup([msg]).run()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('rejects missing, ambiguous and cross-conversation results', async () => {
|
||||
await expect(setup([]).run()).rejects.toThrow('expected one');
|
||||
await expect(setup([message(), message()]).run()).rejects.toThrow('expected one');
|
||||
await expect(setup([message()], 'u_other').run()).rejects.toThrow('conversation');
|
||||
});
|
||||
|
||||
it('retries only the confirmed message while storage catches up', async () => {
|
||||
const ctx = setup();
|
||||
vi.useFakeTimers();
|
||||
ctx.sendPacket.mockResolvedValueOnce(
|
||||
lookupResponse({ result: 100000301, friendUid: peer, messages: [] }),
|
||||
);
|
||||
const work = ctx.run();
|
||||
await vi.advanceTimersByTimeAsync(99);
|
||||
expect(ctx.sendPacket).toHaveBeenCalledTimes(1);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(await work).toMatchObject({ ntMsgSeq: 87, clientSeq: 9 });
|
||||
expect(ctx.sendPacket).toHaveBeenCalledTimes(2);
|
||||
expect(ctx.sendPacket.mock.calls[1]).toEqual(ctx.sendPacket.mock.calls[0]);
|
||||
});
|
||||
|
||||
it('reports a bounded failure when storage never contains the confirmed message', async () => {
|
||||
const ctx = setup();
|
||||
vi.useFakeTimers();
|
||||
ctx.sendPacket.mockResolvedValue(
|
||||
lookupResponse({ result: 100000301, friendUid: peer, messages: [] }),
|
||||
);
|
||||
const rejected = expect(ctx.run()).rejects.toThrow('still unavailable after 6 attempts');
|
||||
await vi.runAllTimersAsync();
|
||||
await rejected;
|
||||
expect(ctx.sendPacket).toHaveBeenCalledTimes(6);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ result: 100000302, friendUid: peer, messages: [] },
|
||||
{ result: 100000301, friendUid: 'u_other', messages: [] },
|
||||
{ result: 100000301, messages: [] },
|
||||
{ result: 100000301, friendUid: peer, messages: [message()] },
|
||||
] satisfies SsoGetC2cMsgResponse[])(
|
||||
'does not retry unrelated or inconsistent errors: %j',
|
||||
async (response) => {
|
||||
const ctx = setup();
|
||||
ctx.sendPacket.mockResolvedValue(lookupResponse(response));
|
||||
await expect(ctx.run()).rejects.toThrow();
|
||||
expect(ctx.sendPacket).toHaveBeenCalledOnce();
|
||||
},
|
||||
);
|
||||
|
||||
it('preserves distinct identities for equal text', async () => {
|
||||
const first = setup();
|
||||
const msg = message();
|
||||
msg.contentHead = { ...msg.contentHead, sequence: 10, ntMsgSeq: 88 };
|
||||
const second = setup([msg]);
|
||||
const event = await fetchClientMessage(second, second.identity, {
|
||||
...second.receipt,
|
||||
clientSequence: 10,
|
||||
sequence: 88,
|
||||
});
|
||||
expect((await first.run()).ntMsgSeq).toBe(87);
|
||||
expect(event.ntMsgSeq).toBe(88);
|
||||
expect(event.elements).toEqual((await first.run()).elements);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ success: false, gotResponse: false, responseData: null },
|
||||
{ success: true, gotResponse: false, responseData: null },
|
||||
{ success: true, gotResponse: true, responseData: Buffer.alloc(0) },
|
||||
] satisfies Partial<SendPacketResult>[])(
|
||||
'surfaces unsuccessful lookup transport: %j',
|
||||
async (result) => {
|
||||
const ctx = setup();
|
||||
ctx.sendPacket.mockResolvedValue({
|
||||
success: true,
|
||||
gotResponse: true,
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData: null,
|
||||
...result,
|
||||
});
|
||||
await expect(ctx.run()).rejects.toThrow('Client message lookup');
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user