/** * Integration Test for Hot-Reload Configuration * * Tests the file watching and hot-reload functionality of the credential manager. * Validates that configuration changes are detected and applied without manual restarts. */ import { CredentialManager } from '@/core/credential-manager/CredentialManager' import { Platform } from '@/types/credential' import fs from 'fs/promises' import path from 'path' import { tmpdir } from 'os' describe('Hot-Reload Integration Test', () => { let credentialManager: CredentialManager let tempConfigPath: string let tempDir: string beforeEach(async () => { // Create temporary directory for test configs tempDir = await fs.mkdtemp(path.join(tmpdir(), 'credential-manager-test-')) tempConfigPath = path.join(tempDir, 'accounts.json') // Initial configuration const initialConfig = { accounts: [ { id: 'initial-account', name: 'Initial Account', platform: Platform.PACIFICA, enabled: true, credentials: { type: 'ed25519' as const, privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5' } } ], hedging: { platforms: { [Platform.PACIFICA]: { enabled: true, primaryAccounts: ['initial-account'], backupAccounts: [], loadBalanceStrategy: 'round-robin', healthCheckInterval: 30000, failoverThreshold: 3 } }, hedging: { enableCrossplatformBalancing: false, maxAccountsPerPlatform: 5, reservationTimeoutMs: 60000 } } } await fs.writeFile(tempConfigPath, JSON.stringify(initialConfig, null, 2)) credentialManager = new CredentialManager() await credentialManager.loadConfigurationFromFile(tempConfigPath, { enableHotReload: true, watchDebounceMs: 100 }) }) afterEach(async () => { await credentialManager.shutdown() // Clean up temp files try { await fs.rm(tempDir, { recursive: true }) } catch (error) { // Ignore cleanup errors } }) describe('Configuration File Watching', () => { test('should detect new account additions', async () => { // Initial state - should have 1 account let accounts = credentialManager.getAllAccounts() expect(accounts).toHaveLength(1) expect(accounts[0].id).toBe('initial-account') // Update configuration with new account const updatedConfig = { accounts: [ { id: 'initial-account', name: 'Initial Account', platform: Platform.PACIFICA, enabled: true, credentials: { type: 'ed25519' as const, privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5' } }, { id: 'new-account', name: 'New Account', platform: Platform.ASTER, enabled: true, credentials: { type: 'secp256k1' as const, privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' } } ], hedging: { platforms: { [Platform.PACIFICA]: { enabled: true, primaryAccounts: ['initial-account'], backupAccounts: [], loadBalanceStrategy: 'round-robin', healthCheckInterval: 30000, failoverThreshold: 3 }, [Platform.ASTER]: { enabled: true, primaryAccounts: ['new-account'], backupAccounts: [], loadBalanceStrategy: 'round-robin', healthCheckInterval: 30000, failoverThreshold: 3 } }, hedging: { enableCrossplatformBalancing: true, maxAccountsPerPlatform: 5, reservationTimeoutMs: 60000 } } } await fs.writeFile(tempConfigPath, JSON.stringify(updatedConfig, null, 2)) // Wait for file watcher to detect change await new Promise(resolve => setTimeout(resolve, 200)) // Should now have 2 accounts accounts = credentialManager.getAllAccounts() expect(accounts).toHaveLength(2) expect(accounts.map(a => a.id)).toContain('initial-account') expect(accounts.map(a => a.id)).toContain('new-account') }) test('should detect account removal', async () => { // Initial state let accounts = credentialManager.getAllAccounts() expect(accounts).toHaveLength(1) // Remove account from configuration const updatedConfig = { accounts: [], hedging: { platforms: {}, hedging: { enableCrossplatformBalancing: false, maxAccountsPerPlatform: 5, reservationTimeoutMs: 60000 } } } await fs.writeFile(tempConfigPath, JSON.stringify(updatedConfig, null, 2)) // Wait for file watcher await new Promise(resolve => setTimeout(resolve, 200)) // Should have no accounts accounts = credentialManager.getAllAccounts() expect(accounts).toHaveLength(0) }) test('should detect account modifications', async () => { // Initial state let account = credentialManager.getAccount('initial-account') expect(account?.name).toBe('Initial Account') expect(account?.enabled).toBe(true) // Update account properties const updatedConfig = { accounts: [ { id: 'initial-account', name: 'Modified Account Name', platform: Platform.PACIFICA, enabled: false, // Disable the account credentials: { type: 'ed25519' as const, privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5' } } ], hedging: { platforms: { [Platform.PACIFICA]: { enabled: true, primaryAccounts: ['initial-account'], backupAccounts: [], loadBalanceStrategy: 'round-robin', healthCheckInterval: 30000, failoverThreshold: 3 } }, hedging: { enableCrossplatformBalancing: false, maxAccountsPerPlatform: 5, reservationTimeoutMs: 60000 } } } await fs.writeFile(tempConfigPath, JSON.stringify(updatedConfig, null, 2)) // Wait for file watcher await new Promise(resolve => setTimeout(resolve, 200)) // Account should be updated account = credentialManager.getAccount('initial-account') expect(account?.name).toBe('Modified Account Name') expect(account?.enabled).toBe(false) }) }) describe('Performance Requirements', () => { test('should reload configuration within 100ms', async () => { const updatedConfig = { accounts: [ { id: 'performance-test', name: 'Performance Test Account', platform: Platform.PACIFICA, enabled: true, credentials: { type: 'ed25519' as const, privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5' } } ], hedging: { platforms: { [Platform.PACIFICA]: { enabled: true, primaryAccounts: ['performance-test'], backupAccounts: [], loadBalanceStrategy: 'round-robin', healthCheckInterval: 30000, failoverThreshold: 3 } }, hedging: { enableCrossplatformBalancing: false, maxAccountsPerPlatform: 5, reservationTimeoutMs: 60000 } } } const startTime = Date.now() await fs.writeFile(tempConfigPath, JSON.stringify(updatedConfig, null, 2)) // Wait for reload to complete let reloaded = false const maxWaitTime = 100 const checkInterval = 10 for (let elapsed = 0; elapsed < maxWaitTime; elapsed += checkInterval) { await new Promise(resolve => setTimeout(resolve, checkInterval)) const account = credentialManager.getAccount('performance-test') if (account) { reloaded = true break } } const reloadTime = Date.now() - startTime expect(reloaded).toBe(true) expect(reloadTime).toBeLessThan(100) // Performance requirement }) test('should handle rapid configuration changes', async () => { const configs = [ { accounts: [ { id: 'rapid-test-1', name: 'Rapid Test 1', platform: Platform.PACIFICA, enabled: true, credentials: { type: 'ed25519' as const, privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5' } } ] }, { accounts: [ { id: 'rapid-test-2', name: 'Rapid Test 2', platform: Platform.ASTER, enabled: true, credentials: { type: 'secp256k1' as const, privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' } } ] } ] // Rapid succession of changes for (const config of configs) { const fullConfig = { ...config, hedging: { platforms: {}, hedging: { enableCrossplatformBalancing: false, maxAccountsPerPlatform: 5, reservationTimeoutMs: 60000 } } } await fs.writeFile(tempConfigPath, JSON.stringify(fullConfig, null, 2)) await new Promise(resolve => setTimeout(resolve, 50)) } // Wait for final state await new Promise(resolve => setTimeout(resolve, 200)) // Should have the last configuration const accounts = credentialManager.getAllAccounts() expect(accounts).toHaveLength(1) expect(accounts[0].id).toBe('rapid-test-2') }) }) describe('Error Handling', () => { test('should handle invalid JSON gracefully', async () => { const invalidJson = '{ invalid json content' await fs.writeFile(tempConfigPath, invalidJson) // Wait for file watcher await new Promise(resolve => setTimeout(resolve, 200)) // Should still have original accounts (no change due to error) const accounts = credentialManager.getAllAccounts() expect(accounts).toHaveLength(1) expect(accounts[0].id).toBe('initial-account') }) test('should handle missing file gracefully', async () => { // Delete the config file await fs.unlink(tempConfigPath) // Wait for file watcher await new Promise(resolve => setTimeout(resolve, 200)) // Should handle missing file without crashing // Behavior depends on implementation - might keep existing accounts or clear them const accounts = credentialManager.getAllAccounts() expect(Array.isArray(accounts)).toBe(true) }) test('should validate configuration before applying changes', async () => { const invalidConfig = { accounts: [ { id: '', // Invalid empty ID name: 'Invalid Account', platform: Platform.PACIFICA, enabled: true, credentials: { type: 'ed25519' as const, privateKey: 'invalid-key' // Invalid key } } ] } await fs.writeFile(tempConfigPath, JSON.stringify(invalidConfig, null, 2)) // Wait for file watcher await new Promise(resolve => setTimeout(resolve, 200)) // Should reject invalid config and keep original const accounts = credentialManager.getAllAccounts() expect(accounts).toHaveLength(1) expect(accounts[0].id).toBe('initial-account') }) }) describe('Configuration Backup and Recovery', () => { test('should maintain service availability during configuration updates', async () => { const message = new TextEncoder().encode('availability test') // Service should work before update let result = await credentialManager.sign('initial-account', message) expect(result.success).toBe(true) // Update configuration const updatedConfig = { accounts: [ { id: 'initial-account', name: 'Updated Account', platform: Platform.PACIFICA, enabled: true, credentials: { type: 'ed25519' as const, privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5' } } ], hedging: { platforms: { [Platform.PACIFICA]: { enabled: true, primaryAccounts: ['initial-account'], backupAccounts: [], loadBalanceStrategy: 'round-robin', healthCheckInterval: 30000, failoverThreshold: 3 } }, hedging: { enableCrossplatformBalancing: false, maxAccountsPerPlatform: 5, reservationTimeoutMs: 60000 } } } await fs.writeFile(tempConfigPath, JSON.stringify(updatedConfig, null, 2)) // Wait for reload await new Promise(resolve => setTimeout(resolve, 200)) // Service should still work after update result = await credentialManager.sign('initial-account', message) expect(result.success).toBe(true) // Account should have updated name const account = credentialManager.getAccount('initial-account') expect(account?.name).toBe('Updated Account') }) test('should handle partial configuration updates', async () => { // Add second account first const configWithTwoAccounts = { accounts: [ { id: 'initial-account', name: 'Initial Account', platform: Platform.PACIFICA, enabled: true, credentials: { type: 'ed25519' as const, privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5' } }, { id: 'second-account', name: 'Second Account', platform: Platform.ASTER, enabled: true, credentials: { type: 'secp256k1' as const, privateKey: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' } } ], hedging: { platforms: { [Platform.PACIFICA]: { enabled: true, primaryAccounts: ['initial-account'], backupAccounts: [], loadBalanceStrategy: 'round-robin', healthCheckInterval: 30000, failoverThreshold: 3 }, [Platform.ASTER]: { enabled: true, primaryAccounts: ['second-account'], backupAccounts: [], loadBalanceStrategy: 'round-robin', healthCheckInterval: 30000, failoverThreshold: 3 } }, hedging: { enableCrossplatformBalancing: true, maxAccountsPerPlatform: 5, reservationTimeoutMs: 60000 } } } await fs.writeFile(tempConfigPath, JSON.stringify(configWithTwoAccounts, null, 2)) await new Promise(resolve => setTimeout(resolve, 200)) // Should have both accounts let accounts = credentialManager.getAllAccounts() expect(accounts).toHaveLength(2) // Now remove one account const configWithOneAccount = { accounts: [ { id: 'initial-account', name: 'Initial Account', platform: Platform.PACIFICA, enabled: true, credentials: { type: 'ed25519' as const, privateKey: 'f26670e2ca334117f8859f9f32e50251641953a30b54f6ffcf82db836cfdfea5' } } ], hedging: { platforms: { [Platform.PACIFICA]: { enabled: true, primaryAccounts: ['initial-account'], backupAccounts: [], loadBalanceStrategy: 'round-robin', healthCheckInterval: 30000, failoverThreshold: 3 } }, hedging: { enableCrossplatformBalancing: false, maxAccountsPerPlatform: 5, reservationTimeoutMs: 60000 } } } await fs.writeFile(tempConfigPath, JSON.stringify(configWithOneAccount, null, 2)) await new Promise(resolve => setTimeout(resolve, 200)) // Should have only one account accounts = credentialManager.getAllAccounts() expect(accounts).toHaveLength(1) expect(accounts[0].id).toBe('initial-account') }) }) })