909 lines
34 KiB
Python
909 lines
34 KiB
Python
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 = []
|
|
self.get_calls = []
|
|
|
|
async def get(self, path, params=None, authenticated=False):
|
|
self.get_calls.append({"path": path, "params": params, "authenticated": authenticated})
|
|
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,
|
|
},
|
|
],
|
|
}
|
|
if path == "marketplace_trends":
|
|
return {
|
|
"status": "ok",
|
|
"data": [
|
|
{
|
|
"id_item": 2791,
|
|
"item_name": "\"Quantanium\" Water Bottle",
|
|
"item_slug": "quantanium-water-bottle",
|
|
"currency": "UEC",
|
|
"price_avg_sell": "937500",
|
|
"price_avg_month_sell": "1072222",
|
|
"price_min_sell": "750000",
|
|
"price_max_sell": "1200000",
|
|
"listings_count_sell": 4,
|
|
"price_avg_buy": "500000",
|
|
"price_avg_month_buy": "525000",
|
|
"price_min_buy": "450000",
|
|
"price_max_buy": "550000",
|
|
"listings_count_buy": 2,
|
|
"total_listings_count": 6,
|
|
"negotiations_count": 18,
|
|
"negotiations_open": 7,
|
|
"negotiations_success": 9,
|
|
"link_prices": "https://uexcorp.space/marketplace/home/?id_item=2791&mode=list",
|
|
"link_prices_history": "https://uexcorp.space/marketplace/averages/?id_item=2791&quality_tier=q0&unit=unit",
|
|
}
|
|
],
|
|
}
|
|
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": """
|
|
<html>
|
|
<head>
|
|
<title>Star Citizen - Salvage modifier - Abrade Scraper Module</title>
|
|
<meta property="og:image" content="/images/abrade.png">
|
|
</head>
|
|
<body>
|
|
<table>
|
|
<tr><td>NAME</td><td>Abrade Scraper Module</td></tr>
|
|
<tr><td>MANUFACTURER</td><td>Greycat Industrial</td></tr>
|
|
</table>
|
|
<table>
|
|
<tr><th>LOCATION</th><th>BASE PRICE</th><th>VERIFIED</th></tr>
|
|
<tr><td>Stanton - ArcCorp - Area18 - Dumper's Depot</td><td>21 250</td><td>2956-01-29</td></tr>
|
|
<tr><td>Stanton - microTech - Port Tressler - Platinum Bay</td><td>21 250</td><td>2956-01-04</td></tr>
|
|
</table>
|
|
</body>
|
|
</html>
|
|
""",
|
|
}
|
|
|
|
async def get_image_data(self, url, max_bytes=10_000_000):
|
|
assert url == f"{self.base_url}/images/abrade.png"
|
|
return {
|
|
"url": url,
|
|
"content_type": "image/png",
|
|
"size_bytes": 12,
|
|
"image_data": "ZmFrZS1pbWFnZQ==",
|
|
}
|
|
|
|
|
|
class FakeSCWiki:
|
|
base_url = "https://starcitizen.tools"
|
|
api_base_url = "https://api.star-citizen.wiki"
|
|
|
|
async def search_pages(self, query, limit=5):
|
|
assert query == "Carrack"
|
|
return [
|
|
{
|
|
"pageid": 415,
|
|
"title": "Carrack",
|
|
"description": "Deep-space multi-crew explorer manufactured by Anvil Aerospace",
|
|
"extract": "The Anvil Carrack is a multi-crew explorer.",
|
|
"thumbnail": "https://media.starcitizen.tools/carrack.webp",
|
|
"url": "https://starcitizen.tools/Carrack",
|
|
}
|
|
][:limit]
|
|
|
|
async def get_page_summary(self, title=None, pageid=None, chars=700):
|
|
assert title == "Carrack" or pageid == 415
|
|
return {
|
|
"pageid": 415,
|
|
"title": "Carrack",
|
|
"description": "Deep-space multi-crew explorer manufactured by Anvil Aerospace",
|
|
"extract": "The Anvil Carrack is a multi-crew explorer.",
|
|
"thumbnail": "https://media.starcitizen.tools/carrack.webp",
|
|
"url": "https://starcitizen.tools/Carrack",
|
|
}
|
|
|
|
async def search_verse(self, query):
|
|
assert query == "Carrack"
|
|
return [
|
|
{
|
|
"type": "vehicles",
|
|
"label": "Vehicles",
|
|
"results": [
|
|
{
|
|
"name": "Anvil Carrack",
|
|
"class_name": "ANVL_Carrack",
|
|
"extra_label": "Exploration",
|
|
"web_url": "https://api.star-citizen.wiki/vehicles/anvl-carrack",
|
|
"api_url": "https://api.star-citizen.wiki/api/vehicles/anvl-carrack",
|
|
}
|
|
],
|
|
}
|
|
]
|
|
|
|
async def get_vehicle(self, slug):
|
|
assert slug == "anvl-carrack"
|
|
return {
|
|
"name": "Carrack",
|
|
"game_name": "Anvil Carrack",
|
|
"slug": "anvl-carrack",
|
|
"manufacturer": {"name": "Anvil Aerospace"},
|
|
"career": "Exploration",
|
|
"role": "Expedition",
|
|
"size_class": 5,
|
|
"cargo_capacity": 456,
|
|
"crew": {"min": 6, "max": 6},
|
|
"msrp": 600,
|
|
"pledge_url": "https://robertsspaceindustries.com/pledge/ships/carrack/Carrack",
|
|
"uex_prices": {
|
|
"purchase": [
|
|
{
|
|
"price_buy": 34398000,
|
|
"terminal_name": "Astro Armada - Area 18",
|
|
"starmap_location": {"name": "Area18", "parent_name": "ArcCorp", "star_system_name": "Stanton"},
|
|
"game_version": "4.8.1-LIVE.11952564",
|
|
"date_updated": "2026-05-20T18:39:37-04:00",
|
|
"uex_link": "https://uexcorp.space/vehicles/home/list/in_game_sell/?id_terminal=148",
|
|
}
|
|
]
|
|
},
|
|
"description": {"en_EN": "The Anvil Carrack features reinforced fuel tanks for long-duration flight."},
|
|
"web_url": "https://api.star-citizen.wiki/vehicles/anvl-carrack",
|
|
"updated_at": "2026-06-08T00:34:00Z",
|
|
"version": "4.8.1-LIVE.11952564",
|
|
}
|
|
|
|
|
|
class FakeWikelo:
|
|
base_url = "https://wikelo-projects.test"
|
|
|
|
async def list_ship_projects(self):
|
|
return [
|
|
{
|
|
"id": "ship-1",
|
|
"ship_name": "Polaris Wikelo Special",
|
|
"description": "Now make Polaris. Short Time Deal",
|
|
"status": "planning",
|
|
"privacy": "public",
|
|
"owner_name": "Chimpanz33",
|
|
"required_materials": [
|
|
{"material_name": "Wikelo Favor", "quantity_needed": 50.0, "quantity_collected": 0.0},
|
|
{"material_name": "Polaris Bit", "quantity_needed": 15.0, "quantity_collected": 2.0},
|
|
],
|
|
},
|
|
{
|
|
"id": "ship-2",
|
|
"ship_name": "Guardian",
|
|
"description": "Guardian Fight Mod",
|
|
"status": "planning",
|
|
"privacy": "public",
|
|
"owner_name": "Chimpanz33",
|
|
"required_materials": [
|
|
{"material_name": "Wikelo Favor", "quantity_needed": 20.0, "quantity_collected": 0.0},
|
|
],
|
|
},
|
|
]
|
|
|
|
|
|
@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"
|
|
|
|
|
|
def test_uex_client_uses_configured_close_endpoint():
|
|
client = UEXClient(
|
|
"https://api.uexcorp.space/2.0",
|
|
secret_key="secret",
|
|
bearer_token="bearer",
|
|
negotiation_close_endpoint="custom_close_endpoint",
|
|
)
|
|
|
|
assert client.negotiation_close_endpoint == "custom_close_endpoint"
|
|
|
|
|
|
@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_get_marketplace_listings_accepts_item_and_operation_filters():
|
|
fake = FakeUEX()
|
|
registry = ToolRegistry(fake)
|
|
|
|
result = await registry.execute(
|
|
"get_uex_marketplace_listings",
|
|
{
|
|
"id_item": 2791,
|
|
"operation": "sell",
|
|
"fields": ["id", "slug", "operation"],
|
|
},
|
|
)
|
|
|
|
assert result["params"] == {"id_item": 2791, "operation": "sell"}
|
|
assert fake.get_calls[-1]["path"] == "marketplace_listings"
|
|
assert fake.get_calls[-1]["params"] == {"id_item": 2791, "operation": "sell"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_marketplace_trends_returns_compact_wts_wtb_and_negotiation_metrics():
|
|
fake = FakeUEX()
|
|
registry = ToolRegistry(fake)
|
|
|
|
result = await registry.get_marketplace_trends(item_name="Quantanium", currency="UEC", quality_tier=0)
|
|
|
|
assert result["status"] == "ok"
|
|
assert result["count"] == 1
|
|
assert result["filters"] == {"item_name": "Quantanium", "currency": "UEC", "quality_tier": 0}
|
|
assert fake.get_calls[-1]["path"] == "marketplace_trends"
|
|
assert fake.get_calls[-1]["params"] == {"id_item": None, "item_name": "Quantanium", "item_slug": None, "id_category": None, "currency": "UEC", "quality_tier": 0}
|
|
assert result["trends"][0] == {
|
|
"id_item": 2791,
|
|
"item_name": "\"Quantanium\" Water Bottle",
|
|
"item_slug": "quantanium-water-bottle",
|
|
"currency": "UEC",
|
|
"sell": {
|
|
"avg_price": "937500",
|
|
"avg_price_month": "1072222",
|
|
"min_price": "750000",
|
|
"max_price": "1200000",
|
|
"listings_count": 4,
|
|
},
|
|
"buy": {
|
|
"avg_price": "500000",
|
|
"avg_price_month": "525000",
|
|
"min_price": "450000",
|
|
"max_price": "550000",
|
|
"listings_count": 2,
|
|
},
|
|
"total_listings_count": 6,
|
|
"negotiations_count": 18,
|
|
"negotiations_open": 7,
|
|
"negotiations_success": 9,
|
|
"link_prices": "https://uexcorp.space/marketplace/home/?id_item=2791&mode=list",
|
|
"link_prices_history": "https://uexcorp.space/marketplace/averages/?id_item=2791&quality_tier=q0&unit=unit",
|
|
}
|
|
|
|
|
|
@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 "get_marketplace_trends" 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
|
|
assert "get_cornerstone_item_media" in names
|
|
assert "draft_marketplace_listing_with_cornerstone_image" in names
|
|
|
|
|
|
def test_schemas_expose_scwiki_tools():
|
|
registry = ToolRegistry(FakeUEX(), scwiki=FakeSCWiki())
|
|
|
|
names = {schema["function"]["name"] for schema in registry.schemas}
|
|
|
|
assert "search_scwiki_pages" in names
|
|
assert "get_scwiki_page" in names
|
|
assert "search_scwiki_vehicles" in names
|
|
assert "get_scwiki_vehicle" 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",
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_cornerstone_item_media_returns_absolute_image_urls():
|
|
registry = ToolRegistry(FakeUEX(), cornerstone=FakeCornerstone())
|
|
|
|
result = await registry.get_cornerstone_item_media(query="abrade")
|
|
|
|
assert result["media"] == [
|
|
{
|
|
"url": "https://finder.cstone.test/images/abrade.png",
|
|
"source": "og:image",
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_scwiki_pages_returns_general_knowledge_matches():
|
|
registry = ToolRegistry(FakeUEX(), scwiki=FakeSCWiki())
|
|
|
|
result = await registry.search_scwiki_pages(query="Carrack")
|
|
|
|
assert result["source"] == "https://starcitizen.tools"
|
|
assert result["matched"] == 1
|
|
assert result["pages"][0]["title"] == "Carrack"
|
|
assert result["pages"][0]["url"] == "https://starcitizen.tools/Carrack"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_scwiki_vehicle_returns_ship_prices_and_store_context():
|
|
registry = ToolRegistry(FakeUEX(), scwiki=FakeSCWiki())
|
|
|
|
result = await registry.get_scwiki_vehicle(query="Carrack")
|
|
|
|
assert result["source"] == "https://api.star-citizen.wiki"
|
|
vehicle = result["vehicle"]
|
|
assert vehicle["name"] == "Carrack"
|
|
assert vehicle["manufacturer"] == "Anvil Aerospace"
|
|
assert vehicle["msrp"] == 600
|
|
assert vehicle["purchase_locations"] == [
|
|
{
|
|
"price_buy": 34398000,
|
|
"terminal_name": "Astro Armada - Area 18",
|
|
"location": "Area18",
|
|
"parent_location": "ArcCorp",
|
|
"star_system": "Stanton",
|
|
"game_version": "4.8.1-LIVE.11952564",
|
|
"date_updated": "2026-05-20T18:39:37-04:00",
|
|
"uex_link": "https://uexcorp.space/vehicles/home/list/in_game_sell/?id_terminal=148",
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_search_wikelo_ship_projects_returns_material_matches():
|
|
registry = ToolRegistry(FakeUEX(), wikelo=FakeWikelo())
|
|
|
|
result = await registry.search_wikelo_ship_projects(query="Polaris")
|
|
|
|
assert result["source"] == "https://wikelo-projects.test/Ships"
|
|
assert result["matched"] == 1
|
|
assert result["projects"][0]["ship_name"] == "Polaris Wikelo Special"
|
|
assert result["projects"][0]["required_materials"][0]["material_name"] == "Wikelo Favor"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_wikelo_ship_project_returns_full_requirements():
|
|
registry = ToolRegistry(FakeUEX(), wikelo=FakeWikelo())
|
|
|
|
result = await registry.get_wikelo_ship_project(ship_name="Guardian")
|
|
|
|
assert result["project"]["ship_name"] == "Guardian"
|
|
assert result["project"]["materials_count"] == 1
|
|
assert result["project"]["required_materials"] == [
|
|
{
|
|
"material_name": "Wikelo Favor",
|
|
"quantity_needed": 20,
|
|
"quantity_collected": 0,
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_draft_marketplace_listing_with_cornerstone_image_adds_image_data_and_redacts_display():
|
|
registry = ToolRegistry(FakeUEX(), cornerstone=FakeCornerstone())
|
|
|
|
result = await registry.draft_marketplace_listing_with_cornerstone_image(
|
|
item_query="abrade",
|
|
id_category=3,
|
|
operation="sell",
|
|
type="item",
|
|
unit="unit",
|
|
title="Abrade Scraper Module",
|
|
description="Clean module, ready for pickup.",
|
|
price=21250,
|
|
currency="UEC",
|
|
language="en_US",
|
|
source="purchased_in_game",
|
|
in_stock=1,
|
|
)
|
|
|
|
pending = result["pending_action"]
|
|
stored = registry.pending_actions[pending["id"]]
|
|
|
|
assert pending["endpoint"] == "marketplace_advertise"
|
|
assert pending["payload"]["image_data"].startswith("<base64 image data redacted")
|
|
assert stored.payload["image_data"] == "ZmFrZS1pbWFnZQ=="
|
|
assert pending["metadata"]["cornerstone_image_status"] == "included"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_draft_marketplace_listing_can_reuse_pasted_chat_image():
|
|
registry = ToolRegistry(FakeUEX())
|
|
|
|
with registry.chat_image_scope([{"name": "listing.png", "content_type": "image/png", "image_data": "ZmFrZS1pbWFnZQ=="}]):
|
|
result = await registry.draft_marketplace_listing(
|
|
id_category=3,
|
|
operation="sell",
|
|
type="item",
|
|
unit="unit",
|
|
title="Abrade Scraper Module",
|
|
description="Clean module, ready for pickup.",
|
|
price=21250,
|
|
currency="UEC",
|
|
language="en_US",
|
|
use_attached_image=True,
|
|
)
|
|
|
|
pending = result["pending_action"]
|
|
stored = registry.pending_actions[pending["id"]]
|
|
assert pending["payload"]["image_data"].startswith("<base64 image data redacted")
|
|
assert stored.payload["image_data"] == "ZmFrZS1pbWFnZQ=="
|
|
assert pending["metadata"]["attached_chat_image_name"] == "listing.png"
|
|
assert pending["metadata"]["attached_chat_image_status"] == "included"
|
|
|
|
|
|
def test_parse_cornerstone_item_page_extracts_locations():
|
|
parsed = parse_cornerstone_item_page(
|
|
"""
|
|
<html><head><title>Star Citizen - Food - Whamburger</title><meta property="og:image" content="/img/wham.png"></head>
|
|
<body><table><tr><td>NAME</td><td>Whamburger</td></tr></table>
|
|
<img src="https://example.test/extra.png" alt="Whamburger">
|
|
<table><tr><th>LOCATION</th><th>BASE PRICE</th><th>VERIFIED</th></tr>
|
|
<tr><td>Stanton - Area18 - Cubby Blast</td><td>9</td><td>2956-01-01</td></tr></table></body></html>
|
|
""",
|
|
"https://finder.cstone.test/Search/item-wham",
|
|
)
|
|
|
|
assert parsed["name"] == "Whamburger"
|
|
assert parsed["locations"][0]["base_price"] == 9
|
|
assert parsed["media"][0]["url"] == "https://finder.cstone.test/img/wham.png"
|
|
assert parsed["media"][1]["url"] == "https://example.test/extra.png"
|
|
|
|
|
|
@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}]}
|