kt-template-online-api/test/modules/qqbot/plugin-platform/manifest.spec.ts

242 lines
6.4 KiB
TypeScript

import {
QQBOT_PLUGIN_ALLOWED_PERMISSIONS,
parseQqbotPluginManifest,
QqbotPluginManifestValidationError,
} from '../../../../src/modules/qqbot/plugin-platform/domain/manifest';
const createValidManifest = () => ({
assets: [
{
key: 'song-card-bg',
path: 'assets/song-card-bg.png',
},
],
author: 'KT',
configSchema: {
properties: {
apiBaseUrl: {
default: 'https://example.com',
type: 'string',
},
},
type: 'object',
},
description: 'BangDream query operations',
entry: 'src/index.ts',
events: [
{
eventName: 'message.group',
handlerName: 'onGroupMessage',
key: 'bangdream.group-message',
name: '群消息事件',
},
],
homepage: 'https://example.com/bangdream',
license: 'MIT',
legacyAliases: ['bangDream'],
migrations: [
{
path: 'migrations/001-init.sql',
version: '1.0.0',
},
],
minApiSdkVersion: '1.0.0',
name: 'BangDream',
operations: [
{
aliases: ['/查歌', '/bd song'],
description: 'Search songs',
handlerName: 'searchSong',
inputSchema: {
properties: {
keyword: {
type: 'string',
},
},
type: 'object',
},
key: 'bangdream.song.search',
name: '查歌',
outputSchema: {
type: 'object',
},
permissions: ['qqbot.send', 'runtime.http', 'asset.read'],
timeoutMs: 5000,
},
],
permissions: ['qqbot.send', 'runtime.http', 'asset.read'],
pluginKey: 'bangdream',
runtime: {
maxConcurrency: 2,
memoryMb: 256,
timeoutMs: 8000,
workerType: 'node-worker',
},
version: '1.2.3',
});
const expectValidationError = (
manifest: unknown,
code: string,
path?: string,
) => {
try {
parseQqbotPluginManifest(manifest, { pluginRoot: 'D:/plugins/bangdream' });
throw new Error('Expected manifest validation to fail');
} catch (error) {
expect(error).toBeInstanceOf(QqbotPluginManifestValidationError);
expect((error as QqbotPluginManifestValidationError).issues).toEqual(
expect.arrayContaining([
expect.objectContaining({
code,
...(path ? { path } : {}),
}),
]),
);
}
};
describe('QQBot plugin manifest contract', () => {
it('parses a complete manifest and normalizes declared plugin capabilities', () => {
const manifest = parseQqbotPluginManifest(createValidManifest(), {
pluginRoot: 'D:/plugins/bangdream',
});
expect(manifest).toMatchObject({
entry: 'src/index.ts',
minApiSdkVersion: '1.0.0',
pluginKey: 'bangdream',
legacyAliases: ['bangDream'],
runtime: {
maxConcurrency: 2,
memoryMb: 256,
timeoutMs: 8000,
workerType: 'node-worker',
},
version: '1.2.3',
});
expect(manifest.permissions).toEqual([
'qqbot.send',
'runtime.http',
'asset.read',
]);
expect(manifest.operations[0]).toMatchObject({
aliases: ['/查歌', '/bd song'],
key: 'bangdream.song.search',
permissions: ['qqbot.send', 'runtime.http', 'asset.read'],
timeoutMs: 5000,
});
expect(manifest.events[0]).toMatchObject({
eventName: 'message.group',
key: 'bangdream.group-message',
});
expect(manifest.assets[0].path).toBe('assets/song-card-bg.png');
expect(manifest.migrations[0].path).toBe('migrations/001-init.sql');
expect(QQBOT_PLUGIN_ALLOWED_PERMISSIONS).toEqual(
expect.arrayContaining([
'asset.read',
'plugin.config.read',
'plugin.config.write',
'plugin.storage.read',
'plugin.storage.write',
'qqbot.event.receive',
'qqbot.send',
'runtime.http',
]),
);
});
it('rejects unknown permissions from both plugin and operation scopes', () => {
const manifest = createValidManifest();
manifest.permissions = ['qqbot.send', 'host.env.read'];
manifest.operations[0].permissions = ['qqbot.send', 'host.fs.read'];
expectValidationError(manifest, 'UNKNOWN_PERMISSION', 'permissions[1]');
expectValidationError(
manifest,
'UNKNOWN_PERMISSION',
'operations[0].permissions[1]',
);
});
it('rejects duplicate operation keys and duplicate event keys', () => {
const manifest = createValidManifest();
manifest.operations.push({
...manifest.operations[0],
handlerName: 'searchSongAgain',
});
manifest.events.push({
...manifest.events[0],
handlerName: 'onGroupMessageAgain',
});
expectValidationError(manifest, 'DUPLICATE_OPERATION_KEY', 'operations[1]');
expectValidationError(manifest, 'DUPLICATE_EVENT_KEY', 'events[1]');
});
it('rejects manifests without explicit runtime budgets', () => {
const manifest = createValidManifest();
manifest.runtime = {
workerType: 'node-worker',
} as typeof manifest.runtime;
delete manifest.operations[0].timeoutMs;
expectValidationError(manifest, 'MISSING_RUNTIME_BUDGET', 'runtime');
expectValidationError(
manifest,
'MISSING_OPERATION_TIMEOUT',
'operations[0].timeoutMs',
);
});
it('rejects package paths outside the plugin root', () => {
const manifest = createValidManifest();
manifest.entry = '../outside.ts';
manifest.assets[0].path = 'assets/../../secret.txt';
manifest.migrations[0].path = 'C:/secrets/drop.sql';
expectValidationError(manifest, 'PATH_OUTSIDE_PLUGIN_ROOT', 'entry');
expectValidationError(
manifest,
'PATH_OUTSIDE_PLUGIN_ROOT',
'assets[0].path',
);
expectValidationError(
manifest,
'PATH_OUTSIDE_PLUGIN_ROOT',
'migrations[0].path',
);
});
it('rejects incomplete capability metadata', () => {
const manifest = createValidManifest();
manifest.operations[0].handlerName = '';
manifest.events[0].handlerName = '';
manifest.assets[0].key = '';
manifest.migrations[0].version = 'v1';
manifest.legacyAliases = ['../bad'];
expectValidationError(
manifest,
'MISSING_OPERATION_HANDLER',
'operations[0].handlerName',
);
expectValidationError(
manifest,
'MISSING_EVENT_HANDLER',
'events[0].handlerName',
);
expectValidationError(manifest, 'MISSING_ASSET_KEY', 'assets[0].key');
expectValidationError(
manifest,
'INVALID_SEMVER',
'migrations[0].version',
);
expectValidationError(
manifest,
'INVALID_LEGACY_ALIAS',
'legacyAliases[0]',
);
});
});