@zhin.js/core 1.0.0 → 1.0.2
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/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 +27 -0
- package/lib/component.d.ts.map +1 -0
- package/lib/component.js +469 -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/jsx-runtime.d.ts +12 -0
- package/lib/jsx-runtime.d.ts.map +1 -0
- package/lib/jsx-runtime.js +11 -0
- package/lib/jsx-runtime.js.map +1 -0
- package/lib/jsx.d.ts +32 -0
- package/lib/jsx.d.ts.map +1 -0
- package/lib/jsx.js +57 -0
- package/lib/jsx.js.map +1 -0
- package/lib/message.d.ts +47 -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 +121 -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 +340 -0
- package/lib/utils.js.map +1 -0
- package/package.json +23 -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 +561 -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/jsx-runtime.ts +12 -0
- package/src/jsx.d.ts +52 -0
- package/src/jsx.ts +92 -0
- package/src/message.ts +47 -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 +80 -31
- package/src/utils.ts +313 -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-new.test.ts +348 -0
- package/tests/config.test.ts +1 -1
- package/tests/errors.test.ts +311 -0
- package/tests/expression-evaluation.test.ts +258 -0
- package/tests/message.test.ts +402 -0
- package/tests/plugin.test.ts +284 -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,348 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
defineComponent,
|
|
4
|
+
createComponentContext,
|
|
5
|
+
renderComponents,
|
|
6
|
+
getProps,
|
|
7
|
+
Component,
|
|
8
|
+
ComponentContext,
|
|
9
|
+
Fragment,
|
|
10
|
+
Fetch
|
|
11
|
+
} from '../src/component'
|
|
12
|
+
import { Message } from '../src/message'
|
|
13
|
+
import { SendOptions } from '../src/types'
|
|
14
|
+
|
|
15
|
+
// Mock utils functions
|
|
16
|
+
vi.mock('../src/utils', () => ({
|
|
17
|
+
getValueWithRuntime: vi.fn((expression, context) => {
|
|
18
|
+
// Simple mock implementation for testing
|
|
19
|
+
if (typeof expression === 'string' && context) {
|
|
20
|
+
// Handle simple variable access
|
|
21
|
+
if (expression in context) {
|
|
22
|
+
return context[expression]
|
|
23
|
+
}
|
|
24
|
+
// Handle object property access like 'user.name'
|
|
25
|
+
if (expression.includes('.')) {
|
|
26
|
+
const [obj, prop] = expression.split('.')
|
|
27
|
+
return context[obj]?.[prop]
|
|
28
|
+
}
|
|
29
|
+
// Handle simple expressions
|
|
30
|
+
try {
|
|
31
|
+
return eval(expression)
|
|
32
|
+
} catch {
|
|
33
|
+
return expression
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return expression
|
|
37
|
+
}),
|
|
38
|
+
compiler: vi.fn((template, context) => template),
|
|
39
|
+
segment: {
|
|
40
|
+
toString: vi.fn((content) => typeof content === 'string' ? content : JSON.stringify(content)),
|
|
41
|
+
from: vi.fn((content) => content),
|
|
42
|
+
escape: vi.fn((content) => content.replace(/</g, '<').replace(/>/g, '>'))
|
|
43
|
+
}
|
|
44
|
+
}))
|
|
45
|
+
|
|
46
|
+
describe('函数式组件系统测试', () => {
|
|
47
|
+
let mockContext: ComponentContext
|
|
48
|
+
let mockMessage: Message
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
mockMessage = {
|
|
52
|
+
$id: '1',
|
|
53
|
+
$adapter: 'test',
|
|
54
|
+
$bot: 'test-bot',
|
|
55
|
+
$content: [],
|
|
56
|
+
$sender: { id: 'user1', name: 'User' },
|
|
57
|
+
$reply: vi.fn(),
|
|
58
|
+
$channel: { id: 'channel1', type: 'private' },
|
|
59
|
+
$timestamp: Date.now(),
|
|
60
|
+
$raw: 'test'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
mockContext = createComponentContext(
|
|
64
|
+
{ user: { name: 'John', age: 25 } },
|
|
65
|
+
undefined,
|
|
66
|
+
'test template'
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('defineComponent 函数测试', () => {
|
|
71
|
+
it('应该正确创建函数式组件', () => {
|
|
72
|
+
const TestComponent = defineComponent(async function TestComponent(props: { name: string }, context: ComponentContext) {
|
|
73
|
+
return `Hello ${props.name}`
|
|
74
|
+
}, 'test-component')
|
|
75
|
+
|
|
76
|
+
expect(TestComponent).toBeInstanceOf(Function)
|
|
77
|
+
expect(TestComponent.name).toBe('test-component')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('应该支持异步组件', async () => {
|
|
81
|
+
const AsyncComponent = defineComponent(async function AsyncComponent(props: { delay: number }, context: ComponentContext) {
|
|
82
|
+
await new Promise(resolve => setTimeout(resolve, props.delay))
|
|
83
|
+
return `Delayed: ${props.delay}ms`
|
|
84
|
+
}, 'async-component')
|
|
85
|
+
|
|
86
|
+
const result = await AsyncComponent({ delay: 10 }, mockContext)
|
|
87
|
+
expect(result).toBe('Delayed: 10ms')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('getProps 函数测试', () => {
|
|
92
|
+
it('应该正确解析简单属性', () => {
|
|
93
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
94
|
+
return 'test'
|
|
95
|
+
}, 'test')
|
|
96
|
+
|
|
97
|
+
const template = '<test name="John" age={25} active={true} />'
|
|
98
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
99
|
+
|
|
100
|
+
expect(props.name).toBe('John')
|
|
101
|
+
expect(props.age).toBe(25)
|
|
102
|
+
expect(props.active).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('应该正确解析表达式属性', () => {
|
|
106
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
107
|
+
return 'test'
|
|
108
|
+
}, 'test')
|
|
109
|
+
|
|
110
|
+
const template = '<test sum={1+1} />'
|
|
111
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
112
|
+
|
|
113
|
+
expect(props.sum).toBe(2)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('应该正确处理 kebab-case 到 camelCase 转换', () => {
|
|
117
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
118
|
+
return 'test'
|
|
119
|
+
}, 'test')
|
|
120
|
+
|
|
121
|
+
const template = '<test user-name="John" user-age={25} is-active={true} />'
|
|
122
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
123
|
+
|
|
124
|
+
expect(props.userName).toBe('John')
|
|
125
|
+
expect(props.userAge).toBe(25)
|
|
126
|
+
expect(props.isActive).toBe(true)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('应该正确处理 children 属性', () => {
|
|
130
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
131
|
+
return 'test'
|
|
132
|
+
}, 'test')
|
|
133
|
+
|
|
134
|
+
const template = '<test>Hello World</test>'
|
|
135
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
136
|
+
|
|
137
|
+
expect(props.children).toBe('Hello World')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('应该正确处理自闭合标签', () => {
|
|
141
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
142
|
+
return 'test'
|
|
143
|
+
}, 'test')
|
|
144
|
+
|
|
145
|
+
const template = '<test name="John" />'
|
|
146
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
147
|
+
|
|
148
|
+
expect(props.name).toBe('John')
|
|
149
|
+
expect(props.children).toBeUndefined()
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('createComponentContext 函数测试', () => {
|
|
154
|
+
it('应该正确创建组件上下文', () => {
|
|
155
|
+
const context = createComponentContext(
|
|
156
|
+
{ user: { name: 'John' } },
|
|
157
|
+
undefined,
|
|
158
|
+
'test template'
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
expect(context.props).toEqual({ user: { name: 'John' } })
|
|
162
|
+
expect(context.root).toBe('test template')
|
|
163
|
+
expect(context.parent).toBeUndefined()
|
|
164
|
+
expect(context.children).toBeUndefined()
|
|
165
|
+
expect(typeof context.render).toBe('function')
|
|
166
|
+
expect(typeof context.getValue).toBe('function')
|
|
167
|
+
expect(typeof context.compile).toBe('function')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('应该正确处理父上下文', () => {
|
|
171
|
+
const parentContext = createComponentContext({ parent: 'data' })
|
|
172
|
+
const childContext = createComponentContext(
|
|
173
|
+
{ child: 'data' },
|
|
174
|
+
parentContext,
|
|
175
|
+
'child template'
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
expect(childContext.parent).toBe(parentContext)
|
|
179
|
+
expect(childContext.props).toEqual({ child: 'data' })
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('内置组件测试', () => {
|
|
184
|
+
it('Fragment 组件应该正确渲染 children', async () => {
|
|
185
|
+
const result = await Fragment({ children: 'Hello World' }, mockContext)
|
|
186
|
+
expect(result).toBe('Hello World')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('Fetch 组件应该正确获取远程内容', async () => {
|
|
190
|
+
// Mock fetch
|
|
191
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
192
|
+
text: () => Promise.resolve('Remote content')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const result = await Fetch({ url: 'https://example.com' }, mockContext)
|
|
196
|
+
expect(result).toBe('Remote content')
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe('renderComponents 函数测试', () => {
|
|
201
|
+
it('应该正确渲染单个组件', async () => {
|
|
202
|
+
const TestComponent = defineComponent(async function TestComponent(props: { name: string }, context: ComponentContext) {
|
|
203
|
+
return `Hello ${props.name}`
|
|
204
|
+
}, 'test')
|
|
205
|
+
|
|
206
|
+
const componentMap = new Map([['test', TestComponent]])
|
|
207
|
+
const options: SendOptions = {
|
|
208
|
+
content: '<test name="John" />',
|
|
209
|
+
type: 'text',
|
|
210
|
+
context: 'test',
|
|
211
|
+
bot: 'test'
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const result = await renderComponents(componentMap, options)
|
|
215
|
+
expect(result.content).toContain('Hello John')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('应该正确渲染多个组件', async () => {
|
|
219
|
+
const Component1 = defineComponent(async function Component1(props: { text: string }, context: ComponentContext) {
|
|
220
|
+
return `[${props.text}]`
|
|
221
|
+
}, 'comp1')
|
|
222
|
+
|
|
223
|
+
const Component2 = defineComponent(async function Component2(props: { number: number }, context: ComponentContext) {
|
|
224
|
+
return `{${props.number}}`
|
|
225
|
+
}, 'comp2')
|
|
226
|
+
|
|
227
|
+
const componentMap = new Map([
|
|
228
|
+
['comp1', Component1],
|
|
229
|
+
['comp2', Component2]
|
|
230
|
+
])
|
|
231
|
+
|
|
232
|
+
const options: SendOptions = {
|
|
233
|
+
content: '<comp1 text="Hello" /> <comp2 number={42} />',
|
|
234
|
+
type: 'text',
|
|
235
|
+
context: 'test',
|
|
236
|
+
bot: 'test'
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const result = await renderComponents(componentMap, options)
|
|
240
|
+
expect(result.content).toContain('[Hello]')
|
|
241
|
+
expect(result.content).toContain('{42}')
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('应该正确处理嵌套组件', async () => {
|
|
245
|
+
const OuterComponent = defineComponent(async function OuterComponent(props: { title: string }, context: ComponentContext) {
|
|
246
|
+
return `标题: ${props.title}`
|
|
247
|
+
}, 'outer')
|
|
248
|
+
|
|
249
|
+
const InnerComponent = defineComponent(async function InnerComponent(props: { content: string }, context: ComponentContext) {
|
|
250
|
+
return `Content: ${props.content}`
|
|
251
|
+
}, 'inner')
|
|
252
|
+
|
|
253
|
+
const componentMap = new Map([
|
|
254
|
+
['outer', OuterComponent],
|
|
255
|
+
['inner', InnerComponent]
|
|
256
|
+
])
|
|
257
|
+
|
|
258
|
+
const options: SendOptions = {
|
|
259
|
+
content: '<outer title="Test"><inner content="Nested" /></outer>',
|
|
260
|
+
type: 'text',
|
|
261
|
+
context: 'test',
|
|
262
|
+
bot: 'test'
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const result = await renderComponents(componentMap, options)
|
|
266
|
+
// 现在嵌套组件渲染应该工作了
|
|
267
|
+
expect(result.content).toContain('标题: Test')
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
describe('表达式求值测试', () => {
|
|
272
|
+
it('应该正确计算数学表达式', () => {
|
|
273
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
274
|
+
return 'test'
|
|
275
|
+
}, 'test')
|
|
276
|
+
|
|
277
|
+
const template = '<test sum={1+2+3} product={2*3*4} />'
|
|
278
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
279
|
+
|
|
280
|
+
expect(props.sum).toBe(6)
|
|
281
|
+
expect(props.product).toBe(24)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('应该正确处理比较表达式', () => {
|
|
285
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
286
|
+
return 'test'
|
|
287
|
+
}, 'test')
|
|
288
|
+
|
|
289
|
+
const template = '<test greater={5>3} equal={2==2} />'
|
|
290
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
291
|
+
|
|
292
|
+
expect(props.greater).toBe(true)
|
|
293
|
+
expect(props.equal).toBe(true)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('应该正确处理三元运算符', () => {
|
|
297
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
298
|
+
return 'test'
|
|
299
|
+
}, 'test')
|
|
300
|
+
|
|
301
|
+
const template = '<test result={5>3 ? "yes" : "no"} />'
|
|
302
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
303
|
+
|
|
304
|
+
expect(props.result).toBe('yes')
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('应该正确处理数组和对象表达式', () => {
|
|
308
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
309
|
+
return 'test'
|
|
310
|
+
}, 'test')
|
|
311
|
+
|
|
312
|
+
const template = '<test items={[1,2,3]} config={{name:"test",value:42}} />'
|
|
313
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
314
|
+
|
|
315
|
+
expect(props.items).toEqual([1, 2, 3])
|
|
316
|
+
expect(props.config).toEqual({ name: 'test', value: 42 })
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
describe('错误处理测试', () => {
|
|
321
|
+
it('应该正确处理无效的组件模板', () => {
|
|
322
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
323
|
+
return 'test'
|
|
324
|
+
}, 'test')
|
|
325
|
+
|
|
326
|
+
const template = 'invalid template'
|
|
327
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
328
|
+
|
|
329
|
+
expect(props).toEqual({})
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('应该正确处理组件渲染错误', async () => {
|
|
333
|
+
const ErrorComponent = defineComponent(async function ErrorComponent(props: any, context: ComponentContext) {
|
|
334
|
+
throw new Error('Test error')
|
|
335
|
+
}, 'error')
|
|
336
|
+
|
|
337
|
+
const componentMap = new Map([['error', ErrorComponent]])
|
|
338
|
+
const options: SendOptions = {
|
|
339
|
+
content: '<error />',
|
|
340
|
+
type: 'text',
|
|
341
|
+
context: 'test',
|
|
342
|
+
bot: 'test'
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await expect(renderComponents(componentMap, options)).rejects.toThrow('Test error')
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
})
|
package/tests/config.test.ts
CHANGED
|
@@ -101,7 +101,7 @@ module.exports = {
|
|
|
101
101
|
]
|
|
102
102
|
}
|
|
103
103
|
`
|
|
104
|
-
const configPath = path.join(testDir, 'zhin.config.
|
|
104
|
+
const configPath = path.join(testDir, 'zhin.config.ts')
|
|
105
105
|
fs.writeFileSync(configPath, config)
|
|
106
106
|
|
|
107
107
|
const [loadedPath, loadedConfig] = await loadConfig({ configPath })
|
|
@@ -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
|
+
})
|