import pytest import respx from httpx import Response from traderai.cornerstone_client import CornerstoneClient, parse_cornerstone_item_page 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]} class FakeSCMDB: base_url = "https://scmdb.test" async def list_versions(self): return [ {"version": "4.8.0-ptu.1", "file": "merged-4.8.0-ptu.1.json"}, {"version": "4.7.2-live.1", "file": "merged-4.7.2-live.1.json"}, ] async def get_data(self, version=None, channel="live"): return { "version": version or "4.7.2-live.1", "factions": { "fac-haul": {"name": "Covalex"}, "fac-bounty": {"name": "Bounty Hunters Guild"}, }, "scopes": { "scope-rep": {"scopeName": "FactionReputation"}, }, "factionRewardsPools": [ [{"factionGuid": "fac-haul", "scopeGuid": "scope-rep", "amount": 125}], [{"factionGuid": "fac-bounty", "scopeGuid": "scope-rep", "amount": 250}], ], "partialRewardPayoutPools": [ [], [{"minPercentage": 50, "maxPercentage": 99, "currencyRewardMultiplier": 0.75, "reputationMultipliers": None}], ], "resourcePools": { "res-tungsten": {"name": "Tungsten"}, }, "blueprintPools": { "bp-pool": { "name": "Ship Salvage Rewards", "blueprints": [{"name": "Abrade Scraper Module"}], }, }, "locationPools": { "loc-a18": {"name": "Area18"}, "loc-baijini": {"name": "Baijini Point"}, }, "contracts": [ { "id": "mission-haul", "debugName": "Haul_Tungsten_Test", "title": "Move Tungsten", "description": "Move Tungsten to Baijini Point.", "missionType": "Hauling", "category": "career", "factionGuid": "fac-haul", "rewardUEC": 50250, "factionRewardsIndex": 0, "partialRewardPayoutIndex": 1, "haulingOrders": [{"resource": "res-tungsten", "minSCU": 6, "maxSCU": 6, "maxContainerSize": 1}], "locations": ["loc-a18"], "destinations": ["loc-baijini"], "systems": ["Stanton"], "illegal": False, "canBeShared": False, }, { "id": "mission-bounty", "debugName": "Bounty_Blueprint_Test", "title": "Ambush Op", "description": "Clean out targets.", "missionType": "Bounty Hunter", "factionGuid": "fac-bounty", "rewardUEC": 120000, "factionRewardsIndex": 1, "partialRewardPayoutIndex": 0, "itemRewards": [{"name": "Council Scrip", "amount": 5}], "blueprintRewards": [{"blueprintPool": "bp-pool", "chance": 1, "trigger": "complete"}], "systems": ["Pyro"], "illegal": True, "canBeShared": True, }, ], "legacyContracts": [ { "id": "legacy-delivery", "debugName": "Legacy_Delivery_Test", "title": "Old Box Run", "missionType": "Delivery", "factionGuid": "fac-haul", "rewardUEC": 1000, "factionRewardsIndex": 0, "partialRewardPayoutIndex": 0, "systems": ["Stanton"], } ], } class FakeCornerstone: base_url = "https://finder.cstone.test" async def list_items(self): return [ {"id": "item-abrade", "name": "Abrade Scraper Module", "sold": True}, {"id": "item-cinch", "name": "Cinch Scraper Module", "sold": True}, {"id": "item-poster", "name": "Zeus 2955 Ship Showdown Poster", "sold": False}, ] async def get_item_page(self, item_id): assert item_id == "item-abrade" return { "url": f"{self.base_url}/ShipSalvageMods1/{item_id}", "html": """ Star Citizen - Salvage modifier - Abrade Scraper Module
NAMEAbrade Scraper Module
MANUFACTURERGreycat Industrial
LOCATIONBASE PRICEVERIFIED
Stanton - ArcCorp - Area18 - Dumper's Depot21 2502956-01-29
Stanton - microTech - Port Tressler - Platinum Bay21 2502956-01-04
""", } @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 def test_schemas_expose_scmdb_mission_tools(): registry = ToolRegistry(FakeUEX(), scmdb=FakeSCMDB()) names = {schema["function"]["name"] for schema in registry.schemas} assert "list_scmdb_versions" in names assert "search_scmdb_missions" in names assert "get_scmdb_mission_rewards" in names def test_schemas_expose_cornerstone_item_tools(): registry = ToolRegistry(FakeUEX(), cornerstone=FakeCornerstone()) names = {schema["function"]["name"] for schema in registry.schemas} assert "search_cornerstone_items" in names assert "get_cornerstone_item_locations" in names @pytest.mark.asyncio async def test_search_scmdb_missions_returns_reward_summary(): registry = ToolRegistry(FakeUEX(), scmdb=FakeSCMDB()) result = await registry.search_scmdb_missions(query="tungsten", mission_type="hauling") assert result["version"] == "4.7.2-live.1" assert result["matched"] == 1 mission = result["missions"][0] assert mission["title"] == "Move Tungsten" assert mission["rewards"]["uec"] == 50250 assert mission["rewards"]["reputation"] == [{"faction": "Covalex", "scope": "FactionReputation", "amount": 125}] assert mission["rewards"]["hauling"] == [ {"resource": "Tungsten", "min_scu": 6, "max_scu": 6, "max_container_size_scu": 1} ] @pytest.mark.asyncio async def test_get_scmdb_mission_rewards_enriches_items_blueprints_and_locations(): registry = ToolRegistry(FakeUEX(), scmdb=FakeSCMDB()) result = await registry.get_scmdb_mission_rewards(debug_name="Bounty_Blueprint_Test") mission = result["mission"] assert mission["title"] == "Ambush Op" assert mission["faction"] == "Bounty Hunters Guild" assert mission["rewards"]["items"] == [{"name": "Council Scrip", "amount": 5}] assert mission["rewards"]["blueprints"][0]["blueprints"] == ["Abrade Scraper Module"] @pytest.mark.asyncio async def test_search_cornerstone_items_filters_sold_items(): registry = ToolRegistry(FakeUEX(), cornerstone=FakeCornerstone()) result = await registry.search_cornerstone_items(query="scraper", sold_only=True) assert result["matched"] == 2 assert {item["name"] for item in result["items"]} == {"Abrade Scraper Module", "Cinch Scraper Module"} assert result["items"][0]["url"].startswith("https://finder.cstone.test/Search/item-") @pytest.mark.asyncio async def test_get_cornerstone_item_locations_parses_store_prices(): registry = ToolRegistry(FakeUEX(), cornerstone=FakeCornerstone()) result = await registry.get_cornerstone_item_locations(query="abrade", location="Area18") assert result["item"]["name"] == "Abrade Scraper Module" assert result["item"]["general"]["manufacturer"] == "Greycat Industrial" assert result["matched_locations"] == 1 assert result["locations"] == [ { "location": "Stanton - ArcCorp - Area18 - Dumper's Depot", "base_price": 21250, "base_price_display": "21 250", "verified": "2956-01-29", } ] def test_parse_cornerstone_item_page_extracts_locations(): parsed = parse_cornerstone_item_page( """ Star Citizen - Food - Whamburger
NAMEWhamburger
LOCATIONBASE PRICEVERIFIED
Stanton - Area18 - Cubby Blast92956-01-01
""" ) assert parsed["name"] == "Whamburger" assert parsed["locations"][0]["base_price"] == 9 @pytest.mark.asyncio @respx.mock async def test_cornerstone_client_accepts_json_encoded_string_payload(): respx.get("https://finder.cstone.space/GetSearch").mock( return_value=Response( 200, json='[{"id":"item-1","name":"Abrade Scraper Module","Sold":1}]', ) ) client = CornerstoneClient("https://finder.cstone.space") assert await client.list_items() == [{"id": "item-1", "name": "Abrade Scraper Module", "sold": True}] @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}]}