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
115 lines
5.0 KiB
TypeScript
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);
|
|
});
|
|
});
|