"""Graceful degradation: fake ticker, dead network, empty RSS. All three failures are induced for real — an unknown symbol really is requested from the provider, the network really is unreachable (an unroutable proxy), and the empty feed really comes back empty. Nothing here is mocked, because the behaviour under test is precisely what happens when the outside world misbehaves. In every case the requirement is identical: per-item errors are recorded, the run continues, and the brief is explicitly marked partial. Never a crash. """ from __future__ import annotations from typing import Any import pytest from app.core.events import EventBus from app.core.settings import Settings from app.graph.compose import compose_brief from app.graph.verify import verify_brief from app.mcp_server.providers import ( ProviderContext, call_provider, fetch_fundamentals, fetch_price_history, fetch_rss_news, normalise_ticker, ) from app.mcp_server.registry import compute_metrics_from_bars from tests.conftest import connected_context #: A symbol no exchange lists. Verified against the live provider. UNKNOWN_TICKER = "ZZZZQQQQ" #: Loopback port with nothing listening: a real, immediate connection failure. DEAD_PROXY = "http://127.0.0.1:9" class TestFakeTicker: async def test_price_history_degrades_with_an_error(self, settings: Settings) -> None: async with connected_context(settings, EventBus()) as ctx: history = await ctx.mcp.get_price_history(UNKNOWN_TICKER, 60) assert not history.ok assert history.error is not None assert history.bars == [] async def test_fundamentals_degrade_with_an_error(self, settings: Settings) -> None: async with connected_context(settings, EventBus()) as ctx: fundamentals = await ctx.mcp.get_fundamentals(UNKNOWN_TICKER) assert not fundamentals.ok assert fundamentals.error is not None async def test_metrics_degrade_rather_than_raise(self, settings: Settings) -> None: async with connected_context(settings, EventBus()) as ctx: history = await ctx.mcp.get_price_history(UNKNOWN_TICKER, 60) metrics = await ctx.mcp.compute_metrics(UNKNOWN_TICKER, list(history.bars), None) assert not metrics.ok assert "insufficient price history" in (metrics.error or "") def test_malformed_symbols_are_rejected_before_any_request(self) -> None: for bad in ("", " ", "A" * 40, "AAPL; DROP TABLE runs", "../../etc/passwd", "