Restructure the repository into a pnpm workspace under packages/ (core, webui, runtime). Move source files for core and webui into packages/*, add package manifests for @snowluma/core and @snowluma/runtime, and remove the old launcher package.json. Update root package.json to be a monorepo controller (scripts delegate to pnpm filters, set packageManager and Node engine). Configure build outputs to emit into the repo-root dist/ (vite config changes) so the existing release pipeline continues to work. Add .npmrc with mirrors, expand .gitignore for monorepo layout, add tsconfig base and package-level tsconfigs, and add vitest config for the core package. Adjust injector native search paths and update tests import paths to the new package layout. Small CI/workflow comment added to clarify build behavior and README updated with monorepo structure and common commands.
249 lines
7.9 KiB
TypeScript
249 lines
7.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { parseMessage } from '../src/onebot/message-parser';
|
|
|
|
describe('parseMessage', () => {
|
|
describe('plain text', () => {
|
|
it('parses simple text', async () => {
|
|
const result = await parseMessage('hello world', false);
|
|
expect(result).toEqual([{ type: 'text', text: 'hello world' }]);
|
|
});
|
|
|
|
it('returns empty for empty string', async () => {
|
|
const result = await parseMessage('', false);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('autoEscape treats CQ codes as text', async () => {
|
|
const result = await parseMessage('[CQ:face,id=123]', true);
|
|
expect(result).toEqual([{ type: 'text', text: '[CQ:face,id=123]' }]);
|
|
});
|
|
});
|
|
|
|
describe('CQ code parsing', () => {
|
|
it('parses face CQ code', async () => {
|
|
const result = await parseMessage('[CQ:face,id=123]', false);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('face');
|
|
expect(result[0].faceId).toBe(123);
|
|
});
|
|
|
|
it('parses at CQ code', async () => {
|
|
const result = await parseMessage('[CQ:at,qq=12345]', false);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('at');
|
|
expect(result[0].targetUin).toBe(12345);
|
|
});
|
|
|
|
it('parses at all', async () => {
|
|
const result = await parseMessage('[CQ:at,qq=all]', false);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('at');
|
|
expect(result[0].targetUin).toBe(0);
|
|
});
|
|
|
|
it('parses mixed text and CQ codes', async () => {
|
|
const result = await parseMessage('Hello [CQ:face,id=1] World', false);
|
|
expect(result).toHaveLength(3);
|
|
expect(result[0]).toEqual({ type: 'text', text: 'Hello ' });
|
|
expect(result[1].type).toBe('face');
|
|
expect(result[2]).toEqual({ type: 'text', text: ' World' });
|
|
});
|
|
|
|
it('unescapes CQ special chars', async () => {
|
|
const result = await parseMessage('a&b[c]d', false);
|
|
expect(result).toEqual([{ type: 'text', text: 'a&b[c]d' }]);
|
|
});
|
|
});
|
|
|
|
describe('JSON segment array', () => {
|
|
it('parses text segment', async () => {
|
|
const result = await parseMessage(
|
|
[{ type: 'text', data: { text: 'hello' } }] as any,
|
|
false,
|
|
);
|
|
expect(result).toEqual([{ type: 'text', text: 'hello' }]);
|
|
});
|
|
|
|
it('parses face segment', async () => {
|
|
const result = await parseMessage(
|
|
[{ type: 'face', data: { id: 123 } }] as any,
|
|
false,
|
|
);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('face');
|
|
expect(result[0].faceId).toBe(123);
|
|
});
|
|
|
|
it('parses image segment', async () => {
|
|
const result = await parseMessage(
|
|
[{ type: 'image', data: { file: 'https://example.com/img.png' } }] as any,
|
|
false,
|
|
);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('image');
|
|
expect(result[0].url).toBe('https://example.com/img.png');
|
|
});
|
|
|
|
it('parses at segment', async () => {
|
|
const result = await parseMessage(
|
|
[{ type: 'at', data: { qq: 12345 } }] as any,
|
|
false,
|
|
);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('at');
|
|
expect(result[0].targetUin).toBe(12345);
|
|
});
|
|
|
|
it('parses multiple segments', async () => {
|
|
const result = await parseMessage(
|
|
[
|
|
{ type: 'text', data: { text: 'hi ' } },
|
|
{ type: 'at', data: { qq: 999 } },
|
|
{ type: 'text', data: { text: ' there' } },
|
|
] as any,
|
|
false,
|
|
);
|
|
expect(result).toHaveLength(3);
|
|
expect(result[0].type).toBe('text');
|
|
expect(result[1].type).toBe('at');
|
|
expect(result[2].type).toBe('text');
|
|
});
|
|
|
|
it('skips unknown segment types', async () => {
|
|
const result = await parseMessage(
|
|
[
|
|
{ type: 'text', data: { text: 'ok' } },
|
|
{ type: 'unknown_type_xyz', data: {} },
|
|
] as any,
|
|
false,
|
|
);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('text');
|
|
});
|
|
});
|
|
|
|
describe('special segments', () => {
|
|
it('parses json segment', async () => {
|
|
const result = await parseMessage(
|
|
[{ type: 'json', data: { data: '{"app":"test"}' } }] as any,
|
|
false,
|
|
);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('json');
|
|
expect(result[0].text).toBe('{"app":"test"}');
|
|
});
|
|
|
|
it('parses reply segment with resolveReplySequence', async () => {
|
|
const result = await parseMessage(
|
|
[{ type: 'reply', data: { id: 42 } }] as any,
|
|
false,
|
|
{ resolveReplySequence: (id) => id === 42 ? 100 : null },
|
|
);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('reply');
|
|
expect(result[0].replySeq).toBe(100);
|
|
});
|
|
|
|
it('parses share as json card', async () => {
|
|
const result = await parseMessage(
|
|
[{ type: 'share', data: { url: 'https://example.com', title: 'Test' } }] as any,
|
|
false,
|
|
);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('json');
|
|
expect(result[0].text).toBeDefined();
|
|
const parsed = JSON.parse(result[0].text!);
|
|
expect(parsed.app).toBe('com.tencent.structmsg');
|
|
expect(parsed.meta.news.title).toBe('Test');
|
|
});
|
|
|
|
it('parses rps as face', async () => {
|
|
const result = await parseMessage(
|
|
[{ type: 'rps', data: {} }] as any,
|
|
false,
|
|
);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('face');
|
|
expect(result[0].faceId).toBe(359);
|
|
});
|
|
|
|
it('parses dice as face', async () => {
|
|
const result = await parseMessage(
|
|
[{ type: 'dice', data: {} }] as any,
|
|
false,
|
|
);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('face');
|
|
expect(result[0].faceId).toBe(358);
|
|
});
|
|
|
|
it('parses shake as poke', async () => {
|
|
const result = await parseMessage(
|
|
[{ type: 'shake', data: {} }] as any,
|
|
false,
|
|
);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('poke');
|
|
});
|
|
|
|
it('ignores anonymous segment', async () => {
|
|
const result = await parseMessage(
|
|
[
|
|
{ type: 'anonymous', data: {} },
|
|
{ type: 'text', data: { text: 'hi' } },
|
|
] as any,
|
|
false,
|
|
);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('text');
|
|
});
|
|
|
|
it('parses location as json card', async () => {
|
|
const result = await parseMessage(
|
|
[{ type: 'location', data: { lat: '39.9', lon: '116.3', title: 'Beijing' } }] as any,
|
|
false,
|
|
);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('json');
|
|
expect(result[0].text).toBeDefined();
|
|
const parsed = JSON.parse(result[0].text!);
|
|
expect(parsed.app).toBe('com.tencent.map');
|
|
expect(parsed.meta.Location.lat).toBe('39.9');
|
|
});
|
|
|
|
it('parses contact as json card', async () => {
|
|
const result = await parseMessage(
|
|
[{ type: 'contact', data: { type: 'group', id: '12345' } }] as any,
|
|
false,
|
|
);
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('json');
|
|
expect(result[0].text).toBeDefined();
|
|
const parsed = JSON.parse(result[0].text!);
|
|
expect(parsed.meta.contact.type).toBe('group');
|
|
});
|
|
});
|
|
|
|
describe('single segment object', () => {
|
|
it('parses single segment object', async () => {
|
|
const result = await parseMessage(
|
|
{ type: 'text', data: { text: 'single' } } as any,
|
|
false,
|
|
);
|
|
expect(result).toEqual([{ type: 'text', text: 'single' }]);
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('returns empty for null', async () => {
|
|
const result = await parseMessage(null as any, false);
|
|
expect(result).toEqual([]);
|
|
});
|
|
|
|
it('returns empty for number', async () => {
|
|
const result = await parseMessage(42 as any, false);
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
});
|