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

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

@@ -0,0 +1,129 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_encode } from '@snowluma/proton';
import type { OidbBase, OidbEmpty } from '@snowluma/proto-defs/oidb';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { ApproveDoubtBuddyReq } from '../../../src/oidb-services/friend/approve-doubt-buddy-req';
import { env, s, v } from '../_pb-oracle';
function makeSender(selfUid = 'u_self', selfUin = '10001') {
const respEnv: OidbBase<OidbEmpty> = { command: 0xd72, subCommand: 0, body: {} };
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.from(protobuf_encode<OidbBase<OidbEmpty>>(respEnv)),
};
return {
sendRawPacket: vi.fn(async (_cmd: string, _bytes: Uint8Array) => r),
identity: { selfUid, uin: selfUin } as ApproveDoubtBuddyReq.Deps['identity'],
resolveUserUid: vi.fn(async () => 'u_lookup_self'),
};
}
describe('ApproveDoubtBuddyReq namespace', () => {
it('keeps the approving account separate from the applicant', async () => {
const sender = makeSender();
expect(await ApproveDoubtBuddyReq.serialize(sender, { uid: 'u_applicant' })).toEqual({
selfUid: 'u_self',
targetUid: 'u_applicant',
field3: 0,
field4: '',
});
expect(sender.resolveUserUid).not.toHaveBeenCalled();
});
it('matches the approval request bytes including explicit default options', async () => {
const sender = makeSender();
await ApproveDoubtBuddyReq.invoke(sender, { uid: 'u_applicant' });
expect(sender.sendRawPacket).toHaveBeenCalledOnce();
const [cmd, bytes] = sender.sendRawPacket.mock.calls[0]!;
expect(cmd).toBe('OidbSvcTrpcTcp.0xd72_0');
const body = [...s(1, 'u_self'), ...s(2, 'u_applicant'), ...v(3, 0), ...s(4, '')];
expect(Buffer.from(bytes).toString('hex')).toBe(env(0xd72, 0, body, false));
});
it('resolves the current account when its self profile is not cached', async () => {
const sender = makeSender('');
await ApproveDoubtBuddyReq.invoke(sender, { uid: 'u_applicant' });
expect(sender.resolveUserUid).toHaveBeenCalledWith(10001);
const body = [...s(1, 'u_lookup_self'), ...s(2, 'u_applicant'), ...v(3, 0), ...s(4, '')];
expect(Buffer.from(sender.sendRawPacket.mock.calls[0]![1]).toString('hex')).toBe(
env(0xd72, 0, body, false),
);
});
it('does not send when the current account cannot be identified', async () => {
const sender = makeSender('', '0');
await expect(ApproveDoubtBuddyReq.invoke(sender, { uid: 'u_applicant' })).rejects.toThrow(
'self uid is unavailable',
);
expect(sender.sendRawPacket).not.toHaveBeenCalled();
expect(sender.resolveUserUid).not.toHaveBeenCalled();
});
it('propagates self identity lookup errors without sending', async () => {
const sender = makeSender('');
sender.resolveUserUid.mockRejectedValue(new Error('profile lookup failed'));
await expect(ApproveDoubtBuddyReq.invoke(sender, { uid: 'u_applicant' })).rejects.toThrow(
'profile lookup failed',
);
expect(sender.sendRawPacket).not.toHaveBeenCalled();
});
it('rejects an empty resolved self identity before sending', async () => {
const sender = makeSender('');
sender.resolveUserUid.mockResolvedValue('');
await expect(ApproveDoubtBuddyReq.invoke(sender, { uid: 'u_applicant' })).rejects.toThrow(
'self uid is unavailable',
);
expect(sender.sendRawPacket).not.toHaveBeenCalled();
});
it.each(['', ' '])('rejects an absent applicant identity (%j)', async (uid) => {
const sender = makeSender();
await expect(ApproveDoubtBuddyReq.invoke(sender, { uid })).rejects.toThrow(
'applicant uid is unavailable',
);
expect(sender.sendRawPacket).not.toHaveBeenCalled();
});
it('propagates a rejected approval without retrying a different operation', async () => {
const sender = makeSender();
sender.sendRawPacket.mockResolvedValue({
success: false,
gotResponse: true,
errorCode: 1201,
errorMessage: 'request rejected',
responseData: Buffer.alloc(0),
});
await expect(ApproveDoubtBuddyReq.invoke(sender, { uid: 'u_applicant' })).rejects.toThrow(
'request rejected',
);
expect(sender.sendRawPacket).toHaveBeenCalledOnce();
});
it('does not mistake a successful transport for an accepted approval', async () => {
const sender = makeSender();
sender.sendRawPacket.mockResolvedValue({
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.from(
protobuf_encode<OidbBase<OidbEmpty>>({
command: 0xd72,
subCommand: 0,
errorCode: 1201,
errorMsg: 'approval denied',
body: {},
}),
),
});
await expect(ApproveDoubtBuddyReq.invoke(sender, { uid: 'u_applicant' })).rejects.toThrow(
'approval denied',
);
expect(sender.sendRawPacket).toHaveBeenCalledOnce();
});
});
@@ -0,0 +1,35 @@
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { describe, expect, it, vi } from 'vitest';
import { ClearFriendRemark } from '../../../src/oidb-services/friend/clear-friend-remark';
import { env, m, s } from '../_pb-oracle';
function makeDeps() {
const result: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.alloc(0),
};
return {
sendRawPacket: vi.fn(async () => result),
resolveUserUid: vi.fn(async () => 'resolved-uid'),
};
}
describe('ClearFriendRemark namespace', () => {
it('sends the current QQ 0x912F_0 wire shape', async () => {
const deps = makeDeps();
await ClearFriendRemark.invoke(deps, { userId: 10001 });
const [wireName, bytes] = deps.sendRawPacket.mock.calls[0]!;
const body = m(1, s(7, 'resolved-uid'));
expect({
wireName,
hex: Buffer.from(bytes).toString('hex'),
}).toEqual({
wireName: 'OidbSvcTrpcTcp.0x912f_0',
hex: env(0x912f, 0, body, false),
});
});
});
@@ -0,0 +1,84 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { OidbDeleteFriend } from '@snowluma/proto-defs/oidb-actions/base';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { DeleteFriend } from '../../../src/oidb-services/friend/delete-friend';
import { env, m, s, v } from '../_pb-oracle';
function makeDeps() {
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.alloc(0),
};
return {
sendRawPacket: vi.fn(async () => r),
resolveUserUid: vi.fn(async () => 'resolved-uid'),
};
}
describe('DeleteFriend namespace', () => {
it('declares 0x126B_0', () => {
expect(DeleteFriend.command).toBe(0x126b);
expect(DeleteFriend.subCommand).toBe(0);
});
describe('invoke', () => {
it('routes to OidbSvcTrpcTcp.0x126b_0', async () => {
const deps = makeDeps();
await DeleteFriend.invoke(deps, { userId: 10001 });
expect(deps.sendRawPacket.mock.calls[0]![0]).toBe('OidbSvcTrpcTcp.0x126b_0');
});
it('resolves the target uid before encoding', async () => {
const deps = makeDeps();
await DeleteFriend.invoke(deps, { userId: 10001 });
expect(deps.resolveUserUid).toHaveBeenCalledWith(10001);
});
it('packages the nested envelope with verbatim magic constants', async () => {
const deps = makeDeps();
await DeleteFriend.invoke(deps, { userId: 10001, block: true });
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
const env = protobuf_decode<OidbBase<OidbDeleteFriend>>(bytes);
expect(env.body?.field1).toMatchObject({
targetUid: 'resolved-uid',
block: true,
field2: {
field1: 130,
field2: 109,
field3: { field1: 8, field2: 8, field3: 50 },
},
});
});
it('matches the current QQ wire for a non-blocking delete', async () => {
const deps = makeDeps();
await DeleteFriend.invoke(deps, { userId: 10001 });
const [wireName, bytes] = deps.sendRawPacket.mock.calls[0]!;
const discriminator = [
...v(1, 130),
...v(2, 109),
...m(3, [...v(1, 8), ...v(2, 8), ...v(3, 50)]),
];
const body = m(1, [
...s(1, 'resolved-uid'),
...m(2, discriminator),
// Current QQ explicitly emits both booleans.
...v(3, 0),
...v(4, 1),
]);
expect({
wireName,
hex: Buffer.from(bytes).toString('hex'),
}).toEqual({
wireName: 'OidbSvcTrpcTcp.0x126b_0',
hex: env(0x126b, 0, body, false),
});
});
});
});
@@ -0,0 +1 @@
08e91a22a60c080112a10c0a7e0880c2d72f12066e69636b3030180020fe012a056d73673030320851512067726f75703a1766696c746572656420667269656e6420726571756573744080c3bbc2064880cee4cd02500062006a30abc8a360ea075bcf8bbfa1e1e0e010beb1ac5228138eb82586704b41691e9efea640392a2a65e9dd50de82f154e0201b0a7d08c7aa9b3012066e69636b3031180020012a056d73673031320851512067726f75703a1766696c746572656420667269656e6420726571756573744080e6c0c206488dcee4cd02500162006a30ee591d017a9bb6807f7db7c8d3665e34923c7db6f6252d672a8f1f8986c7c9e3d21415ea86987a1b689c8d675d2a26800a7d088e93df3012066e69636b3032180020012a056d73673032320851512067726f75703a1766696c746572656420667269656e642072657175657374408089c6c206489acee4cd02500062006a305fe9e2cce8e6bc04c4f76fe8872347907e73a73b9184c3a7cbbd9c85a1d319c252129d950d06920d50732964df4afcf20a810108d5fba23112066e69636b3033180020ffffffff0f2a056d73673033320851512067726f75703a1766696c746572656420667269656e6420726571756573744080accbc20648a7cee4cd02500062006a3006b881debd01f92c98ef3c1726406ebeb88cae9d12d89e9fa36c20a77495dd0a3eb9fbbe7b14f65d657e979adfda5c600a7d089ce4e63112066e69636b3034180020012a056d73673034320851512067726f75703a1766696c746572656420667269656e6420726571756573744080cfd0c20648b4cee4cd02500062006a30316f76c654c33c8f6f3689a9475d8b871f74654789edb72d3f1a87bb1ba11cf07286b8d748f4d08d6fbe5afee8208e010a870108e3ccaa3212066e69636b3035185920ffffffff0f2a056d73673035320851512067726f75703a1766696c746572656420667269656e6420726571756573744080f2d5c20648c1cee4cd0250006206e4b89ce4baac6a30a3038a382c30ee6a8686410089e1c618f0b11bf92fdf2726da2910d2082a147a5569d04815e7c42287b424ef45b00ff70a860108aab5ee3212066e69636b3036180120002a056d73673036320851512067726f75703a1766696c746572656420667269656e642072657175657374408095dbc20648cecee4cd0250006209e7a59ee5a588e5b79d6a3097d6cc1f21ca290ab424147c3181e68490c49de47eda96eb422ec861eefdc8f580578655ac4f874ab7e8467cfc0e9fd30a810108f19db23312066e69636b3037180020ffffffff0f2a056d73673037320851512067726f75703a1766696c746572656420667269656e6420726571756573744080b8e0c20648dbcee4cd02500062006a303c1421acb295e3a5a4c914625ef1dbee99b552716ed28046cfa3b9171518dab5c247e0ddd3a24cb5f318a1cb60d6c9b30a810108b886f63312066e69636b3038180020ffffffff0f2a056d73673038320851512067726f75703a1766696c746572656420667269656e6420726571756573744080dbe5c20648e8cee4cd02500162006a3020ac3ddb705e3246f8fe7d266becea5bf92ec584a2cc12d4a83197d782047ca61d0ed90c5c4419e0866940f22a3eb1420a7d08ffeeb93412066e69636b3039180020012a056d73673039320851512067726f75703a1766696c746572656420667269656e6420726571756573744080feeac20648f5cee4cd02500162006a3047da9cf4f576624c5edd21b6bcfda8898c91143711f0bd251e00dfb1c273738290fa190cf6dc231c41dc31ad2086c6050a7d08c6d7fd3412066e69636b3130181420002a056d73673130320851512067726f75703a1766696c746572656420667269656e6420726571756573744080a1f0c2064882cfe4cd02500062006a300ab4598cfe36f3f340ec5237766213a06ddd99d900c3b3ac4d18530d4308f17943a22ef6b85d4d3f08086dcaf36465550a8401088dc0c13512066e69636b3131181220fe012a056d73673131320851512067726f75703a1766696c746572656420667269656e6420726571756573744080c4f5c206488fcfe4cd0250016206e697a0e994a16a3006e504c8f85b64fa8d5cb196d4b8cebc8098c752d879880bd8ab0108a829a1b29f952ff4e4b3807af4d54f70fb0dbf50
@@ -0,0 +1,183 @@
import { describe, expect, it, vi } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { OidbDoubtGetResp } from '@snowluma/proto-defs/oidb-actions/doubt-buddy';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { GetDoubtBuddyReq } from '../../../src/oidb-services/friend/get-doubt-buddy-req';
import { env, v, s, m } from '../_pb-oracle';
function makeSender(): { sendRawPacket: ReturnType<typeof vi.fn> } {
const respEnv: OidbBase<OidbDoubtGetResp> = {
command: 0xd69,
subCommand: 0,
body: {
status: 1,
body: {
list: [
{
uid: 'u_alice',
nick: 'Alice',
source: '可能认识',
reason: '',
msg: 'hi',
reqTime: 1700000000n,
},
],
reason: '',
},
},
};
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.from(protobuf_encode<OidbBase<OidbDoubtGetResp>>(respEnv)),
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('GetDoubtBuddyReq namespace', () => {
it('declares command 0xD69 sub 0, uin form', () => {
expect(GetDoubtBuddyReq.command).toBe(0xd69);
expect(GetDoubtBuddyReq.subCommand).toBe(0);
expect(GetDoubtBuddyReq.uinForm).toBe(true);
});
it('serializes {1:1, 2:{1:count, 2:uk}} (reqId is not on the wire)', () => {
expect(GetDoubtBuddyReq.serialize({} as any, { count: 50 })).toEqual({
field1: 1,
inner: { num: 50, uk: '' },
});
});
it('byte-oracle: routes to 0xd69_0 and locks nested req tags {1:const1, 2:{1:num, 2:uk}}', async () => {
const sender = makeSender();
await GetDoubtBuddyReq.invoke(sender, { count: 50 });
const [cmd, bytes] = sender.sendRawPacket.mock.calls[0]!;
expect(cmd).toBe('OidbSvcTrpcTcp.0xd69_0');
// uk '' omitted (proto3 default); inner carries only num.
const inner = [...v(1, 50)];
const body = [...v(1, 1), ...m(2, inner)];
expect(Buffer.from(bytes).toString('hex')).toBe(env(0xd69, 0, body, true));
});
it('deserializes the string uid item to the OneBot shape', async () => {
const list = await GetDoubtBuddyReq.invoke(makeSender(), { count: 10 });
expect(list).toEqual([
{
uid: 'u_alice',
user_id: 0,
nick: 'Alice',
source: '可能认识',
reason: '',
msg: 'hi',
group_code: '',
reqTime: 1700000000,
},
]);
});
it('deserialize returns [] when the body has no list', () => {
expect(GetDoubtBuddyReq.deserialize({} as any, { status: 1 })).toEqual([]);
});
it('keeps an empty uid when the reply only carries the account number', () => {
expect(
GetDoubtBuddyReq.deserialize({} as any, {
status: 1,
body: { list: [{ nick: 'Alice', uin: 12345n, reqTime: 1700000000n }] },
}),
).toEqual([
{
uid: '',
user_id: 12345,
nick: 'Alice',
source: '',
reason: '',
msg: '',
group_code: '',
reqTime: 1700000000,
},
]);
});
it('keeps the uid string and does not reread it as the account number', async () => {
const item = [
...s(1, 'u_alice'),
...s(2, 'Alice'),
...s(5, 'hi'),
...s(6, 'QQ号查找'),
...s(7, 'caution'),
];
const sender = {
sendRawPacket: vi.fn(async () => ({
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.from(
env(0xd69, 0, [...v(1, 1), ...m(2, m(1, item))], false),
'hex',
),
})),
};
const list = await GetDoubtBuddyReq.invoke(sender, { count: 10 });
expect(list).toEqual([
{
uid: 'u_alice',
user_id: 0,
nick: 'Alice',
source: 'QQ号查找',
reason: 'caution',
msg: 'hi',
group_code: '',
reqTime: 0,
},
]);
});
// Live capture from QQ 3.2.32-52194 on Linux. Locks the real wire shape:
// tag1 is the applicant's account number (not a string uid), tag8 is the
// request unix time, and tag9 is the source group. Before this, a tag8-as-uin
// read made the uin->uid fallback query the request time as an account.
it('decodes a captured Linux reply: account at tag1, time at tag8, group at tag9', () => {
const hex = readFileSync(
fileURLToPath(new URL('./fixtures/d69-linux-3.2.32.hex', import.meta.url)),
'utf8',
).trim();
const list = GetDoubtBuddyReq.deserialize(
{} as any,
GetDoubtBuddyReq.decode(Buffer.from(hex, 'hex')).body!,
);
expect(list).toHaveLength(12);
expect(list[0]).toMatchObject({
uid: '',
user_id: 100000000,
nick: 'nick00',
source: 'QQ group',
reason: 'filtered friend request',
msg: 'msg00',
group_code: '700000000',
reqTime: 1750000000,
});
expect(list[1]).toMatchObject({
uid: '',
user_id: 101111111,
group_code: '700000013',
reqTime: 1750086400,
});
// Account numbers stay in the QQ range; request times land in 2025-2026.
for (const item of list) {
expect(item.user_id).toBeGreaterThan(100000);
expect(item.user_id).toBeLessThan(4294967296);
expect(item.reqTime).toBeGreaterThan(1700000000);
expect(item.reqTime).toBeLessThan(2000000000);
}
});
});
@@ -0,0 +1,70 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { OidbFriendRequestAction } from '@snowluma/proto-defs/oidb-actions/base';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { HandleFriendRequest } from '../../../src/oidb-services/friend/handle-friend-request';
function makeDeps(resolvedUid = 'resolved-uid') {
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.alloc(0),
};
return {
sendRawPacket: vi.fn(async () => r),
resolveUserUid: vi.fn(async () => resolvedUid),
};
}
describe('HandleFriendRequest namespace', () => {
it('declares 0xB5D_44', () => {
expect(HandleFriendRequest.command).toBe(0xb5d);
expect(HandleFriendRequest.subCommand).toBe(44);
});
describe('invoke', () => {
it('routes to OidbSvcTrpcTcp.0xb5d_44', async () => {
const deps = makeDeps();
await HandleFriendRequest.invoke(deps, { uidOrFlag: 'uid', approve: true });
expect(deps.sendRawPacket.mock.calls[0]![0]).toBe('OidbSvcTrpcTcp.0xb5d_44');
});
it('detects digit-only flag → resolves via resolveUserUid', async () => {
const deps = makeDeps('resolved-from-uin');
await HandleFriendRequest.invoke(deps, { uidOrFlag: '10001', approve: true });
expect(deps.resolveUserUid).toHaveBeenCalledWith(10001);
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
const env = protobuf_decode<OidbBase<OidbFriendRequestAction>>(bytes);
expect(env.body?.targetUid).toBe('resolved-from-uin');
});
it('passes non-numeric flag through verbatim (no resolveUserUid call)', async () => {
const deps = makeDeps();
await HandleFriendRequest.invoke(deps, { uidOrFlag: 'flag-abc', approve: false });
expect(deps.resolveUserUid).not.toHaveBeenCalled();
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
const env = protobuf_decode<OidbBase<OidbFriendRequestAction>>(bytes);
expect(env.body?.targetUid).toBe('flag-abc');
});
it('encodes accept=3 when approve=true', async () => {
const deps = makeDeps();
await HandleFriendRequest.invoke(deps, { uidOrFlag: 'u', approve: true });
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
const env = protobuf_decode<OidbBase<OidbFriendRequestAction>>(bytes);
expect(env.body?.accept).toBe(3);
});
it('encodes accept=5 when approve=false', async () => {
const deps = makeDeps();
await HandleFriendRequest.invoke(deps, { uidOrFlag: 'u', approve: false });
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
const env = protobuf_decode<OidbBase<OidbFriendRequestAction>>(bytes);
expect(env.body?.accept).toBe(5);
});
});
});
@@ -0,0 +1,44 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_encode } from '@snowluma/proton';
import type { OidbBase, OidbEmpty } from '@snowluma/proto-defs/oidb';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { RejectDoubtBuddyReq } from '../../../src/oidb-services/friend/reject-doubt-buddy-req';
import { env, v, s, m } from '../_pb-oracle';
function makeSender() {
const respEnv: OidbBase<OidbEmpty> = { command: 0xd69, subCommand: 0, body: {} };
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.from(protobuf_encode<OidbBase<OidbEmpty>>(respEnv)),
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('RejectDoubtBuddyReq namespace', () => {
it('declares command 0xD69 sub 0, uin form', () => {
expect(RejectDoubtBuddyReq.command).toBe(0xd69);
expect(RejectDoubtBuddyReq.subCommand).toBe(0);
expect(RejectDoubtBuddyReq.uinForm).toBe(true);
});
it('serializes {1: const 3 (op discriminator), 3: {1: uid}}', () => {
expect(RejectDoubtBuddyReq.serialize({} as any, { uid: 'u_x' })).toEqual({
field1: 3,
inner: { uid: 'u_x' },
});
});
it('byte-oracle: routes to 0xd69_0 and locks req tags {1:const3, 3:{1:uid}}', async () => {
const sender = makeSender();
await RejectDoubtBuddyReq.invoke(sender, { uid: 'u_abc' });
const [cmd, bytes] = sender.sendRawPacket.mock.calls[0]!;
expect(cmd).toBe('OidbSvcTrpcTcp.0xd69_0');
const inner = [...s(1, 'u_abc')];
const body = [...v(1, 3), ...m(3, inner)];
expect(Buffer.from(bytes).toString('hex')).toBe(env(0xd69, 0, body, true));
});
});
@@ -0,0 +1,151 @@
import { describe, expect, it, vi } from 'vitest';
import { subscribeLogs, type LogEntry } from '@snowluma/common/logger';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { SetFriendRemark } from '../../../src/oidb-services/friend/set-friend-remark';
import { env, m, s, v } from '../_pb-oracle';
function successBody(uid = 'resolved-uid', remark = 'best-friend', uin = 0): number[] {
return m(1, [...m(1, [...(uin > 0 ? v(3, uin) : []), ...s(7, uid)]), ...s(2, remark)]);
}
function makeDeps(responseBody = successBody()) {
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.from(env(0x912e, 0, responseBody, false), 'hex'),
};
return {
sendRawPacket: vi.fn(async () => r),
resolveUserUid: vi.fn(async () => 'resolved-uid'),
identity: { updateFriendRemark: vi.fn() },
};
}
describe('SetFriendRemark namespace', () => {
it('sends the current QQ 0x912E_0 wire shape for a non-empty remark', async () => {
const deps = makeDeps();
await SetFriendRemark.invoke(deps, { userId: 10001, remark: 'best-friend' });
const [wireName, bytes] = deps.sendRawPacket.mock.calls[0]!;
const body = [
...m(1, [...m(1, s(7, 'resolved-uid')), ...s(2, 'best-friend')]),
// QQ explicitly emits scene=0 for an ordinary friend.
...v(2, 0),
];
expect({
wireName,
hex: Buffer.from(bytes).toString('hex'),
}).toEqual({
wireName: 'OidbSvcTrpcTcp.0x912e_0',
hex: env(0x912e, 0, body, false),
});
});
it('resolves the target UID before encoding', async () => {
const deps = makeDeps();
await SetFriendRemark.invoke(deps, { userId: 10001, remark: 'best-friend' });
expect(deps.resolveUserUid).toHaveBeenCalledWith(10001);
});
it('updates identity only after the server confirms the requested value', async () => {
const deps = makeDeps(successBody('resolved-uid', 'best-friend', 10001));
await SetFriendRemark.invoke(deps, { userId: 10001, remark: 'best-friend' });
expect(deps.identity.updateFriendRemark).toHaveBeenCalledWith(
'resolved-uid',
10001,
'best-friend',
);
});
it('reports local synchronization failures without retrying a confirmed remote update', async () => {
const deps = makeDeps(successBody('resolved-uid', 'best-friend', 10001));
deps.identity.updateFriendRemark.mockImplementation(() => {
throw new Error('identity cache unavailable');
});
const captured: LogEntry[] = [];
const unsubscribe = subscribeLogs((entry) => {
if (entry.scope === 'Bridge.Friend') {
captured.push(entry);
}
});
try {
await expect(
SetFriendRemark.invoke(deps, {
userId: 10001,
remark: 'best-friend',
}),
).resolves.toBeUndefined();
} finally {
unsubscribe();
}
expect(deps.sendRawPacket).toHaveBeenCalledTimes(1);
expect(captured).toContainEqual(
expect.objectContaining({
level: 'error',
message: expect.stringContaining('identity cache unavailable'),
}),
);
});
it('rejects a business failure carried inside a successful OIDB response', async () => {
const deps = makeDeps(m(3, [...v(2, 120), ...s(3, 'remark rejected')]));
await expect(
SetFriendRemark.invoke(deps, {
userId: 10001,
remark: 'best-friend',
}),
).rejects.toThrow(/remark rejected/);
expect(deps.identity.updateFriendRemark).not.toHaveBeenCalled();
});
it('rejects a response that contains neither a result nor a business error', async () => {
const deps = makeDeps([]);
await expect(
SetFriendRemark.invoke(deps, {
userId: 10001,
remark: 'best-friend',
}),
).rejects.toThrow(/missing result/);
});
it('rejects a success result for a different target', async () => {
const deps = makeDeps(successBody('another-uid'));
await expect(
SetFriendRemark.invoke(deps, {
userId: 10001,
remark: 'best-friend',
}),
).rejects.toThrow(/target mismatch/);
});
it('rejects a success result that does not confirm the requested remark', async () => {
const deps = makeDeps(successBody('resolved-uid', 'old-remark'));
await expect(
SetFriendRemark.invoke(deps, {
userId: 10001,
remark: 'best-friend',
}),
).rejects.toThrow(/does not match/);
});
it('rejects a success result carrying a different numeric account', async () => {
const deps = makeDeps(successBody('resolved-uid', 'best-friend', 20002));
await expect(
SetFriendRemark.invoke(deps, {
userId: 10001,
remark: 'best-friend',
}),
).rejects.toThrow(/expected UIN 10001, got 20002/);
});
});