bingocode 1.0.29 → 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 (45) 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/package.json +1 -1
  22. package/src/server/__tests__/conversation-service.test.ts +173 -0
  23. package/src/server/__tests__/conversations.test.ts +458 -0
  24. package/src/server/__tests__/cron-scheduler.test.ts +575 -0
  25. package/src/server/__tests__/e2e/business-flow.test.ts +841 -0
  26. package/src/server/__tests__/e2e/full-flow.test.ts +357 -0
  27. package/src/server/__tests__/fixtures/mock-sdk-cli.ts +123 -0
  28. package/src/server/__tests__/haha-oauth-api.test.ts +146 -0
  29. package/src/server/__tests__/haha-oauth-service.test.ts +185 -0
  30. package/src/server/__tests__/providers-real.test.ts +244 -0
  31. package/src/server/__tests__/providers.test.ts +579 -0
  32. package/src/server/__tests__/proxy-streaming.test.ts +317 -0
  33. package/src/server/__tests__/proxy-transform.test.ts +469 -0
  34. package/src/server/__tests__/real-llm-test.ts +526 -0
  35. package/src/server/__tests__/scheduled-tasks.test.ts +371 -0
  36. package/src/server/__tests__/sessions.test.ts +786 -0
  37. package/src/server/__tests__/settings.test.ts +376 -0
  38. package/src/server/__tests__/skills.test.ts +125 -0
  39. package/src/server/__tests__/tasks.test.ts +171 -0
  40. package/src/server/__tests__/team-watcher.test.ts +400 -0
  41. package/src/server/__tests__/teams.test.ts +627 -0
  42. package/src/server/middleware/cors.test.ts +27 -0
  43. package/src/utils/__tests__/cronFrequency.test.ts +153 -0
  44. package/src/utils/__tests__/cronTasks.test.ts +204 -0
  45. package/src/utils/computerUse/permissions.test.ts +44 -0
