cc-haha/src/server/__tests__/persistence-upgrade.test.ts
程序员阿江(Relakkes) 45ec701124 Bring responsive desktop sessions and thinking controls onto main
This merge carries the worktree fix into the local main branch so long multi-session desktop usage keeps rendering cost bounded while DeepSeek follows the shared General Settings Thinking toggle. It preserves running-session behavior and keeps legacy DeepSeek managed env from pinning thinking off after upgrade.

Constraint: User requested the worktree result merged into local main without pushing.
Confidence: high
Scope-risk: moderate
Directive: Keep DeepSeek controlled by the global Thinking setting; do not restore provider-specific disabled-thinking defaults without both toggle-state tests.
Tested: bun test src/server/__tests__/conversations.test.ts -t "global Thinking setting control DeepSeek"
Tested: bun test src/server/__tests__/persistence-upgrade.test.ts src/server/__tests__/provider-presets.test.ts src/server/__tests__/providers.test.ts src/server/__tests__/title-service.test.ts src/utils/__tests__/thinking.test.ts src/utils/__tests__/providerManagedEnvCompat.test.ts
Tested: cd desktop && bun run test -- MessageList.test.tsx ContextUsageIndicator.test.tsx
Tested: bun run check:server
2026-05-14 22:42:30 +08:00

