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,553 @@
import { describe, expect, it, vi } from 'vitest';
import type { ApiActionContext } from '../src/api-handler';
import { actions } from '../src/actions/message';
import type { JsonObject, MessageMeta } from '../src/types';
function requireAction(name: string) {
const action = actions.find((entry) => entry.names[0] === name);
if (!action) {
throw new Error(`${name} action missing`);
}
return action;
}
function asCtx(partial: Partial<ApiActionContext>): ApiActionContext {
return { refreshMessageMedia: vi.fn(async () => {}), ...partial } as ApiActionContext;
}
const sendMsg = requireAction('send_msg');
const sendPrivateMsg = requireAction('send_private_msg');
const sendGroupMsg = requireAction('send_group_msg');
const getMsg = requireAction('get_msg');
const deleteMsg = requireAction('delete_msg');
describe('message actions catalog', () => {
it('exports the public message actions in source order', () => {
expect(actions.map((entry) => entry.names[0])).toEqual([
'send_custom_face',
'send_msg',
'send_private_msg',
'send_group_msg',
'get_msg',
'delete_msg',
]);
});
it('marks only get_msg as read-only', () => {
expect(sendMsg.describe().readOnly).toBe(false);
expect(sendPrivateMsg.describe().readOnly).toBe(false);
expect(sendGroupMsg.describe().readOnly).toBe(false);
expect(getMsg.describe().readOnly).toBe(true);
expect(deleteMsg.describe().readOnly).toBe(false);
});
});
describe('send_custom_face', () => {
const action = requireAction('send_custom_face');
const url = 'https://p.qpic.cn/qq_expression/10001/saved-sticker/0';
const image = { type: 'image', data: { file: url, sub_type: 1, summary: '[动画表情]' } };
function context() {
const resolveCustomFace = vi.fn(async () => ({
emojiId: 'saved-sticker',
url,
md5: 'a'.repeat(32),
}));
const sendGroupMessage = vi.fn(async () => ({ messageId: 7 }));
const sendPrivateMessage = vi.fn(async () => ({ messageId: 8 }));
const ctx = asCtx({
bridge: {
apis: { profile: { resolveCustomFace } },
} as unknown as ApiActionContext['bridge'],
sendGroupMessage,
sendPrivateMessage,
});
return { ctx, resolveCustomFace, sendGroupMessage, sendPrivateMessage };
}
it('resolves the saved id and sends a sticker with an optional reply', async () => {
const { ctx, resolveCustomFace, sendGroupMessage } = context();
expect(
await action.toHandler(ctx)({
emoji_id: 'saved-sticker',
group_id: 123,
reply_to: -42,
}),
).toMatchObject({ status: 'ok', data: { message_id: 7 } });
expect(resolveCustomFace).toHaveBeenCalledWith('saved-sticker');
expect(sendGroupMessage).toHaveBeenCalledWith(
123,
[{ type: 'reply', data: { id: '-42' } }, image],
false,
);
});
it('supports a private destination and propagates lookup failures', async () => {
const { ctx, resolveCustomFace, sendPrivateMessage } = context();
expect(
await action.toHandler(ctx)({ emoji_id: 'a'.repeat(32), user_id: 456 }),
).toMatchObject({ status: 'ok', data: { message_id: 8 } });
expect(sendPrivateMessage).toHaveBeenCalledWith(456, [image], false);
resolveCustomFace.mockRejectedValueOnce(new Error('sticker missing'));
await expect(action.toHandler(ctx)({ emoji_id: 'missing', user_id: 456 })).rejects.toThrow(
'sticker missing',
);
expect(sendPrivateMessage).toHaveBeenCalledTimes(1);
});
it.each([{}, { group_id: 123, user_id: 456 }])(
'rejects an ambiguous destination before lookup',
async (target) => {
const { ctx, resolveCustomFace } = context();
expect(
await action.toHandler(ctx)({ emoji_id: 'saved-sticker', ...target }),
).toMatchObject({ retcode: 1400 });
expect(resolveCustomFace).not.toHaveBeenCalled();
},
);
});
describe('send_msg parse', () => {
it('defaults auto_escape to false and leaves optional ids unset', () => {
expect(sendMsg.parse({ message: 'hello route' })).toEqual({
ok: true,
value: {
message: 'hello route',
message_type: undefined,
group_id: undefined,
user_id: undefined,
auto_escape: false,
},
});
});
it('parses a JSON-array message string and numeric ids', () => {
expect(
sendMsg.parse({
message: '[{"type":"text","data":{"text":"hi"}}]',
message_type: 'group',
group_id: '314159',
user_id: '271828',
auto_escape: 'on',
}),
).toEqual({
ok: true,
value: {
message: [{ type: 'text', data: { text: 'hi' } }],
message_type: 'group',
group_id: 314159,
user_id: 271828,
auto_escape: true,
},
});
});
it('requires message and rejects group_id 0', () => {
expect(sendMsg.parse({})).toEqual({
ok: false,
field: 'message',
reason: 'is required',
});
expect(sendMsg.parse({ message: 'x', group_id: 0 })).toEqual({
ok: false,
field: 'group_id',
reason: 'must be >= 1',
});
});
it('treats an empty group_id string as absent', () => {
expect(
sendMsg.parse({
message_type: 'private',
user_id: 1316864423,
group_id: '',
message: 'hi',
}),
).toEqual({
ok: true,
value: {
message: 'hi',
message_type: 'private',
group_id: undefined,
user_id: 1316864423,
auto_escape: false,
},
});
});
});
describe('send_msg run', () => {
it('sends a group message when message_type is group', async () => {
const sendGroupMessage = vi.fn(async () => ({ messageId: 9001 }));
const sendPrivateMessage = vi.fn(async () => ({ messageId: 0 }));
const response = await sendMsg.toHandler(asCtx({ sendGroupMessage, sendPrivateMessage }))({
message_type: 'group',
group_id: 314159,
message: 'group hello',
});
expect(response).toEqual({ status: 'ok', retcode: 0, data: { message_id: 9001 } });
expect(sendGroupMessage.mock.calls).toEqual([[314159, 'group hello', false]]);
expect(sendPrivateMessage).not.toHaveBeenCalled();
});
it('sends a group message when only group_id is present', async () => {
const sendGroupMessage = vi.fn(async () => ({ messageId: 9002 }));
const sendPrivateMessage = vi.fn(async () => ({ messageId: 0 }));
const response = await sendMsg.toHandler(asCtx({ sendGroupMessage, sendPrivateMessage }))({
group_id: 80001,
user_id: 271828,
message: [{ type: 'text', data: { text: 'by group_id' } }],
auto_escape: true,
});
expect(response).toEqual({ status: 'ok', retcode: 0, data: { message_id: 9002 } });
expect(sendGroupMessage.mock.calls).toEqual([
[80001, [{ type: 'text', data: { text: 'by group_id' } }], true],
]);
expect(sendPrivateMessage).not.toHaveBeenCalled();
});
it('sends a private message when group_id is an empty string', async () => {
const sendGroupMessage = vi.fn(async () => ({ messageId: 0 }));
const sendPrivateMessage = vi.fn(async () => ({ messageId: 9006 }));
const response = await sendMsg.toHandler(asCtx({ sendGroupMessage, sendPrivateMessage }))({
message_type: 'private',
user_id: 1316864423,
group_id: '',
message: [{ type: 'image', data: { file: 'base64://x' } }],
});
expect(response).toEqual({ status: 'ok', retcode: 0, data: { message_id: 9006 } });
expect(sendPrivateMessage.mock.calls).toEqual([
[1316864423, [{ type: 'image', data: { file: 'base64://x' } }], false, undefined],
]);
expect(sendGroupMessage).not.toHaveBeenCalled();
});
it('sends a private message when only user_id is present', async () => {
const sendGroupMessage = vi.fn(async () => ({ messageId: 0 }));
const sendPrivateMessage = vi.fn(async () => ({ messageId: 9003 }));
const response = await sendMsg.toHandler(asCtx({ sendGroupMessage, sendPrivateMessage }))({
user_id: 271828,
message: 'private hello',
});
expect(response).toEqual({ status: 'ok', retcode: 0, data: { message_id: 9003 } });
expect(sendPrivateMessage.mock.calls).toEqual([
[271828, 'private hello', false, undefined],
]);
expect(sendGroupMessage).not.toHaveBeenCalled();
});
it('routes private + both ids into a temp-session reply', async () => {
const sendGroupMessage = vi.fn(async () => ({ messageId: 0 }));
const sendPrivateMessage = vi.fn(async () => ({ messageId: 9004 }));
const response = await sendMsg.toHandler(asCtx({ sendGroupMessage, sendPrivateMessage }))({
message_type: 'private',
user_id: 13579,
group_id: 70001,
message: 'temp hi',
auto_escape: false,
});
expect(response).toEqual({ status: 'ok', retcode: 0, data: { message_id: 9004 } });
expect(sendPrivateMessage.mock.calls).toEqual([[13579, 'temp hi', false, 70001]]);
expect(sendGroupMessage).not.toHaveBeenCalled();
});
it('still sends a group message when message_type is group and user_id is also set', async () => {
const sendGroupMessage = vi.fn(async () => ({ messageId: 9005 }));
const sendPrivateMessage = vi.fn(async () => ({ messageId: 0 }));
const response = await sendMsg.toHandler(asCtx({ sendGroupMessage, sendPrivateMessage }))({
message_type: 'group',
group_id: 80002,
user_id: 30003,
message: 'not temp',
});
expect(response).toEqual({ status: 'ok', retcode: 0, data: { message_id: 9005 } });
expect(sendGroupMessage.mock.calls).toEqual([[80002, 'not temp', false]]);
expect(sendPrivateMessage).not.toHaveBeenCalled();
});
it('returns BAD_REQUEST when message_type is group without group_id', async () => {
const sendGroupMessage = vi.fn(async () => ({ messageId: 0 }));
const sendPrivateMessage = vi.fn(async () => ({ messageId: 0 }));
const response = await sendMsg.toHandler(asCtx({ sendGroupMessage, sendPrivateMessage }))({
message_type: 'group',
message: 'missing group',
});
expect(response).toEqual({
status: 'failed',
retcode: 1400,
data: null,
wording: 'group_id is required',
});
expect(sendGroupMessage).not.toHaveBeenCalled();
expect(sendPrivateMessage).not.toHaveBeenCalled();
});
it('returns BAD_REQUEST when neither a group route nor user_id is present', async () => {
const sendGroupMessage = vi.fn(async () => ({ messageId: 0 }));
const sendPrivateMessage = vi.fn(async () => ({ messageId: 0 }));
const response = await sendMsg.toHandler(asCtx({ sendGroupMessage, sendPrivateMessage }))({
message_type: 'private',
message: 'missing user',
});
expect(response).toEqual({
status: 'failed',
retcode: 1400,
data: null,
wording: 'user_id is required',
});
expect(sendGroupMessage).not.toHaveBeenCalled();
expect(sendPrivateMessage).not.toHaveBeenCalled();
});
it('shapes a parse failure as BAD_REQUEST wording', async () => {
const response = await sendMsg.toHandler(asCtx({}))({});
expect(response).toEqual({
status: 'failed',
retcode: 1400,
data: null,
wording: 'message: is required',
});
});
});
describe('send_private_msg', () => {
it('parses group_id 0 and defaults auto_escape', () => {
expect(
sendPrivateMsg.parse({
user_id: 24680,
message: 'plain private',
group_id: 0,
}),
).toEqual({
ok: true,
value: {
user_id: 24680,
message: 'plain private',
group_id: 0,
auto_escape: false,
},
});
});
it('rejects a negative group_id before sending', () => {
expect(
sendPrivateMsg.parse({
user_id: 24680,
message: 'x',
group_id: -1,
}),
).toEqual({
ok: false,
field: 'group_id',
reason: 'must be >= 0',
});
});
it('sends a friend private message when group_id is absent', async () => {
const sendPrivateMessage = vi.fn(async () => ({ messageId: 9101 }));
const response = await sendPrivateMsg.toHandler(asCtx({ sendPrivateMessage }))({
user_id: 24680,
message: 'friend hi',
});
expect(response).toEqual({ status: 'ok', retcode: 0, data: { message_id: 9101 } });
expect(sendPrivateMessage.mock.calls).toEqual([[24680, 'friend hi', false, undefined]]);
});
it('treats group_id 0 as no temp session', async () => {
const sendPrivateMessage = vi.fn(async () => ({ messageId: 9102 }));
const response = await sendPrivateMsg.toHandler(asCtx({ sendPrivateMessage }))({
user_id: 24682,
group_id: 0,
message: 'zero group',
auto_escape: true,
});
expect(response).toEqual({ status: 'ok', retcode: 0, data: { message_id: 9102 } });
expect(sendPrivateMessage.mock.calls).toEqual([[24682, 'zero group', true, undefined]]);
});
it('forwards a positive group_id as the temp-session source', async () => {
const sendPrivateMessage = vi.fn(async () => ({ messageId: 9103 }));
const response = await sendPrivateMsg.toHandler(asCtx({ sendPrivateMessage }))({
user_id: 24681,
group_id: 70002,
message: 'temp from group',
});
expect(response).toEqual({ status: 'ok', retcode: 0, data: { message_id: 9103 } });
expect(sendPrivateMessage.mock.calls).toEqual([[24681, 'temp from group', false, 70002]]);
});
});
describe('send_group_msg', () => {
it('requires group_id from the groupAction preset', () => {
expect(sendGroupMsg.parse({ message: 'need group' })).toEqual({
ok: false,
field: 'group_id',
reason: 'is required',
});
expect(sendGroupMsg.parse({ group_id: 888001, message: 'ok' })).toEqual({
ok: true,
value: {
group_id: 888001,
message: 'ok',
auto_escape: false,
},
});
});
it('sends the group message and returns the receipt id', async () => {
const sendGroupMessage = vi.fn(async () => ({ messageId: 9201 }));
const response = await sendGroupMsg.toHandler(asCtx({ sendGroupMessage }))({
group_id: 888001,
message: 'group body',
auto_escape: true,
});
expect(response).toEqual({ status: 'ok', retcode: 0, data: { message_id: 9201 } });
expect(sendGroupMessage.mock.calls).toEqual([[888001, 'group body', true]]);
});
});
describe('get_msg', () => {
it('accepts a negative message_id and rejects 0', () => {
expect(getMsg.parse({ message_id: -2147483648 })).toEqual({
ok: true,
value: { message_id: -2147483648 },
});
expect(getMsg.parse({ message_id: 0 })).toEqual({
ok: false,
field: 'message_id',
reason: 'must not be 0',
});
});
it('returns ACTION_FAILED when the store has no event', async () => {
const getMessage = vi.fn(() => null);
const response = await getMsg.toHandler(asCtx({ getMessage }))({ message_id: 424243 });
expect(response).toEqual({
status: 'failed',
retcode: 100,
data: null,
wording: 'message not found',
});
expect(getMessage.mock.calls).toEqual([[424243]]);
});
it('strips post_type/self_id and sets real_id from the stored message_id', async () => {
const stored: JsonObject = {
post_type: 'message',
self_id: 10001,
message_id: 424242,
message_type: 'group',
group_id: 314159,
user_id: 271828,
message: 'plain stored text',
time: 1700000000,
};
const getMessage = vi.fn(() => stored);
const getImageInfo = vi.fn(async () => ({ url: 'https://unused.example/img' }));
const response = await getMsg.toHandler(asCtx({ getMessage, getImageInfo }))({
message_id: 424242,
});
expect(response).toEqual({
status: 'ok',
retcode: 0,
data: {
message_id: 424242,
message_type: 'group',
group_id: 314159,
user_id: 271828,
message: 'plain stored text',
time: 1700000000,
real_id: 424242,
},
});
expect(stored).toEqual({
post_type: 'message',
self_id: 10001,
message_id: 424242,
message_type: 'group',
group_id: 314159,
user_id: 271828,
message: 'plain stored text',
time: 1700000000,
});
expect(getImageInfo).not.toHaveBeenCalled();
});
it('falls back to the request message_id when the stored event has none', async () => {
const getMessage = vi.fn(() => ({
message_type: 'private',
user_id: 13579,
message: 'no stored id',
}));
const response = await getMsg.toHandler(asCtx({ getMessage }))({ message_id: 555001 });
expect(response).toEqual({
status: 'ok',
retcode: 0,
data: {
message_type: 'private',
user_id: 13579,
message: 'no stored id',
real_id: 555001,
},
});
});
});
describe('delete_msg', () => {
it('returns ACTION_FAILED when no retractable meta exists', async () => {
const getMessageMeta = vi.fn(() => null);
const deleteMessage = vi.fn(async () => {});
const response = await deleteMsg.toHandler(asCtx({ getMessageMeta, deleteMessage }))({
message_id: 424245,
});
expect(response).toEqual({
status: 'failed',
retcode: 100,
data: null,
wording: 'message not found or not retractable',
});
expect(getMessageMeta.mock.calls).toEqual([[424245]]);
expect(deleteMessage).not.toHaveBeenCalled();
});
it('deletes with the stored meta and returns an empty ok payload', async () => {
const meta: MessageMeta = {
isGroup: true,
targetId: 314159,
sequence: 77,
sequenceAuthoritative: true,
eventName: 'group_message',
clientSequence: 0,
random: 123,
timestamp: 1700000000,
};
const getMessageMeta = vi.fn(() => meta);
const deleteMessage = vi.fn(async () => {});
const response = await deleteMsg.toHandler(asCtx({ getMessageMeta, deleteMessage }))({
message_id: -424244,
});
expect(response).toEqual({ status: 'ok', retcode: 0, data: null });
expect(getMessageMeta.mock.calls).toEqual([[-424244]]);
expect(deleteMessage.mock.calls).toEqual([[-424244, meta]]);
});
});