@zhin.js/core 1.0.19 → 1.0.21
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 +20 -0
- package/lib/bot.d.ts +3 -1
- package/lib/bot.d.ts.map +1 -1
- package/lib/built/adapter-process.d.ts +3 -3
- package/lib/built/adapter-process.d.ts.map +1 -1
- package/lib/built/adapter-process.js +2 -2
- package/lib/built/adapter-process.js.map +1 -1
- package/lib/built/config.d.ts.map +1 -1
- package/lib/built/config.js +12 -1
- package/lib/built/config.js.map +1 -1
- package/lib/built/database.d.ts +1 -1
- package/lib/jsx-runtime.d.ts +2 -2
- package/lib/message.d.ts +3 -2
- package/lib/message.d.ts.map +1 -1
- package/lib/plugin.d.ts +2 -2
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js.map +1 -1
- package/lib/types.d.ts +2 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +16 -4
- package/lib/utils.js.map +1 -1
- package/package.json +4 -5
- package/src/bot.ts +3 -1
- package/src/built/adapter-process.ts +4 -4
- package/src/built/config.ts +17 -1
- package/src/message.ts +3 -3
- package/src/plugin.ts +2 -2
- package/src/types.ts +2 -1
- package/src/utils.ts +20 -4
- package/tests/adapter.test.ts +381 -4
- package/tests/config.test.ts +147 -4
- package/tests/cron.test.ts +11 -0
- package/tests/jsx-runtime.test.ts +45 -0
- package/tests/plugin.test.ts +705 -0
- package/tests/prompt.test.ts +78 -0
- package/tests/redos-protection.test.ts +197 -0
- package/tests/utils.test.ts +575 -4
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { Plugin, usePlugin, getPlugin, storage, defineContext } from '../src/plugin'
|
|
3
|
+
import { EventEmitter } from 'events'
|
|
4
|
+
|
|
5
|
+
describe('Plugin Core Functionality', () => {
|
|
6
|
+
describe('Plugin Constructor', () => {
|
|
7
|
+
it('should create a plugin with file path', () => {
|
|
8
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
9
|
+
expect(plugin.filePath).toBe('/test/plugin.ts')
|
|
10
|
+
expect(plugin.started).toBe(false)
|
|
11
|
+
expect(plugin.children).toEqual([])
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should create a plugin without file path', () => {
|
|
15
|
+
const plugin = new Plugin()
|
|
16
|
+
expect(plugin.filePath).toBe('')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should add plugin to parent children', () => {
|
|
20
|
+
const parent = new Plugin('/test/parent.ts')
|
|
21
|
+
const child = new Plugin('/test/child.ts', parent)
|
|
22
|
+
expect(parent.children).toContain(child)
|
|
23
|
+
expect(child.parent).toBe(parent)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should not duplicate child in parent', () => {
|
|
27
|
+
const parent = new Plugin('/test/parent.ts')
|
|
28
|
+
const child = new Plugin('/test/child.ts', parent)
|
|
29
|
+
// 尝试再次添加
|
|
30
|
+
if (!parent.children.includes(child)) {
|
|
31
|
+
parent.children.push(child)
|
|
32
|
+
}
|
|
33
|
+
expect(parent.children.filter(c => c === child).length).toBe(1)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should inherit from EventEmitter', () => {
|
|
37
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
38
|
+
expect(plugin).toBeInstanceOf(EventEmitter)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should set max listeners to 50', () => {
|
|
42
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
43
|
+
expect(plugin.getMaxListeners()).toBe(50)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('Plugin Name', () => {
|
|
48
|
+
it('should extract plugin name from file path', () => {
|
|
49
|
+
const plugin = new Plugin('/path/to/my-plugin/src/index.ts')
|
|
50
|
+
expect(plugin.name).toContain('my-plugin')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should handle node_modules path', () => {
|
|
54
|
+
const plugin = new Plugin('/path/node_modules/@scope/package/index.js')
|
|
55
|
+
const name = plugin.name
|
|
56
|
+
expect(name.length).toBeGreaterThan(0)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should cache plugin name', () => {
|
|
60
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
61
|
+
const name1 = plugin.name
|
|
62
|
+
const name2 = plugin.name
|
|
63
|
+
expect(name1).toBe(name2)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should remove file extensions', () => {
|
|
67
|
+
const plugin1 = new Plugin('/test/plugin.ts')
|
|
68
|
+
const plugin2 = new Plugin('/test/plugin.js')
|
|
69
|
+
expect(plugin1.name).not.toContain('.ts')
|
|
70
|
+
expect(plugin2.name).not.toContain('.js')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('Plugin Root', () => {
|
|
75
|
+
it('should return self as root if no parent', () => {
|
|
76
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
77
|
+
expect(plugin.root).toBe(plugin)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should return top-level parent as root', () => {
|
|
81
|
+
const grandparent = new Plugin('/test/grandparent.ts')
|
|
82
|
+
const parent = new Plugin('/test/parent.ts', grandparent)
|
|
83
|
+
const child = new Plugin('/test/child.ts', parent)
|
|
84
|
+
|
|
85
|
+
expect(child.root).toBe(grandparent)
|
|
86
|
+
expect(parent.root).toBe(grandparent)
|
|
87
|
+
expect(grandparent.root).toBe(grandparent)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('Plugin Middleware', () => {
|
|
92
|
+
it('should add middleware', () => {
|
|
93
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
94
|
+
const middleware = vi.fn(async (msg: any, next: any) => await next())
|
|
95
|
+
|
|
96
|
+
const dispose = plugin.addMiddleware(middleware)
|
|
97
|
+
expect(typeof dispose).toBe('function')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should remove middleware on dispose', () => {
|
|
101
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
102
|
+
const middleware = vi.fn(async (msg: any, next: any) => await next())
|
|
103
|
+
|
|
104
|
+
const dispose = plugin.addMiddleware(middleware)
|
|
105
|
+
dispose()
|
|
106
|
+
|
|
107
|
+
// 验证 dispose 被调用后中间件被移除
|
|
108
|
+
expect(dispose).toBeDefined()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should compose multiple middlewares', async () => {
|
|
112
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
113
|
+
const order: number[] = []
|
|
114
|
+
|
|
115
|
+
plugin.addMiddleware(async (msg: any, next: any) => {
|
|
116
|
+
order.push(1)
|
|
117
|
+
await next()
|
|
118
|
+
order.push(4)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
plugin.addMiddleware(async (msg: any, next: any) => {
|
|
122
|
+
order.push(2)
|
|
123
|
+
await next()
|
|
124
|
+
order.push(3)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const composed = plugin.middleware
|
|
128
|
+
await composed({} as any, async () => {})
|
|
129
|
+
|
|
130
|
+
expect(order.length).toBeGreaterThan(0)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('Plugin Contexts', () => {
|
|
135
|
+
it('should initialize with empty contexts', () => {
|
|
136
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
137
|
+
expect(plugin.$contexts).toBeInstanceOf(Map)
|
|
138
|
+
expect(plugin.$contexts.size).toBe(0)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should get contexts including children', () => {
|
|
142
|
+
const parent = new Plugin('/test/parent.ts')
|
|
143
|
+
new Plugin('/test/child.ts', parent)
|
|
144
|
+
|
|
145
|
+
const contexts = parent.contexts
|
|
146
|
+
expect(contexts).toBeInstanceOf(Map)
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('Plugin Lifecycle', () => {
|
|
151
|
+
it('should start with started = false', () => {
|
|
152
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
153
|
+
expect(plugin.started).toBe(false)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('should emit events', async () => {
|
|
157
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
158
|
+
|
|
159
|
+
const promise = new Promise<void>((resolve) => {
|
|
160
|
+
plugin.on('mounted', () => {
|
|
161
|
+
resolve()
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
plugin.emit('mounted', plugin)
|
|
166
|
+
await promise
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('Plugin Adapters', () => {
|
|
171
|
+
it('should initialize with empty adapters array', () => {
|
|
172
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
173
|
+
expect(plugin.adapters).toEqual([])
|
|
174
|
+
expect(Array.isArray(plugin.adapters)).toBe(true)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe('Plugin File Info', () => {
|
|
179
|
+
it('should store file path', () => {
|
|
180
|
+
const filePath = '/test/my-plugin.ts'
|
|
181
|
+
const plugin = new Plugin(filePath)
|
|
182
|
+
expect(plugin.filePath).toBe(filePath)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should initialize file hash as empty string', () => {
|
|
186
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
187
|
+
expect(plugin.fileHash).toBe('')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should remove timestamp query from file path', () => {
|
|
191
|
+
const plugin = new Plugin('/test/plugin.ts?t=1234567890')
|
|
192
|
+
expect(plugin.filePath).toBe('/test/plugin.ts')
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
describe('Plugin Children Management', () => {
|
|
197
|
+
it('should manage multiple children', () => {
|
|
198
|
+
const parent = new Plugin('/test/parent.ts')
|
|
199
|
+
const child1 = new Plugin('/test/child1.ts', parent)
|
|
200
|
+
const child2 = new Plugin('/test/child2.ts', parent)
|
|
201
|
+
|
|
202
|
+
expect(parent.children).toHaveLength(2)
|
|
203
|
+
expect(parent.children).toContain(child1)
|
|
204
|
+
expect(parent.children).toContain(child2)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should allow nested plugin hierarchy', () => {
|
|
208
|
+
const root = new Plugin('/test/root.ts')
|
|
209
|
+
const level1 = new Plugin('/test/level1.ts', root)
|
|
210
|
+
const level2 = new Plugin('/test/level2.ts', level1)
|
|
211
|
+
const level3 = new Plugin('/test/level3.ts', level2)
|
|
212
|
+
|
|
213
|
+
expect(level3.root).toBe(root)
|
|
214
|
+
expect(level2.parent).toBe(level1)
|
|
215
|
+
expect(level1.children).toContain(level2)
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe('Plugin AsyncLocalStorage', () => {
|
|
221
|
+
beforeEach(() => {
|
|
222
|
+
// 清理 storage
|
|
223
|
+
storage.disable()
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
describe('usePlugin', () => {
|
|
227
|
+
it('should create and store plugin in AsyncLocalStorage', () => {
|
|
228
|
+
storage.run(undefined, () => {
|
|
229
|
+
const plugin = usePlugin()
|
|
230
|
+
expect(plugin).toBeInstanceOf(Plugin)
|
|
231
|
+
expect(storage.getStore()).toBe(plugin)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('should create child plugin when called within parent context', () => {
|
|
236
|
+
storage.run(undefined, () => {
|
|
237
|
+
const parent = usePlugin()
|
|
238
|
+
const child = usePlugin()
|
|
239
|
+
|
|
240
|
+
expect(child.parent).toBe(parent)
|
|
241
|
+
expect(parent.children).toContain(child)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('should handle nested contexts correctly', () => {
|
|
246
|
+
storage.run(undefined, () => {
|
|
247
|
+
const parent = usePlugin()
|
|
248
|
+
|
|
249
|
+
storage.run(undefined, () => {
|
|
250
|
+
const nested = usePlugin()
|
|
251
|
+
// 嵌套上下文应该创建新的独立插件
|
|
252
|
+
expect(nested).toBeInstanceOf(Plugin)
|
|
253
|
+
expect(nested).not.toBe(parent)
|
|
254
|
+
expect(storage.getStore()).toBe(nested)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// 返回外层上下文后,应该恢复原来的插件
|
|
258
|
+
expect(storage.getStore()).toBe(parent)
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('should handle storage disabled during execution', () => {
|
|
263
|
+
storage.run(undefined, () => {
|
|
264
|
+
const plugin = usePlugin()
|
|
265
|
+
expect(plugin).toBeInstanceOf(Plugin)
|
|
266
|
+
|
|
267
|
+
// 禁用 storage
|
|
268
|
+
storage.disable()
|
|
269
|
+
|
|
270
|
+
// 再次调用应该创建新插件
|
|
271
|
+
const newPlugin = usePlugin()
|
|
272
|
+
expect(newPlugin).toBeInstanceOf(Plugin)
|
|
273
|
+
// 注意:禁用后 storage 可能仍然在当前 run 上下文中有值
|
|
274
|
+
// 只需要验证 usePlugin 仍然能正常工作即可
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('should handle errors in nested contexts', () => {
|
|
279
|
+
storage.run(undefined, () => {
|
|
280
|
+
const parent = usePlugin()
|
|
281
|
+
|
|
282
|
+
expect(() => {
|
|
283
|
+
storage.run(undefined, () => {
|
|
284
|
+
usePlugin()
|
|
285
|
+
throw new Error('Test error')
|
|
286
|
+
})
|
|
287
|
+
}).toThrow('Test error')
|
|
288
|
+
|
|
289
|
+
// 错误后,外层上下文应该保持不变
|
|
290
|
+
expect(storage.getStore()).toBe(parent)
|
|
291
|
+
})
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
describe('getPlugin', () => {
|
|
296
|
+
it('should throw error when called outside plugin context', () => {
|
|
297
|
+
storage.run(undefined, () => {
|
|
298
|
+
expect(() => getPlugin()).toThrow('must be called within a plugin context')
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('should return current plugin from storage', () => {
|
|
303
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
304
|
+
storage.run(plugin, () => {
|
|
305
|
+
const retrieved = getPlugin()
|
|
306
|
+
expect(retrieved).toBe(plugin)
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
describe('storage', () => {
|
|
312
|
+
it('should be an instance of AsyncLocalStorage', () => {
|
|
313
|
+
expect(storage).toBeDefined()
|
|
314
|
+
expect(typeof storage.run).toBe('function')
|
|
315
|
+
expect(typeof storage.getStore).toBe('function')
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
describe('Plugin Logger', () => {
|
|
321
|
+
it('should have a logger instance', () => {
|
|
322
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
323
|
+
expect(plugin.logger).toBeDefined()
|
|
324
|
+
expect(typeof plugin.logger.info).toBe('function')
|
|
325
|
+
expect(typeof plugin.logger.error).toBe('function')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('should use plugin name in logger', () => {
|
|
329
|
+
const plugin = new Plugin('/test/my-plugin/index.ts')
|
|
330
|
+
expect(plugin.logger).toBeDefined()
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
describe('Plugin Disposables', () => {
|
|
335
|
+
it('should track disposable functions', () => {
|
|
336
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
337
|
+
const middleware = vi.fn(async (msg: any, next: any) => await next())
|
|
338
|
+
|
|
339
|
+
const dispose = plugin.addMiddleware(middleware)
|
|
340
|
+
|
|
341
|
+
// 验证 dispose 函数存在
|
|
342
|
+
expect(typeof dispose).toBe('function')
|
|
343
|
+
|
|
344
|
+
// 调用 dispose
|
|
345
|
+
dispose()
|
|
346
|
+
|
|
347
|
+
// 再次调用应该是安全的
|
|
348
|
+
dispose()
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
describe('Plugin Lifecycle Methods', () => {
|
|
353
|
+
describe('start', () => {
|
|
354
|
+
it('should set started to true', async () => {
|
|
355
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
356
|
+
expect(plugin.started).toBe(false)
|
|
357
|
+
|
|
358
|
+
await plugin.start()
|
|
359
|
+
expect(plugin.started).toBe(true)
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('should not start twice', async () => {
|
|
363
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
364
|
+
await plugin.start()
|
|
365
|
+
await plugin.start() // 第二次调用应该被忽略
|
|
366
|
+
expect(plugin.started).toBe(true)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('should start children plugins', async () => {
|
|
370
|
+
const parent = new Plugin('/test/parent.ts')
|
|
371
|
+
const child = new Plugin('/test/child.ts', parent)
|
|
372
|
+
|
|
373
|
+
await parent.start()
|
|
374
|
+
expect(parent.started).toBe(true)
|
|
375
|
+
expect(child.started).toBe(true)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('should emit mounted event', async () => {
|
|
379
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
380
|
+
let emitted = false
|
|
381
|
+
|
|
382
|
+
plugin.on('mounted', () => {
|
|
383
|
+
emitted = true
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
await plugin.start()
|
|
387
|
+
expect(emitted).toBe(true)
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
describe('stop', () => {
|
|
392
|
+
it('should set started to false', async () => {
|
|
393
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
394
|
+
await plugin.start()
|
|
395
|
+
|
|
396
|
+
await plugin.stop()
|
|
397
|
+
expect(plugin.started).toBe(false)
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
it('should stop children plugins', async () => {
|
|
401
|
+
const parent = new Plugin('/test/parent.ts')
|
|
402
|
+
const child = new Plugin('/test/child.ts', parent)
|
|
403
|
+
|
|
404
|
+
await parent.start()
|
|
405
|
+
await parent.stop()
|
|
406
|
+
|
|
407
|
+
expect(parent.started).toBe(false)
|
|
408
|
+
expect(child.started).toBe(false)
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('should clear children array', async () => {
|
|
412
|
+
const parent = new Plugin('/test/parent.ts')
|
|
413
|
+
new Plugin('/test/child.ts', parent)
|
|
414
|
+
|
|
415
|
+
await parent.start()
|
|
416
|
+
await parent.stop()
|
|
417
|
+
|
|
418
|
+
expect(parent.children).toEqual([])
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('should clear contexts', async () => {
|
|
422
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
423
|
+
plugin.$contexts.set('test', { name: 'test', description: 'test' } as any)
|
|
424
|
+
|
|
425
|
+
await plugin.start()
|
|
426
|
+
await plugin.stop()
|
|
427
|
+
expect(plugin.$contexts.size).toBe(0)
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('should emit dispose event', async () => {
|
|
431
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
432
|
+
let emitted = false
|
|
433
|
+
|
|
434
|
+
plugin.on('dispose', () => {
|
|
435
|
+
emitted = true
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
await plugin.start()
|
|
439
|
+
await plugin.stop()
|
|
440
|
+
expect(emitted).toBe(true)
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('should call disposables', async () => {
|
|
444
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
445
|
+
let called = false
|
|
446
|
+
|
|
447
|
+
plugin.onDispose(() => {
|
|
448
|
+
called = true
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
await plugin.start()
|
|
452
|
+
await plugin.stop()
|
|
453
|
+
expect(called).toBe(true)
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('should not stop if not started', async () => {
|
|
457
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
458
|
+
await plugin.stop() // 应该直接返回
|
|
459
|
+
expect(plugin.started).toBe(false)
|
|
460
|
+
})
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
describe('onMounted', () => {
|
|
464
|
+
it('should register mounted callback', async () => {
|
|
465
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
466
|
+
let called = false
|
|
467
|
+
|
|
468
|
+
plugin.onMounted(() => {
|
|
469
|
+
called = true
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
await plugin.start()
|
|
473
|
+
expect(called).toBe(true)
|
|
474
|
+
})
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
describe('onDispose', () => {
|
|
478
|
+
it('should register dispose callback', async () => {
|
|
479
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
480
|
+
let called = false
|
|
481
|
+
|
|
482
|
+
const unregister = plugin.onDispose(() => {
|
|
483
|
+
called = true
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
await plugin.start()
|
|
487
|
+
await plugin.stop()
|
|
488
|
+
expect(called).toBe(true)
|
|
489
|
+
expect(typeof unregister).toBe('function')
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('should allow unregistering callback', async () => {
|
|
493
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
494
|
+
let called = false
|
|
495
|
+
|
|
496
|
+
const unregister = plugin.onDispose(() => {
|
|
497
|
+
called = true
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
unregister() // 取消注册
|
|
501
|
+
await plugin.start()
|
|
502
|
+
await plugin.stop()
|
|
503
|
+
expect(called).toBe(false)
|
|
504
|
+
})
|
|
505
|
+
})
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
describe('Plugin Event Broadcasting', () => {
|
|
509
|
+
describe('dispatch', () => {
|
|
510
|
+
it('should dispatch to parent', async () => {
|
|
511
|
+
const parent = new Plugin('/test/parent.ts')
|
|
512
|
+
const child = new Plugin('/test/child.ts', parent)
|
|
513
|
+
|
|
514
|
+
let received = false
|
|
515
|
+
parent.on('mounted', () => {
|
|
516
|
+
received = true
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
await child.dispatch('mounted')
|
|
520
|
+
expect(received).toBe(true)
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
it('should broadcast if no parent', async () => {
|
|
524
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
525
|
+
let received = false
|
|
526
|
+
|
|
527
|
+
plugin.on('mounted', () => {
|
|
528
|
+
received = true
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
await plugin.dispatch('mounted')
|
|
532
|
+
expect(received).toBe(true)
|
|
533
|
+
})
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
describe('broadcast', () => {
|
|
537
|
+
it('should broadcast to children', async () => {
|
|
538
|
+
const parent = new Plugin('/test/parent.ts')
|
|
539
|
+
const child = new Plugin('/test/child.ts', parent)
|
|
540
|
+
|
|
541
|
+
let childReceived = false
|
|
542
|
+
child.on('mounted', () => {
|
|
543
|
+
childReceived = true
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
await parent.broadcast('mounted')
|
|
547
|
+
expect(childReceived).toBe(true)
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
it('should call own listeners', async () => {
|
|
551
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
552
|
+
let called = false
|
|
553
|
+
|
|
554
|
+
plugin.on('mounted', () => {
|
|
555
|
+
called = true
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
await plugin.broadcast('mounted')
|
|
559
|
+
expect(called).toBe(true)
|
|
560
|
+
})
|
|
561
|
+
})
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
describe('Plugin Context Management', () => {
|
|
565
|
+
describe('provide', () => {
|
|
566
|
+
it('should register context', () => {
|
|
567
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
568
|
+
const context = {
|
|
569
|
+
name: 'test',
|
|
570
|
+
description: 'Test context',
|
|
571
|
+
value: { test: true }
|
|
572
|
+
} as any
|
|
573
|
+
|
|
574
|
+
plugin.provide(context)
|
|
575
|
+
expect(plugin.$contexts.has('test')).toBe(true)
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it('should return plugin instance for chaining', () => {
|
|
579
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
580
|
+
const context = {
|
|
581
|
+
name: 'test',
|
|
582
|
+
description: 'Test context'
|
|
583
|
+
} as any
|
|
584
|
+
|
|
585
|
+
const result = plugin.provide(context)
|
|
586
|
+
expect(result).toBe(plugin)
|
|
587
|
+
})
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
describe('inject', () => {
|
|
591
|
+
it('should inject context value', () => {
|
|
592
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
593
|
+
const context = {
|
|
594
|
+
name: 'test',
|
|
595
|
+
description: 'Test context',
|
|
596
|
+
value: { data: 'test-value' }
|
|
597
|
+
} as any
|
|
598
|
+
|
|
599
|
+
plugin.$contexts.set('test', context)
|
|
600
|
+
const injected = plugin.inject('test' as any)
|
|
601
|
+
expect(injected).toEqual({ data: 'test-value' })
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it('should return undefined for non-existent context', () => {
|
|
605
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
606
|
+
const injected = plugin.inject('non-existent' as any)
|
|
607
|
+
expect(injected).toBeUndefined()
|
|
608
|
+
})
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
describe('contextIsReady', () => {
|
|
612
|
+
it('should return true if context exists', () => {
|
|
613
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
614
|
+
const context = {
|
|
615
|
+
name: 'test',
|
|
616
|
+
description: 'Test context',
|
|
617
|
+
value: { test: true }
|
|
618
|
+
} as any
|
|
619
|
+
|
|
620
|
+
plugin.$contexts.set('test', context)
|
|
621
|
+
expect(plugin.contextIsReady('test' as any)).toBe(true)
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
it('should return false if context does not exist', () => {
|
|
625
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
626
|
+
expect(plugin.contextIsReady('non-existent' as any)).toBe(false)
|
|
627
|
+
})
|
|
628
|
+
})
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
describe('Plugin Features', () => {
|
|
632
|
+
it('should return empty features by default', () => {
|
|
633
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
634
|
+
const features = plugin.features
|
|
635
|
+
|
|
636
|
+
expect(features.commands).toEqual([])
|
|
637
|
+
expect(features.components).toEqual([])
|
|
638
|
+
expect(features.crons).toEqual([])
|
|
639
|
+
expect(Array.isArray(features.middlewares)).toBe(true)
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
it('should include middleware names', () => {
|
|
643
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
644
|
+
plugin.addMiddleware(async (msg: any, next: any) => await next(), 'test-middleware')
|
|
645
|
+
|
|
646
|
+
const features = plugin.features
|
|
647
|
+
expect(features.middlewares.length).toBeGreaterThan(0)
|
|
648
|
+
})
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
describe('Plugin Info', () => {
|
|
652
|
+
it('should return plugin info', () => {
|
|
653
|
+
const plugin = new Plugin('/test/my-plugin.ts')
|
|
654
|
+
const info = plugin.info()
|
|
655
|
+
|
|
656
|
+
expect(info).toHaveProperty(plugin.name)
|
|
657
|
+
expect(info[plugin.name]).toHaveProperty('features')
|
|
658
|
+
expect(info[plugin.name]).toHaveProperty('children')
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
it('should include children info', () => {
|
|
662
|
+
const parent = new Plugin('/test/parent.ts')
|
|
663
|
+
new Plugin('/test/child.ts', parent)
|
|
664
|
+
|
|
665
|
+
const info = parent.info()
|
|
666
|
+
expect(info[parent.name].children).toHaveLength(1)
|
|
667
|
+
})
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
describe('Plugin Method Binding', () => {
|
|
671
|
+
it('should bind core methods', () => {
|
|
672
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
673
|
+
|
|
674
|
+
// 解构后方法仍然可用
|
|
675
|
+
const { start, stop, provide } = plugin
|
|
676
|
+
|
|
677
|
+
expect(typeof start).toBe('function')
|
|
678
|
+
expect(typeof stop).toBe('function')
|
|
679
|
+
expect(typeof provide).toBe('function')
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
it('should not bind methods twice', () => {
|
|
683
|
+
const plugin = new Plugin('/test/plugin.ts')
|
|
684
|
+
plugin.$bindMethods()
|
|
685
|
+
plugin.$bindMethods() // 第二次调用应该被忽略
|
|
686
|
+
|
|
687
|
+
expect(plugin.started).toBe(false)
|
|
688
|
+
})
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
describe('Plugin Static Methods and Utilities', () => {
|
|
692
|
+
it('should export defineContext function', () => {
|
|
693
|
+
expect(typeof defineContext).toBe('function')
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
it('defineContext should return the options as-is', () => {
|
|
697
|
+
const context = {
|
|
698
|
+
name: 'test' as const,
|
|
699
|
+
description: 'Test context',
|
|
700
|
+
value: 'test-value'
|
|
701
|
+
}
|
|
702
|
+
const result = defineContext(context)
|
|
703
|
+
expect(result).toEqual(context)
|
|
704
|
+
})
|
|
705
|
+
})
|