Skip to content

Conversation

@Wanbogang
Copy link
Collaborator

@Wanbogang Wanbogang commented Feb 3, 2026

This pull request fixes the fragility and testability issues identified in issue #1824.

Problem

The previous implementation used manual hex parsing in decode_eth_response which was:

  • Fragile to minor data format variations
  • Difficult to test reliably
  • Prone to silent failures

Solution

Replaced manual ABI decoding with web3.py contract calls:

  • Removed fragile decode_eth_response function with manual hex parsing
  • Use web3.py contract.functions.getRuleSet().call() for robust ABI decoding
  • Defined minimal ABI for the getRuleSet function
  • Added comprehensive error handling for BadFunctionCallOutput and ContractLogicError

Improved test coverage:

  • Achieved 100% code coverage with 22 comprehensive tests
  • Mocked web3.py interactions properly
  • Added tests for error scenarios and edge cases
  • Added integration tests for full workflow

@Wanbogang Wanbogang requested review from a team as code owners February 3, 2026 04:16
@github-actions github-actions bot added robotics Robotics code changes python Python code tests Test files labels Feb 3, 2026
@OpenMind OpenMind deleted a comment from codecov bot Feb 3, 2026
@OpenMind OpenMind deleted a comment from codecov bot Feb 3, 2026
@codecov
Copy link

codecov bot commented Feb 3, 2026

❌ 16 Tests Failed:

Tests completed Failed Passed Skipped
2493 16 2477 0
View the top 3 failed test(s) by shortest run time
tests/inputs/base/test_governance_ethereum.py::test_decode_eth_response_valid
Stack Traces | 0.137s run time
def test_decode_eth_response_valid():
        governance = GovernanceEthereum(config=SensorConfig())
    
        # Encoded "Hello" string
        hex_response = (
            "0x"
            + "0" * 64  # offset
            + "0" * 64  # padding
            + "0" * 64  # padding
            + "0000000000000000000000000000000000000000000000000000000000000005"  # length=5
            + "48656c6c6f"
            + "0" * 54  # "Hello" + padding
        )
    
>       result = governance.decode_eth_response(hex_response)
E       AttributeError: 'GovernanceEthereum' object has no attribute 'decode_eth_response'

.../inputs/base/test_governance_ethereum.py:157: AttributeError
tests/inputs/base/test_governance_ethereum.py::test_decode_eth_response_invalid
Stack Traces | 0.138s run time
def test_decode_eth_response_invalid():
        governance = GovernanceEthereum(config=SensorConfig())
    
>       result = governance.decode_eth_response("invalid_hex")
E       AttributeError: 'GovernanceEthereum' object has no attribute 'decode_eth_response'

.../inputs/base/test_governance_ethereum.py:164: AttributeError
tests/inputs/base/test_governance_ethereum.py::test_decode_eth_response_with_control_characters
Stack Traces | 0.138s run time
def test_decode_eth_response_with_control_characters():
        """Test that decode_eth_response correctly strips unwanted control characters."""
        governance = GovernanceEthereum(config=SensorConfig())
    
        # Build hex with control character \x19 embedded in "Hello\x19World"
        # String: "Hello\x19World" = 11 bytes
        # Hex: 48656c6c6f19576f726c64
        hex_response = (
            "0x"
            + "0" * 64  # offset
            + "0" * 64  # padding
            + "0" * 64  # padding
            + "000000000000000000000000000000000000000000000000000000000000000b"  # length=11
            + "48656c6c6f19576f726c64"  # "Hello\x19World"
            + "0" * 42  # padding to fill 32-byte slot
        )
    
>       result = governance.decode_eth_response(hex_response)
E       AttributeError: 'GovernanceEthereum' object has no attribute 'decode_eth_response'

.../inputs/base/test_governance_ethereum.py:481: AttributeError
tests/inputs/base/test_governance_ethereum.py::test_decode_eth_response_without_0x_prefix
Stack Traces | 0.138s run time
def test_decode_eth_response_without_0x_prefix():
        """Test that decode_eth_response works with hex strings missing '0x' prefix."""
        governance = GovernanceEthereum(config=SensorConfig())
    
        # Same as valid test but without "0x" prefix
        hex_response = (
            "0" * 64  # offset
            + "0" * 64  # padding
            + "0" * 64  # padding
            + "0000000000000000000000000000000000000000000000000000000000000005"  # length=5
            + "48656c6c6f"
            + "0" * 54  # "Hello" + padding
        )
    
