Integrate the current development branch, preserve the album comment fixes, and align the changes with repository formatting and dependency tooling.
188 lines
6.4 KiB
TypeScript
188 lines
6.4 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import type { BridgeInterface } from '../../core/src/bridge/bridge-interface';
|
|
import type { OneBotInstanceContext } from '../src/instance-context';
|
|
import {
|
|
sendGroupMessage,
|
|
sendPrivateForwardMessage,
|
|
sendPrivateMessage,
|
|
} from '../src/modules/message-actions';
|
|
import { TempSessionStore } from '../src/temp-session-store';
|
|
|
|
const receipt = {
|
|
messageId: 1,
|
|
sequence: 100,
|
|
clientSequence: 0,
|
|
random: 1,
|
|
timestamp: 1_700_000_000,
|
|
};
|
|
|
|
function makeContext(messageApi: Record<string, ReturnType<typeof vi.fn>>): OneBotInstanceContext {
|
|
return {
|
|
uin: '10001',
|
|
selfId: 10001,
|
|
bridge: {
|
|
identity: { nickname: 'SnowLuma' },
|
|
resolveUserUid: vi.fn(),
|
|
apis: { message: messageApi },
|
|
} as unknown as BridgeInterface,
|
|
messageStore: {
|
|
findEvent: () => null,
|
|
findMeta: () => null,
|
|
storeEvent: vi.fn(),
|
|
} as never,
|
|
cacheMessageMeta: vi.fn(),
|
|
mediaStore: {} as never,
|
|
converterCtx: {
|
|
selfId: 10001,
|
|
imageUrlResolver: null,
|
|
mediaUrlResolver: null,
|
|
messageIdResolver: null,
|
|
mediaSegmentSink: null,
|
|
},
|
|
tempSessions: new TempSessionStore(),
|
|
musicSignUrl: '',
|
|
} as unknown as OneBotInstanceContext;
|
|
}
|
|
|
|
describe('private window shake messages', () => {
|
|
it('sends a poke segment through the ordinary friend private path', async () => {
|
|
const sendPrivate = vi.fn(async () => ({ ...receipt, clientSequence: 9 }));
|
|
const ctx = makeContext({ sendPrivate });
|
|
|
|
await sendPrivateMessage(ctx, 67890, [{ type: 'poke', data: { type: 1 } }], false);
|
|
|
|
expect(sendPrivate).toHaveBeenCalledWith(67890, [{ type: 'poke', subType: 1 }]);
|
|
});
|
|
|
|
it('normalizes the legacy shake segment to the same private element', async () => {
|
|
const sendPrivate = vi.fn(async () => ({ ...receipt, clientSequence: 9 }));
|
|
const ctx = makeContext({ sendPrivate });
|
|
|
|
await sendPrivateMessage(ctx, 67890, [{ type: 'shake', data: {} }], false);
|
|
|
|
expect(sendPrivate).toHaveBeenCalledWith(67890, [{ type: 'poke', subType: 1 }]);
|
|
});
|
|
|
|
it('rejects mixed private content before resolving an earlier segment', async () => {
|
|
const sendPrivate = vi.fn(async () => ({ ...receipt, clientSequence: 9 }));
|
|
const ctx = makeContext({ sendPrivate });
|
|
|
|
await expect(
|
|
sendPrivateMessage(
|
|
ctx,
|
|
67890,
|
|
[
|
|
{ type: 'at', data: { qq: 12345 } },
|
|
{ type: 'poke', data: { type: 1 } },
|
|
],
|
|
false,
|
|
),
|
|
).rejects.toMatchObject({
|
|
code: 'UNSENDABLE_TYPE',
|
|
elementType: 'poke',
|
|
message: expect.stringContaining('only segment'),
|
|
});
|
|
|
|
expect(ctx.bridge.resolveUserUid).not.toHaveBeenCalled();
|
|
expect(sendPrivate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('keeps empty text significant for the standalone window-shake policy', async () => {
|
|
const sendPrivate = vi.fn(async () => ({ ...receipt, clientSequence: 9 }));
|
|
const ctx = makeContext({ sendPrivate });
|
|
|
|
await expect(
|
|
sendPrivateMessage(
|
|
ctx,
|
|
67890,
|
|
[
|
|
{ type: 'poke', data: { type: 1 } },
|
|
{ type: 'text', data: { text: '' } },
|
|
],
|
|
false,
|
|
),
|
|
).rejects.toMatchObject({
|
|
code: 'UNSENDABLE_TYPE',
|
|
elementType: 'poke',
|
|
message: expect.stringContaining('only segment'),
|
|
});
|
|
|
|
expect(sendPrivate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects group window shakes before resolving an earlier segment', async () => {
|
|
const sendGroup = vi.fn(async () => receipt);
|
|
const ctx = makeContext({ sendGroup });
|
|
|
|
await expect(
|
|
sendGroupMessage(
|
|
ctx,
|
|
12345,
|
|
[
|
|
{ type: 'at', data: { qq: 12345 } },
|
|
{ type: 'poke', data: { type: 1 } },
|
|
],
|
|
false,
|
|
),
|
|
).rejects.toMatchObject({
|
|
code: 'UNSENDABLE_TYPE',
|
|
elementType: 'poke',
|
|
message: expect.stringContaining('direct private chat'),
|
|
});
|
|
|
|
expect(ctx.bridge.resolveUserUid).not.toHaveBeenCalled();
|
|
expect(sendGroup).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects poke segments in group temporary sessions before calling the bridge', async () => {
|
|
const sendGroupTempMessage = vi.fn(async () => ({ ...receipt, clientSequence: 9 }));
|
|
const ctx = makeContext({ sendGroupTempMessage });
|
|
ctx.tempSessions.record(67890, 12345);
|
|
|
|
await expect(
|
|
sendPrivateMessage(ctx, 67890, [{ type: 'poke', data: { type: 1 } }], false, 12345),
|
|
).rejects.toMatchObject({
|
|
code: 'UNSENDABLE_TYPE',
|
|
elementType: 'poke',
|
|
message: expect.stringContaining('direct private chat'),
|
|
});
|
|
|
|
expect(sendGroupTempMessage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects a later forward window shake before parsing an earlier node', async () => {
|
|
const upload = vi.fn();
|
|
const sendPrivate = vi.fn();
|
|
const ctx = makeContext({ sendPrivate });
|
|
(ctx.bridge.apis as unknown as Record<string, unknown>).forward = { upload };
|
|
|
|
await expect(
|
|
sendPrivateForwardMessage(ctx, 67890, [
|
|
{
|
|
type: 'node',
|
|
data: {
|
|
user_id: 10001,
|
|
nickname: 'alice',
|
|
content: [{ type: 'at', data: { qq: 12345 } }],
|
|
},
|
|
},
|
|
{
|
|
type: 'node',
|
|
data: {
|
|
user_id: 10002,
|
|
nickname: 'bob',
|
|
content: [{ type: 'poke', data: { type: 1 } }],
|
|
},
|
|
},
|
|
]),
|
|
).rejects.toMatchObject({
|
|
code: 'UNSENDABLE_TYPE',
|
|
elementType: 'poke',
|
|
});
|
|
|
|
expect(ctx.bridge.resolveUserUid).not.toHaveBeenCalled();
|
|
expect(upload).not.toHaveBeenCalled();
|
|
expect(sendPrivate).not.toHaveBeenCalled();
|
|
});
|
|
});
|