@zhin.js/core 1.0.0 → 1.0.1
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 +9 -0
- package/LICENSE +21 -0
- package/README.md +295 -74
- package/lib/adapter.d.ts +39 -0
- package/lib/adapter.d.ts.map +1 -0
- package/{dist → lib}/adapter.js +20 -2
- package/lib/adapter.js.map +1 -0
- package/lib/app.d.ts +115 -0
- package/lib/app.d.ts.map +1 -0
- package/{dist → lib}/app.js +148 -78
- package/lib/app.js.map +1 -0
- package/lib/bot.d.ts +31 -0
- package/lib/bot.d.ts.map +1 -0
- package/lib/command.d.ts +32 -0
- package/lib/command.d.ts.map +1 -0
- package/lib/command.js +46 -0
- package/lib/command.js.map +1 -0
- package/lib/component.d.ts +107 -0
- package/lib/component.d.ts.map +1 -0
- package/lib/component.js +273 -0
- package/lib/component.js.map +1 -0
- package/{dist → lib}/config.d.ts.map +1 -1
- package/{dist → lib}/config.js +6 -9
- package/lib/config.js.map +1 -0
- package/lib/cron.d.ts +81 -0
- package/lib/cron.d.ts.map +1 -0
- package/lib/cron.js +159 -0
- package/lib/cron.js.map +1 -0
- package/lib/errors.d.ts +165 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +306 -0
- package/lib/errors.js.map +1 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +17 -0
- package/lib/index.js.map +1 -0
- package/lib/message.d.ts +44 -0
- package/lib/message.d.ts.map +1 -0
- package/lib/message.js +11 -0
- package/lib/message.js.map +1 -0
- package/lib/plugin.d.ts +50 -0
- package/lib/plugin.d.ts.map +1 -0
- package/lib/plugin.js +170 -0
- package/lib/plugin.js.map +1 -0
- package/lib/prompt.d.ts +116 -0
- package/lib/prompt.d.ts.map +1 -0
- package/lib/prompt.js +240 -0
- package/lib/prompt.js.map +1 -0
- package/lib/schema.d.ts +83 -0
- package/lib/schema.d.ts.map +1 -0
- package/lib/schema.js +245 -0
- package/lib/schema.js.map +1 -0
- package/{dist → lib}/types-generator.d.ts.map +1 -1
- package/{dist → lib}/types-generator.js +6 -3
- package/lib/types-generator.js.map +1 -0
- package/lib/types.d.ts +119 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/utils.d.ts +52 -0
- package/lib/utils.d.ts.map +1 -0
- package/lib/utils.js +338 -0
- package/lib/utils.js.map +1 -0
- package/package.json +15 -9
- package/src/adapter.ts +25 -9
- package/src/app.ts +363 -258
- package/src/bot.ts +29 -8
- package/src/command.ts +50 -0
- package/src/component.ts +318 -0
- package/src/config.ts +9 -12
- package/src/cron.ts +176 -0
- package/src/errors.ts +365 -0
- package/src/index.ts +16 -13
- package/src/message.ts +44 -0
- package/src/plugin.ts +148 -66
- package/src/prompt.ts +290 -0
- package/src/schema.ts +273 -0
- package/src/types-generator.ts +7 -3
- package/src/types.ts +77 -30
- package/src/utils.ts +312 -0
- package/tests/adapter.test.ts +36 -22
- package/tests/app.test.ts +30 -0
- package/tests/command.test.ts +545 -0
- package/tests/component.test.ts +656 -0
- package/tests/config.test.ts +1 -1
- package/tests/errors.test.ts +311 -0
- package/tests/message.test.ts +402 -0
- package/tests/plugin.test.ts +275 -143
- package/tests/utils.test.ts +80 -0
- package/tsconfig.json +3 -4
- package/dist/adapter.d.ts +0 -22
- package/dist/adapter.d.ts.map +0 -1
- package/dist/adapter.js.map +0 -1
- package/dist/app.d.ts +0 -69
- package/dist/app.d.ts.map +0 -1
- package/dist/app.js.map +0 -1
- package/dist/bot.d.ts +0 -9
- package/dist/bot.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -12
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts +0 -3
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -3
- package/dist/logger.js.map +0 -1
- package/dist/plugin.d.ts +0 -41
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -95
- package/dist/plugin.js.map +0 -1
- package/dist/types-generator.js.map +0 -1
- package/dist/types.d.ts +0 -69
- package/dist/types.d.ts.map +0 -1
- package/src/logger.ts +0 -3
- package/tests/logger.test.ts +0 -170
- package/tsconfig.tsbuildinfo +0 -1
- /package/{dist → lib}/bot.js +0 -0
- /package/{dist → lib}/bot.js.map +0 -0
- /package/{dist → lib}/config.d.ts +0 -0
- /package/{dist → lib}/types-generator.d.ts +0 -0
- /package/{dist → lib}/types.js +0 -0
- /package/{dist → lib}/types.js.map +0 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
ZhinError,
|
|
4
|
+
ConfigError,
|
|
5
|
+
PluginError,
|
|
6
|
+
AdapterError,
|
|
7
|
+
ConnectionError,
|
|
8
|
+
MessageError,
|
|
9
|
+
ContextError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
PermissionError,
|
|
12
|
+
TimeoutError,
|
|
13
|
+
ErrorManager,
|
|
14
|
+
RetryManager,
|
|
15
|
+
CircuitBreaker,
|
|
16
|
+
errorManager
|
|
17
|
+
} from '../src/errors.js'
|
|
18
|
+
|
|
19
|
+
describe('错误处理系统', () => {
|
|
20
|
+
describe('ZhinError基础类', () => {
|
|
21
|
+
it('应该正确创建基础错误', () => {
|
|
22
|
+
const error = new ZhinError('测试错误', 'TEST_ERROR', { key: 'value' })
|
|
23
|
+
|
|
24
|
+
expect(error).toBeInstanceOf(Error)
|
|
25
|
+
expect(error.name).toBe('ZhinError')
|
|
26
|
+
expect(error.message).toBe('测试错误')
|
|
27
|
+
expect(error.code).toBe('TEST_ERROR')
|
|
28
|
+
expect(error.context).toEqual({ key: 'value' })
|
|
29
|
+
expect(error.timestamp).toBeInstanceOf(Date)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('应该正确转换为JSON', () => {
|
|
33
|
+
const error = new ZhinError('测试错误', 'TEST_ERROR', { key: 'value' })
|
|
34
|
+
const json = error.toJSON()
|
|
35
|
+
|
|
36
|
+
expect(json).toEqual({
|
|
37
|
+
name: 'ZhinError',
|
|
38
|
+
message: '测试错误',
|
|
39
|
+
code: 'TEST_ERROR',
|
|
40
|
+
timestamp: expect.any(String),
|
|
41
|
+
context: { key: 'value' },
|
|
42
|
+
stack: expect.any(String)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('应该正确转换为用户友好格式', () => {
|
|
47
|
+
const error = new ZhinError('测试错误', 'TEST_ERROR')
|
|
48
|
+
expect(error.toUserString()).toBe('[TEST_ERROR] 测试错误')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('特定错误类型', () => {
|
|
53
|
+
it('ConfigError应该包含正确信息', () => {
|
|
54
|
+
const error = new ConfigError('配置无效', { file: 'config.json' })
|
|
55
|
+
|
|
56
|
+
expect(error.code).toBe('CONFIG_ERROR')
|
|
57
|
+
expect(error.context).toEqual({ file: 'config.json' })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('PluginError应该包含插件信息', () => {
|
|
61
|
+
const error = new PluginError('插件加载失败', 'test-plugin')
|
|
62
|
+
|
|
63
|
+
expect(error.code).toBe('PLUGIN_ERROR')
|
|
64
|
+
expect(error.pluginName).toBe('test-plugin')
|
|
65
|
+
expect(error.context?.pluginName).toBe('test-plugin')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('AdapterError应该包含适配器信息', () => {
|
|
69
|
+
const error = new AdapterError('适配器连接失败', 'icqq', 'bot-123')
|
|
70
|
+
|
|
71
|
+
expect(error.code).toBe('ADAPTER_ERROR')
|
|
72
|
+
expect(error.adapterName).toBe('icqq')
|
|
73
|
+
expect(error.botName).toBe('bot-123')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('ConnectionError应该包含重试信息', () => {
|
|
77
|
+
const error = new ConnectionError('连接超时', false)
|
|
78
|
+
|
|
79
|
+
expect(error.code).toBe('CONNECTION_ERROR')
|
|
80
|
+
expect(error.retryable).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('MessageError应该包含消息信息', () => {
|
|
84
|
+
const error = new MessageError('消息发送失败', 'msg-123', 'channel-456')
|
|
85
|
+
|
|
86
|
+
expect(error.code).toBe('MESSAGE_ERROR')
|
|
87
|
+
expect(error.messageId).toBe('msg-123')
|
|
88
|
+
expect(error.channelId).toBe('channel-456')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('ValidationError应该包含验证信息', () => {
|
|
92
|
+
const error = new ValidationError('字段验证失败', 'username', 'invalid_value')
|
|
93
|
+
|
|
94
|
+
expect(error.code).toBe('VALIDATION_ERROR')
|
|
95
|
+
expect(error.field).toBe('username')
|
|
96
|
+
expect(error.value).toBe('invalid_value')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('PermissionError应该包含权限信息', () => {
|
|
100
|
+
const error = new PermissionError('权限不足', 'user-123', 'admin')
|
|
101
|
+
|
|
102
|
+
expect(error.code).toBe('PERMISSION_ERROR')
|
|
103
|
+
expect(error.userId).toBe('user-123')
|
|
104
|
+
expect(error.requiredPermission).toBe('admin')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('TimeoutError应该包含超时信息', () => {
|
|
108
|
+
const error = new TimeoutError('操作超时', 5000)
|
|
109
|
+
|
|
110
|
+
expect(error.code).toBe('TIMEOUT_ERROR')
|
|
111
|
+
expect(error.timeoutMs).toBe(5000)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('ErrorManager错误管理器', () => {
|
|
116
|
+
let manager: ErrorManager
|
|
117
|
+
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
manager = new ErrorManager()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('应该能注册和调用错误处理器', async () => {
|
|
123
|
+
const handler = vi.fn()
|
|
124
|
+
const error = new PluginError('测试错误', 'test-plugin')
|
|
125
|
+
|
|
126
|
+
manager.register('PluginError', handler)
|
|
127
|
+
await manager.handle(error)
|
|
128
|
+
|
|
129
|
+
expect(handler).toHaveBeenCalledWith(error, undefined)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('应该能注册和调用全局处理器', async () => {
|
|
133
|
+
const handler = vi.fn()
|
|
134
|
+
const error = new Error('普通错误')
|
|
135
|
+
|
|
136
|
+
manager.registerGlobal(handler)
|
|
137
|
+
await manager.handle(error)
|
|
138
|
+
|
|
139
|
+
expect(handler).toHaveBeenCalledWith(error, undefined)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('应该能传递上下文信息', async () => {
|
|
143
|
+
const handler = vi.fn()
|
|
144
|
+
const error = new Error('测试错误')
|
|
145
|
+
const context = { key: 'value' }
|
|
146
|
+
|
|
147
|
+
manager.registerGlobal(handler)
|
|
148
|
+
await manager.handle(error, context)
|
|
149
|
+
|
|
150
|
+
expect(handler).toHaveBeenCalledWith(error, context)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('应该能移除错误处理器', () => {
|
|
154
|
+
const handler = vi.fn()
|
|
155
|
+
|
|
156
|
+
manager.register('Error', handler)
|
|
157
|
+
const removed = manager.unregister('Error', handler)
|
|
158
|
+
|
|
159
|
+
expect(removed).toBe(true)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('应该能清理所有处理器', async () => {
|
|
163
|
+
const handler = vi.fn()
|
|
164
|
+
const error = new Error('测试错误')
|
|
165
|
+
|
|
166
|
+
manager.registerGlobal(handler)
|
|
167
|
+
manager.clear()
|
|
168
|
+
await manager.handle(error)
|
|
169
|
+
|
|
170
|
+
expect(handler).not.toHaveBeenCalled()
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('处理器内部错误不应该影响其他处理器', async () => {
|
|
174
|
+
const failingHandler = vi.fn().mockRejectedValue(new Error('Handler error'))
|
|
175
|
+
const workingHandler = vi.fn()
|
|
176
|
+
const error = new Error('测试错误')
|
|
177
|
+
|
|
178
|
+
manager.registerGlobal(failingHandler)
|
|
179
|
+
manager.registerGlobal(workingHandler)
|
|
180
|
+
|
|
181
|
+
await manager.handle(error)
|
|
182
|
+
|
|
183
|
+
expect(failingHandler).toHaveBeenCalled()
|
|
184
|
+
expect(workingHandler).toHaveBeenCalled()
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('RetryManager重试管理器', () => {
|
|
189
|
+
it('应该在成功时不重试', async () => {
|
|
190
|
+
const fn = vi.fn().mockResolvedValue('success')
|
|
191
|
+
|
|
192
|
+
const result = await RetryManager.retry(fn, { maxRetries: 3 })
|
|
193
|
+
|
|
194
|
+
expect(result).toBe('success')
|
|
195
|
+
expect(fn).toHaveBeenCalledTimes(1)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('应该在失败时重试', async () => {
|
|
199
|
+
const fn = vi.fn()
|
|
200
|
+
.mockRejectedValueOnce(new Error('第一次失败'))
|
|
201
|
+
.mockRejectedValueOnce(new Error('第二次失败'))
|
|
202
|
+
.mockResolvedValue('成功')
|
|
203
|
+
|
|
204
|
+
const result = await RetryManager.retry(fn, { maxRetries: 3, delay: 10 })
|
|
205
|
+
|
|
206
|
+
expect(result).toBe('成功')
|
|
207
|
+
expect(fn).toHaveBeenCalledTimes(3)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('应该在达到最大重试次数后抛出错误', async () => {
|
|
211
|
+
const error = new Error('持续失败')
|
|
212
|
+
const fn = vi.fn().mockRejectedValue(error)
|
|
213
|
+
|
|
214
|
+
await expect(
|
|
215
|
+
RetryManager.retry(fn, { maxRetries: 2, delay: 10 })
|
|
216
|
+
).rejects.toThrow('持续失败')
|
|
217
|
+
|
|
218
|
+
expect(fn).toHaveBeenCalledTimes(3) // 初始调用 + 2次重试
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('应该遵循重试条件', async () => {
|
|
222
|
+
const error = new ConnectionError('不可重试的错误', false)
|
|
223
|
+
const fn = vi.fn().mockRejectedValue(error)
|
|
224
|
+
const retryCondition = (err: Error) => err instanceof ConnectionError && (err as ConnectionError).retryable
|
|
225
|
+
|
|
226
|
+
await expect(
|
|
227
|
+
RetryManager.retry(fn, { maxRetries: 3, retryCondition })
|
|
228
|
+
).rejects.toThrow('不可重试的错误')
|
|
229
|
+
|
|
230
|
+
expect(fn).toHaveBeenCalledTimes(1) // 不应该重试
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('应该支持指数退避', async () => {
|
|
234
|
+
const fn = vi.fn()
|
|
235
|
+
.mockRejectedValueOnce(new Error('失败'))
|
|
236
|
+
.mockResolvedValue('成功')
|
|
237
|
+
|
|
238
|
+
const startTime = Date.now()
|
|
239
|
+
await RetryManager.retry(fn, {
|
|
240
|
+
maxRetries: 1,
|
|
241
|
+
delay: 100,
|
|
242
|
+
exponentialBackoff: true
|
|
243
|
+
})
|
|
244
|
+
const endTime = Date.now()
|
|
245
|
+
|
|
246
|
+
// 允许5毫秒的误差范围,因为JavaScript时间精度问题
|
|
247
|
+
expect(endTime - startTime).toBeGreaterThanOrEqual(95)
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
describe('CircuitBreaker断路器', () => {
|
|
252
|
+
let circuitBreaker: CircuitBreaker
|
|
253
|
+
|
|
254
|
+
beforeEach(() => {
|
|
255
|
+
circuitBreaker = new CircuitBreaker(2, 1000, 500) // 失败阈值2, 超时1秒, 监控500ms
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('应该在正常情况下执行操作', async () => {
|
|
259
|
+
const fn = vi.fn().mockResolvedValue('success')
|
|
260
|
+
|
|
261
|
+
const result = await circuitBreaker.execute(fn)
|
|
262
|
+
|
|
263
|
+
expect(result).toBe('success')
|
|
264
|
+
expect(circuitBreaker.getState()).toBe('CLOSED')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('应该在失败次数达到阈值后打开断路器', async () => {
|
|
268
|
+
const fn = vi.fn().mockRejectedValue(new Error('失败'))
|
|
269
|
+
|
|
270
|
+
// 第一次失败
|
|
271
|
+
await expect(circuitBreaker.execute(fn)).rejects.toThrow('失败')
|
|
272
|
+
expect(circuitBreaker.getState()).toBe('CLOSED')
|
|
273
|
+
|
|
274
|
+
// 第二次失败,应该打开断路器
|
|
275
|
+
await expect(circuitBreaker.execute(fn)).rejects.toThrow('失败')
|
|
276
|
+
expect(circuitBreaker.getState()).toBe('OPEN')
|
|
277
|
+
|
|
278
|
+
// 后续调用应该直接拒绝
|
|
279
|
+
await expect(circuitBreaker.execute(fn)).rejects.toThrow('Circuit breaker is OPEN')
|
|
280
|
+
expect(fn).toHaveBeenCalledTimes(2) // 不应该再次调用原函数
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('应该在超时后尝试半开状态', async () => {
|
|
284
|
+
const fn = vi.fn().mockRejectedValue(new Error('失败'))
|
|
285
|
+
|
|
286
|
+
// 触发断路器打开
|
|
287
|
+
await expect(circuitBreaker.execute(fn)).rejects.toThrow()
|
|
288
|
+
await expect(circuitBreaker.execute(fn)).rejects.toThrow()
|
|
289
|
+
expect(circuitBreaker.getState()).toBe('OPEN')
|
|
290
|
+
|
|
291
|
+
// 模拟时间过去(这里无法真正等待,但可以测试逻辑)
|
|
292
|
+
// 在真实场景中,需要等待timeoutMs后再次调用
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('应该能重置断路器', async () => {
|
|
296
|
+
const fn = vi.fn().mockRejectedValue(new Error('失败'))
|
|
297
|
+
|
|
298
|
+
// 触发断路器打开
|
|
299
|
+
await expect(circuitBreaker.execute(fn)).rejects.toThrow()
|
|
300
|
+
await expect(circuitBreaker.execute(fn)).rejects.toThrow()
|
|
301
|
+
expect(circuitBreaker.getState()).toBe('OPEN')
|
|
302
|
+
|
|
303
|
+
// 重置断路器
|
|
304
|
+
circuitBreaker.reset()
|
|
305
|
+
expect(circuitBreaker.getState()).toBe('CLOSED')
|
|
306
|
+
|
|
307
|
+
// 应该能再次执行
|
|
308
|
+
await expect(circuitBreaker.execute(fn)).rejects.toThrow('失败')
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
})
|