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,256 @@
import { describe, expect, it, vi } from 'vitest';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { SetAdminPermissions } from '../../../src/oidb-services/group-admin/set-admin-permissions';
import { GetAdminPermissions } from '../../../src/oidb-services/group-admin/get-admin-permissions';
import { SetAdminReceiveVerify } from '../../../src/oidb-services/group-admin/set-admin-receive-verify';
import { env, m, v } from '../_pb-oracle';
const target = { groupId: 12345, userId: 67890 };
function sender(body: number[] = []) {
const result: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.from(body),
};
return { sendRawPacket: vi.fn(async (_cmd: string, _bytes: Uint8Array) => result) };
}
function member(uin: number, permissions: number[]) {
return [...v(1, uin), ...m(3, m(52, permissions))];
}
function snapshot(permissions: number[], group = target.groupId, uin = target.userId) {
return m(4, [...v(1, group), ...m(3, member(uin, permissions))]);
}
function permission(id: number, value: number, extra?: number[]) {
return m(1, [...v(1, id), ...v(2, value), ...(extra === undefined ? [] : m(3, extra))]);
}
describe('administrator permission requests', () => {
it('writes both permissions with the service-specific envelope and nested daily limit', async () => {
const deps = sender();
await SetAdminPermissions.invoke(deps, {
...target,
recallMemberMessage: false,
kickMember: true,
kickMemberPerDay: 20,
});
const [cmd, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(cmd).toBe('OidbSvcTrpcTcp.0x9c1c_2');
expect(Buffer.from(bytes).toString('hex')).toBe(
env(
0x9c1c,
2,
[
...v(1, target.groupId),
...v(2, target.userId),
...m(3, [...v(1, 1), ...v(2, 1), ...m(3, v(1, 20))]),
...m(3, [...v(1, 2), ...v(2, 2)]),
],
false,
),
);
});
it('does not write omitted permissions and encodes disable as 2', async () => {
const deps = sender();
await SetAdminPermissions.invoke(deps, { ...target, kickMember: false });
expect(Buffer.from(deps.sendRawPacket.mock.calls[0]![1]).toString('hex')).toBe(
env(
0x9c1c,
2,
[
...v(1, target.groupId),
...v(2, target.userId),
...m(3, [...v(1, 1), ...v(2, 2)]),
],
false,
),
);
await SetAdminPermissions.invoke(deps, { ...target, recallMemberMessage: true });
expect(Buffer.from(deps.sendRawPacket.mock.calls[1]![1]).toString('hex')).toBe(
env(
0x9c1c,
2,
[
...v(1, target.groupId),
...v(2, target.userId),
...m(3, [...v(1, 2), ...v(2, 1)]),
],
false,
),
);
});
it('accepts explicit unlimited without manufacturing an extra limit field', async () => {
const deps = sender();
await SetAdminPermissions.invoke(deps, {
...target,
kickMember: true,
kickMemberPerDay: 0,
});
expect(Buffer.from(deps.sendRawPacket.mock.calls[0]![1]).toString('hex')).toBe(
env(
0x9c1c,
2,
[
...v(1, target.groupId),
...v(2, target.userId),
...m(3, [...v(1, 1), ...v(2, 1)]),
],
false,
),
);
});
it.each([
{},
{ kickMemberPerDay: 20 },
{ kickMember: true },
{ kickMember: false, kickMemberPerDay: 20 },
{ kickMember: true, kickMemberPerDay: -1 },
{ kickMember: true, kickMemberPerDay: 1 },
{ kickMember: true, kickMemberPerDay: 201 },
{ kickMember: true, kickMemberPerDay: NaN },
])('rejects invalid patches before sending: %j', async (patch) => {
const deps = sender();
await expect(SetAdminPermissions.invoke(deps, { ...target, ...patch })).rejects.toThrow();
expect(deps.sendRawPacket).not.toHaveBeenCalled();
});
it('propagates envelope rejection instead of accepting an empty response body', async () => {
const deps = sender(v(3, 1287));
await expect(
SetAdminPermissions.invoke(deps, { ...target, recallMemberMessage: false }),
).rejects.toThrow('OIDB error 1287');
});
it('does not retry transport failures', async () => {
const deps = sender();
deps.sendRawPacket.mockRejectedValueOnce(new Error('transport disconnected'));
await expect(
SetAdminPermissions.invoke(deps, { ...target, recallMemberMessage: false }),
).rejects.toThrow('transport disconnected');
expect(deps.sendRawPacket).toHaveBeenCalledTimes(1);
});
});
describe('administrator permission snapshot', () => {
it('requests only permission data and reads the matching member', async () => {
const deps = sender(snapshot([...permission(2, 2), ...permission(1, 1, v(1, 50))]));
await expect(GetAdminPermissions.invoke(deps, target)).resolves.toEqual({
recallMemberMessage: false,
kickMember: true,
kickMemberPerDay: 50,
});
const [cmd, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(cmd).toBe('OidbSvcTrpcTcp.0x9164_2');
expect(Buffer.from(bytes).toString('hex')).toBe(
env(
0x9164,
2,
[...v(1, target.groupId), ...v(2, target.userId), ...m(4, m(52, [1]))],
true,
),
);
});
it('finds the requested member rather than using the first returned member', async () => {
const deps = sender(
m(4, [
...v(1, target.groupId),
...m(3, member(1, permission(2, 1))),
...m(3, member(target.userId, permission(2, 2))),
]),
);
await expect(GetAdminPermissions.invoke(deps, target)).resolves.toEqual({
recallMemberMessage: false,
kickMember: null,
kickMemberPerDay: null,
});
});
it('uses the protobuf zero limit only when removal is explicitly enabled', async () => {
await expect(
GetAdminPermissions.invoke(sender(snapshot(permission(1, 1))), target),
).resolves.toEqual({ recallMemberMessage: null, kickMember: true, kickMemberPerDay: 0 });
await expect(
GetAdminPermissions.invoke(sender(snapshot(permission(1, 2))), target),
).resolves.toEqual({
recallMemberMessage: null,
kickMember: false,
kickMemberPerDay: null,
});
});
it('does not turn missing permissions or future item IDs into enabled defaults', async () => {
await expect(
GetAdminPermissions.invoke(
sender(snapshot([...permission(999, 1), ...permission(2, 0)])),
target,
),
).resolves.toEqual({ recallMemberMessage: null, kickMember: null, kickMemberPerDay: null });
});
it.each([
['missing body', []],
['wrong group', snapshot([], 1)],
['wrong member', snapshot([], target.groupId, 1)],
['no member', m(4, v(1, target.groupId))],
[
'duplicate member',
m(4, [
...v(1, target.groupId),
...m(3, member(target.userId, [])),
...m(3, member(target.userId, [])),
]),
],
['duplicate permission', snapshot([...permission(1, 1), ...permission(1, 2)])],
['unsupported value', snapshot(permission(2, 3))],
['malformed limit', snapshot(permission(1, 1, [8, 128]))],
] as const)('rejects %s', async (_name, bytes) => {
await expect(GetAdminPermissions.invoke(sender([...bytes]), target)).rejects.toThrow();
});
});
describe('administrator join verification', () => {
it.each([true, false])(
'encodes enable=%s and requires a successful target acknowledgement',
async (enable) => {
const deps = sender(m(4, [...v(1, target.groupId), ...m(2, v(1, target.userId))]));
await expect(
SetAdminReceiveVerify.invoke(deps, { ...target, enable }),
).resolves.toBeUndefined();
const [cmd, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(cmd).toBe('OidbSvcTrpcTcp.0x904_1');
expect(Buffer.from(bytes).toString('hex')).toBe(
env(
0x904,
1,
[
...v(1, target.groupId),
...m(2, [...v(1, target.userId), ...v(2, 1), ...v(3, enable ? 1 : 0)]),
],
true,
),
);
},
);
it.each(
[
[],
m(4, v(1, target.groupId)),
m(4, [...v(1, 1), ...m(2, v(1, target.userId))]),
m(4, [...v(1, target.groupId), ...m(2, v(1, 1))]),
].map((response) => ({ response })),
)('rejects an unconfirmed target: %j', async ({ response }) => {
await expect(
SetAdminReceiveVerify.invoke(sender(response), { ...target, enable: false }),
).rejects.toThrow();
});
});
@@ -0,0 +1,152 @@
import { describe, expect, it, vi } from 'vitest';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { GetAdminReceiveVerify } from '../../../src/oidb-services/group-admin/get-admin-receive-verify';
import { env, m, s, v } from '../_pb-oracle';
const target = { groupId: 12345, userId: 67890 };
const uid = 'test-member-uid';
function member(flag?: number, memberUid = uid, uin: number | undefined = target.userId) {
return m(2, [
...m(1, [...s(2, memberUid), ...(uin === undefined ? [] : v(4, uin))]),
...(flag === undefined ? [] : v(104, flag)),
]);
}
function response(members: number[], group: number | undefined = target.groupId) {
return m(4, [...(group === undefined ? [] : v(1, group)), ...members]);
}
function sender(body = response(member(0))) {
const result: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.from(body),
};
return {
sendRawPacket: vi.fn(async (_command: string, _bytes: Uint8Array) => result),
resolveUserUid: vi.fn(async (_userId: number, _groupId?: number) => uid),
};
}
describe('administrator receive-verification read', () => {
it('resolves the member once in its group and requests only the verification state', async () => {
const deps = sender();
await expect(GetAdminReceiveVerify.invoke(deps, target)).resolves.toBe(true);
expect(deps.resolveUserUid).toHaveBeenCalledExactlyOnceWith(target.userId, target.groupId);
expect(deps.sendRawPacket).toHaveBeenCalledTimes(1);
const [command, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(command).toBe('OidbSvcTrpcTcp.0xfe7_4');
expect(Buffer.from(bytes).toString('hex')).toBe(
env(
0xfe7,
4,
[
...v(1, target.groupId),
...v(2, 3),
...v(3, 0),
...m(4, v(104, 1)),
...m(5, s(2, uid)),
],
false,
),
);
});
it.each([
[0, true],
[64, false],
[32, true],
[96, false],
[0x80000000, true],
[0xffffffff, false],
[undefined, null],
] as const)('decodes flag %s as %s while preserving missing data', async (flag, enabled) => {
const deps = sender(response(member(flag)));
await expect(GetAdminReceiveVerify.invoke(deps, target)).resolves.toBe(enabled);
});
it('selects the requested member even when unrelated members are returned first', async () => {
const deps = sender(response([...member(64, 'unrelated-member', 10001), ...member(0)]));
await expect(GetAdminReceiveVerify.invoke(deps, target)).resolves.toBe(true);
});
it('accepts UID-bound responses without an optional numeric identity', async () => {
const deps = sender(response(m(2, [...m(1, s(2, uid)), ...v(104, 64)])));
await expect(GetAdminReceiveVerify.invoke(deps, target)).resolves.toBe(false);
});
it('preserves large numeric group and member identifiers', async () => {
const large = { groupId: 2 ** 40 + 17, userId: 2 ** 41 + 19 };
const deps = sender(response(member(0, uid, large.userId), large.groupId));
await expect(GetAdminReceiveVerify.invoke(deps, large)).resolves.toBe(true);
expect(Buffer.from(deps.sendRawPacket.mock.calls[0]![1]).toString('hex')).toBe(
env(
0xfe7,
4,
[
...v(1, large.groupId),
...v(2, 3),
...v(3, 0),
...m(4, v(104, 1)),
...m(5, s(2, uid)),
],
false,
),
);
});
it.each([
['missing group', m(4, member(0)), 'missing or mismatched group'],
['wrong group', response(member(0), 999), 'missing or mismatched group'],
['missing member', response([]), 'exactly one matching member'],
['wrong member', response(member(0, 'other-uid')), 'exactly one matching member'],
[
'missing UID',
response(m(2, [...m(1, v(4, target.userId)), ...v(104, 0)])),
'exactly one matching member',
],
[
'duplicate member',
response([...member(0), ...member(64)]),
'exactly one matching member',
],
['conflicting numeric identity', response(member(0, uid, 999)), 'mismatched member UIN'],
] as const)(
'rejects %s rather than reporting a successful state',
async (_label, body, error) => {
const deps = sender([...body]);
await expect(GetAdminReceiveVerify.invoke(deps, target)).rejects.toThrow(error);
},
);
it('does not send when identity resolution fails', async () => {
const deps = sender();
deps.resolveUserUid.mockRejectedValueOnce(new Error('identity unavailable'));
await expect(GetAdminReceiveVerify.invoke(deps, target)).rejects.toThrow(
'identity unavailable',
);
expect(deps.sendRawPacket).not.toHaveBeenCalled();
});
it('propagates server rejection even if the body contains a valid state', async () => {
const deps = sender([...v(3, 1287), ...response(member(0))]);
await expect(GetAdminReceiveVerify.invoke(deps, target)).rejects.toThrow('OIDB error 1287');
});
it('propagates transport failures without retrying', async () => {
const deps = sender();
deps.sendRawPacket.mockRejectedValueOnce(new Error('transport disconnected'));
await expect(GetAdminReceiveVerify.invoke(deps, target)).rejects.toThrow(
'transport disconnected',
);
expect(deps.sendRawPacket).toHaveBeenCalledTimes(1);
});
it('rejects truncated responses', async () => {
const deps = sender([0x22, 0x80]);
await expect(GetAdminReceiveVerify.invoke(deps, target)).rejects.toThrow();
});
});
@@ -0,0 +1,67 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { Oidb0x8a7Req, Oidb0x8a7Resp } from '@snowluma/proto-defs/oidb-actions/base';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { GetAtAllRemain } from '../../../src/oidb-services/group-admin/get-at-all-remain';
function makeDeps(body?: Oidb0x8a7Resp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<Oidb0x8a7Resp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return {
sendRawPacket: vi.fn(async () => r),
identity: { uin: '10001' } as any,
};
}
describe('GetAtAllRemain namespace', () => {
it('declares 0x8A7_0', () => {
expect(GetAtAllRemain.command).toBe(0x8a7);
expect(GetAtAllRemain.subCommand).toBe(0);
});
it('emits the basic{1,2,3}=1,2,1 cookie + bot uin + groupId, routes to 0x8a7_0', async () => {
const deps = makeDeps({ canAtAll: true, groupRemain: 12, uinRemain: 5 });
await GetAtAllRemain.invoke(deps, { groupId: 12345 });
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x8a7_0');
const env = protobuf_decode<OidbBase<Oidb0x8a7Req>>(bytes);
expect(env.body).toMatchObject({
basic1: 1,
basic2: 2,
basic3: 1,
uin: 10001n,
groupId: 12345n,
});
});
it('decodes canAtAll + groupRemain + uinRemain as plain numbers / boolean', async () => {
const deps = makeDeps({ canAtAll: true, groupRemain: 12, uinRemain: 5 });
const out = await GetAtAllRemain.invoke(deps, { groupId: 12345 });
expect(out).toEqual({
can_at_all: true,
remain_at_all_count_for_group: 12,
remain_at_all_count_for_uin: 5,
});
});
it('falls back to false / 0 when the response body is empty', async () => {
const deps = makeDeps({} as any);
const out = await GetAtAllRemain.invoke(deps, { groupId: 1 });
expect(out).toEqual({
can_at_all: false,
remain_at_all_count_for_group: 0,
remain_at_all_count_for_uin: 0,
});
});
});
@@ -0,0 +1,93 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbGetGroupExtReq,
OidbGetGroupExtResp,
} from '@snowluma/proto-defs/oidb-actions/group-ext';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { FetchGroupExtInfo } from '../../../src/oidb-services/group-admin/get-group-ext-info';
import { env, v, m } from '../_pb-oracle';
function makeSender(body?: OidbGetGroupExtResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<OidbGetGroupExtResp>>({ body }))
: Buffer.alloc(0);
const result: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => result) };
}
describe('FetchGroupExtInfo namespace', () => {
it('declares command 0xEF0 sub 1, uin form', () => {
expect(FetchGroupExtInfo.command).toBe(0xef0);
expect(FetchGroupExtInfo.subCommand).toBe(1);
expect(FetchGroupExtInfo.uinForm).toBe(true);
});
it('byte-oracle: routes to 0xef0_1 and locks {1:groupCode, 2:{29:1,30:1}}', async () => {
const sender = makeSender({
items: [
{
groupCode: 12345n,
ext: { inviteRobotMemberSwitch: 1, inviteRobotMemberExamine: 2 },
},
],
});
await FetchGroupExtInfo.invoke(sender, { groupId: 12345 });
const [cmd, bytes] = sender.sendRawPacket.mock.calls[0]!;
expect(cmd).toBe('OidbSvcTrpcTcp.0xef0_1');
const filter = [...v(29, 1), ...v(30, 1)];
const body = [...v(1, 12345), ...m(2, filter)];
expect(Buffer.from(bytes).toString('hex')).toBe(env(0xef0, 1, body, true));
const envelope = protobuf_decode<OidbBase<OidbGetGroupExtReq>>(bytes);
expect(envelope.reserved).toBe(1);
expect(envelope.body?.groupCodes).toEqual([12345n]);
});
it('returns robot-add switch/examine from the first item', async () => {
const sender = makeSender({
items: [
{
groupCode: 12345n,
ext: { inviteRobotMemberSwitch: 1, inviteRobotMemberExamine: 2 },
},
],
});
await expect(FetchGroupExtInfo.invoke(sender, { groupId: 12345 })).resolves.toEqual({
robotMemberSwitch: 1,
robotMemberExamine: 2,
});
});
it('treats omitted zeros as 0', async () => {
const sender = makeSender({ items: [{ groupCode: 12345n, ext: {} }] });
await expect(FetchGroupExtInfo.invoke(sender, { groupId: 12345 })).resolves.toEqual({
robotMemberSwitch: 0,
robotMemberExamine: 0,
});
});
it('throws when the list is empty', async () => {
const sender = makeSender({ items: [] });
await expect(FetchGroupExtInfo.invoke(sender, { groupId: 12345 })).rejects.toThrow(
/unable to read group robot-add option/,
);
});
it('throws on a non-zero item result', async () => {
const sender = makeSender({
items: [{ groupCode: 12345n, resultCode: 5, ext: { inviteRobotMemberSwitch: 1 } }],
});
await expect(FetchGroupExtInfo.invoke(sender, { groupId: 12345 })).rejects.toThrow(
/result=5/,
);
});
});
@@ -0,0 +1,126 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { OidbKickMember } from '@snowluma/proto-defs/oidb-actions/base';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { KickMember } from '../../../src/oidb-services/group-admin/kick-member';
import { m, s, v } from '../_pb-oracle';
/** 24-byte uid: success items on the wire are `{ result:0, uid }` with uid length 0x18. */
const TRACE_UID = 'u_abcdefghijklmnopqrstuv';
function kickResultItem(result: number, uid: string): number[] {
return [...v(1, result), ...s(2, uid)];
}
function kickResponseBytes(groupUin: number, items: number[][]): Buffer {
const body = [...v(1, groupUin), ...items.flatMap((item) => m(2, item))];
return Buffer.from(m(4, body));
}
function makeDeps(responseData: Buffer = Buffer.alloc(0)) {
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return {
sendRawPacket: vi.fn(async () => r),
resolveUserUid: vi.fn(async () => 'resolved-uid'),
};
}
describe('KickMember namespace', () => {
it('declares 0x8A0_1', () => {
expect(KickMember.command).toBe(0x8a0);
expect(KickMember.subCommand).toBe(1);
});
it('resolves uid per-group and forwards reject + reason', async () => {
const deps = makeDeps();
await KickMember.invoke(deps, {
groupId: 12345,
userId: 67890,
reject: true,
reason: 'bye',
});
expect(deps.resolveUserUid).toHaveBeenCalledWith(67890, 12345);
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x8a0_1');
const env = protobuf_decode<OidbBase<OidbKickMember>>(bytes);
expect(env.body).toMatchObject({
groupUin: 12345,
targetUid: 'resolved-uid',
rejectAddRequest: true,
reason: 'bye',
});
});
it('defaults reason to empty string', async () => {
const deps = makeDeps();
await KickMember.invoke(deps, { groupId: 1, userId: 2, reject: false });
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
const env = protobuf_decode<OidbBase<OidbKickMember>>(bytes);
expect(env.body?.reason ?? '').toBe('');
expect(env.body?.rejectAddRequest ?? false).toBe(false);
});
it('treats an empty body as success', async () => {
await expect(
KickMember.invoke(makeDeps(), {
groupId: 1,
userId: 2,
reject: false,
}),
).resolves.toBeUndefined();
});
it('treats a zero per-member result plus uid as success (#413)', async () => {
const bytes = kickResponseBytes(12345, [kickResultItem(0, TRACE_UID)]);
const decoded = KickMember.decode(bytes);
expect(decoded.body?.groupUin).toBe(12345);
expect(decoded.body?.results).toEqual([{ result: 0, uid: TRACE_UID }]);
await expect(
KickMember.invoke(makeDeps(bytes), {
groupId: 12345,
userId: 67890,
reject: false,
}),
).resolves.toBeUndefined();
});
it('rejects a non-zero per-member result with the numeric code, not the uid (#298)', async () => {
const bytes = kickResponseBytes(12345, [kickResultItem(7, TRACE_UID)]);
await expect(
KickMember.invoke(makeDeps(bytes), {
groupId: 12345,
userId: 67890,
reject: false,
}),
).rejects.toThrow('kick member failed: result=7');
});
it('deserialize: omitted/zero results succeed; non-zero throws', () => {
expect(() => KickMember.deserialize({} as never, {})).not.toThrow();
expect(() => KickMember.deserialize({} as never, { results: [] })).not.toThrow();
expect(() =>
KickMember.deserialize({} as never, {
results: [{ result: 0, uid: TRACE_UID }],
}),
).not.toThrow();
expect(() =>
KickMember.deserialize({} as never, {
results: [{ uid: TRACE_UID }],
}),
).not.toThrow();
expect(() =>
KickMember.deserialize({} as never, {
results: [{ result: 1, uid: TRACE_UID }],
}),
).toThrow('kick member failed: result=1');
});
});
@@ -0,0 +1,114 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { Oidb0x8a0Req } from '@snowluma/proto-defs/oidb-actions/base';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { KickMembers } from '../../../src/oidb-services/group-admin/kick-members';
import { m, s, v } from '../_pb-oracle';
const TRACE_UID_A = 'u_abcdefghijklmnopqrstuv';
const TRACE_UID_B = 'u_bbbbbbbbbbbbbbbbbbbbbb';
function kickResultItem(result: number, uid: string): number[] {
return [...v(1, result), ...s(2, uid)];
}
function kickResponseBytes(groupUin: number, items: number[][]): Buffer {
const body = [...v(1, groupUin), ...items.flatMap((item) => m(2, item))];
return Buffer.from(m(4, body));
}
function makeDeps(
resolveSequence: string[] = ['uid-a', 'uid-b'],
responseData: Buffer = Buffer.alloc(0),
) {
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
const resolveUserUid = vi.fn();
for (const uid of resolveSequence) {
resolveUserUid.mockResolvedValueOnce(uid);
}
return {
sendRawPacket: vi.fn(async () => r),
resolveUserUid,
};
}
describe('KickMembers namespace', () => {
it('declares 0x8A0_1 (same as KickMember; disambiguated by proto body)', () => {
expect(KickMembers.command).toBe(0x8a0);
expect(KickMembers.subCommand).toBe(1);
});
it('resolves each uid in parallel and packages targetUids[]', async () => {
const deps = makeDeps(['uid-a', 'uid-b']);
await KickMembers.invoke(deps, { groupId: 12345, userIds: [11, 22], reject: false });
expect(deps.resolveUserUid).toHaveBeenCalledTimes(2);
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x8a0_1');
const env = protobuf_decode<OidbBase<Oidb0x8a0Req>>(bytes);
expect(env.body?.targetUids).toEqual(['uid-a', 'uid-b']);
expect(env.body?.groupId).toBe(12345n);
expect(env.body?.rejectAddRequest ?? 0).toBe(0);
});
it('reject=true => rejectAddRequest=1', async () => {
const deps = makeDeps(['u']);
await KickMembers.invoke(deps, { groupId: 1, userIds: [2], reject: true });
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
const env = protobuf_decode<OidbBase<Oidb0x8a0Req>>(bytes);
expect(env.body?.rejectAddRequest).toBe(1);
});
it('treats an empty body as success', async () => {
await expect(
KickMembers.invoke(makeDeps(['u']), {
groupId: 1,
userIds: [2],
reject: false,
}),
).resolves.toBeUndefined();
});
it('treats zero per-member results as success (#413)', async () => {
const bytes = kickResponseBytes(12345, [
kickResultItem(0, TRACE_UID_A),
kickResultItem(0, TRACE_UID_B),
]);
const decoded = KickMembers.decode(bytes);
expect(decoded.body?.results).toEqual([
{ result: 0, uid: TRACE_UID_A },
{ result: 0, uid: TRACE_UID_B },
]);
await expect(
KickMembers.invoke(makeDeps(['uid-a', 'uid-b'], bytes), {
groupId: 12345,
userIds: [11, 22],
reject: false,
}),
).resolves.toBeUndefined();
});
it('rejects when any per-member result is non-zero, with the numeric code', async () => {
const bytes = kickResponseBytes(12345, [
kickResultItem(0, TRACE_UID_A),
kickResultItem(7, TRACE_UID_B),
]);
await expect(
KickMembers.invoke(makeDeps(['uid-a', 'uid-b'], bytes), {
groupId: 12345,
userIds: [11, 22],
reject: false,
}),
).rejects.toThrow('kick members failed: result=7');
});
});
@@ -0,0 +1,34 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { OidbLeaveGroup } from '@snowluma/proto-defs/oidb-actions/base';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { LeaveGroup } from '../../../src/oidb-services/group-admin/leave-group';
function makeDeps() {
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.alloc(0),
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('LeaveGroup namespace', () => {
it('declares 0x1097_1', () => {
expect(LeaveGroup.command).toBe(0x1097);
expect(LeaveGroup.subCommand).toBe(1);
});
it('routes to 0x1097_1 with groupUin only', async () => {
const deps = makeDeps();
await LeaveGroup.invoke(deps, { groupId: 12345 });
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x1097_1');
const env = protobuf_decode<OidbBase<OidbLeaveGroup>>(bytes);
expect(env.body).toMatchObject({ groupUin: 12345 });
});
});
@@ -0,0 +1,95 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { OidbModifyGroupExtResp } from '@snowluma/proto-defs/oidb-actions/group-ext';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { ModifyGroupExtInfo } from '../../../src/oidb-services/group-admin/modify-group-ext-info';
import { env, v, m } from '../_pb-oracle';
function makeSender(result = 0) {
const respEnv: OidbBase<OidbModifyGroupExtResp> = {
command: 0xf00,
subCommand: 3,
body: { groupCode: 12345, result },
};
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.from(protobuf_encode<OidbBase<OidbModifyGroupExtResp>>(respEnv)),
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('ModifyGroupExtInfo namespace', () => {
it('declares command 0xF00 sub 3, uin form', () => {
expect(ModifyGroupExtInfo.command).toBe(0xf00);
expect(ModifyGroupExtInfo.subCommand).toBe(3);
expect(ModifyGroupExtInfo.uinForm).toBe(true);
});
it('only includes the fields the caller provides', () => {
expect(
ModifyGroupExtInfo.serialize({} as any, { groupId: 1, robotMemberSwitch: 1 }),
).toEqual({
groupCode: 1,
info: { groupCode: 1, ext: { inviteRobotMemberSwitch: 1 } },
});
expect(ModifyGroupExtInfo.serialize({} as any, { groupId: 1 })).toEqual({
groupCode: 1,
info: { groupCode: 1, ext: {} },
});
});
it('byte-oracle: routes to 0xf00_3 and locks nested tags {1:gc,2:{1:gc,2:{29:switch,30:examine}}}', async () => {
const sender = makeSender();
await ModifyGroupExtInfo.invoke(sender, {
groupId: 12345,
robotMemberSwitch: 1,
robotMemberExamine: 2,
});
const [cmd, bytes] = sender.sendRawPacket.mock.calls[0]!;
expect(cmd).toBe('OidbSvcTrpcTcp.0xf00_3');
const ext = [...v(29, 1), ...v(30, 2)];
const info = [...v(1, 12345), ...m(2, ext)];
const body = [...v(1, 12345), ...m(2, info)];
expect(Buffer.from(bytes).toString('hex')).toBe(env(0xf00, 3, body, true));
});
it('emits switch=0 on the wire (explicit presence; omit would look like "unchanged")', async () => {
const sender = makeSender();
await ModifyGroupExtInfo.invoke(sender, { groupId: 12345, robotMemberSwitch: 0 });
const [, bytes] = sender.sendRawPacket.mock.calls[0]!;
const ext = [...v(29, 0)];
const info = [...v(1, 12345), ...m(2, ext)];
const body = [...v(1, 12345), ...m(2, info)];
expect(Buffer.from(bytes).toString('hex')).toBe(env(0xf00, 3, body, true));
});
it('emits both zeros when the caller sets switch and examine to 0', async () => {
const sender = makeSender();
await ModifyGroupExtInfo.invoke(sender, {
groupId: 12345,
robotMemberSwitch: 0,
robotMemberExamine: 0,
});
const [, bytes] = sender.sendRawPacket.mock.calls[0]!;
const ext = [...v(29, 0), ...v(30, 0)];
const info = [...v(1, 12345), ...m(2, ext)];
const body = [...v(1, 12345), ...m(2, info)];
expect(Buffer.from(bytes).toString('hex')).toBe(env(0xf00, 3, body, true));
});
it('does not send when neither field is provided', async () => {
const sender = makeSender();
await ModifyGroupExtInfo.invoke(sender, { groupId: 12345 });
expect(sender.sendRawPacket).not.toHaveBeenCalled();
});
it('deserialize throws on a non-zero body result', () => {
expect(() => ModifyGroupExtInfo.deserialize({} as any, { result: 5 })).toThrow(/result=5/);
expect(() => ModifyGroupExtInfo.deserialize({} as any, { result: 0 })).not.toThrow();
});
});
@@ -0,0 +1,51 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { OidbMuteAll } from '@snowluma/proto-defs/oidb-actions/base';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { MuteAll } from '../../../src/oidb-services/group-admin/mute-all';
function makeDeps() {
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.alloc(0),
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('MuteAll namespace', () => {
it('declares 0x89A_0', () => {
expect(MuteAll.command).toBe(0x89a);
expect(MuteAll.subCommand).toBe(0);
});
it('enable=true emits the 0xFFFFFFFF "permanent" state', async () => {
const deps = makeDeps();
await MuteAll.invoke(deps, { groupId: 12345, enable: true });
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x89a_0');
const env = protobuf_decode<OidbBase<OidbMuteAll>>(bytes);
expect(env.body).toMatchObject({ groupUin: 12345, muteState: { state: 0xffffffff } });
});
it('enable=false forces state=0 ONTO the wire (so the server can disambiguate)', async () => {
const deps = makeDeps();
await MuteAll.invoke(deps, { groupId: 12345, enable: false });
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
const env = protobuf_decode<OidbBase<OidbMuteAll>>(bytes);
expect(env.body?.muteState?.state).toBe(0);
// The fix (issue #70): state=0 is a `pb_optional<>` field, so it must be
// PRESENT on the wire — field 17 (varint tag 0x88 0x01) + value 0x00 — not
// omitted. An empty `muteState` is indistinguishable from the other
// commands sharing OIDB (0x89A, 0), so the server rejects unmute with 1007.
const arr = Array.from(bytes);
const hasStateField = arr.some(
(_, i) => arr[i] === 0x88 && arr[i + 1] === 0x01 && arr[i + 2] === 0x00,
);
expect(hasStateField).toBe(true);
});
});
@@ -0,0 +1,51 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { OidbMuteMember } from '@snowluma/proto-defs/oidb-actions/base';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { MuteMember } from '../../../src/oidb-services/group-admin/mute-member';
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('MuteMember namespace', () => {
it('declares 0x1253_1', () => {
expect(MuteMember.command).toBe(0x1253);
expect(MuteMember.subCommand).toBe(1);
});
it('resolves uid before encoding and routes to 0x1253_1', async () => {
const deps = makeDeps();
await MuteMember.invoke(deps, { groupId: 12345, userId: 67890, duration: 600 });
expect(deps.resolveUserUid).toHaveBeenCalledWith(67890, 12345);
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x1253_1');
const env = protobuf_decode<OidbBase<OidbMuteMember>>(bytes);
expect(env.body).toMatchObject({
groupUin: 12345,
type: 1,
body: { targetUid: 'resolved-uid', duration: 600 },
});
});
it('duration = 0 acts as unmute', async () => {
const deps = makeDeps();
await MuteMember.invoke(deps, { groupId: 1, userId: 2, duration: 0 });
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
const env = protobuf_decode<OidbBase<OidbMuteMember>>(bytes);
// duration=0 is the proto3 default and is omitted on the wire.
expect(env.body?.body?.duration ?? 0).toBe(0);
});
});
@@ -0,0 +1,91 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { Oidb0x89a_0AddOption } from '@snowluma/proto-defs/oidb-actions/base';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import {
isQuestionAddType,
SetAddOption,
wireAddType,
} from '../../../src/oidb-services/group-admin/set-add-option';
import { env, v, s, m } 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) };
}
describe('SetAddOption namespace', () => {
it('declares 0x89A_0 (shared cmd with MuteAll / SetSearch / SetGroupName)', () => {
expect(SetAddOption.command).toBe(0x89a);
expect(SetAddOption.subCommand).toBe(0);
});
it('packages settings.addType and routes to 0x89a_0', async () => {
const deps = makeDeps();
await SetAddOption.invoke(deps, { groupId: 12345, addType: 2 });
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x89a_0');
const decoded = protobuf_decode<OidbBase<Oidb0x89a_0AddOption>>(bytes);
expect(decoded.body).toMatchObject({ groupUin: 12345n, settings: { addType: 2 } });
expect(decoded.body?.settings?.groupQuestion ?? undefined).toBeUndefined();
expect(decoded.body?.settings?.groupAnswer ?? undefined).toBeUndefined();
});
it('treats 4 as question+answer and 5/55 as question-only', () => {
expect(isQuestionAddType(4)).toBe(true);
expect(isQuestionAddType(5)).toBe(true);
expect(isQuestionAddType(55)).toBe(true);
expect(isQuestionAddType(2)).toBe(false);
expect(wireAddType(55)).toBe(5);
expect(wireAddType(4)).toBe(4);
});
it('byte-oracle: type 4 writes question and answer', async () => {
const deps = makeDeps();
await SetAddOption.invoke(deps, {
groupId: 12345,
addType: 4,
groupQuestion: 'q',
groupAnswer: 'a',
});
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
const settings = [...v(16, 4), ...s(30, 'q'), ...s(31, 'a')];
const body = [...v(1, 12345), ...m(2, settings)];
expect(Buffer.from(bytes).toString('hex')).toBe(env(0x89a, 0, body, false));
});
it('byte-oracle: type 5 writes question and clears answer', async () => {
const deps = makeDeps();
await SetAddOption.invoke(deps, {
groupId: 12345,
addType: 5,
groupQuestion: 'why',
groupAnswer: 'ignored',
});
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
const settings = [...v(16, 5), ...s(30, 'why'), ...s(31, '')];
const body = [...v(1, 12345), ...m(2, settings)];
expect(Buffer.from(bytes).toString('hex')).toBe(env(0x89a, 0, body, false));
});
it('maps add_type 55 onto the official question-only mode', async () => {
const deps = makeDeps();
await SetAddOption.invoke(deps, { groupId: 1, addType: 55, groupQuestion: 'q' });
const decoded = protobuf_decode<OidbBase<Oidb0x89a_0AddOption>>(
deps.sendRawPacket.mock.calls[0]![1],
);
expect(decoded.body?.settings).toMatchObject({
addType: 5,
groupQuestion: 'q',
groupAnswer: '',
});
});
});
@@ -0,0 +1,110 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { OidbGroupRequestAction } from '@snowluma/proto-defs/oidb-actions/base';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { SetAddRequest } from '../../../src/oidb-services/group-admin/set-add-request';
function makeDeps() {
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.alloc(0),
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('SetAddRequest namespace', () => {
it('declares 0x10C8 with uinForm=true and dynamic subCommand', () => {
expect(SetAddRequest.command).toBe(0x10c8);
expect(SetAddRequest.uinForm).toBe(true);
expect(
SetAddRequest.resolveSubCommand({
groupId: 1,
sequence: 1,
eventType: 0,
approve: true,
filtered: false,
}),
).toBe(1);
expect(
SetAddRequest.resolveSubCommand({
groupId: 1,
sequence: 1,
eventType: 0,
approve: true,
filtered: true,
}),
).toBe(2);
});
it('approve=true => accept=1, filtered=false => 0x10c8_1, reserved=1 (uinForm)', async () => {
const deps = makeDeps();
await SetAddRequest.invoke(deps, {
groupId: 12345,
sequence: 5,
eventType: 1,
approve: true,
reason: 'ok',
});
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x10c8_1');
const env = protobuf_decode<OidbBase<OidbGroupRequestAction>>(bytes);
expect(env.reserved).toBe(1);
expect(env.body?.accept).toBe(1);
expect(env.body?.body).toMatchObject({
sequence: 5n,
eventType: 1,
groupUin: 12345,
message: 'ok',
});
});
it('approve=false + filtered=true => accept=2, 0x10c8_2', async () => {
const deps = makeDeps();
await SetAddRequest.invoke(deps, {
groupId: 12345,
sequence: 5,
eventType: 1,
approve: false,
reason: 'no',
filtered: true,
});
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x10c8_2');
const env = protobuf_decode<OidbBase<OidbGroupRequestAction>>(bytes);
expect(env.body?.accept).toBe(2);
});
it('writes a space when the reason is empty so field 4 stays present', async () => {
const deps = makeDeps();
await SetAddRequest.invoke(deps, { groupId: 1, sequence: 1, eventType: 1, approve: true });
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
const env = protobuf_decode<OidbBase<OidbGroupRequestAction>>(bytes);
expect(env.body?.body?.message).toBe(' ');
const arr = Array.from(bytes);
const hasMessageField = arr.some(
(_, i) => arr[i] === 0x22 && arr[i + 1] === 0x01 && arr[i + 2] === 0x20,
);
expect(hasMessageField).toBe(true);
});
it('forwards operateTransInfo as body field 7', async () => {
const deps = makeDeps();
const operateTransInfo = Uint8Array.from([0x01, 0x02, 0x03]);
await SetAddRequest.invoke(deps, {
groupId: 1,
sequence: 1,
eventType: 1,
approve: true,
reason: 'ok',
operateTransInfo,
});
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
const env = protobuf_decode<OidbBase<OidbGroupRequestAction>>(bytes);
expect(env.body?.body?.operateTransInfo).toEqual(operateTransInfo);
});
});
@@ -0,0 +1,46 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { OidbSetAdmin } from '@snowluma/proto-defs/oidb-actions/base';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { SetAdmin } from '../../../src/oidb-services/group-admin/set-admin';
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('SetAdmin namespace', () => {
it('declares 0x1096_1', () => {
expect(SetAdmin.command).toBe(0x1096);
expect(SetAdmin.subCommand).toBe(1);
});
it('enable=true promotes (isAdmin=true)', async () => {
const deps = makeDeps();
await SetAdmin.invoke(deps, { groupId: 12345, userId: 67890, enable: true });
expect(deps.resolveUserUid).toHaveBeenCalledWith(67890, 12345);
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x1096_1');
const env = protobuf_decode<OidbBase<OidbSetAdmin>>(bytes);
expect(env.body).toMatchObject({ groupUin: 12345, uid: 'resolved-uid', isAdmin: true });
});
it('enable=false demotes (isAdmin omitted on the wire)', async () => {
const deps = makeDeps();
await SetAdmin.invoke(deps, { groupId: 1, userId: 2, enable: false });
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
const env = protobuf_decode<OidbBase<OidbSetAdmin>>(bytes);
expect(env.body?.isAdmin ?? false).toBe(false);
});
});
@@ -0,0 +1,34 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { OidbRenameGroup } from '@snowluma/proto-defs/oidb-actions/base';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { SetGroupName } from '../../../src/oidb-services/group-admin/set-group-name';
function makeDeps() {
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.alloc(0),
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('SetGroupName namespace', () => {
it('declares 0x89A_15', () => {
expect(SetGroupName.command).toBe(0x89a);
expect(SetGroupName.subCommand).toBe(15);
});
it('routes to 0x89a_15 with the new name', async () => {
const deps = makeDeps();
await SetGroupName.invoke(deps, { groupId: 12345, name: 'newName' });
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x89a_15');
const env = protobuf_decode<OidbBase<OidbRenameGroup>>(bytes);
expect(env.body).toMatchObject({ groupUin: 12345, body: { targetName: 'newName' } });
});
});
@@ -0,0 +1,60 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { Oidb0xf16Req } from '@snowluma/proto-defs/oidb-actions/base';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { SetGroupRemark } from '../../../src/oidb-services/group-admin/set-group-remark';
function makeDeps() {
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.alloc(0),
};
return {
sendRawPacket: vi.fn(async () => r),
identity: { uin: '10001', updateGroupRemark: vi.fn(() => true) },
};
}
describe('SetGroupRemark namespace', () => {
it('declares 0xF16_1', () => {
expect(SetGroupRemark.command).toBe(0xf16);
expect(SetGroupRemark.subCommand).toBe(1);
});
it('routes to 0xf16_1 with groupId + remark wrapped in inner', async () => {
const deps = makeDeps();
await SetGroupRemark.invoke(deps, { groupId: 12345, remark: 'my group' });
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0xf16_1');
const env = protobuf_decode<OidbBase<Oidb0xf16Req>>(bytes);
expect(env.body?.inner).toMatchObject({ groupId: 12345n, remark: 'my group' });
});
it('updates the local group roster after QQ confirms the remark', async () => {
const deps = makeDeps();
await SetGroupRemark.invoke(deps as any, { groupId: 12345, remark: 'my group' });
expect(deps.identity.updateGroupRemark).toHaveBeenCalledWith(12345, 'my group');
});
it('does not update the local roster when QQ rejects the mutation', async () => {
const deps = makeDeps();
deps.sendRawPacket.mockResolvedValueOnce({
success: false,
gotResponse: true,
errorCode: 120,
errorMessage: 'denied',
responseData: Buffer.alloc(0),
});
await expect(
SetGroupRemark.invoke(deps as any, { groupId: 12345, remark: 'not-applied' }),
).rejects.toThrow();
expect(deps.identity.updateGroupRemark).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,38 @@
import { describe, expect, it, vi } from 'vitest';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { SetMemberCard } from '../../../src/oidb-services/group-admin/set-member-card';
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<SetMemberCard.Deps['sendRawPacket']>().mockResolvedValue(r),
resolveUserUid: vi.fn(async () => 'resolved-uid'),
};
}
describe('SetMemberCard namespace', () => {
it('declares 0x8FC_3', () => {
expect(SetMemberCard.command).toBe(0x8fc);
expect(SetMemberCard.subCommand).toBe(3);
});
it.each(['', 'newCard', '群名片'])('sends card %j in field 8', async (card) => {
const deps = makeDeps();
await SetMemberCard.invoke(deps, { groupId: 12345, userId: 67890, card });
expect(deps.resolveUserUid).toHaveBeenCalledWith(67890, 12345);
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x8fc_3');
const member = [...s(1, 'resolved-uid'), ...s(8, card)];
const body = [...v(1, 12345), ...m(3, member)];
expect(Buffer.from(bytes).toString('hex')).toBe(env(0x8fc, 3, body, false));
});
});
@@ -0,0 +1,109 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { Oidb0x89a_0InvitePolicy } from '@snowluma/proto-defs/oidb-actions/base';
import {
MEMBER_INVITE_PRIVILEGE_MASK,
decodeMemberInvitePolicy,
mergeMemberInvitePrivilegeFlag,
SetMemberInvitePolicy,
type GroupMemberInvitePolicy,
} from '../../../src/oidb-services/group-admin/set-member-invite-policy';
function makeSender() {
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.alloc(0),
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('SetMemberInvitePolicy namespace', () => {
it.each<[GroupMemberInvitePolicy, number, number]>([
['disabled', 0x04000000, 0],
['require_approval', 0, 1],
['no_approval', 0x00100000, 1],
['no_approval_under_100', 0x02000000, 1],
])('maps %s to the verified privilege bits', async (policy, policyBits, allowMemberInvite) => {
const sender = makeSender();
const unrelatedBits = 0x80000001;
await SetMemberInvitePolicy.invoke(sender, {
groupId: 12345,
currentPrivilegeFlag: unrelatedBits + 0x06100000,
policy,
});
const [cmd, bytes] = sender.sendRawPacket.mock.calls[0]!;
expect(cmd).toBe('OidbSvcTrpcTcp.0x89a_0');
const env = protobuf_decode<OidbBase<Oidb0x89a_0InvitePolicy>>(bytes);
expect(env.command).toBe(0x89a);
expect(env.body?.groupUin).toBe(12345n);
expect(env.body?.settings).toEqual({
appPrivilegeFlag: unrelatedBits + policyBits,
appPrivilegeMask: MEMBER_INVITE_PRIVILEGE_MASK,
allowMemberInvite,
});
});
it('preserves unrelated bits while replacing only the member-invite mask', () => {
expect(mergeMemberInvitePrivilegeFlag(0xe7100001, 'require_approval')).toBe(0xe1000001);
});
it.each<[number, GroupMemberInvitePolicy]>([
[0x04000000, 'disabled'],
[0, 'require_approval'],
[0x00100000, 'no_approval'],
[0x02000000, 'no_approval_under_100'],
[0x80100001, 'no_approval'],
[0x84000001, 'disabled'],
])('decodes privilege %# as %s', (privilegeFlag, policy) => {
expect(decodeMemberInvitePolicy(privilegeFlag)).toBe(policy);
});
it('round-trips each invite policy through merge then decode', () => {
const policies: GroupMemberInvitePolicy[] = [
'disabled',
'require_approval',
'no_approval',
'no_approval_under_100',
];
for (const policy of policies) {
expect(
decodeMemberInvitePolicy(mergeMemberInvitePrivilegeFlag(0x80000001, policy)),
).toBe(policy);
}
});
it('keeps explicit zero fields on the wire', async () => {
const sender = makeSender();
await SetMemberInvitePolicy.invoke(sender, {
groupId: 1,
currentPrivilegeFlag: MEMBER_INVITE_PRIVILEGE_MASK,
policy: 'require_approval',
});
const env = protobuf_decode<OidbBase<Oidb0x89a_0InvitePolicy>>(
sender.sendRawPacket.mock.calls[0]![1],
);
expect(env.body?.settings?.appPrivilegeFlag).toBe(0);
expect(env.body?.settings?.allowMemberInvite).toBe(1);
});
it('rejects an invalid current flag before sending a packet', async () => {
const sender = makeSender();
await expect(
SetMemberInvitePolicy.invoke(sender, {
groupId: 1,
currentPrivilegeFlag: -1,
policy: 'disabled',
}),
).rejects.toThrow(/unsigned 32-bit/);
expect(sender.sendRawPacket).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,141 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { Oidb0x89a_0MemberPermission } from '@snowluma/proto-defs/oidb-actions/base';
import {
decodeGroupMemberPermissions,
GROUP_MEMBER_PERMISSION_MASKS,
mergeGroupMemberPermission,
SetMemberPermission,
type GroupMemberPermission,
} from '../../../src/oidb-services/group-admin/set-member-permission';
function makeSender() {
const result: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.alloc(0),
};
return { sendRawPacket: vi.fn(async () => result) };
}
describe('SetMemberPermission namespace', () => {
it('declares 0x89A_0', () => {
expect(SetMemberPermission.command).toBe(0x89a);
expect(SetMemberPermission.subCommand).toBe(0);
});
it.each<[GroupMemberPermission, number]>([
['upload_album', 0x1],
['temporary_session', 0x10000],
['create_group', 0x8000],
])('uses the verified deny-bit mask for %s', async (permission, mask) => {
const sender = makeSender();
await SetMemberPermission.invoke(sender, {
groupId: 12345,
currentPrivilegeFlag: 0x80018001,
permission,
allow: true,
});
const [command, bytes] = sender.sendRawPacket.mock.calls[0]!;
expect(command).toBe('OidbSvcTrpcTcp.0x89a_0');
const envelope = protobuf_decode<OidbBase<Oidb0x89a_0MemberPermission>>(bytes);
expect(envelope.command).toBe(0x89a);
expect(envelope.subCommand ?? 0).toBe(0);
expect(envelope.body).toMatchObject({
groupUin: 12345n,
settings: {
appPrivilegeFlag: Number(BigInt(0x80018001) & ~BigInt(mask)),
appPrivilegeMask: mask,
},
});
});
it('sets a deny bit when a capability is disabled', async () => {
const sender = makeSender();
await SetMemberPermission.invoke(sender, {
groupId: 12345,
currentPrivilegeFlag: 0x80000000,
permission: 'temporary_session',
allow: false,
});
const envelope = protobuf_decode<OidbBase<Oidb0x89a_0MemberPermission>>(
sender.sendRawPacket.mock.calls[0]![1],
);
expect(envelope.body?.settings).toEqual({
appPrivilegeFlag: 0x80010000,
appPrivilegeMask: GROUP_MEMBER_PERMISSION_MASKS.temporary_session,
});
});
it('preserves unrelated bits while changing only the selected capability', () => {
expect(mergeGroupMemberPermission(0xf0018001, 'upload_album', true)).toBe(0xf0018000);
expect(mergeGroupMemberPermission(0xf0000000, 'create_group', false)).toBe(0xf0008000);
});
it('treats permission bits as deny flags when decoding', () => {
expect(decodeGroupMemberPermissions(0)).toEqual({
allowMemberUploadAlbum: true,
allowMemberTemporarySession: true,
allowMemberCreateGroup: true,
});
expect(decodeGroupMemberPermissions(0x80018001)).toEqual({
allowMemberUploadAlbum: false,
allowMemberTemporarySession: false,
allowMemberCreateGroup: false,
});
expect(decodeGroupMemberPermissions(0x80000000)).toEqual({
allowMemberUploadAlbum: true,
allowMemberTemporarySession: true,
allowMemberCreateGroup: true,
});
});
it('round-trips each permission through merge then decode', () => {
let flag = 0x80000000;
flag = mergeGroupMemberPermission(flag, 'upload_album', false);
flag = mergeGroupMemberPermission(flag, 'temporary_session', true);
flag = mergeGroupMemberPermission(flag, 'create_group', false);
expect(decodeGroupMemberPermissions(flag)).toEqual({
allowMemberUploadAlbum: false,
allowMemberTemporarySession: true,
allowMemberCreateGroup: false,
});
});
it('keeps explicit zero fields on the wire', async () => {
const sender = makeSender();
await SetMemberPermission.invoke(sender, {
groupId: 1,
currentPrivilegeFlag: 1,
permission: 'upload_album',
allow: true,
});
const envelope = protobuf_decode<OidbBase<Oidb0x89a_0MemberPermission>>(
sender.sendRawPacket.mock.calls[0]![1],
);
expect(envelope.body?.settings?.appPrivilegeFlag).toBe(0);
expect(envelope.body?.settings?.appPrivilegeMask).toBe(1);
});
it('rejects an invalid current flag before sending', async () => {
const sender = makeSender();
await expect(
SetMemberPermission.invoke(sender, {
groupId: 1,
currentPrivilegeFlag: 0x1_0000_0000,
permission: 'upload_album',
allow: true,
}),
).rejects.toThrow(/unsigned 32-bit/);
expect(sender.sendRawPacket).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,94 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { Oidb0x89a_0HistoryVisibility } from '@snowluma/proto-defs/oidb-actions/base';
import {
decodeGroupHistoryVisibility,
GROUP_HISTORY_VISIBILITY_MASK,
mergeGroupHistoryVisibility,
SetNewMemberHistoryVisibility,
} from '../../../src/oidb-services/group-admin/set-new-member-history-visibility';
function makeSender() {
const result: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.alloc(0),
};
return { sendRawPacket: vi.fn(async () => result) };
}
describe('SetNewMemberHistoryVisibility namespace', () => {
it.each([
[true, GROUP_HISTORY_VISIBILITY_MASK],
[false, 0],
])('writes visible=%s as the mutation bit only', async (visible, expected) => {
const sender = makeSender();
await SetNewMemberHistoryVisibility.invoke(sender, {
groupId: 12345,
visible,
});
const [command, bytes] = sender.sendRawPacket.mock.calls[0]!;
expect(command).toBe('OidbSvcTrpcTcp.0x89a_0');
const envelope = protobuf_decode<OidbBase<Oidb0x89a_0HistoryVisibility>>(bytes);
expect(envelope.command).toBe(0x89a);
expect(envelope.body).toMatchObject({
groupUin: 12345n,
settings: {
groupFlagExt4: expected,
groupFlagExt4Mask: GROUP_HISTORY_VISIBILITY_MASK,
},
});
});
it('merges only the verified history-visibility bit', () => {
expect(mergeGroupHistoryVisibility(0xf0000001, true)).toBe(0xf0000005);
expect(mergeGroupHistoryVisibility(0xf0000005, false)).toBe(0xf0000001);
});
it('decodes the history-visibility switch from a 0/1 reply or the mutation bit', () => {
expect(decodeGroupHistoryVisibility(0)).toBe(false);
expect(decodeGroupHistoryVisibility(1)).toBe(true);
expect(decodeGroupHistoryVisibility(GROUP_HISTORY_VISIBILITY_MASK)).toBe(true);
expect(decodeGroupHistoryVisibility(0x80000001)).toBe(false);
expect(decodeGroupHistoryVisibility(0x80000005)).toBe(true);
});
it('round-trips history visibility through merge then decode', () => {
expect(decodeGroupHistoryVisibility(mergeGroupHistoryVisibility(0x80000001, true))).toBe(
true,
);
expect(decodeGroupHistoryVisibility(mergeGroupHistoryVisibility(0x80000005, false))).toBe(
false,
);
});
it('does not treat a 0/1 detail reply as a missing mutation bit (#387)', () => {
expect(decodeGroupHistoryVisibility(1)).toBe(true);
expect(decodeGroupHistoryVisibility(mergeGroupHistoryVisibility(0, true))).toBe(true);
});
it('keeps an explicit zero value on the wire', async () => {
const sender = makeSender();
await SetNewMemberHistoryVisibility.invoke(sender, {
groupId: 1,
visible: false,
});
const envelope = protobuf_decode<OidbBase<Oidb0x89a_0HistoryVisibility>>(
sender.sendRawPacket.mock.calls[0]![1],
);
expect(envelope.body?.settings?.groupFlagExt4).toBe(0);
expect(envelope.body?.settings?.groupFlagExt4Mask).toBe(GROUP_HISTORY_VISIBILITY_MASK);
});
it('rejects an invalid current flag when merging', () => {
expect(() => mergeGroupHistoryVisibility(-1, true)).toThrow(/unsigned 32-bit/);
});
});
@@ -0,0 +1,34 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { Oidb0x89a_0Search } from '@snowluma/proto-defs/oidb-actions/base';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { SetSearch } from '../../../src/oidb-services/group-admin/set-search';
function makeDeps() {
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.alloc(0),
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('SetSearch namespace', () => {
it('declares 0x89A_0 (shared cmd shape with other 0x89a_0 cmds)', () => {
expect(SetSearch.command).toBe(0x89a);
expect(SetSearch.subCommand).toBe(0);
});
it('emits an empty settings byte string and routes to 0x89a_0', async () => {
const deps = makeDeps();
await SetSearch.invoke(deps, { groupId: 12345 });
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x89a_0');
const env = protobuf_decode<OidbBase<Oidb0x89a_0Search>>(bytes);
expect(env.body?.groupUin).toBe(12345n);
});
});
@@ -0,0 +1,64 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { OidbSpecialTitle } from '@snowluma/proto-defs/oidb-actions/base';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { SetSpecialTitle } from '../../../src/oidb-services/group-admin/set-special-title';
import { env, v, s, m } 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('SetSpecialTitle namespace', () => {
it('declares 0x8FC_2', () => {
expect(SetSpecialTitle.command).toBe(0x8fc);
expect(SetSpecialTitle.subCommand).toBe(2);
});
it('routes to 0x8fc_2 with -1 (permanent) expireTime sentinel', async () => {
const deps = makeDeps();
await SetSpecialTitle.invoke(deps, { groupId: 12345, userId: 67890, title: 'crown' });
expect(deps.resolveUserUid).toHaveBeenCalledWith(67890, 12345);
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x8fc_2');
const env = protobuf_decode<OidbBase<OidbSpecialTitle>>(bytes);
expect(env.body?.groupUin).toBe(12345);
expect(env.body?.body?.targetUid).toBe('resolved-uid');
expect(env.body?.body?.specialTitle).toBe('crown');
// uinName (tag 7) must mirror specialTitle or the server silently no-ops.
expect(env.body?.body?.uinName).toBe('crown');
// int_32 preserves the signed -1 sentinel after the wire round-trip.
expect(env.body?.body?.expireTime).toBe(-1);
});
it.each(['', ' ', '龙王👑'])('preserves title %j in both wire fields', async (title) => {
const deps = makeDeps();
await SetSpecialTitle.invoke(deps, { groupId: 12345, userId: 67890, title });
expect(deps.resolveUserUid).toHaveBeenCalledWith(67890, 12345);
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x8fc_2');
// Empty titles must emit tag 5 + length 0 and tag 7 + length 0:
// omitting either field turns clearing the title into a silent no-op (#517).
const member = [
...s(1, 'resolved-uid'),
...s(5, title),
...v(6, 0xffffffff),
...s(7, title),
];
const body = [...v(1, 12345), ...m(3, member)];
expect(Buffer.from(bytes).toString('hex')).toBe(env(0x8fc, 2, body, false));
});
});