Files
TanTan fa9c73363d
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
SnowLuma v2.4.2 source snapshot
2026-11-13 09:02:00 +08:00

115 lines
5.0 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { atAll, chain, face, fromCQString, message, parseSegments, text, toCQString } from '../src';
describe('MessageChain', () => {
it.each([undefined, true])(
'preserves animation results through builders and CQ with large=%s',
(large) => {
const options = { resultId: '0', ...(large !== undefined ? { large } : {}) };
const expected = [{ type: 'face', data: { id: '114', ...options } }];
expect(face(114, options).toSegments()).toEqual(expected);
expect(chain().face(114, options).toSegments()).toEqual(expected);
expect(message.face(114, options)).toEqual(expected[0]);
expect(parseSegments(toCQString(face(114, options)))).toEqual(expected);
},
);
it.each([false, true])(
'preserves face animation selection through builders and CQ, large=%s',
(large) => {
const expected = [{ type: 'face', data: { id: '451', large } }];
const built = face(451, { large });
expect(built.toSegments()).toEqual(expected);
expect(chain().face(451, { large }).toSegments()).toEqual(expected);
expect(message.face(451, { large })).toEqual(expected[0]);
expect(parseSegments(toCQString(built))).toEqual(expected);
expect(parseSegments(`[CQ:face,id=451,large=${large ? 1 : 0}]`)).toEqual(expected);
},
);
it('rejects an invalid face animation choice instead of discarding it', () => {
expect(() => parseSegments('[CQ:face,id=451,large=invalid]')).toThrow('face.large');
});
it('builds fluent segment chains', () => {
const chain = text('hello').at(10001).br().image('/tmp/a.png');
expect(chain.length).toBe(4);
expect(chain.toSegments()).toEqual([
{ type: 'text', data: { text: 'hello' } },
{ type: 'at', data: { qq: '10001' } },
{ type: 'text', data: { text: '\n' } },
{ type: 'image', data: { file: '/tmp/a.png' } },
]);
});
it('keeps the old segment builder API available', () => {
expect(message.text('hello')).toEqual({ type: 'text', data: { text: 'hello' } });
expect(atAll().toSegments()).toEqual([{ type: 'at', data: { qq: 'all' } }]);
});
it('allows reply only once in a chain at runtime', () => {
const chain = text('hello').reply(1);
expect(() => (chain as any).reply(2)).toThrow(/reply/);
expect(() => chain.append(message.reply(2))).toThrow(/reply/);
});
it('appends other messages and converts to CQ string', () => {
const chain = text('hello').append([message.at('all'), message.face(14)]);
expect(toCQString(chain)).toBe('hello[CQ:at,qq=all][CQ:face,id=14]');
});
it('parses CQ strings into chains', () => {
const chain = fromCQString('hi[CQ:at,qq=all][CQ:image,file=a,b]');
expect(chain.toSegments()).toEqual([
{ type: 'text', data: { text: 'hi' } },
{ type: 'at', data: { qq: 'all' } },
{
type: 'image',
data: { file: 'a,b', url: undefined, type: undefined, summary: undefined },
},
]);
expect(parseSegments('[CQ:reply,id=7]')).toEqual([{ type: 'reply', data: { id: '7' } }]);
});
it.each([
message.image('literal,,.png'),
message.customReply('literal , [CQ:at,qq=all] &[],'),
message.raw('extension', { value: '[],& &[],' }),
])('decodes CQ parameters exactly once for $type', (segment) => {
expect(parseSegments(toCQString(segment))).toEqual([segment]);
});
it('preserves string custom replies and their metadata through CQ conversion', () => {
const reply = message.customReply('hello[CQ:at,qq=all] & goodbye', {
id: '-123',
qq: '10001',
time: '1700000000',
seq: '42',
});
expect(parseSegments(toCQString(reply))).toEqual([reply]);
const inline = message.customReply('quoted');
expect(parseSegments(toCQString(inline))).toEqual([inline]);
});
it.each([[message.text('quoted')], message.text('quoted')].map((content) => ({ content })))(
'rejects nested custom replies during CQ conversion ($content)',
({ content }) => {
expect(() => toCQString(message.customReply(content))).toThrow(/must be sent as JSON/);
},
);
it('can send itself through a compatible client', async () => {
const client = {
sendGroupMessage: vi.fn().mockResolvedValue({ message_id: 1 }),
sendPrivateMessage: vi.fn().mockResolvedValue({ message_id: 2 }),
};
await text('hi').sendToGroup(client, 123);
await text('yo').sendToPrivate(client, 456);
expect(client.sendGroupMessage).toHaveBeenCalledWith(123, text('hi'), undefined);
expect(client.sendPrivateMessage).toHaveBeenCalledWith(456, text('yo'), undefined);
});
});