135 lines
3.6 KiB
TypeScript
135 lines
3.6 KiB
TypeScript
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
import { join, relative } from 'path';
|
|
|
|
const repoRoot = join(__dirname, '../../../..');
|
|
const pluginRoot = join(repoRoot, 'src/modules/qqbot/plugins');
|
|
|
|
const collectTsFiles = (root: string): string[] => {
|
|
if (!existsSync(root)) return [];
|
|
|
|
return readdirSync(root).flatMap((name) => {
|
|
const filePath = join(root, name);
|
|
const stat = statSync(filePath);
|
|
if (stat.isDirectory()) return collectTsFiles(filePath);
|
|
return filePath.endsWith('.ts') ? [filePath] : [];
|
|
});
|
|
};
|
|
|
|
const toRepoPath = (filePath: string) =>
|
|
relative(repoRoot, filePath).replace(/\\/g, '/');
|
|
|
|
const requiredPluginPaths = [
|
|
'plugin.json',
|
|
'src/index.ts',
|
|
'src/operations',
|
|
'src/events',
|
|
'src/domain',
|
|
'src/application',
|
|
'src/infrastructure/integration',
|
|
'src/infrastructure/storage',
|
|
'src/config',
|
|
'src/assets',
|
|
'src/migrations',
|
|
'src/tests',
|
|
];
|
|
|
|
describe('QQBot plugin package boundary', () => {
|
|
it('uses only approved built-in plugin package keys as directory names', () => {
|
|
const pluginDirs = readdirSync(pluginRoot)
|
|
.filter((name) => statSync(join(pluginRoot, name)).isDirectory())
|
|
.sort();
|
|
|
|
expect(pluginDirs).toEqual([
|
|
'bangdream',
|
|
'ff14-market',
|
|
'fflogs',
|
|
'repeater',
|
|
]);
|
|
});
|
|
|
|
it('uses the same package shape for every built-in plugin', () => {
|
|
const missing = ['bangdream', 'ff14-market', 'fflogs', 'repeater'].flatMap(
|
|
(pluginKey) =>
|
|
requiredPluginPaths
|
|
.map((pathName) => `${pluginKey}/${pathName}`)
|
|
.filter((pathName) => !existsSync(join(pluginRoot, pathName))),
|
|
);
|
|
|
|
expect(missing).toEqual([]);
|
|
});
|
|
|
|
it('keeps plugin runtime source independent from host internals and direct IO', () => {
|
|
const banned = [
|
|
{
|
|
name: '@nestjs',
|
|
pattern: /@nestjs\//,
|
|
},
|
|
{
|
|
name: 'Admin module import',
|
|
pattern: /@\/modules\/admin\//,
|
|
},
|
|
{
|
|
name: 'QQBot core import',
|
|
pattern: /@\/modules\/qqbot\/core\//,
|
|
},
|
|
{
|
|
name: 'ConfigService',
|
|
pattern: /\bConfigService\b/,
|
|
},
|
|
{
|
|
name: 'DictService',
|
|
pattern: /\bDictService\b/,
|
|
},
|
|
{
|
|
name: 'process.env',
|
|
pattern: /process\.env/,
|
|
},
|
|
{
|
|
name: 'direct axios',
|
|
pattern: /\baxios\b/,
|
|
},
|
|
{
|
|
name: 'direct fetch',
|
|
pattern: /\bfetch\(/,
|
|
},
|
|
{
|
|
name: 'direct fs',
|
|
pattern: /from ['"](?:node:)?fs['"]|require\(['"](?:node:)?fs['"]\)/,
|
|
},
|
|
{
|
|
name: 'import-time timer',
|
|
pattern: /\bset(?:Interval|Timeout)\(/,
|
|
},
|
|
];
|
|
|
|
const violations = collectTsFiles(pluginRoot).flatMap((filePath) => {
|
|
const source = readFileSync(filePath, 'utf8');
|
|
return banned
|
|
.filter(({ pattern }) => pattern.test(source))
|
|
.map(({ name }) => `${toRepoPath(filePath)} :: ${name}`);
|
|
});
|
|
|
|
expect(violations).toEqual([]);
|
|
});
|
|
|
|
it('uses plugin.json as the only operation metadata source', () => {
|
|
const duplicateMetadataFiles = collectTsFiles(pluginRoot)
|
|
.filter((filePath) => {
|
|
const file = toRepoPath(filePath);
|
|
if (file.endsWith('/src/index.ts')) return false;
|
|
const source = readFileSync(filePath, 'utf8');
|
|
return (
|
|
/operation-registry|COMMAND_DEFINITIONS|OperationDefinition/.test(
|
|
file,
|
|
) ||
|
|
/operation-registry|COMMAND_DEFINITIONS|OperationDefinition/.test(
|
|
source,
|
|
)
|
|
);
|
|
})
|
|
.map(toRepoPath);
|
|
|
|
expect(duplicateMetadataFiles).toEqual([]);
|
|
});
|
|
});
|