@zhin.js/cli 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/TEST_GENERATION.md +398 -0
- package/lib/commands/new.d.ts.map +1 -1
- package/lib/commands/new.js +534 -4
- package/lib/commands/new.js.map +1 -1
- package/package.json +22 -14
- package/src/commands/new.ts +552 -6
- package/tests/new-integration.test.ts +232 -0
- package/tests/utils.test.ts +77 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/cli",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Zhin
|
|
3
|
+
"version": "1.0.15",
|
|
4
|
+
"description": "Command-line interface for Zhin.js bot framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -14,18 +14,18 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"commander": "^
|
|
18
|
-
"chalk": "^5.
|
|
19
|
-
"ora": "^
|
|
20
|
-
"fs-extra": "^11.
|
|
21
|
-
"cross-spawn": "^7.0.
|
|
22
|
-
"dotenv": "^16.
|
|
23
|
-
"glob": "^
|
|
24
|
-
"inquirer": "^
|
|
25
|
-
"@zhin.js/logger": "0.1.
|
|
17
|
+
"commander": "^14.0.2",
|
|
18
|
+
"chalk": "^5.6.2",
|
|
19
|
+
"ora": "^8.2.0",
|
|
20
|
+
"fs-extra": "^11.3.2",
|
|
21
|
+
"cross-spawn": "^7.0.6",
|
|
22
|
+
"dotenv": "^16.4.7",
|
|
23
|
+
"glob": "^11.0.0",
|
|
24
|
+
"inquirer": "^12.10.0",
|
|
25
|
+
"@zhin.js/logger": "0.1.4"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"zhin.js": "1.0.
|
|
28
|
+
"zhin.js": "1.0.20"
|
|
29
29
|
},
|
|
30
30
|
"peerDependenciesMeta": {
|
|
31
31
|
"zhin.js": {
|
|
@@ -35,9 +35,10 @@
|
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/fs-extra": "^11.0.4",
|
|
37
37
|
"@types/cross-spawn": "^6.0.6",
|
|
38
|
-
"@types/node": "^
|
|
38
|
+
"@types/node": "^24.10.0",
|
|
39
39
|
"@types/inquirer": "^9.0.7",
|
|
40
|
-
"typescript": "^5.3
|
|
40
|
+
"typescript": "^5.9.3",
|
|
41
|
+
"zhin.js": "1.0.20"
|
|
41
42
|
},
|
|
42
43
|
"repository": {
|
|
43
44
|
"type": "git",
|
|
@@ -48,6 +49,13 @@
|
|
|
48
49
|
"access": "public",
|
|
49
50
|
"registry": "https://registry.npmjs.org"
|
|
50
51
|
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"zhin",
|
|
54
|
+
"bot",
|
|
55
|
+
"cli",
|
|
56
|
+
"command-line",
|
|
57
|
+
"tool"
|
|
58
|
+
],
|
|
51
59
|
"scripts": {
|
|
52
60
|
"build": "tsc",
|
|
53
61
|
"clean": "rm -rf lib"
|
package/src/commands/new.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { execSync } from 'node:child_process';
|
|
|
8
8
|
interface NewPluginOptions {
|
|
9
9
|
skipInstall?: boolean;
|
|
10
10
|
isOfficial?: boolean;
|
|
11
|
+
type?: 'normal' | 'service' | 'adapter';
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export const newCommand = new Command('new')
|
|
@@ -15,6 +16,7 @@ export const newCommand = new Command('new')
|
|
|
15
16
|
.argument('[plugin-name]', '插件名称(如: my-plugin)')
|
|
16
17
|
.option('--is-official', '是否为官方插件', false)
|
|
17
18
|
.option('--skip-install', '跳过依赖安装', false)
|
|
19
|
+
.option('--type <type>', '插件类型 (normal|service|adapter)', 'normal')
|
|
18
20
|
.action(async (pluginName: string, options: NewPluginOptions) => {
|
|
19
21
|
try {
|
|
20
22
|
let name = pluginName;
|
|
@@ -48,7 +50,25 @@ export const newCommand = new Command('new')
|
|
|
48
50
|
process.exit(1);
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
// 询问插件类型(如果未指定)
|
|
54
|
+
if (!options.type) {
|
|
55
|
+
const { type } = await inquirer.prompt([
|
|
56
|
+
{
|
|
57
|
+
type: 'list',
|
|
58
|
+
name: 'type',
|
|
59
|
+
message: '请选择插件类型:',
|
|
60
|
+
choices: [
|
|
61
|
+
{ name: '普通插件 (Normal)', value: 'normal' },
|
|
62
|
+
{ name: '服务 (Service)', value: 'service' },
|
|
63
|
+
{ name: '适配器 (Adapter)', value: 'adapter' }
|
|
64
|
+
],
|
|
65
|
+
default: 'normal'
|
|
66
|
+
}
|
|
67
|
+
]);
|
|
68
|
+
options.type = type;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
logger.info(`正在创建${options.type === 'service' ? '服务' : options.type === 'adapter' ? '适配器' : '插件'}包 ${name}...`);
|
|
52
72
|
|
|
53
73
|
// 创建插件包结构
|
|
54
74
|
await createPluginPackage(pluginDir, name, options);
|
|
@@ -70,7 +90,13 @@ export const newCommand = new Command('new')
|
|
|
70
90
|
logger.log(` pnpm publish`);
|
|
71
91
|
|
|
72
92
|
} catch (error) {
|
|
73
|
-
|
|
93
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
94
|
+
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
95
|
+
|
|
96
|
+
logger.error(`创建插件失败: ${errorMessage}`);
|
|
97
|
+
if (errorStack && process.env.DEBUG) {
|
|
98
|
+
logger.error(errorStack);
|
|
99
|
+
}
|
|
74
100
|
process.exit(1);
|
|
75
101
|
}
|
|
76
102
|
});
|
|
@@ -85,6 +111,7 @@ async function createPluginPackage(pluginDir: string, pluginName: string, option
|
|
|
85
111
|
await fs.ensureDir(path.join(pluginDir, 'client'));
|
|
86
112
|
await fs.ensureDir(path.join(pluginDir, 'lib'));
|
|
87
113
|
await fs.ensureDir(path.join(pluginDir, 'dist'));
|
|
114
|
+
await fs.ensureDir(path.join(pluginDir, 'tests'));
|
|
88
115
|
|
|
89
116
|
// 创建 package.json
|
|
90
117
|
const packageJson = {
|
|
@@ -117,6 +144,9 @@ async function createPluginPackage(pluginDir: string, pluginName: string, option
|
|
|
117
144
|
'build:client': 'zhin-console build',
|
|
118
145
|
dev: 'tsc --watch',
|
|
119
146
|
clean: 'rm -rf lib dist',
|
|
147
|
+
test: 'vitest run',
|
|
148
|
+
'test:watch': 'vitest',
|
|
149
|
+
'test:coverage': 'vitest run --coverage',
|
|
120
150
|
prepublishOnly: 'pnpm build'
|
|
121
151
|
},
|
|
122
152
|
keywords: [
|
|
@@ -141,7 +171,9 @@ async function createPluginPackage(pluginDir: string, pluginName: string, option
|
|
|
141
171
|
"@zhin.js/client":"workspace:*",
|
|
142
172
|
'lucide-react': 'latest',
|
|
143
173
|
'radix-ui': 'latest',
|
|
144
|
-
'@radix-ui/themes': 'latest'
|
|
174
|
+
'@radix-ui/themes': 'latest',
|
|
175
|
+
'vitest': 'latest',
|
|
176
|
+
'@vitest/coverage-v8': 'latest'
|
|
145
177
|
}
|
|
146
178
|
};
|
|
147
179
|
|
|
@@ -337,6 +369,9 @@ dist/
|
|
|
337
369
|
|
|
338
370
|
await fs.writeFile(path.join(pluginDir, '.gitignore'), gitignoreContent);
|
|
339
371
|
|
|
372
|
+
// 生成测试文件
|
|
373
|
+
await generateTestFiles(pluginDir, pluginName, capitalizedName, options);
|
|
374
|
+
|
|
340
375
|
// 安装依赖
|
|
341
376
|
if (!options.skipInstall) {
|
|
342
377
|
logger.info('正在安装依赖...');
|
|
@@ -352,6 +387,516 @@ dist/
|
|
|
352
387
|
}
|
|
353
388
|
}
|
|
354
389
|
|
|
390
|
+
/**
|
|
391
|
+
* 生成测试文件
|
|
392
|
+
*/
|
|
393
|
+
async function generateTestFiles(
|
|
394
|
+
pluginDir: string,
|
|
395
|
+
pluginName: string,
|
|
396
|
+
capitalizedName: string,
|
|
397
|
+
options: NewPluginOptions
|
|
398
|
+
) {
|
|
399
|
+
const testsDir = path.join(pluginDir, 'tests');
|
|
400
|
+
const pluginType = options.type || 'plugin';
|
|
401
|
+
|
|
402
|
+
// 根据插件类型生成不同的测试文件
|
|
403
|
+
if (pluginType === 'service') {
|
|
404
|
+
await generateServiceTest(testsDir, pluginName, capitalizedName);
|
|
405
|
+
} else if (pluginType === 'adapter') {
|
|
406
|
+
await generateAdapterTest(testsDir, pluginName, capitalizedName);
|
|
407
|
+
} else {
|
|
408
|
+
await generatePluginTest(testsDir, pluginName, capitalizedName);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
logger.success('✓ 测试文件已生成');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* 生成普通插件测试
|
|
416
|
+
*/
|
|
417
|
+
async function generatePluginTest(testsDir: string, pluginName: string, capitalizedName: string) {
|
|
418
|
+
const testContent = `import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
419
|
+
import { Plugin } from '@zhin.js/core'
|
|
420
|
+
|
|
421
|
+
describe('${capitalizedName} Plugin', () => {
|
|
422
|
+
let plugin: Plugin
|
|
423
|
+
let rootPlugin: Plugin
|
|
424
|
+
|
|
425
|
+
beforeEach(async () => {
|
|
426
|
+
rootPlugin = new Plugin('/test/root-plugin.ts')
|
|
427
|
+
plugin = new Plugin('/plugins/${pluginName}/src/index.ts', rootPlugin)
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
afterEach(async () => {
|
|
431
|
+
if (plugin && plugin.started) {
|
|
432
|
+
await plugin.stop()
|
|
433
|
+
}
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
describe('Plugin Instance', () => {
|
|
437
|
+
it('should create plugin instance', () => {
|
|
438
|
+
expect(plugin).toBeDefined()
|
|
439
|
+
expect(plugin).toBeInstanceOf(Plugin)
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('should have correct name', () => {
|
|
443
|
+
expect(plugin.name).toBe('${pluginName}')
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it('should have parent plugin', () => {
|
|
447
|
+
expect(plugin.parent).toBe(rootPlugin)
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it('should have logger', () => {
|
|
451
|
+
expect(plugin.logger).toBeDefined()
|
|
452
|
+
expect(typeof plugin.logger.info).toBe('function')
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
describe('Plugin Lifecycle', () => {
|
|
457
|
+
it('should start successfully', async () => {
|
|
458
|
+
await expect(plugin.start()).resolves.not.toThrow()
|
|
459
|
+
expect(plugin.started).toBe(true)
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
it('should stop successfully', async () => {
|
|
463
|
+
await plugin.start()
|
|
464
|
+
await expect(plugin.stop()).resolves.not.toThrow()
|
|
465
|
+
expect(plugin.started).toBe(false)
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
it('should emit mounted event on start', async () => {
|
|
469
|
+
const mountedSpy = vi.fn()
|
|
470
|
+
plugin.on('mounted', mountedSpy)
|
|
471
|
+
|
|
472
|
+
await plugin.start()
|
|
473
|
+
|
|
474
|
+
expect(mountedSpy).toHaveBeenCalled()
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
it('should emit dispose event on stop', async () => {
|
|
478
|
+
const disposeSpy = vi.fn()
|
|
479
|
+
plugin.on('dispose', disposeSpy)
|
|
480
|
+
|
|
481
|
+
await plugin.start()
|
|
482
|
+
await plugin.stop()
|
|
483
|
+
|
|
484
|
+
expect(disposeSpy).toHaveBeenCalled()
|
|
485
|
+
})
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
describe('Plugin Features', () => {
|
|
489
|
+
it('should register middleware', () => {
|
|
490
|
+
plugin.addMiddleware(async (event, next) => {
|
|
491
|
+
return next()
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
expect(plugin.middlewares.length).toBeGreaterThan(0)
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
it('should execute middleware', async () => {
|
|
498
|
+
const executionOrder: number[] = []
|
|
499
|
+
|
|
500
|
+
plugin.addMiddleware(async (event, next) => {
|
|
501
|
+
executionOrder.push(1)
|
|
502
|
+
await next()
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
plugin.addMiddleware(async (event, next) => {
|
|
506
|
+
executionOrder.push(2)
|
|
507
|
+
await next()
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
const mockEvent = {
|
|
511
|
+
$adapter: 'test',
|
|
512
|
+
$bot: 'test-bot',
|
|
513
|
+
$content: [],
|
|
514
|
+
$raw: 'test'
|
|
515
|
+
} as any
|
|
516
|
+
|
|
517
|
+
await plugin.middleware(mockEvent, async () => {})
|
|
518
|
+
|
|
519
|
+
expect(executionOrder).toEqual([1, 2])
|
|
520
|
+
})
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
describe('Custom Tests', () => {
|
|
524
|
+
// 在这里添加你的自定义测试
|
|
525
|
+
it('should pass custom test', () => {
|
|
526
|
+
expect(true).toBe(true)
|
|
527
|
+
})
|
|
528
|
+
})
|
|
529
|
+
})
|
|
530
|
+
`;
|
|
531
|
+
|
|
532
|
+
await fs.writeFile(path.join(testsDir, 'index.test.ts'), testContent);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* 生成服务测试
|
|
537
|
+
*/
|
|
538
|
+
async function generateServiceTest(testsDir: string, pluginName: string, capitalizedName: string) {
|
|
539
|
+
const testContent = `import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
540
|
+
import { Plugin } from '@zhin.js/core'
|
|
541
|
+
|
|
542
|
+
describe('${capitalizedName} Service', () => {
|
|
543
|
+
let plugin: Plugin
|
|
544
|
+
let service: any
|
|
545
|
+
|
|
546
|
+
beforeEach(async () => {
|
|
547
|
+
plugin = new Plugin('/test/service-plugin.ts')
|
|
548
|
+
// TODO: 初始化你的服务实例
|
|
549
|
+
// service = await createYourService(plugin)
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
afterEach(() => {
|
|
553
|
+
if (plugin && typeof (plugin as any).stop === 'function') {
|
|
554
|
+
(plugin as any).stop()
|
|
555
|
+
}
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
describe('Service Instance', () => {
|
|
559
|
+
it('should create service instance', () => {
|
|
560
|
+
// TODO: 取消注释并实现
|
|
561
|
+
// expect(service).toBeDefined()
|
|
562
|
+
// expect(service).not.toBeNull()
|
|
563
|
+
expect(true).toBe(true)
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
it('should have correct type', () => {
|
|
567
|
+
// TODO: 取消注释并实现
|
|
568
|
+
// expect(typeof service).toBe('object')
|
|
569
|
+
expect(true).toBe(true)
|
|
570
|
+
})
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
describe('Service Methods', () => {
|
|
574
|
+
it('should have required methods', () => {
|
|
575
|
+
// TODO: 添加你的服务方法测试
|
|
576
|
+
// expect(service).toHaveProperty('methodName')
|
|
577
|
+
// expect(typeof service.methodName).toBe('function')
|
|
578
|
+
expect(true).toBe(true)
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
it('should execute methods correctly', async () => {
|
|
582
|
+
// TODO: 测试方法执行
|
|
583
|
+
// const result = await service.methodName()
|
|
584
|
+
// expect(result).toBeDefined()
|
|
585
|
+
expect(true).toBe(true)
|
|
586
|
+
})
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
describe('Service Lifecycle', () => {
|
|
590
|
+
it('should handle initialization', async () => {
|
|
591
|
+
// TODO: 测试初始化
|
|
592
|
+
expect(true).toBe(true)
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
it('should handle cleanup on dispose', async () => {
|
|
596
|
+
// TODO: 测试清理逻辑
|
|
597
|
+
expect(true).toBe(true)
|
|
598
|
+
})
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
describe('Service Dependencies', () => {
|
|
602
|
+
it('should inject required dependencies', () => {
|
|
603
|
+
// TODO: 测试依赖注入
|
|
604
|
+
// plugin.provide({ name: 'dep', value: mockDep })
|
|
605
|
+
// const dep = plugin.inject('dep')
|
|
606
|
+
// expect(dep).toBeDefined()
|
|
607
|
+
expect(true).toBe(true)
|
|
608
|
+
})
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
describe('Custom Tests', () => {
|
|
612
|
+
// 在这里添加你的自定义测试
|
|
613
|
+
it('should pass custom test', () => {
|
|
614
|
+
expect(true).toBe(true)
|
|
615
|
+
})
|
|
616
|
+
})
|
|
617
|
+
})
|
|
618
|
+
`;
|
|
619
|
+
|
|
620
|
+
await fs.writeFile(path.join(testsDir, 'index.test.ts'), testContent);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* 生成适配器测试
|
|
625
|
+
*/
|
|
626
|
+
async function generateAdapterTest(testsDir: string, pluginName: string, capitalizedName: string) {
|
|
627
|
+
const testContent = `import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
628
|
+
import { Plugin, Adapter, Bot } from '@zhin.js/core'
|
|
629
|
+
import { EventEmitter } from 'events'
|
|
630
|
+
|
|
631
|
+
// TODO: 导入你的适配器和 Bot 类
|
|
632
|
+
// import { ${capitalizedName}Adapter, ${capitalizedName}Bot } from '../src/index'
|
|
633
|
+
|
|
634
|
+
// Mock Bot 类(用于测试)
|
|
635
|
+
class Mock${capitalizedName}Bot extends EventEmitter implements Bot {
|
|
636
|
+
adapter: string
|
|
637
|
+
unique: string
|
|
638
|
+
self_id: string
|
|
639
|
+
quote_self: boolean
|
|
640
|
+
forward_length: number
|
|
641
|
+
$connected: boolean = false
|
|
642
|
+
|
|
643
|
+
constructor(adapter: any, config: any) {
|
|
644
|
+
super()
|
|
645
|
+
this.adapter = '${pluginName}'
|
|
646
|
+
this.unique = config.name || 'mock-bot'
|
|
647
|
+
this.self_id = config.self_id || 'mock-bot-id'
|
|
648
|
+
this.quote_self = config.quote_self ?? true
|
|
649
|
+
this.forward_length = config.forward_length ?? 3
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async connect() {
|
|
653
|
+
this.$connected = true
|
|
654
|
+
this.emit('online')
|
|
655
|
+
return true
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async disconnect() {
|
|
659
|
+
this.$connected = false
|
|
660
|
+
this.emit('offline')
|
|
661
|
+
return true
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async sendMessage(channel_id: string, content: any) {
|
|
665
|
+
return 'mock-message-id'
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async recallMessage(message_id: string) {
|
|
669
|
+
return true
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Mock Adapter 类(用于测试)
|
|
674
|
+
class Mock${capitalizedName}Adapter extends Adapter<any, any> {
|
|
675
|
+
constructor(plugin: Plugin, name: string, config: any[]) {
|
|
676
|
+
super(plugin, name)
|
|
677
|
+
config.forEach(cfg => {
|
|
678
|
+
const bot = this.createBot(cfg)
|
|
679
|
+
this.bots.set(bot.unique, bot)
|
|
680
|
+
})
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
createBot(config: any): Bot {
|
|
684
|
+
return new Mock${capitalizedName}Bot(this, config)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
describe('${capitalizedName} Adapter', () => {
|
|
689
|
+
let plugin: Plugin
|
|
690
|
+
let adapter: Mock${capitalizedName}Adapter
|
|
691
|
+
|
|
692
|
+
beforeEach(() => {
|
|
693
|
+
plugin = new Plugin('/test/adapter-plugin.ts')
|
|
694
|
+
adapter = new Mock${capitalizedName}Adapter(plugin, '${pluginName}', [
|
|
695
|
+
{ name: 'test-bot', token: 'test-token' }
|
|
696
|
+
])
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
afterEach(async () => {
|
|
700
|
+
if (adapter) {
|
|
701
|
+
await adapter.stop()
|
|
702
|
+
}
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
describe('Adapter Instance', () => {
|
|
706
|
+
it('should create adapter instance', () => {
|
|
707
|
+
expect(adapter).toBeDefined()
|
|
708
|
+
expect(adapter).toBeInstanceOf(Adapter)
|
|
709
|
+
})
|
|
710
|
+
|
|
711
|
+
it('should have correct name', () => {
|
|
712
|
+
expect(adapter.name).toBe('${pluginName}')
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
it('should have plugin reference', () => {
|
|
716
|
+
expect(adapter.plugin).toBe(plugin)
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
it('should have logger', () => {
|
|
720
|
+
expect(adapter.logger).toBeDefined()
|
|
721
|
+
expect(typeof adapter.logger.info).toBe('function')
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
it('should initialize with bots', () => {
|
|
725
|
+
expect(adapter.bots).toBeInstanceOf(Map)
|
|
726
|
+
expect(adapter.bots.size).toBeGreaterThan(0)
|
|
727
|
+
})
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
describe('Bot Management', () => {
|
|
731
|
+
it('should create bots from config', () => {
|
|
732
|
+
expect(adapter.bots.size).toBe(1)
|
|
733
|
+
const bot = adapter.bots.values().next().value
|
|
734
|
+
expect(bot).toBeDefined()
|
|
735
|
+
expect(bot.adapter).toBe('${pluginName}')
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
it('should have createBot method', () => {
|
|
739
|
+
expect(typeof adapter.createBot).toBe('function')
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
it('should create bot with correct properties', () => {
|
|
743
|
+
const bot = adapter.bots.values().next().value
|
|
744
|
+
expect(bot.unique).toBeDefined()
|
|
745
|
+
expect(bot.self_id).toBeDefined()
|
|
746
|
+
})
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
describe('Adapter Lifecycle', () => {
|
|
750
|
+
it('should have start method', () => {
|
|
751
|
+
expect(typeof adapter.start).toBe('function')
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
it('should have stop method', () => {
|
|
755
|
+
expect(typeof adapter.stop).toBe('function')
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
it('should start successfully', async () => {
|
|
759
|
+
await expect(adapter.start()).resolves.not.toThrow()
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
it('should stop successfully', async () => {
|
|
763
|
+
await adapter.start()
|
|
764
|
+
await expect(adapter.stop()).resolves.not.toThrow()
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
it('should add to plugin adapters on start', async () => {
|
|
768
|
+
await adapter.start()
|
|
769
|
+
expect(plugin.adapters).toContain(adapter)
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
it('should remove from plugin adapters on stop', async () => {
|
|
773
|
+
await adapter.start()
|
|
774
|
+
await adapter.stop()
|
|
775
|
+
expect(plugin.adapters).not.toContain(adapter)
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
it('should clear bots on stop', async () => {
|
|
779
|
+
await adapter.start()
|
|
780
|
+
await adapter.stop()
|
|
781
|
+
expect(adapter.bots.size).toBe(0)
|
|
782
|
+
})
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
describe('Event Handling', () => {
|
|
786
|
+
it('should listen to call.recallMessage event', () => {
|
|
787
|
+
const listeners = adapter.listeners('call.recallMessage')
|
|
788
|
+
expect(listeners.length).toBeGreaterThan(0)
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
it('should listen to call.sendMessage event', () => {
|
|
792
|
+
const listeners = adapter.listeners('call.sendMessage')
|
|
793
|
+
expect(listeners.length).toBeGreaterThan(0)
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
it('should listen to message.receive event', () => {
|
|
797
|
+
const listeners = adapter.listeners('message.receive')
|
|
798
|
+
expect(listeners.length).toBeGreaterThan(0)
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
it('should remove all listeners on stop', async () => {
|
|
802
|
+
await adapter.start()
|
|
803
|
+
await adapter.stop()
|
|
804
|
+
|
|
805
|
+
expect(adapter.listenerCount('call.recallMessage')).toBe(0)
|
|
806
|
+
expect(adapter.listenerCount('call.sendMessage')).toBe(0)
|
|
807
|
+
expect(adapter.listenerCount('message.receive')).toBe(0)
|
|
808
|
+
})
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
describe('Message Sending', () => {
|
|
812
|
+
it('should handle sendMessage event', async () => {
|
|
813
|
+
const bot = adapter.bots.values().next().value
|
|
814
|
+
const sendSpy = vi.spyOn(bot, 'sendMessage')
|
|
815
|
+
|
|
816
|
+
await adapter.emit('call.sendMessage', bot.unique, 'test-channel', 'test message')
|
|
817
|
+
|
|
818
|
+
expect(sendSpy).toHaveBeenCalled()
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
it('should throw error when bot not found for sending', async () => {
|
|
822
|
+
await expect(
|
|
823
|
+
adapter.emit('call.sendMessage', 'non-existent-bot', 'channel', 'message')
|
|
824
|
+
).rejects.toThrow()
|
|
825
|
+
})
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
describe('Message Receiving', () => {
|
|
829
|
+
it('should process received messages through middleware', async () => {
|
|
830
|
+
const bot = adapter.bots.values().next().value
|
|
831
|
+
|
|
832
|
+
const middlewareSpy = vi.fn()
|
|
833
|
+
plugin.addMiddleware(async (event, next) => {
|
|
834
|
+
middlewareSpy(event)
|
|
835
|
+
return next()
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
const mockMessage = {
|
|
839
|
+
adapter: '${pluginName}',
|
|
840
|
+
bot: bot.unique,
|
|
841
|
+
channel: { id: 'test-channel', type: 'text' },
|
|
842
|
+
sender: { id: 'test-user' },
|
|
843
|
+
content: 'test message',
|
|
844
|
+
timestamp: Date.now()
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
await adapter.emit('message.receive', mockMessage)
|
|
848
|
+
|
|
849
|
+
expect(middlewareSpy).toHaveBeenCalled()
|
|
850
|
+
})
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
describe('Bot Methods', () => {
|
|
854
|
+
let bot: Mock${capitalizedName}Bot
|
|
855
|
+
|
|
856
|
+
beforeEach(() => {
|
|
857
|
+
bot = adapter.bots.values().next().value as Mock${capitalizedName}Bot
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
it('should have connect method', () => {
|
|
861
|
+
expect(typeof bot.connect).toBe('function')
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
it('should have disconnect method', () => {
|
|
865
|
+
expect(typeof bot.disconnect).toBe('function')
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
it('should have sendMessage method', () => {
|
|
869
|
+
expect(typeof bot.sendMessage).toBe('function')
|
|
870
|
+
})
|
|
871
|
+
|
|
872
|
+
it('should have recallMessage method', () => {
|
|
873
|
+
expect(typeof bot.recallMessage).toBe('function')
|
|
874
|
+
})
|
|
875
|
+
|
|
876
|
+
it('should connect successfully', async () => {
|
|
877
|
+
await bot.connect()
|
|
878
|
+
expect(bot.$connected).toBe(true)
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
it('should disconnect successfully', async () => {
|
|
882
|
+
await bot.connect()
|
|
883
|
+
await bot.disconnect()
|
|
884
|
+
expect(bot.$connected).toBe(false)
|
|
885
|
+
})
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
describe('Custom Tests', () => {
|
|
889
|
+
// 在这里添加你的自定义测试
|
|
890
|
+
it('should pass custom test', () => {
|
|
891
|
+
expect(true).toBe(true)
|
|
892
|
+
})
|
|
893
|
+
})
|
|
894
|
+
})
|
|
895
|
+
`;
|
|
896
|
+
|
|
897
|
+
await fs.writeFile(path.join(testsDir, 'index.test.ts'), testContent);
|
|
898
|
+
}
|
|
899
|
+
|
|
355
900
|
async function addPluginToApp(pluginName: string, isOfficial?: boolean) {
|
|
356
901
|
try {
|
|
357
902
|
const rootPackageJsonPath = path.resolve(process.cwd(), 'package.json');
|
|
@@ -389,7 +934,8 @@ async function addPluginToApp(pluginName: string, isOfficial?: boolean) {
|
|
|
389
934
|
} catch (error) {
|
|
390
935
|
logger.warn('⚠ 依赖更新失败,请手动执行 pnpm install');
|
|
391
936
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
937
|
+
} catch (error) {
|
|
938
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
939
|
+
logger.warn(`⚠ 添加到 package.json 失败: ${errorMessage}`);
|
|
940
|
+
}
|
|
395
941
|
}
|