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.
- package/adapters/common/__tests__/chat-queue.test.ts +61 -0
- package/adapters/common/__tests__/format.test.ts +148 -0
- package/adapters/common/__tests__/http-client.test.ts +105 -0
- package/adapters/common/__tests__/message-buffer.test.ts +84 -0
- package/adapters/common/__tests__/message-dedup.test.ts +57 -0
- package/adapters/common/__tests__/session-store.test.ts +62 -0
- package/adapters/common/__tests__/ws-bridge.test.ts +177 -0
- package/adapters/common/attachment/__tests__/attachment-limits.test.ts +52 -0
- package/adapters/common/attachment/__tests__/attachment-store.test.ts +108 -0
- package/adapters/common/attachment/__tests__/image-block-watcher.test.ts +115 -0
- package/adapters/feishu/__tests__/card-errors.test.ts +194 -0
- package/adapters/feishu/__tests__/cardkit.test.ts +295 -0
- package/adapters/feishu/__tests__/extract-payload.test.ts +77 -0
- package/adapters/feishu/__tests__/feishu.test.ts +907 -0
- package/adapters/feishu/__tests__/flush-controller.test.ts +290 -0
- package/adapters/feishu/__tests__/markdown-style.test.ts +353 -0
- package/adapters/feishu/__tests__/media.test.ts +120 -0
- package/adapters/feishu/__tests__/streaming-card.test.ts +914 -0
- package/adapters/telegram/__tests__/media.test.ts +86 -0
- package/adapters/telegram/__tests__/telegram.test.ts +115 -0
- package/adapters/tsconfig.json +18 -0
- package/bunfig.toml +1 -0
- package/package.json +1 -1
- package/preload.ts +30 -0
- package/scripts/count-app-loc.ts +256 -0
- package/scripts/release.ts +130 -0
- package/src/server/__tests__/conversation-service.test.ts +173 -0
- package/src/server/__tests__/conversations.test.ts +458 -0
- package/src/server/__tests__/cron-scheduler.test.ts +575 -0
- package/src/server/__tests__/e2e/business-flow.test.ts +841 -0
- package/src/server/__tests__/e2e/full-flow.test.ts +357 -0
- package/src/server/__tests__/fixtures/mock-sdk-cli.ts +123 -0
- package/src/server/__tests__/haha-oauth-api.test.ts +146 -0
- package/src/server/__tests__/haha-oauth-service.test.ts +185 -0
- package/src/server/__tests__/providers-real.test.ts +244 -0
- package/src/server/__tests__/providers.test.ts +579 -0
- package/src/server/__tests__/proxy-streaming.test.ts +317 -0
- package/src/server/__tests__/proxy-transform.test.ts +469 -0
- package/src/server/__tests__/real-llm-test.ts +526 -0
- package/src/server/__tests__/scheduled-tasks.test.ts +371 -0
- package/src/server/__tests__/sessions.test.ts +786 -0
- package/src/server/__tests__/settings.test.ts +376 -0
- package/src/server/__tests__/skills.test.ts +125 -0
- package/src/server/__tests__/tasks.test.ts +171 -0
- package/src/server/__tests__/team-watcher.test.ts +400 -0
- package/src/server/__tests__/teams.test.ts +627 -0
- package/src/server/middleware/cors.test.ts +27 -0
- package/src/utils/__tests__/cronFrequency.test.ts +153 -0
- package/src/utils/__tests__/cronTasks.test.ts +204 -0
- package/src/utils/computerUse/permissions.test.ts +44 -0
- package/stubs/ant-claude-for-chrome-mcp.ts +24 -0
- package/stubs/color-diff-napi.ts +45 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for HahaOAuthService — haha 自管 OAuth 的核心 service 层。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
|
|
6
|
+
import * as fs from 'fs/promises'
|
|
7
|
+
import * as path from 'path'
|
|
8
|
+
import * as os from 'os'
|
|
9
|
+
import {
|
|
10
|
+
HahaOAuthService,
|
|
11
|
+
type StoredOAuthTokens,
|
|
12
|
+
} from '../services/hahaOAuthService.js'
|
|
13
|
+
|
|
14
|
+
let tmpDir: string
|
|
15
|
+
let originalConfigDir: string | undefined
|
|
16
|
+
let service: HahaOAuthService
|
|
17
|
+
|
|
18
|
+
async function setup() {
|
|
19
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'haha-oauth-test-'))
|
|
20
|
+
originalConfigDir = process.env.CLAUDE_CONFIG_DIR
|
|
21
|
+
process.env.CLAUDE_CONFIG_DIR = tmpDir
|
|
22
|
+
service = new HahaOAuthService()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function teardown() {
|
|
26
|
+
if (originalConfigDir === undefined) {
|
|
27
|
+
delete process.env.CLAUDE_CONFIG_DIR
|
|
28
|
+
} else {
|
|
29
|
+
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
|
|
30
|
+
}
|
|
31
|
+
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('HahaOAuthService — file storage', () => {
|
|
35
|
+
beforeEach(setup)
|
|
36
|
+
afterEach(teardown)
|
|
37
|
+
|
|
38
|
+
test('loadTokens returns null when file does not exist', async () => {
|
|
39
|
+
expect(await service.loadTokens()).toBeNull()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('saveTokens writes file with 0600 permissions', async () => {
|
|
43
|
+
const tokens: StoredOAuthTokens = {
|
|
44
|
+
accessToken: 'sk-ant-oat01-xxx',
|
|
45
|
+
refreshToken: 'sk-ant-ort01-xxx',
|
|
46
|
+
expiresAt: Date.now() + 3600_000,
|
|
47
|
+
scopes: ['user:inference', 'user:profile'],
|
|
48
|
+
subscriptionType: 'max',
|
|
49
|
+
}
|
|
50
|
+
await service.saveTokens(tokens)
|
|
51
|
+
|
|
52
|
+
const oauthPath = path.join(tmpDir, 'bingo', 'oauth.json')
|
|
53
|
+
const stat = await fs.stat(oauthPath)
|
|
54
|
+
expect(stat.mode & 0o777).toBe(0o600)
|
|
55
|
+
|
|
56
|
+
const loaded = await service.loadTokens()
|
|
57
|
+
expect(loaded).toEqual(tokens)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('deleteTokens removes file', async () => {
|
|
61
|
+
await service.saveTokens({
|
|
62
|
+
accessToken: 'a',
|
|
63
|
+
refreshToken: null,
|
|
64
|
+
expiresAt: null,
|
|
65
|
+
scopes: [],
|
|
66
|
+
subscriptionType: null,
|
|
67
|
+
})
|
|
68
|
+
await service.deleteTokens()
|
|
69
|
+
expect(await service.loadTokens()).toBeNull()
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('HahaOAuthService — session management', () => {
|
|
74
|
+
beforeEach(setup)
|
|
75
|
+
afterEach(teardown)
|
|
76
|
+
|
|
77
|
+
test('startSession creates session with PKCE + state', () => {
|
|
78
|
+
const session = service.startSession({ serverPort: 54321 })
|
|
79
|
+
expect(session.state).toMatch(/^[A-Za-z0-9_-]{43}$/)
|
|
80
|
+
expect(session.codeVerifier).toMatch(/^[A-Za-z0-9_-]{43}$/)
|
|
81
|
+
expect(session.authorizeUrl).toContain('code_challenge_method=S256')
|
|
82
|
+
expect(session.authorizeUrl).toContain(`state=${encodeURIComponent(session.state)}`)
|
|
83
|
+
expect(session.authorizeUrl).toContain('redirect_uri=')
|
|
84
|
+
expect(session.authorizeUrl).toContain(encodeURIComponent(
|
|
85
|
+
'http://localhost:54321/callback',
|
|
86
|
+
))
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('getSession returns stored session by state', () => {
|
|
90
|
+
const session = service.startSession({ serverPort: 54321 })
|
|
91
|
+
const found = service.getSession(session.state)
|
|
92
|
+
expect(found?.codeVerifier).toBe(session.codeVerifier)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('getSession returns null for unknown state', () => {
|
|
96
|
+
expect(service.getSession('unknown-state')).toBeNull()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('consumeSession removes session after fetch', () => {
|
|
100
|
+
const session = service.startSession({ serverPort: 54321 })
|
|
101
|
+
expect(service.consumeSession(session.state)).not.toBeNull()
|
|
102
|
+
expect(service.getSession(session.state)).toBeNull()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('completeSession stores subscription type fetched from profile info', async () => {
|
|
106
|
+
const session = service.startSession({ serverPort: 54321 })
|
|
107
|
+
;(service as any).exchangeWithCustomCallback = async () => ({
|
|
108
|
+
access_token: 'fresh-access-token',
|
|
109
|
+
refresh_token: 'fresh-refresh-token',
|
|
110
|
+
expires_in: 3600,
|
|
111
|
+
scope: 'user:inference',
|
|
112
|
+
})
|
|
113
|
+
service.setFetchProfileFn(async () => ({
|
|
114
|
+
subscriptionType: 'team',
|
|
115
|
+
}))
|
|
116
|
+
|
|
117
|
+
const tokens = await service.completeSession('authorization-code', session.state)
|
|
118
|
+
|
|
119
|
+
expect(tokens.subscriptionType).toBe('team')
|
|
120
|
+
expect((await service.loadTokens())?.subscriptionType).toBe('team')
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('HahaOAuthService — ensureFreshAccessToken', () => {
|
|
125
|
+
beforeEach(setup)
|
|
126
|
+
afterEach(teardown)
|
|
127
|
+
|
|
128
|
+
test('returns null when no token file exists', async () => {
|
|
129
|
+
expect(await service.ensureFreshAccessToken()).toBeNull()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('returns token unchanged if not expired', async () => {
|
|
133
|
+
const tokens: StoredOAuthTokens = {
|
|
134
|
+
accessToken: 'still-valid',
|
|
135
|
+
refreshToken: 'refresh-xxx',
|
|
136
|
+
expiresAt: Date.now() + 30 * 60_000,
|
|
137
|
+
scopes: ['user:inference'],
|
|
138
|
+
subscriptionType: 'max',
|
|
139
|
+
}
|
|
140
|
+
await service.saveTokens(tokens)
|
|
141
|
+
|
|
142
|
+
expect(await service.ensureFreshAccessToken()).toBe('still-valid')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('refreshes token when expired (within 5-min buffer)', async () => {
|
|
146
|
+
const oldTokens: StoredOAuthTokens = {
|
|
147
|
+
accessToken: 'expired',
|
|
148
|
+
refreshToken: 'refresh-xxx',
|
|
149
|
+
expiresAt: Date.now() + 60_000,
|
|
150
|
+
scopes: ['user:inference'],
|
|
151
|
+
subscriptionType: 'max',
|
|
152
|
+
}
|
|
153
|
+
await service.saveTokens(oldTokens)
|
|
154
|
+
|
|
155
|
+
service.setRefreshFn(async () => ({
|
|
156
|
+
accessToken: 'new-fresh-token',
|
|
157
|
+
refreshToken: 'new-refresh-xxx',
|
|
158
|
+
expiresAt: Date.now() + 3600_000,
|
|
159
|
+
scopes: ['user:inference'],
|
|
160
|
+
subscriptionType: 'max',
|
|
161
|
+
rateLimitTier: null,
|
|
162
|
+
}))
|
|
163
|
+
|
|
164
|
+
const fresh = await service.ensureFreshAccessToken()
|
|
165
|
+
expect(fresh).toBe('new-fresh-token')
|
|
166
|
+
|
|
167
|
+
const loaded = await service.loadTokens()
|
|
168
|
+
expect(loaded?.accessToken).toBe('new-fresh-token')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('returns null when refresh fails', async () => {
|
|
172
|
+
await service.saveTokens({
|
|
173
|
+
accessToken: 'expired',
|
|
174
|
+
refreshToken: 'bad-refresh',
|
|
175
|
+
expiresAt: Date.now() + 60_000,
|
|
176
|
+
scopes: ['user:inference'],
|
|
177
|
+
subscriptionType: null,
|
|
178
|
+
})
|
|
179
|
+
service.setRefreshFn(async () => {
|
|
180
|
+
throw new Error('401 Unauthorized')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
expect(await service.ensureFreshAccessToken()).toBeNull()
|
|
184
|
+
})
|
|
185
|
+
})
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用真实的 Provider 配置测试 ProviderService
|
|
3
|
+
* 验证添加、激活、bingo/settings.json 同步是否正确
|
|
4
|
+
* (provider env 写到 ~/.claude/bingo/settings.json,不污染原版 settings.json)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
|
|
8
|
+
import * as fs from 'fs/promises'
|
|
9
|
+
import * as path from 'path'
|
|
10
|
+
import * as os from 'os'
|
|
11
|
+
import { ProviderService } from '../services/providerService.js'
|
|
12
|
+
|
|
13
|
+
const MODEL_MAPPING = {
|
|
14
|
+
main: 'MiniMax-M2.7-highspeed',
|
|
15
|
+
haiku: 'MiniMax-M2.7-highspeed',
|
|
16
|
+
sonnet: 'MiniMax-M2.7-highspeed',
|
|
17
|
+
opus: 'MiniMax-M2.7-highspeed',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('Real Provider Configs', () => {
|
|
21
|
+
let tmpDir: string
|
|
22
|
+
let service: ProviderService
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'provider-real-'))
|
|
26
|
+
process.env.CLAUDE_CONFIG_DIR = tmpDir
|
|
27
|
+
service = new ProviderService()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
delete process.env.CLAUDE_CONFIG_DIR
|
|
32
|
+
await fs.rm(tmpDir, { recursive: true, force: true })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Helper: read the Haha-specific settings file
|
|
36
|
+
async function readCcHahaSettings(): Promise<Record<string, unknown>> {
|
|
37
|
+
const raw = await fs.readFile(path.join(tmpDir, 'bingo', 'settings.json'), 'utf-8')
|
|
38
|
+
return JSON.parse(raw)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Helper: check original settings.json is NOT modified
|
|
42
|
+
async function originalSettingsExists(): Promise<boolean> {
|
|
43
|
+
try {
|
|
44
|
+
await fs.access(path.join(tmpDir, 'settings.json'))
|
|
45
|
+
return true
|
|
46
|
+
} catch {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test('添加 MiniMax Provider 并激活 — 写入 bingo/settings.json', async () => {
|
|
52
|
+
const minimax = await service.addProvider({
|
|
53
|
+
presetId: 'minimax',
|
|
54
|
+
name: 'MiniMax',
|
|
55
|
+
baseUrl: 'https://api.minimaxi.com/anthropic',
|
|
56
|
+
apiKey: 'sk-fake-test-key-for-testing-only',
|
|
57
|
+
models: MODEL_MAPPING,
|
|
58
|
+
notes: 'MiniMax 官方 Anthropic 兼容接口',
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(minimax.name).toBe('MiniMax')
|
|
62
|
+
|
|
63
|
+
// 激活 provider
|
|
64
|
+
await service.activateProvider(minimax.id)
|
|
65
|
+
|
|
66
|
+
// 验证写入 bingo/settings.json
|
|
67
|
+
const settings = await readCcHahaSettings()
|
|
68
|
+
expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.minimaxi.com/anthropic')
|
|
69
|
+
expect((settings.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('sk-fake-test-key-for-testing-only')
|
|
70
|
+
expect((settings.env as Record<string, string>).ANTHROPIC_MODEL).toBe('MiniMax-M2.7-highspeed')
|
|
71
|
+
|
|
72
|
+
// 验证原版 settings.json 没有被创建
|
|
73
|
+
expect(await originalSettingsExists()).toBe(false)
|
|
74
|
+
|
|
75
|
+
console.log('✅ Provider 写入 bingo/settings.json,原版 settings.json 未被污染')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('切换 Provider — 更新 bingo/settings.json', async () => {
|
|
79
|
+
const minimax = await service.addProvider({
|
|
80
|
+
presetId: 'minimax',
|
|
81
|
+
name: 'MiniMax',
|
|
82
|
+
baseUrl: 'https://api.minimaxi.com/anthropic',
|
|
83
|
+
apiKey: 'sk-api-test-minimax',
|
|
84
|
+
models: MODEL_MAPPING,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const jiekou = await service.addProvider({
|
|
88
|
+
presetId: 'custom',
|
|
89
|
+
name: '接口AI中转站',
|
|
90
|
+
baseUrl: 'https://api.jiekou.ai/anthropic',
|
|
91
|
+
apiKey: 'sk-fake-test-key-for-testing-only',
|
|
92
|
+
models: {
|
|
93
|
+
main: 'claude-opus-4-7',
|
|
94
|
+
haiku: 'claude-haiku-4-5',
|
|
95
|
+
sonnet: 'claude-sonnet-4-6',
|
|
96
|
+
opus: 'claude-opus-4-7',
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// 先激活 MiniMax
|
|
101
|
+
await service.activateProvider(minimax.id)
|
|
102
|
+
let settings = await readCcHahaSettings()
|
|
103
|
+
expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.minimaxi.com/anthropic')
|
|
104
|
+
|
|
105
|
+
// 切换到接口AI中转站
|
|
106
|
+
await service.activateProvider(jiekou.id)
|
|
107
|
+
settings = await readCcHahaSettings()
|
|
108
|
+
expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.jiekou.ai/anthropic')
|
|
109
|
+
expect((settings.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('sk-fake-test-key-for-testing-only')
|
|
110
|
+
expect((settings.env as Record<string, string>).ANTHROPIC_MODEL).toBe('claude-opus-4-7')
|
|
111
|
+
|
|
112
|
+
// 验证 activeId 正确
|
|
113
|
+
const list = await service.listProviders()
|
|
114
|
+
expect(list.activeId).toBe(jiekou.id)
|
|
115
|
+
|
|
116
|
+
// 原版 settings.json 依然不存在
|
|
117
|
+
expect(await originalSettingsExists()).toBe(false)
|
|
118
|
+
|
|
119
|
+
console.log('✅ 切换 Provider 成功,bingo/settings.json 更新正确')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('bingo/settings.json 保留已有字段', async () => {
|
|
123
|
+
// 预写一个有内容的 bingo/settings.json(模拟用户已有配置)
|
|
124
|
+
await fs.mkdir(path.join(tmpDir, 'bingo'), { recursive: true })
|
|
125
|
+
await fs.writeFile(
|
|
126
|
+
path.join(tmpDir, 'bingo', 'settings.json'),
|
|
127
|
+
JSON.stringify({
|
|
128
|
+
customField: 'should_be_preserved',
|
|
129
|
+
env: {
|
|
130
|
+
EXISTING_VAR: 'should_be_preserved',
|
|
131
|
+
},
|
|
132
|
+
}, null, 2),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
// 添加并激活 provider
|
|
136
|
+
const provider = await service.addProvider({
|
|
137
|
+
presetId: 'custom',
|
|
138
|
+
name: '接口AI中转站',
|
|
139
|
+
baseUrl: 'https://api.jiekou.ai/anthropic',
|
|
140
|
+
apiKey: 'sk_test',
|
|
141
|
+
models: {
|
|
142
|
+
main: 'claude-opus-4-7',
|
|
143
|
+
haiku: 'claude-haiku-4-5',
|
|
144
|
+
sonnet: 'claude-sonnet-4-6',
|
|
145
|
+
opus: 'claude-opus-4-7',
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
await service.activateProvider(provider.id)
|
|
149
|
+
|
|
150
|
+
const settings = await readCcHahaSettings()
|
|
151
|
+
|
|
152
|
+
// 验证新字段写入
|
|
153
|
+
expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.jiekou.ai/anthropic')
|
|
154
|
+
expect((settings.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('sk_test')
|
|
155
|
+
|
|
156
|
+
// 验证已有字段保留
|
|
157
|
+
expect(settings.customField).toBe('should_be_preserved')
|
|
158
|
+
expect((settings.env as Record<string, string>).EXISTING_VAR).toBe('should_be_preserved')
|
|
159
|
+
|
|
160
|
+
console.log('✅ bingo/settings.json 已有字段全部保留')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
test('activateOfficial 清除 provider env', async () => {
|
|
164
|
+
const provider = await service.addProvider({
|
|
165
|
+
presetId: 'minimax',
|
|
166
|
+
name: 'MiniMax',
|
|
167
|
+
baseUrl: 'https://api.minimaxi.com/anthropic',
|
|
168
|
+
apiKey: 'sk-test',
|
|
169
|
+
models: MODEL_MAPPING,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
await service.activateProvider(provider.id)
|
|
173
|
+
|
|
174
|
+
// 确认写入了
|
|
175
|
+
let settings = await readCcHahaSettings()
|
|
176
|
+
expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBeDefined()
|
|
177
|
+
|
|
178
|
+
// 切换到 official
|
|
179
|
+
await service.activateOfficial()
|
|
180
|
+
|
|
181
|
+
settings = await readCcHahaSettings()
|
|
182
|
+
const env = settings.env as Record<string, string> | undefined
|
|
183
|
+
expect(env?.ANTHROPIC_BASE_URL).toBeUndefined()
|
|
184
|
+
expect(env?.ANTHROPIC_AUTH_TOKEN).toBeUndefined()
|
|
185
|
+
expect(env?.ANTHROPIC_MODEL).toBeUndefined()
|
|
186
|
+
|
|
187
|
+
console.log('✅ activateOfficial 正确清除了 provider env')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('连通性测试 — 返回结构正确', async () => {
|
|
191
|
+
const result = await service.testProviderConfig({
|
|
192
|
+
baseUrl: 'https://api.minimaxi.com/anthropic',
|
|
193
|
+
apiKey: 'sk-fake-test-key',
|
|
194
|
+
modelId: 'MiniMax-M2.7-highspeed',
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// testProviderConfig 返回 { connectivity: { ... }, proxy?: { ... } }
|
|
198
|
+
expect(result.connectivity).toBeDefined()
|
|
199
|
+
expect(result.connectivity.latencyMs).toBeGreaterThanOrEqual(0)
|
|
200
|
+
expect(result.connectivity.modelUsed).toBe('MiniMax-M2.7-highspeed')
|
|
201
|
+
|
|
202
|
+
console.log('🔌 MiniMax 连通性测试结果:')
|
|
203
|
+
console.log(' success:', result.connectivity.success)
|
|
204
|
+
console.log(' latencyMs:', result.connectivity.latencyMs)
|
|
205
|
+
console.log(' error:', result.connectivity.error)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('providers.json 和 bingo/settings.json 独立于 settings.json', async () => {
|
|
209
|
+
// 模拟原版 Claude Code 的 settings.json 已存在
|
|
210
|
+
await fs.writeFile(
|
|
211
|
+
path.join(tmpDir, 'settings.json'),
|
|
212
|
+
JSON.stringify({
|
|
213
|
+
effortLevel: 'high',
|
|
214
|
+
env: {
|
|
215
|
+
ANTHROPIC_BASE_URL: 'https://original-claude-code.api.com',
|
|
216
|
+
ANTHROPIC_AUTH_TOKEN: 'original-key',
|
|
217
|
+
},
|
|
218
|
+
}, null, 2),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
// Haha 添加并激活自己的 provider
|
|
222
|
+
const provider = await service.addProvider({
|
|
223
|
+
presetId: 'minimax',
|
|
224
|
+
name: 'MiniMax',
|
|
225
|
+
baseUrl: 'https://api.minimaxi.com/anthropic',
|
|
226
|
+
apiKey: 'sk-haha-key',
|
|
227
|
+
models: MODEL_MAPPING,
|
|
228
|
+
})
|
|
229
|
+
await service.activateProvider(provider.id)
|
|
230
|
+
|
|
231
|
+
// 验证原版 settings.json 没被修改
|
|
232
|
+
const original = JSON.parse(await fs.readFile(path.join(tmpDir, 'settings.json'), 'utf-8'))
|
|
233
|
+
expect((original.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://original-claude-code.api.com')
|
|
234
|
+
expect((original.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('original-key')
|
|
235
|
+
expect(original.effortLevel).toBe('high')
|
|
236
|
+
|
|
237
|
+
// 验证 bingo/settings.json 是 Haha 自己的
|
|
238
|
+
const haha = await readCcHahaSettings()
|
|
239
|
+
expect((haha.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.minimaxi.com/anthropic')
|
|
240
|
+
expect((haha.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('sk-haha-key')
|
|
241
|
+
|
|
242
|
+
console.log('✅ 原版 settings.json 完好无损,Haha 配置独立存储')
|
|
243
|
+
})
|
|
244
|
+
})
|