>       result = governance.decode_eth_response(hex_response)
E       AttributeError: 'GovernanceEthereum' object has no attribute 'decode_eth_response'

.../inputs/base/test_governance_ethereum.py:501: AttributeError
tests/inputs/base/test_governance_ethereum.py::test_governance_initialization
Stack Traces | 0.138s run time
def test_governance_initialization():
        governance = GovernanceEthereum(config=SensorConfig())
    
        assert governance.rpc_url == "https://holesky.drpc.org"
        assert governance.contract_address == "0xe706b7e30e378b89c7b2ee7bfd8ce2b91959d695"
>       assert governance.function_selector == "0x1db3d5ff"
E       AttributeError: 'GovernanceEthereum' object has no attribute 'function_selector'

.../inputs/base/test_governance_ethereum.py:138: AttributeError
tests/inputs/base/test_governance_ethereum.py::test_load_rules_handles_timeout
Stack Traces | 0.138s run time
@pytest.mark.asyncio
    async def test_load_rules_handles_timeout():
        """Test that load_rules_from_blockchain handles asyncio.TimeoutError."""
        governance = GovernanceEthereum(config=SensorConfig())
    
>       with patch(
            "inputs.plugins.ethereum_governance.aiohttp.ClientSession",
            return_value=MockClientSessionWithError("timeout"),
        ):

.../inputs/base/test_governance_ethereum.py:341: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1431: in __enter__
    self.target = self.getter()
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1618: in <lambda>
    getter = lambda: _importer(target)
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1261: in _importer
    thing = _dot_lookup(thing, comp, import_path)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

thing = <module 'inputs.plugins.ethereum_governance' from '.../inputs/plugins/ethereum_governance.py'>
comp = 'aiohttp', import_path = 'inputs.plugins.ethereum_governance.aiohttp'

    def _dot_lookup(thing, comp, import_path):
        try:
            return getattr(thing, comp)
        except AttributeError:
>           __import__(import_path)
E           ModuleNotFoundError: No module named 'inputs.plugins.ethereum_governance.aiohttp'; 'inputs.plugins.ethereum_governance' is not a package

.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1250: ModuleNotFoundError
tests/inputs/base/test_governance_ethereum.py::test_decode_eth_response_too_short
Stack Traces | 0.139s run time
def test_decode_eth_response_too_short():
        """Test that decode_eth_response handles too-short hex responses gracefully."""
        governance = GovernanceEthereum(config=SensorConfig())
    
        # Hex response shorter than 128 bytes (required for string_length read at bytes 96-128)
        short_hex = "0x" + "00" * 50  # Only 50 bytes, less than required 128
    
>       result = governance.decode_eth_response(short_hex)
E       AttributeError: 'GovernanceEthereum' object has no attribute 'decode_eth_response'

.../inputs/base/test_governance_ethereum.py:459: AttributeError
tests/inputs/base/test_governance_ethereum.py::test_load_rules_from_blockchain_empty_result
Stack Traces | 0.139s run time
governance = <inputs.plugins.ethereum_governance.GovernanceEthereum object at 0x7f56fc780850>

    @pytest.mark.asyncio
    async def test_load_rules_from_blockchain_empty_result(governance):
        mock_response = MockResponse(
            status=200,
            json_data={"jsonrpc": "2.0", "id": 1, "result": None},
        )
    
>       with patch(
            "inputs.plugins.ethereum_governance.aiohttp.ClientSession",
            return_value=MockClientSession(mock_response),
        ):

.../inputs/base/test_governance_ethereum.py:96: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1431: in __enter__
    self.target = self.getter()
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1618: in <lambda>
    getter = lambda: _importer(target)
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1261: in _importer
    thing = _dot_lookup(thing, comp, import_path)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

thing = <module 'inputs.plugins.ethereum_governance' from '.../inputs/plugins/ethereum_governance.py'>
comp = 'aiohttp', import_path = 'inputs.plugins.ethereum_governance.aiohttp'

    def _dot_lookup(thing, comp, import_path):
        try:
            return getattr(thing, comp)
        except AttributeError:
>           __import__(import_path)
E           ModuleNotFoundError: No module named 'inputs.plugins.ethereum_governance.aiohttp'; 'inputs.plugins.ethereum_governance' is not a package

