From b948fd98166ae8fe6fa9ad9b443c7c4d06f8c8da Mon Sep 17 00:00:00 2001 From: Thykof Date: Wed, 27 May 2026 17:22:55 +0200 Subject: [PATCH] tests: mock miner price-fetch, drop validator-side PYTH_API_KEY MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The validator never needs PYTH_API_KEY — only the miner's Lazer live-price fetch does. But CI set it and tests reached the live Lazer endpoint through generate_simulations(), so an unrelated Lazer error failed every PR (e.g. run 26459886896). - Mock synth.miner.simulations.get_asset_price in the test fixtures that call generate_simulations (tests/utils.py prepare_random_predictions, tests/test_simulations.py, and the three direct-call tests in tests/test_forward.py). The GBM math still runs on a pinned price; no network call. - Drop PYTH_API_KEY from the CI workflow env; keep PYTH_BACKEND=pro. - Default PYTH_BACKEND=pro in conftest so local runs match CI. The scoring tests fetch real prices live from Pyth; the legacy hermes/benchmarks endpoint rate-limits (429) under the suite's volume, the public Pro router does not. setdefault leaves an explicit override untouched. - Clarify in .env.example that PYTH_API_KEY is miner-only. Co-Authored-By: Claude Opus 4.7 --- .env.example | 6 +++-- .github/workflows/ci.yaml | 1 - tests/conftest.py | 6 +++++ tests/test_forward.py | 10 ++++++++ tests/test_simulations.py | 10 ++++++-- tests/utils.py | 51 ++++++++++++++++++++++----------------- 6 files changed, 57 insertions(+), 27 deletions(-) diff --git a/.env.example b/.env.example index 04a7c355..d5bcce1c 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,8 @@ LOG_ID_PREFIX= # and the miner's get_asset_price to Pyth Lazer # https://pyth-lazer.dourolabs.app/v1/latest_price (requires PYTH_API_KEY). PYTH_BACKEND=hermes -# Free Bearer token from pythdata.app -> Pyth Terminal. Only required when -# PYTH_BACKEND=pro. +# Miner-only. Bearer token from pythdata.app -> Pyth Terminal, used by +# the miner's get_asset_price live-price fetch via Pyth Lazer when +# PYTH_BACKEND=pro. Validators do NOT need this: on `pro` they read the +# public history endpoint above, which takes no auth. PYTH_API_KEY= diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3ab013f6..6a64f216 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,6 @@ jobs: runs-on: ubuntu-latest env: PYTH_BACKEND: pro - PYTH_API_KEY: ${{ secrets.PYTH_API_KEY }} steps: - name: Checkout code uses: actions/checkout@v5 diff --git a/tests/conftest.py b/tests/conftest.py index cfff9ecf..95c37425 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,12 @@ from sqlalchemy import create_engine from testcontainers.postgres import PostgresContainer +# The scoring tests fetch real prices live from Pyth. Default to the Pro +# router (public, no API key) like CI does — the legacy hermes/benchmarks +# endpoint rate-limits (429) under the suite's call volume. `setdefault` +# leaves an explicit `PYTH_BACKEND=hermes` override untouched. +os.environ.setdefault("PYTH_BACKEND", "pro") + postgres = PostgresContainer("postgres:16-alpine") diff --git a/tests/test_forward.py b/tests/test_forward.py index 90563aa7..67230703 100644 --- a/tests/test_forward.py +++ b/tests/test_forward.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta, timezone import logging +from unittest.mock import patch # from numpy.testing import assert_almost_equal import bittensor as bt @@ -73,7 +74,12 @@ def test_calculate_moving_average_and_update_rewards(db_engine: Engine): print("moving_averages_data", moving_averages_data) +# Pin the miner's live-price fetch (Pyth Lazer on PYTH_BACKEND=pro) so these +# tests don't depend on it. PriceDataProvider's history fetch stays live — it +# hits the public Pyth Pro history endpoint and is part of what's scored. +@patch("synth.miner.simulations.get_asset_price", return_value=90000.0) def test_calculate_moving_average_and_update_rewards_new_miner( + mock_get_asset_price, db_engine: Engine, ): miner_uids = [10, 20, 33, 40, 50, 60] @@ -173,7 +179,9 @@ def test_calculate_moving_average_and_update_rewards_new_miner( print("moving_averages_data", moving_averages_data) +@patch("synth.miner.simulations.get_asset_price", return_value=90000.0) def test_calculate_moving_average_and_update_rewards_new_miner_registration( + mock_get_asset_price, db_engine: Engine, ): bt.logging._logger.setLevel(logging.DEBUG) @@ -314,7 +322,9 @@ def test_calculate_moving_average_and_update_rewards_new_miner_registration( # assert_almost_equal(sum(miner_weights), 0.5, decimal=12) +@patch("synth.miner.simulations.get_asset_price", return_value=90000.0) def test_calculate_moving_average_and_update_rewards_only_invalid( + mock_get_asset_price, db_engine: Engine, ): handler = MinerDataHandler(db_engine) diff --git a/tests/test_simulations.py b/tests/test_simulations.py index 49b8ef70..678e6bbc 100644 --- a/tests/test_simulations.py +++ b/tests/test_simulations.py @@ -1,10 +1,15 @@ +from unittest.mock import patch + from synth.miner.simulations import generate_simulations from synth.simulation_input import SimulationInput from synth.utils.helpers import get_current_time, round_time_to_minutes from synth.validator.response_validation_v2 import CORRECT, validate_responses -def test_generate_simulations(): +# get_asset_price hits a live price feed (Pyth Lazer on PYTH_BACKEND=pro); +# pin it so these tests exercise the simulation math without a network call. +@patch("synth.miner.simulations.get_asset_price", return_value=90000.0) +def test_generate_simulations(mock_get_asset_price): result = generate_simulations( asset="BTC", start_time="2025-02-04T00:00:00+00:00", @@ -21,7 +26,8 @@ def test_generate_simulations(): ) -def test_run(): +@patch("synth.miner.simulations.get_asset_price", return_value=90000.0) +def test_run(mock_get_asset_price): simulation_input = SimulationInput( asset="BTC", time_increment=300, diff --git a/tests/utils.py b/tests/utils.py index 08b2cbed..112a08bd 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta, timezone +from unittest.mock import patch from sqlalchemy import Engine, insert @@ -53,28 +54,34 @@ def prepare_random_predictions(db_engine: Engine, start_time: str): num_simulations=1, ) - simulation_data = { - miner_uids[0]: ( - generate_simulations(start_time=start_time), - response_validation_v2.CORRECT, - "1.2", - ), - miner_uids[1]: ( - generate_simulations(start_time=start_time), - response_validation_v2.CORRECT, - "3", - ), - miner_uids[2]: ( - generate_simulations(start_time=start_time), - response_validation_v2.CORRECT, - "15", - ), - miner_uids[3]: ( - generate_simulations(start_time=start_time), - "time out or internal server error (process time is None)", - "2.1", - ), - } + # generate_simulations() fetches a live current price via get_asset_price + # (Pyth Lazer when PYTH_BACKEND=pro). Tests must not depend on that + # external call, so pin the price and let the GBM math run on it. + with patch( + "synth.miner.simulations.get_asset_price", return_value=90000.0 + ): + simulation_data = { + miner_uids[0]: ( + generate_simulations(start_time=start_time), + response_validation_v2.CORRECT, + "1.2", + ), + miner_uids[1]: ( + generate_simulations(start_time=start_time), + response_validation_v2.CORRECT, + "3", + ), + miner_uids[2]: ( + generate_simulations(start_time=start_time), + response_validation_v2.CORRECT, + "15", + ), + miner_uids[3]: ( + generate_simulations(start_time=start_time), + "time out or internal server error (process time is None)", + "2.1", + ), + } handler.save_responses(simulation_data, simulation_input, datetime.now()) return handler, simulation_input, miner_uids