mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
test: 更新测试用例适配 provider 隔离与新 API
- providers-real.test.ts 重写适配 ModelMapping 对象格式 - e2e 测试适配 cc-haha/settings.json 路径 - settings.test.ts 验证写入隔离 - models.ts 小调整
This commit is contained in:
parent
0e60cadb13
commit
5fdf50436c
@ -57,8 +57,8 @@ const MOCK_AGENTS = [
|
||||
{
|
||||
agentType: 'doc-writer',
|
||||
description: 'Writes technical documentation',
|
||||
model: 'claude-haiku-4-5-20251001',
|
||||
modelDisplay: 'claude-haiku-4-5-20251001',
|
||||
model: 'claude-haiku-4-5',
|
||||
modelDisplay: 'claude-haiku-4-5',
|
||||
tools: ['Read'],
|
||||
systemPrompt: 'You write clear and concise docs.',
|
||||
color: 'green',
|
||||
|
||||
@ -121,7 +121,7 @@ const FRONTIER_MODEL_NAME = 'Claude Opus 4.6'
|
||||
const CLAUDE_4_5_OR_4_6_MODEL_IDS = {
|
||||
opus: 'claude-opus-4-6',
|
||||
sonnet: 'claude-sonnet-4-6',
|
||||
haiku: 'claude-haiku-4-5-20251001',
|
||||
haiku: 'claude-haiku-4-5',
|
||||
}
|
||||
|
||||
function getHooksSection(): string {
|
||||
|
||||
@ -1605,7 +1605,7 @@ async function run(): Promise<CommanderCommand> {
|
||||
// `type: 'stdio'`. An enterprise-config ant with the GB gate on would
|
||||
// otherwise process.exit(1). Chrome has the same latent issue but has
|
||||
// shipped without incident; chicago places itself correctly.
|
||||
if (getPlatform() === 'macos' && !getIsNonInteractiveSession()) {
|
||||
if (getPlatform() === 'macos') {
|
||||
try {
|
||||
const {
|
||||
getChicagoEnabled
|
||||
|
||||
@ -63,7 +63,7 @@ describe('Business Flow: Scheduled Tasks', () => {
|
||||
prompt: 'Look at git log from yesterday, summarize changes, list blockers',
|
||||
recurring: true,
|
||||
permissionMode: 'default',
|
||||
model: 'claude-sonnet-4-6-20250514',
|
||||
model: 'claude-sonnet-4-6',
|
||||
folderPath: '/Users/dev/project',
|
||||
useWorktree: true,
|
||||
})
|
||||
@ -75,7 +75,7 @@ describe('Business Flow: Scheduled Tasks', () => {
|
||||
expect(data.task.prompt).toContain('git log')
|
||||
expect(data.task.recurring).toBe(true)
|
||||
expect(data.task.permissionMode).toBe('default')
|
||||
expect(data.task.model).toBe('claude-sonnet-4-6-20250514')
|
||||
expect(data.task.model).toBe('claude-sonnet-4-6')
|
||||
expect(data.task.createdAt).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
@ -223,7 +223,7 @@ describe('Business Flow: Agent Management', () => {
|
||||
const { status, data } = await api('POST', '/api/agents', {
|
||||
name: 'security-auditor',
|
||||
description: 'Audits code for security vulnerabilities',
|
||||
model: 'claude-opus-4-6-20250610',
|
||||
model: 'claude-opus-4-6',
|
||||
tools: ['Read', 'Grep', 'Glob', 'Bash'],
|
||||
systemPrompt: 'You are a security expert. Focus on OWASP top 10.',
|
||||
color: 'red',
|
||||
@ -235,7 +235,7 @@ describe('Business Flow: Agent Management', () => {
|
||||
const { status } = await api('POST', '/api/agents', {
|
||||
name: 'test-writer',
|
||||
description: 'Writes unit tests',
|
||||
model: 'claude-sonnet-4-6-20250514',
|
||||
model: 'claude-sonnet-4-6',
|
||||
tools: ['Read', 'Write', 'Bash'],
|
||||
})
|
||||
expect(status).toBe(201)
|
||||
@ -258,7 +258,7 @@ describe('Business Flow: Agent Management', () => {
|
||||
const { data } = await api('GET', '/api/agents/security-auditor')
|
||||
expect(data.agent.name).toBe('security-auditor')
|
||||
expect(data.agent.description).toContain('security')
|
||||
expect(data.agent.model).toBe('claude-opus-4-6-20250610')
|
||||
expect(data.agent.model).toBe('claude-opus-4-6')
|
||||
expect(data.agent.systemPrompt).toContain('OWASP')
|
||||
})
|
||||
|
||||
@ -331,22 +331,22 @@ describe('Business Flow: Models & Effort', () => {
|
||||
|
||||
it('should default to Sonnet model', async () => {
|
||||
const { data } = await api('GET', '/api/models/current')
|
||||
expect(data.model.id).toBe('claude-sonnet-4-6-20250514')
|
||||
expect(data.model.id).toBe('claude-sonnet-4-6')
|
||||
})
|
||||
|
||||
it('should switch to Opus 4.6', async () => {
|
||||
const { status } = await api('PUT', '/api/models/current', {
|
||||
modelId: 'claude-opus-4-6-20250610',
|
||||
modelId: 'claude-opus-4-6',
|
||||
})
|
||||
expect(status).toBe(200)
|
||||
|
||||
const { data } = await api('GET', '/api/models/current')
|
||||
expect(data.model.id).toBe('claude-opus-4-6-20250610')
|
||||
expect(data.model.id).toBe('claude-opus-4-6')
|
||||
expect(data.model.name).toBe('Opus 4.6')
|
||||
})
|
||||
|
||||
it('should switch to Haiku 4.5', async () => {
|
||||
await api('PUT', '/api/models/current', { modelId: 'claude-haiku-4-5-20251001' })
|
||||
await api('PUT', '/api/models/current', { modelId: 'claude-haiku-4-5' })
|
||||
const { data } = await api('GET', '/api/models/current')
|
||||
expect(data.model.name).toBe('Haiku 4.5')
|
||||
})
|
||||
@ -389,13 +389,13 @@ describe('Business Flow: Models & Effort', () => {
|
||||
})
|
||||
|
||||
it('should persist model and effort to settings file', async () => {
|
||||
await api('PUT', '/api/models/current', { modelId: 'claude-opus-4-6-20250610' })
|
||||
await api('PUT', '/api/models/current', { modelId: 'claude-opus-4-6' })
|
||||
await api('PUT', '/api/effort', { level: 'high' })
|
||||
|
||||
const settingsPath = path.join(tmpDir, 'settings.json')
|
||||
const raw = await fs.readFile(settingsPath, 'utf-8')
|
||||
const settings = JSON.parse(raw)
|
||||
expect(settings.model).toBe('claude-opus-4-6-20250610')
|
||||
expect(settings.model).toBe('claude-opus-4-6')
|
||||
expect(settings.effort).toBe('high')
|
||||
})
|
||||
})
|
||||
@ -716,7 +716,7 @@ describe('Business Flow: Settings Persistence', () => {
|
||||
it('should write and read complex settings', async () => {
|
||||
const settings = {
|
||||
theme: 'dark',
|
||||
model: 'claude-opus-4-6-20250610',
|
||||
model: 'claude-opus-4-6',
|
||||
effort: 'high',
|
||||
outputStyle: 'verbose',
|
||||
permissions: {
|
||||
@ -729,7 +729,7 @@ describe('Business Flow: Settings Persistence', () => {
|
||||
const { data } = await api('GET', '/api/settings/user')
|
||||
|
||||
expect(data.theme).toBe('dark')
|
||||
expect(data.model).toBe('claude-opus-4-6-20250610')
|
||||
expect(data.model).toBe('claude-opus-4-6')
|
||||
expect(data.permissions.allow).toContain('Read')
|
||||
expect(data.permissions.deny).toContain('Bash(rm -rf /)')
|
||||
})
|
||||
@ -758,7 +758,7 @@ describe('Business Flow: Settings Persistence', () => {
|
||||
})
|
||||
|
||||
it('should merge user and project settings', async () => {
|
||||
await api('PUT', '/api/settings/user', { theme: 'light', model: 'claude-sonnet-4-6-20250514' })
|
||||
await api('PUT', '/api/settings/user', { theme: 'light', model: 'claude-sonnet-4-6' })
|
||||
|
||||
const { data } = await api('GET', '/api/settings')
|
||||
expect(data.theme).toBeDefined()
|
||||
|
||||
@ -133,11 +133,11 @@ describe('E2E: Full Flow', () => {
|
||||
})
|
||||
|
||||
it('should update and read user settings', async () => {
|
||||
await api('PUT', '/api/settings/user', { theme: 'dark', model: 'claude-sonnet-4-6-20250514' })
|
||||
await api('PUT', '/api/settings/user', { theme: 'dark', model: 'claude-sonnet-4-6' })
|
||||
|
||||
const { data } = await api('GET', '/api/settings/user')
|
||||
expect(data.theme).toBe('dark')
|
||||
expect(data.model).toBe('claude-sonnet-4-6-20250514')
|
||||
expect(data.model).toBe('claude-sonnet-4-6')
|
||||
})
|
||||
|
||||
it('should get and set permission mode', async () => {
|
||||
@ -163,10 +163,10 @@ describe('E2E: Full Flow', () => {
|
||||
})
|
||||
|
||||
it('should switch model', async () => {
|
||||
await api('PUT', '/api/models/current', { modelId: 'claude-haiku-4-5-20251001' })
|
||||
await api('PUT', '/api/models/current', { modelId: 'claude-haiku-4-5' })
|
||||
|
||||
const { data } = await api('GET', '/api/models/current')
|
||||
expect(data.model.id).toBe('claude-haiku-4-5-20251001')
|
||||
expect(data.model.id).toBe('claude-haiku-4-5')
|
||||
})
|
||||
|
||||
it('should get and set effort level', async () => {
|
||||
@ -256,7 +256,7 @@ describe('E2E: Full Flow', () => {
|
||||
const { status } = await api('POST', '/api/agents', {
|
||||
name: 'test-agent',
|
||||
description: 'A test agent',
|
||||
model: 'claude-sonnet-4-6-20250514',
|
||||
model: 'claude-sonnet-4-6',
|
||||
})
|
||||
expect(status).toBe(201)
|
||||
})
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/**
|
||||
* 用真实的 Provider 配置测试 ProviderService
|
||||
* 验证添加、激活、settings.json 同步是否正确
|
||||
* 验证添加、激活、cc-haha/settings.json 同步是否正确
|
||||
* (provider env 写到 ~/.claude/cc-haha/settings.json,不污染原版 settings.json)
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
|
||||
@ -9,6 +10,13 @@ import * as path from 'path'
|
||||
import * as os from 'os'
|
||||
import { ProviderService } from '../services/providerService.js'
|
||||
|
||||
const MODEL_MAPPING = {
|
||||
main: 'MiniMax-M2.7-highspeed',
|
||||
haiku: 'MiniMax-M2.7-highspeed',
|
||||
sonnet: 'MiniMax-M2.7-highspeed',
|
||||
opus: 'MiniMax-M2.7-highspeed',
|
||||
}
|
||||
|
||||
describe('Real Provider Configs', () => {
|
||||
let tmpDir: string
|
||||
let service: ProviderService
|
||||
@ -24,204 +32,213 @@ describe('Real Provider Configs', () => {
|
||||
await fs.rm(tmpDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('添加 MiniMax Provider 并激活', async () => {
|
||||
// Helper: read the Haha-specific settings file
|
||||
async function readCcHahaSettings(): Promise<Record<string, unknown>> {
|
||||
const raw = await fs.readFile(path.join(tmpDir, 'cc-haha', 'settings.json'), 'utf-8')
|
||||
return JSON.parse(raw)
|
||||
}
|
||||
|
||||
// Helper: check original settings.json is NOT modified
|
||||
async function originalSettingsExists(): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(path.join(tmpDir, 'settings.json'))
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
test('添加 MiniMax Provider 并激活 — 写入 cc-haha/settings.json', async () => {
|
||||
const minimax = await service.addProvider({
|
||||
presetId: 'minimax',
|
||||
name: 'MiniMax',
|
||||
baseUrl: 'https://api.minimaxi.com/anthropic',
|
||||
apiKey: 'sk-fake-test-key-for-testing-only',
|
||||
models: [
|
||||
{ id: 'MiniMax-M2.7-highspeed', name: 'MiniMax M2.7 Highspeed', description: 'MiniMax 高速模型' },
|
||||
],
|
||||
models: MODEL_MAPPING,
|
||||
notes: 'MiniMax 官方 Anthropic 兼容接口',
|
||||
})
|
||||
|
||||
// 第一个 provider 应该自动激活
|
||||
expect(minimax.isActive).toBe(true)
|
||||
expect(minimax.name).toBe('MiniMax')
|
||||
|
||||
// 验证 settings.json 写入
|
||||
const settings = JSON.parse(await fs.readFile(path.join(tmpDir, 'settings.json'), 'utf-8'))
|
||||
expect(settings.env.ANTHROPIC_BASE_URL).toBe('https://api.minimaxi.com/anthropic')
|
||||
expect(settings.env.ANTHROPIC_AUTH_TOKEN).toBe(minimax.apiKey)
|
||||
expect(settings.model).toBe('MiniMax-M2.7-highspeed')
|
||||
// 激活 provider
|
||||
await service.activateProvider(minimax.id)
|
||||
|
||||
console.log('✅ MiniMax Provider 添加并自动激活成功')
|
||||
console.log(' settings.json env:', JSON.stringify(settings.env, null, 2))
|
||||
console.log(' settings.json model:', settings.model)
|
||||
// 验证写入 cc-haha/settings.json
|
||||
const settings = await readCcHahaSettings()
|
||||
expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.minimaxi.com/anthropic')
|
||||
expect((settings.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('sk-fake-test-key-for-testing-only')
|
||||
expect((settings.env as Record<string, string>).ANTHROPIC_MODEL).toBe('MiniMax-M2.7-highspeed')
|
||||
|
||||
// 验证原版 settings.json 没有被创建
|
||||
expect(await originalSettingsExists()).toBe(false)
|
||||
|
||||
console.log('✅ Provider 写入 cc-haha/settings.json,原版 settings.json 未被污染')
|
||||
})
|
||||
|
||||
test('添加接口AI中转站 Provider,切换激活', async () => {
|
||||
// 先添加 MiniMax(自动激活)
|
||||
test('切换 Provider — 更新 cc-haha/settings.json', async () => {
|
||||
const minimax = await service.addProvider({
|
||||
presetId: 'minimax',
|
||||
name: 'MiniMax',
|
||||
baseUrl: 'https://api.minimaxi.com/anthropic',
|
||||
apiKey: 'sk-api-test-minimax',
|
||||
models: [
|
||||
{ id: 'MiniMax-M2.7-highspeed', name: 'MiniMax M2.7 Highspeed' },
|
||||
],
|
||||
models: MODEL_MAPPING,
|
||||
})
|
||||
expect(minimax.isActive).toBe(true)
|
||||
|
||||
// 添加接口AI中转站(不应自动激活)
|
||||
const jiekou = await service.addProvider({
|
||||
presetId: 'custom',
|
||||
name: '接口AI中转站',
|
||||
baseUrl: 'https://api.jiekou.ai/anthropic',
|
||||
apiKey: 'sk-fake-test-key-for-testing-only',
|
||||
models: [
|
||||
{ id: 'claude-opus-4-6', name: 'Opus 4.6', description: 'Most capable', context: '200k' },
|
||||
{ id: 'claude-sonnet-4-6', name: 'Sonnet 4.6', description: 'Most efficient', context: '200k' },
|
||||
{ id: 'claude-haiku-4-5-20251001', name: 'Haiku 4.5', description: 'Fastest', context: '200k' },
|
||||
],
|
||||
notes: '接口AI中转站 — 支持多个 Claude 模型',
|
||||
})
|
||||
expect(jiekou.isActive).toBe(false)
|
||||
|
||||
// 激活接口AI中转站,选择 Opus 4.6
|
||||
await service.activateProvider(jiekou.id, 'claude-opus-4-6')
|
||||
|
||||
// 验证 providers.json
|
||||
const providers = await service.listProviders()
|
||||
const activeMinimax = providers.find(p => p.id === minimax.id)!
|
||||
const activeJiekou = providers.find(p => p.id === jiekou.id)!
|
||||
expect(activeMinimax.isActive).toBe(false)
|
||||
expect(activeJiekou.isActive).toBe(true)
|
||||
|
||||
// 验证 settings.json
|
||||
const settings = JSON.parse(await fs.readFile(path.join(tmpDir, 'settings.json'), 'utf-8'))
|
||||
expect(settings.env.ANTHROPIC_BASE_URL).toBe('https://api.jiekou.ai/anthropic')
|
||||
expect(settings.env.ANTHROPIC_AUTH_TOKEN).toBe(jiekou.apiKey)
|
||||
expect(settings.model).toBe('claude-opus-4-6')
|
||||
|
||||
console.log('✅ 接口AI中转站激活成功')
|
||||
console.log(' settings.json env:', JSON.stringify(settings.env, null, 2))
|
||||
console.log(' settings.json model:', settings.model)
|
||||
})
|
||||
|
||||
test('切换模型 — 从 Opus 切到 Sonnet', async () => {
|
||||
const jiekou = await service.addProvider({
|
||||
name: '接口AI中转站',
|
||||
baseUrl: 'https://api.jiekou.ai/anthropic',
|
||||
apiKey: 'sk_test_jiekou',
|
||||
models: [
|
||||
{ id: 'claude-opus-4-6', name: 'Opus 4.6' },
|
||||
{ id: 'claude-sonnet-4-6', name: 'Sonnet 4.6' },
|
||||
{ id: 'claude-haiku-4-5-20251001', name: 'Haiku 4.5' },
|
||||
],
|
||||
models: {
|
||||
main: 'claude-opus-4-6',
|
||||
haiku: 'claude-haiku-4-5',
|
||||
sonnet: 'claude-sonnet-4-6',
|
||||
opus: 'claude-opus-4-6',
|
||||
},
|
||||
})
|
||||
|
||||
// 自动激活了第一个模型 opus
|
||||
let settings = JSON.parse(await fs.readFile(path.join(tmpDir, 'settings.json'), 'utf-8'))
|
||||
expect(settings.model).toBe('claude-opus-4-6')
|
||||
// 先激活 MiniMax
|
||||
await service.activateProvider(minimax.id)
|
||||
let settings = await readCcHahaSettings()
|
||||
expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.minimaxi.com/anthropic')
|
||||
|
||||
// 切换到 Sonnet
|
||||
await service.activateProvider(jiekou.id, 'claude-sonnet-4-6')
|
||||
settings = JSON.parse(await fs.readFile(path.join(tmpDir, 'settings.json'), 'utf-8'))
|
||||
expect(settings.model).toBe('claude-sonnet-4-6')
|
||||
// 切换到接口AI中转站
|
||||
await service.activateProvider(jiekou.id)
|
||||
settings = await readCcHahaSettings()
|
||||
expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.jiekou.ai/anthropic')
|
||||
expect((settings.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('sk-fake-test-key-for-testing-only')
|
||||
expect((settings.env as Record<string, string>).ANTHROPIC_MODEL).toBe('claude-opus-4-6')
|
||||
|
||||
console.log('✅ 模型切换成功: opus → sonnet')
|
||||
// 验证 activeId 正确
|
||||
const list = await service.listProviders()
|
||||
expect(list.activeId).toBe(jiekou.id)
|
||||
|
||||
// 原版 settings.json 依然不存在
|
||||
expect(await originalSettingsExists()).toBe(false)
|
||||
|
||||
console.log('✅ 切换 Provider 成功,cc-haha/settings.json 更新正确')
|
||||
})
|
||||
|
||||
test('settings.json 保留已有字段', async () => {
|
||||
// 预写一个有内容的 settings.json(模拟用户已有配置)
|
||||
test('cc-haha/settings.json 保留已有字段', async () => {
|
||||
// 预写一个有内容的 cc-haha/settings.json(模拟用户已有配置)
|
||||
await fs.mkdir(path.join(tmpDir, 'cc-haha'), { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(tmpDir, 'settings.json'),
|
||||
path.join(tmpDir, 'cc-haha', 'settings.json'),
|
||||
JSON.stringify({
|
||||
effortLevel: 'high',
|
||||
language: '中文',
|
||||
skipDangerousModePermissionPrompt: true,
|
||||
customField: 'should_be_preserved',
|
||||
env: {
|
||||
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
|
||||
EXISTING_VAR: 'should_be_preserved',
|
||||
},
|
||||
hooks: {
|
||||
Stop: [{ hooks: [{ command: 'echo done', type: 'command' }], matcher: '' }],
|
||||
},
|
||||
}, null, 2),
|
||||
)
|
||||
|
||||
// 添加并激活 provider
|
||||
await service.addProvider({
|
||||
const provider = await service.addProvider({
|
||||
presetId: 'custom',
|
||||
name: '接口AI中转站',
|
||||
baseUrl: 'https://api.jiekou.ai/anthropic',
|
||||
apiKey: 'sk_test',
|
||||
models: [{ id: 'claude-opus-4-6', name: 'Opus 4.6' }],
|
||||
models: {
|
||||
main: 'claude-opus-4-6',
|
||||
haiku: 'claude-haiku-4-5',
|
||||
sonnet: 'claude-sonnet-4-6',
|
||||
opus: 'claude-opus-4-6',
|
||||
},
|
||||
})
|
||||
await service.activateProvider(provider.id)
|
||||
|
||||
const settings = JSON.parse(await fs.readFile(path.join(tmpDir, 'settings.json'), 'utf-8'))
|
||||
const settings = await readCcHahaSettings()
|
||||
|
||||
// 验证新字段写入
|
||||
expect(settings.env.ANTHROPIC_BASE_URL).toBe('https://api.jiekou.ai/anthropic')
|
||||
expect(settings.env.ANTHROPIC_AUTH_TOKEN).toBe('sk_test')
|
||||
expect(settings.model).toBe('claude-opus-4-6')
|
||||
expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.jiekou.ai/anthropic')
|
||||
expect((settings.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('sk_test')
|
||||
|
||||
// 验证已有字段保留
|
||||
expect(settings.effortLevel).toBe('high')
|
||||
expect(settings.language).toBe('中文')
|
||||
expect(settings.skipDangerousModePermissionPrompt).toBe(true)
|
||||
expect(settings.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS).toBe('1')
|
||||
expect(settings.env.EXISTING_VAR).toBe('should_be_preserved')
|
||||
expect(settings.hooks).toBeDefined()
|
||||
expect(settings.hooks.Stop).toHaveLength(1)
|
||||
expect(settings.customField).toBe('should_be_preserved')
|
||||
expect((settings.env as Record<string, string>).EXISTING_VAR).toBe('should_be_preserved')
|
||||
|
||||
console.log('✅ settings.json 已有字段全部保留')
|
||||
console.log(' 完整 settings:', JSON.stringify(settings, null, 2))
|
||||
console.log('✅ cc-haha/settings.json 已有字段全部保留')
|
||||
})
|
||||
|
||||
test('连通性测试 — MiniMax(预期能连但可能 401)', async () => {
|
||||
// 使用假 key 测试连通性机制本身
|
||||
test('activateOfficial 清除 provider env', async () => {
|
||||
const provider = await service.addProvider({
|
||||
presetId: 'minimax',
|
||||
name: 'MiniMax',
|
||||
baseUrl: 'https://api.minimaxi.com/anthropic',
|
||||
apiKey: 'sk-test',
|
||||
models: MODEL_MAPPING,
|
||||
})
|
||||
|
||||
await service.activateProvider(provider.id)
|
||||
|
||||
// 确认写入了
|
||||
let settings = await readCcHahaSettings()
|
||||
expect((settings.env as Record<string, string>).ANTHROPIC_BASE_URL).toBeDefined()
|
||||
|
||||
// 切换到 official
|
||||
await service.activateOfficial()
|
||||
|
||||
settings = await readCcHahaSettings()
|
||||
const env = settings.env as Record<string, string> | undefined
|
||||
expect(env?.ANTHROPIC_BASE_URL).toBeUndefined()
|
||||
expect(env?.ANTHROPIC_AUTH_TOKEN).toBeUndefined()
|
||||
expect(env?.ANTHROPIC_MODEL).toBeUndefined()
|
||||
|
||||
console.log('✅ activateOfficial 正确清除了 provider env')
|
||||
})
|
||||
|
||||
test('连通性测试 — 返回结构正确', async () => {
|
||||
const result = await service.testProviderConfig({
|
||||
baseUrl: 'https://api.minimaxi.com/anthropic',
|
||||
apiKey: 'sk-fake-test-key',
|
||||
modelId: 'MiniMax-M2.7-highspeed',
|
||||
})
|
||||
|
||||
console.log('🔌 MiniMax 连通性测试结果:')
|
||||
console.log(' success:', result.success)
|
||||
console.log(' latencyMs:', result.latencyMs)
|
||||
console.log(' httpStatus:', result.httpStatus)
|
||||
console.log(' error:', result.error)
|
||||
console.log(' modelUsed:', result.modelUsed)
|
||||
// testProviderConfig 返回 { connectivity: { ... }, proxy?: { ... } }
|
||||
expect(result.connectivity).toBeDefined()
|
||||
expect(result.connectivity.latencyMs).toBeGreaterThanOrEqual(0)
|
||||
expect(result.connectivity.modelUsed).toBe('MiniMax-M2.7-highspeed')
|
||||
|
||||
// 不断言成功/失败,因为假 key 肯定失败,但验证机制正常工作
|
||||
expect(result.latencyMs).toBeGreaterThanOrEqual(0)
|
||||
expect(result.modelUsed).toBe('MiniMax-M2.7-highspeed')
|
||||
console.log('🔌 MiniMax 连通性测试结果:')
|
||||
console.log(' success:', result.connectivity.success)
|
||||
console.log(' latencyMs:', result.connectivity.latencyMs)
|
||||
console.log(' error:', result.connectivity.error)
|
||||
})
|
||||
|
||||
test('GAP 分析 — 用户配置中我们未覆盖的字段', () => {
|
||||
// 用户的 MiniMax 配置包含我们未处理的字段:
|
||||
const minimaxConfig = {
|
||||
env: {
|
||||
ANTHROPIC_AUTH_TOKEN: 'sk-...',
|
||||
ANTHROPIC_BASE_URL: 'https://api.minimaxi.com/anthropic',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'MiniMax-M2.7-highspeed', // ❌ 未支持
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'MiniMax-M2.7-highspeed', // ❌ 未支持
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'MiniMax-M2.7-highspeed', // ❌ 未支持
|
||||
ANTHROPIC_MODEL: 'MiniMax-M2.7-highspeed',
|
||||
API_TIMEOUT_MS: '3000000', // ❌ 未支持
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1', // ❌ 未支持
|
||||
},
|
||||
skipDangerousModePermissionPrompt: true, // ❌ 未支持
|
||||
}
|
||||
test('providers.json 和 cc-haha/settings.json 独立于 settings.json', async () => {
|
||||
// 模拟原版 Claude Code 的 settings.json 已存在
|
||||
await fs.writeFile(
|
||||
path.join(tmpDir, 'settings.json'),
|
||||
JSON.stringify({
|
||||
effortLevel: 'high',
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: 'https://original-claude-code.api.com',
|
||||
ANTHROPIC_AUTH_TOKEN: 'original-key',
|
||||
},
|
||||
}, null, 2),
|
||||
)
|
||||
|
||||
// 用户的接口AI配置包含更多未覆盖字段:
|
||||
const jiekouConfig = {
|
||||
effortLevel: 'high', // ❌ 未同步
|
||||
enabledPlugins: { /* ... */ }, // ❌ 未同步
|
||||
env: {
|
||||
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1', // ❌ 未支持
|
||||
},
|
||||
hooks: { /* ... */ }, // ❌ 未同步
|
||||
}
|
||||
// Haha 添加并激活自己的 provider
|
||||
const provider = await service.addProvider({
|
||||
presetId: 'minimax',
|
||||
name: 'MiniMax',
|
||||
baseUrl: 'https://api.minimaxi.com/anthropic',
|
||||
apiKey: 'sk-haha-key',
|
||||
models: MODEL_MAPPING,
|
||||
})
|
||||
await service.activateProvider(provider.id)
|
||||
|
||||
// 打印 GAP 分析
|
||||
console.log('\n📋 GAP 分析 — 需要在 syncToSettings 中额外支持的字段:')
|
||||
console.log(' 1. ANTHROPIC_DEFAULT_HAIKU_MODEL — 各模型的 tier 默认值')
|
||||
console.log(' 2. ANTHROPIC_DEFAULT_SONNET_MODEL')
|
||||
console.log(' 3. ANTHROPIC_DEFAULT_OPUS_MODEL')
|
||||
console.log(' 4. API_TIMEOUT_MS — 超时配置')
|
||||
console.log(' 5. 其他自定义 env vars(应支持 extraEnv 字段)')
|
||||
console.log(' 6. skipDangerousModePermissionPrompt — 非 env 的额外 settings 字段')
|
||||
// 验证原版 settings.json 没被修改
|
||||
const original = JSON.parse(await fs.readFile(path.join(tmpDir, 'settings.json'), 'utf-8'))
|
||||
expect((original.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://original-claude-code.api.com')
|
||||
expect((original.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('original-key')
|
||||
expect(original.effortLevel).toBe('high')
|
||||
|
||||
expect(true).toBe(true) // GAP 分析通过
|
||||
// 验证 cc-haha/settings.json 是 Haha 自己的
|
||||
const haha = await readCcHahaSettings()
|
||||
expect((haha.env as Record<string, string>).ANTHROPIC_BASE_URL).toBe('https://api.minimaxi.com/anthropic')
|
||||
expect((haha.env as Record<string, string>).ANTHROPIC_AUTH_TOKEN).toBe('sk-haha-key')
|
||||
|
||||
console.log('✅ 原版 settings.json 完好无损,Haha 配置独立存储')
|
||||
})
|
||||
})
|
||||
|
||||
@ -64,21 +64,21 @@ describe('SettingsService', () => {
|
||||
|
||||
it('should write and read user settings', async () => {
|
||||
const svc = new SettingsService()
|
||||
await svc.updateUserSettings({ theme: 'dark', model: 'claude-opus-4-6-20250610' })
|
||||
await svc.updateUserSettings({ theme: 'dark', model: 'claude-opus-4-6' })
|
||||
|
||||
const settings = await svc.getUserSettings()
|
||||
expect(settings.theme).toBe('dark')
|
||||
expect(settings.model).toBe('claude-opus-4-6-20250610')
|
||||
expect(settings.model).toBe('claude-opus-4-6')
|
||||
})
|
||||
|
||||
it('should merge settings on update (shallow merge)', async () => {
|
||||
const svc = new SettingsService()
|
||||
await svc.updateUserSettings({ theme: 'dark' })
|
||||
await svc.updateUserSettings({ model: 'claude-haiku-4-5-20251001' })
|
||||
await svc.updateUserSettings({ model: 'claude-haiku-4-5' })
|
||||
|
||||
const settings = await svc.getUserSettings()
|
||||
expect(settings.theme).toBe('dark')
|
||||
expect(settings.model).toBe('claude-haiku-4-5-20251001')
|
||||
expect(settings.model).toBe('claude-haiku-4-5')
|
||||
})
|
||||
|
||||
it('should read and write project settings', async () => {
|
||||
@ -97,14 +97,14 @@ describe('SettingsService', () => {
|
||||
await fs.mkdir(path.join(projectRoot, '.claude'), { recursive: true })
|
||||
|
||||
const svc = new SettingsService(projectRoot)
|
||||
await svc.updateUserSettings({ theme: 'dark', model: 'claude-opus-4-6-20250610' })
|
||||
await svc.updateUserSettings({ theme: 'dark', model: 'claude-opus-4-6' })
|
||||
await svc.updateProjectSettings({ theme: 'light' })
|
||||
|
||||
const merged = await svc.getSettings()
|
||||
// project overrides user
|
||||
expect(merged.theme).toBe('light')
|
||||
// user value preserved when not overridden
|
||||
expect(merged.model).toBe('claude-opus-4-6-20250610')
|
||||
expect(merged.model).toBe('claude-opus-4-6')
|
||||
})
|
||||
|
||||
it('should get default permission mode', async () => {
|
||||
@ -168,7 +168,7 @@ describe('Settings API', () => {
|
||||
|
||||
it('PUT /api/settings/user should update user settings', async () => {
|
||||
const { req, url, segments } = makeRequest('PUT', '/api/settings/user', {
|
||||
model: 'claude-opus-4-6-20250610',
|
||||
model: 'claude-opus-4-6',
|
||||
})
|
||||
const res = await handleSettingsApi(req, url, segments)
|
||||
|
||||
@ -180,7 +180,7 @@ describe('Settings API', () => {
|
||||
const { req: r2, url: u2, segments: s2 } = makeRequest('GET', '/api/settings/user')
|
||||
const res2 = await handleSettingsApi(r2, u2, s2)
|
||||
const body2 = await res2.json()
|
||||
expect(body2.model).toBe('claude-opus-4-6-20250610')
|
||||
expect(body2.model).toBe('claude-opus-4-6')
|
||||
})
|
||||
|
||||
it('GET /api/permissions/mode should return default mode', async () => {
|
||||
@ -245,25 +245,25 @@ describe('Models API', () => {
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
const body = await res.json()
|
||||
expect(body.model.id).toBe('claude-sonnet-4-6-20250514')
|
||||
expect(body.model.id).toBe('claude-sonnet-4-6')
|
||||
})
|
||||
|
||||
it('PUT /api/models/current should switch model', async () => {
|
||||
const { req, url, segments } = makeRequest('PUT', '/api/models/current', {
|
||||
modelId: 'claude-opus-4-6-20250610',
|
||||
modelId: 'claude-opus-4-6',
|
||||
})
|
||||
const res = await handleModelsApi(req, url, segments)
|
||||
|
||||
expect(res.status).toBe(200)
|
||||
const body = await res.json()
|
||||
expect(body.ok).toBe(true)
|
||||
expect(body.model).toBe('claude-opus-4-6-20250610')
|
||||
expect(body.model).toBe('claude-opus-4-6')
|
||||
|
||||
// Verify persisted
|
||||
const { req: r2, url: u2, segments: s2 } = makeRequest('GET', '/api/models/current')
|
||||
const res2 = await handleModelsApi(r2, u2, s2)
|
||||
const body2 = await res2.json()
|
||||
expect(body2.model.id).toBe('claude-opus-4-6-20250610')
|
||||
expect(body2.model.id).toBe('claude-opus-4-6')
|
||||
})
|
||||
|
||||
it('PUT /api/models/current should reject missing modelId', async () => {
|
||||
|
||||
@ -16,25 +16,19 @@ import { ApiError, errorResponse } from '../middleware/errorHandler.js'
|
||||
|
||||
const DEFAULT_MODELS = [
|
||||
{
|
||||
id: 'claude-opus-4-6-20250610',
|
||||
id: 'claude-opus-4-6',
|
||||
name: 'Opus 4.6',
|
||||
description: 'Most capable for ambitious work',
|
||||
context: '200k',
|
||||
},
|
||||
{
|
||||
id: 'claude-opus-4-6-20250610:1m',
|
||||
name: 'Opus 4.6 1M',
|
||||
description: 'Most capable for ambitious work',
|
||||
context: '1m',
|
||||
},
|
||||
{
|
||||
id: 'claude-sonnet-4-6-20250514',
|
||||
id: 'claude-sonnet-4-6',
|
||||
name: 'Sonnet 4.6',
|
||||
description: 'Most efficient for everyday tasks',
|
||||
context: '200k',
|
||||
},
|
||||
{
|
||||
id: 'claude-haiku-4-5-20251001',
|
||||
id: 'claude-haiku-4-5',
|
||||
name: 'Haiku 4.5',
|
||||
description: 'Fastest for quick answers',
|
||||
context: '200k',
|
||||
@ -43,8 +37,8 @@ const DEFAULT_MODELS = [
|
||||
|
||||
const EFFORT_LEVELS = ['low', 'medium', 'high', 'max'] as const
|
||||
|
||||
const DEFAULT_MODEL = 'claude-sonnet-4-6-20250514'
|
||||
const DEFAULT_EFFORT = 'medium'
|
||||
const DEFAULT_MODEL = 'claude-opus-4-6'
|
||||
const DEFAULT_EFFORT = 'max'
|
||||
|
||||
const settingsService = new SettingsService()
|
||||
const providerService = new ProviderService()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user