bingocode 1.0.27 → 1.0.29

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 (54) hide show
  1. package/package.json +1 -2
  2. package/.github/FUNDING.yml +0 -1
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -44
  4. package/.github/ISSUE_TEMPLATE/config.yml +0 -1
  5. package/.github/ISSUE_TEMPLATE/question.md +0 -40
  6. package/.github/workflows/build-desktop-dev.yml +0 -210
  7. package/.github/workflows/deploy-docs.yml +0 -59
  8. package/.github/workflows/release-desktop.yml +0 -162
  9. package/.spine/user.yaml +0 -5
  10. package/.spine/workspace.yaml +0 -1
  11. package/adapters/common/__tests__/chat-queue.test.ts +0 -61
  12. package/adapters/common/__tests__/format.test.ts +0 -148
  13. package/adapters/common/__tests__/http-client.test.ts +0 -105
  14. package/adapters/common/__tests__/message-buffer.test.ts +0 -84
  15. package/adapters/common/__tests__/message-dedup.test.ts +0 -57
  16. package/adapters/common/__tests__/session-store.test.ts +0 -62
  17. package/adapters/common/__tests__/ws-bridge.test.ts +0 -177
  18. package/adapters/common/attachment/__tests__/attachment-limits.test.ts +0 -52
  19. package/adapters/common/attachment/__tests__/attachment-store.test.ts +0 -108
  20. package/adapters/common/attachment/__tests__/image-block-watcher.test.ts +0 -115
  21. package/adapters/feishu/__tests__/card-errors.test.ts +0 -194
  22. package/adapters/feishu/__tests__/cardkit.test.ts +0 -295
  23. package/adapters/feishu/__tests__/extract-payload.test.ts +0 -77
  24. package/adapters/feishu/__tests__/feishu.test.ts +0 -907
  25. package/adapters/feishu/__tests__/flush-controller.test.ts +0 -290
  26. package/adapters/feishu/__tests__/markdown-style.test.ts +0 -353
  27. package/adapters/feishu/__tests__/media.test.ts +0 -120
  28. package/adapters/feishu/__tests__/streaming-card.test.ts +0 -914
  29. package/adapters/telegram/__tests__/media.test.ts +0 -86
  30. package/adapters/telegram/__tests__/telegram.test.ts +0 -115
  31. package/src/server/__tests__/conversation-service.test.ts +0 -173
  32. package/src/server/__tests__/conversations.test.ts +0 -458
  33. package/src/server/__tests__/cron-scheduler.test.ts +0 -575
  34. package/src/server/__tests__/e2e/business-flow.test.ts +0 -841
  35. package/src/server/__tests__/e2e/full-flow.test.ts +0 -357
  36. package/src/server/__tests__/fixtures/mock-sdk-cli.ts +0 -123
  37. package/src/server/__tests__/haha-oauth-api.test.ts +0 -146
  38. package/src/server/__tests__/haha-oauth-service.test.ts +0 -185
  39. package/src/server/__tests__/providers-real.test.ts +0 -244
  40. package/src/server/__tests__/providers.test.ts +0 -579
  41. package/src/server/__tests__/proxy-streaming.test.ts +0 -317
  42. package/src/server/__tests__/proxy-transform.test.ts +0 -469
  43. package/src/server/__tests__/real-llm-test.ts +0 -526
  44. package/src/server/__tests__/scheduled-tasks.test.ts +0 -371
  45. package/src/server/__tests__/sessions.test.ts +0 -786
  46. package/src/server/__tests__/settings.test.ts +0 -376
  47. package/src/server/__tests__/skills.test.ts +0 -125
  48. package/src/server/__tests__/tasks.test.ts +0 -171
  49. package/src/server/__tests__/team-watcher.test.ts +0 -400
  50. package/src/server/__tests__/teams.test.ts +0 -627
  51. package/src/server/middleware/cors.test.ts +0 -27
  52. package/src/utils/__tests__/cronFrequency.test.ts +0 -153
  53. package/src/utils/__tests__/cronTasks.test.ts +0 -204
  54. package/src/utils/computerUse/permissions.test.ts +0 -44
