"""Regression tests for session-scoped model/provider overrides in gateway agents. These cover the bug where `/model ...` stored a session override, but fresh agent constructions still resolved model/provider from global config/runtime. That let helper agents (and cache-miss main agents) route GPT-5.4 to the wrong provider, e.g. Nous instead of OpenAI Codex. """ import asyncio import sys import threading import types from unittest.mock import AsyncMock, MagicMock import pytest import gateway.run as gateway_run from gateway.config import Platform from gateway.session import SessionSource class _CapturingAgent: """Fake agent that records init kwargs for assertions.""" last_init = None def __init__(self, *args, **kwargs): type(self).last_init = dict(kwargs) self.tools = [] def run_conversation(self, user_message: str, conversation_history=None, task_id=None): return { "final_response": "ok", "messages": [], "api_calls": 1, } def _make_runner(): runner = object.__new__(gateway_run.GatewayRunner) runner.adapters = {} runner.session_store = None runner.config = None runner._voice_mode = {} runner._ephemeral_system_prompt = "" runner._prefill_messages = [] runner._reasoning_config = None runner._show_reasoning = False runner._provider_routing = {} runner._fallback_model = None runner._service_tier = None runner._running_agents = {} runner._running_agents_ts = {} runner._background_tasks = set() runner._session_db = None runner._session_model_overrides = {} runner._pending_model_notes = {} runner._pending_approvals = {} runner._agent_cache = {} runner._agent_cache_lock = threading.Lock() runner._get_or_create_gateway_honcho = lambda session_key: (None, None) runner.hooks = MagicMock() runner.hooks.emit = AsyncMock() runner.hooks.loaded_hooks = [] return runner def _codex_override(): return { "model": "gpt-5.4", "provider": "openai-codex", "api_key": "***", "base_url": "https://chatgpt.com/backend-api/codex", "api_mode": "codex_responses", } def _explode_runtime_resolution(): raise AssertionError( "global runtime resolution should not run when a complete session override exists" ) def test_run_agent_prefers_session_override_over_global_runtime(monkeypatch): monkeypatch.setattr(gateway_run, "_load_gateway_config", lambda: {}) monkeypatch.setattr(gateway_run, "load_dotenv", lambda *args, **kwargs: None) monkeypatch.setattr(gateway_run, "_resolve_runtime_agent_kwargs", _explode_runtime_resolution) fake_run_agent = types.ModuleType("run_agent") fake_run_agent.AIAgent = _CapturingAgent monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent) _CapturingAgent.last_init = None runner = _make_runner() source = SessionSource( platform=Platform.LOCAL, chat_id="cli", chat_name="CLI", chat_type="dm", user_id="user-1", ) session_key = "agent:main:local:dm" runner._session_model_overrides[session_key] = _codex_override() result = asyncio.run( runner._run_agent( message="ping", context_prompt="", history=[], source=source, session_id="session-1", session_key=session_key, ) ) assert result["final_response"] == "ok" assert _CapturingAgent.last_init is not None assert _CapturingAgent.last_init["model"] == "gpt-5.4" assert _CapturingAgent.last_init["provider"] == "openai-codex" assert _CapturingAgent.last_init["api_mode"] == "codex_responses" assert _CapturingAgent.last_init["base_url"] == "https://chatgpt.com/backend-api/codex" assert _CapturingAgent.last_init["api_key"] == "***" @pytest.mark.asyncio async def test_background_task_prefers_session_override_over_global_runtime(monkeypatch): monkeypatch.setattr(gateway_run, "_load_gateway_config", lambda: {}) monkeypatch.setattr(gateway_run, "_resolve_runtime_agent_kwargs", _explode_runtime_resolution) fake_run_agent = types.ModuleType("run_agent") fake_run_agent.AIAgent = _CapturingAgent monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent) _CapturingAgent.last_init = None runner = _make_runner() adapter = AsyncMock() adapter.send = AsyncMock() adapter.extract_media = MagicMock(return_value=([], "ok")) adapter.extract_images = MagicMock(return_value=([], "ok")) runner.adapters[Platform.TELEGRAM] = adapter source = SessionSource( platform=Platform.TELEGRAM, user_id="12345", chat_id="67890", user_name="testuser", ) session_key = runner._session_key_for_source(source) runner._session_model_overrides[session_key] = _codex_override() await runner._run_background_task("say hello", source, "bg_test") assert _CapturingAgent.last_init is not None assert _CapturingAgent.last_init["model"] == "gpt-5.4" assert _CapturingAgent.last_init["provider"] == "openai-codex" assert _CapturingAgent.last_init["api_mode"] == "codex_responses" assert _CapturingAgent.last_init["base_url"] == "https://chatgpt.com/backend-api/codex" assert _CapturingAgent.last_init["api_key"] == "***"