From 3d957bfb7629fb73cdc9c5acb6bed51ba4de578e Mon Sep 17 00:00:00 2001 From: openhands Date: Tue, 17 Mar 2026 19:34:58 +0000 Subject: [PATCH] fix: txnBot has zero test coverage after deleting recenterAccess.test.ts (#919) Co-Authored-By: Claude Sonnet 4.6 --- services/txnBot/src/service.test.ts | 134 ++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 services/txnBot/src/service.test.ts diff --git a/services/txnBot/src/service.test.ts b/services/txnBot/src/service.test.ts new file mode 100644 index 0000000..ff095e3 --- /dev/null +++ b/services/txnBot/src/service.test.ts @@ -0,0 +1,134 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { createTxnBot, type TxnBotDependencies } from './service.js'; +import type { BotConfigService } from './services/BotConfigService.js'; +import type { BlockchainService } from './services/BlockchainService.js'; +import type { GraphQLService } from './services/GraphQLService.js'; + +const BASE_CONFIG = { + PROVIDER_URL: 'http://localhost:8545', + PRIVATE_KEY: '0xdeadbeef000000000000000000000000000000000000000000000000deadbeef', + LM_CONTRACT_ADDRESS: '0x0000000000000000000000000000000000000001', + STAKE_CONTRACT_ADDRESS: '0x0000000000000000000000000000000000000002', + GRAPHQL_ENDPOINT: 'http://localhost:42069/graphql', + ENVIRONMENT: 'test', + PORT: '3000', +}; + +function makeTx(hash: string) { + return { hash, wait: (): Promise => Promise.resolve(null) }; +} + +function makeDeps( + blockchain?: Partial<{ + estimateRecenterGas: () => Promise; + recenter: () => Promise>; + }>, +): TxnBotDependencies { + return { + configService: { + getConfig: () => ({ ...BASE_CONFIG }), + getPort: () => '3000', + } as unknown as BotConfigService, + + blockchainService: { + estimateRecenterGas: blockchain?.estimateRecenterGas ?? (() => Promise.resolve()), + recenter: blockchain?.recenter ?? (() => Promise.resolve(makeTx('0xabc'))), + checkFunds: () => Promise.resolve('1.0'), + payTax: () => Promise.resolve(makeTx('0xpay')), + } as unknown as BlockchainService, + + graphQLService: { + fetchActivePositions: () => Promise.resolve([]), + } as unknown as GraphQLService, + }; +} + +describe('evaluateRecenterOpportunity', () => { + it('returns canRecenter: true when gas estimation succeeds', async () => { + const bot = createTxnBot(makeDeps()); + const result = await bot.evaluateRecenterOpportunity(); + + assert.equal(result.canRecenter, true); + assert.equal(result.reason, null); + assert.equal(result.error, null); + assert.ok(result.checkedAtMs > 0); + }); + + it('returns canRecenter: false with extracted revert reason', async () => { + const bot = createTxnBot( + makeDeps({ + estimateRecenterGas: () => + Promise.reject({ + shortMessage: 'execution reverted: recenter not needed', + message: 'execution reverted: recenter not needed', + }), + }), + ); + const result = await bot.evaluateRecenterOpportunity(); + + assert.equal(result.canRecenter, false); + assert.equal(result.reason, 'recenter not needed'); + assert.equal(result.error, 'execution reverted: recenter not needed'); + }); + + it('returns canRecenter: false with generic message when no revert prefix', async () => { + const bot = createTxnBot( + makeDeps({ + estimateRecenterGas: () => Promise.reject({ message: 'connection refused' }), + }), + ); + const result = await bot.evaluateRecenterOpportunity(); + + assert.equal(result.canRecenter, false); + assert.equal(result.reason, 'connection refused'); + assert.ok(result.checkedAtMs > 0); + }); +}); + +describe('attemptRecenter', () => { + it('calls recenter() and returns executed: true when eligible', async () => { + let recenterCalled = false; + const bot = createTxnBot( + makeDeps({ + recenter: () => { + recenterCalled = true; + return Promise.resolve(makeTx('0xdeadbeef')); + }, + }), + ); + const result = await bot.attemptRecenter(); + + assert.equal(result.executed, true); + assert.equal(result.txHash, '0xdeadbeef'); + assert.ok(recenterCalled, 'recenter() should have been called'); + }); + + it('skips recenter() and returns executed: false when not eligible', async () => { + let recenterCalled = false; + const bot = createTxnBot( + makeDeps({ + estimateRecenterGas: () => Promise.reject({ message: 'recenter not needed' }), + recenter: () => { + recenterCalled = true; + return Promise.resolve(makeTx('0xabc')); + }, + }), + ); + const result = await bot.attemptRecenter(); + + assert.equal(result.executed, false); + assert.equal(recenterCalled, false, 'recenter() must not be called when not eligible'); + assert.ok(result.message); + }); + + it('propagates error thrown by recenter() — caught by liquidityLoop', async () => { + const bot = createTxnBot( + makeDeps({ + recenter: () => Promise.reject(new Error('tx submission failed')), + }), + ); + + await assert.rejects(bot.attemptRecenter(), /tx submission failed/); + }); +});