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
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:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,188 @@
|
||||
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}`);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user