bingocode 1.0.28 → 1.0.30

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.
Files changed (53) hide show
  1. package/adapters/common/__tests__/chat-queue.test.ts +61 -0
  2. package/adapters/common/__tests__/format.test.ts +148 -0
  3. package/adapters/common/__tests__/http-client.test.ts +105 -0
  4. package/adapters/common/__tests__/message-buffer.test.ts +84 -0
  5. package/adapters/common/__tests__/message-dedup.test.ts +57 -0
  6. package/adapters/common/__tests__/session-store.test.ts +62 -0
  7. package/adapters/common/__tests__/ws-bridge.test.ts +177 -0
  8. package/adapters/common/attachment/__tests__/attachment-limits.test.ts +52 -0
  9. package/adapters/common/attachment/__tests__/attachment-store.test.ts +108 -0
  10. package/adapters/common/attachment/__tests__/image-block-watcher.test.ts +115 -0
  11. package/adapters/feishu/__tests__/card-errors.test.ts +194 -0
  12. package/adapters/feishu/__tests__/cardkit.test.ts +295 -0
  13. package/adapters/feishu/__tests__/extract-payload.test.ts +77 -0
  14. package/adapters/feishu/__tests__/feishu.test.ts +907 -0
  15. package/adapters/feishu/__tests__/flush-controller.test.ts +290 -0
  16. package/adapters/feishu/__tests__/markdown-style.test.ts +353 -0
  17. package/adapters/feishu/__tests__/media.test.ts +120 -0
  18. package/adapters/feishu/__tests__/streaming-card.test.ts +914 -0
  19. package/adapters/telegram/__tests__/media.test.ts +86 -0
  20. package/adapters/telegram/__tests__/telegram.test.ts +115 -0
  21. package/adapters/tsconfig.json +18 -0
  22. package/bunfig.toml +1 -0
  23. package/package.json +1 -1
  24. package/preload.ts +30 -0
  25. package/scripts/count-app-loc.ts +256 -0
  26. package/scripts/release.ts +130 -0
  27. package/src/server/__tests__/conversation-service.test.ts +173 -0
  28. package/src/server/__tests__/conversations.test.ts +458 -0
  29. package/src/server/__tests__/cron-scheduler.test.ts +575 -0
  30. package/src/server/__tests__/e2e/business-flow.test.ts +841 -0
  31. package/src/server/__tests__/e2e/full-flow.test.ts +357 -0
  32. package/src/server/__tests__/fixtures/mock-sdk-cli.ts +123 -0
  33. package/src/server/__tests__/haha-oauth-api.test.ts +146 -0
  34. package/src/server/__tests__/haha-oauth-service.test.ts +185 -0
  35. package/src/server/__tests__/providers-real.test.ts +244 -0
  36. package/src/server/__tests__/providers.test.ts +579 -0
  37. package/src/server/__tests__/proxy-streaming.test.ts +317 -0
  38. package/src/server/__tests__/proxy-transform.test.ts +469 -0
  39. package/src/server/__tests__/real-llm-test.ts +526 -0
  40. package/src/server/__tests__/scheduled-tasks.test.ts +371 -0
  41. package/src/server/__tests__/sessions.test.ts +786 -0
  42. package/src/server/__tests__/settings.test.ts +376 -0
  43. package/src/server/__tests__/skills.test.ts +125 -0
  44. package/src/server/__tests__/tasks.test.ts +171 -0
  45. package/src/server/__tests__/team-watcher.test.ts +400 -0
  46. package/src/server/__tests__/teams.test.ts +627 -0
  47. package/src/server/middleware/cors.test.ts +27 -0
  48. package/src/utils/__tests__/cronFrequency.test.ts +153 -0
  49. package/src/utils/__tests__/cronTasks.test.ts +204 -0
  50. package/src/utils/computerUse/permissions.test.ts +44 -0
  51. package/stubs/ant-claude-for-chrome-mcp.ts +24 -0
  52. package/stubs/color-diff-napi.ts +45 -0
  53. package/tsconfig.json +24 -0
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Unit tests for CronService, SearchService, and Scheduled Tasks API
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach, spyOn } from 'bun:test'
6
+ import * as fs from 'fs/promises'
7
+ import * as path from 'path'
8
+ import * as os from 'os'
9
+ import { CronService } from '../services/cronService.js'
10
+ import { SearchService } from '../services/searchService.js'
11
+
12
+ // ─── Test helpers ───────────────────────────────────────────────────────────
13
+
14
+ let tmpDir: string
15
+ const originalConfigDir = process.env.CLAUDE_CONFIG_DIR
16
+
17
+ async function createTmpDir(): Promise<string> {
18
+ const dir = path.join(os.tmpdir(), `claude-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
19
+ await fs.mkdir(dir, { recursive: true })
20
+ return dir
21
+ }
22
+
23
+ async function cleanupTmpDir(dir: string): Promise<void> {
24
+ try {
25
+ await fs.rm(dir, { recursive: true, force: true })
26
+ } catch {
27
+ // ignore
28
+ }
29
+ }
30
+
31
+ // ─── CronService tests ─────────────────────────────────────────────────────
32
+
33
+ describe('CronService', () => {
34
+ let service: CronService
35
+
36
+ beforeEach(async () => {
37
+ tmpDir = await createTmpDir()
38
+ process.env.CLAUDE_CONFIG_DIR = tmpDir
39
+ service = new CronService()
40
+ })
41
+
42
+ afterEach(async () => {
43
+ if (originalConfigDir) {
44
+ process.env.CLAUDE_CONFIG_DIR = originalConfigDir
45
+ } else {
46
+ delete process.env.CLAUDE_CONFIG_DIR
47
+ }
48
+ await cleanupTmpDir(tmpDir)
49
+ })
50
+
51
+ it('should return empty list when no tasks file exists', async () => {
52
+ const tasks = await service.listTasks()
53
+ expect(tasks).toEqual([])
54
+ })
55
+
56
+ it('should create a task with generated id and createdAt', async () => {
57
+ const task = await service.createTask({
58
+ cron: '0 9 * * *',
59
+ prompt: 'Review commits',
60
+ recurring: true,
61
+ })
62
+
63
+ expect(task.id).toBeDefined()
64
+ expect(task.id).toHaveLength(8) // 4 bytes hex
65
+ expect(task.cron).toBe('0 9 * * *')
66
+ expect(task.prompt).toBe('Review commits')
67
+ expect(task.recurring).toBe(true)
68
+ expect(task.createdAt).toBeGreaterThan(0)
69
+ })
70
+
71
+ it('should persist tasks to file', async () => {
72
+ await service.createTask({ cron: '0 9 * * *', prompt: 'Task 1' })
73
+ await service.createTask({ cron: '30 18 * * 5', prompt: 'Task 2' })
74
+
75
+ const tasks = await service.listTasks()
76
+ expect(tasks).toHaveLength(2)
77
+ expect(tasks[0].prompt).toBe('Task 1')
78
+ expect(tasks[1].prompt).toBe('Task 2')
79
+ })
80
+
81
+ it('should update an existing task', async () => {
82
+ const created = await service.createTask({
83
+ cron: '0 9 * * *',
84
+ prompt: 'Original prompt',
85
+ })
86
+
87
+ const updated = await service.updateTask(created.id, {
88
+ prompt: 'Updated prompt',
89
+ recurring: true,
90
+ })
91
+
92
+ expect(updated.id).toBe(created.id)
93
+ expect(updated.prompt).toBe('Updated prompt')
94
+ expect(updated.recurring).toBe(true)
95
+ expect(updated.createdAt).toBe(created.createdAt)
96
+ })
97
+
98
+ it('should throw when updating a non-existent task', async () => {
99
+ await expect(
100
+ service.updateTask('nonexistent', { prompt: 'x' }),
101
+ ).rejects.toThrow('Task not found')
102
+ })
103
+
104
+ it('should delete a task', async () => {
105
+ const created = await service.createTask({
106
+ cron: '0 9 * * *',
107
+ prompt: 'To delete',
108
+ })
109
+
110
+ await service.deleteTask(created.id)
111
+ const tasks = await service.listTasks()
112
+ expect(tasks).toHaveLength(0)
113
+ })
114
+
115
+ it('should throw when deleting a non-existent task', async () => {
116
+ await expect(service.deleteTask('nonexistent')).rejects.toThrow(
117
+ 'Task not found',
118
+ )
119
+ })
120
+
121
+ it('should generate unique IDs', async () => {
122
+ const ids = new Set<string>()
123
+ for (let i = 0; i < 20; i++) {
124
+ const task = await service.createTask({
125
+ cron: '* * * * *',
126
+ prompt: `Task ${i}`,
127
+ })
128
+ ids.add(task.id)
129
+ }
130
+ expect(ids.size).toBe(20)
131
+ })
132
+
133
+ it('should reject create when cron or prompt is missing', async () => {
134
+ await expect(
135
+ service.createTask({ cron: '', prompt: 'something' }),
136
+ ).rejects.toThrow()
137
+
138
+ await expect(
139
+ service.createTask({ cron: '* * * * *', prompt: '' }),
140
+ ).rejects.toThrow()
141
+ })
142
+
143
+ it('should retry the atomic write when rename returns ENOENT', async () => {
144
+ const originalRename = fs.rename
145
+ let renameCalls = 0
146
+
147
+ const renameSpy = spyOn(fs, 'rename')
148
+ renameSpy.mockImplementation(async (...args) => {
149
+ renameCalls += 1
150
+
151
+ if (renameCalls === 1) {
152
+ const error = new Error(
153
+ 'ENOENT: no such file or directory, rename tmp -> scheduled_tasks.json',
154
+ ) as NodeJS.ErrnoException
155
+ error.code = 'ENOENT'
156
+ throw error
157
+ }
158
+
159
+ return originalRename(...args)
160
+ })
161
+
162
+ try {
163
+ const task = await service.createTask({
164
+ cron: '0 9 * * *',
165
+ prompt: 'Retry rename once',
166
+ })
167
+
168
+ const tasks = await service.listTasks()
169
+ expect(task.id).toBeDefined()
170
+ expect(tasks).toHaveLength(1)
171
+ expect(tasks[0]?.prompt).toBe('Retry rename once')
172
+ expect(renameCalls).toBe(2)
173
+ } finally {
174
+ renameSpy.mockRestore()
175
+ }
176
+ })
177
+ })
178
+
179
+ // ─── SearchService tests ────────────────────────────────────────────────────
180
+
181
+ describe('SearchService', () => {
182
+ let service: SearchService
183
+ let searchDir: string
184
+
185
+ beforeEach(async () => {
186
+ tmpDir = await createTmpDir()
187
+ process.env.CLAUDE_CONFIG_DIR = tmpDir
188
+ service = new SearchService()
189
+
190
+ // 创建搜索用的临时文件
191
+ searchDir = path.join(tmpDir, 'workspace')
192
+ await fs.mkdir(searchDir, { recursive: true })
193
+ await fs.writeFile(
194
+ path.join(searchDir, 'hello.txt'),
195
+ 'Hello World\nThis is a test\nAnother line\n',
196
+ )
197
+ await fs.writeFile(
198
+ path.join(searchDir, 'code.ts'),
199
+ 'function greet() {\n return "hello"\n}\n',
200
+ )
201
+ })
202
+
203
+ afterEach(async () => {
204
+ if (originalConfigDir) {
205
+ process.env.CLAUDE_CONFIG_DIR = originalConfigDir
206
+ } else {
207
+ delete process.env.CLAUDE_CONFIG_DIR
208
+ }
209
+ await cleanupTmpDir(tmpDir)
210
+ })
211
+
212
+ it('should find matches in workspace files', async () => {
213
+ const results = await service.searchWorkspace('Hello', { cwd: searchDir })
214
+ expect(results.length).toBeGreaterThan(0)
215
+ expect(results[0].text).toContain('Hello')
216
+ })
217
+
218
+ it('should return empty results when nothing matches', async () => {
219
+ const results = await service.searchWorkspace('ZZZZNONEXISTENT', {
220
+ cwd: searchDir,
221
+ })
222
+ expect(results).toHaveLength(0)
223
+ })
224
+
225
+ it('should respect maxResults limit', async () => {
226
+ // 写入多行匹配
227
+ const lines = Array.from({ length: 50 }, (_, i) => `match line ${i}`).join(
228
+ '\n',
229
+ )
230
+ await fs.writeFile(path.join(searchDir, 'many.txt'), lines)
231
+
232
+ const results = await service.searchWorkspace('match', {
233
+ cwd: searchDir,
234
+ maxResults: 5,
235
+ })
236
+ expect(results.length).toBeLessThanOrEqual(5)
237
+ })
238
+
239
+ it('should reject empty query', async () => {
240
+ await expect(service.searchWorkspace('')).rejects.toThrow()
241
+ })
242
+
243
+ it('should return empty session results when no projects dir exists', async () => {
244
+ const results = await service.searchSessions('test')
245
+ expect(results).toEqual([])
246
+ })
247
+ })
248
+
249
+ // ─── Scheduled Tasks API integration ────────────────────────────────────────
250
+
251
+ describe('Scheduled Tasks API', () => {
252
+ // 直接测试 handler 函数,不需要启动完整服务器
253
+ let handleScheduledTasksApi: (
254
+ req: Request,
255
+ url: URL,
256
+ segments: string[],
257
+ ) => Promise<Response>
258
+
259
+ beforeEach(async () => {
260
+ tmpDir = await createTmpDir()
261
+ process.env.CLAUDE_CONFIG_DIR = tmpDir
262
+
263
+ // 动态导入以获取最新的环境变量
264
+ const mod = await import('../api/scheduled-tasks.js')
265
+ handleScheduledTasksApi = mod.handleScheduledTasksApi
266
+ })
267
+
268
+ afterEach(async () => {
269
+ if (originalConfigDir) {
270
+ process.env.CLAUDE_CONFIG_DIR = originalConfigDir
271
+ } else {
272
+ delete process.env.CLAUDE_CONFIG_DIR
273
+ }
274
+ await cleanupTmpDir(tmpDir)
275
+ })
276
+
277
+ it('should list empty tasks via GET', async () => {
278
+ const req = new Request('http://localhost/api/scheduled-tasks', {
279
+ method: 'GET',
280
+ })
281
+ const url = new URL(req.url)
282
+ const resp = await handleScheduledTasksApi(req, url, [
283
+ 'api',
284
+ 'scheduled-tasks',
285
+ ])
286
+ const body = (await resp.json()) as { tasks: unknown[] }
287
+ expect(resp.status).toBe(200)
288
+ expect(body.tasks).toEqual([])
289
+ })
290
+
291
+ it('should create a task via POST', async () => {
292
+ const req = new Request('http://localhost/api/scheduled-tasks', {
293
+ method: 'POST',
294
+ headers: { 'Content-Type': 'application/json' },
295
+ body: JSON.stringify({
296
+ cron: '0 9 * * *',
297
+ prompt: 'Daily review',
298
+ recurring: true,
299
+ }),
300
+ })
301
+ const url = new URL(req.url)
302
+ const resp = await handleScheduledTasksApi(req, url, [
303
+ 'api',
304
+ 'scheduled-tasks',
305
+ ])
306
+ const body = (await resp.json()) as { task: { id: string; prompt: string } }
307
+ expect(resp.status).toBe(201)
308
+ expect(body.task.id).toBeDefined()
309
+ expect(body.task.prompt).toBe('Daily review')
310
+ })
311
+
312
+ it('should CRUD a full lifecycle', async () => {
313
+ // Create
314
+ const createReq = new Request('http://localhost/api/scheduled-tasks', {
315
+ method: 'POST',
316
+ headers: { 'Content-Type': 'application/json' },
317
+ body: JSON.stringify({ cron: '0 9 * * *', prompt: 'Test task' }),
318
+ })
319
+ const createResp = await handleScheduledTasksApi(
320
+ createReq,
321
+ new URL(createReq.url),
322
+ ['api', 'scheduled-tasks'],
323
+ )
324
+ const { task } = (await createResp.json()) as {
325
+ task: { id: string; prompt: string }
326
+ }
327
+
328
+ // Update
329
+ const updateReq = new Request(
330
+ `http://localhost/api/scheduled-tasks/${task.id}`,
331
+ {
332
+ method: 'PUT',
333
+ headers: { 'Content-Type': 'application/json' },
334
+ body: JSON.stringify({ prompt: 'Updated task' }),
335
+ },
336
+ )
337
+ const updateResp = await handleScheduledTasksApi(
338
+ updateReq,
339
+ new URL(updateReq.url),
340
+ ['api', 'scheduled-tasks', task.id],
341
+ )
342
+ const updated = (await updateResp.json()) as {
343
+ task: { id: string; prompt: string }
344
+ }
345
+ expect(updated.task.prompt).toBe('Updated task')
346
+
347
+ // Delete
348
+ const deleteReq = new Request(
349
+ `http://localhost/api/scheduled-tasks/${task.id}`,
350
+ { method: 'DELETE' },
351
+ )
352
+ const deleteResp = await handleScheduledTasksApi(
353
+ deleteReq,
354
+ new URL(deleteReq.url),
355
+ ['api', 'scheduled-tasks', task.id],
356
+ )
357
+ expect(deleteResp.status).toBe(200)
358
+
359
+ // Verify empty
360
+ const listReq = new Request('http://localhost/api/scheduled-tasks', {
361
+ method: 'GET',
362
+ })
363
+ const listResp = await handleScheduledTasksApi(
364
+ listReq,
365
+ new URL(listReq.url),
366
+ ['api', 'scheduled-tasks'],
367
+ )
368
+ const list = (await listResp.json()) as { tasks: unknown[] }
369
+ expect(list.tasks).toHaveLength(0)
370
+ })
371
+ })