This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
import itertools
|
||||
|
||||
from traderai.agent import OllamaAgent, SYSTEM_PROMPT
|
||||
from traderai.memory import MemoryStore
|
||||
@@ -217,6 +218,38 @@ def test_ollama_options_include_num_ctx():
|
||||
assert agent._ollama_options() == {"num_ctx": 64000}
|
||||
|
||||
|
||||
def test_deepseek_tool_rounds_are_not_capped_at_ten():
|
||||
agent = OllamaAgent("https://api.deepseek.com", "deepseek-v4-flash", EmptyTools(), provider="deepseek", api_key="test")
|
||||
|
||||
rounds = list(itertools.islice(agent._tool_rounds(), 12))
|
||||
|
||||
assert len(rounds) == 12
|
||||
|
||||
|
||||
def test_plan_draft_normalization_extracts_json_and_defaults():
|
||||
seed = {"title": "Wikelo Polaris", "objective": "Find parts", "kind": "buying", "constraints": {}, "items": []}
|
||||
raw = 'draft:\n{"title":"Wikelo Polaris Parts","objective":"Find and draft deals for the parts below","kind":"buying","cadence":"0 */3 * * *","constraints":{"message_tone":"casual","instructions":"Prioritize cheap listings first."},"items":[{"item_name":"RCMBNT-RGL-1","desired_quantity":2}]}'
|
||||
|
||||
draft = OllamaAgent._normalize_plan_draft(raw, seed)
|
||||
|
||||
assert draft["title"] == "Wikelo Polaris Parts"
|
||||
assert draft["cadence"] == "0 */3 * * *"
|
||||
assert draft["constraints"]["message_tone"] == "casual"
|
||||
assert draft["items"][0]["item_name"] == "RCMBNT-RGL-1"
|
||||
assert draft["items"][0]["desired_quantity"] == 2
|
||||
|
||||
|
||||
def test_plan_draft_heuristic_fills_in_basic_instructions():
|
||||
seed = {"title": "Watch open negotiations", "objective": "", "kind": "custom", "constraints": {}, "items": []}
|
||||
|
||||
draft = OllamaAgent._heuristic_plan_draft(seed)
|
||||
|
||||
assert draft["kind"] == "custom"
|
||||
assert draft["cadence"] == "0 */4 * * *"
|
||||
assert "summarize" in draft["constraints"]["instructions"].casefold()
|
||||
assert draft["constraints"]["message_tone"] == "friendly and direct"
|
||||
|
||||
|
||||
def test_codex_prompt_mentions_tools_and_images(tmp_path):
|
||||
memory = MemoryStore(str(tmp_path / "memory.sqlite3"))
|
||||
agent = OllamaAgent("codex", "gpt-5.3-codex", EmptyTools(), memory=memory, provider="codex")
|
||||
@@ -257,6 +290,34 @@ def test_codex_prompt_mentions_tools_and_images(tmp_path):
|
||||
assert "tool search_marketplace_listings" in prompt
|
||||
|
||||
|
||||
def test_deepseek_openai_messages_include_reasoning_content_for_tool_turns():
|
||||
agent = OllamaAgent("https://api.deepseek.com", "deepseek-v4-flash", EmptyTools(), provider="deepseek", api_key="test")
|
||||
|
||||
messages = agent._openai_messages(
|
||||
"check listing",
|
||||
[
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{"role": "user", "content": "Check this listing"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"reasoning_content": "I should check the current listing first.",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_123",
|
||||
"type": "function",
|
||||
"function": {"name": "search_marketplace_listings", "arguments": "{\"query\":\"panel\"}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_name": "search_marketplace_listings", "tool_call_id": "call_123", "content": "{\"ok\":true}"},
|
||||
],
|
||||
)
|
||||
|
||||
assistant_turn = next(message for message in messages if message["role"] == "assistant")
|
||||
assert assistant_turn["reasoning_content"] == "I should check the current listing first."
|
||||
|
||||
|
||||
def test_codex_structured_response_extracts_text_and_tool_calls():
|
||||
agent = OllamaAgent("codex", "gpt-5.3-codex", EmptyTools(), provider="codex")
|
||||
|
||||
|
||||
+20
-2
@@ -1,10 +1,22 @@
|
||||
from traderai.config import Settings
|
||||
|
||||
|
||||
def test_model_provider_accepts_codex():
|
||||
def test_model_provider_codex_falls_back_to_ollama():
|
||||
settings = Settings(model_provider="codex")
|
||||
|
||||
assert settings.model_provider == "codex"
|
||||
assert settings.model_provider == "ollama"
|
||||
|
||||
|
||||
def test_model_provider_openai_falls_back_to_ollama():
|
||||
settings = Settings(model_provider="openai")
|
||||
|
||||
assert settings.model_provider == "ollama"
|
||||
|
||||
|
||||
def test_model_provider_accepts_deepseek():
|
||||
settings = Settings(model_provider="deepseek")
|
||||
|
||||
assert settings.model_provider == "deepseek"
|
||||
|
||||
|
||||
def test_model_provider_invalid_value_falls_back_to_ollama():
|
||||
@@ -23,3 +35,9 @@ def test_reasoning_effort_accepts_supported_values():
|
||||
settings = Settings(model_reasoning_effort="high")
|
||||
|
||||
assert settings.model_reasoning_effort == "high"
|
||||
|
||||
|
||||
def test_reasoning_effort_accepts_max():
|
||||
settings = Settings(model_reasoning_effort="max")
|
||||
|
||||
assert settings.model_reasoning_effort == "max"
|
||||
|
||||
@@ -55,3 +55,22 @@ def test_memory_store_renames_threads_and_deletes_outbox_items(tmp_path):
|
||||
assert renamed["title"] == "Market Check"
|
||||
assert deleted is True
|
||||
assert store.list_outbox() == []
|
||||
|
||||
|
||||
def test_memory_store_uses_absolute_path_across_working_directory_changes(tmp_path, monkeypatch):
|
||||
original_cwd = tmp_path / "start"
|
||||
original_cwd.mkdir()
|
||||
monkeypatch.chdir(original_cwd)
|
||||
|
||||
store = MemoryStore("data/memory.sqlite3")
|
||||
|
||||
moved_cwd = tmp_path / "moved"
|
||||
moved_cwd.mkdir()
|
||||
monkeypatch.chdir(moved_cwd)
|
||||
|
||||
store.add_outbox("Notification survived cwd change")
|
||||
|
||||
snapshot = store.inspect()
|
||||
|
||||
assert store.path.is_absolute()
|
||||
assert snapshot["outbox"][0]["content"] == "Notification survived cwd change"
|
||||
|
||||
@@ -142,6 +142,9 @@ async def test_buying_runner_tracks_candidates_and_drafts_only(tmp_path):
|
||||
assert len(tools.pending_actions) == 1
|
||||
assert not tools.uex.posts
|
||||
assert "Drafted 1 negotiation" in memory.list_outbox()[0]["content"]
|
||||
pending = next(iter(tools.pending_actions.values()))
|
||||
assert "Tone note" not in pending.payload["message"]
|
||||
assert "Polaris build" not in pending.payload["message"] or "putting together parts for a Polaris build" in pending.payload["message"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
+110
-5
@@ -86,6 +86,7 @@ def test_config_update_rebuilds_runtime_without_restart(monkeypatch, tmp_path):
|
||||
model_provider=values.get("model_provider", state["settings"].model_provider),
|
||||
ollama_model=values.get("ollama_model", state["settings"].ollama_model),
|
||||
codex_model=values.get("codex_model", state["settings"].codex_model),
|
||||
deepseek_model=values.get("deepseek_model", state["settings"].deepseek_model),
|
||||
)
|
||||
return {"values": values, "fields": {}, "secrets_configured": {}, "app_data_dir": str(tmp_path)}
|
||||
|
||||
@@ -114,19 +115,120 @@ def test_config_update_rebuilds_runtime_without_restart(monkeypatch, tmp_path):
|
||||
|
||||
updated = client.post(
|
||||
"/api/config",
|
||||
json={"values": {"model_provider": "codex", "codex_model": "gpt-5.4"}},
|
||||
json={"values": {"model_provider": "deepseek", "deepseek_model": "deepseek-v4-flash"}},
|
||||
).json()
|
||||
assert updated["restart_required"] is False
|
||||
|
||||
after = client.get("/api/health").json()
|
||||
assert after["model_provider"] == "codex"
|
||||
assert after["inference"]["provider"] == "codex"
|
||||
assert after["model_provider"] == "deepseek"
|
||||
assert after["inference"]["provider"] == "deepseek"
|
||||
|
||||
chat = client.post("/api/chat", json={"message": "hi", "thread_id": "thread-1", "images": []}).json()
|
||||
assert chat["message"] == "codex:gpt-5.4"
|
||||
assert chat["message"] == "deepseek:deepseek-v4-flash"
|
||||
|
||||
|
||||
def make_settings(tmp_path, model_provider="ollama", ollama_model="qwen3.5:9b", codex_model="gpt-5.4"):
|
||||
def test_plan_draft_endpoint_returns_agent_draft(monkeypatch, tmp_path):
|
||||
state = {"settings": make_settings(tmp_path)}
|
||||
|
||||
class FakeScheduler:
|
||||
def __init__(self, memory):
|
||||
self.memory = memory
|
||||
|
||||
def bind_agent(self, agent):
|
||||
self.agent = agent
|
||||
|
||||
def bind_plan_runner(self, plan_runner):
|
||||
self.plan_runner = plan_runner
|
||||
|
||||
def bind_uex_notifications(self, uex, poll_seconds=60):
|
||||
self.uex = uex
|
||||
self.poll_seconds = poll_seconds
|
||||
|
||||
def start(self):
|
||||
return None
|
||||
|
||||
def shutdown(self):
|
||||
return None
|
||||
|
||||
def list_jobs(self):
|
||||
return []
|
||||
|
||||
class FakeUEXClient:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
async def get_user(self, username=None, authenticated=False):
|
||||
return {}
|
||||
|
||||
class FakeToolRegistry:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.pending_actions = {}
|
||||
self.plan_runner = None
|
||||
|
||||
async def approve(self, action_id):
|
||||
return {"approved": action_id}
|
||||
|
||||
async def decline(self, action_id):
|
||||
return {"declined": action_id}
|
||||
|
||||
class FakePlanRunner:
|
||||
def __init__(self, store, tools, memory, agent=None):
|
||||
self.store = store
|
||||
self.tools = tools
|
||||
self.memory = memory
|
||||
self.agent = agent
|
||||
|
||||
def bind_agent(self, agent):
|
||||
self.agent = agent
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def fake_get_settings():
|
||||
return state["settings"]
|
||||
|
||||
monkeypatch.setattr(server, "WakeScheduler", FakeScheduler)
|
||||
monkeypatch.setattr(server, "UEXClient", FakeUEXClient)
|
||||
monkeypatch.setattr(server, "ToolRegistry", FakeToolRegistry)
|
||||
monkeypatch.setattr(server, "ContinualPlanRunner", FakePlanRunner)
|
||||
monkeypatch.setattr(server, "SCMDBClient", FakeClient)
|
||||
monkeypatch.setattr(server, "CornerstoneClient", FakeClient)
|
||||
monkeypatch.setattr(server, "StarCitizenWikiClient", FakeClient)
|
||||
monkeypatch.setattr(server, "get_settings", fake_get_settings)
|
||||
monkeypatch.setattr(
|
||||
server,
|
||||
"settings_payload",
|
||||
lambda settings=None: {"app_data_dir": str(tmp_path), "values": {}, "fields": {}, "secrets_configured": {}},
|
||||
)
|
||||
|
||||
async def fake_generate_plan_draft(self, title="", objective="", kind="buying", constraints=None, items=None):
|
||||
return {
|
||||
"title": title or "Draft title",
|
||||
"objective": objective or "Draft objective",
|
||||
"kind": kind,
|
||||
"cadence": "0 */3 * * *",
|
||||
"constraints": {"message_tone": "friendly and direct", "instructions": "Start with the best listings."},
|
||||
"items": [{"item_name": "RCMBNT-RGL-1", "desired_quantity": 1, "max_unit_price": None}],
|
||||
}
|
||||
|
||||
monkeypatch.setattr(server.OllamaAgent, "generate_plan_draft", fake_generate_plan_draft)
|
||||
|
||||
app = server.create_app()
|
||||
with TestClient(app) as client:
|
||||
response = client.post(
|
||||
"/api/plans/draft",
|
||||
json={"title": "Polaris parts", "objective": "Find the required parts", "kind": "buying", "constraints": {}, "items": []},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
draft = response.json()["draft"]
|
||||
assert draft["cadence"] == "0 */3 * * *"
|
||||
assert draft["constraints"]["instructions"] == "Start with the best listings."
|
||||
assert draft["items"][0]["item_name"] == "RCMBNT-RGL-1"
|
||||
|
||||
|
||||
def make_settings(tmp_path, model_provider="ollama", ollama_model="qwen3.5:9b", codex_model="gpt-5.4", deepseek_model="deepseek-v4-flash"):
|
||||
return SimpleNamespace(
|
||||
traderai_memory_path=str(tmp_path / "memory.sqlite3"),
|
||||
model_provider=model_provider,
|
||||
@@ -136,6 +238,9 @@ def make_settings(tmp_path, model_provider="ollama", ollama_model="qwen3.5:9b",
|
||||
openai_base_url="https://api.openai.com/v1",
|
||||
openai_api_key=None,
|
||||
openai_model="gpt-5.4-mini",
|
||||
deepseek_base_url="https://api.deepseek.com",
|
||||
deepseek_api_key=None,
|
||||
deepseek_model=deepseek_model,
|
||||
model_reasoning_effort="medium",
|
||||
codex_command="codex",
|
||||
codex_model=codex_model,
|
||||
|
||||
@@ -368,6 +368,37 @@ class FakeSCWiki:
|
||||
}
|
||||
|
||||
|
||||
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())
|
||||
@@ -686,6 +717,35 @@ async def test_get_scwiki_vehicle_returns_ship_prices_and_store_context():
|
||||
]
|
||||
|
||||
|
||||
@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())
|
||||
|
||||
Reference in New Issue
Block a user