@@ -0,0 +1,153 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ frequencyToCron,
4
+ cronToFrequency,
5
+ FREQUENCY_OPTIONS,
6
+ } from '../cronFrequency.js'
7
+
8
+ describe('frequencyToCron', () => {
9
+ test('manual returns empty string', () => {
10
+ expect(frequencyToCron('manual')).toBe('')
11
+ expect(frequencyToCron('manual', '09:00')).toBe('')
12
+ })
13
+
14
+ test('hourly returns minute-only cron', () => {
15
+ expect(frequencyToCron('hourly')).toBe('0 * * * *')
16
+ expect(frequencyToCron('hourly', '15')).toBe('') // malformed: must be HH:MM
17
+ expect(frequencyToCron('hourly', '0 * * * *')).toBe('') // malformed time
18
+ expect(frequencyToCron('hourly', ':15')).toBe('') // malformed
19
+ expect(frequencyToCron('hourly', '00:15')).toBe('15 * * * *')
20
+ })
21
+
22
+ test('daily returns correct cron', () => {
23
+ expect(frequencyToCron('daily', '09:00')).toBe('0 9 * * *')
24
+ expect(frequencyToCron('daily', '14:30')).toBe('30 14 * * *')
25
+ })
26
+
27
+ test('weekdays returns cron with 1-5 dow', () => {
28
+ expect(frequencyToCron('weekdays', '09:00')).toBe('0 9 * * 1-5')
29
+ expect(frequencyToCron('weekdays', '08:30')).toBe('30 8 * * 1-5')
30
+ })
31
+
32
+ test('weekly returns cron with dow=1', () => {
33
+ expect(frequencyToCron('weekly', '09:00')).toBe('0 9 * * 1')
34
+ expect(frequencyToCron('weekly', '17:00')).toBe('0 17 * * 1')
35
+ })
36
+
37
+ test('rejects invalid time formats', () => {
38
+ expect(frequencyToCron('daily', '')).toBe('')
39
+ expect(frequencyToCron('daily', '25:00')).toBe('')
40
+ expect(frequencyToCron('daily', '09:60')).toBe('')
41
+ expect(frequencyToCron('daily', 'abc')).toBe('')
42
+ expect(frequencyToCron('daily', '9')).toBe('')
43
+ expect(frequencyToCron('daily', ':30')).toBe('')
44
+ })
45
+
46
+ test('defaults to 09:00 for missing time', () => {
47
+ expect(frequencyToCron('daily')).toBe('0 9 * * *')
48
+ expect(frequencyToCron('weekdays')).toBe('0 9 * * 1-5')
49
+ expect(frequencyToCron('weekly')).toBe('0 9 * * 1')
50
+ })
51
+ })
52
+
53
+ describe('cronToFrequency', () => {
54
+ test('empty cron returns manual', () => {
55
+ expect(cronToFrequency('')).toEqual({ frequency: 'manual' })
56
+ expect(cronToFrequency(' ')).toEqual({ frequency: 'manual' })
57
+ })
58
+
59
+ test('invalid cron returns manual', () => {
60
+ expect(cronToFrequency('not a cron')).toEqual({ frequency: 'manual' })
61
+ expect(cronToFrequency('*')).toEqual({ frequency: 'manual' })
62
+ expect(cronToFrequency('* *')).toEqual({ frequency: 'manual' })
63
+ })
64
+
65
+ test('hourly pattern detected', () => {
66
+ expect(cronToFrequency('7 * * * *')).toEqual({ frequency: 'hourly' })
67
+ expect(cronToFrequency('0 * * * *')).toEqual({ frequency: 'hourly' })
68
+ expect(cronToFrequency('30 * * * *')).toEqual({ frequency: 'hourly' })
69
+ })
70
+
71
+ test('daily pattern detected', () => {
72
+ expect(cronToFrequency('0 9 * * *')).toEqual({ frequency: 'daily', time: '09:00' })
73
+ expect(cronToFrequency('30 14 * * *')).toEqual({ frequency: 'daily', time: '14:30' })
74
+ })
75
+
76
+ test('weekdays pattern detected', () => {
77
+ expect(cronToFrequency('0 9 * * 1-5')).toEqual({ frequency: 'weekdays', time: '09:00' })
78
+ expect(cronToFrequency('30 8 * * 1-5')).toEqual({ frequency: 'weekdays', time: '08:30' })
79
+ })
80
+
81
+ test('weekly pattern detected', () => {
82
+ expect(cronToFrequency('0 9 * * 1')).toEqual({ frequency: 'weekly', time: '09:00' })
83
+ expect(cronToFrequency('0 17 * * 1')).toEqual({ frequency: 'weekly', time: '17:00' })
84
+ })
85
+
86
+ test('complex crons return manual', () => {
87
+ expect(cronToFrequency('0 9 1-15 * *')).toEqual({ frequency: 'manual' })
88
+ expect(cronToFrequency('0 9 * * 0,6')).toEqual({ frequency: 'manual' })
89
+ expect(cronToFrequency('0 9 * 1,2 *')).toEqual({ frequency: 'manual' })
90
+ expect(cronToFrequency('0 */2 * * *')).toEqual({ frequency: 'manual' })
91
+ })
92
+
93
+ test('hourly when minute is step but hour is wildcard', () => {
94
+ // */5 in minute + * in hour matches the first branch: hour === '*' && all wildcards
95
+ expect(cronToFrequency('*/5 * * * *')).toEqual({ frequency: 'hourly' })
96
+ })
97
+
98
+ test('out-of-range values return manual', () => {
99
+ expect(cronToFrequency('60 25 * * *')).toEqual({ frequency: 'manual' })
100
+ expect(cronToFrequency('99 99 * * *')).toEqual({ frequency: 'manual' })
101
+ })
102
+
103
+ test('pads hour and minute correctly', () => {
104
+ expect(cronToFrequency('5 9 * * *')).toEqual({ frequency: 'daily', time: '09:05' })
105
+ expect(cronToFrequency('30 0 * * *')).toEqual({ frequency: 'daily', time: '00:30' })
106
+ })
107
+ })
108
+
109
+ describe('FREQUENCY_OPTIONS', () => {
110
+ test('contains all expected frequencies', () => {
111
+ const values = FREQUENCY_OPTIONS.map(o => o.value)
112
+ expect(values).toContain('manual')
113
+ expect(values).toContain('hourly')
114
+ expect(values).toContain('daily')
115
+ expect(values).toContain('weekdays')
116
+ expect(values).toContain('weekly')
117
+ })
118
+
119
+ test('all have labels', () => {
120
+ for (const opt of FREQUENCY_OPTIONS) {
121
+ expect(typeof opt.label).toBe('string')
122
+ expect(opt.label.length).toBeGreaterThan(0)
123
+ }
124
+ })
125
+ })
126
+
127
+ describe('round-trip: frequency → cron → frequency', () => {
128
+ test('daily round-trip preserves data', () => {
129
+ const cron = frequencyToCron('daily', '09:00')
130
+ const result = cronToFrequency(cron)
131
+ expect(result).toEqual({ frequency: 'daily', time: '09:00' })
132
+ })
133
+
134
+ test('weekdays round-trip preserves data', () => {
135
+ const cron = frequencyToCron('weekdays', '08:30')
136
+ const result = cronToFrequency(cron)
137
+ expect(result).toEqual({ frequency: 'weekdays', time: '08:30' })
138
+ })
139
+
140
+ test('weekly round-trip preserves data', () => {
141
+ const cron = frequencyToCron('weekly', '17:00')
142
+ const result = cronToFrequency(cron)
143
+ expect(result).toEqual({ frequency: 'weekly', time: '17:00' })
144
+ })
145
+
146
+ test('hourly round-trip preserves minute', () => {
147
+ // frequencyToCron('hourly', '00:15') produces "15 * * * *"
148
+ // cronToFrequency('15 * * * *') returns 'hourly' (minute is numeric, hour is '*')
149
+ const cron = frequencyToCron('hourly', '00:15')
150
+ const result = cronToFrequency(cron)
151
+ expect(result).toEqual({ frequency: 'hourly' })
152
+ })
153
+ })
@@ -0,0 +1,204 @@
1
+ import { describe, expect, test, beforeEach, afterEach } from 'bun:test'
2
+ import { mkdir, writeFile, rm } from 'fs/promises'
3
+ import { join } from 'path'
4
+ import { randomUUID } from 'crypto'
5
+
6
+ // We'll test the updateCronTask by directly exercising the exported functions
7
+ // through a temporary directory approach.
8
+ // Note: These are integration tests that use actual filesystem operations.
9
+
10
+ describe('updateCronTask integration', () => {
11
+ const tmpDir = join('/tmp', `cron-test-${randomUUID().slice(0, 8)}`)
12
+
13
+ beforeEach(async () => {
14
+ // Create temp project structure
15
+ await mkdir(join(tmpDir, '.claude'), { recursive: true })
16
+ })
17
+
18
+ afterEach(async () => {
19
+ await rm(tmpDir, { recursive: true, force: true })
20
+ })
21
+
22
+ test('CRON_FILE_REL constant is correct', async () => {
23
+ // Import and verify the file relative path
24
+ const { getCronFilePath } = await import('../cronTasks.js')
25
+ const filePath = getCronFilePath(tmpDir)
26
+ expect(filePath).toContain('.claude')
27
+ expect(filePath).toContain('scheduled_tasks.json')
28
+ })
29
+
30
+ test('getCronFilePath returns correct path', async () => {
31
+ const { getCronFilePath } = await import('../cronTasks.js')
32
+ const filePath = getCronFilePath(tmpDir)
33
+ expect(filePath).toBe(join(tmpDir, '.claude', 'scheduled_tasks.json'))
34
+ })
35
+
36
+ test('getCronFilePath uses project root when no dir provided', async () => {
37
+ const { getCronFilePath } = await import('../cronTasks.js')
38
+ // Without dir, should use getProjectRoot() which is process.cwd()
39
+ // Just verify it returns a valid-looking path
40
+ const filePath = getCronFilePath()
41
+ expect(filePath).toContain('scheduled_tasks.json')
42
+ })
43
+ })
44
+
45
+ describe('CronTaskMeta type coverage', () => {
46
+ test('all UI fields are optional on CronTask', async () => {
47
+ // Verify all new fields exist on the type by creating tasks with them
48
+ const { addCronTask } = await import('../cronTasks.js')
49
+
50
+ // Create a task with all metadata fields (durable=true writes to disk in test dir)
51
+ const tmpDir = join('/tmp', `cron-meta-test-${randomUUID().slice(0, 8)}`)
52
+ await mkdir(join(tmpDir, '.claude'), { recursive: true })
53
+
54
+ const id = await addCronTask(
55
+ '0 9 * * *',
56
+ 'test prompt',
57
+ true, // recurring
58
+ true, // durable (writes to disk)
59
+ undefined, // agentId
60
+ {
61
+ name: 'test-name',
62
+ description: 'test description',
63
+ folder: '/test/folder',
64
+ model: 'claude-opus-4-7',
65
+ permissionMode: 'ask',
66
+ worktree: false,
67
+ frequency: 'daily',
68
+ scheduledTime: '09:00',
69
+ },
70
+ )
71
+
72
+ expect(typeof id).toBe('string')
73
+ expect(id.length).toBe(8) // Short ID
74
+
75
+ // Clean up
76
+ await rm(tmpDir, { recursive: true, force: true })
77
+ })
78
+ })
79
+
80
+ describe('readCronTasks backward compatibility', () => {
81
+ test('handles empty file', async () => {
82
+ const { readCronTasks } = await import('../cronTasks.js')
83
+ const tmpDir = join('/tmp', `cron-empty-${randomUUID().slice(0, 8)}`)
84
+ await mkdir(join(tmpDir, '.claude'), { recursive: true })
85
+
86
+ const tasks = await readCronTasks(tmpDir)
87
+ expect(Array.isArray(tasks)).toBe(true)
88
+ expect(tasks.length).toBe(0)
89
+
90
+ await rm(tmpDir, { recursive: true, force: true })
91
+ })
92
+
93
+ test('skips malformed JSON', async () => {
94
+ const { readCronTasks } = await import('../cronTasks.js')
95
+ const tmpDir = join('/tmp', `cron-malformed-${randomUUID().slice(0, 8)}`)
96
+ await mkdir(join(tmpDir, '.claude'), { recursive: true })
97
+
98
+ // Write malformed JSON
99
+ const filePath = join(tmpDir, '.claude', 'scheduled_tasks.json')
100
+ await writeFile(filePath, 'not valid json{{{')
101
+
102
+ const tasks = await readCronTasks(tmpDir)
103
+ expect(tasks.length).toBe(0) // Malformed entries skipped
104
+
105
+ await rm(tmpDir, { recursive: true, force: true })
106
+ })
107
+
108
+ test('skips tasks with invalid cron strings', async () => {
109
+ const { readCronTasks } = await import('../cronTasks.js')
110
+ const tmpDir = join('/tmp', `cron-invalid-${randomUUID().slice(0, 8)}`)
111
+ await mkdir(join(tmpDir, '.claude'), { recursive: true })
112
+
113
+ // Write task with invalid cron
114
+ const filePath = join(tmpDir, '.claude', 'scheduled_tasks.json')
115
+ await writeFile(
116
+ filePath,
117
+ JSON.stringify({
118
+ tasks: [
119
+ {
120
+ id: 'abcd1234',
121
+ cron: 'invalid-cron',
122
+ prompt: 'test',
123
+ createdAt: Date.now(),
124
+ },
125
+ ],
126
+ }),
127
+ )
128
+
129
+ const tasks = await readCronTasks(tmpDir)
130
+ expect(tasks.length).toBe(0) // Invalid cron skipped
131
+
132
+ await rm(tmpDir, { recursive: true, force: true })
133
+ })
134
+
135
+ test('preserves new fields when reading', async () => {
136
+ const { readCronTasks } = await import('../cronTasks.js')
137
+ const tmpDir = join('/tmp', `cron-preserve-${randomUUID().slice(0, 8)}`)
138
+ await mkdir(join(tmpDir, '.claude'), { recursive: true })
139
+
140
+ const filePath = join(tmpDir, '.claude', 'scheduled_tasks.json')
141
+ const task = {
142
+ id: 'abcd1234',
143
+ cron: '0 9 * * *',
144
+ prompt: 'test prompt',
145
+ createdAt: Date.now(),
146
+ recurring: true,
147
+ name: 'my-task',
148
+ description: 'A test task',
149
+ folder: '/project',
150
+ model: 'claude-sonnet-4-6',
151
+ permissionMode: 'bypass',
152
+ worktree: true,
153
+ frequency: 'daily',
154
+ scheduledTime: '09:00',
155
+ }
156
+ await writeFile(filePath, JSON.stringify({ tasks: [task] }))
157
+
158
+ const tasks = await readCronTasks(tmpDir)
159
+ expect(tasks.length).toBe(1)
160
+ expect(tasks[0].name).toBe('my-task')
161
+ expect(tasks[0].description).toBe('A test task')
162
+ expect(tasks[0].folder).toBe('/project')
163
+ expect(tasks[0].model).toBe('claude-sonnet-4-6')
164
+ expect(tasks[0].permissionMode).toBe('bypass')
165
+ expect(tasks[0].worktree).toBe(true)
166
+ expect(tasks[0].frequency).toBe('daily')
167
+ expect(tasks[0].scheduledTime).toBe('09:00')
168
+
169
+ await rm(tmpDir, { recursive: true, force: true })
170
+ })
171
+ })
172
+
173
+ describe('writeCronTasks strips runtime fields', () => {
174
+ test('strips durable and agentId on write', async () => {
175
+ const { readCronTasks, writeCronTasks } = await import('../cronTasks.js')
176
+ const tmpDir = join('/tmp', `cron-strip-${randomUUID().slice(0, 8)}`)
177
+ await mkdir(join(tmpDir, '.claude'), { recursive: true })
178
+
179
+ const taskWithRuntimeFields = {
180
+ id: 'abcd1234',
181
+ cron: '0 9 * * *',
182
+ prompt: 'test',
183
+ createdAt: Date.now(),
184
+ recurring: true,
185
+ durable: true, // runtime-only, should be stripped
186
+ agentId: 'agent-123', // runtime-only, should be stripped
187
+ name: 'test-task', // new field, should be preserved
188
+ }
189
+
190
+ await writeCronTasks([taskWithRuntimeFields as any], tmpDir)
191
+
192
+ // Read back and verify runtime fields are stripped
193
+ const filePath = join(tmpDir, '.claude', 'scheduled_tasks.json')
194
+ const { readFileSync } = await import('fs')
195
+ const raw = readFileSync(filePath, 'utf-8')
196
+ const parsed = JSON.parse(raw)
197
+
198
+ expect(parsed.tasks[0].durable).toBeUndefined()
199
+ expect(parsed.tasks[0].agentId).toBeUndefined()
200
+ expect(parsed.tasks[0].name).toBe('test-task')
201
+
202
+ await rm(tmpDir, { recursive: true, force: true })
203
+ })
204
+ })
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { normalizeOsPermissions } from './permissions.js'
3
+
4
+ describe('normalizeOsPermissions', () => {
5
+ it('treats explicit grants as granted', () => {
6
+ expect(
7
+ normalizeOsPermissions({ accessibility: true, screenRecording: true }),
8
+ ).toEqual({
9
+ granted: true,
10
+ accessibility: true,
11
+ screenRecording: true,
12
+ })
13
+ })
14
+
15
+ it('treats screen recording unknown as non-blocking when accessibility is granted', () => {
16
+ expect(
17
+ normalizeOsPermissions({ accessibility: true, screenRecording: null }),
18
+ ).toEqual({
19
+ granted: true,
20
+ accessibility: true,
21
+ screenRecording: true,
22
+ })
23
+ })
24
+
25
+ it('still blocks when accessibility is missing', () => {
26
+ expect(
27
+ normalizeOsPermissions({ accessibility: false, screenRecording: null }),
28
+ ).toEqual({
29
+ granted: false,
30
+ accessibility: false,
31
+ screenRecording: true,
32
+ })
33
+ })
34
+
35
+ it('blocks when screen recording is explicitly denied', () => {
36
+ expect(
37
+ normalizeOsPermissions({ accessibility: true, screenRecording: false }),
38
+ ).toEqual({
39
+ granted: false,
40
+ accessibility: true,
41
+ screenRecording: false,
42
+ })
43
+ })
44
+ })