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,325 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { BridgeInterface } from '../../src/bridge/bridge-interface';
|
||||
import { ApiHandler, type ApiActionContext } from '../src/api-handler';
|
||||
import type { OneBotInstanceContext } from '../src/instance-context';
|
||||
import {
|
||||
deleteMessage,
|
||||
sendGroupMessage,
|
||||
sendPrivateMessage,
|
||||
setEssenceMessage,
|
||||
} from '../src/modules/message-actions';
|
||||
import type { MessageMeta } from '../src/types';
|
||||
|
||||
function instanceContext(
|
||||
sendGroup: ReturnType<typeof vi.fn>,
|
||||
sendPrivate: ReturnType<typeof vi.fn> = vi.fn(),
|
||||
): OneBotInstanceContext {
|
||||
const bridge = {
|
||||
identity: { nickname: 'SnowLuma' },
|
||||
apis: { message: { sendGroup, sendPrivate } },
|
||||
resolveUserUid: vi.fn(async () => 'u_peer'),
|
||||
} as unknown as BridgeInterface;
|
||||
|
||||
return {
|
||||
uin: '10001',
|
||||
selfId: 10001,
|
||||
bridge,
|
||||
messageStore: {
|
||||
resolveReplySequence: () => 0,
|
||||
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,
|
||||
},
|
||||
musicSignUrl: '',
|
||||
} as unknown as OneBotInstanceContext;
|
||||
}
|
||||
|
||||
describe('outbound message validation at the Action boundary', () => {
|
||||
const sendReceipt = {
|
||||
messageId: 1,
|
||||
sequence: 100,
|
||||
clientSequence: 0,
|
||||
random: 1,
|
||||
timestamp: 1700000000,
|
||||
};
|
||||
|
||||
it('returns BAD_REQUEST and performs no send when a later segment is unknown', async () => {
|
||||
const bridgeSend = vi.fn();
|
||||
const ref = instanceContext(bridgeSend);
|
||||
const api = new ApiHandler({
|
||||
sendGroupMessage: (groupId, message, autoEscape) =>
|
||||
sendGroupMessage(ref, groupId, message, autoEscape),
|
||||
} as ApiActionContext);
|
||||
|
||||
const response = await api.handle('send_group_msg', {
|
||||
group_id: 12345,
|
||||
message: [
|
||||
{ type: 'text', data: { text: 'must not be sent' } },
|
||||
{ type: 'unknown_late_segment', data: {} },
|
||||
],
|
||||
});
|
||||
|
||||
expect(response).toMatchObject({
|
||||
status: 'failed',
|
||||
retcode: 1400,
|
||||
wording: expect.stringContaining('unknown message segment type'),
|
||||
});
|
||||
expect(bridgeSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns BAD_REQUEST and performs no send for malformed JSON cards', async () => {
|
||||
const bridgeSend = vi.fn();
|
||||
const ref = instanceContext(bridgeSend);
|
||||
const api = new ApiHandler({
|
||||
sendGroupMessage: (groupId, message, autoEscape) =>
|
||||
sendGroupMessage(ref, groupId, message, autoEscape),
|
||||
} as ApiActionContext);
|
||||
|
||||
const response = await api.handle('send_group_msg', {
|
||||
group_id: 12345,
|
||||
message: [{ type: 'json', data: { data: 'not-json' } }],
|
||||
});
|
||||
|
||||
expect(response).toMatchObject({
|
||||
status: 'failed',
|
||||
retcode: 1400,
|
||||
wording: expect.stringContaining('must contain a JSON object'),
|
||||
});
|
||||
expect(bridgeSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends object-valued JSON cards from AstrBot-compatible clients', async () => {
|
||||
const bridgeSend = vi.fn(async () => sendReceipt);
|
||||
const ref = instanceContext(bridgeSend);
|
||||
const api = new ApiHandler({
|
||||
sendGroupMessage: (groupId, message, autoEscape) =>
|
||||
sendGroupMessage(ref, groupId, message, autoEscape),
|
||||
} as ApiActionContext);
|
||||
|
||||
const response = await api.handle('send_group_msg', {
|
||||
group_id: 12345,
|
||||
message: [
|
||||
{
|
||||
type: 'json',
|
||||
data: {
|
||||
data: { app: 'com.tencent.music.lua', meta: { music: { title: '晴天' } } },
|
||||
config: { token: 'signed-token' },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(response).toMatchObject({ status: 'ok', retcode: 0 });
|
||||
expect(bridgeSend).toHaveBeenCalledOnce();
|
||||
expect(bridgeSend.mock.calls[0]?.[1]).toEqual([
|
||||
{
|
||||
type: 'json',
|
||||
text: '{"app":"com.tencent.music.lua","meta":{"music":{"title":"晴天"}}}',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('reports the public data field when a JSON segment uses data.text', async () => {
|
||||
const bridgeSend = vi.fn();
|
||||
const ref = instanceContext(bridgeSend);
|
||||
const api = new ApiHandler({
|
||||
sendGroupMessage: (groupId, message, autoEscape) =>
|
||||
sendGroupMessage(ref, groupId, message, autoEscape),
|
||||
} as ApiActionContext);
|
||||
|
||||
const response = await api.handle('send_group_msg', {
|
||||
group_id: 12345,
|
||||
message: [
|
||||
{
|
||||
type: 'json',
|
||||
data: { text: '{"app":"com.tencent.contact.lua"}' },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(response).toMatchObject({
|
||||
status: 'failed',
|
||||
retcode: 1400,
|
||||
wording:
|
||||
'message segment "json" field "data" must be a JSON object or non-empty JSON string',
|
||||
});
|
||||
expect(bridgeSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects video mixed with other visible content before sending', async () => {
|
||||
const bridgeSend = vi.fn(async () => sendReceipt);
|
||||
const ref = instanceContext(bridgeSend);
|
||||
const api = new ApiHandler({
|
||||
sendGroupMessage: (groupId, message, autoEscape) =>
|
||||
sendGroupMessage(ref, groupId, message, autoEscape),
|
||||
} as ApiActionContext);
|
||||
|
||||
const response = await api.handle('send_group_msg', {
|
||||
group_id: 12345,
|
||||
message: [
|
||||
{ type: 'video', data: { file: 'https://example.com/video.mp4' } },
|
||||
{ type: 'text', data: { text: 'must not become hidden content' } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(response).toMatchObject({
|
||||
status: 'failed',
|
||||
retcode: 1400,
|
||||
wording: expect.stringContaining('video'),
|
||||
});
|
||||
expect(bridgeSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects mixed video content before resolving a later segment', async () => {
|
||||
const bridgeSend = vi.fn(async () => sendReceipt);
|
||||
const getGroupRecommendArk = vi.fn(async () => '{"app":"contact"}');
|
||||
const ref = instanceContext(bridgeSend);
|
||||
(ref.bridge.apis as any).contacts = { getGroupRecommendArk };
|
||||
const api = new ApiHandler({
|
||||
sendGroupMessage: (groupId, message, autoEscape) =>
|
||||
sendGroupMessage(ref, groupId, message, autoEscape),
|
||||
} as ApiActionContext);
|
||||
|
||||
const response = await api.handle('send_group_msg', {
|
||||
group_id: 12345,
|
||||
message: [
|
||||
{ type: 'video', data: { file: 'https://example.com/video.mp4' } },
|
||||
{ type: 'contact', data: { type: 'group', id: 67890 } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(response).toMatchObject({ status: 'failed', retcode: 1400 });
|
||||
expect(getGroupRecommendArk).not.toHaveBeenCalled();
|
||||
expect(bridgeSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects multiple video segments before sending', async () => {
|
||||
const bridgeSend = vi.fn(async () => sendReceipt);
|
||||
const ref = instanceContext(bridgeSend);
|
||||
const api = new ApiHandler({
|
||||
sendGroupMessage: (groupId, message, autoEscape) =>
|
||||
sendGroupMessage(ref, groupId, message, autoEscape),
|
||||
} as ApiActionContext);
|
||||
|
||||
const response = await api.handle('send_group_msg', {
|
||||
group_id: 12345,
|
||||
message: [
|
||||
{ type: 'video', data: { file: 'https://example.com/first.mp4' } },
|
||||
{ type: 'video', data: { file: 'https://example.com/second.mp4' } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(response).toMatchObject({
|
||||
status: 'failed',
|
||||
retcode: 1400,
|
||||
wording: expect.stringContaining('video'),
|
||||
});
|
||||
expect(bridgeSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects hidden content after a video in CQ string form', async () => {
|
||||
const bridgeSend = vi.fn(async () => sendReceipt);
|
||||
const ref = instanceContext(bridgeSend);
|
||||
const api = new ApiHandler({
|
||||
sendGroupMessage: (groupId, message, autoEscape) =>
|
||||
sendGroupMessage(ref, groupId, message, autoEscape),
|
||||
} as ApiActionContext);
|
||||
|
||||
const response = await api.handle('send_group_msg', {
|
||||
group_id: 12345,
|
||||
message: '[CQ:video,file=https://example.com/video.mp4]hidden text',
|
||||
});
|
||||
|
||||
expect(response).toMatchObject({ status: 'failed', retcode: 1400 });
|
||||
expect(bridgeSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects video mixed with private-chat content before sending', async () => {
|
||||
const privateSend = vi.fn(async () => sendReceipt);
|
||||
const ref = instanceContext(vi.fn(), privateSend);
|
||||
const api = new ApiHandler({
|
||||
sendPrivateMessage: (userId, message, autoEscape, tempGroupId) =>
|
||||
sendPrivateMessage(ref, userId, message, autoEscape, tempGroupId),
|
||||
} as ApiActionContext);
|
||||
|
||||
const response = await api.handle('send_private_msg', {
|
||||
user_id: 67890,
|
||||
message: [
|
||||
{ type: 'video', data: { file: 'https://example.com/video.mp4' } },
|
||||
{ type: 'text', data: { text: 'must not become hidden content' } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(response).toMatchObject({
|
||||
status: 'failed',
|
||||
retcode: 1400,
|
||||
wording: expect.stringContaining('only segment'),
|
||||
});
|
||||
expect(privateSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps exact empty text placeholders compatible with a single video', async () => {
|
||||
const bridgeSend = vi.fn(async () => sendReceipt);
|
||||
const ref = instanceContext(bridgeSend);
|
||||
const api = new ApiHandler({
|
||||
sendGroupMessage: (groupId, message, autoEscape) =>
|
||||
sendGroupMessage(ref, groupId, message, autoEscape),
|
||||
} as ApiActionContext);
|
||||
|
||||
const response = await api.handle('send_group_msg', {
|
||||
group_id: 12345,
|
||||
message: [
|
||||
{ type: 'text', data: { text: '' } },
|
||||
{ type: 'video', data: { file: 'https://example.com/video.mp4' } },
|
||||
{ type: 'text', data: { text: '' } },
|
||||
],
|
||||
});
|
||||
|
||||
expect(response).toMatchObject({ status: 'ok', retcode: 0 });
|
||||
expect(bridgeSend).toHaveBeenCalledOnce();
|
||||
expect(bridgeSend.mock.calls[0]?.[1]).toMatchObject([{ type: 'video' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('server-sequence action validation', () => {
|
||||
const localOnlyMeta: MessageMeta = {
|
||||
isGroup: true,
|
||||
targetId: 12345,
|
||||
sequence: 0,
|
||||
sequenceAuthoritative: false,
|
||||
eventName: 'group_message',
|
||||
clientSequence: 0,
|
||||
random: 0,
|
||||
timestamp: 0,
|
||||
};
|
||||
|
||||
it('does not recall a message that has only a local id', async () => {
|
||||
const recallGroup = vi.fn();
|
||||
const bridge = { apis: { message: { recallGroup } } } as unknown as BridgeInterface;
|
||||
|
||||
await expect(deleteMessage(bridge, localOnlyMeta)).rejects.toThrow(
|
||||
'no authoritative QQ sequence',
|
||||
);
|
||||
expect(recallGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not mark a message that has only a local id as essence', async () => {
|
||||
const setEssence = vi.fn();
|
||||
const bridge = { apis: { interaction: { setEssence } } } as unknown as BridgeInterface;
|
||||
const store = { findMeta: () => localOnlyMeta } as any;
|
||||
|
||||
await expect(setEssenceMessage(bridge, store, 1, true)).rejects.toThrow(
|
||||
'no authoritative QQ sequence',
|
||||
);
|
||||
expect(setEssence).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user