import pytest import respx from httpx import Response from traderai.tools import ToolRegistry from traderai.uex_client import UEXClient class FakeUEX: async def get(self, path, params=None, authenticated=False): 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}} @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 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 @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}]}