269 lines
7.6 KiB
TypeScript
269 lines
7.6 KiB
TypeScript
import {
|
|
mkdtempSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join, sep } from 'node:path';
|
|
|
|
import { QqbotPluginPackagePathPolicyService } from '@/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-path-policy.service';
|
|
import { QqbotPluginPackageSourceService } from '@/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-source.service';
|
|
|
|
describe('QqbotPluginPackageSourceService', () => {
|
|
let tempRoot: string;
|
|
|
|
beforeEach(() => {
|
|
tempRoot = mkdtempSync(join(tmpdir(), 'qqbot-plugin-source-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(tempRoot, { force: true, recursive: true });
|
|
});
|
|
|
|
it('discovers packages from plugin.json without importing package code', async () => {
|
|
const packageRoot = join(tempRoot, 'sample');
|
|
mkdirSync(join(packageRoot, 'src'), { recursive: true });
|
|
writeFileSync(
|
|
join(packageRoot, 'plugin.json'),
|
|
JSON.stringify({
|
|
key: 'sample',
|
|
name: 'Sample',
|
|
version: '1.0.0',
|
|
entry: 'src/index.ts',
|
|
runtime: {
|
|
workerType: 'thread',
|
|
timeoutMs: 5000,
|
|
memoryMb: 128,
|
|
maxConcurrency: 1,
|
|
configKeys: ['SAMPLE_TOKEN'],
|
|
},
|
|
operations: [],
|
|
}),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
join(packageRoot, 'src', 'index.ts'),
|
|
'throw new Error("entry must not be imported during discovery");',
|
|
'utf8',
|
|
);
|
|
|
|
const source = new QqbotPluginPackageSourceService(
|
|
new QqbotPluginPackagePathPolicyService([tempRoot]),
|
|
);
|
|
|
|
await expect(source.discoverPackages()).resolves.toEqual([
|
|
expect.objectContaining({
|
|
entry: 'src/index.ts',
|
|
entryFile: join(packageRoot, 'src', 'index.ts'),
|
|
manifest: expect.objectContaining({
|
|
key: 'sample',
|
|
pluginKey: 'sample',
|
|
runtime: expect.objectContaining({ workerType: 'thread' }),
|
|
}),
|
|
packageRoot,
|
|
pluginKey: 'sample',
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it('rejects package entries that escape controlled roots', async () => {
|
|
const packageRoot = join(tempRoot, 'escape');
|
|
mkdirSync(packageRoot, { recursive: true });
|
|
writeFileSync(
|
|
join(packageRoot, 'plugin.json'),
|
|
JSON.stringify({
|
|
key: 'escape',
|
|
name: 'Escape',
|
|
version: '1.0.0',
|
|
entry: `..${sep}outside.ts`,
|
|
runtime: {
|
|
workerType: 'thread',
|
|
timeoutMs: 5000,
|
|
memoryMb: 128,
|
|
maxConcurrency: 1,
|
|
},
|
|
operations: [],
|
|
}),
|
|
'utf8',
|
|
);
|
|
|
|
const source = new QqbotPluginPackageSourceService(
|
|
new QqbotPluginPackagePathPolicyService([tempRoot]),
|
|
);
|
|
|
|
await expect(source.discoverPackages()).rejects.toThrow(
|
|
'PATH_OUTSIDE_PLUGIN_ROOT',
|
|
);
|
|
});
|
|
|
|
it('resolves entry files from parser-normalized package paths', async () => {
|
|
const packageRoot = join(tempRoot, 'windows-entry');
|
|
mkdirSync(join(packageRoot, 'src'), { recursive: true });
|
|
writeFileSync(
|
|
join(packageRoot, 'plugin.json'),
|
|
JSON.stringify({
|
|
key: 'windows-entry',
|
|
name: 'Windows Entry',
|
|
version: '1.0.0',
|
|
entry: 'src\\\\index.ts',
|
|
runtime: {
|
|
workerType: 'thread',
|
|
timeoutMs: 5000,
|
|
memoryMb: 128,
|
|
maxConcurrency: 1,
|
|
},
|
|
operations: [],
|
|
}),
|
|
'utf8',
|
|
);
|
|
|
|
const pathPolicy = new QqbotPluginPackagePathPolicyService([tempRoot]);
|
|
const resolveEntryFileSpy = jest.spyOn(pathPolicy, 'resolveEntryFile');
|
|
const source = new QqbotPluginPackageSourceService(pathPolicy);
|
|
|
|
await expect(source.discoverPackages()).resolves.toEqual([
|
|
expect.objectContaining({
|
|
entry: 'src/index.ts',
|
|
entryFile: join(packageRoot, 'src', 'index.ts'),
|
|
pluginKey: 'windows-entry',
|
|
}),
|
|
]);
|
|
expect(resolveEntryFileSpy).toHaveBeenCalledWith(
|
|
packageRoot,
|
|
'src/index.ts',
|
|
);
|
|
});
|
|
|
|
it('resolves TypeScript manifest entries to compiled JavaScript files in dist packages', async () => {
|
|
const packageRoot = join(tempRoot, 'compiled-entry');
|
|
mkdirSync(join(packageRoot, 'src'), { recursive: true });
|
|
writeFileSync(
|
|
join(packageRoot, 'plugin.json'),
|
|
JSON.stringify({
|
|
key: 'compiled-entry',
|
|
name: 'Compiled Entry',
|
|
version: '1.0.0',
|
|
entry: 'src/index.ts',
|
|
runtime: {
|
|
workerType: 'thread',
|
|
timeoutMs: 5000,
|
|
memoryMb: 128,
|
|
maxConcurrency: 1,
|
|
},
|
|
operations: [],
|
|
}),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
join(packageRoot, 'src', 'index.js'),
|
|
'module.exports = { createPlugin() {} };',
|
|
'utf8',
|
|
);
|
|
|
|
const source = new QqbotPluginPackageSourceService(
|
|
new QqbotPluginPackagePathPolicyService([tempRoot]),
|
|
);
|
|
|
|
await expect(source.discoverPackages()).resolves.toEqual([
|
|
expect.objectContaining({
|
|
entry: 'src/index.ts',
|
|
entryFile: join(packageRoot, 'src', 'index.js'),
|
|
pluginKey: 'compiled-entry',
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it('discovers the default built-in plugin root from production dist output', async () => {
|
|
const previousCwd = process.cwd();
|
|
const productionRoot = join(tempRoot, 'production-app');
|
|
const distPluginRoot = join(
|
|
productionRoot,
|
|
'dist',
|
|
'modules',
|
|
'qqbot',
|
|
'plugins',
|
|
);
|
|
mkdirSync(distPluginRoot, { recursive: true });
|
|
|
|
try {
|
|
process.chdir(productionRoot);
|
|
jest.resetModules();
|
|
const {
|
|
QqbotPluginPackagePathPolicyService: RuntimePathPolicyService,
|
|
} = await import(
|
|
'../../../../src/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-path-policy.service'
|
|
);
|
|
const pathPolicy = new RuntimePathPolicyService();
|
|
|
|
expect(pathPolicy.listExistingRoots()).toEqual([distPluginRoot]);
|
|
} finally {
|
|
process.chdir(previousCwd);
|
|
jest.resetModules();
|
|
}
|
|
});
|
|
|
|
it('resolves persisted source package paths to the current dist controlled root', () => {
|
|
const distPluginRoot = join(
|
|
tempRoot,
|
|
'production-app',
|
|
'dist',
|
|
'modules',
|
|
'qqbot',
|
|
'plugins',
|
|
);
|
|
const packageRoot = join(distPluginRoot, 'bilibili-card');
|
|
mkdirSync(join(packageRoot, 'src'), { recursive: true });
|
|
writeFileSync(
|
|
join(packageRoot, 'src', 'index.js'),
|
|
'module.exports = { createPlugin() {} };',
|
|
'utf8',
|
|
);
|
|
|
|
const source = new QqbotPluginPackageSourceService(
|
|
new QqbotPluginPackagePathPolicyService([distPluginRoot]),
|
|
);
|
|
|
|
expect(
|
|
source.resolveDescriptor('src/modules/qqbot/plugins/bilibili-card', {
|
|
key: 'bilibili-card',
|
|
name: 'Bilibili Card',
|
|
version: '1.0.0',
|
|
entry: 'src/index.ts',
|
|
runtime: {
|
|
workerType: 'thread',
|
|
timeoutMs: 10000,
|
|
memoryMb: 128,
|
|
maxConcurrency: 1,
|
|
},
|
|
operations: [],
|
|
events: [],
|
|
}),
|
|
).toEqual(
|
|
expect.objectContaining({
|
|
entryFile: join(packageRoot, 'src', 'index.js'),
|
|
packageRoot,
|
|
pluginKey: 'bilibili-card',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('does not keep platform-side manifest transfer shims', () => {
|
|
const source = readFileSync(
|
|
join(
|
|
process.cwd(),
|
|
'src/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-source.service.ts',
|
|
),
|
|
'utf8',
|
|
);
|
|
|
|
expect(source).not.toMatch(
|
|
/normalizeManifestForCurrentParser|attachPackageKeyAlias|readPackageKey/,
|
|
);
|
|
expect(source).not.toMatch(
|
|
/thread[\s\S]{0,80}node-worker|node-worker[\s\S]{0,80}thread/,
|
|
);
|
|
});
|
|
});
|