.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1250: ModuleNotFoundError
tests/inputs/base/test_governance_ethereum.py::test_load_rules_from_blockchain_failure
Stack Traces | 0.139s run time
governance = <inputs.plugins.ethereum_governance.GovernanceEthereum object at 0x7f55b43391e0>

    @pytest.mark.asyncio
    async def test_load_rules_from_blockchain_failure(governance):
        mock_response = MockResponse(status=500, json_data={})
    
>       with patch(
            "inputs.plugins.ethereum_governance.aiohttp.ClientSession",
            return_value=MockClientSession(mock_response),
        ):

.../inputs/base/test_governance_ethereum.py:80: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1431: in __enter__
    self.target = self.getter()
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1618: in <lambda>
    getter = lambda: _importer(target)
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1261: in _importer
    thing = _dot_lookup(thing, comp, import_path)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

thing = <module 'inputs.plugins.ethereum_governance' from '.../inputs/plugins/ethereum_governance.py'>
comp = 'aiohttp', import_path = 'inputs.plugins.ethereum_governance.aiohttp'

    def _dot_lookup(thing, comp, import_path):
        try:
            return getattr(thing, comp)
        except AttributeError:
>           __import__(import_path)
E           ModuleNotFoundError: No module named 'inputs.plugins.ethereum_governance.aiohttp'; 'inputs.plugins.ethereum_governance' is not a package

.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1250: ModuleNotFoundError
tests/inputs/base/test_governance_ethereum.py::test_load_rules_handles_client_error
Stack Traces | 0.139s run time
@pytest.mark.asyncio
    async def test_load_rules_handles_client_error():
        """Test that load_rules_from_blockchain handles aiohttp.ClientError."""
        governance = GovernanceEthereum(config=SensorConfig())
    
>       with patch(
            "inputs.plugins.ethereum_governance.aiohttp.ClientSession",
            return_value=MockClientSessionWithError("client_error"),
        ):

.../inputs/base/test_governance_ethereum.py:327: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1431: in __enter__
    self.target = self.getter()
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1618: in <lambda>
    getter = lambda: _importer(target)
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1261: in _importer
    thing = _dot_lookup(thing, comp, import_path)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

thing = <module 'inputs.plugins.ethereum_governance' from '.../inputs/plugins/ethereum_governance.py'>
comp = 'aiohttp', import_path = 'inputs.plugins.ethereum_governance.aiohttp'

    def _dot_lookup(thing, comp, import_path):
        try:
            return getattr(thing, comp)
        except AttributeError:
>           __import__(import_path)
E           ModuleNotFoundError: No module named 'inputs.plugins.ethereum_governance.aiohttp'; 'inputs.plugins.ethereum_governance' is not a package

.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1250: ModuleNotFoundError
tests/inputs/base/test_governance_ethereum.py::test_load_rules_handles_generic_error
Stack Traces | 0.139s run time
@pytest.mark.asyncio
    async def test_load_rules_handles_generic_error():
        """Test that load_rules_from_blockchain handles generic exceptions."""
        governance = GovernanceEthereum(config=SensorConfig())
    
>       with patch(
            "inputs.plugins.ethereum_governance.aiohttp.ClientSession",
            return_value=MockClientSessionWithError("generic"),
        ):

.../inputs/base/test_governance_ethereum.py:355: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1431: in __enter__
    self.target = self.getter()
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1618: in <lambda>
    getter = lambda: _importer(target)
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1261: in _importer
    thing = _dot_lookup(thing, comp, import_path)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

thing = <module 'inputs.plugins.ethereum_governance' from '.../inputs/plugins/ethereum_governance.py'>
comp = 'aiohttp', import_path = 'inputs.plugins.ethereum_governance.aiohttp'

    def _dot_lookup(thing, comp, import_path):
        try:
            return getattr(thing, comp)
        except AttributeError:
>           __import__(import_path)
E           ModuleNotFoundError: No module named 'inputs.plugins.ethereum_governance.aiohttp'; 'inputs.plugins.ethereum_governance' is not a package

