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,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);
}
});
});