281 lines
10 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import * as fs from 'fs/promises'
import * as os from 'os'
import * as path from 'path'
import { ProviderService } from '../services/providerService.js'
import {
CURRENT_PROVIDER_INDEX_SCHEMA_VERSION,
ensurePersistentStorageUpgraded,
resetPersistentStorageMigrationsForTests,
} from '../services/persistentStorageMigrations.js'
let tempDir: string
async function listFiles(dir: string) {
try {
return await fs.readdir(dir)
} catch {
return []
}
}
describe('persistent storage upgrade migrations', () => {
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cc-haha-persistence-'))
process.env.CLAUDE_CONFIG_DIR = tempDir
resetPersistentStorageMigrationsForTests()
})
afterEach(async () => {
resetPersistentStorageMigrationsForTests()
delete process.env.CLAUDE_CONFIG_DIR
await fs.rm(tempDir, { recursive: true, force: true })
})
test('migrates legacy providers index and writes a backup before changing it', async () => {
const ccHahaDir = path.join(tempDir, 'cc-haha')
await fs.mkdir(ccHahaDir, { recursive: true })
await fs.writeFile(
path.join(ccHahaDir, 'providers.json'),
JSON.stringify({
activeProviderId: 'provider-1',
rootFutureField: { keep: true },
providers: [{
id: 'provider-1',
presetId: 'custom',
name: 'Legacy Provider',
apiKey: 'token',
baseUrl: 'https://example.test',
models: { main: 'model-main', haiku: '', sonnet: '', opus: '' },
extraFutureField: 'keep-me',
}],
}, null, 2),
'utf-8',
)
const report = await ensurePersistentStorageUpgraded()
expect(report.failures).toEqual([])
expect(report.migratedEntries).toContain('cc-haha/providers.json')
const migrated = JSON.parse(await fs.readFile(path.join(ccHahaDir, 'providers.json'), 'utf-8')) as {
schemaVersion?: number
activeId?: string | null
activeProviderId?: string
rootFutureField?: unknown
providers?: Array<Record<string, unknown>>
}
expect(migrated.schemaVersion).toBe(CURRENT_PROVIDER_INDEX_SCHEMA_VERSION)
expect(migrated.activeId).toBe('provider-1')
expect(migrated.activeProviderId).toBeUndefined()
expect(migrated.rootFutureField).toEqual({ keep: true })
expect(migrated.providers?.[0]?.extraFutureField).toBe('keep-me')
const backups = (await listFiles(ccHahaDir)).filter((file) => file.startsWith('providers.json.bak-before-migration-'))
expect(backups.length).toBe(1)
const service = new ProviderService()
const { providers, activeId } = await service.listProviders()
expect(providers).toHaveLength(1)
expect(activeId).toBe('provider-1')
await service.updateProvider('provider-1', { name: 'Renamed Provider' })
const rewritten = JSON.parse(await fs.readFile(path.join(ccHahaDir, 'providers.json'), 'utf-8')) as {
rootFutureField?: unknown
providers?: Array<Record<string, unknown>>
}
expect(rewritten.rootFutureField).toEqual({ keep: true })
expect(rewritten.providers?.[0]?.extraFutureField).toBe('keep-me')
})
test('imports legacy root providers config into cc-haha storage without deleting the source', async () => {
await fs.writeFile(
path.join(tempDir, 'providers.json'),
JSON.stringify({
version: 1,
activeModel: 'legacy-sonnet',
providers: [{
id: 'legacy-provider',
name: 'Legacy Root Provider',
baseUrl: 'https://legacy.example.test',
apiKey: 'legacy-token',
models: [
{ id: 'legacy-haiku', name: 'Legacy Haiku' },
{ id: 'legacy-sonnet', name: 'Legacy Sonnet' },
],
isActive: true,
createdAt: 1,
updatedAt: 2,
notes: 'keep note',
}],
}, null, 2),
'utf-8',
)
const report = await ensurePersistentStorageUpgraded()
expect(report.failures).toEqual([])
expect(report.migratedEntries).toContain('providers.json -> cc-haha/providers.json')
expect(report.migratedEntries).toContain('providers.json -> cc-haha/settings.json')
expect(JSON.parse(await fs.readFile(path.join(tempDir, 'providers.json'), 'utf-8'))).toMatchObject({
version: 1,
activeModel: 'legacy-sonnet',
})
const migrated = JSON.parse(await fs.readFile(path.join(tempDir, 'cc-haha', 'providers.json'), 'utf-8')) as {
activeId?: string | null
providers?: Array<{
id?: string
presetId?: string
apiFormat?: string
models?: Record<string, string>
notes?: string
}>
}
expect(migrated.activeId).toBe('legacy-provider')
expect(migrated.providers?.[0]).toMatchObject({
id: 'legacy-provider',
presetId: 'custom',
apiFormat: 'anthropic',
notes: 'keep note',
models: {
main: 'legacy-sonnet',
haiku: 'legacy-sonnet',
sonnet: 'legacy-sonnet',
opus: 'legacy-sonnet',
},
})
const managedSettings = JSON.parse(await fs.readFile(path.join(tempDir, 'cc-haha', 'settings.json'), 'utf-8')) as {
env?: Record<string, string>
}
expect(managedSettings.env).toMatchObject({
ANTHROPIC_BASE_URL: 'https://legacy.example.test',
ANTHROPIC_AUTH_TOKEN: 'legacy-token',
ANTHROPIC_MODEL: 'legacy-sonnet',
})
const service = new ProviderService()
const { providers, activeId } = await service.listProviders()
expect(activeId).toBe('legacy-provider')
expect(providers[0]?.models.main).toBe('legacy-sonnet')
})
test('does not overwrite current cc-haha provider storage with a legacy root config', async () => {
const ccHahaDir = path.join(tempDir, 'cc-haha')
await fs.mkdir(ccHahaDir, { recursive: true })
await fs.writeFile(
path.join(tempDir, 'providers.json'),
JSON.stringify({
version: 1,
activeModel: 'legacy-model',
providers: [{
id: 'legacy-provider',
name: 'Legacy Root Provider',
baseUrl: 'https://legacy.example.test',
apiKey: 'legacy-token',
models: [{ id: 'legacy-model' }],
isActive: true,
}],
}, null, 2),
'utf-8',
)
await fs.writeFile(
path.join(ccHahaDir, 'providers.json'),
JSON.stringify({
schemaVersion: CURRENT_PROVIDER_INDEX_SCHEMA_VERSION,
activeId: null,
providers: [],
}, null, 2),
'utf-8',
)
const report = await ensurePersistentStorageUpgraded()
expect(report.failures).toEqual([])
expect(report.migratedEntries).not.toContain('providers.json -> cc-haha/providers.json')
const current = JSON.parse(await fs.readFile(path.join(ccHahaDir, 'providers.json'), 'utf-8')) as {
activeId?: string | null
providers?: unknown[]
}
expect(current.activeId).toBeNull()
expect(current.providers).toEqual([])
})
test('does not write repo-owned schema metadata into shared user settings', async () => {
await fs.writeFile(
path.join(tempDir, 'settings.json'),
JSON.stringify({
defaultMode: 'acceptEdits',
userOwnedFutureField: { nested: true },
}, null, 2),
'utf-8',
)
const report = await ensurePersistentStorageUpgraded()
expect(report.failures).toEqual([])
const settings = JSON.parse(await fs.readFile(path.join(tempDir, 'settings.json'), 'utf-8')) as Record<string, unknown>
expect(settings.schemaVersion).toBeUndefined()
expect(settings.userOwnedFutureField).toEqual({ nested: true })
})
test('quarantines malformed managed settings instead of blocking startup', async () => {
const ccHahaDir = path.join(tempDir, 'cc-haha')
await fs.mkdir(ccHahaDir, { recursive: true })
await fs.writeFile(path.join(ccHahaDir, 'settings.json'), '{"env":', 'utf-8')
const report = await ensurePersistentStorageUpgraded()
expect(report.failures).toEqual([])
expect(report.migratedEntries).toContain('cc-haha/settings.json')
expect(JSON.parse(await fs.readFile(path.join(ccHahaDir, 'settings.json'), 'utf-8'))).toEqual({})
const quarantined = (await listFiles(ccHahaDir)).filter((file) => file.startsWith('settings.json.invalid-'))
expect(quarantined.length).toBe(1)
})
test('upgrades existing DeepSeek managed env to follow global thinking settings', async () => {
const ccHahaDir = path.join(tempDir, 'cc-haha')
await fs.mkdir(ccHahaDir, { recursive: true })
await fs.writeFile(
path.join(ccHahaDir, 'settings.json'),
JSON.stringify({
env: {
ANTHROPIC_BASE_URL: 'https://api.deepseek.com/anthropic',
ANTHROPIC_AUTH_TOKEN: 'test-token',
ANTHROPIC_MODEL: 'deepseek-v4-pro',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'deepseek-v4-flash',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'deepseek-v4-pro',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'deepseek-v4-pro',
CC_HAHA_SEND_DISABLED_THINKING: '1',
USER_CUSTOM_ENV: 'keep-me',
},
}, null, 2),
'utf-8',
)
const report = await ensurePersistentStorageUpgraded()
expect(report.failures).toEqual([])
expect(report.migratedEntries).toContain('cc-haha/settings.json')
const migrated = JSON.parse(await fs.readFile(path.join(ccHahaDir, 'settings.json'), 'utf-8')) as {
env?: Record<string, string>
}
expect(migrated.env?.CC_HAHA_SEND_DISABLED_THINKING).toBeUndefined()
expect(migrated.env?.ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES).toBe(
'thinking,effort,adaptive_thinking,max_effort',
)
expect(migrated.env?.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES).toBe(
'thinking,effort,adaptive_thinking,max_effort',
)
expect(migrated.env?.ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES).toBe(
'thinking,effort,adaptive_thinking,max_effort',
)
expect(migrated.env?.USER_CUSTOM_ENV).toBe('keep-me')
const backups = (await listFiles(ccHahaDir)).filter((file) => file.startsWith('settings.json.bak-before-migration-'))
expect(backups.length).toBe(1)
})
})