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> } 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> } 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 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 } 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 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 } 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) }) })