@@ -1,400 +0,0 @@
1
- /**
2
- * Unit tests for TeamWatcher — real-time team status push via WebSocket
3
- */
4
-
5
- import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test'
6
- import * as fs from 'node:fs/promises'
7
- import * as fsSyn from 'node:fs'
8
- import * as path from 'node:path'
9
- import * as os from 'node:os'
10
- import type { TeamMemberStatus } from '../ws/events.js'
11
-
12
- // ============================================================================
13
- // Test helpers
14
- // ============================================================================
15
-
16
- let tmpDir: string
17
-
18
- async function setupTmpConfigDir(): Promise<string> {
19
- tmpDir = path.join(
20
- os.tmpdir(),
21
- `claude-watcher-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
22
- )
23
- await fs.mkdir(path.join(tmpDir, 'teams'), { recursive: true })
24
- process.env.CLAUDE_CONFIG_DIR = tmpDir
25
- return tmpDir
26
- }
27
-
28
- async function cleanupTmpDir(): Promise<void> {
29
- if (tmpDir) {
30
- await fs.rm(tmpDir, { recursive: true, force: true })
31
- }
32
- delete process.env.CLAUDE_CONFIG_DIR
33
- }
34
-
35
- /** Write a team config.json to the temp directory. */
36
- async function writeTeamConfig(
37
- teamName: string,
38
- config: Record<string, unknown>,
39
- ): Promise<string> {
40
- const teamDir = path.join(tmpDir, 'teams', teamName)
41
- await fs.mkdir(teamDir, { recursive: true })
42
- const configPath = path.join(teamDir, 'config.json')
43
- await fs.writeFile(configPath, JSON.stringify(config), 'utf-8')
44
- return configPath
45
- }
46
-
47
- /** Create a standard team config for testing. */
48
- function makeTeamConfig(overrides?: Record<string, unknown>) {
49
- return {
50
- name: 'test-team',
51
- description: 'A test team',
52
- createdAt: 1700000000000,
53
- leadAgentId: 'agent-lead',
54
- members: [
55
- {
56
- agentId: 'agent-lead',
57
- name: 'Lead Agent',
58
- agentType: 'lead',
59
- model: 'claude-opus-4-7',
60
- color: '#ff0000',
61
- joinedAt: 1700000000000,
62
- tmuxPaneId: '%0',
63
- cwd: '/tmp/project',
64
- sessionId: 'session-lead-001',
65
- isActive: true,
66
- },
67
- {
68
- agentId: 'agent-worker',
69
- name: 'Worker Agent',
70
- agentType: 'worker',
71
- model: 'claude-sonnet-4-20250514',
72
- color: '#00ff00',
73
- joinedAt: 1700000001000,
74
- tmuxPaneId: '%1',
75
- cwd: '/tmp/project/src',
76
- sessionId: 'session-worker-001',
77
- isActive: false,
78
- },
79
- ],
80
- ...overrides,
81
- }
82
- }
83
-
84
- // ============================================================================
85
- // Mock the WebSocket handler exports
86
- // ============================================================================
87
-
88
- // Track all messages sent via broadcast
89
- let broadcastedMessages: Array<{ sessionId: string; message: unknown }> = []
90
- let mockActiveSessionIds: string[] = []
91
-
92
- // We need to mock the handler module before importing TeamWatcher
93
- // Use Bun's module mock
94
- const mockSendToSession = mock((sessionId: string, message: unknown) => {
95
- broadcastedMessages.push({ sessionId, message })
96
- return true
97
- })
98
-
99
- const mockGetActiveSessionIds = mock(() => {
100
- return mockActiveSessionIds
101
- })
102
-
103
- // Mock the handler module
104
- import { TeamWatcher } from '../services/teamWatcher.js'
105
-
106
- // Since TeamWatcher imports from handler.js at the module level, we need to
107
- // test using the class directly and override the broadcast behavior.
108
- // Instead, we test extractMemberStatuses directly and test the integration
109
- // by verifying the check cycle behavior via a wrapper approach.
110
-
111
- // ============================================================================
112
- // TeamWatcher.extractMemberStatuses tests
113
- // ============================================================================
114
-
115
- describe('TeamWatcher.extractMemberStatuses', () => {
116
- let watcher: TeamWatcher
117
-
118
- beforeEach(() => {
119
- watcher = new TeamWatcher()
120
- })
121
-
122
- it('should extract member statuses from a valid config', () => {
123
- const config = makeTeamConfig()
124
- const statuses = watcher.extractMemberStatuses(config)
125
-
126
- expect(statuses).toHaveLength(2)
127
- expect(statuses[0]).toEqual({
128
- agentId: 'agent-lead',
129
- role: 'Lead Agent',
130
- status: 'running',
131
- currentTask: undefined,
132
- })
133
- expect(statuses[1]).toEqual({
134
- agentId: 'agent-worker',
135
- role: 'Worker Agent',
136
- status: 'idle',
137
- currentTask: undefined,
138
- })
139
- })
140
-
141
- it('should return running status when isActive is undefined', () => {
142
- const config = makeTeamConfig()
143
- delete (config.members[0] as Record<string, unknown>).isActive
144
- const statuses = watcher.extractMemberStatuses(config)
145
-
146
- expect(statuses[0]!.status).toBe('running')
147
- })
148
-
149
- it('should return idle status when isActive is false', () => {
150
- const config = makeTeamConfig()
151
- const statuses = watcher.extractMemberStatuses(config)
152
-
153
- expect(statuses[1]!.status).toBe('idle')
154
- })
155
-
156
- it('should prefer member name as role when present', () => {
157
- const config = makeTeamConfig()
158
- const statuses = watcher.extractMemberStatuses(config)
159
-
160
- expect(statuses[0]!.role).toBe('Lead Agent')
161
- expect(statuses[1]!.role).toBe('Worker Agent')
162
- })
163
-
164
- it('should fall back to name when agentType is missing', () => {
165
- const config = makeTeamConfig()
166
- delete (config.members[0] as Record<string, unknown>).agentType
167
- const statuses = watcher.extractMemberStatuses(config)
168
-
169
- expect(statuses[0]!.role).toBe('Lead Agent')
170
- })
171
-
172
- it('should fall back to "member" when both agentType and name are missing', () => {
173
- const config = makeTeamConfig()
174
- delete (config.members[0] as Record<string, unknown>).agentType
175
- delete (config.members[0] as Record<string, unknown>).name
176
- const statuses = watcher.extractMemberStatuses(config)
177
-
178
- expect(statuses[0]!.role).toBe('member')
179
- })
180
-
181
- it('should return empty array when config has no members', () => {
182
- const config = { name: 'empty-team' }
183
- const statuses = watcher.extractMemberStatuses(config)
184
- expect(statuses).toEqual([])
185
- })
186
-
187
- it('should return empty array when members is not an array', () => {
188
- const config = { name: 'bad-team', members: 'not-an-array' }
189
- const statuses = watcher.extractMemberStatuses(config)
190
- expect(statuses).toEqual([])
191
- })
192
-
193
- it('should include currentTask when present in config', () => {
194
- const config = makeTeamConfig()
195
- ;(config.members[0] as Record<string, unknown>).currentTask = 'Implementing feature X'
196
- const statuses = watcher.extractMemberStatuses(config)
197
-
198
- expect(statuses[0]!.currentTask).toBe('Implementing feature X')
199
- })
200
- })
201
-
202
- // ============================================================================
203
- // TeamWatcher polling integration tests
204
- // ============================================================================
205
-
206
- describe('TeamWatcher polling', () => {
207
- let watcher: TeamWatcher
208
-
209
- beforeEach(async () => {
210
- await setupTmpConfigDir()
211
- watcher = new TeamWatcher()
212
- })
213
-
214
- afterEach(async () => {
215
- watcher.stop()
216
- watcher.reset()
217
- await cleanupTmpDir()
218
- })
219
-
220
- it('should detect new team creation via checkNow()', async () => {
221
- // First poll: no teams
222
- watcher.checkNow()
223
-
224
- // Create a team
225
- await writeTeamConfig('new-team', makeTeamConfig({ name: 'new-team' }))
226
-
227
- // The watcher internally calls broadcast which calls sendToSession.
228
- // Since sendToSession depends on active sessions, we test that the
229
- // internal snapshot state is updated correctly.
230
- // After checkNow, the watcher should have recorded the team.
231
- watcher.checkNow()
232
-
233
- // Now modify the team config and check again -- this proves the previous
234
- // checkNow() recorded the snapshot (otherwise it would emit team_created again)
235
- const updatedConfig = makeTeamConfig({ name: 'new-team', description: 'updated' })
236
- await writeTeamConfig('new-team', updatedConfig)
237
- watcher.checkNow()
238
-
239
- // If we got here without errors, the snapshot logic is working
240
- })
241
-
242
- it('should detect team config changes', async () => {
243
- // Create initial team
244
- await writeTeamConfig('change-team', makeTeamConfig({ name: 'change-team' }))
245
-
246
- // First poll picks up the team
247
- watcher.checkNow()
248
-
249
- // Modify the config
250
- const updatedConfig = makeTeamConfig({
251
- name: 'change-team',
252
- description: 'updated description',
253
- })
254
- await writeTeamConfig('change-team', updatedConfig)
255
-
256
- // Second poll should detect the change
257
- watcher.checkNow()
258
- // No error means the diff detection worked
259
- })
260
-
261
- it('should detect team deletion', async () => {
262
- // Create a team
263
- await writeTeamConfig('doomed-team', makeTeamConfig({ name: 'doomed-team' }))
264
-
265
- // First poll picks it up
266
- watcher.checkNow()
267
-
268
- // Delete the team directory
269
- await fs.rm(path.join(tmpDir, 'teams', 'doomed-team'), { recursive: true, force: true })
270
-
271
- // Next poll should detect deletion
272
- watcher.checkNow()
273
- // If no error, deletion detection worked
274
- })
275
-
276
- it('should handle missing teams directory gracefully', async () => {
277
- // Remove the entire teams directory
278
- await fs.rm(path.join(tmpDir, 'teams'), { recursive: true, force: true })
279
-
280
- // Should not throw
281
- watcher.checkNow()
282
- })
283
-
284
- it('should handle malformed config.json gracefully', async () => {
285
- // Create a team with invalid JSON
286
- const teamDir = path.join(tmpDir, 'teams', 'bad-json')
287
- await fs.mkdir(teamDir, { recursive: true })
288
- await fs.writeFile(path.join(teamDir, 'config.json'), 'not valid json', 'utf-8')
289
-
290
- // Should not throw
291
- watcher.checkNow()
292
- })
293
-
294
- it('should skip directories without config.json', async () => {
295
- // Create a directory with no config.json
296
- const teamDir = path.join(tmpDir, 'teams', 'no-config')
297
- await fs.mkdir(teamDir, { recursive: true })
298
-
299
- // Should not throw
300
- watcher.checkNow()
301
- })
302
-
303
- it('should track multiple teams independently', async () => {
304
- await writeTeamConfig('team-a', makeTeamConfig({ name: 'team-a' }))
305
- await writeTeamConfig('team-b', makeTeamConfig({ name: 'team-b' }))
306
-
307
- // Pick up both teams
308
- watcher.checkNow()
309
-
310
- // Modify only team-a
311
- await writeTeamConfig('team-a', makeTeamConfig({ name: 'team-a', description: 'changed' }))
312
-
313
- // Should detect change in team-a but not team-b
314
- watcher.checkNow()
315
-
316
- // Delete only team-b
317
- await fs.rm(path.join(tmpDir, 'teams', 'team-b'), { recursive: true, force: true })
318
-
319
- watcher.checkNow()
320
- // No errors means independent tracking works
321
- })
322
-
323
- it('should start and stop polling without errors', async () => {
324
- // Start with a short interval
325
- watcher.start(50)
326
-
327
- // Let it run a couple of cycles
328
- await new Promise((resolve) => setTimeout(resolve, 150))
329
-
330
- // Stop
331
- watcher.stop()
332
-
333
- // Starting again should work
334
- watcher.start(50)
335
- watcher.stop()
336
- })
337
-
338
- it('should not start duplicate intervals when start() called twice', async () => {
339
- watcher.start(100)
340
- watcher.start(100) // second call should be a no-op
341
-
342
- // Let it run briefly
343
- await new Promise((resolve) => setTimeout(resolve, 50))
344
-
345
- watcher.stop()
346
- })
347
-
348
- it('should handle teams directory appearing after initial check', async () => {
349
- // Remove teams dir
350
- await fs.rm(path.join(tmpDir, 'teams'), { recursive: true, force: true })
351
-
352
- // First check -- no teams dir
353
- watcher.checkNow()
354
-
355
- // Create teams dir and a team
356
- await fs.mkdir(path.join(tmpDir, 'teams'), { recursive: true })
357
- await writeTeamConfig('late-team', makeTeamConfig({ name: 'late-team' }))
358
-
359
- // Second check should pick it up
360
- watcher.checkNow()
361
- })
362
-
363
- it('reset() should clear internal state', async () => {
364
- await writeTeamConfig('reset-team', makeTeamConfig({ name: 'reset-team' }))
365
-
366
- // Pick up the team
367
- watcher.checkNow()
368
-
369
- // Reset and check again -- should treat it as new
370
- watcher.reset()
371
- watcher.checkNow()
372
- // No error means reset worked
373
- })
374
- })
375
-
376
- // ============================================================================
377
- // Broadcast integration tests
378
- // ============================================================================
379
-
380
- describe('TeamWatcher broadcast', () => {
381
- it('should call sendToSession for each active session', async () => {
382
- // This test verifies the broadcast logic by importing the real module
383
- // and checking that getActiveSessionIds/sendToSession are called.
384
- // Since the handler module manages real WebSocket state, we verify
385
- // that when there are no active sessions, broadcast is a no-op.
386
-
387
- await setupTmpConfigDir()
388
- const watcher = new TeamWatcher()
389
-
390
- await writeTeamConfig('broadcast-team', makeTeamConfig({ name: 'broadcast-team' }))
391
-
392
- // With no active WebSocket sessions, checkNow should still succeed
393
- // (broadcast sends to zero sessions)
394
- watcher.checkNow()
395
-
396
- watcher.stop()
397
- watcher.reset()
398
- await cleanupTmpDir()
399
- })
400
- })