/** * Integration test for hot configuration reload * * This test verifies the complete hot reload workflow: * 1. Load initial configuration * 2. Start watching for changes * 3. Modify configuration file * 4. Verify automatic reload within 100ms * 5. Verify account updates are reflected * * Tests MUST FAIL initially until implementation is provided. */ import { describe, test, expect, beforeEach, afterEach } from '@jest/globals'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; // Import types (this import will fail until types are implemented) import type { ICredentialManager, Account, Platform, ConfigFile } from '@/specs/001-credential-manager/contracts/credential-manager'; describe('Hot Configuration Reload Integration Tests', () => { let credentialManager: ICredentialManager; let tempDir: string; let configPath: string; beforeEach(async () => { // This will fail until CredentialManager is implemented const { CredentialManager } = await import('@/core/credential-manager/CredentialManager'); credentialManager = new CredentialManager(); // Create temporary directory for test files tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'hot-reload-test-')); configPath = path.join(tempDir, 'credentials.json'); }); afterEach(async () => { // Clean up credentialManager.stopWatching(); // Remove temporary directory try { await fs.rm(tempDir, { recursive: true, force: true }); } catch (error) { // Ignore cleanup errors } }); describe('Initial Load and Setup', () => { test('should load initial configuration successfully', async () => { // Arrange const initialConfig: ConfigFile = { version: "1.0", accounts: [ { id: "initial-pacifica-account", platform: Platform.PACIFICA, name: "Initial Pacifica Account", credentials: { type: "ed25519", privateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } } ] }; await fs.writeFile(configPath, JSON.stringify(initialConfig, null, 2)); // Act const loadResult = await credentialManager.loadConfig(configPath); // Assert expect(loadResult.success).toBe(true); expect(loadResult.accounts).toHaveLength(1); expect(loadResult.accounts[0].id).toBe("initial-pacifica-account"); // Verify account is accessible via getAccount const account = credentialManager.getAccount("initial-pacifica-account"); expect(account).not.toBeNull(); expect(account!.platform).toBe(Platform.PACIFICA); }); test('should start file watching without errors', async () => { // Arrange const initialConfig: ConfigFile = { version: "1.0", accounts: [] }; await fs.writeFile(configPath, JSON.stringify(initialConfig)); await credentialManager.loadConfig(configPath); const changeCallback = jest.fn(); // Act & Assert - should not throw expect(() => { credentialManager.watchConfig(configPath, changeCallback); }).not.toThrow(); }); }); describe('Hot Reload Performance', () => { test('should complete hot reload within 100ms performance requirement', async () => { // Arrange const initialConfig: ConfigFile = { version: "1.0", accounts: [ { id: "performance-test-account", platform: Platform.PACIFICA, name: "Performance Test Account", credentials: { type: "ed25519", privateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } } ] }; await fs.writeFile(configPath, JSON.stringify(initialConfig, null, 2)); await credentialManager.loadConfig(configPath); // Setup reload detection const reloadPromise = new Promise<{accounts: Account[], reloadTime: number}>((resolve) => { const startTime = Date.now(); credentialManager.watchConfig(configPath, (accounts) => { const reloadTime = Date.now() - startTime; resolve({ accounts, reloadTime }); }); }); // Wait for watcher to initialize await new Promise(resolve => setTimeout(resolve, 100)); // Act - modify configuration const modifiedConfig: ConfigFile = { version: "1.0", accounts: [ { id: "performance-test-account", platform: Platform.PACIFICA, name: "Modified Performance Test Account", credentials: { type: "ed25519", privateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } }, { id: "new-account", platform: Platform.BINANCE, name: "New Account", credentials: { type: "hmac", apiKey: "test-api-key", secretKey: "test-secret-key" } } ] }; const modifyStartTime = Date.now(); await fs.writeFile(configPath, JSON.stringify(modifiedConfig, null, 2)); // Assert - wait for reload with timeout const { accounts, reloadTime } = await Promise.race([ reloadPromise, new Promise((_, reject) => setTimeout(() => reject(new Error('Hot reload timeout - exceeded 5 seconds')), 5000) ) ]); // Performance requirement: < 100ms expect(reloadTime).toBeLessThan(100); expect(accounts).toHaveLength(2); // Verify accounts are updated in credential manager const updatedAccount = credentialManager.getAccount("performance-test-account"); expect(updatedAccount!.name).toBe("Modified Performance Test Account"); const newAccount = credentialManager.getAccount("new-account"); expect(newAccount).not.toBeNull(); expect(newAccount!.platform).toBe(Platform.BINANCE); }); test('should handle rapid successive file changes efficiently', async () => { // Arrange const baseConfig: ConfigFile = { version: "1.0", accounts: [] }; await fs.writeFile(configPath, JSON.stringify(baseConfig)); await credentialManager.loadConfig(configPath); let reloadCount = 0; const reloadTimes: number[] = []; credentialManager.watchConfig(configPath, (accounts) => { reloadCount++; reloadTimes.push(Date.now()); }); // Wait for watcher to initialize await new Promise(resolve => setTimeout(resolve, 100)); // Act - make rapid changes const startTime = Date.now(); for (let i = 0; i < 5; i++) { const config = { ...baseConfig, accounts: [ { id: `rapid-account-${i}`, platform: Platform.PACIFICA, name: `Rapid Account ${i}`, credentials: { type: "ed25519", privateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } } ] }; await fs.writeFile(configPath, JSON.stringify(config)); await new Promise(resolve => setTimeout(resolve, 20)); // Small delay between changes } // Wait for debouncing and final reload await new Promise(resolve => setTimeout(resolve, 500)); // Assert - should be debounced (fewer reloads than changes) expect(reloadCount).toBeLessThan(5); expect(reloadCount).toBeGreaterThan(0); // Final state should be correct const finalAccount = credentialManager.getAccount("rapid-account-4"); expect(finalAccount).not.toBeNull(); }); }); describe('Account Updates and State Management', () => { test('should add new accounts on configuration update', async () => { // Arrange const initialConfig: ConfigFile = { version: "1.0", accounts: [ { id: "existing-account", platform: Platform.PACIFICA, name: "Existing Account", credentials: { type: "ed25519", privateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } } ] }; await fs.writeFile(configPath, JSON.stringify(initialConfig)); await credentialManager.loadConfig(configPath); const updatePromise = new Promise((resolve) => { credentialManager.watchConfig(configPath, resolve); }); // Wait for watcher await new Promise(resolve => setTimeout(resolve, 100)); // Act - add new account const updatedConfig: ConfigFile = { version: "1.0", accounts: [ ...initialConfig.accounts, { id: "new-aster-account", platform: Platform.ASTER, name: "New Aster Account", credentials: { type: "eip191", privateKey: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" } } ] }; await fs.writeFile(configPath, JSON.stringify(updatedConfig, null, 2)); // Assert const accounts = await updatePromise; expect(accounts).toHaveLength(2); // Verify both accounts are accessible expect(credentialManager.getAccount("existing-account")).not.toBeNull(); expect(credentialManager.getAccount("new-aster-account")).not.toBeNull(); expect(credentialManager.getAccount("new-aster-account")!.platform).toBe(Platform.ASTER); }); test('should remove accounts when deleted from configuration', async () => { // Arrange const initialConfig: ConfigFile = { version: "1.0", accounts: [ { id: "account-to-keep", platform: Platform.PACIFICA, name: "Account to Keep", credentials: { type: "ed25519", privateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } }, { id: "account-to-remove", platform: Platform.BINANCE, name: "Account to Remove", credentials: { type: "hmac", apiKey: "test-api-key", secretKey: "test-secret-key" } } ] }; await fs.writeFile(configPath, JSON.stringify(initialConfig)); await credentialManager.loadConfig(configPath); // Verify both accounts exist initially expect(credentialManager.getAccount("account-to-keep")).not.toBeNull(); expect(credentialManager.getAccount("account-to-remove")).not.toBeNull(); const updatePromise = new Promise((resolve) => { credentialManager.watchConfig(configPath, resolve); }); // Wait for watcher await new Promise(resolve => setTimeout(resolve, 100)); // Act - remove one account const updatedConfig: ConfigFile = { version: "1.0", accounts: [ { id: "account-to-keep", platform: Platform.PACIFICA, name: "Account to Keep", credentials: { type: "ed25519", privateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } } ] }; await fs.writeFile(configPath, JSON.stringify(updatedConfig, null, 2)); // Assert const accounts = await updatePromise; expect(accounts).toHaveLength(1); // Verify correct account state expect(credentialManager.getAccount("account-to-keep")).not.toBeNull(); expect(credentialManager.getAccount("account-to-remove")).toBeNull(); }); test('should update existing account credentials', async () => { // Arrange const initialConfig: ConfigFile = { version: "1.0", accounts: [ { id: "updatable-account", platform: Platform.PACIFICA, name: "Original Name", credentials: { type: "ed25519", privateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } } ] }; await fs.writeFile(configPath, JSON.stringify(initialConfig)); await credentialManager.loadConfig(configPath); const updatePromise = new Promise((resolve) => { credentialManager.watchConfig(configPath, resolve); }); // Wait for watcher await new Promise(resolve => setTimeout(resolve, 100)); // Act - update account details const updatedConfig: ConfigFile = { version: "1.0", accounts: [ { id: "updatable-account", platform: Platform.PACIFICA, name: "Updated Name", credentials: { type: "ed25519", privateKey: "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" } } ] }; await fs.writeFile(configPath, JSON.stringify(updatedConfig, null, 2)); // Assert const accounts = await updatePromise; expect(accounts).toHaveLength(1); const updatedAccount = credentialManager.getAccount("updatable-account"); expect(updatedAccount).not.toBeNull(); expect(updatedAccount!.name).toBe("Updated Name"); expect(updatedAccount!.credentials.privateKey).toBe("fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"); }); }); describe('Error Handling During Hot Reload', () => { test('should handle malformed configuration during reload', async () => { // Arrange const initialConfig: ConfigFile = { version: "1.0", accounts: [ { id: "stable-account", platform: Platform.PACIFICA, name: "Stable Account", credentials: { type: "ed25519", privateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } } ] }; await fs.writeFile(configPath, JSON.stringify(initialConfig)); await credentialManager.loadConfig(configPath); // Verify initial state expect(credentialManager.getAccount("stable-account")).not.toBeNull(); let errorOccurred = false; credentialManager.watchConfig(configPath, (accounts) => { // This callback should not be called for malformed config errorOccurred = true; }); // Wait for watcher await new Promise(resolve => setTimeout(resolve, 100)); // Act - write malformed configuration const malformedConfig = '{ "version": "1.0", "accounts": [ invalid json }'; await fs.writeFile(configPath, malformedConfig); // Wait for file watcher to process await new Promise(resolve => setTimeout(resolve, 500)); // Assert - original state should be preserved expect(errorOccurred).toBe(false); expect(credentialManager.getAccount("stable-account")).not.toBeNull(); }); test('should handle file deletion during watching', async () => { // Arrange const initialConfig: ConfigFile = { version: "1.0", accounts: [ { id: "persistent-account", platform: Platform.PACIFICA, name: "Persistent Account", credentials: { type: "ed25519", privateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } } ] }; await fs.writeFile(configPath, JSON.stringify(initialConfig)); await credentialManager.loadConfig(configPath); credentialManager.watchConfig(configPath, (accounts) => { // Should handle file deletion gracefully }); // Wait for watcher await new Promise(resolve => setTimeout(resolve, 100)); // Act - delete configuration file await fs.unlink(configPath); // Wait for file watcher to process await new Promise(resolve => setTimeout(resolve, 500)); // Assert - should not crash, previous state may be preserved expect(() => { credentialManager.getAccount("persistent-account"); }).not.toThrow(); }); test('should handle temporary file operations (atomic writes)', async () => { // Arrange const initialConfig: ConfigFile = { version: "1.0", accounts: [] }; await fs.writeFile(configPath, JSON.stringify(initialConfig)); await credentialManager.loadConfig(configPath); let updateCount = 0; credentialManager.watchConfig(configPath, (accounts) => { updateCount++; }); // Wait for watcher await new Promise(resolve => setTimeout(resolve, 100)); // Act - simulate atomic write operations (common with editors) const tempPath = configPath + '.tmp'; const finalConfig: ConfigFile = { version: "1.0", accounts: [ { id: "atomic-account", platform: Platform.PACIFICA, name: "Atomic Account", credentials: { type: "ed25519", privateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } } ] }; // Write to temp file then rename (atomic operation) await fs.writeFile(tempPath, JSON.stringify(finalConfig, null, 2)); await fs.rename(tempPath, configPath); // Wait for file watcher to process await new Promise(resolve => setTimeout(resolve, 500)); // Assert - should detect the final state expect(updateCount).toBeGreaterThan(0); expect(credentialManager.getAccount("atomic-account")).not.toBeNull(); }); }); describe('Cleanup and Resource Management', () => { test('should stop watching when requested', async () => { // Arrange const initialConfig: ConfigFile = { version: "1.0", accounts: [] }; await fs.writeFile(configPath, JSON.stringify(initialConfig)); await credentialManager.loadConfig(configPath); let callbackCount = 0; credentialManager.watchConfig(configPath, () => { callbackCount++; }); // Wait for watcher await new Promise(resolve => setTimeout(resolve, 100)); // Act - stop watching credentialManager.stopWatching(); // Modify file after stopping const modifiedConfig: ConfigFile = { version: "1.0", accounts: [ { id: "should-not-trigger", platform: Platform.PACIFICA, name: "Should Not Trigger", credentials: { type: "ed25519", privateKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" } } ] }; await fs.writeFile(configPath, JSON.stringify(modifiedConfig)); // Wait to ensure no callback is triggered await new Promise(resolve => setTimeout(resolve, 500)); // Assert - callback should not have been called after stopping expect(callbackCount).toBe(0); }); test('should handle multiple start/stop watch cycles', async () => { // Arrange const config: ConfigFile = { version: "1.0", accounts: [] }; await fs.writeFile(configPath, JSON.stringify(config)); await credentialManager.loadConfig(configPath); // Act & Assert - multiple cycles should not cause issues for (let i = 0; i < 3; i++) { credentialManager.watchConfig(configPath, () => {}); await new Promise(resolve => setTimeout(resolve, 50)); credentialManager.stopWatching(); await new Promise(resolve => setTimeout(resolve, 50)); } // Should end in clean state expect(() => { credentialManager.stopWatching(); }).not.toThrow(); }); }); });