Fix runtime logging and frontend app regressions
This commit is contained in:
@@ -2,11 +2,13 @@
|
||||
# pylint: disable=W0212
|
||||
import asyncio
|
||||
import time
|
||||
import logging
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
import pytest
|
||||
from backend.services.market import MarketService
|
||||
from backend.data.mock_price_manager import MockPriceManager
|
||||
from backend.data.polling_price_manager import PollingPriceManager
|
||||
from backend.llm.models import RetryChatModel
|
||||
|
||||
|
||||
class TestMockPriceManager:
|
||||
@@ -231,6 +233,59 @@ class TestPollingPriceManager:
|
||||
|
||||
assert len(manager.open_prices) == 0
|
||||
|
||||
def test_fetch_prices_suppresses_repeated_failures(self, caplog):
|
||||
manager = PollingPriceManager(provider="yfinance", poll_interval=10)
|
||||
manager.subscribe(["AAPL"])
|
||||
|
||||
with patch.object(manager, "_fetch_quote", side_effect=ValueError("empty quote")):
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
for _ in range(3):
|
||||
manager._fetch_prices()
|
||||
|
||||
assert manager._failure_counts["AAPL"] == 3
|
||||
warning_messages = [record.message for record in caplog.records if record.levelno >= logging.WARNING]
|
||||
assert any("Failed to fetch AAPL price: empty quote" in message for message in warning_messages)
|
||||
|
||||
def test_fetch_prices_logs_recovery_after_failure(self, caplog):
|
||||
manager = PollingPriceManager(provider="yfinance", poll_interval=10)
|
||||
manager.subscribe(["AAPL"])
|
||||
|
||||
with patch.object(
|
||||
manager,
|
||||
"_fetch_quote",
|
||||
side_effect=[
|
||||
ValueError("temporary outage"),
|
||||
{"c": 100.0, "o": 99.0, "h": 101.0, "l": 98.0, "pc": 99.5, "d": 0.5, "dp": 0.5, "t": 1},
|
||||
],
|
||||
):
|
||||
with caplog.at_level(logging.INFO):
|
||||
manager._fetch_prices()
|
||||
manager._fetch_prices()
|
||||
|
||||
assert "AAPL" not in manager._failure_counts
|
||||
assert any("recovered after 1 consecutive failures" in record.message for record in caplog.records)
|
||||
|
||||
|
||||
class TestRetryChatModel:
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_retry_recovers_from_disconnect(self):
|
||||
attempts = {"count": 0}
|
||||
|
||||
class FakeAsyncModel:
|
||||
model_name = "fake-async-model"
|
||||
|
||||
async def __call__(self, *args, **kwargs):
|
||||
attempts["count"] += 1
|
||||
if attempts["count"] < 2:
|
||||
raise RuntimeError("Server disconnected")
|
||||
return {"ok": True}
|
||||
|
||||
wrapped = RetryChatModel(FakeAsyncModel(), max_retries=2, initial_delay=0.01)
|
||||
result = await wrapped("hello")
|
||||
|
||||
assert result == {"ok": True}
|
||||
assert attempts["count"] == 2
|
||||
|
||||
|
||||
class TestMarketService:
|
||||
def test_init_mock_mode(self):
|
||||
@@ -255,9 +310,23 @@ class TestMarketService:
|
||||
assert service.mock_mode is False
|
||||
assert service.api_key == "test_key"
|
||||
|
||||
@patch("backend.services.market.get_data_source", return_value="yfinance")
|
||||
@patch("backend.services.market.get_data_sources", return_value=["yfinance", "local_csv"])
|
||||
@patch.object(PollingPriceManager, "start")
|
||||
def test_start_real_mode_with_yfinance(self, _mock_start, _mock_source):
|
||||
def test_start_real_mode_with_yfinance(self, _mock_start, _mock_sources):
|
||||
service = MarketService(
|
||||
tickers=["AAPL"],
|
||||
poll_interval=10,
|
||||
mock_mode=False,
|
||||
)
|
||||
|
||||
service._start_real_mode()
|
||||
|
||||
assert isinstance(service._price_manager, PollingPriceManager)
|
||||
assert service._price_manager.provider == "yfinance"
|
||||
|
||||
@patch("backend.services.market.get_data_sources", return_value=["financial_datasets", "yfinance", "local_csv"])
|
||||
@patch.object(PollingPriceManager, "start")
|
||||
def test_start_real_mode_uses_first_supported_live_provider(self, _mock_start, _mock_sources):
|
||||
service = MarketService(
|
||||
tickers=["AAPL"],
|
||||
poll_interval=10,
|
||||
@@ -287,9 +356,9 @@ class TestMarketService:
|
||||
|
||||
service.stop()
|
||||
|
||||
@patch("backend.services.market.get_data_source", return_value="finnhub")
|
||||
@patch("backend.services.market.get_data_sources", return_value=["finnhub", "yfinance"])
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_real_mode_without_api_key(self, _mock_source):
|
||||
async def test_start_real_mode_without_api_key(self, _mock_sources):
|
||||
service = MarketService(
|
||||
tickers=["AAPL"],
|
||||
mock_mode=False,
|
||||
|
||||
Reference in New Issue
Block a user