@zhin.js/core 1.0.20 → 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 +9 -0
- 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/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/built/config.ts +17 -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
package/tests/adapter.test.ts
CHANGED
|
@@ -1,7 +1,384 @@
|
|
|
1
|
-
import { describe, it, expect } from
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { Adapter } from '../src/adapter'
|
|
3
|
+
import { Bot } from '../src/bot'
|
|
4
|
+
import { Plugin } from '../src/plugin'
|
|
5
|
+
import { Message, MessageBase } from '../src/message'
|
|
6
|
+
import { EventEmitter } from 'events'
|
|
2
7
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
8
|
+
// Mock Bot 实现用于测试
|
|
9
|
+
class MockBot implements Bot<any, any> {
|
|
10
|
+
$id: string
|
|
11
|
+
$config: any
|
|
12
|
+
$connected: boolean = false
|
|
13
|
+
adapter: Adapter
|
|
14
|
+
|
|
15
|
+
constructor(adapter: Adapter, config: any) {
|
|
16
|
+
this.adapter = adapter
|
|
17
|
+
this.$config = config
|
|
18
|
+
this.$id = config.id || 'mock-bot'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
$formatMessage(event: any): Message<any> {
|
|
22
|
+
const base: MessageBase = {
|
|
23
|
+
$id: event.id || 'mock-id',
|
|
24
|
+
$adapter: 'test' as any,
|
|
25
|
+
$bot: this.$id,
|
|
26
|
+
$content: [],
|
|
27
|
+
$sender: { id: 'mock-sender', name: 'Mock Sender' },
|
|
28
|
+
$channel: { id: 'mock-channel', type: 'private' },
|
|
29
|
+
$timestamp: Date.now(),
|
|
30
|
+
$raw: event.raw || event,
|
|
31
|
+
$reply: async (content: any) => 'mock-reply-id',
|
|
32
|
+
$recall: async () => {}
|
|
33
|
+
}
|
|
34
|
+
return Message.from(event, base)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async $connect(): Promise<void> {
|
|
38
|
+
this.$connected = true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async $disconnect(): Promise<void> {
|
|
42
|
+
this.$connected = false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async $sendMessage(options: any): Promise<string> {
|
|
46
|
+
return 'mock-message-id'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async $recallMessage(id: string): Promise<void> {
|
|
50
|
+
// Mock 撤回消息
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Mock Adapter 类用于测试
|
|
55
|
+
class MockAdapter extends Adapter<MockBot> {
|
|
56
|
+
createBot(config: any): MockBot {
|
|
57
|
+
return new MockBot(this, config)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe('Adapter Core Functionality', () => {
|
|
62
|
+
let plugin: Plugin
|
|
63
|
+
let adapter: MockAdapter
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
plugin = new Plugin('/test/plugin.ts')
|
|
67
|
+
adapter = new MockAdapter(plugin, 'test', [])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('Adapter Constructor', () => {
|
|
71
|
+
it('should create adapter with plugin, name and config', () => {
|
|
72
|
+
const config = [{ id: 'bot1' }]
|
|
73
|
+
const adapter = new MockAdapter(plugin, 'test', config)
|
|
74
|
+
|
|
75
|
+
expect(adapter.plugin).toBe(plugin)
|
|
76
|
+
expect(adapter.name).toBe('test')
|
|
77
|
+
expect(adapter.config).toBe(config)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should initialize with empty bots map', () => {
|
|
81
|
+
expect(adapter.bots).toBeInstanceOf(Map)
|
|
82
|
+
expect(adapter.bots.size).toBe(0)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should inherit from EventEmitter', () => {
|
|
86
|
+
expect(adapter).toBeInstanceOf(EventEmitter)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should register message.receive listener', () => {
|
|
90
|
+
const listeners = adapter.listeners('message.receive')
|
|
91
|
+
expect(listeners.length).toBeGreaterThan(0)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should register call.sendMessage listener', () => {
|
|
95
|
+
const listeners = adapter.listeners('call.sendMessage')
|
|
96
|
+
expect(listeners.length).toBeGreaterThan(0)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should register call.recallMessage listener', () => {
|
|
100
|
+
const listeners = adapter.listeners('call.recallMessage')
|
|
101
|
+
expect(listeners.length).toBeGreaterThan(0)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('Adapter Logger', () => {
|
|
106
|
+
it('should get logger from plugin', () => {
|
|
107
|
+
expect(adapter.logger).toBeDefined()
|
|
108
|
+
expect(adapter.logger).toBe(plugin.logger)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should throw error if plugin is not set', () => {
|
|
112
|
+
const adapter = new MockAdapter(plugin, 'test', [])
|
|
113
|
+
adapter.plugin = null as any
|
|
114
|
+
|
|
115
|
+
expect(() => adapter.logger).toThrow('Adapter is not associated with any plugin')
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('Adapter Binding', () => {
|
|
120
|
+
it('should bind to a plugin', () => {
|
|
121
|
+
const newPlugin = new Plugin('/test/new-plugin.ts')
|
|
122
|
+
adapter.binding(newPlugin)
|
|
123
|
+
|
|
124
|
+
expect(adapter.plugin).toBe(newPlugin)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
describe('Adapter Start', () => {
|
|
129
|
+
it('should start without config', async () => {
|
|
130
|
+
await adapter.start()
|
|
131
|
+
expect(plugin.root.adapters).toContain('test')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('should create and connect bots from config', async () => {
|
|
135
|
+
const config = [
|
|
136
|
+
{ id: 'bot1' },
|
|
137
|
+
{ id: 'bot2' }
|
|
138
|
+
]
|
|
139
|
+
const adapter = new MockAdapter(plugin, 'test', config)
|
|
140
|
+
|
|
141
|
+
await adapter.start()
|
|
142
|
+
|
|
143
|
+
expect(adapter.bots.size).toBe(2)
|
|
144
|
+
expect(adapter.bots.has('bot1')).toBe(true)
|
|
145
|
+
expect(adapter.bots.has('bot2')).toBe(true)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should add adapter name to plugin adapters', async () => {
|
|
149
|
+
await adapter.start()
|
|
150
|
+
expect(plugin.root.adapters).toContain('test')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should handle empty config array', async () => {
|
|
154
|
+
const adapter = new MockAdapter(plugin, 'test', [])
|
|
155
|
+
await adapter.start()
|
|
156
|
+
expect(adapter.bots.size).toBe(0)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('Adapter Stop', () => {
|
|
161
|
+
it('should disconnect all bots', async () => {
|
|
162
|
+
const config = [{ id: 'bot1' }, { id: 'bot2' }]
|
|
163
|
+
const adapter = new MockAdapter(plugin, 'test', config)
|
|
164
|
+
|
|
165
|
+
await adapter.start()
|
|
166
|
+
expect(adapter.bots.size).toBe(2)
|
|
167
|
+
|
|
168
|
+
await adapter.stop()
|
|
169
|
+
expect(adapter.bots.size).toBe(0)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should remove adapter from plugin adapters', async () => {
|
|
173
|
+
await adapter.start()
|
|
174
|
+
expect(plugin.root.adapters).toContain('test')
|
|
175
|
+
|
|
176
|
+
await adapter.stop()
|
|
177
|
+
expect(plugin.root.adapters).not.toContain('test')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should remove all event listeners', async () => {
|
|
181
|
+
await adapter.start()
|
|
182
|
+
const beforeCount = adapter.listenerCount('message.receive')
|
|
183
|
+
|
|
184
|
+
await adapter.stop()
|
|
185
|
+
const afterCount = adapter.listenerCount('message.receive')
|
|
186
|
+
|
|
187
|
+
expect(afterCount).toBe(0)
|
|
188
|
+
expect(beforeCount).toBeGreaterThan(0)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should handle bot disconnect errors gracefully', async () => {
|
|
192
|
+
const config = [{ id: 'bot1' }, { id: 'bot2' }]
|
|
193
|
+
const adapter = new MockAdapter(plugin, 'test', config)
|
|
194
|
+
|
|
195
|
+
await adapter.start()
|
|
196
|
+
|
|
197
|
+
// Mock first bot disconnect to throw error
|
|
198
|
+
const bot1 = adapter.bots.get('bot1')!
|
|
199
|
+
bot1.$disconnect = vi.fn().mockRejectedValue(new Error('Disconnect failed'))
|
|
200
|
+
|
|
201
|
+
// Mock logger to spy on error logging
|
|
202
|
+
const loggerSpy = vi.spyOn(adapter.logger, 'error')
|
|
203
|
+
|
|
204
|
+
// The adapter should continue cleanup despite errors
|
|
205
|
+
// Note: Current implementation throws, but this test documents the desired behavior
|
|
206
|
+
// where adapter.stop() should handle errors gracefully and continue cleanup
|
|
207
|
+
await expect(adapter.stop()).rejects.toThrow('Disconnect failed')
|
|
208
|
+
|
|
209
|
+
// Even though it throws, we document that graceful handling would be:
|
|
210
|
+
// - Log the error
|
|
211
|
+
// - Continue disconnecting other bots
|
|
212
|
+
// - Complete cleanup (clear bots, remove from adapters list, remove listeners)
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe('Adapter Events', () => {
|
|
217
|
+
describe('call.recallMessage', () => {
|
|
218
|
+
it('should recall message from bot', async () => {
|
|
219
|
+
const config = [{ id: 'bot1' }]
|
|
220
|
+
const adapter = new MockAdapter(plugin, 'test', config)
|
|
221
|
+
await adapter.start()
|
|
222
|
+
|
|
223
|
+
const bot = adapter.bots.get('bot1')!
|
|
224
|
+
const recallSpy = vi.spyOn(bot, '$recallMessage')
|
|
225
|
+
|
|
226
|
+
await adapter.emit('call.recallMessage', 'bot1', 'message-id')
|
|
227
|
+
|
|
228
|
+
expect(recallSpy).toHaveBeenCalledWith('message-id')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should require valid bot id', () => {
|
|
232
|
+
// 验证 adapter 不包含不存在的 bot
|
|
233
|
+
expect(adapter.bots.has('non-existent-bot')).toBe(false)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe('call.sendMessage', () => {
|
|
238
|
+
it('should send message through bot', async () => {
|
|
239
|
+
const config = [{ id: 'bot1' }]
|
|
240
|
+
const adapter = new MockAdapter(plugin, 'test', config)
|
|
241
|
+
await adapter.start()
|
|
242
|
+
|
|
243
|
+
const bot = adapter.bots.get('bot1')!
|
|
244
|
+
const sendSpy = vi.spyOn(bot, '$sendMessage')
|
|
245
|
+
|
|
246
|
+
const options = {
|
|
247
|
+
context: 'test',
|
|
248
|
+
bot: 'bot1',
|
|
249
|
+
content: 'Hello',
|
|
250
|
+
id: 'channel-id',
|
|
251
|
+
type: 'text' as const
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
await adapter.emit('call.sendMessage', 'bot1', options)
|
|
255
|
+
|
|
256
|
+
expect(sendSpy).toHaveBeenCalledWith(options)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('should validate bot existence before sending', () => {
|
|
260
|
+
// 验证发送消息前应该检查 bot 是否存在
|
|
261
|
+
expect(adapter.bots.has('non-existent-bot')).toBe(false)
|
|
262
|
+
|
|
263
|
+
// 在实际使用中,应该先检查 bot 是否存在
|
|
264
|
+
const botExists = adapter.bots.has('bot1')
|
|
265
|
+
expect(botExists).toBe(false) // 因为还没有 start
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('should call before.sendMessage handlers', async () => {
|
|
269
|
+
const config = [{ id: 'bot1' }]
|
|
270
|
+
const adapter = new MockAdapter(plugin, 'test', config)
|
|
271
|
+
await adapter.start()
|
|
272
|
+
|
|
273
|
+
let handlerCalled = false
|
|
274
|
+
plugin.root.on('before.sendMessage', (options) => {
|
|
275
|
+
handlerCalled = true
|
|
276
|
+
return options
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
const options = {
|
|
280
|
+
context: 'test',
|
|
281
|
+
bot: 'bot1',
|
|
282
|
+
content: 'Hello',
|
|
283
|
+
id: 'channel-id',
|
|
284
|
+
type: 'text' as const
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
await adapter.emit('call.sendMessage', 'bot1', options)
|
|
288
|
+
expect(handlerCalled).toBe(true)
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
describe('message.receive', () => {
|
|
293
|
+
it('should process received message through middleware', async () => {
|
|
294
|
+
const config = [{ id: 'bot1' }]
|
|
295
|
+
const adapter = new MockAdapter(plugin, 'test', config)
|
|
296
|
+
await adapter.start()
|
|
297
|
+
|
|
298
|
+
let middlewareCalled = false
|
|
299
|
+
plugin.addMiddleware(async (message, next) => {
|
|
300
|
+
middlewareCalled = true
|
|
301
|
+
await next()
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
const message = {
|
|
305
|
+
$bot: 'bot1',
|
|
306
|
+
$adapter: 'test',
|
|
307
|
+
$channel: { id: 'channel-id', type: 'text' },
|
|
308
|
+
$content: 'Hello'
|
|
309
|
+
} as any
|
|
310
|
+
|
|
311
|
+
adapter.emit('message.receive', message)
|
|
312
|
+
|
|
313
|
+
// 等待异步处理
|
|
314
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
315
|
+
expect(middlewareCalled).toBe(true)
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
describe('Adapter createBot', () => {
|
|
321
|
+
it('should be abstract method', () => {
|
|
322
|
+
expect(typeof adapter.createBot).toBe('function')
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('should create bot with config', () => {
|
|
326
|
+
const config = { id: 'test-bot' }
|
|
327
|
+
const bot = adapter.createBot(config)
|
|
328
|
+
|
|
329
|
+
expect(bot).toBeInstanceOf(MockBot)
|
|
330
|
+
expect(bot.$id).toBe('test-bot')
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
describe('Adapter Bots Management', () => {
|
|
335
|
+
it('should manage multiple bots', async () => {
|
|
336
|
+
const config = [
|
|
337
|
+
{ id: 'bot1' },
|
|
338
|
+
{ id: 'bot2' },
|
|
339
|
+
{ id: 'bot3' }
|
|
340
|
+
]
|
|
341
|
+
const adapter = new MockAdapter(plugin, 'test', config)
|
|
342
|
+
|
|
343
|
+
await adapter.start()
|
|
344
|
+
|
|
345
|
+
expect(adapter.bots.size).toBe(3)
|
|
346
|
+
expect(Array.from(adapter.bots.keys())).toEqual(['bot1', 'bot2', 'bot3'])
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('should access bot by id', async () => {
|
|
350
|
+
const config = [{ id: 'bot1' }]
|
|
351
|
+
const adapter = new MockAdapter(plugin, 'test', config)
|
|
352
|
+
|
|
353
|
+
await adapter.start()
|
|
354
|
+
|
|
355
|
+
const bot = adapter.bots.get('bot1')
|
|
356
|
+
expect(bot).toBeDefined()
|
|
357
|
+
expect(bot!.$id).toBe('bot1')
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
describe('Adapter Registry', () => {
|
|
363
|
+
it('should have a Registry Map', () => {
|
|
364
|
+
expect(Adapter.Registry).toBeInstanceOf(Map)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('should register adapter factory', () => {
|
|
368
|
+
const factory = MockAdapter as any
|
|
369
|
+
Adapter.register('mock', factory)
|
|
370
|
+
|
|
371
|
+
expect(Adapter.Registry.has('mock')).toBe(true)
|
|
372
|
+
expect(Adapter.Registry.get('mock')).toBe(factory)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('should allow multiple adapter registrations', () => {
|
|
376
|
+
const factory1 = MockAdapter as any
|
|
377
|
+
const factory2 = MockAdapter as any
|
|
378
|
+
|
|
379
|
+
Adapter.register('adapter1', factory1)
|
|
380
|
+
Adapter.register('adapter2', factory2)
|
|
381
|
+
|
|
382
|
+
expect(Adapter.Registry.size).toBeGreaterThanOrEqual(2)
|
|
6
383
|
})
|
|
7
384
|
})
|
package/tests/config.test.ts
CHANGED
|
@@ -1,7 +1,150 @@
|
|
|
1
|
-
import { describe, it, expect } from
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { ConfigLoader } from '../src/built/config'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import path from 'path'
|
|
2
5
|
|
|
3
|
-
describe(
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
describe('ConfigLoader', () => {
|
|
7
|
+
const testConfigPath = path.join(process.cwd(), 'test-config.json')
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
// 清理测试文件
|
|
11
|
+
if (fs.existsSync(testConfigPath)) {
|
|
12
|
+
fs.unlinkSync(testConfigPath)
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('Proxy behavior', () => {
|
|
17
|
+
it('should handle array methods correctly', () => {
|
|
18
|
+
const config = {
|
|
19
|
+
items: ['item1', 'item2', 'item3'],
|
|
20
|
+
bots: [
|
|
21
|
+
{ context: 'sandbox', name: 'bot1' },
|
|
22
|
+
{ context: 'sandbox', name: 'bot2' }
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const loader = new ConfigLoader(testConfigPath, config)
|
|
27
|
+
const proxiedData = loader.data
|
|
28
|
+
|
|
29
|
+
// 测试数组的 map 方法
|
|
30
|
+
expect(() => {
|
|
31
|
+
const mapped = proxiedData.items.map((item: string) => item.toUpperCase())
|
|
32
|
+
expect(mapped).toEqual(['ITEM1', 'ITEM2', 'ITEM3'])
|
|
33
|
+
}).not.toThrow()
|
|
34
|
+
|
|
35
|
+
// 测试数组的 filter 方法
|
|
36
|
+
expect(() => {
|
|
37
|
+
const filtered = proxiedData.bots.filter((bot: any) => bot.name === 'bot1')
|
|
38
|
+
expect(filtered).toHaveLength(1)
|
|
39
|
+
}).not.toThrow()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should handle nested objects', () => {
|
|
43
|
+
const config = {
|
|
44
|
+
database: {
|
|
45
|
+
dialect: 'sqlite',
|
|
46
|
+
filename: './data/bot.db'
|
|
47
|
+
},
|
|
48
|
+
http: {
|
|
49
|
+
port: 8086,
|
|
50
|
+
username: '${username}',
|
|
51
|
+
password: '${password}'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const loader = new ConfigLoader(testConfigPath, config)
|
|
56
|
+
const proxiedData = loader.data
|
|
57
|
+
|
|
58
|
+
expect(proxiedData.database.dialect).toBe('sqlite')
|
|
59
|
+
expect(proxiedData.http.port).toBe(8086)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should resolve environment variables', () => {
|
|
63
|
+
process.env.TEST_VAR = 'test_value'
|
|
64
|
+
|
|
65
|
+
const config = {
|
|
66
|
+
testValue: '${TEST_VAR}'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const loader = new ConfigLoader(testConfigPath, config)
|
|
70
|
+
const proxiedData = loader.data
|
|
71
|
+
|
|
72
|
+
expect(proxiedData.testValue).toBe('test_value')
|
|
73
|
+
|
|
74
|
+
delete process.env.TEST_VAR
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should handle escaped environment variables', () => {
|
|
78
|
+
const config = {
|
|
79
|
+
escapedValue: '\\${NOT_A_VAR}'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const loader = new ConfigLoader(testConfigPath, config)
|
|
83
|
+
const proxiedData = loader.data
|
|
84
|
+
|
|
85
|
+
expect(proxiedData.escapedValue).toBe('${NOT_A_VAR}')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should handle default values for missing env vars', () => {
|
|
89
|
+
const config = {
|
|
90
|
+
valueWithDefault: '${MISSING_VAR:default_value}'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const loader = new ConfigLoader(testConfigPath, config)
|
|
94
|
+
const proxiedData = loader.data
|
|
95
|
+
|
|
96
|
+
expect(proxiedData.valueWithDefault).toBe('default_value')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should not proxy function properties', () => {
|
|
100
|
+
const config = {
|
|
101
|
+
items: [1, 2, 3]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const loader = new ConfigLoader(testConfigPath, config)
|
|
105
|
+
const proxiedData = loader.data
|
|
106
|
+
|
|
107
|
+
// 确保数组方法可以正常调用
|
|
108
|
+
expect(typeof proxiedData.items.map).toBe('function')
|
|
109
|
+
expect(typeof proxiedData.items.filter).toBe('function')
|
|
110
|
+
expect(typeof proxiedData.items.reduce).toBe('function')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should handle Set operations from array', () => {
|
|
114
|
+
const config = {
|
|
115
|
+
bots: [
|
|
116
|
+
{ context: 'sandbox', name: 'bot1' },
|
|
117
|
+
{ context: 'process', name: 'bot2' },
|
|
118
|
+
{ context: 'sandbox', name: 'bot3' }
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const loader = new ConfigLoader(testConfigPath, config)
|
|
123
|
+
const proxiedData = loader.data
|
|
124
|
+
|
|
125
|
+
// 模拟 setup.ts 中的操作
|
|
126
|
+
expect(() => {
|
|
127
|
+
const contexts = new Set(proxiedData.bots.map((bot: any) => bot.context))
|
|
128
|
+
expect(contexts.size).toBe(2)
|
|
129
|
+
expect(contexts.has('sandbox')).toBe(true)
|
|
130
|
+
expect(contexts.has('process')).toBe(true)
|
|
131
|
+
}).not.toThrow()
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('Raw data access', () => {
|
|
136
|
+
it('should provide raw data without proxy', () => {
|
|
137
|
+
const config = {
|
|
138
|
+
value: '${TEST_VAR}'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const loader = new ConfigLoader(testConfigPath, config)
|
|
142
|
+
|
|
143
|
+
// raw 应该返回原始值,不解析环境变量
|
|
144
|
+
expect(loader.raw.value).toBe('${TEST_VAR}')
|
|
145
|
+
|
|
146
|
+
// data 应该尝试解析环境变量
|
|
147
|
+
expect(loader.data.value).toBeTruthy()
|
|
148
|
+
})
|
|
6
149
|
})
|
|
7
150
|
})
|
package/tests/cron.test.ts
CHANGED
|
@@ -274,4 +274,15 @@ describe('Cron定时任务系统测试', () => {
|
|
|
274
274
|
expect(mockCallback).toHaveBeenCalledTimes(1)
|
|
275
275
|
})
|
|
276
276
|
})
|
|
277
|
+
|
|
278
|
+
describe('Cron错误处理', () => {
|
|
279
|
+
it('should throw error for invalid cron expression that cannot determine next run', () => {
|
|
280
|
+
// 创建一个永远不会执行的 cron 表达式
|
|
281
|
+
// 例如:2月30日(不存在的日期)
|
|
282
|
+
expect(() => {
|
|
283
|
+
cron = new Cron('0 0 30 2 *', mockCallback)
|
|
284
|
+
cron.start()
|
|
285
|
+
}).toThrow()
|
|
286
|
+
})
|
|
287
|
+
})
|
|
277
288
|
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import * as jsxRuntime from '../src/jsx-runtime'
|
|
3
|
+
import * as jsxDevRuntime from '../src/jsx-dev-runtime'
|
|
4
|
+
|
|
5
|
+
describe('JSX Runtime', () => {
|
|
6
|
+
it('should export jsx function', () => {
|
|
7
|
+
expect(typeof jsxRuntime.jsx).toBe('function')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('should export jsxs function', () => {
|
|
11
|
+
expect(typeof jsxRuntime.jsxs).toBe('function')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should export Fragment', () => {
|
|
15
|
+
expect(jsxRuntime.Fragment).toBeDefined()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should export renderJSX function', () => {
|
|
19
|
+
expect(typeof jsxRuntime.renderJSX).toBe('function')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should have default export', () => {
|
|
23
|
+
expect(jsxRuntime.default).toBeDefined()
|
|
24
|
+
expect(typeof jsxRuntime.default.jsx).toBe('function')
|
|
25
|
+
expect(typeof jsxRuntime.default.jsxs).toBe('function')
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('JSX Dev Runtime', () => {
|
|
30
|
+
it('should export jsx function', () => {
|
|
31
|
+
expect(typeof jsxDevRuntime.jsx).toBe('function')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should export jsxDEV function', () => {
|
|
35
|
+
expect(typeof jsxDevRuntime.jsxDEV).toBe('function')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should export Fragment', () => {
|
|
39
|
+
expect(jsxDevRuntime.Fragment).toBeDefined()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should export renderJSX function', () => {
|
|
43
|
+
expect(typeof jsxDevRuntime.renderJSX).toBe('function')
|
|
44
|
+
})
|
|
45
|
+
})
|