123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629 |
- /**
- * 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<never>((_, 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<Account[]>((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<Account[]>((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<Account[]>((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();
- });
- });
- });
|