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\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\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.
|
||||
- [ ] 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:
|
||||
|
||||
```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.
|
||||
@ -448,7 +448,7 @@ git -C D:\MyFiles\KT\Node\kt-template-online-api commit -m "feat: 重建QQBot插
|
||||
- [ ] Run:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
@ -471,7 +471,7 @@ powershell -ExecutionPolicy Bypass -File D:\MyFiles\KT\Node\kt-template-online-a
|
||||
- [ ] Commit:
|
||||
|
||||
```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 commit -m "refactor: 重写BangDream插件"
|
||||
```
|
||||
@ -739,7 +739,7 @@ Expected: no old runtime implementation references; only explicit legacy alias f
|
||||
- [ ] Run:
|
||||
|
||||
```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\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
|
||||
@ -771,7 +771,7 @@ git -C D:\MyFiles\KT commit -m "docs: 更新QQBot插件架构规则"
|
||||
```powershell
|
||||
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 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
|
||||
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