@zhin.js/core 1.0.38 → 1.0.40
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/ai/index.d.ts +10 -5
- package/lib/ai/index.d.ts.map +1 -1
- package/lib/ai/index.js +7 -4
- package/lib/ai/index.js.map +1 -1
- package/lib/ai/providers/anthropic.d.ts.map +1 -1
- package/lib/ai/providers/anthropic.js +2 -0
- package/lib/ai/providers/anthropic.js.map +1 -1
- package/lib/ai/providers/openai.d.ts.map +1 -1
- package/lib/ai/providers/openai.js +8 -0
- package/lib/ai/providers/openai.js.map +1 -1
- package/lib/ai/types.d.ts +1 -0
- package/lib/ai/types.d.ts.map +1 -1
- package/lib/cron.d.ts +2 -43
- package/lib/cron.d.ts.map +1 -1
- package/lib/cron.js +2 -126
- package/lib/cron.js.map +1 -1
- package/lib/errors.d.ts +3 -146
- package/lib/errors.d.ts.map +1 -1
- package/lib/errors.js +3 -279
- package/lib/errors.js.map +1 -1
- package/lib/feature.d.ts +5 -87
- package/lib/feature.d.ts.map +1 -1
- package/lib/feature.js +4 -105
- package/lib/feature.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/scheduler/index.d.ts +3 -7
- package/lib/scheduler/index.d.ts.map +1 -1
- package/lib/scheduler/index.js +2 -9
- package/lib/scheduler/index.js.map +1 -1
- package/lib/types.d.ts +8 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/utils.d.ts +7 -52
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +9 -325
- package/lib/utils.js.map +1 -1
- package/package.json +6 -4
- package/src/ai/index.ts +15 -9
- package/src/ai/providers/anthropic.ts +1 -0
- package/src/ai/providers/openai.ts +5 -1
- package/src/ai/types.ts +1 -0
- package/src/cron.ts +2 -140
- package/src/errors.ts +15 -334
- package/src/feature.ts +5 -154
- package/src/index.ts +3 -1
- package/src/scheduler/index.ts +8 -17
- package/src/types.ts +10 -2
- package/src/utils.ts +37 -334
- package/tests/cron.test.ts +4 -299
- package/tests/errors.test.ts +17 -307
- package/tests/utils.test.ts +11 -516
- package/tests/feature.test.ts +0 -145
package/tests/cron.test.ts
CHANGED
|
@@ -1,302 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CronFeature 测试(Cron 类测试已迁移到 @zhin.js/kernel)
|
|
3
|
+
*/
|
|
1
4
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
2
5
|
import { Cron } from '../src/cron'
|
|
3
6
|
|
|
4
|
-
describe('Cron定时任务系统测试', () => {
|
|
5
|
-
let mockCallback: () => void | Promise<void>
|
|
6
|
-
let cron: Cron
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
vi.useFakeTimers()
|
|
10
|
-
mockCallback = vi.fn()
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
if (cron && !cron.disposed) {
|
|
15
|
-
cron.dispose()
|
|
16
|
-
}
|
|
17
|
-
vi.useRealTimers()
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
describe('Cron实例化', () => {
|
|
21
|
-
it('应该正确创建Cron实例', () => {
|
|
22
|
-
cron = new Cron('0 0 * * * *', mockCallback) // 每小时执行
|
|
23
|
-
expect(cron).toBeInstanceOf(Cron)
|
|
24
|
-
expect(cron.running).toBe(false)
|
|
25
|
-
expect(cron.disposed).toBe(false)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('应该正确保存cron表达式', () => {
|
|
29
|
-
cron = new Cron('0 0 12 * * *', mockCallback) // 每天中午12点
|
|
30
|
-
expect(cron.cronExpression).toContain('0 12 * * *')
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('应该拒绝无效的cron表达式', () => {
|
|
34
|
-
expect(() => {
|
|
35
|
-
new Cron('invalid expression', mockCallback)
|
|
36
|
-
}).toThrow(/Invalid cron expression/)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('应该接受异步回调函数', () => {
|
|
40
|
-
const asyncCallback = vi.fn().mockResolvedValue(undefined)
|
|
41
|
-
cron = new Cron('0 0 * * * *', asyncCallback)
|
|
42
|
-
expect(cron).toBeInstanceOf(Cron)
|
|
43
|
-
})
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
describe('任务执行控制', () => {
|
|
47
|
-
beforeEach(() => {
|
|
48
|
-
cron = new Cron('*/5 * * * * *', mockCallback) // 每5秒执行
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('应该能够启动任务', () => {
|
|
52
|
-
expect(cron.running).toBe(false)
|
|
53
|
-
cron.run()
|
|
54
|
-
expect(cron.running).toBe(true)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('应该能够停止任务', () => {
|
|
58
|
-
cron.run()
|
|
59
|
-
expect(cron.running).toBe(true)
|
|
60
|
-
cron.stop()
|
|
61
|
-
expect(cron.running).toBe(false)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('应该防止重复启动', () => {
|
|
65
|
-
cron.run()
|
|
66
|
-
expect(cron.running).toBe(true)
|
|
67
|
-
|
|
68
|
-
// 第二次调用run应该无效果
|
|
69
|
-
cron.run()
|
|
70
|
-
expect(cron.running).toBe(true)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('应该能够销毁任务', () => {
|
|
74
|
-
cron.run()
|
|
75
|
-
expect(cron.running).toBe(true)
|
|
76
|
-
expect(cron.disposed).toBe(false)
|
|
77
|
-
|
|
78
|
-
cron.dispose()
|
|
79
|
-
expect(cron.running).toBe(false)
|
|
80
|
-
expect(cron.disposed).toBe(true)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('应该拒绝操作已销毁的任务', () => {
|
|
84
|
-
cron.dispose()
|
|
85
|
-
expect(() => cron.run()).toThrow(/Cannot run a disposed cron job/)
|
|
86
|
-
expect(() => cron.getNextExecutionTime()).toThrow(/Cannot get next execution time for a disposed cron job/)
|
|
87
|
-
})
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
describe('时间计算', () => {
|
|
91
|
-
beforeEach(() => {
|
|
92
|
-
// 设置固定时间: 2024-01-01 00:00:00
|
|
93
|
-
vi.setSystemTime(new Date('2024-01-01T00:00:00.000Z'))
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('应该计算下一次执行时间', () => {
|
|
97
|
-
cron = new Cron('0 0 12 * * *', mockCallback) // 每天中午12点
|
|
98
|
-
const nextTime = cron.getNextExecutionTime()
|
|
99
|
-
|
|
100
|
-
// 下一次执行应该是当天中午12点
|
|
101
|
-
expect(nextTime.getHours()).toBe(12)
|
|
102
|
-
expect(nextTime.getMinutes()).toBe(0)
|
|
103
|
-
expect(nextTime.getSeconds()).toBe(0)
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('应该处理跨日期的执行时间', () => {
|
|
107
|
-
vi.setSystemTime(new Date('2024-01-01T23:00:00.000Z')) // 晚上11点
|
|
108
|
-
cron = new Cron('0 0 1 * * *', mockCallback) // 每天凌晨1点
|
|
109
|
-
|
|
110
|
-
const nextTime = cron.getNextExecutionTime()
|
|
111
|
-
// 从晚上11点到第二天凌晨1点,可能还是当天或第二天,取决于实现
|
|
112
|
-
expect(nextTime.getHours()).toBe(1)
|
|
113
|
-
expect(nextTime.getDate()).toBeGreaterThanOrEqual(1)
|
|
114
|
-
})
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
describe('cron表达式解析', () => {
|
|
118
|
-
it('应该支持每分钟执行', () => {
|
|
119
|
-
cron = new Cron('0 * * * * *', mockCallback)
|
|
120
|
-
expect(cron.cronExpression).toContain('* * * * *')
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('应该支持每小时执行', () => {
|
|
124
|
-
cron = new Cron('0 0 * * * *', mockCallback)
|
|
125
|
-
expect(cron.cronExpression).toContain('0 * * * *')
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('应该支持每天执行', () => {
|
|
129
|
-
cron = new Cron('0 0 0 * * *', mockCallback)
|
|
130
|
-
expect(cron.cronExpression).toContain('0 0 * * *')
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
it('应该支持步长表达式', () => {
|
|
134
|
-
cron = new Cron('0 */15 * * * *', mockCallback) // 每15分钟
|
|
135
|
-
expect(cron.cronExpression).toContain('*/15')
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('应该支持范围表达式', () => {
|
|
139
|
-
cron = new Cron('0 0 9-17 * * *', mockCallback) // 工作时间
|
|
140
|
-
expect(cron.cronExpression).toContain('9-17')
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
it('应该支持列表表达式', () => {
|
|
144
|
-
cron = new Cron('0 0 0 * * 1,3,5', mockCallback) // 周一、三、五
|
|
145
|
-
expect(cron.cronExpression).toContain('1,3,5')
|
|
146
|
-
})
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
describe('任务执行', () => {
|
|
150
|
-
it('应该在指定时间执行回调', async () => {
|
|
151
|
-
// 每秒执行的任务
|
|
152
|
-
cron = new Cron('* * * * * *', mockCallback)
|
|
153
|
-
cron.run()
|
|
154
|
-
|
|
155
|
-
// 推进时间1秒
|
|
156
|
-
await vi.advanceTimersByTimeAsync(1000)
|
|
157
|
-
|
|
158
|
-
expect(mockCallback).toHaveBeenCalledTimes(1)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('应该处理异步回调', async () => {
|
|
162
|
-
const asyncCallback = vi.fn().mockResolvedValue(undefined)
|
|
163
|
-
cron = new Cron('* * * * * *', asyncCallback)
|
|
164
|
-
cron.run()
|
|
165
|
-
|
|
166
|
-
await vi.advanceTimersByTimeAsync(1000)
|
|
167
|
-
|
|
168
|
-
expect(asyncCallback).toHaveBeenCalledTimes(1)
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('应该处理回调中的错误', async () => {
|
|
172
|
-
const errorCallback = vi.fn().mockRejectedValue(new Error('Test error'))
|
|
173
|
-
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
174
|
-
|
|
175
|
-
cron = new Cron('* * * * * *', errorCallback)
|
|
176
|
-
cron.run()
|
|
177
|
-
|
|
178
|
-
await vi.advanceTimersByTimeAsync(1000)
|
|
179
|
-
|
|
180
|
-
expect(errorCallback).toHaveBeenCalledTimes(1)
|
|
181
|
-
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
182
|
-
expect.stringContaining('Error executing cron callback')
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
consoleErrorSpy.mockRestore()
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it('应该继续执行即使回调出错', async () => {
|
|
189
|
-
const errorCallback = vi.fn()
|
|
190
|
-
.mockRejectedValueOnce(new Error('First error'))
|
|
191
|
-
.mockResolvedValueOnce(undefined)
|
|
192
|
-
|
|
193
|
-
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
194
|
-
|
|
195
|
-
cron = new Cron('* * * * * *', errorCallback)
|
|
196
|
-
cron.run()
|
|
197
|
-
|
|
198
|
-
// 第一次执行(出错)
|
|
199
|
-
await vi.advanceTimersByTimeAsync(1000)
|
|
200
|
-
expect(errorCallback).toHaveBeenCalledTimes(1)
|
|
201
|
-
|
|
202
|
-
// 第二次执行(成功)
|
|
203
|
-
await vi.advanceTimersByTimeAsync(1000)
|
|
204
|
-
expect(errorCallback).toHaveBeenCalledTimes(2)
|
|
205
|
-
|
|
206
|
-
consoleErrorSpy.mockRestore()
|
|
207
|
-
})
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
describe('定时器管理', () => {
|
|
211
|
-
it('应该在停止时清除定时器', () => {
|
|
212
|
-
cron = new Cron('0 0 * * * *', mockCallback)
|
|
213
|
-
cron.run()
|
|
214
|
-
|
|
215
|
-
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout')
|
|
216
|
-
cron.stop()
|
|
217
|
-
|
|
218
|
-
expect(clearTimeoutSpy).toHaveBeenCalled()
|
|
219
|
-
clearTimeoutSpy.mockRestore()
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
it('应该在销毁时清除定时器', () => {
|
|
223
|
-
cron = new Cron('0 0 * * * *', mockCallback)
|
|
224
|
-
cron.run()
|
|
225
|
-
|
|
226
|
-
const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout')
|
|
227
|
-
cron.dispose()
|
|
228
|
-
|
|
229
|
-
expect(clearTimeoutSpy).toHaveBeenCalled()
|
|
230
|
-
clearTimeoutSpy.mockRestore()
|
|
231
|
-
})
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
describe('边界情况', () => {
|
|
235
|
-
it('应该处理立即执行的情况', async () => {
|
|
236
|
-
// 设置当前时间为准确的秒开始
|
|
237
|
-
vi.setSystemTime(new Date('2024-01-01T00:00:00.000Z'))
|
|
238
|
-
|
|
239
|
-
cron = new Cron('* * * * * *', mockCallback) // 每秒执行
|
|
240
|
-
cron.run()
|
|
241
|
-
|
|
242
|
-
// 只推进1秒,避免无限循环
|
|
243
|
-
await vi.advanceTimersByTimeAsync(1000)
|
|
244
|
-
|
|
245
|
-
expect(mockCallback).toHaveBeenCalled()
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
it('应该处理非常频繁的任务', async () => {
|
|
249
|
-
// 每秒执行多次的任务(这在实际使用中可能不常见,但测试边界情况)
|
|
250
|
-
cron = new Cron('* * * * * *', mockCallback)
|
|
251
|
-
cron.run()
|
|
252
|
-
|
|
253
|
-
// 推进5秒
|
|
254
|
-
for (let i = 0; i < 5; i++) {
|
|
255
|
-
await vi.advanceTimersByTimeAsync(1000)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
expect(mockCallback).toHaveBeenCalledTimes(5)
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
it('应该在停止后不再执行', async () => {
|
|
262
|
-
cron = new Cron('* * * * * *', mockCallback)
|
|
263
|
-
cron.run()
|
|
264
|
-
|
|
265
|
-
// 推进1秒并执行
|
|
266
|
-
await vi.advanceTimersByTimeAsync(1000)
|
|
267
|
-
expect(mockCallback).toHaveBeenCalledTimes(1)
|
|
268
|
-
|
|
269
|
-
// 停止任务
|
|
270
|
-
cron.stop()
|
|
271
|
-
|
|
272
|
-
// 再推进时间,不应该再执行
|
|
273
|
-
await vi.advanceTimersByTimeAsync(2000)
|
|
274
|
-
expect(mockCallback).toHaveBeenCalledTimes(1)
|
|
275
|
-
})
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
describe('Cron错误处理', () => {
|
|
279
|
-
it('should throw error for truly invalid cron expression', () => {
|
|
280
|
-
// 完全无效的 cron 表达式应该抛出错误
|
|
281
|
-
expect(() => {
|
|
282
|
-
cron = new Cron('not a valid cron', mockCallback)
|
|
283
|
-
cron.run()
|
|
284
|
-
}).toThrow()
|
|
285
|
-
})
|
|
286
|
-
|
|
287
|
-
it('should accept edge-case cron expression like Feb 30 without throwing', () => {
|
|
288
|
-
// 2月30日虽然不存在,但 cron 库接受该表达式(不会抛错)
|
|
289
|
-
expect(() => {
|
|
290
|
-
cron = new Cron('0 0 30 2 *', mockCallback)
|
|
291
|
-
cron.run()
|
|
292
|
-
}).not.toThrow()
|
|
293
|
-
})
|
|
294
|
-
})
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
// ============================================================================
|
|
298
|
-
// CronFeature 补全测试
|
|
299
|
-
// ============================================================================
|
|
300
7
|
describe('CronFeature', () => {
|
|
301
8
|
let feature: import('../src/built/cron.js').CronFeature
|
|
302
9
|
let mockCallback: ReturnType<typeof vi.fn>
|
|
@@ -339,7 +46,6 @@ describe('CronFeature', () => {
|
|
|
339
46
|
const cron2 = new Cron('*/2 * * * * *', mockCallback)
|
|
340
47
|
feature.add(cron1, 'p1')
|
|
341
48
|
feature.add(cron2, 'p2')
|
|
342
|
-
|
|
343
49
|
feature.stopAll()
|
|
344
50
|
expect(cron1.running).toBe(false)
|
|
345
51
|
expect(cron2.running).toBe(false)
|
|
@@ -351,7 +57,6 @@ describe('CronFeature', () => {
|
|
|
351
57
|
feature.add(cron1, 'p1')
|
|
352
58
|
feature.add(cron2, 'p2')
|
|
353
59
|
feature.stopAll()
|
|
354
|
-
|
|
355
60
|
feature.startAll()
|
|
356
61
|
expect(cron1.running).toBe(true)
|
|
357
62
|
expect(cron2.running).toBe(true)
|
|
@@ -374,4 +79,4 @@ describe('CronFeature', () => {
|
|
|
374
79
|
feature.dispose()
|
|
375
80
|
expect(cron.running).toBe(false)
|
|
376
81
|
})
|
|
377
|
-
})
|
|
82
|
+
})
|
package/tests/errors.test.ts
CHANGED
|
@@ -1,311 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
})
|
|
1
|
+
/**
|
|
2
|
+
* Core 特有的错误类型测试(通用错误测试已迁移到 @zhin.js/kernel)
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest'
|
|
5
|
+
import { AdapterError, MessageError } from '../src/errors.js'
|
|
6
|
+
|
|
7
|
+
describe('Core 特有错误类型', () => {
|
|
8
|
+
it('AdapterError应该包含适配器信息', () => {
|
|
9
|
+
const error = new AdapterError('适配器连接失败', 'icqq', 'bot-123')
|
|
10
|
+
expect(error.code).toBe('ADAPTER_ERROR')
|
|
11
|
+
expect(error.adapterName).toBe('icqq')
|
|
12
|
+
expect(error.botName).toBe('bot-123')
|
|
249
13
|
})
|
|
250
14
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
})
|
|
15
|
+
it('MessageError应该包含消息信息', () => {
|
|
16
|
+
const error = new MessageError('消息发送失败', 'msg-123', 'channel-456')
|
|
17
|
+
expect(error.code).toBe('MESSAGE_ERROR')
|
|
18
|
+
expect(error.messageId).toBe('msg-123')
|
|
19
|
+
expect(error.channelId).toBe('channel-456')
|
|
310
20
|
})
|
|
311
21
|
})
|