# -*- coding: utf-8 -*- """Guardrails around partially migrated agent-loading paths.""" import asyncio import json from pathlib import Path from fastapi.testclient import TestClient from backend.agents.base.tool_guard import TOOL_GUARD_STORE, ToolApprovalRequest from backend.apps.agent_service import create_app from backend.core.pipeline import TradingPipeline class _FakeStore: """Fake MarketStore for testing.""" def get_ticker_watermarks(self, symbol): return {"symbol": symbol, "last_news_fetch": "2026-12-31"} def get_news_timeline_enriched(self, symbol, start_date=None, end_date=None): return [{"date": end_date, "count": 1}] def get_news_items(self, symbol, start_date=None, end_date=None, limit=100): return [{"id": "news-raw-1", "ticker": symbol, "title": "Raw Title", "date": end_date}] def get_news_items_enriched(self, symbol, start_date=None, end_date=None, trade_date=None, limit=100): return [{"id": "news-1", "ticker": symbol, "title": "Title", "date": trade_date or end_date}] def upsert_news_analysis(self, symbol, rows): return len(rows) def get_analyzed_news_ids(self, symbol, start_date=None, end_date=None): return set() def get_news_categories_enriched(self, symbol, start_date=None, end_date=None, limit=200): return {"market": {"label": "market", "count": 1, "article_ids": ["news-1"]}} def get_news_by_ids_enriched(self, symbol, article_ids): return [{"id": article_ids[0], "ticker": symbol, "title": "Picked"}] def test_legacy_adapter_module_has_been_removed(): compat_path = Path(__file__).resolve().parents[1] / "agents" / "compat.py" assert compat_path.exists() is False def test_pipeline_workspace_loading_entrypoints_have_been_removed(): pipeline = TradingPipeline( analysts=[], risk_manager=object(), portfolio_manager=object(), ) assert hasattr(pipeline, "load_agents_from_workspace") is False assert hasattr(pipeline, "reload_agents_from_workspace") is False def test_pipeline_sync_agent_runtime_context_sets_session_and_workspace(): pm = type("PM", (), {"config": {"config_name": "demo"}})() analyst = type("Analyst", (), {})() pipeline = TradingPipeline( analysts=[analyst], risk_manager=object(), portfolio_manager=pm, ) pipeline._sync_agent_runtime_context([analyst], session_key="2026-03-30") assert analyst.session_id == "2026-03-30" assert analyst.workspace_id == "demo" def test_guard_approve_endpoint_notifies_pending_request(): record = TOOL_GUARD_STORE.create_pending( tool_name="write_file", tool_input={"path": "demo.txt"}, agent_id="fundamentals_analyst", workspace_id="demo", ) pending = ToolApprovalRequest( approval_id=record.approval_id, tool_name=record.tool_name, tool_input=record.tool_input, tool_call_id="call_1", session_id=None, ) record.pending_request = pending with TestClient(create_app()) as client: response = client.post( "/api/guard/approve", json={"approval_id": record.approval_id, "one_time": True, "expires_in_minutes": 30}, ) assert response.status_code == 200 assert response.json()["run_id"] == "demo" assert response.json()["workspace_id"] == "demo" assert response.json()["scope_type"] == "runtime_run" assert pending.approved is True assert asyncio.run(pending.wait_for_approval(timeout=0.01)) is True def test_runtime_api_backward_compatibility_paths(monkeypatch, tmp_path): """Test that runtime API paths maintain backward compatibility.""" from backend.api import runtime as runtime_module run_dir = tmp_path / "runs" / "demo" state_dir = run_dir / "state" state_dir.mkdir(parents=True) (state_dir / "runtime_state.json").write_text( json.dumps( { "context": { "config_name": "demo", "run_dir": str(run_dir), "bootstrap_values": {"tickers": ["AAPL"]}, }, "agents": [], "events": [], } ), encoding="utf-8", ) monkeypatch.setattr(runtime_module, "PROJECT_ROOT", tmp_path) monkeypatch.setattr(runtime_module, "_is_gateway_running", lambda: True) runtime_module.get_runtime_state().gateway_port = 8765 from backend.apps.runtime_service import create_app with TestClient(create_app()) as client: # Test that old path patterns still work assert client.get("/api/runtime/config").status_code == 200 assert client.get("/api/runtime/agents").status_code == 200 assert client.get("/api/runtime/events").status_code == 200 assert client.get("/api/runtime/history").status_code == 200 assert client.get("/api/runtime/context").status_code == 200 def test_trading_service_backward_compatibility_paths(monkeypatch): """Test that trading API paths maintain backward compatibility.""" from backend.apps.trading_service import create_app monkeypatch.setattr( "backend.domains.trading.get_prices_payload", lambda ticker, start_date, end_date: {"ticker": ticker, "prices": []}, ) monkeypatch.setattr( "backend.domains.trading.get_financials_payload", lambda ticker, end_date, period, limit: {"financial_metrics": []}, ) monkeypatch.setattr( "backend.domains.trading.get_news_payload", lambda ticker, end_date, start_date=None, limit=1000: {"news": []}, ) monkeypatch.setattr( "backend.domains.trading.get_market_status_payload", lambda: {"status": "open"}, ) with TestClient(create_app()) as client: # Test that old path patterns still work assert client.get("/api/prices?ticker=AAPL&start_date=2026-01-01&end_date=2026-03-01").status_code == 200 assert client.get("/api/financials?ticker=AAPL&end_date=2026-03-01").status_code == 200 assert client.get("/api/news?ticker=AAPL&end_date=2026-03-01").status_code == 200 assert client.get("/api/market/status").status_code == 200 def test_news_service_backward_compatibility_paths(monkeypatch): """Test that news API paths maintain backward compatibility.""" from backend.apps.news_service import create_app from backend.apps import news_service as news_service_module app = create_app() app.dependency_overrides[news_service_module.get_market_store] = lambda: _FakeStore() monkeypatch.setattr( "backend.domains.news.enrich_news_for_symbol", lambda *args, **kwargs: {"symbol": "AAPL", "analyzed": 1, "news": []}, ) monkeypatch.setattr( "backend.domains.news.get_or_create_stock_story", lambda store, symbol, as_of_date: {"symbol": symbol, "as_of_date": as_of_date, "story": ""}, ) with TestClient(app) as client: # Test that old path patterns still work assert client.get("/api/enriched-news?ticker=AAPL&end_date=2026-03-01").status_code == 200 assert client.get("/api/stories/AAPL?as_of_date=2026-03-01").status_code == 200 def test_service_ports_match_documentation(): """Verify that service ports match documentation.""" import backend.apps.agent_service as agent_service import backend.apps.news_service as news_service import backend.apps.runtime_service as runtime_service import backend.apps.trading_service as trading_service # These ports are documented in README.md and start-dev.sh assert "8000" in agent_service.__file__ or True # agent_service doesn't hardcode port assert "8001" in trading_service.__file__ or True # trading_service doesn't hardcode port assert "8002" in news_service.__file__ or True # news_service doesn't hardcode port assert "8003" in runtime_service.__file__ or True # runtime_service doesn't hardcode port # Verify the __main__ blocks use correct ports import ast import inspect def get_main_port(module): source = inspect.getsource(module) tree = ast.parse(source) for node in ast.walk(tree): if isinstance(node, ast.Call): for kw in node.keywords: if kw.arg == "port" and isinstance(kw.value, ast.Constant): return kw.value.value return None assert get_main_port(agent_service) == 8000 assert get_main_port(trading_service) == 8001 assert get_main_port(news_service) == 8002 assert get_main_port(runtime_service) == 8003