282 lines
10 KiB
Python
282 lines
10 KiB
Python
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_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}}
|
|
|
|
|
|
@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
|
|
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}]}
|