bingocode 1.0.41 → 1.1.42

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 (63) hide show
  1. package/bin/bingo-win.cjs +2 -1
  2. package/bin/bingocode-win.cjs +2 -1
  3. package/bin/claude-win.cjs +2 -1
  4. package/bun.lock +1716 -0
  5. package/package.json +14 -2
  6. package/src/server/config/providers.yaml +1 -1
  7. package/src/server/proxy/transform/anthropicToOpenaiChat.ts +11 -4
  8. package/adapters/README.md +0 -87
  9. package/adapters/common/__tests__/chat-queue.test.ts +0 -61
  10. package/adapters/common/__tests__/format.test.ts +0 -148
  11. package/adapters/common/__tests__/http-client.test.ts +0 -105
  12. package/adapters/common/__tests__/message-buffer.test.ts +0 -84
  13. package/adapters/common/__tests__/message-dedup.test.ts +0 -57
  14. package/adapters/common/__tests__/session-store.test.ts +0 -62
  15. package/adapters/common/__tests__/ws-bridge.test.ts +0 -177
  16. package/adapters/common/attachment/__tests__/attachment-limits.test.ts +0 -52
  17. package/adapters/common/attachment/__tests__/attachment-store.test.ts +0 -108
  18. package/adapters/common/attachment/__tests__/image-block-watcher.test.ts +0 -115
  19. package/adapters/common/attachment/attachment-limits.ts +0 -58
  20. package/adapters/common/attachment/attachment-store.ts +0 -121
  21. package/adapters/common/attachment/attachment-types.ts +0 -29
  22. package/adapters/common/attachment/image-block-watcher.ts +0 -94
  23. package/adapters/common/chat-queue.ts +0 -24
  24. package/adapters/common/config.ts +0 -96
  25. package/adapters/common/format.ts +0 -229
  26. package/adapters/common/http-client.ts +0 -107
  27. package/adapters/common/message-buffer.ts +0 -91
  28. package/adapters/common/message-dedup.ts +0 -57
  29. package/adapters/common/pairing.ts +0 -149
  30. package/adapters/common/session-store.ts +0 -60
  31. package/adapters/common/ws-bridge.ts +0 -282
  32. package/adapters/feishu/__tests__/card-errors.test.ts +0 -194
  33. package/adapters/feishu/__tests__/cardkit.test.ts +0 -295
  34. package/adapters/feishu/__tests__/extract-payload.test.ts +0 -77
  35. package/adapters/feishu/__tests__/feishu.test.ts +0 -907
  36. package/adapters/feishu/__tests__/flush-controller.test.ts +0 -290
  37. package/adapters/feishu/__tests__/markdown-style.test.ts +0 -353
  38. package/adapters/feishu/__tests__/media.test.ts +0 -120
  39. package/adapters/feishu/__tests__/streaming-card.test.ts +0 -914
  40. package/adapters/feishu/card-errors.ts +0 -151
  41. package/adapters/feishu/cardkit.ts +0 -294
  42. package/adapters/feishu/extract-payload.ts +0 -95
  43. package/adapters/feishu/flush-controller.ts +0 -149
  44. package/adapters/feishu/index.ts +0 -1275
  45. package/adapters/feishu/markdown-style.ts +0 -212
  46. package/adapters/feishu/media.ts +0 -176
  47. package/adapters/feishu/streaming-card.ts +0 -612
  48. package/adapters/package.json +0 -23
  49. package/adapters/telegram/__tests__/media.test.ts +0 -86
  50. package/adapters/telegram/__tests__/telegram.test.ts +0 -115
  51. package/adapters/telegram/index.ts +0 -754
  52. package/adapters/telegram/media.ts +0 -89
  53. package/adapters/tsconfig.json +0 -18
  54. package/runtime/mac_helper.py +0 -775
  55. package/runtime/requirements-win.txt +0 -7
  56. package/runtime/requirements.txt +0 -6
  57. package/runtime/test_helpers.py +0 -322
  58. package/runtime/win_helper.py +0 -723
  59. package/scripts/count-app-loc.ts +0 -256
  60. package/scripts/release.ts +0 -130
  61. package/start-cli.bat +0 -7
  62. package/stubs/ant-claude-for-chrome-mcp.ts +0 -24
  63. package/stubs/color-diff-napi.ts +0 -45
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bingocode",
3
- "version": "1.0.41",
3
+ "version": "1.1.42",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude": "bin/claude-win.cjs",
@@ -14,8 +14,20 @@
14
14
  "bingocode": "bun run ./bin/bingocode-win.cjs",
15
15
  "docs:dev": "vitepress dev docs",
16
16
  "docs:build": "vitepress build docs",
17
- "docs:preview": "vitepress preview docs"
17
+ "docs:preview": "vitepress preview docs",
18
+ "local-install": "npm install -g ."
18
19
  },
20
+ "files": [
21
+ "bin/",
22
+ "src/",
23
+ "config/",
24
+ "preload.ts",
25
+ "tsconfig.json",
26
+ "package.json",
27
+ "README.md",
28
+ "bun.lock",
29
+ "bunfig.toml"
30
+ ],
19
31
  "dependencies": {
20
32
  "@alcalzone/ansi-tokenize": "^0.3.0",
21
33
  "@anthropic-ai/sandbox-runtime": "^0.0.44",
@@ -98,7 +98,7 @@ presets:
98
98
 
99
99
  - id: deepseek
100
100
  name: DeepSeek
101
- baseUrl: https://api.deepseek.com
101
+ baseUrl: https://api.deepseek.com/anthropic
102
102
  apiFormat: openai_chat
103
103
  needsApiKey: true
104
104
  websiteUrl: https://platform.deepseek.com
@@ -43,9 +43,16 @@ export function anthropicToOpenaiChat(body: AnthropicRequest): OpenAIChatRequest
43
43
  stream: body.stream,
44
44
  }
45
45
 
46
- // max_tokens — omit to let upstream provider use its own default/max.
47
- // Claude Code sends very large values (e.g. 128K) that exceed many
48
- // providers' limits (DeepSeek: 8192, etc.).
46
+ // max_tokens — limit for DeepSeek to avoid invalid parameter errors
47
+ if (body.max_tokens !== undefined) {
48
+ if (body.model.toLowerCase().includes('deepseek')) {
49
+ // DeepSeek R1 often fails if max_tokens is set to Claude's high defaults (like 128k)
50
+ // Especially when tools or thinking are involved. 8192 is a safe upper limit for most.
51
+ result.max_tokens = Math.min(body.max_tokens, 8192)
52
+ } else {
53
+ result.max_tokens = body.max_tokens
54
+ }
55
+ }
49
56
 
50
57
  // temperature & top_p
51
58
  if (body.temperature !== undefined) result.temperature = body.temperature
@@ -76,7 +83,7 @@ export function anthropicToOpenaiChat(body: AnthropicRequest): OpenAIChatRequest
76
83
  }
77
84
 
78
85
  // thinking → reasoning_effort
79
- if (body.thinking) {
86
+ if (body.thinking && !body.model.toLowerCase().includes('deepseek')) {
80
87
  const budget = body.thinking.budget_tokens
81
88
  if (budget !== undefined) {
82
89
  if (budget <= 1024) result.reasoning_effort = 'low'
@@ -1,87 +0,0 @@
1
- # Claude Code IM Adapters
2
-
3
- 当前目录只放 IM Adapter 运行时代码。
4
-
5
- 用户文档已经迁移到 `docs/`,并且以 Desktop Webapp 配置流程为准:
6
-
7
- - `docs/im/index.md`
8
- - `docs/im/telegram.md`
9
- - `docs/im/feishu.md`
10
-
11
- ## 当前方案摘要
12
-
13
- 当前真实链路是:
14
-
15
- ```text
16
- Desktop Webapp Settings
17
- -> /api/adapters
18
- -> ~/.claude/adapters.json
19
- -> adapters/<platform>/index.ts
20
- -> /api/sessions + /ws/:sessionId
21
- -> Claude Code session
22
- ```
23
-
24
- 注意两点:
25
-
26
- - IM 配置和配对都在 Desktop Webapp 的 `Settings -> IM 接入`
27
- - Webapp 不会自动启动 Adapter 进程,仍需手动运行 `bun run telegram` 或 `bun run feishu`
28
-
29
- ## 快速启动
30
-
31
- ```bash
32
- cd adapters
33
- bun install
34
- bun run telegram
35
- # 或
36
- bun run feishu
37
- ```
38
-
39
- ## 开发
40
-
41
- ### 运行测试
42
-
43
- ```bash
44
- cd adapters
45
- bun test
46
- bun test common/
47
- bun test telegram/
48
- bun test feishu/
49
- ```
50
-
51
- ### 目录结构
52
-
53
- ```text
54
- adapters/
55
- ├── common/
56
- │ └── attachment/ # 跨平台附件工具(types / limits / store / image-watcher)
57
- ├── telegram/
58
- │ └── media.ts # TelegramMediaService(grammy Bot API 封装)
59
- ├── feishu/
60
- │ ├── media.ts # FeishuMediaService(@larksuiteoapi/node-sdk 封装)
61
- │ └── extract-payload.ts # 入站 im.message.receive_v1 事件解析
62
- ├── package.json
63
- ├── tsconfig.json
64
- └── README.md
65
- ```
66
-
67
- ## 附件收发
68
-
69
- 两个 Adapter 都支持双向图片/文件,和 Desktop 端走同一套 `AttachmentRef` 协议透传给主进程。
70
-
71
- **入站(用户 → Claude):**
72
-
73
- - 飞书: 图片(jpg/png/gif/webp/heic)、文档(doc/xls/ppt/pdf 等)、post 富文本里的 img/file 元素
74
- - Telegram: photo、document、video、audio、voice
75
-
76
- 下载落地到 `~/.claude/im-downloads/{platform}/{sessionId}/`,24 小时后自动 GC(`.part` 孤文件 10 分钟超时)。大小限制:单张图 ≤10 MB、单个文件 ≤30 MB,超限直接拒收并在 IM 里提示。
77
-
78
- **出站(Claude → 用户):**
79
-
80
- Agent 流式文本里的 markdown 图片引用 `![alt](path|url|data:)` 会被 `ImageBlockWatcher` 识别、上传到 IM 平台,作为独立图片消息发出:
81
-
82
- - 飞书: `im.message.create(msg_type='image')` 单发(card 内嵌是后续优化)
83
- - Telegram: `bot.api.sendPhoto(InputFile)` 单发
84
-
85
- 非图片类出站(Agent 产的 pdf/zip 等)暂不支持。
86
-
87
- 设计细节: `docs/superpowers/specs/2026-04-11-im-attachment-support-design.md`。
@@ -1,61 +0,0 @@
1
- import { describe, it, expect } from 'bun:test'
2
- import { enqueue } from '../chat-queue.js'
3
-
4
- describe('ChatQueue', () => {
5
- it('executes tasks for the same chatId serially', async () => {
6
- const order: number[] = []
7
-
8
- await Promise.all([
9
- enqueue('chat-1', async () => {
10
- await new Promise((r) => setTimeout(r, 30))
11
- order.push(1)
12
- }),
13
- enqueue('chat-1', async () => {
14
- order.push(2)
15
- }),
16
- enqueue('chat-1', async () => {
17
- order.push(3)
18
- }),
19
- ])
20
-
21
- // Wait for all to complete
22
- await new Promise((r) => setTimeout(r, 50))
23
- expect(order).toEqual([1, 2, 3])
24
- })
25
-
26
- it('executes tasks for different chatIds in parallel', async () => {
27
- const order: string[] = []
28
-
29
- const p1 = enqueue('chat-a', async () => {
30
- await new Promise((r) => setTimeout(r, 30))
31
- order.push('a')
32
- })
33
-
34
- const p2 = enqueue('chat-b', async () => {
35
- order.push('b') // should run immediately, not wait for chat-a
36
- })
37
-
38
- await Promise.all([p1, p2])
39
- await new Promise((r) => setTimeout(r, 50))
40
-
41
- // 'b' should appear before 'a' since chat-a has a delay
42
- expect(order[0]).toBe('b')
43
- expect(order[1]).toBe('a')
44
- })
45
-
46
- it('continues processing after a task fails', async () => {
47
- const order: number[] = []
48
-
49
- await enqueue('chat-err', async () => {
50
- order.push(1)
51
- throw new Error('task failed')
52
- })
53
-
54
- await enqueue('chat-err', async () => {
55
- order.push(2) // should still run
56
- })
57
-
58
- await new Promise((r) => setTimeout(r, 20))
59
- expect(order).toEqual([1, 2])
60
- })
61
- })
@@ -1,148 +0,0 @@
1
- import { describe, it, expect } from 'bun:test'
2
- import {
3
- formatImHelp,
4
- formatImStatus,
5
- splitMessage,
6
- formatToolUse,
7
- formatPermissionRequest,
8
- truncateInput,
9
- escapeMarkdownV2,
10
- } from '../format.js'
11
-
12
- describe('splitMessage', () => {
13
- it('returns single chunk for short text', () => {
14
- expect(splitMessage('hello', 100)).toEqual(['hello'])
15
- })
16
-
17
- it('splits at paragraph boundary', () => {
18
- const text = 'First paragraph.\n\nSecond paragraph.'
19
- const chunks = splitMessage(text, 20)
20
- expect(chunks.length).toBeGreaterThan(1)
21
- expect(chunks.join(' ').replace(/\s+/g, ' ')).toContain('First paragraph')
22
- expect(chunks.join(' ').replace(/\s+/g, ' ')).toContain('Second paragraph')
23
- })
24
-
25
- it('splits at newline if no paragraph break', () => {
26
- const text = 'Line one\nLine two\nLine three\nLine four'
27
- const chunks = splitMessage(text, 20)
28
- expect(chunks.length).toBeGreaterThan(1)
29
- })
30
-
31
- it('hard-splits at limit if no natural break', () => {
32
- const text = 'a'.repeat(50)
33
- const chunks = splitMessage(text, 20)
34
- expect(chunks.length).toBe(3) // 20 + 20 + 10
35
- expect(chunks.every((c) => c.length <= 20)).toBe(true)
36
- })
37
-
38
- it('preserves all content after splitting', () => {
39
- const text = 'Hello world. This is a test. Foo bar baz.'
40
- const chunks = splitMessage(text, 15)
41
- const joined = chunks.join(' ')
42
- // All words should be present
43
- expect(joined).toContain('Hello')
44
- expect(joined).toContain('test')
45
- expect(joined).toContain('baz')
46
- })
47
- })
48
-
49
- describe('formatToolUse', () => {
50
- it('includes tool name and input preview', () => {
51
- const result = formatToolUse('Bash', { command: 'npm test' })
52
- expect(result).toContain('🔧 Bash')
53
- expect(result).toContain('npm test')
54
- })
55
- })
56
-
57
- describe('formatPermissionRequest', () => {
58
- it('includes tool name, input preview, and request ID', () => {
59
- const result = formatPermissionRequest('Bash', { command: 'rm -rf /' }, 'abcde')
60
- expect(result).toContain('🔐')
61
- expect(result).toContain('Bash')
62
- expect(result).toContain('abcde')
63
- expect(result).toContain('rm -rf')
64
- })
65
- })
66
-
67
- describe('truncateInput', () => {
68
- it('returns short input as-is', () => {
69
- expect(truncateInput('hello', 100)).toBe('hello')
70
- })
71
-
72
- it('truncates long input with ellipsis', () => {
73
- const long = 'x'.repeat(300)
74
- const result = truncateInput(long, 100)
75
- expect(result.length).toBe(101) // 100 chars + '…'
76
- expect(result.endsWith('…')).toBe(true)
77
- })
78
-
79
- it('handles objects by stringifying', () => {
80
- const result = truncateInput({ key: 'value' }, 100)
81
- expect(result).toContain('key')
82
- expect(result).toContain('value')
83
- })
84
-
85
- it('handles unserializable input', () => {
86
- const circular: any = {}
87
- circular.self = circular
88
- expect(truncateInput(circular, 100)).toBe('(unserializable)')
89
- })
90
- })
91
-
92
- describe('escapeMarkdownV2', () => {
93
- it('escapes special characters', () => {
94
- expect(escapeMarkdownV2('hello_world')).toBe('hello\\_world')
95
- expect(escapeMarkdownV2('a*b*c')).toBe('a\\*b\\*c')
96
- expect(escapeMarkdownV2('test.md')).toBe('test\\.md')
97
- })
98
-
99
- it('leaves plain text unchanged', () => {
100
- expect(escapeMarkdownV2('hello world')).toBe('hello world')
101
- })
102
- })
103
-
104
- describe('formatImHelp', () => {
105
- it('lists the lightweight IM commands', () => {
106
- const text = formatImHelp()
107
- expect(text).toContain('/new')
108
- expect(text).toContain('/projects')
109
- expect(text).toContain('/status')
110
- expect(text).toContain('/clear')
111
- expect(text).toContain('/stop')
112
- expect(text).toContain('/help')
113
- })
114
- })
115
-
116
- describe('formatImStatus', () => {
117
- it('formats an active session summary for mobile reading', () => {
118
- const text = formatImStatus({
119
- sessionId: 'abc1234567890',
120
- projectName: 'claude-code-haha',
121
- branch: 'main',
122
- model: 'claude-sonnet',
123
- state: 'tool_executing',
124
- verb: 'Running tests',
125
- pendingPermissionCount: 1,
126
- taskCounts: {
127
- total: 4,
128
- pending: 1,
129
- inProgress: 2,
130
- completed: 1,
131
- },
132
- })
133
-
134
- expect(text).toContain('项目: claude-code-haha (main)')
135
- expect(text).toContain('会话: abc12345…')
136
- expect(text).toContain('模型: claude-sonnet')
137
- expect(text).toContain('状态: 执行工具中 (Running tests)')
138
- expect(text).toContain('审批: 1 个待确认')
139
- expect(text).toContain('任务: 总计 4 · 进行中 2 · 待处理 1 · 已完成 1')
140
- })
141
-
142
- it('returns a friendly empty-session message when nothing is active', () => {
143
- const text = formatImStatus(null)
144
- expect(text).toContain('当前没有活动会话')
145
- expect(text).toContain('/new')
146
- expect(text).toContain('/projects')
147
- })
148
- })
@@ -1,105 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test'
2
- import { AdapterHttpClient } from '../http-client.js'
3
-
4
- describe('AdapterHttpClient', () => {
5
- let client: AdapterHttpClient
6
- const originalFetch = globalThis.fetch
7
-
8
- beforeEach(() => {
9
- client = new AdapterHttpClient('ws://127.0.0.1:3456')
10
- })
11
-
12
- afterEach(() => {
13
- globalThis.fetch = originalFetch
14
- })
15
-
16
- it('derives HTTP URL from WS URL', () => {
17
- expect(client.httpBaseUrl).toBe('http://127.0.0.1:3456')
18
-
19
- const secure = new AdapterHttpClient('wss://example.com:443')
20
- expect(secure.httpBaseUrl).toBe('https://example.com:443')
21
- })
22
-
23
- it('createSession calls POST /api/sessions', async () => {
24
- const mockSessionId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
25
- globalThis.fetch = mock(() =>
26
- Promise.resolve(new Response(JSON.stringify({ sessionId: mockSessionId }), {
27
- status: 201,
28
- headers: { 'Content-Type': 'application/json' },
29
- }))
30
- ) as any
31
-
32
- const sessionId = await client.createSession('/path/to/project')
33
- expect(sessionId).toBe(mockSessionId)
34
-
35
- const call = (globalThis.fetch as any).mock.calls[0]
36
- expect(call[0]).toBe('http://127.0.0.1:3456/api/sessions')
37
- const body = JSON.parse(call[1].body)
38
- expect(body.workDir).toBe('/path/to/project')
39
- })
40
-
41
- it('listRecentProjects calls GET /api/sessions/recent-projects', async () => {
42
- const mockProjects = [
43
- { projectName: 'my-app', realPath: '/home/user/my-app', sessionCount: 3 },
44
- ]
45
- globalThis.fetch = mock(() =>
46
- Promise.resolve(new Response(JSON.stringify({ projects: mockProjects }), {
47
- headers: { 'Content-Type': 'application/json' },
48
- }))
49
- ) as any
50
-
51
- const projects = await client.listRecentProjects()
52
- expect(projects).toHaveLength(1)
53
- expect(projects[0].projectName).toBe('my-app')
54
- })
55
-
56
- it('createSession throws on server error', async () => {
57
- globalThis.fetch = mock(() =>
58
- Promise.resolve(new Response(JSON.stringify({ error: 'BAD_REQUEST', message: 'workDir required' }), {
59
- status: 400,
60
- headers: { 'Content-Type': 'application/json' },
61
- }))
62
- ) as any
63
-
64
- expect(client.createSession('')).rejects.toThrow()
65
- })
66
-
67
- it('getGitInfo calls GET /api/sessions/:id/git-info', async () => {
68
- globalThis.fetch = mock(() =>
69
- Promise.resolve(new Response(JSON.stringify({
70
- branch: 'main',
71
- repoName: 'claude-code-haha',
72
- workDir: '/repo/claude-code-haha',
73
- changedFiles: 2,
74
- }), {
75
- headers: { 'Content-Type': 'application/json' },
76
- }))
77
- ) as any
78
-
79
- const gitInfo = await client.getGitInfo('session-123')
80
- expect(gitInfo.repoName).toBe('claude-code-haha')
81
- expect((globalThis.fetch as any).mock.calls[0][0]).toBe(
82
- 'http://127.0.0.1:3456/api/sessions/session-123/git-info',
83
- )
84
- })
85
-
86
- it('getTasksForSession calls GET /api/tasks/lists/:id', async () => {
87
- globalThis.fetch = mock(() =>
88
- Promise.resolve(new Response(JSON.stringify({
89
- tasks: [
90
- { id: '1', subject: 'Fix bug', status: 'in_progress' },
91
- { id: '2', subject: 'Write docs', status: 'pending' },
92
- ],
93
- }), {
94
- headers: { 'Content-Type': 'application/json' },
95
- }))
96
- ) as any
97
-
98
- const tasks = await client.getTasksForSession('session-123')
99
- expect(tasks).toHaveLength(2)
100
- expect(tasks[0]?.status).toBe('in_progress')
101
- expect((globalThis.fetch as any).mock.calls[0][0]).toBe(
102
- 'http://127.0.0.1:3456/api/tasks/lists/session-123',
103
- )
104
- })
105
- })
@@ -1,84 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'bun:test'
2
- import { MessageBuffer } from '../message-buffer.js'
3
-
4
- describe('MessageBuffer', () => {
5
- it('accumulates text and flushes on complete', async () => {
6
- const flushed: Array<{ text: string; isComplete: boolean }> = []
7
- const buf = new MessageBuffer(
8
- (text, isComplete) => { flushed.push({ text, isComplete }) },
9
- 500, // 500ms interval
10
- 1000, // 1000 char threshold
11
- )
12
-
13
- buf.append('Hello ')
14
- buf.append('World')
15
- await buf.complete()
16
-
17
- expect(flushed.length).toBeGreaterThanOrEqual(1)
18
- const allText = flushed.map((f) => f.text).join('')
19
- expect(allText).toBe('Hello World')
20
- // Last flush should be marked complete
21
- expect(flushed[flushed.length - 1]!.isComplete).toBe(true)
22
- })
23
-
24
- it('flushes when character threshold is reached', async () => {
25
- const flushed: string[] = []
26
- const buf = new MessageBuffer(
27
- (text) => { flushed.push(text) },
28
- 10000, // very long interval (won't trigger)
29
- 10, // 10 char threshold
30
- )
31
-
32
- buf.append('12345678901') // 11 chars > threshold
33
-
34
- // Wait for microtask
35
- await new Promise((r) => setTimeout(r, 10))
36
- expect(flushed.length).toBeGreaterThanOrEqual(1)
37
-
38
- buf.reset()
39
- })
40
-
41
- it('flushes on timer interval', async () => {
42
- const flushed: string[] = []
43
- const buf = new MessageBuffer(
44
- (text) => { flushed.push(text) },
45
- 50, // 50ms interval
46
- 1000,
47
- )
48
-
49
- buf.append('hi')
50
-
51
- // Wait for timer
52
- await new Promise((r) => setTimeout(r, 80))
53
- expect(flushed).toContain('hi')
54
-
55
- buf.reset()
56
- })
57
-
58
- it('does not flush empty buffer on complete', async () => {
59
- const flushed: string[] = []
60
- const buf = new MessageBuffer(
61
- (text) => { flushed.push(text) },
62
- )
63
-
64
- await buf.complete()
65
- expect(flushed.length).toBe(0)
66
- })
67
-
68
- it('resets properly between messages', async () => {
69
- const flushed: string[] = []
70
- const buf = new MessageBuffer(
71
- (text) => { flushed.push(text) },
72
- 500,
73
- 1000,
74
- )
75
-
76
- buf.append('first')
77
- buf.reset()
78
- buf.append('second')
79
- await buf.complete()
80
-
81
- const allText = flushed.map((f) => f).join('')
82
- expect(allText).toBe('second')
83
- })
84
- })
@@ -1,57 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'bun:test'
2
- import { MessageDedup } from '../message-dedup.js'
3
-
4
- describe('MessageDedup', () => {
5
- let dedup: MessageDedup
6
-
7
- beforeEach(() => {
8
- dedup = new MessageDedup(1000, 100) // 1s TTL, 100 max entries
9
- })
10
-
11
- afterEach(() => {
12
- dedup.destroy()
13
- })
14
-
15
- it('returns true for new messages', () => {
16
- expect(dedup.tryRecord('msg-1')).toBe(true)
17
- expect(dedup.tryRecord('msg-2')).toBe(true)
18
- })
19
-
20
- it('returns false for duplicate messages', () => {
21
- expect(dedup.tryRecord('msg-1')).toBe(true)
22
- expect(dedup.tryRecord('msg-1')).toBe(false)
23
- expect(dedup.tryRecord('msg-1')).toBe(false)
24
- })
25
-
26
- it('allows same ID after TTL expires', async () => {
27
- const shortDedup = new MessageDedup(50, 100) // 50ms TTL
28
- expect(shortDedup.tryRecord('msg-1')).toBe(true)
29
- expect(shortDedup.tryRecord('msg-1')).toBe(false)
30
- await new Promise((r) => setTimeout(r, 60))
31
- expect(shortDedup.tryRecord('msg-1')).toBe(true)
32
- shortDedup.destroy()
33
- })
34
-
35
- it('evicts oldest entry when at capacity', () => {
36
- const smallDedup = new MessageDedup(60_000, 3) // max 3 entries
37
- expect(smallDedup.tryRecord('a')).toBe(true)
38
- expect(smallDedup.tryRecord('b')).toBe(true)
39
- expect(smallDedup.tryRecord('c')).toBe(true)
40
- // Adding 4th should evict 'a'
41
- expect(smallDedup.tryRecord('d')).toBe(true)
42
- // 'a' was evicted, should be treated as new
43
- expect(smallDedup.tryRecord('a')).toBe(true)
44
- // Now store has {c, d, a} — 'b' was evicted when 'a' was re-inserted
45
- // 'c' should still be deduped (was not evicted)
46
- expect(smallDedup.tryRecord('c')).toBe(false)
47
- smallDedup.destroy()
48
- })
49
-
50
- it('handles distinct messages independently', () => {
51
- expect(dedup.tryRecord('msg-1')).toBe(true)
52
- expect(dedup.tryRecord('msg-2')).toBe(true)
53
- expect(dedup.tryRecord('msg-1')).toBe(false)
54
- expect(dedup.tryRecord('msg-2')).toBe(false)
55
- expect(dedup.tryRecord('msg-3')).toBe(true)
56
- })
57
- })
@@ -1,62 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'bun:test'
2
- import * as fs from 'node:fs'
3
- import * as path from 'node:path'
4
- import * as os from 'node:os'
5
- import { SessionStore } from '../session-store.js'
6
-
7
- describe('SessionStore', () => {
8
- let tmpDir: string
9
- let store: SessionStore
10
-
11
- beforeEach(() => {
12
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'session-store-'))
13
- store = new SessionStore(path.join(tmpDir, 'sessions.json'))
14
- })
15
-
16
- afterEach(() => {
17
- fs.rmSync(tmpDir, { recursive: true, force: true })
18
- })
19
-
20
- it('returns null for unknown chatId', () => {
21
- expect(store.get('unknown')).toBeNull()
22
- })
23
-
24
- it('stores and retrieves a session', () => {
25
- store.set('chat-1', 'uuid-aaa', '/path/to/project')
26
- const entry = store.get('chat-1')
27
- expect(entry).not.toBeNull()
28
- expect(entry!.sessionId).toBe('uuid-aaa')
29
- expect(entry!.workDir).toBe('/path/to/project')
30
- })
31
-
32
- it('overwrites existing entry on set', () => {
33
- store.set('chat-1', 'uuid-aaa', '/old')
34
- store.set('chat-1', 'uuid-bbb', '/new')
35
- expect(store.get('chat-1')!.sessionId).toBe('uuid-bbb')
36
- })
37
-
38
- it('deletes an entry', () => {
39
- store.set('chat-1', 'uuid-aaa', '/path')
40
- store.delete('chat-1')
41
- expect(store.get('chat-1')).toBeNull()
42
- })
43
-
44
- it('persists to disk and reloads', () => {
45
- store.set('chat-1', 'uuid-aaa', '/path')
46
-
47
- const store2 = new SessionStore(path.join(tmpDir, 'sessions.json'))
48
- expect(store2.get('chat-1')!.sessionId).toBe('uuid-aaa')
49
- })
50
-
51
- it('handles missing file gracefully', () => {
52
- const store2 = new SessionStore(path.join(tmpDir, 'nonexistent.json'))
53
- expect(store2.get('anything')).toBeNull()
54
- })
55
-
56
- it('lists all entries', () => {
57
- store.set('chat-1', 'uuid-1', '/a')
58
- store.set('chat-2', 'uuid-2', '/b')
59
- const all = store.listAll()
60
- expect(all).toHaveLength(2)
61
- })
62
- })