from __future__ import annotations from types import SimpleNamespace from fastapi.testclient import TestClient import traderai.server as server def test_config_update_rebuilds_runtime_without_restart(monkeypatch, tmp_path): state = {"settings": make_settings(tmp_path, model_provider="ollama", ollama_model="qwen3.5:9b")} 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 async def fake_health(self): return { "online": True, "provider": self.provider, "model": self.model, "model_available": True, "message": f"{self.provider} ready", } async def fake_chat(self, content, thread_id=None, images=None): return {"message": f"{self.provider}:{self.model}", "pending_actions": [], "thread_id": thread_id} def fake_get_settings(): return state["settings"] def fake_save_settings(values): state["settings"] = make_settings( 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), ) return {"values": values, "fields": {}, "secrets_configured": {}, "app_data_dir": str(tmp_path)} 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, "save_settings", fake_save_settings) monkeypatch.setattr( server, "settings_payload", lambda settings=None: {"app_data_dir": str(tmp_path), "values": {}, "fields": {}, "secrets_configured": {}}, ) monkeypatch.setattr(server.OllamaAgent, "health", fake_health) monkeypatch.setattr(server.OllamaAgent, "chat", fake_chat) app = server.create_app() with TestClient(app) as client: before = client.get("/api/health").json() assert before["model_provider"] == "ollama" assert before["inference"]["provider"] == "ollama" updated = client.post( "/api/config", json={"values": {"model_provider": "codex", "codex_model": "gpt-5.4"}}, ).json() assert updated["restart_required"] is False after = client.get("/api/health").json() assert after["model_provider"] == "codex" assert after["inference"]["provider"] == "codex" chat = client.post("/api/chat", json={"message": "hi", "thread_id": "thread-1", "images": []}).json() assert chat["message"] == "codex:gpt-5.4" def make_settings(tmp_path, model_provider="ollama", ollama_model="qwen3.5:9b", codex_model="gpt-5.4"): return SimpleNamespace( traderai_memory_path=str(tmp_path / "memory.sqlite3"), model_provider=model_provider, ollama_base_url="http://localhost:11434", ollama_model=ollama_model, ollama_num_ctx=64512, openai_base_url="https://api.openai.com/v1", openai_api_key=None, openai_model="gpt-5.4-mini", model_reasoning_effort="medium", codex_command="codex", codex_model=codex_model, uex_base_url="https://api.uexcorp.space/2.0", uex_secret_key=None, uex_bearer_token=None, traderai_user_name=None, uex_notification_poll_seconds=60, require_write_approval=True, scmdb_base_url="https://scmdb.net", cornerstone_base_url="https://finder.cstone.space", scwiki_base_url="https://starcitizen.tools", scwiki_api_base_url="https://api.star-citizen.wiki", )