import pytest import respx from httpx import Response from traderai.tools import ToolRegistry from traderai.uex_client import UEXClient class FakeUEX: def __init__(self): self.posts = [] async def get(self, path, params=None, authenticated=False): if path == "commodities_prices_history": return { "status": "ok", "data": [ { "id": 1, "id_terminal": 7, "id_commodity": 3, "commodity_name": "Gold", "terminal_name": "Port Tressler", "price_buy": 4000, "price_sell": 5000, "scu_buy": 100, "scu_sell": 20, "date_added": 100, }, { "id": 2, "id_terminal": 7, "id_commodity": 3, "commodity_name": "Gold", "terminal_name": "Port Tressler", "price_buy": 4200, "price_sell": 4800, "scu_buy": 80, "scu_sell": 30, "date_added": 200, }, ], } if path == "marketplace_prices_history": return { "status": "ok", "data": [ {"id": 1, "item_name": "Widget", "operation": "sell", "price": 1000, "currency": "UEC", "date_added": 100}, {"id": 2, "item_name": "Widget", "operation": "sell", "price": 1250, "currency": "UEC", "date_added": 200}, ], } if path == "currencies_index_history": return { "status": "ok", "data": [ {"id": 1, "currency": "UEC", "index_value": 100.0, "basket_value": 5000.0, "date_added": 100}, {"id": 2, "currency": "UEC", "index_value": 110.0, "basket_value": 5500.0, "date_added": 200}, ], } if path == "commodities_prices": return { "status": "ok", "data": [ { "id": 10, "commodity_name": "Gold", "terminal_name": "Port Tressler", "price_buy": 4120, "price_sell": 5020, "scu_buy": 1200, "verbose_note": "x" * 300, }, { "id": 11, "commodity_name": "Beryl", "terminal_name": "Area18", "price_buy": 2500, "price_sell": 3100, }, ], } assert path == "marketplace_listings" return { "data": [ { "id": 1, "slug": "gold-haul", "title": "Gold haul escort", "description": "Escort service", "operation": "sell", "type": "service", "price": 5000, "currency": "UEC", "unit": "run", "location": "Port Tressler", "user_username": "pilot_a", "date_expiration": 123, }, { "id": 2, "slug": "armor-set", "title": "Armor set", "description": "Clean set", "operation": "sell", "type": "item", "price": 15000, "currency": "UEC", "unit": "set", "location": "Area18", "user_username": "pilot_b", "date_expiration": 456, }, ] } async def delete(self, path, params=None, authenticated=True): return {"status": "ok", "deleted": {"path": path, "params": params, "authenticated": authenticated}} async def post(self, path, payload, authenticated=True): self.posts.append({"path": path, "payload": payload, "authenticated": authenticated}) return {"status": "ok", "posted": self.posts[-1]} @pytest.mark.asyncio async def test_search_marketplace_listings_filters_locally(): registry = ToolRegistry(FakeUEX()) result = await registry.search_marketplace_listings(query="gold", type="service", max_price=6000) assert result["count"] == 1 assert result["listings"][0]["slug"] == "gold-haul" @pytest.mark.asyncio async def test_draft_message_creates_pending_action(): registry = ToolRegistry(FakeUEX()) result = await registry.draft_negotiation_message(hash="abc", message="Would you take 4500 UEC?") pending = result["pending_action"] assert pending["endpoint"] == "marketplace_negotiations_messages" assert pending["payload"]["message"] == "Would you take 4500 UEC?" assert pending["id"] in registry.pending_actions @pytest.mark.asyncio async def test_decline_pending_action_removes_without_sending(): registry = ToolRegistry(FakeUEX()) result = await registry.draft_negotiation_message(hash="abc", message="Would you take 4500 UEC?") action_id = result["pending_action"]["id"] declined = await registry.decline(action_id) assert declined["declined"] is True assert declined["pending_action"]["id"] == action_id assert action_id not in registry.pending_actions @pytest.mark.asyncio async def test_approve_negotiation_message_forces_production_send(): fake = FakeUEX() registry = ToolRegistry(fake) result = await registry.draft_negotiation_message(hash="abc", message="Ready to close", is_production=0) action_id = result["pending_action"]["id"] approved = await registry.approve(action_id) assert approved["posted"]["path"] == "marketplace_negotiations_messages" assert approved["posted"]["payload"]["is_production"] == 1 def test_uex_client_uses_bearer_and_secret_headers(): client = UEXClient("https://api.uexcorp.space/2.0", secret_key="secret", bearer_token="bearer") headers = client._headers(authenticated=True) assert headers["secret-key"] == "secret" assert headers["Authorization"] == "Bearer bearer" @pytest.mark.asyncio async def test_uex_get_projects_and_limits_results(): registry = ToolRegistry(FakeUEX()) result = await registry.execute( "get_uex_commodities_prices", { "commodity_name": "Gold", "ignored": "drop", "fields": ["id", "commodity_name", "price_buy"], "limit": 1, }, ) assert result["resource"] == "commodities_prices" assert result["params"] == {"commodity_name": "Gold"} assert result["returned"] == 1 assert result["truncated"] is True assert result["items"] == [{"id": 10, "commodity_name": "Gold", "price_buy": 4120}] @pytest.mark.asyncio async def test_uex_api_catalog_exposes_resources_without_live_call(): registry = ToolRegistry(FakeUEX()) result = await registry.uex_api_catalog(group="vehicles") resources = [item["resource"] for item in result["get"]["vehicles"]] assert "vehicles" in resources assert "vehicles_prices" in resources assert "wallet_add" in result["post"] @pytest.mark.asyncio async def test_draft_delete_approves_with_delete_method(): registry = ToolRegistry(FakeUEX()) result = await registry.execute("delete_uex_marketplace_listings", {"id": 123, "label": "Remove listing"}) action_id = result["pending_action"]["id"] approved = await registry.approve(action_id) assert result["pending_action"]["method"] == "DELETE" assert approved["deleted"] == { "path": "marketplace_listings", "params": {"id": 123}, "authenticated": True, } def test_schemas_expose_specific_uex_tools_instead_of_generic_api_tool(): registry = ToolRegistry(FakeUEX()) names = {schema["function"]["name"] for schema in registry.schemas} assert "get_uex_commodities_prices" in names assert "get_uex_vehicles" in names assert "draft_uex_marketplace_advertise" in names assert "delete_uex_marketplace_listings" in names assert "uex_get" not in names assert "uex_draft_post" not in names @pytest.mark.asyncio async def test_search_uex_api_index_finds_history_tools(): registry = ToolRegistry(FakeUEX()) result = await registry.execute("search_uex_api_index", {"query": "history", "history_only": True}) tools = {item["tool"] for item in result["get"]} assert "get_uex_commodities_prices_history" in tools assert "get_uex_marketplace_prices_history" in tools assert "get_uex_currencies_index_history" in tools @pytest.mark.asyncio async def test_summarize_commodity_price_history_returns_trend_metrics(): registry = ToolRegistry(FakeUEX()) result = await registry.execute( "summarize_uex_commodity_price_history", {"id_terminal": 7, "id_commodity": 3}, ) assert result["resource"] == "commodities_prices_history" assert result["count"] == 2 assert result["labels"] == {"commodity_name": "Gold", "terminal_name": "Port Tressler"} assert result["metrics"]["price_buy"]["change"] == 200 assert result["metrics"]["price_sell"]["pct_change"] == -4.0 @pytest.mark.asyncio async def test_summarize_marketplace_and_currency_history(): registry = ToolRegistry(FakeUEX()) market = await registry.execute("summarize_uex_marketplace_price_history", {"item_name": "Widget"}) currency = await registry.execute("summarize_uex_currency_index_history", {"currency": "UEC"}) assert market["metrics"]["price"]["pct_change"] == 25.0 assert currency["metrics"]["index_value"]["change"] == 10.0 @pytest.mark.asyncio @respx.mock async def test_uex_client_get_user_normalizes_user_payload(): respx.get("https://api.uexcorp.space/2.0/user/").mock( return_value=Response(200, json={"status": "ok", "data": [{"username": "pilot_hudson"}]}) ) client = UEXClient("https://api.uexcorp.space/2.0", bearer_token="bearer") result = await client.get_user(authenticated=True) assert result == {"status": "ok", "user": {"username": "pilot_hudson"}} @pytest.mark.asyncio @respx.mock async def test_uex_client_get_user_notifications_normalizes_payload(): respx.get("https://api.uexcorp.space/2.0/user_notifications/").mock( return_value=Response(200, json={"status": "ok", "data": {"id": 7, "message": "Reply waiting", "date_read": 0}}) ) client = UEXClient("https://api.uexcorp.space/2.0", bearer_token="bearer") result = await client.get_user_notifications() assert result == {"status": "ok", "notifications": [{"id": 7, "message": "Reply waiting", "date_read": 0}]}