test: 增加QQBot强收敛门禁

This commit is contained in:
sunlei 2026-06-15 15:17:48 +08:00
parent 80691af285
commit 8ada857f57
7 changed files with 590 additions and 6 deletions

View File

@ -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
```

View 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([]);
});
});

View File

@ -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([]);
});
});

View 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([]);
});
});

View File

@ -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'));
});
});

View File

@ -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([]);
});
});

View File

@ -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');
});
});