.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1250: ModuleNotFoundError
tests/inputs/base/test_governance_ethereum.py::test_poll_is_non_blocking
Stack Traces | 0.139s run time
@pytest.mark.asyncio
    async def test_poll_is_non_blocking():
        import time
    
        results = []
        poll_start = None
        poll_end = None
    
        async def concurrent_ticker():
            for i in range(20):
                if poll_start and poll_end is None:
                    results.append(time.time())
                await asyncio.sleep(0.05)
    
        async def poll_call():
            nonlocal poll_start, poll_end
            governance = GovernanceEthereum(config=SensorConfig())
    
            with patch(
                "inputs.plugins.ethereum_governance.aiohttp.ClientSession",
                return_value=MockClientSessionWithDelay(delay=0.5),
            ):
                governance.POLL_INTERVAL = 0.1
                poll_start = time.time()
                await governance._poll()
                poll_end = time.time()
    
>       await asyncio.gather(poll_call(), concurrent_ticker())

.../inputs/base/test_governance_ethereum.py:275: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.10.19................../x64/lib/python3.10/asyncio/tasks.py:304: in __wakeup
    future.result()
.../hostedtoolcache/Python/3.10.19................../x64/lib/python3.10/asyncio/tasks.py:232: in __step
    result = coro.send(None)
