feat: chat
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import pytest
|
||||
|
||||
from traderai.memory import MemoryStore
|
||||
from traderai.negotiations import NegotiationSyncService, extract_negotiation_hash
|
||||
from traderai.tools import ToolRegistry
|
||||
|
||||
|
||||
class FakeNegotiationUEX:
|
||||
def __init__(self):
|
||||
self.list_calls = []
|
||||
self.message_calls = []
|
||||
self.posts = []
|
||||
|
||||
async def list_negotiations(self, id=None, id_listing=None, hash=None):
|
||||
self.list_calls.append({"id": id, "id_listing": id_listing, "hash": hash})
|
||||
data = [
|
||||
{
|
||||
"id": 11,
|
||||
"hash": "open-hash",
|
||||
"id_listing": 101,
|
||||
"listing_slug": "rgl-open",
|
||||
"listing_title": "RGL Set",
|
||||
"advertiser_username": "seller_a",
|
||||
"client_username": "pilot_hudson",
|
||||
"date_modified": 1_780_975_053,
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"hash": "closed-recent",
|
||||
"id_listing": 102,
|
||||
"listing_slug": "rgl-closed",
|
||||
"listing_title": "Closed Deal",
|
||||
"advertiser_username": "seller_b",
|
||||
"client_username": "pilot_hudson",
|
||||
"date_modified": 1_780_975_053,
|
||||
"date_closed": 1_780_975_054,
|
||||
},
|
||||
]
|
||||
if hash:
|
||||
data = [item for item in data if item["hash"] == hash]
|
||||
return {"status": "ok", "negotiations": data}
|
||||
|
||||
async def get_negotiation_messages(self, hash=None, id_negotiation=None):
|
||||
self.message_calls.append({"hash": hash, "id_negotiation": id_negotiation})
|
||||
return {
|
||||
"status": "ok",
|
||||
"messages": [
|
||||
{
|
||||
"id": 201,
|
||||
"negotiation_hash": hash,
|
||||
"user_username": "seller_a" if hash == "open-hash" else "seller_b",
|
||||
"user_name": "Seller",
|
||||
"message": "Still available.",
|
||||
"date_added": 1_780_975_053,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
async def send_negotiation_message(self, **payload):
|
||||
self.posts.append({"kind": "message", **payload})
|
||||
return {"status": "ok", "posted": self.posts[-1]}
|
||||
|
||||
async def close_negotiation(self, **payload):
|
||||
self.posts.append({"kind": "close", **payload})
|
||||
return {"status": "ok", "posted": self.posts[-1]}
|
||||
|
||||
|
||||
def test_extract_negotiation_hash_handles_uex_redirects():
|
||||
assert extract_negotiation_hash("https://uexcorp.space/marketplace/negotiate/hash/abc-123") == "abc-123"
|
||||
assert extract_negotiation_hash("/marketplace/negotiate/hash/def-456") == "def-456"
|
||||
assert extract_negotiation_hash("/marketplace/item/info/foo") is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_startup_sync_keeps_open_and_recent_threads(tmp_path):
|
||||
memory = MemoryStore(str(tmp_path / "memory.sqlite3"))
|
||||
memory.set_profile("uex_user", {"username": "pilot_hudson"})
|
||||
service = NegotiationSyncService(memory, FakeNegotiationUEX())
|
||||
|
||||
result = await service.startup_sync()
|
||||
negotiations = memory.list_negotiations(limit=10)
|
||||
|
||||
assert result["count"] == 2
|
||||
assert {item["hash"] for item in negotiations} == {"open-hash", "closed-recent"}
|
||||
detail = memory.get_negotiation("open-hash")
|
||||
assert detail["messages"][0]["body"] == "Still available."
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notification_refresh_targets_only_changed_negotiation(tmp_path):
|
||||
memory = MemoryStore(str(tmp_path / "memory.sqlite3"))
|
||||
memory.set_profile("uex_user", {"username": "pilot_hudson"})
|
||||
fake = FakeNegotiationUEX()
|
||||
service = NegotiationSyncService(memory, fake)
|
||||
await service.startup_sync()
|
||||
fake.message_calls.clear()
|
||||
|
||||
await service.handle_notifications(
|
||||
[
|
||||
{
|
||||
"id": 99,
|
||||
"message": "seller_a: ping",
|
||||
"redir": "https://uexcorp.space/marketplace/negotiate/hash/open-hash",
|
||||
"date_added": 1_780_975_060,
|
||||
"date_read": 0,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
assert fake.message_calls == [{"hash": "open-hash", "id_negotiation": None}]
|
||||
assert memory.get_negotiation("open-hash")["unread_count"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_manual_send_refreshes_local_thread(tmp_path):
|
||||
memory = MemoryStore(str(tmp_path / "memory.sqlite3"))
|
||||
memory.set_profile("uex_user", {"username": "pilot_hudson"})
|
||||
fake = FakeNegotiationUEX()
|
||||
service = NegotiationSyncService(memory, fake)
|
||||
await service.startup_sync()
|
||||
|
||||
result = await service.manual_send_message("open-hash", "I can buy tonight.")
|
||||
|
||||
assert result["posted"]["kind"] == "message"
|
||||
assert fake.message_calls[-1]["hash"] == "open-hash"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_draft_negotiation_close_creates_pending_action(tmp_path):
|
||||
memory = MemoryStore(str(tmp_path / "memory.sqlite3"))
|
||||
registry = ToolRegistry(FakeNegotiationUEX(), memory=memory)
|
||||
|
||||
result = await registry.draft_negotiation_close(
|
||||
hash="open-hash",
|
||||
deal_closed=True,
|
||||
deal_value=1_000_000,
|
||||
currency="UEC",
|
||||
clarity_rating=5,
|
||||
speed_rating=5,
|
||||
respect_rating=5,
|
||||
fairness_rating=4,
|
||||
comment="Smooth trade",
|
||||
)
|
||||
|
||||
pending = result["pending_action"]
|
||||
assert pending["endpoint"] == "marketplace_negotiations_close"
|
||||
assert pending["payload"]["deal_closed"] == 1
|
||||
assert pending["payload"]["is_production"] == 1
|
||||
@@ -247,6 +247,7 @@ def make_settings(tmp_path, model_provider="ollama", ollama_model="qwen3.5:9b",
|
||||
uex_base_url="https://api.uexcorp.space/2.0",
|
||||
uex_secret_key=None,
|
||||
uex_bearer_token=None,
|
||||
uex_negotiation_close_endpoint="marketplace_negotiations_close",
|
||||
traderai_user_name=None,
|
||||
uex_notification_poll_seconds=60,
|
||||
require_write_approval=True,
|
||||
|
||||
@@ -452,6 +452,17 @@ def test_uex_client_uses_bearer_and_secret_headers():
|
||||
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())
|
||||
|
||||
Reference in New Issue
Block a user