Files
TraderAI/tests/test_tools.py
T

302 lines
11 KiB
Python

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}]}