test: 增加QQBot强收敛门禁
This commit is contained in:
parent
80691af285
commit
8ada857f57
@ -227,7 +227,7 @@ Expected: fails on current `bangDream`, `ff14Market`, Nest services, and old reg
|
|||||||
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugin-platform\plugin-lifecycle-runtime.spec.ts`
|
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugin-platform\plugin-lifecycle-runtime.spec.ts`
|
||||||
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\core\qqbot-core-plugin-ports.spec.ts`
|
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\core\qqbot-core-plugin-ports.spec.ts`
|
||||||
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\napcat\napcat-persistent-login-state.spec.ts`
|
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\napcat\napcat-persistent-login-state.spec.ts`
|
||||||
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugins\bangdream\bangdream-operation-parity.spec.ts`
|
- Create: `D:\MyFiles\KT\Node\kt-template-online-api\test\modules\qqbot\plugins\bangdream-rewrite\bangdream-operation-parity.spec.ts`
|
||||||
|
|
||||||
- [ ] Lifecycle test covers install, enable, active registry refresh, operation execution, event dispatch, disable, uninstall, timeout, crash isolation, and runtime event persistence.
|
- [ ] Lifecycle test covers install, enable, active registry refresh, operation execution, event dispatch, disable, uninstall, timeout, crash isolation, and runtime event persistence.
|
||||||
- [ ] Core ports test covers command prefix and alias matching in core, raw args handed to Plugin Platform, plugin output normalized back to command log/send queue, and plugin event dispatch after core does not consume a message.
|
- [ ] Core ports test covers command prefix and alias matching in core, raw args handed to Plugin Platform, plugin output normalized back to command log/send queue, and plugin event dispatch after core does not consume a message.
|
||||||
@ -236,7 +236,7 @@ Expected: fails on current `bangDream`, `ff14Market`, Nest services, and old reg
|
|||||||
- [ ] Verify RED:
|
- [ ] Verify RED:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/plugin-platform/plugin-lifecycle-runtime.spec.ts test/modules/qqbot/core/qqbot-core-plugin-ports.spec.ts test/modules/qqbot/napcat/napcat-persistent-login-state.spec.ts test/modules/qqbot/plugins/bangdream/bangdream-operation-parity.spec.ts
|
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/plugin-platform/plugin-lifecycle-runtime.spec.ts test/modules/qqbot/core/qqbot-core-plugin-ports.spec.ts test/modules/qqbot/napcat/napcat-persistent-login-state.spec.ts test/modules/qqbot/plugins/bangdream-rewrite/bangdream-operation-parity.spec.ts
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: fails because current runtime and plugin package boundaries are not the approved model.
|
Expected: fails because current runtime and plugin package boundaries are not the approved model.
|
||||||
@ -448,7 +448,7 @@ git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 重建QQBot插
|
|||||||
- [ ] Run:
|
- [ ] Run:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/plugins/bangdream/bangdream-operation-parity.spec.ts test/modules/qqbot/architecture/qqbot-plugin-package-boundary.spec.ts test/modules/qqbot/plugins/plugin-platform-migration.spec.ts test/modules/qqbot/plugins/plugin-registry-compat.spec.ts
|
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/plugins/bangdream-rewrite/bangdream-operation-parity.spec.ts test/modules/qqbot/architecture/qqbot-plugin-package-boundary.spec.ts test/modules/qqbot/plugins/plugin-platform-migration.spec.ts test/modules/qqbot/plugins/plugin-registry-compat.spec.ts
|
||||||
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
|
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -471,7 +471,7 @@ powershell -ExecutionPolicy Bypass -File D:\MyFiles\KT\Node\kt-template-online-a
|
|||||||
- [ ] Commit:
|
- [ ] Commit:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
git -C D:\MyFiles\KT\Node\kt-template-online-api add src/modules/qqbot/plugins/bangdream test/modules/qqbot/plugins/bangdream test/modules/qqbot/architecture sql/refactor-v3 docs/refactor-v3 API.md
|
git -C D:\MyFiles\KT\Node\kt-template-online-api add src/modules/qqbot/plugins/bangdream test/modules/qqbot/plugins/bangdream-rewrite test/modules/qqbot/architecture sql/refactor-v3 docs/refactor-v3 API.md
|
||||||
git -C D:\MyFiles\KT\Node\kt-template-online-api add -u src/modules/qqbot/plugins/bangDream
|
git -C D:\MyFiles\KT\Node\kt-template-online-api add -u src/modules/qqbot/plugins/bangDream
|
||||||
git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "refactor: 重写BangDream插件"
|
git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "refactor: 重写BangDream插件"
|
||||||
```
|
```
|
||||||
@ -739,7 +739,7 @@ Expected: no old runtime implementation references; only explicit legacy alias f
|
|||||||
- [ ] Run:
|
- [ ] Run:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/architecture/qqbot-module-boundary.spec.ts test/modules/qqbot/architecture/qqbot-plugin-package-boundary.spec.ts test/modules/qqbot/plugin-platform/plugin-lifecycle-runtime.spec.ts test/modules/qqbot/plugins/bangdream/bangdream-operation-parity.spec.ts test/modules/qqbot/napcat/napcat-persistent-login-state.spec.ts
|
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/architecture/qqbot-module-boundary.spec.ts test/modules/qqbot/architecture/qqbot-plugin-package-boundary.spec.ts test/modules/qqbot/plugin-platform/plugin-lifecycle-runtime.spec.ts test/modules/qqbot/plugins/bangdream-rewrite/bangdream-operation-parity.spec.ts test/modules/qqbot/napcat/napcat-persistent-login-state.spec.ts
|
||||||
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
|
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
|
||||||
pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin -F @vben/web-antdv-next run typecheck
|
pnpm --dir D:\MyFiles\KT\Vue\kt-template-admin -F @vben/web-antdv-next run typecheck
|
||||||
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --project api --content-scan-mode changed
|
pnpm --dir D:\MyFiles\KT\mcp\ktWorkflow run global-review -- --project api --content-scan-mode changed
|
||||||
@ -771,7 +771,7 @@ git -C D:\MyFiles\KT commit -m "docs: 更新QQBot插件架构规则"
|
|||||||
```powershell
|
```powershell
|
||||||
git -C D:\MyFiles\KT\Node\kt-template-online-api status --short --branch
|
git -C D:\MyFiles\KT\Node\kt-template-online-api status --short --branch
|
||||||
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
|
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api run typecheck
|
||||||
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/architecture/qqbot-module-boundary.spec.ts test/modules/qqbot/architecture/qqbot-plugin-package-boundary.spec.ts test/modules/qqbot/core/qqbot-core-plugin-ports.spec.ts test/modules/qqbot/plugin-platform/plugin-lifecycle-runtime.spec.ts test/modules/qqbot/plugins/bangdream/bangdream-operation-parity.spec.ts test/modules/qqbot/napcat/napcat-persistent-login-state.spec.ts
|
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand --runTestsByPath test/modules/qqbot/architecture/qqbot-module-boundary.spec.ts test/modules/qqbot/architecture/qqbot-plugin-package-boundary.spec.ts test/modules/qqbot/core/qqbot-core-plugin-ports.spec.ts test/modules/qqbot/plugin-platform/plugin-lifecycle-runtime.spec.ts test/modules/qqbot/plugins/bangdream-rewrite/bangdream-operation-parity.spec.ts test/modules/qqbot/napcat/napcat-persistent-login-state.spec.ts
|
||||||
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand
|
pnpm --dir D:\MyFiles\KT\Node\kt-template-online-api exec jest --runInBand
|
||||||
git -C D:\MyFiles\KT\Node\kt-template-online-api diff --check
|
git -C D:\MyFiles\KT\Node\kt-template-online-api diff --check
|
||||||
```
|
```
|
||||||
|
|||||||
143
test/modules/qqbot/architecture/qqbot-module-boundary.spec.ts
Normal file
143
test/modules/qqbot/architecture/qqbot-module-boundary.spec.ts
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
||||||
|
import { basename, join, relative } from 'path';
|
||||||
|
|
||||||
|
const repoRoot = join(__dirname, '../../../..');
|
||||||
|
const qqbotRoot = join(repoRoot, 'src/modules/qqbot');
|
||||||
|
|
||||||
|
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 readSource = (filePath: string) => readFileSync(filePath, 'utf8');
|
||||||
|
|
||||||
|
const toRepoPath = (filePath: string) =>
|
||||||
|
relative(repoRoot, filePath).replace(/\\/g, '/');
|
||||||
|
|
||||||
|
const listTopLevelEntries = (moduleName: string) => {
|
||||||
|
const root = join(qqbotRoot, moduleName);
|
||||||
|
return readdirSync(root)
|
||||||
|
.map((name) => {
|
||||||
|
const filePath = join(root, name);
|
||||||
|
const stat = statSync(filePath);
|
||||||
|
return {
|
||||||
|
isDirectory: stat.isDirectory(),
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('QQBot third-phase module boundaries', () => {
|
||||||
|
it('keeps core, plugin-platform, and napcat top-level structure strict', () => {
|
||||||
|
const allowedDirectories = new Set([
|
||||||
|
'application',
|
||||||
|
'contract',
|
||||||
|
'domain',
|
||||||
|
'infrastructure',
|
||||||
|
'schema',
|
||||||
|
]);
|
||||||
|
const allowedRootFiles = new Set(['index.ts']);
|
||||||
|
|
||||||
|
const violations = ['core', 'plugin-platform', 'napcat'].flatMap(
|
||||||
|
(moduleName) =>
|
||||||
|
listTopLevelEntries(moduleName)
|
||||||
|
.filter((entry) =>
|
||||||
|
entry.isDirectory
|
||||||
|
? !allowedDirectories.has(entry.name)
|
||||||
|
: !allowedRootFiles.has(entry.name) &&
|
||||||
|
!entry.name.endsWith('.module.ts'),
|
||||||
|
)
|
||||||
|
.map((entry) => `${moduleName}/${entry.name}`),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(violations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prevents QQBot core from importing concrete plugin implementation', () => {
|
||||||
|
const violations = collectTsFiles(join(qqbotRoot, 'core'))
|
||||||
|
.map((filePath) => ({
|
||||||
|
file: toRepoPath(filePath),
|
||||||
|
source: readSource(filePath),
|
||||||
|
}))
|
||||||
|
.filter(({ source }) =>
|
||||||
|
/@\/modules\/qqbot\/plugins\/|src\/modules\/qqbot\/plugins\//.test(
|
||||||
|
source,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map(({ file }) => file);
|
||||||
|
|
||||||
|
expect(violations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps QqbotCoreModule free of plugin platform controllers, registries, SDKs, and concrete plugin services', () => {
|
||||||
|
const source = readSource(join(qqbotRoot, 'core/qqbot-core.module.ts'));
|
||||||
|
const bannedSymbols = [
|
||||||
|
'QqbotPluginController',
|
||||||
|
'QqbotPluginHttpClientService',
|
||||||
|
'QqbotPluginRegistryService',
|
||||||
|
'QqbotEventPluginRegistryService',
|
||||||
|
'QqbotBangDream',
|
||||||
|
'QqbotFf14',
|
||||||
|
'QqbotFflogs',
|
||||||
|
'QqbotRepeater',
|
||||||
|
'TsuguApplicationService',
|
||||||
|
];
|
||||||
|
|
||||||
|
const violations = bannedSymbols.filter((symbol) =>
|
||||||
|
source.includes(symbol),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(violations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps core domain independent from Nest, TypeORM, Admin, Plugin Platform, Docker, and HTTP implementation', () => {
|
||||||
|
const violations = collectTsFiles(join(qqbotRoot, 'core/domain'))
|
||||||
|
.map((filePath) => ({
|
||||||
|
file: toRepoPath(filePath),
|
||||||
|
source: readSource(filePath),
|
||||||
|
}))
|
||||||
|
.filter(({ source }) =>
|
||||||
|
/@nestjs\/|typeorm|@\/modules\/admin|@\/modules\/qqbot\/plugin-platform|child_process|docker|ssh|axios|fetch\(/i.test(
|
||||||
|
source,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map(({ file }) => file);
|
||||||
|
|
||||||
|
expect(violations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps Docker and SSH shell construction inside napcat infrastructure integration only', () => {
|
||||||
|
const violations = collectTsFiles(join(qqbotRoot, 'napcat'))
|
||||||
|
.map((filePath) => ({
|
||||||
|
file: toRepoPath(filePath),
|
||||||
|
source: readSource(filePath),
|
||||||
|
}))
|
||||||
|
.filter(({ file, source }) => {
|
||||||
|
if (file.includes('/napcat/infrastructure/integration/')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return /child_process|spawn\(|\bssh\b|\bdocker\b|docker run|docker logs/i.test(
|
||||||
|
source,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map(({ file }) => file);
|
||||||
|
|
||||||
|
expect(violations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has schema ownership notes for each QQBot runtime module', () => {
|
||||||
|
const missing = ['core', 'plugin-platform', 'napcat']
|
||||||
|
.map((moduleName) => join(qqbotRoot, moduleName, 'schema/README.md'))
|
||||||
|
.filter((filePath) => !existsSync(filePath))
|
||||||
|
.map((filePath) => `${basename(join(filePath, '../..'))}/schema/README.md`);
|
||||||
|
|
||||||
|
expect(missing).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,134 @@
|
|||||||
|
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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
58
test/modules/qqbot/core/qqbot-core-plugin-ports.spec.ts
Normal file
58
test/modules/qqbot/core/qqbot-core-plugin-ports.spec.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { existsSync, readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const repoRoot = join(__dirname, '../../../..');
|
||||||
|
|
||||||
|
const readSource = (relativePath: string) =>
|
||||||
|
readFileSync(join(repoRoot, relativePath), 'utf8');
|
||||||
|
|
||||||
|
describe('QQBot core plugin execution ports', () => {
|
||||||
|
it('defines core-side plugin execution and event dispatch ports', () => {
|
||||||
|
const portPath = join(
|
||||||
|
repoRoot,
|
||||||
|
'src/modules/qqbot/core/domain/plugin-execution.port.ts',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(existsSync(portPath)).toBe(true);
|
||||||
|
if (!existsSync(portPath)) return;
|
||||||
|
|
||||||
|
const source = readFileSync(portPath, 'utf8');
|
||||||
|
expect(source).toEqual(expect.stringContaining('executeOperation'));
|
||||||
|
expect(source).toEqual(expect.stringContaining('dispatchEvent'));
|
||||||
|
expect(source).toEqual(expect.stringContaining('listActiveOperations'));
|
||||||
|
expect(source).toEqual(expect.stringContaining('getOperationByCommand'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps command parser generic and leaves plugin-specific parsing to plugins', () => {
|
||||||
|
const source = readSource(
|
||||||
|
'src/modules/qqbot/core/command/qqbot-command-parser.service.ts',
|
||||||
|
);
|
||||||
|
|
||||||
|
const bannedParserSignals = [
|
||||||
|
'DictService',
|
||||||
|
'ff14Price',
|
||||||
|
'fflogsCharacter',
|
||||||
|
'@/modules/qqbot/plugins/',
|
||||||
|
].filter((signal) => source.includes(signal));
|
||||||
|
|
||||||
|
expect(bannedParserSignals).toEqual([]);
|
||||||
|
expect(source).toContain('rawArgs');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('dispatches unconsumed events through Plugin Platform instead of directly invoking Repeater', () => {
|
||||||
|
const source = readSource(
|
||||||
|
'src/modules/qqbot/core/rule/qqbot-rule-engine.service.ts',
|
||||||
|
);
|
||||||
|
|
||||||
|
const bannedRepeaterSignals = [
|
||||||
|
'QqbotRepeaterPluginService',
|
||||||
|
'@/modules/qqbot/plugins/repeater',
|
||||||
|
].filter((signal) => source.includes(signal));
|
||||||
|
const missingDispatcherSignals = ['dispatchEvent'].filter(
|
||||||
|
(signal) => !source.includes(signal),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(bannedRepeaterSignals).toEqual([]);
|
||||||
|
expect(missingDispatcherSignals).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
import { existsSync, readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const repoRoot = join(__dirname, '../../../..');
|
||||||
|
|
||||||
|
const readSource = (relativePath: string) =>
|
||||||
|
readFileSync(join(repoRoot, relativePath), 'utf8');
|
||||||
|
|
||||||
|
describe('NapCat persistent login state contract', () => {
|
||||||
|
it('owns the approved third-phase persistence tables', () => {
|
||||||
|
const persistenceRoot = join(
|
||||||
|
repoRoot,
|
||||||
|
'src/modules/qqbot/napcat/infrastructure/persistence',
|
||||||
|
);
|
||||||
|
const sqlSchema = readSource('sql/refactor-v3/00-full-schema.sql');
|
||||||
|
const persistenceFiles = existsSync(persistenceRoot)
|
||||||
|
? readSource(
|
||||||
|
'src/modules/qqbot/napcat/infrastructure/persistence/qqbot-account-napcat.entity.ts',
|
||||||
|
) +
|
||||||
|
'\n' +
|
||||||
|
readSource(
|
||||||
|
'src/modules/qqbot/napcat/infrastructure/persistence/qqbot-napcat-container.entity.ts',
|
||||||
|
)
|
||||||
|
: '';
|
||||||
|
const source = `${sqlSchema}\n${persistenceFiles}`;
|
||||||
|
|
||||||
|
const missing = [
|
||||||
|
'napcat_container',
|
||||||
|
'napcat_device_identity',
|
||||||
|
'napcat_account_binding',
|
||||||
|
'napcat_login_session',
|
||||||
|
'napcat_login_challenge',
|
||||||
|
'napcat_runtime_cleanup',
|
||||||
|
].filter((tableName) => !source.includes(tableName));
|
||||||
|
|
||||||
|
expect(missing).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps login truth in persistence rather than in-memory scan sessions', () => {
|
||||||
|
const loginSource = readSource(
|
||||||
|
'src/modules/qqbot/napcat/login/qqbot-napcat-login.service.ts',
|
||||||
|
);
|
||||||
|
|
||||||
|
const bannedMemorySignals = ['new Map', 'scanSessions'].filter((signal) =>
|
||||||
|
loginSource.includes(signal),
|
||||||
|
);
|
||||||
|
const missingPersistenceSignals = [
|
||||||
|
'loginSession',
|
||||||
|
'loginChallenge',
|
||||||
|
'runtimeCleanup',
|
||||||
|
].filter((signal) => !loginSource.includes(signal));
|
||||||
|
|
||||||
|
expect(bannedMemorySignals).toEqual([]);
|
||||||
|
expect(missingPersistenceSignals).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps captcha and new-device challenges recoverable and separate', () => {
|
||||||
|
const stateMachine = readSource(
|
||||||
|
'src/modules/qqbot/napcat/login/napcat-login-state-machine.ts',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(stateMachine).toEqual(expect.stringContaining("type: 'captcha'"));
|
||||||
|
expect(stateMachine).toEqual(expect.stringContaining("type: 'new-device'"));
|
||||||
|
expect(stateMachine).toEqual(expect.stringContaining('qr-pending'));
|
||||||
|
expect(stateMachine).toEqual(expect.stringContaining('scanned'));
|
||||||
|
expect(stateMachine).toEqual(expect.stringContaining('confirming'));
|
||||||
|
expect(stateMachine).toEqual(expect.stringContaining('cleanup-failed'));
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const repoRoot = join(__dirname, '../../../..');
|
||||||
|
|
||||||
|
const readSource = (relativePath: string) =>
|
||||||
|
readFileSync(join(repoRoot, relativePath), 'utf8');
|
||||||
|
|
||||||
|
describe('QQBot plugin platform lifecycle runtime contract', () => {
|
||||||
|
it('uses dedicated lifecycle use cases instead of direct status flips', () => {
|
||||||
|
const controller = readSource(
|
||||||
|
'src/modules/qqbot/plugin-platform/plugin-platform.controller.ts',
|
||||||
|
);
|
||||||
|
const service = readSource(
|
||||||
|
'src/modules/qqbot/plugin-platform/plugin-platform.service.ts',
|
||||||
|
);
|
||||||
|
|
||||||
|
const bannedDirectStatusFlips = [
|
||||||
|
controller.includes('setInstallationStatus')
|
||||||
|
? 'controller.setInstallationStatus'
|
||||||
|
: '',
|
||||||
|
service.includes('setInstallationStatus')
|
||||||
|
? 'service.setInstallationStatus'
|
||||||
|
: '',
|
||||||
|
].filter(Boolean);
|
||||||
|
const missingLifecycleMethods = [
|
||||||
|
'enableInstallation',
|
||||||
|
'disableInstallation',
|
||||||
|
'upgradeInstallation',
|
||||||
|
'uninstallInstallation',
|
||||||
|
].filter((methodName) => !service.includes(methodName));
|
||||||
|
|
||||||
|
expect(bannedDirectStatusFlips).toEqual([]);
|
||||||
|
expect(missingLifecycleMethods).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('activates workers and refreshes active registries during lifecycle transitions', () => {
|
||||||
|
const source = [
|
||||||
|
readSource('src/modules/qqbot/plugin-platform/plugin-platform.service.ts'),
|
||||||
|
readSource('src/modules/qqbot/plugin-platform/runtime/worker-runtime.ts'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const missingRuntimeSignals = [
|
||||||
|
'QqbotPluginWorkerRuntime',
|
||||||
|
'activate',
|
||||||
|
'deactivate',
|
||||||
|
'dispose',
|
||||||
|
'refreshActive',
|
||||||
|
'activeOperation',
|
||||||
|
'activeEvent',
|
||||||
|
].filter((signal) => !source.includes(signal));
|
||||||
|
|
||||||
|
expect(missingRuntimeSignals).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes operation executor and event dispatcher through the platform', () => {
|
||||||
|
const source = readSource(
|
||||||
|
'src/modules/qqbot/plugin-platform/plugin-platform.service.ts',
|
||||||
|
);
|
||||||
|
|
||||||
|
const missingExecutorSignals = [
|
||||||
|
'executeOperation',
|
||||||
|
'dispatchEvent',
|
||||||
|
'runtimeEventRepository',
|
||||||
|
'command log',
|
||||||
|
].filter((signal) => !source.includes(signal));
|
||||||
|
|
||||||
|
expect(missingExecutorSignals).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
import { existsSync, readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const repoRoot = join(__dirname, '../../../../..');
|
||||||
|
const newBangDreamRoot = join(
|
||||||
|
repoRoot,
|
||||||
|
'src/modules/qqbot/plugins/bangdream',
|
||||||
|
);
|
||||||
|
const baselineTest = join(
|
||||||
|
repoRoot,
|
||||||
|
'test/modules/qqbot/architecture/qqbot-current-operation-matrix.spec.ts',
|
||||||
|
);
|
||||||
|
|
||||||
|
const expectedOperationKeys = [
|
||||||
|
'bangdream.song.search',
|
||||||
|
'bangdream.song.chart',
|
||||||
|
'bangdream.song.random',
|
||||||
|
'bangdream.song.meta',
|
||||||
|
'bangdream.card.search',
|
||||||
|
'bangdream.card.illustration',
|
||||||
|
'bangdream.character.search',
|
||||||
|
'bangdream.event.search',
|
||||||
|
'bangdream.event.stage',
|
||||||
|
'bangdream.player.search',
|
||||||
|
'bangdream.gacha.search',
|
||||||
|
'bangdream.gacha.simulate',
|
||||||
|
'bangdream.cutoff.detail',
|
||||||
|
'bangdream.cutoff.all',
|
||||||
|
'bangdream.cutoff.recent',
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('BangDream rewritten plugin parity', () => {
|
||||||
|
it('uses the new platform package structure', () => {
|
||||||
|
const requiredPaths = [
|
||||||
|
'plugin.json',
|
||||||
|
'src/index.ts',
|
||||||
|
'src/operations/song-search',
|
||||||
|
'src/operations/song-chart',
|
||||||
|
'src/operations/song-random',
|
||||||
|
'src/operations/song-meta',
|
||||||
|
'src/operations/card-search',
|
||||||
|
'src/operations/card-illustration',
|
||||||
|
'src/operations/character-search',
|
||||||
|
'src/operations/event-search',
|
||||||
|
'src/operations/event-stage',
|
||||||
|
'src/operations/player-search',
|
||||||
|
'src/operations/gacha-search',
|
||||||
|
'src/operations/gacha-simulate',
|
||||||
|
'src/operations/cutoff-detail',
|
||||||
|
'src/operations/cutoff-all',
|
||||||
|
'src/operations/cutoff-recent',
|
||||||
|
'src/domain/song',
|
||||||
|
'src/domain/card',
|
||||||
|
'src/domain/character',
|
||||||
|
'src/domain/event',
|
||||||
|
'src/domain/gacha',
|
||||||
|
'src/domain/player',
|
||||||
|
'src/domain/cutoff',
|
||||||
|
'src/domain/catalog',
|
||||||
|
'src/application',
|
||||||
|
'src/infrastructure/integration',
|
||||||
|
'src/infrastructure/storage',
|
||||||
|
'src/config',
|
||||||
|
'src/assets',
|
||||||
|
'src/migrations',
|
||||||
|
'src/tests',
|
||||||
|
];
|
||||||
|
|
||||||
|
const missing = requiredPaths.filter(
|
||||||
|
(relativePath) => !existsSync(join(newBangDreamRoot, relativePath)),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(missing).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves the 15 approved operation keys in the new manifest', () => {
|
||||||
|
const manifestPath = join(newBangDreamRoot, 'plugin.json');
|
||||||
|
expect(existsSync(manifestPath)).toBe(true);
|
||||||
|
if (!existsSync(manifestPath)) return;
|
||||||
|
|
||||||
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as {
|
||||||
|
operations?: Array<{ key?: string }>;
|
||||||
|
pluginKey?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(manifest.pluginKey).toBe('bangdream');
|
||||||
|
expect(manifest.operations?.map((operation) => operation.key)).toEqual(
|
||||||
|
expectedOperationKeys,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps event stage split output as a hard parity gate', () => {
|
||||||
|
const baseline = readFileSync(baselineTest, 'utf8');
|
||||||
|
const operationSourcePath = join(
|
||||||
|
newBangDreamRoot,
|
||||||
|
'src/operations/event-stage',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(baseline).toContain('bangdream.event.stage');
|
||||||
|
expect(existsSync(operationSourcePath)).toBe(true);
|
||||||
|
if (!existsSync(operationSourcePath)) return;
|
||||||
|
|
||||||
|
const eventStageSource = readFileSync(
|
||||||
|
join(operationSourcePath, 'index.ts'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
expect(eventStageSource).toContain('imageCount');
|
||||||
|
expect(eventStageSource).toContain('5');
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user