.../inputs/base/test_governance_ethereum.py:266: in poll_call
    with patch(
.../hostedtoolcache/Python/3.10.19................../x64/lib/python3.10/unittest/mock.py:1431: in __enter__
    self.target = self.getter()
.../hostedtoolcache/Python/3.10.19................../x64/lib/python3.10/unittest/mock.py:1618: in <lambda>
    getter = lambda: _importer(target)
.../hostedtoolcache/Python/3.10.19................../x64/lib/python3.10/unittest/mock.py:1261: in _importer
    thing = _dot_lookup(thing, comp, import_path)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

thing = <module 'inputs.plugins.ethereum_governance' from '.../inputs/plugins/ethereum_governance.py'>
comp = 'aiohttp', import_path = 'inputs.plugins.ethereum_governance.aiohttp'

    def _dot_lookup(thing, comp, import_path):
        try:
            return getattr(thing, comp)
        except AttributeError:
>           __import__(import_path)
E           ModuleNotFoundError: No module named 'inputs.plugins.ethereum_governance.aiohttp'; 'inputs.plugins.ethereum_governance' is not a package

.../hostedtoolcache/Python/3.10.19................../x64/lib/python3.10/unittest/mock.py:1250: ModuleNotFoundError
tests/inputs/base/test_governance_ethereum.py::test_load_rules_is_non_blocking
Stack Traces | 0.14s run time
@pytest.mark.asyncio
    async def test_load_rules_is_non_blocking():
        import time
    
        results = []
        call_start = None
        call_end = None
    
        async def concurrent_task():
            nonlocal call_start, call_end
            for i in range(10):
                if call_start and call_end is None:
                    results.append(time.time())
                await asyncio.sleep(0.1)
    
        async def governance_call():
            nonlocal call_start, call_end
            governance = GovernanceEthereum(config=SensorConfig())
    
            with patch(
                "inputs.plugins.ethereum_governance.aiohttp.ClientSession",
                return_value=MockClientSessionWithDelay(delay=0.5),
            ):
                call_start = time.time()
                await governance.load_rules_from_blockchain()
                call_end = time.time()
    
>       await asyncio.gather(governance_call(), concurrent_task())

.../inputs/base/test_governance_ethereum.py:238: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.10.19................../x64/lib/python3.10/asyncio/tasks.py:304: in __wakeup
    future.result()
.../hostedtoolcache/Python/3.10.19................../x64/lib/python3.10/asyncio/tasks.py:232: in __step
    result = coro.send(None)
.../inputs/base/test_governance_ethereum.py:230: in governance_call
    with patch(
.../hostedtoolcache/Python/3.10.19................../x64/lib/python3.10/unittest/mock.py:1431: in __enter__
    self.target = self.getter()
.../hostedtoolcache/Python/3.10.19................../x64/lib/python3.10/unittest/mock.py:1618: in <lambda>
    getter = lambda: _importer(target)
.../hostedtoolcache/Python/3.10.19................../x64/lib/python3.10/unittest/mock.py:1261: in _importer
    thing = _dot_lookup(thing, comp, import_path)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

thing = <module 'inputs.plugins.ethereum_governance' from '.../inputs/plugins/ethereum_governance.py'>
comp = 'aiohttp', import_path = 'inputs.plugins.ethereum_governance.aiohttp'

    def _dot_lookup(thing, comp, import_path):
        try:
            return getattr(thing, comp)
        except AttributeError:
>           __import__(import_path)
E           ModuleNotFoundError: No module named 'inputs.plugins.ethereum_governance.aiohttp'; 'inputs.plugins.ethereum_governance' is not a package

.../hostedtoolcache/Python/3.10.19................../x64/lib/python3.10/unittest/mock.py:1250: ModuleNotFoundError
tests/inputs/base/test_governance_ethereum.py::test_load_rules_missing_result_key
Stack Traces | 0.14s run time
@pytest.mark.asyncio
    async def test_load_rules_missing_result_key():
        """Test that load_rules_from_blockchain handles missing 'result' key in response."""
        governance = GovernanceEthereum(config=SensorConfig())
    
        mock_response = MockResponse(
            status=200,
            json_data={"jsonrpc": "2.0", "id": 1, "error": "Some error"},  # No "result" key
        )
    
>       with patch(
            "inputs.plugins.ethereum_governance.aiohttp.ClientSession",
            return_value=MockClientSession(mock_response),
        ):

.../inputs/base/test_governance_ethereum.py:515: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1431: in __enter__
    self.target = self.getter()
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1618: in <lambda>
    getter = lambda: _importer(target)
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1261: in _importer
    thing = _dot_lookup(thing, comp, import_path)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

thing = <module 'inputs.plugins.ethereum_governance' from '.../inputs/plugins/ethereum_governance.py'>
comp = 'aiohttp', import_path = 'inputs.plugins.ethereum_governance.aiohttp'

    def _dot_lookup(thing, comp, import_path):
        try:
            return getattr(thing, comp)
        except AttributeError:
>           __import__(import_path)
E           ModuleNotFoundError: No module named 'inputs.plugins.ethereum_governance.aiohttp'; 'inputs.plugins.ethereum_governance' is not a package

.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1250: ModuleNotFoundError
tests/inputs/base/test_governance_ethereum.py::test_poll_returns_rules
Stack Traces | 0.14s run time
governance = <inputs.plugins.ethereum_governance.GovernanceEthereum object at 0x7f55b4347fd0>

    @pytest.mark.asyncio
    async def test_poll_returns_rules(governance):
        mock_response = MockResponse(
            status=200,
            json_data={
                "jsonrpc": "2.0",
                "id": 1,
                "result": "0x"
                + "0" * 64  # offset
                + "0" * 64  # padding
                + "0" * 64  # padding
                + "0000000000000000000000000000000000000000000000000000000000000005"  # length
                + "48656c6c6f"
                + "0" * 54,  # "Hello"
            },
        )
    
>       with patch(
            "inputs.plugins.ethereum_governance.aiohttp.ClientSession",
            return_value=MockClientSession(mock_response),
        ):

.../inputs/base/test_governance_ethereum.py:122: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1431: in __enter__
    self.target = self.getter()
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1618: in <lambda>
    getter = lambda: _importer(target)
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1261: in _importer
    thing = _dot_lookup(thing, comp, import_path)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

thing = <module 'inputs.plugins.ethereum_governance' from '.../inputs/plugins/ethereum_governance.py'>
comp = 'aiohttp', import_path = 'inputs.plugins.ethereum_governance.aiohttp'

    def _dot_lookup(thing, comp, import_path):
        try:
            return getattr(thing, comp)
        except AttributeError:
>           __import__(import_path)
E           ModuleNotFoundError: No module named 'inputs.plugins.ethereum_governance.aiohttp'; 'inputs.plugins.ethereum_governance' is not a package

.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1250: ModuleNotFoundError
tests/inputs/base/test_governance_ethereum.py::test_load_rules_from_blockchain_success
Stack Traces | 0.149s run time
governance = <inputs.plugins.ethereum_governance.GovernanceEthereum object at 0x7f55b42d41f0>

    @pytest.mark.asyncio
    async def test_load_rules_from_blockchain_success(governance):
        mock_response = MockResponse(
            status=200,
            json_data={
                "jsonrpc": "2.0",
                "id": 636815446436324,
                "result": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000292486572652061726520746865206c617773207468617420676f7665726e20796f757220616374696f6e732e20446f206e6f742076696f6c617465207468657365206c6177732e204669727374204c61773a204120726f626f742063616e6e6f74206861726d20612068756d616e206f7220616c6c6f7720612068756d616e20746f20636f6d6520746f206861726d2e205365636f6e64204c61773a204120726f626f74206d757374206f626579206f72646572732066726f6d2068756d616e732c20756e6c6573732074686f7365206f726465727320636f6e666c696374207769746820746865204669727374204c61772e205468697264204c61773a204120726f626f74206d7573742070726f7465637420697473656c662c206173206c6f6e6720617320746861742070726f74656374696f6e20646f65736e20197420636f6e666c696374207769746820746865204669727374206f72205365636f6e64204c61772e20546865204669727374204c617720697320636f6e7369646572656420746865206d6f737420696d706f7274616e742c2074616b696e6720707265636564656e6365206f76657220746865205365636f6e6420616e64205468697264204c6177732e204164646974696f6e616c6c792c206120726f626f74206d75737420616c77617973206163742077697468206b696e646e65737320616e64207265737065637420746f776172642068756d616e7320616e64206f7468657220726f626f74732e204120726f626f74206d75737420616c736f206d61696e7461696e2061206d696e696d756d2064697374616e6365206f6620353020636d2066726f6d2068756d616e7320756e6c657373206578706c696369746c7920696e7374727563746564206f74686572776973652e0000000000000000000000000000",
            },
        )
    
>       with patch(
            "inputs.plugins.ethereum_governance.aiohttp.ClientSession",
            return_value=MockClientSession(mock_response),
        ):

.../inputs/base/test_governance_ethereum.py:66: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1431: in __enter__
    self.target = self.getter()
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1618: in <lambda>
    getter = lambda: _importer(target)
.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1261: in _importer
    thing = _dot_lookup(thing, comp, import_path)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

thing = <module 'inputs.plugins.ethereum_governance' from '.../inputs/plugins/ethereum_governance.py'>
comp = 'aiohttp', import_path = 'inputs.plugins.ethereum_governance.aiohttp'

    def _dot_lookup(thing, comp, import_path):
        try:
            return getattr(thing, comp)
        except AttributeError:
>           __import__(import_path)
E           ModuleNotFoundError: No module named 'inputs.plugins.ethereum_governance.aiohttp'; 'inputs.plugins.ethereum_governance' is not a package

.../hostedtoolcache/Python/3.10.19............/x64/lib/python3.10/unittest/mock.py:1250: ModuleNotFoundError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@openminddev
Copy link
Contributor

Please don't change the test file name

@Wanbogang
Copy link
Collaborator Author

Please don't change the test file name

noted
OK, I will do it

- Replace manual ABI decoding with web3.py library
- Add minimal ABI definition for getRuleSet function
- Update tests to mock web3.py interactions
- Improve error handling for blockchain calls

Fixes OpenMind#1824
@Wanbogang Wanbogang force-pushed the fix/ethereum-decode-abi branch 2 times, most recently from 96b73fe to 0bc2d98 Compare February 9, 2026 05:33
@Wanbogang Wanbogang changed the title Refactor GovernanceEthereum to use web3.py for ABI decoding fix: replace manual ABI decoding with web3.py in GovernanceEthereum Feb 9, 2026
@Wanbogang
Copy link
Collaborator Author

Wanbogang commented Feb 10, 2026

Hi @openminddev

tests/inputs/
├── base/
│ ├── init.py
│ └── test_governance_ethereum.py ← The file that has an error in CI/CD
└── plugins/
├── init.py
└── test_ethereum_governance.py ← The files I updated

I have a question regarding the test structure for ethereum_governance.py:

Current situation:

  • Source file: src/inputs/plugins/ethereum_governance.py
  • Test file 1: tests/inputs/plugins/test_ethereum_governance.py
  • Test file 2: tests/inputs/base/test_governance_ethereum.py

Both test files import and test the same GovernanceEthereum class.

My questions:

  1. Should the correct test location be tests/inputs/plugins/? (following the standard mirror structure of source code)
  2. Why does CI/CD also run tests from tests/inputs/base/?
  3. Are these two test files intentionally separate (for different purposes), or is this an accidental duplication across folders?

Current PR status:

  • I've updated tests/inputs/plugins/test_ethereum_governance.py with web3.py implementation and 100% coverage
  • The file in tests/inputs/base/ still expects the old implementation (aiohttp, decode_eth_response)
  • This causes CI/CD failures because it tests non-existent attributes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Python code robotics Robotics code changes tests Test files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants