bingocode 1.0.26 → 1.0.28

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 (65) hide show
  1. package/config/bingo-defaults/settings.json +2 -1
  2. package/package.json +1 -2
  3. package/src/server/services/providerService.ts +104 -0
  4. package/src/utils/managedEnv.ts +1 -17
  5. package/.github/FUNDING.yml +0 -1
  6. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -44
  7. package/.github/ISSUE_TEMPLATE/config.yml +0 -1
  8. package/.github/ISSUE_TEMPLATE/question.md +0 -40
  9. package/.github/workflows/build-desktop-dev.yml +0 -210
  10. package/.github/workflows/deploy-docs.yml +0 -59
  11. package/.github/workflows/release-desktop.yml +0 -162
  12. package/.spine/user.yaml +0 -5
  13. package/.spine/workspace.yaml +0 -1
  14. package/adapters/common/__tests__/chat-queue.test.ts +0 -61
  15. package/adapters/common/__tests__/format.test.ts +0 -148
  16. package/adapters/common/__tests__/http-client.test.ts +0 -105
  17. package/adapters/common/__tests__/message-buffer.test.ts +0 -84
  18. package/adapters/common/__tests__/message-dedup.test.ts +0 -57
  19. package/adapters/common/__tests__/session-store.test.ts +0 -62
  20. package/adapters/common/__tests__/ws-bridge.test.ts +0 -177
  21. package/adapters/common/attachment/__tests__/attachment-limits.test.ts +0 -52
  22. package/adapters/common/attachment/__tests__/attachment-store.test.ts +0 -108
  23. package/adapters/common/attachment/__tests__/image-block-watcher.test.ts +0 -115
  24. package/adapters/feishu/__tests__/card-errors.test.ts +0 -194
  25. package/adapters/feishu/__tests__/cardkit.test.ts +0 -295
  26. package/adapters/feishu/__tests__/extract-payload.test.ts +0 -77
  27. package/adapters/feishu/__tests__/feishu.test.ts +0 -907
  28. package/adapters/feishu/__tests__/flush-controller.test.ts +0 -290
  29. package/adapters/feishu/__tests__/markdown-style.test.ts +0 -353
  30. package/adapters/feishu/__tests__/media.test.ts +0 -120
  31. package/adapters/feishu/__tests__/streaming-card.test.ts +0 -914
  32. package/adapters/telegram/__tests__/media.test.ts +0 -86
  33. package/adapters/telegram/__tests__/telegram.test.ts +0 -115
  34. package/adapters/tsconfig.json +0 -18
  35. package/bunfig.toml +0 -1
  36. package/preload.ts +0 -30
  37. package/scripts/count-app-loc.ts +0 -256
  38. package/scripts/release.ts +0 -130
  39. package/src/server/__tests__/conversation-service.test.ts +0 -173
  40. package/src/server/__tests__/conversations.test.ts +0 -458
  41. package/src/server/__tests__/cron-scheduler.test.ts +0 -575
  42. package/src/server/__tests__/e2e/business-flow.test.ts +0 -841
  43. package/src/server/__tests__/e2e/full-flow.test.ts +0 -357
  44. package/src/server/__tests__/fixtures/mock-sdk-cli.ts +0 -123
  45. package/src/server/__tests__/haha-oauth-api.test.ts +0 -146
  46. package/src/server/__tests__/haha-oauth-service.test.ts +0 -185
  47. package/src/server/__tests__/providers-real.test.ts +0 -244
  48. package/src/server/__tests__/providers.test.ts +0 -579
  49. package/src/server/__tests__/proxy-streaming.test.ts +0 -317
  50. package/src/server/__tests__/proxy-transform.test.ts +0 -469
  51. package/src/server/__tests__/real-llm-test.ts +0 -526
  52. package/src/server/__tests__/scheduled-tasks.test.ts +0 -371
  53. package/src/server/__tests__/sessions.test.ts +0 -786
  54. package/src/server/__tests__/settings.test.ts +0 -376
  55. package/src/server/__tests__/skills.test.ts +0 -125
  56. package/src/server/__tests__/tasks.test.ts +0 -171
  57. package/src/server/__tests__/team-watcher.test.ts +0 -400
  58. package/src/server/__tests__/teams.test.ts +0 -627
  59. package/src/server/middleware/cors.test.ts +0 -27
  60. package/src/utils/__tests__/cronFrequency.test.ts +0 -153
  61. package/src/utils/__tests__/cronTasks.test.ts +0 -204
  62. package/src/utils/computerUse/permissions.test.ts +0 -44
  63. package/stubs/ant-claude-for-chrome-mcp.ts +0 -24
  64. package/stubs/color-diff-napi.ts +0 -45
  65. package/tsconfig.json +0 -24
@@ -1,162 +0,0 @@
1
- name: Release Desktop
2
-
3
- on:
4
- push:
5
- tags: ['v*.*.*']
6
- workflow_dispatch:
7
- inputs:
8
- draft:
9
- description: 'Create as draft release'
10
- required: false
11
- default: true
12
- type: boolean
13
-
14
- permissions:
15
- contents: write
16
-
17
- concurrency:
18
- group: release-desktop-${{ github.ref }}
19
- cancel-in-progress: true
20
-
21
- jobs:
22
- build:
23
- strategy:
24
- fail-fast: false
25
- matrix:
26
- include:
27
- # macOS Apple Silicon
28
- - platform: macos-latest
29
- rust_target: aarch64-apple-darwin
30
- tauri_args: '--target aarch64-apple-darwin'
31
- label: macOS-ARM64
32
-
33
- # macOS Intel
34
- - platform: macos-latest
35
- rust_target: x86_64-apple-darwin
36
- tauri_args: '--target x86_64-apple-darwin'
37
- label: macOS-x64
38
-
39
- # Linux x64
40
- - platform: ubuntu-22.04
41
- rust_target: x86_64-unknown-linux-gnu
42
- tauri_args: '--bundles deb'
43
- label: Linux-x64
44
-
45
- # Linux ARM64
46
- - platform: ubuntu-22.04-arm
47
- rust_target: aarch64-unknown-linux-gnu
48
- tauri_args: '--bundles deb'
49
- label: Linux-ARM64
50
-
51
- # Windows x64
52
- - platform: windows-latest
53
- rust_target: x86_64-pc-windows-msvc
54
- tauri_args: ''
55
- label: Windows-x64
56
-
57
- runs-on: ${{ matrix.platform }}
58
- name: Build (${{ matrix.label }})
59
-
60
- steps:
61
- - name: Checkout
62
- uses: actions/checkout@v4
63
-
64
- # ── System dependencies (Linux) ──────────────────────────
65
- - name: Install Linux dependencies
66
- if: contains(matrix.platform, 'ubuntu')
67
- run: |
68
- sudo apt-get update
69
- sudo apt-get install -y \
70
- build-essential \
71
- curl \
72
- wget \
73
- file \
74
- libxdo-dev \
75
- libssl-dev \
76
- libwebkit2gtk-4.1-dev \
77
- libayatana-appindicator3-dev \
78
- librsvg2-dev \
79
- patchelf \
80
- libfuse2
81
-
82
- # ── Bun ──────────────────────────────────────────────────
83
- - name: Setup Bun
84
- uses: oven-sh/setup-bun@v2
85
- with:
86
- bun-version: latest
87
-
88
- # ── Node.js (for tauri-action compatibility) ─────────────
89
- - name: Setup Node.js
90
- uses: actions/setup-node@v4
91
- with:
92
- node-version: 20
93
-
94
- # ── Rust ─────────────────────────────────────────────────
95
- - name: Setup Rust
96
- uses: dtolnay/rust-toolchain@stable
97
- with:
98
- targets: ${{ matrix.rust_target }}
99
-
100
- - name: Rust cache
101
- uses: swatinem/rust-cache@v2
102
- with:
103
- workspaces: 'desktop/src-tauri -> target'
104
- shared-key: ${{ matrix.rust_target }}
105
-
106
- # ── Install dependencies ─────────────────────────────────
107
- - name: Install root dependencies
108
- run: bun install
109
-
110
- - name: Install desktop dependencies
111
- working-directory: desktop
112
- run: bun install
113
-
114
- - name: Install adapter dependencies
115
- working-directory: adapters
116
- run: bun install
117
-
118
- # ── Build sidecars ───────────────────────────────────────
119
- - name: Build sidecars
120
- working-directory: desktop
121
- env:
122
- TAURI_ENV_TARGET_TRIPLE: ${{ matrix.rust_target }}
123
- run: bun run build:sidecars
124
-
125
- # ── Build Tauri app ──────────────────────────────────────
126
- - name: Build Tauri app
127
- uses: tauri-apps/tauri-action@v0
128
- env:
129
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
130
- TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
131
- TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
132
- with:
133
- projectPath: desktop
134
- tauriScript: bunx tauri
135
- tagName: v__VERSION__
136
- releaseName: 'Claude Code Haha v__VERSION__'
137
- releaseBody: |
138
- ## Claude Code Haha v__VERSION__
139
-
140
- ### Downloads
141
-
142
- | Platform | File |
143
- |----------|------|
144
- | macOS (Apple Silicon) | `.dmg` |
145
- | macOS (Intel) | `.dmg` |
146
- | Windows | `.exe` (NSIS installer) |
147
- | Linux | `.deb` |
148
-
149
- ### First-time installation
150
-
151
- **macOS**: This build is unsigned. If you see "app is damaged" or "unidentified developer", run:
152
- ```bash
153
- xattr -cr /Applications/Claude\ Code\ Haha.app
154
- ```
155
-
156
- **Windows**: If SmartScreen blocks the app, click "More info" → "Run anyway".
157
-
158
- ---
159
- See [Installation Guide](https://github.com/NanmiCoder/cc-haha/blob/main/docs/desktop/04-installation.md) for details.
160
- releaseDraft: ${{ github.event_name == 'workflow_dispatch' && inputs.draft || false }}
161
- prerelease: false
162
- args: ${{ matrix.tauri_args }} --config src-tauri/tauri.release-ci.json
package/.spine/user.yaml DELETED
@@ -1,5 +0,0 @@
1
- name: leanchy
2
- email: ""
3
- isGuest: false
4
- login_at: 2026-04-23T10:20:39.366Z
5
- hostname: CNCDUW2771
@@ -1 +0,0 @@
1
- current_project: P-001-演示项目
@@ -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
- })