Files
TanTan fa9c73363d
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
SnowLuma v2.4.2 source snapshot
2026-11-13 09:02:00 +08:00

152 lines
5.3 KiB
TypeScript

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