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
189 lines
6.6 KiB
TypeScript
189 lines
6.6 KiB
TypeScript
import type { PacketInfo } from '@snowluma/common/protocol-types';
|
|
import { BridgeEventBus } from '@snowluma/protocol/event-bus';
|
|
import type { GroupInviteEvent, QQEventVariant } from '@snowluma/protocol/events';
|
|
import { IdentityService } from '@snowluma/protocol/identity-service';
|
|
import { GroupInviteTracker } from '@snowluma/protocol/group-invite-tracker';
|
|
import { IncomingPacketPipeline } from '@snowluma/protocol/packet-pipeline';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const SELF_UIN = 10001;
|
|
const GROUP_ID = 12345;
|
|
const INVITOR_UID = 'u_inviter';
|
|
const INVITOR_UIN = 20002;
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
function packet(): PacketInfo {
|
|
return {
|
|
pid: 42,
|
|
uin: String(SELF_UIN),
|
|
serviceCmd: 'test.self-invite',
|
|
seqId: 1,
|
|
retCode: 0,
|
|
fromClient: false,
|
|
body: Buffer.from([0x00]),
|
|
};
|
|
}
|
|
|
|
/** The MSF notice: no approval msgseq, only the legacy flag from the decoder. */
|
|
function selfInviteNotice(): GroupInviteEvent {
|
|
return {
|
|
kind: 'group_invite',
|
|
time: 1,
|
|
selfUin: SELF_UIN,
|
|
groupId: GROUP_ID,
|
|
fromUin: INVITOR_UIN,
|
|
fromUid: INVITOR_UID,
|
|
invitedUin: SELF_UIN,
|
|
invitedUid: undefined,
|
|
subType: 'invite',
|
|
message: '',
|
|
flag: `invite:${GROUP_ID}:${INVITOR_UID}`,
|
|
};
|
|
}
|
|
|
|
/** The private "qun.invite" card, which carries the real approval msgseq. */
|
|
function inviteCard(sequence = 777): QQEventVariant {
|
|
return {
|
|
kind: 'friend_message',
|
|
time: 1,
|
|
selfUin: SELF_UIN,
|
|
senderUin: INVITOR_UIN,
|
|
senderUid: INVITOR_UID,
|
|
senderNick: 'inviter',
|
|
msgSeq: 1,
|
|
msgId: 2,
|
|
elements: [],
|
|
inviteCardGroupUin: GROUP_ID,
|
|
inviteCardSequence: sequence,
|
|
} as QQEventVariant;
|
|
}
|
|
|
|
function makePipeline(options: { request?: 'found' | 'missing' } = {}) {
|
|
const identity = IdentityService.memory(String(SELF_UIN));
|
|
const events = new BridgeEventBus();
|
|
const received: QQEventVariant[] = [];
|
|
events.onAny((event) => {
|
|
// The card arrives as a friend_message and the pipeline derives a
|
|
// group_invite from it; only the latter is under test here.
|
|
if ((event as QQEventVariant).kind === 'group_invite') {
|
|
received.push(event as QQEventVariant);
|
|
}
|
|
});
|
|
|
|
const resolveGroupJoinRequest = vi.fn(async () =>
|
|
options.request === 'found'
|
|
? {
|
|
groupId: GROUP_ID,
|
|
groupName: '',
|
|
targetUid: 'u_target',
|
|
targetUin: 0,
|
|
targetName: '',
|
|
invitorUid: INVITOR_UID,
|
|
invitorUin: INVITOR_UIN,
|
|
invitorName: '',
|
|
operatorUid: '',
|
|
operatorUin: 0,
|
|
operatorName: '',
|
|
sequence: 42,
|
|
state: 1,
|
|
notifyType: 1,
|
|
eventType: 1,
|
|
comment: '',
|
|
filtered: false,
|
|
}
|
|
: null,
|
|
);
|
|
|
|
const pipeline = new IncomingPacketPipeline({
|
|
identity,
|
|
events,
|
|
fetchGroupList: vi.fn(async () => {}),
|
|
fetchGroupMemberList: vi.fn(async () => {}),
|
|
resolveGroupJoinRequest,
|
|
groupInvites: new GroupInviteTracker(),
|
|
});
|
|
|
|
return { pipeline, received, resolveGroupJoinRequest };
|
|
}
|
|
|
|
describe('self-invite delivery', () => {
|
|
it('dispatches a self-invite that the request inbox cannot resolve', async () => {
|
|
const { pipeline, received } = makePipeline({ request: 'missing' });
|
|
pipeline.registerCmd('test.self-invite', () => [selfInviteNotice()]);
|
|
|
|
await pipeline.process(packet());
|
|
|
|
expect(received).toHaveLength(1);
|
|
expect(received[0]).toMatchObject({
|
|
kind: 'group_invite',
|
|
groupId: GROUP_ID,
|
|
invitedUin: SELF_UIN,
|
|
subType: 'invite',
|
|
});
|
|
});
|
|
|
|
it('keeps the decoder-provided legacy flag so the request stays actionable', async () => {
|
|
const { pipeline, received } = makePipeline({ request: 'missing' });
|
|
pipeline.registerCmd('test.self-invite', () => [selfInviteNotice()]);
|
|
|
|
await pipeline.process(packet());
|
|
|
|
expect((received[0] as GroupInviteEvent).flag).toBe(
|
|
`invite:${GROUP_ID}:${INVITOR_UID}`,
|
|
);
|
|
});
|
|
|
|
it('keeps the legacy flag even when the inbox has a matching request', async () => {
|
|
// Self-invite notices are absent from the UIN-based inbox, so the pipeline
|
|
// does not consult it at all -- the legacy flag must therefore survive.
|
|
const { pipeline, received, resolveGroupJoinRequest } = makePipeline({ request: 'found' });
|
|
pipeline.registerCmd('test.self-invite', () => [selfInviteNotice()]);
|
|
|
|
await pipeline.process(packet());
|
|
|
|
expect(resolveGroupJoinRequest).not.toHaveBeenCalled();
|
|
expect((received[0] as GroupInviteEvent).flag).toBe(
|
|
`invite:${GROUP_ID}:${INVITOR_UID}`,
|
|
);
|
|
});
|
|
|
|
it('does not query the inbox and still dispatches a self-invite', async () => {
|
|
const { pipeline, received, resolveGroupJoinRequest } = makePipeline({ request: 'missing' });
|
|
pipeline.registerCmd('test.self-invite', () => [selfInviteNotice()]);
|
|
|
|
await pipeline.process(packet());
|
|
|
|
expect(received).toHaveLength(1);
|
|
// A self-invite never shows up in the UIN-based inbox, so the lookup is skipped.
|
|
expect(resolveGroupJoinRequest).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('lets the card win when it arrives first', async () => {
|
|
const { pipeline, received } = makePipeline({ request: 'missing' });
|
|
pipeline.registerCmd('test.self-invite', () => [selfInviteNotice()]);
|
|
pipeline.registerCmd('test.card', () => [inviteCard()]);
|
|
|
|
await pipeline.process({ ...packet(), serviceCmd: 'test.card' });
|
|
await pipeline.process(packet());
|
|
|
|
expect(received).toHaveLength(1);
|
|
expect((received[0] as GroupInviteEvent).flag).toBe(`slreq:1:777:${GROUP_ID}:2:0`);
|
|
});
|
|
|
|
it('suppresses the notice when the card already delivered the invitation', async () => {
|
|
const { pipeline, received } = makePipeline({ request: 'missing' });
|
|
pipeline.registerCmd('test.self-invite', () => [selfInviteNotice()]);
|
|
pipeline.registerCmd('test.card', () => [inviteCard()]);
|
|
|
|
await pipeline.process(packet());
|
|
await pipeline.process({ ...packet(), serviceCmd: 'test.card' });
|
|
|
|
expect(received).toHaveLength(1);
|
|
expect((received[0] as GroupInviteEvent).flag).toBe(`invite:${GROUP_ID}:${INVITOR_UID}`);
|
|
});
|
|
});
|