From fb847a65336c0213e8bce3179c4ecc665e126209 Mon Sep 17 00:00:00 2001 From: Gas Optimization Bot Date: Fri, 27 Mar 2026 08:41:24 +0100 Subject: [PATCH 1/6] Fix: Standardize Stellar SDK version across API and Oracle - Update Oracle from @stellar/stellar-sdk@^12.0.0 to @stellar/stellar-sdk@^14.5.0 - Replace deprecated SorobanRpc import with rpc import - Update all SorobanRpc references to rpc in contract-updater.ts - Update test mocks to use new rpc import structure - Ensure compatibility with API's SDK version Resolves #56 --- oracle/package.json | 2 +- oracle/src/services/contract-updater.ts | 16 ++++++------- oracle/tests/contract-updater.test.ts | 32 ++++++++++++------------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/oracle/package.json b/oracle/package.json index 4063646d..65ead517 100644 --- a/oracle/package.json +++ b/oracle/package.json @@ -17,7 +17,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@stellar/stellar-sdk": "^12.0.0", + "@stellar/stellar-sdk": "^14.5.0", "axios": "^1.6.0", "dotenv": "^16.3.0", "ioredis": "^5.3.0", diff --git a/oracle/src/services/contract-updater.ts b/oracle/src/services/contract-updater.ts index d1914d50..897bbf9b 100644 --- a/oracle/src/services/contract-updater.ts +++ b/oracle/src/services/contract-updater.ts @@ -5,7 +5,7 @@ import { Keypair, Contract, - SorobanRpc, + rpc, TransactionBuilder, Networks, xdr, @@ -42,14 +42,14 @@ const DEFAULT_CONFIG: Partial = { */ export class ContractUpdater { private config: ContractUpdaterConfig; - private server: SorobanRpc.Server; + private server: rpc.Server; private adminKeypair: Keypair; private networkPassphrase: string; constructor(config: ContractUpdaterConfig) { this.config = { ...DEFAULT_CONFIG, ...config } as ContractUpdaterConfig; - this.server = new SorobanRpc.Server(this.config.rpcUrl); + this.server = new rpc.Server(this.config.rpcUrl); this.adminKeypair = Keypair.fromSecret(this.config.adminSecretKey); this.networkPassphrase = this.config.network === 'testnet' ? Networks.TESTNET : Networks.PUBLIC; @@ -168,15 +168,15 @@ export class ContractUpdater { const simulated = await this.server.simulateTransaction(transaction); - if (SorobanRpc.Api.isSimulationError(simulated)) { + if (rpc.Api.isSimulationError(simulated)) { throw new Error(`Simulation failed: ${simulated.error}`); } - if (!SorobanRpc.Api.isSimulationSuccess(simulated)) { + if (!rpc.Api.isSimulationSuccess(simulated)) { throw new Error('Simulation did not succeed'); } - const prepared = SorobanRpc.assembleTransaction(transaction, simulated).build(); + const prepared = rpc.assembleTransaction(transaction, simulated).build(); prepared.sign(this.adminKeypair); const response = await this.server.sendTransaction(prepared); @@ -192,7 +192,7 @@ export class ContractUpdater { let attempts = 0; while ( - getResponse.status === SorobanRpc.Api.GetTransactionStatus.NOT_FOUND && + getResponse.status === rpc.Api.GetTransactionStatus.NOT_FOUND && attempts < MAX_POLL_ATTEMPTS ) { await this.sleep(1000); @@ -209,7 +209,7 @@ export class ContractUpdater { throw new Error(`Transaction polling timed out after ${MAX_POLL_ATTEMPTS} attempts`); } - if (getResponse.status === SorobanRpc.Api.GetTransactionStatus.FAILED) { + if (getResponse.status === rpc.Api.GetTransactionStatus.FAILED) { throw new Error(`Transaction failed on-chain`); } diff --git a/oracle/tests/contract-updater.test.ts b/oracle/tests/contract-updater.test.ts index fc43077b..2edeca68 100644 --- a/oracle/tests/contract-updater.test.ts +++ b/oracle/tests/contract-updater.test.ts @@ -37,7 +37,7 @@ vi.mock('@stellar/stellar-sdk', () => { /* operation */ }), })), - SorobanRpc: { + rpc: { Server: vi.fn().mockImplementation((url: string) => ({ getAccount: vi.fn().mockResolvedValue(mockAccount), simulateTransaction: vi.fn().mockResolvedValue({ @@ -239,8 +239,8 @@ describe('ContractUpdater', () => { describe('retry mechanism', () => { it('should retry on failure', async () => { - const { SorobanRpc } = await import('@stellar/stellar-sdk'); - const mockServer = new SorobanRpc.Server('mock'); + const { rpc } = await import('@stellar/stellar-sdk'); + const mockServer = new rpc.Server('mock'); // First attempt fails, second succeeds let attemptCount = 0; @@ -272,8 +272,8 @@ describe('ContractUpdater', () => { }); it('should use exponential backoff for retries', async () => { - const { SorobanRpc } = await import('@stellar/stellar-sdk'); - const mockServer = new SorobanRpc.Server('mock'); + const { rpc } = await import('@stellar/stellar-sdk'); + const mockServer = new rpc.Server('mock'); let attemptCount = 0; const attemptTimes: number[] = []; @@ -303,9 +303,9 @@ describe('ContractUpdater', () => { describe('error handling', () => { it('should handle simulation errors', async () => { - const { SorobanRpc } = await import('@stellar/stellar-sdk'); + const { rpc } = await import('@stellar/stellar-sdk'); - vi.spyOn(SorobanRpc.Api, 'isSimulationError').mockReturnValue(true); + vi.spyOn(rpc.Api, 'isSimulationError').mockReturnValue(true); const result = await updater.updatePrice('XLM', 150000n, Date.now()); @@ -314,8 +314,8 @@ describe('ContractUpdater', () => { }); it('should handle transaction send errors', async () => { - const { SorobanRpc } = await import('@stellar/stellar-sdk'); - const mockServer = new SorobanRpc.Server('mock'); + const { rpc } = await import('@stellar/stellar-sdk'); + const mockServer = new rpc.Server('mock'); vi.spyOn(mockServer, 'sendTransaction').mockResolvedValue({ status: 'ERROR', @@ -329,11 +329,11 @@ describe('ContractUpdater', () => { }); it('should handle transaction failures on-chain', async () => { - const { SorobanRpc } = await import('@stellar/stellar-sdk'); - const mockServer = new SorobanRpc.Server('mock'); + const { rpc } = await import('@stellar/stellar-sdk'); + const mockServer = new rpc.Server('mock'); vi.spyOn(mockServer, 'getTransaction').mockResolvedValue({ - status: SorobanRpc.Api.GetTransactionStatus.FAILED, + status: rpc.Api.GetTransactionStatus.FAILED, } as any); const result = await updater.updatePrice('XLM', 150000n, Date.now()); @@ -380,7 +380,7 @@ describe('ContractUpdater', () => { }); it('should throw timeout error if transaction is NOT_FOUND for too long', async () => { - const { SorobanRpc } = await import('@stellar/stellar-sdk'); + const { rpc } = await import('@stellar/stellar-sdk'); // Create a specific updater with maxRetries: 1 to avoid long test runs const timeoutUpdater = createContractUpdater({ @@ -389,8 +389,8 @@ describe('ContractUpdater', () => { }); // Ensure simulation doesn't fail - vi.spyOn(SorobanRpc.Api, 'isSimulationError').mockReturnValue(false); - vi.spyOn(SorobanRpc.Api, 'isSimulationSuccess').mockReturnValue(true); + vi.spyOn(rpc.Api, 'isSimulationError').mockReturnValue(false); + vi.spyOn(rpc.Api, 'isSimulationSuccess').mockReturnValue(true); // Use fake timers vi.useFakeTimers(); @@ -399,7 +399,7 @@ describe('ContractUpdater', () => { const getTransactionMock = vi .spyOn((timeoutUpdater as any).server, 'getTransaction') .mockResolvedValue({ - status: SorobanRpc.Api.GetTransactionStatus.NOT_FOUND, + status: rpc.Api.GetTransactionStatus.NOT_FOUND, } as any); // Start the update From 7177c336cabbfadde96355ba745ccfee1e565607 Mon Sep 17 00:00:00 2001 From: Gas Optimization Bot Date: Fri, 27 Mar 2026 08:54:16 +0100 Subject: [PATCH 2/6] Fix: Resolve CI/CD issues for SDK upgrade - Update Oracle Node.js engine requirement to >=20.0.0 for SDK v14 compatibility - Remove Node.js 18 testing matrix for Oracle since SDK v14 requires Node.js 20+ - Add Vercel silent mode to reduce deployment noise on forks - Fix coverage upload conditions after removing matrix strategy Resolves CI failures and Quality Gate pending status --- .github/workflows/ci-cd.yml | 10 +++------- oracle/package.json | 2 +- vercel.json | 3 +++ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 9100551b..7771f90e 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -162,9 +162,6 @@ jobs: oracle: name: Oracle — Lint, Test, Build runs-on: ubuntu-latest - strategy: - matrix: - node-version: [18, 20] defaults: run: working-directory: oracle @@ -172,10 +169,10 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Setup Node.js ${{ matrix.node-version }} + - name: Setup Node.js 20 uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node-version }} + node-version: 20 - name: Install dependencies run: npm ci @@ -193,14 +190,13 @@ jobs: run: npm test - name: Run tests with coverage - if: matrix.node-version == 20 run: npm run test:coverage - name: Build run: npm run build - name: Upload coverage - if: always() && matrix.node-version == 20 + if: always() uses: actions/upload-artifact@v4 with: name: oracle-coverage diff --git a/oracle/package.json b/oracle/package.json index 65ead517..87c6baeb 100644 --- a/oracle/package.json +++ b/oracle/package.json @@ -36,7 +36,7 @@ "vitest": "^1.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "keywords": [ "stellar", diff --git a/vercel.json b/vercel.json index 07d97d22..f33e748b 100644 --- a/vercel.json +++ b/vercel.json @@ -9,5 +9,8 @@ "api/src/vercel.ts": { "runtime": "@vercel/node@3" } + }, + "github": { + "silent": true } } From d8d85a8b005a7b1dd31d8fc4052943368d0dc1be Mon Sep 17 00:00:00 2001 From: Gas Optimization Bot Date: Fri, 27 Mar 2026 09:07:40 +0100 Subject: [PATCH 3/6] Feature: Add Oracle service lifecycle integration tests - Add comprehensive lifecycle.test.ts with 5 required test scenarios - Test normal start/stop cycle with resource leak prevention - Test double start scenarios for idempotency - Test stop during active price updates for graceful shutdown - Test restart after failure scenarios - Test graceful shutdown with pending updates - Mock external services (RPC, providers) to avoid actual API calls - Ensure proper resource cleanup and timing reliability - Addresses issue #48 requirements Fixes #48 --- oracle/tests/lifecycle.test.ts | 481 +++++++++++++++++++++++++++++++++ 1 file changed, 481 insertions(+) create mode 100644 oracle/tests/lifecycle.test.ts diff --git a/oracle/tests/lifecycle.test.ts b/oracle/tests/lifecycle.test.ts new file mode 100644 index 00000000..c50ceb4f --- /dev/null +++ b/oracle/tests/lifecycle.test.ts @@ -0,0 +1,481 @@ +/** + * Oracle Service Lifecycle Integration Tests + * Tests for startup/shutdown edge cases and race conditions + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { OracleService } from '../src/index.js'; +import type { OracleServiceConfig } from '../src/config.js'; + +// Mock contract updater to avoid actual blockchain calls +vi.mock('../src/services/contract-updater.js', () => ({ + createContractUpdater: vi.fn(() => ({ + updatePrices: vi + .fn() + .mockImplementation(async (prices) => { + // Simulate some processing time + await new Promise(resolve => setTimeout(resolve, 50)); + return prices.map((price, index) => ({ + success: true, + asset: `ASSET_${index}`, + price: BigInt(Math.floor(price.price * 1000000)), + timestamp: Date.now(), + })); + }), + healthCheck: vi.fn().mockResolvedValue(true), + getAdminPublicKey: vi.fn().mockReturnValue('GTEST123'), + })), + ContractUpdater: vi.fn(), +})); + +// Mock providers to avoid actual API calls +vi.mock('../src/providers/coingecko.js', () => ({ + createCoinGeckoProvider: vi.fn(() => ({ + name: 'coingecko', + isEnabled: true, + priority: 1, + weight: 0.6, + getSupportedAssets: () => ['XLM', 'BTC', 'ETH', 'USDC', 'SOL'], + fetchPrice: vi.fn().mockImplementation(async (asset) => { + // Simulate network delay + await new Promise(resolve => setTimeout(resolve, 100)); + return { + asset, + price: asset === 'XLM' ? 0.15 : asset === 'BTC' ? 45000 : asset === 'ETH' ? 3000 : 1, + timestamp: Math.floor(Date.now() / 1000), + source: 'coingecko', + }; + }), + })), +})); + +vi.mock('../src/providers/binance.js', () => ({ + createBinanceProvider: vi.fn(() => ({ + name: 'binance', + isEnabled: true, + priority: 2, + weight: 0.4, + getSupportedAssets: () => ['XLM', 'BTC', 'ETH', 'USDC', 'SOL'], + fetchPrice: vi.fn().mockImplementation(async (asset) => { + // Simulate network delay + await new Promise(resolve => setTimeout(resolve, 80)); + return { + asset, + price: asset === 'XLM' ? 0.152 : asset === 'BTC' ? 45100 : asset === 'ETH' ? 3010 : 1.01, + timestamp: Math.floor(Date.now() / 1000), + source: 'binance', + }; + }), + })), +})); + +describe('OracleService Lifecycle Integration Tests', () => { + let service: OracleService; + let mockConfig: OracleServiceConfig; + + beforeEach(() => { + vi.clearAllMocks(); + mockConfig = { + stellarNetwork: 'testnet', + stellarRpcUrl: 'https://soroban-testnet.stellar.org', + contractId: 'CTEST123', + adminSecretKey: 'STEST123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ123456', + updateIntervalMs: 200, // Short interval for faster tests + maxPriceDeviationPercent: 10, + priceStaleThresholdSeconds: 300, + cacheTtlSeconds: 30, + logLevel: 'error', // Reduce log noise in tests + providers: [ + { + name: 'coingecko', + enabled: true, + priority: 1, + weight: 0.6, + baseUrl: 'https://api.coingecko.com/api/v3', + rateLimit: { maxRequests: 10, windowMs: 60000 }, + }, + { + name: 'binance', + enabled: true, + priority: 2, + weight: 0.4, + baseUrl: 'https://api.binance.com/api/v3', + rateLimit: { maxRequests: 1200, windowMs: 60000 }, + }, + ], + }; + }); + + afterEach(() => { + if (service) { + service.stop(); + } + vi.clearAllTimers(); + }); + + describe('Scenario 1: Normal start/stop cycle', () => { + it('should complete normal lifecycle without resource leaks', async () => { + service = new OracleService(mockConfig); + + // Verify initial state + expect(service.getStatus().isRunning).toBe(false); + + // Start service + await service.start(['XLM', 'BTC']); + expect(service.getStatus().isRunning).toBe(true); + + // Allow at least one update cycle + await new Promise(resolve => setTimeout(resolve, 250)); + + // Stop service + service.stop(); + expect(service.getStatus().isRunning).toBe(false); + + // Verify no pending intervals (by waiting a bit longer) + await new Promise(resolve => setTimeout(resolve, 300)); + expect(service.getStatus().isRunning).toBe(false); + }); + + it('should handle multiple start/stop cycles', async () => { + service = new OracleService(mockConfig); + + // Perform multiple cycles + for (let i = 0; i < 3; i++) { + await service.start(['XLM']); + await new Promise(resolve => setTimeout(resolve, 250)); + service.stop(); + await new Promise(resolve => setTimeout(resolve, 100)); + + expect(service.getStatus().isRunning).toBe(false); + } + }); + }); + + describe('Scenario 2: Double start (should be idempotent or error)', () => { + it('should handle double start gracefully', async () => { + service = new OracleService(mockConfig); + + // First start + await service.start(['XLM']); + const firstStartStatus = service.getStatus(); + expect(firstStartStatus.isRunning).toBe(true); + + // Second start should be handled gracefully + await service.start(['XLM', 'BTC']); + const secondStartStatus = service.getStatus(); + expect(secondStartStatus.isRunning).toBe(true); + + // Service should still be functional + await new Promise(resolve => setTimeout(resolve, 250)); + expect(service.getStatus().isRunning).toBe(true); + + service.stop(); + }); + + it('should not create multiple intervals on double start', async () => { + service = new OracleService(mockConfig); + + const setIntervalSpy = vi.spyOn(global, 'setInterval'); + + await service.start(['XLM']); + const firstCallCount = setIntervalSpy.mock.calls.length; + + await service.start(['BTC']); + const secondCallCount = setIntervalSpy.mock.calls.length; + + // Should not create additional intervals + expect(secondCallCount).toBe(firstCallCount); + + setIntervalSpy.mockRestore(); + service.stop(); + }); + }); + + describe('Scenario 3: Stop during active price update', () => { + it('should handle stop during active price update gracefully', async () => { + service = new OracleService(mockConfig); + + await service.start(['XLM', 'BTC', 'ETH']); + + // Wait for update to start, then stop immediately + await new Promise(resolve => setTimeout(resolve, 50)); + service.stop(); + + // Should stop without throwing + expect(service.getStatus().isRunning).toBe(false); + + // Wait to ensure no background processes + await new Promise(resolve => setTimeout(resolve, 200)); + expect(service.getStatus().isRunning).toBe(false); + }); + + it('should handle multiple concurrent stop calls during update', async () => { + service = new OracleService(mockConfig); + + await service.start(['XLM']); + + // Wait for update to start + await new Promise(resolve => setTimeout(resolve, 50)); + + // Call stop multiple times concurrently + const stopPromises = [ + Promise.resolve(service.stop()), + Promise.resolve(service.stop()), + Promise.resolve(service.stop()), + ]; + + await Promise.all(stopPromises); + + expect(service.getStatus().isRunning).toBe(false); + }); + + it('should clean up resources even when stop is called during update', async () => { + service = new OracleService(mockConfig); + + const clearIntervalSpy = vi.spyOn(global, 'clearInterval'); + + await service.start(['XLM', 'BTC']); + + // Stop during active update + await new Promise(resolve => setTimeout(resolve, 50)); + service.stop(); + + // Verify cleanup was called + expect(clearIntervalSpy).toHaveBeenCalled(); + + clearIntervalSpy.mockRestore(); + }); + }); + + describe('Scenario 4: Restart after failure', () => { + it('should restart successfully after price update failure', async () => { + // Mock contract updater to fail initially + const { createContractUpdater } = await import('../src/services/contract-updater.js'); + let callCount = 0; + + vi.mocked(createContractUpdater).mockReturnValueOnce({ + updatePrices: vi.fn().mockImplementation(async () => { + callCount++; + if (callCount <= 2) { + throw new Error('Network failure'); + } + return [{ success: true, asset: 'XLM', price: 150000n, timestamp: Date.now() }]; + }), + healthCheck: vi.fn().mockResolvedValue(true), + getAdminPublicKey: vi.fn().mockReturnValue('GTEST123'), + } as any); + + service = new OracleService(mockConfig); + + await service.start(['XLM']); + + // Wait for failure cycles + await new Promise(resolve => setTimeout(resolve, 500)); + + // Service should still be running despite failures + expect(service.getStatus().isRunning).toBe(true); + + service.stop(); + + // Restart should work + await service.start(['XLM']); + expect(service.getStatus().isRunning).toBe(true); + + service.stop(); + }); + + it('should handle restart after provider failure', async () => { + service = new OracleService(mockConfig); + + // Start and let it run + await service.start(['XLM']); + await new Promise(resolve => setTimeout(resolve, 100)); + + // Stop + service.stop(); + + // Restart should work normally + await service.start(['BTC']); + expect(service.getStatus().isRunning).toBe(true); + + await new Promise(resolve => setTimeout(resolve, 200)); + service.stop(); + }); + + it('should maintain state consistency across restart cycles', async () => { + service = new OracleService(mockConfig); + + // First run + await service.start(['XLM']); + await new Promise(resolve => setTimeout(resolve, 150)); + service.stop(); + + const statusAfterStop = service.getStatus(); + expect(statusAfterStop.isRunning).toBe(false); + + // Second run + await service.start(['BTC']); + const statusAfterRestart = service.getStatus(); + expect(statusAfterRestart.isRunning).toBe(true); + + await new Promise(resolve => setTimeout(resolve, 150)); + service.stop(); + }); + }); + + describe('Scenario 5: Graceful shutdown with pending updates', () => { + it('should handle graceful shutdown with pending operations', async () => { + service = new OracleService(mockConfig); + + // Start service with multiple assets to ensure longer update times + await service.start(['XLM', 'BTC', 'ETH', 'USDC', 'SOL']); + + // Wait for updates to be in progress + await new Promise(resolve => setTimeout(resolve, 100)); + + // Initiate graceful shutdown + const stopStartTime = Date.now(); + service.stop(); + const stopEndTime = Date.now(); + + // Stop should be quick (not waiting for pending operations) + expect(stopEndTime - stopStartTime).toBeLessThan(100); + + // Service should be stopped + expect(service.getStatus().isRunning).toBe(false); + + // Wait to ensure no background activity + await new Promise(resolve => setTimeout(resolve, 300)); + expect(service.getStatus().isRunning).toBe(false); + }); + + it('should clean up all resources during shutdown', async () => { + service = new OracleService(mockConfig); + + const clearIntervalSpy = vi.spyOn(global, 'clearInterval'); + + await service.start(['XLM', 'BTC']); + + // Ensure updates are running + await new Promise(resolve => setTimeout(resolve, 100)); + + service.stop(); + + // Verify cleanup + expect(clearIntervalSpy).toHaveBeenCalled(); + expect(service.getStatus().isRunning).toBe(false); + + clearIntervalSpy.mockRestore(); + }); + + it('should handle shutdown when multiple update cycles are pending', async () => { + service = new OracleService(mockConfig); + + // Start with very short interval to create overlapping updates + const fastConfig = { ...mockConfig, updateIntervalMs: 50 }; + service = new OracleService(fastConfig); + + await service.start(['XLM', 'BTC']); + + // Let multiple cycles start + await new Promise(resolve => setTimeout(resolve, 150)); + + // Shutdown should handle overlapping operations + service.stop(); + expect(service.getStatus().isRunning).toBe(false); + + // Ensure complete shutdown + await new Promise(resolve => setTimeout(resolve, 200)); + expect(service.getStatus().isRunning).toBe(false); + }); + + it('should not leave any promises hanging after shutdown', async () => { + service = new OracleService(mockConfig); + + await service.start(['XLM', 'BTC']); + + // Start some manual operations + const updatePromise1 = service.updatePrices(['ETH']); + const updatePromise2 = service.updatePrices(['USDC']); + + // Stop during operations + service.stop(); + + // All operations should resolve or be handled gracefully + await Promise.allSettled([updatePromise1, updatePromise2]); + + expect(service.getStatus().isRunning).toBe(false); + }); + }); + + describe('Resource leak prevention', () => { + it('should not accumulate interval references', async () => { + service = new OracleService(mockConfig); + + const setIntervalSpy = vi.spyOn(global, 'setInterval'); + const clearIntervalSpy = vi.spyOn(global, 'clearInterval'); + + // Multiple start/stop cycles + for (let i = 0; i < 5; i++) { + await service.start(['XLM']); + await new Promise(resolve => setTimeout(resolve, 100)); + service.stop(); + } + + // Should have balanced set/clear calls + expect(setIntervalSpy.mock.calls.length).toBe(clearIntervalSpy.mock.calls.length); + + setIntervalSpy.mockRestore(); + clearIntervalSpy.mockRestore(); + }); + + it('should handle rapid start/stop without accumulating timers', async () => { + service = new OracleService(mockConfig); + + // Rapid start/stop cycles + for (let i = 0; i < 10; i++) { + await service.start(['XLM']); + service.stop(); + await new Promise(resolve => setTimeout(resolve, 10)); + } + + expect(service.getStatus().isRunning).toBe(false); + }); + }); + + describe('Timing reliability', () => { + it('should handle timing variations without flaky behavior', async () => { + service = new OracleService(mockConfig); + + // Test with variable delays + const delays = [0, 10, 50, 100, 200]; + + for (const delay of delays) { + await service.start(['XLM']); + await new Promise(resolve => setTimeout(resolve, delay)); + service.stop(); + + expect(service.getStatus().isRunning).toBe(false); + await new Promise(resolve => setTimeout(resolve, 50)); + } + }); + + it('should maintain consistent behavior under load', async () => { + service = new OracleService(mockConfig); + + await service.start(['XLM', 'BTC']); + + // Simulate load with concurrent operations + const operations = Array.from({ length: 10 }, (_, i) => + service.updatePrices([`ASSET_${i}`]) + ); + + // Stop during load + service.stop(); + + await Promise.allSettled(operations); + + expect(service.getStatus().isRunning).toBe(false); + }); + }); +}); From 190099130f686ff33cbdaaf4d6144a64a3cef100 Mon Sep 17 00:00:00 2001 From: Gas Optimization Bot Date: Fri, 27 Mar 2026 09:14:39 +0100 Subject: [PATCH 4/6] Fix: Update lifecycle test timeouts and mock configurations - Align test intervals with existing test patterns (1000ms) - Update timeout values to ensure proper test execution - Fix mock return types to match expected interfaces - Ensure circuit breaker configuration is included - Improve test reliability and reduce flaky behavior Resolves CI check failures and Vercel build issues --- oracle/tests/lifecycle.test.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/oracle/tests/lifecycle.test.ts b/oracle/tests/lifecycle.test.ts index c50ceb4f..b29cee31 100644 --- a/oracle/tests/lifecycle.test.ts +++ b/oracle/tests/lifecycle.test.ts @@ -17,8 +17,8 @@ vi.mock('../src/services/contract-updater.js', () => ({ await new Promise(resolve => setTimeout(resolve, 50)); return prices.map((price, index) => ({ success: true, - asset: `ASSET_${index}`, - price: BigInt(Math.floor(price.price * 1000000)), + asset: price.asset || `ASSET_${index}`, + price: BigInt(Math.floor((price.price || 0.15) * 1000000)), timestamp: Date.now(), })); }), @@ -80,11 +80,15 @@ describe('OracleService Lifecycle Integration Tests', () => { stellarRpcUrl: 'https://soroban-testnet.stellar.org', contractId: 'CTEST123', adminSecretKey: 'STEST123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ123456', - updateIntervalMs: 200, // Short interval for faster tests + updateIntervalMs: 1000, // Use same interval as existing tests maxPriceDeviationPercent: 10, priceStaleThresholdSeconds: 300, cacheTtlSeconds: 30, logLevel: 'error', // Reduce log noise in tests + circuitBreaker: { + failureThreshold: 5, + backoffMs: 1000, + }, providers: [ { name: 'coingecko', @@ -125,7 +129,7 @@ describe('OracleService Lifecycle Integration Tests', () => { expect(service.getStatus().isRunning).toBe(true); // Allow at least one update cycle - await new Promise(resolve => setTimeout(resolve, 250)); + await new Promise(resolve => setTimeout(resolve, 1100)); // Stop service service.stop(); @@ -142,7 +146,7 @@ describe('OracleService Lifecycle Integration Tests', () => { // Perform multiple cycles for (let i = 0; i < 3; i++) { await service.start(['XLM']); - await new Promise(resolve => setTimeout(resolve, 250)); + await new Promise(resolve => setTimeout(resolve, 1100)); service.stop(); await new Promise(resolve => setTimeout(resolve, 100)); @@ -198,7 +202,7 @@ describe('OracleService Lifecycle Integration Tests', () => { await service.start(['XLM', 'BTC', 'ETH']); // Wait for update to start, then stop immediately - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise(resolve => setTimeout(resolve, 500)); service.stop(); // Should stop without throwing @@ -289,7 +293,7 @@ describe('OracleService Lifecycle Integration Tests', () => { // Start and let it run await service.start(['XLM']); - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise(resolve => setTimeout(resolve, 1100)); // Stop service.stop(); @@ -298,7 +302,7 @@ describe('OracleService Lifecycle Integration Tests', () => { await service.start(['BTC']); expect(service.getStatus().isRunning).toBe(true); - await new Promise(resolve => setTimeout(resolve, 200)); + await new Promise(resolve => setTimeout(resolve, 1100)); service.stop(); }); @@ -307,7 +311,7 @@ describe('OracleService Lifecycle Integration Tests', () => { // First run await service.start(['XLM']); - await new Promise(resolve => setTimeout(resolve, 150)); + await new Promise(resolve => setTimeout(resolve, 1100)); service.stop(); const statusAfterStop = service.getStatus(); @@ -318,7 +322,7 @@ describe('OracleService Lifecycle Integration Tests', () => { const statusAfterRestart = service.getStatus(); expect(statusAfterRestart.isRunning).toBe(true); - await new Promise(resolve => setTimeout(resolve, 150)); + await new Promise(resolve => setTimeout(resolve, 1100)); service.stop(); }); }); @@ -331,7 +335,7 @@ describe('OracleService Lifecycle Integration Tests', () => { await service.start(['XLM', 'BTC', 'ETH', 'USDC', 'SOL']); // Wait for updates to be in progress - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise(resolve => setTimeout(resolve, 500)); // Initiate graceful shutdown const stopStartTime = Date.now(); From b660f72f2b495a647e14a0251022b2a853e0a88f Mon Sep 17 00:00:00 2001 From: Gas Optimization Bot Date: Fri, 27 Mar 2026 09:15:59 +0100 Subject: [PATCH 5/6] Fix: Update Vercel configuration for reliable builds - Use npm ci instead of npm install for faster, reliable builds - Add NODE_ENV environment variable for production builds - Ensure consistent dependency resolution in CI/CD Resolves Vercel deployment issues --- vercel.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vercel.json b/vercel.json index f33e748b..f61e8451 100644 --- a/vercel.json +++ b/vercel.json @@ -1,6 +1,6 @@ { "version": 2, - "buildCommand": "cd api && npm install && npm run build", + "buildCommand": "cd api && npm ci && npm run build", "outputDirectory": "api/dist", "rewrites": [ { "source": "/(.*)", "destination": "/api/src/vercel.ts" } @@ -12,5 +12,8 @@ }, "github": { "silent": true + }, + "env": { + "NODE_ENV": "production" } } From 7995072df71beeb5c0ad70390c77be9507a58d53 Mon Sep 17 00:00:00 2001 From: Gas Optimization Bot Date: Fri, 27 Mar 2026 09:16:45 +0100 Subject: [PATCH 6/6] Fix: Update root package.json for better workspace support - Add version field for proper package management - Add npm engine requirement for consistency - Add shared devDependencies for workspace builds - Ensure proper dependency resolution across workspaces Resolves CI workspace build issues --- package.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6a4da509..60216089 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "stellarlend", + "version": "1.0.0", "private": true, "workspaces": [ "api", @@ -14,6 +15,11 @@ "typecheck:all": "npm run typecheck --workspaces --if-present" }, "engines": { - "node": ">=18.0.0" + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "devDependencies": { + "@types/node": "^20.10.5", + "typescript": "^5.3.3" } }